docir — design documents Graph

Documents / Architecture / arch-0a3c2d6d54a6

Keep the corpus trustworthy (maintenance, CI, staleness)

How the corpus stays consistent: reindex, check, repair, and the merge guard.

arch-0a3c2d6d54a6architectureactive#integrity#persistence
View as Markdown◉ View in graph

Backbone

list the decisions a branch touches → merge branches → rebuild index → check structure → review stale docs → re-verify → (repair?)

Event timeline

# Event Actor Trigger Evidence
0 GoverningDecisionsListed ACT-003 docir query --code <changed files> on a pull request .github/workflows/ci.yml:83-97
1 BranchesMerged ACT-006 git merge tests/modules/documents/test_merge_safety.py
2 IndexRebuilt ACT-002 docir reindex [--changed] maintenance_service.py:58-69
3 StructureChecked ACT-003 docir check [--strict] maintenance_service.py:84-100
4 DuplicateIdDetected system file scan, not index maintenance_service.py:109-124
5 StaleFlagged system past review_days since verified/updated graph_checks.py:84-111
6 DocumentReVerified ACT-007 docir update <id> --verified document_service.py:341-342
7 AdvisoryLinted ACT-002 docir lint --deep maintenance_service.py:126-134
8 EmbeddingsRebuilt ACT-002 docir embed --flush, or any full docir reindex maintenance_service.py:71-82
9 UnmatchedCodeFlagged system a governed code: glob matches nothing on disk graph_checks.py:124-166, maintenance_service.py:159-169

Why event 0 is numbered from zero

Event 0 is numbered from zero because it happens before the flow this document was written around: it is the only step that runs while the change is still a proposal, and it is a notice, not a gate (adr-b2cfed9d5888). Everything below it runs on a corpus that has already been changed; event 0 runs on the change itself, and the only thing it can do is tell a reviewer what to read.

Event 9 — the code half of the same linkage

Event 9 is the code half of the same linkage. It reports a document whose governed code moved or was deleted — including a decision bound to the test that enforced it, which is how "the rule is a test" notices that the rule is gone. A warning, like staleness: the corpus is intact and a pattern is out of date.

Hotspots

Eight hotspots were confirmed by the 2026-07-26 discovery pass against v0.2.1 (ref-9e4cce368b80). All eight are closed. They are kept here as the record of what the corpus looked like before the maintenance surface existed — read each one's Closed line for the behaviour that ships today, and the linked issue for the argument.

H1 — reindex did not restore the id counter

