An overview's borrowed sources were a snapshot taken at creation: a
sub-article that left kept feeding it, a merge moved the borrowed notes
into the target, and losing the borrowed notes deleted the overview and
retired its natural slug while its sub-articles lived on.
Overviews now carry an overview mark (optional plan field, format
unchanged) and only their own sources; what they are written from is
derived every run from their current sub-articles (in turn, at most 12),
for staleness, fingerprints and the write. Note removal never deletes an
overview; one left without sub-articles is deleted when it has no own
sources (slug stays free) or becomes an ordinary article.
An overview proposal under a redirected or retired slug is dropped
instead of parenting the children under the redirect target. An overview
takes the parent most of its children have, none on a tie. The
consolidate prompt carries the no-concrete-names rule for titles and
slugs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Consolidation could only link existing articles as parents, so subjects
whose natural parent had no article (heating, lighting, floors) stayed
flat. The consolidate reply may now propose new overview articles over
two or more group members. An overview is written from its children's
first sources (in turn, at most 12) and an article with sub-articles is
never folded for being thin. The prompts name floors EG/OG/DG/KG and
treat UG, KG and Keller as one floor.
MaxFoldsPerRun 5 -> 100 and MaxConsolidationsPerRun 60 -> 150: a
bootstrap plan of ~400 articles needs several passes and the calls are
small. Prompts.Version 9 re-asks remembered consolidation groups; the
plan format is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
JsonObject builds its dictionary on first access and throws ArgumentException
on a repeated key, which escaped reply validation and failed the whole run
(assign reply with titleEn twice). Parsing now rejects duplicates up front, so
the reply counts as unusable like any other malformed one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Consolidation runs passes until one changes nothing; a changing reply ends
the pass and the next starts from the first seed, so the last pass's groups
are the ones the next run forms. Memory is pruned to that pass. A stable
neighbourhood after a merge is no longer asked again the next night.
Assign treats a redirected slug, proposed as new or referenced directly,
as its live target instead of dropping the note.
A plan reset deletes article files whose slug the new plan lacks.
The sub-articles heading has no id, so it cannot collide with a body
heading.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Article vectors (titleDe | titleEn | scope, embedded locally, cosine in
memory) feed the assign candidates alongside the note-neighbour articles
and form the groups of a new consolidate step after assign: one bounded
call per group of up to 8 nearby articles, at most 60 per run, returning
merges (fold mechanics, redirects incl. public ones) and parent links
(no cycles anywhere in the plan). Groups are remembered by fingerprint
in _state.json.
PlannedArticle gains a parent; split children get the overview. The write
prompt marks main and sub-articles among its link targets (children
capped at 20); the article page shows the main article and the visible
sub-articles from the plan. Parent changes do not make articles stale.
The assign prompt also sends room notes to the room article; the write
prompt writes room articles from the room's angle.
The plan file gets version 3: the v2 plan reads as no plan, its state is
dropped, and the next run bootstraps. Prompts.Version 8.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ArticleQueries serves anonymous readers only the redirects listed in
publicRedirects, so a retired slug whose article was not public (or a chain
through one) answers the usual 404, identical to a missing slug. Signed-in
readers follow every redirect. Body links of the anonymous view resolve
through the same filtered set.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A text-model HttpRequestException skips its assign batch (not remembered),
split or fold article; only embedding/index failures end planning.
- Neighbour timeouts (TaskCanceledException, run not cancelled) count as
service down.
- Thin articles with a nearby article and no keep at their inputs are not
written until their fold decision; split children of the run are not
folded.
- NearestAsync widens its query up to 8x the batch size, then fills from
the open notes in slug order.
- Deleted articles' slugs (and redirects to them) stay reserved in
_plan.json "retired"; a note left without articles is unassigned, not
skipped.
- Fold records whether the folded article was public in both languages
("publicRedirects"); repointed redirects keep their flag.
- Assigned counts only notes assigned to an article.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The service no longer schedules work itself. POST /api/v1/encyclopedia/run
syncs and runs the encyclopedia; it answers 502 when the run fails or the
text model stopped it, so a CronJob shows a failed run as a failed Job.
The sync loop keeps its startup sync and the webhook trigger; the 30-min
interval is a CronJob calling POST /api/v1/sync.
Removed: EncyclopediaBackgroundService, Encyclopedia RunAt/TimeZone,
Wiki:Sync IntervalSeconds/RetrySeconds.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A scanner hit in the slug or titles, or the German leak check flagging them, makes the article private in both languages. The plan prompt forbids concrete identifiers in titles and slugs and keeps existing titles; prompt version 5.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#14 put the read API behind a session or an API key. External agents
reach the wiki through wiki-index.renner.uno with BasicAuth only, and
since #14 they got 401: a regression.
The service now checks `Authorization: Basic` itself against the same
htpasswd lines Traefik's wiki-api-basic-auth middleware uses
(Auth:Basic:UsersPath: a file, or a mounted Secret directory where every
visible key is read, as Traefik reads every key of an Opaque Secret).
This is a credential check in the service, not trust in a proxy: the
header proves nothing unless the password matches the hash.
- Hash formats Traefik accepts: bcrypt ($2a$/$2b$/$2y$, BCrypt.Net-Next
4.2.1), Apache MD5 ($apr1$, implemented here and checked against the
Traefik docs example and `openssl passwd -apr1`), SHA1 ({SHA}). Any
other format authenticates nobody.
- The identity is a machine named after the BasicAuth user.
- Policy "read" accepts it; "write" stays API-key only, and the UI stays
OIDC only.
- A verified (user, password, hash) triple is cached in memory, so bcrypt
costs once per process.
- With no UsersPath configured the scheme authenticates nobody, so this
image is safe before the deployment mounts the Secret.
Tests: all three hash formats read the API; a wrong password, an unknown
user, an empty password and a user found only in a hidden Secret entry
are 401; BasicAuth cannot write and does not open the UI.
Auth:Oidc:PublicOrigin was a hand copy of the Ingress host. Traefik routes on
the Host header and Authentik matches redirect URIs strictly, so the URI is
https://<request host>/signin-oidc; https because TLS ends at Traefik and
browsers only reach the wiki over https.