The index is documented as fully rebuildable from files (thesis #1), and id_sequences is part of the index. Rebuilding it lost the counter, so the next add re-minted a live id. CONFIRMED end to end: after clone→reindex→add, two files claimed adr-0001 and the older document became invisible to get, query, search and context while its file remained on disk. This was the single most damaging finding in the run: it fired on the documented happy path (git clone + docir reindex), needed no concurrency, no --force, and no unusual input.

Closedreindex now raises each prefix to max(numeric suffix on disk) + 1 (_restore_id_sequences, monotonic), backed by two further guards: IdGenerator skips a candidate already indexed, and a create refuses to write when a file already claims the id (DuplicateDocumentIdError). → issue-b7ddde3ce860.

H2 — check --strict could not serve as the CI gate it was sold as

It exited 1 if any finding existed, and orphan fires for every document with no relations — the default state of a newly created document. CONFIRMED: a store with two brand-new unrelated documents exited 1. A team adopting the documented CI gate got a red build on day one and had to either link every document or drop the gate — which also dropped the duplicate-id detection that is the gate's actual purpose.

Closed — findings carry a severity derived from their kind. ERROR_KINDS is duplicate-id/dangling/malformed (the corpus is broken); everything else is a warning about shape or age. --strict gates on errors only; --strict-all restores fail-on-anything for anyone who wants it. → issue-9cb85759076d.

H3 — the default profile made the canonical modelling a permanent warning

In the software profile decision is level 3 and issue is level 1, and the layering check flagged any non-supersedes/contradicts edge from a higher to a lower level. So docir add --type decision … --related issue-0001 — the exact pairing in the README's own quickstart — produced a permanent layering finding. CONFIRMED.

Closed — layering now reads only edges the schema marks dependency (_find_layering_violations consults is_dependency_relation), so an ordinary relates_to link from a decision to the issue that motivated it is silent. → issue-40d1792bc9f9.

H4 — check detected, nothing repaired

duplicate-id, dangling, malformed and unknown-type were all reported and none could be fixed by any command. Every confirmed failure mode in this analysis terminated in a state the product could not exit.

Closeddocir check --fix (MaintenanceService.repair) is the sanctioned recovery path. It repairs exactly what needs no guess: duplicate ids are re-issued (the oldest file keeps the id, so existing edges stay valid) and dangling edges are dropped. It reindexes first and does not advance updated. malformed and unknown-type are still left unrepaired deliberately and come back in RepairResult.remaining — each needs somebody to read something and decide what the file or the schema should say, and a repair has nothing to read with. run-22e0a6ce6ae1 and run-f4a756206fe0 are the runbooks. → issue-476b4e188fab.

H5 — staleness had no route to a human

owner was captured, interpolated into a check message, and never used again: no --owner query filter, no "documents I own" view.

Closedquery --owner X --stale is the review queue and update <id> --verified clears an entry. --stale is applied before --limit, so --stale --limit 10 means ten overdue documents. Delivery stays pull, not push: there is deliberately no notifier or scheduler, because an automated nag a bot can clear is not somebody vouching for content. → issue-b4f441c7210f.

H6 — an administrative rename reset the trust clock

tag rename and tag rm --force rewrote every referencing document with updated = today. For any document without an explicit verified date, staleness falls back to updated — so renaming a tag made stale documents look freshly reviewed.

ClosedTagService has no Clock: it was injected only to stamp the date it must not stamp (tag_service.py). The tag paths rewrite the classification and leave updated alone, alongside check --fix and delete --force. Only a content edit moves updated. → issue-9ed4905e0db8.

H7 — reindex --changed never removed deleted documents

The removal sweep was skipped when changed_only, so a document deleted from the filesystem stayed in the index and kept being returned by every read path until a full reindex.

Closed — the sweep runs in both modes (_reindex_documents); --changed now only skips re-saving files whose content is unchanged. Both modes leave the index agreeing with the filesystem. → issue-c33edcf431fa.

H8 — malformed files were skipped silently by reindex

scan() swallowed ValidationError and continued, and reindex reported documents_indexed with no count of files skipped, so a broken file looked like a successful rebuild.

ClosedReindexResult.documents_skipped counts source files that will not parse and is printed by the CLI; a non-zero value means run check, which names each file. → issue-5f979576ef7d.

Off-system steps

  • Resolving every finding check can produce. All manual, all undocumented. This is the work of ACT-008 (support / operator), the actor the product never serves.
  • Deciding whether a stale document is still true. By design (adr-bd7c4f3c5764 — human re-verification is the honest baseline). Correctly out of system; recorded for completeness.

Rules

BR-041, BR-042, BR-043, BR-044, BR-045, BR-046, BR-047

Gaps

issue-b7ddde3ce860, issue-9cb85759076d, issue-40d1792bc9f9, issue-b4f441c7210f, issue-476b4e188fab, issue-9ed4905e0db8, issue-c33edcf431fa, issue-5f979576ef7d

check reports what is ready, not only what is wrong

Since 0.18.0 check also reports unblocked: a live document whose every depends_on target has closed. The one finding here that is good news — the work is ready to start.

It exists because the edge was asserted and nothing read it. A depends_on claims this work waits on that work, and until now only context expansion followed it, and only when a caller happened to query nearby — so a blocker could clear and the thing it blocked would sit there with the graph holding the answer and nobody asking.

A warning, never an error, on the same argument as stale: nothing is broken, this is a scheduling fact, and it is cleared by doing something real rather than by a flag — start the work, or drop an edge that is no longer true.

Which kinds count is the schema's blocking property, not the name depends_on. That property is deliberately separate from dependency, which layering reads: one is temporal and the other structural, and reading one for both announced a decision refining a superseded one as ready to start (adr-716c2eeb4e51).

To amend: Re-verify: