# Business rule register

47 rules (BR-001–BR-074, sparsely numbered), reconstructed from code and
tests. Every one is `assumed`: no subject-matter expert confirmed them, so they record
what the system does, not what anybody promised. `pattern` names the rule shape
(unwanted, required, state-driven, …).

## BR-001

**Statement.** When a document is created, the system shall reject the write unless every supplied tag key is already present in the tag registry.

**Pattern:** unwanted · **Flow:** arch-3e305bc76ff0 · **Actor:** AI coding agent · **Confidence:** observed · **Status:** assumed · **Owner:** repo maintainer

- *Given* registry contains {auth} · *when* add --tags auth,security · *then* rejected, UnknownTagError, exit 2; nothing written

**Boundaries:** empty tag list (allowed), tag registered in the same transaction

**Evidence:**
- `src/docir/modules/documents/domain/services/validation.py:51-59`
- `src/docir/modules/documents/application/services/document_service.py:98`

## BR-002

**Statement.** When a document is created, the system shall reject the write unless every `related` target id already exists in the index.

**Pattern:** unwanted · **Flow:** arch-3e305bc76ff0 · **Actor:** AI coding agent · **Confidence:** observed · **Status:** assumed · **Owner:** repo maintainer

**Notes:** Consequence: forward references are impossible. Two documents that reference each other can only be created by adding one, adding the second, then updating the first.

**Evidence:**
- `src/docir/modules/documents/domain/services/validation.py:61-66`

## BR-003

**Statement.** Where the schema registers relation kinds, when an edge is written, the system shall reject any edge whose kind is not registered, and any edge whose kind or target type is outside the source type's `allowed_relations` whitelist.

**Pattern:** complex · **Flow:** arch-3e305bc76ff0 · **Actor:** AI coding agent · **Confidence:** observed · **Status:** assumed · **Owner:** repo maintainer

- *Given* legal profile; obligation type; allowed_relations.implements = [policy, contract] · *when* add --type obligation --related obl-0002:implements · *then* rejected — an obligation may not `implements` another obligation

**Decision table:** {'conditions': ['relation_types_registry', 'kind_registered', 'allowed_relations_for_type', 'target_type_listed'], 'rows': ['[empty, any, any, any] -> allow', '[non-empty, false, any, any] -> reject:UnknownRelationKindError', '[non-empty, true, empty, any] -> allow', '[non-empty, true, kind absent, any] -> reject:DisallowedRelationError', '[non-empty, true, kind present, empty list] -> allow', '[non-empty, true, kind present, listed] -> allow', '[non-empty, true, kind present, not listed] -> reject:DisallowedRelationError', '[non-empty, true, kind present, target id unknown to index] -> allow']}

**Evidence:**
- `src/docir/modules/documents/domain/services/validation.py:68-93`
- `src/docir/modules/documents/domain/schema.py:72-84`

## BR-004

**Statement.** When no status is given at creation, the system shall assign the type's `default_status`.

**Pattern:** event · **Flow:** arch-3e305bc76ff0 · **Actor:** — · **Confidence:** observed · **Status:** assumed · **Owner:** repo maintainer

**Evidence:**
- `src/docir/modules/documents/application/services/document_service.py:92`
- `src/docir/modules/documents/infra/profiles.py:29`

## BR-005

**Statement.** While a document is at status S, when a change to status T is requested, the system shall permit it only if T is reachable from S in the type's transition map, unless the caller passes `--override`, in which case only T's membership in the status enum is checked.

**Pattern:** complex · **Flow:** arch-3e305bc76ff0 · **Actor:** — · **Confidence:** observed · **Status:** assumed · **Owner:** repo maintainer

- *Given* decision at 'rejected' (a terminal status) · *when* update --status accepted --override · *then* allowed; the document is now 'accepted' with no record that a rule was bypassed

**Decision table:** {'conditions': ['target_in_enum', 'transition_declared', 'override_flag'], 'rows': ['[false, any, false] -> reject:InvalidStatusError', '[false, any, true] -> reject:InvalidStatusError', '[true, true, any] -> allow', '[true, false, false] -> reject:InvalidStatusTransitionError', '[true, false, true] -> allow, UNAUDITED', '[S == T, any, any] -> allow (self-loop always permitted)']}

**Open questions:** issue-99afeec3a7ce

**Evidence:**
- `src/docir/modules/documents/application/services/document_service.py:324-329`
- `src/docir/modules/documents/domain/schema.py:66-70`

## BR-006

**Statement.** The system shall allocate every document id itself and shall never accept a caller-supplied id: a `sequential` type draws from a per-prefix counter in the index, a `random` type mints a hex token and retries on collision.

**Pattern:** complex · **Flow:** arch-3e305bc76ff0 · **Actor:** — · **Confidence:** observed · **Status:** confirmed · **Owner:** repo maintainer

**Notes:** Restated 2026-07-26. The original wording ("from a per-prefix counter") described only the sequential path and read as though the counter were the sole mechanism — which is what made BR-007's uniqueness claim look safe when it was not.

**Evidence:**
- `src/docir/modules/documents/application/services/id_generator.py:26-48`
- `src/docir/platform/persistence/repositories.py:48-66`
- `src/docir/modules/documents/domain/value_objects/identifiers.py:40-48`

## BR-007

**Statement.** The system shall issue a unique id to each document.

**Pattern:** ubiquitous · **Flow:** arch-3e305bc76ff0 · **Actor:** — · **Confidence:** observed · **Status:** confirmed · **Owner:** repo maintainer

- *Given* a store with adr-0001, adr-0002 on disk and no index (fresh clone) · *when* docir reindex && docir add --type decision --title 'Third' · *then* OBSERVED: the new document is issued adr-0001; adr-0001 'First' becomes invisible to get/query/search/context while its file remains on disk

**Notes:** RESOLVED 2026-07-26 — held now, was not when first written. Both violations are fixed (issue-b7ddde3ce860, issue-389dc5dac58a) and `docir init` defaults to `id_style: random`, which removes the counter from the picture entirely for new stores. Original finding, kept for the record: CLAUDE.md said "Ids are allocated from the DB counter (SequenceRow), never by scanning files — that is what keeps parallel agents from minting the same id." Two confirmed violations: (a) --no-daemon concurrency: 6 simultaneous adds all returned adr-0002 (issue-389dc5dac58a); (b) reindex loses the counter, so the next add re-mints a live id (issue-b7ddde3ce860). Uniqueness is actually provided by the daemon's single-connection serialization plus a counter that survives, not by the counter itself.

**Open questions:** issue-88dd653b9f39, issue-96b03701503b

**Evidence:**
- `CLAUDE.md`
- `src/docir/modules/documents/application/services/id_generator.py:3-5`

## BR-008

**Statement.** When a document is written, the system shall persist the markdown file and update the metadata, full-text and relation projections such that either all of them reflect the write or none do.

**Pattern:** event · **Flow:** arch-3e305bc76ff0 · **Actor:** — · **Confidence:** observed · **Status:** assumed · **Owner:** repo maintainer

**Notes:** Holds for the three index projections (one UoW). Does NOT hold across the file/DB boundary: the file is written before the commit, so a crash in between leaves an unindexed file. → issue-61b66ed696de.

**Evidence:**
- `src/docir/modules/documents/application/services/document_service.py:118-123`
- `docs/adr/adr-d3e3616400bf-shared-derived-index.md`

## BR-009

**Statement.** When a document's title, description or body changes, the system shall mark its embedding stale and recompute it off the critical path; the command shall return before the recompute completes unless `--wait-embeddings` is given.

**Pattern:** event · **Flow:** arch-3e305bc76ff0 · **Actor:** — · **Confidence:** observed · **Status:** assumed · **Owner:** repo maintainer

**Boundaries:** metadata-only change → embedding NOT marked dirty (correct: tags/status are not in embedding_text)

**Evidence:**
- `src/docir/modules/documents/application/services/document_service.py:153-158`
- `src/docir/modules/indexing/infra/scheduler.py:105-115`

## BR-010

**Statement.** If a delete is requested for a document other documents reference, then the system shall refuse it and name the referencing documents, unless `--force` is given.

**Pattern:** unwanted · **Flow:** arch-3e305bc76ff0 · **Actor:** — · **Confidence:** observed · **Status:** assumed · **Owner:** repo maintainer

- *Given* adr-0001 related: [issue-0001] · *when* docir delete issue-0001 --force · *then* OBSERVED: issue-0001 gone; adr-0001's file still reads `related: [issue-0001]`; `docir check` reports dangling; `docir update adr-0001 --set-title X` succeeds and rewrites the broken edge back to disk

**Notes:** With --force the referencing documents' files keep the now-broken id. No compensating action exists, and no later write repairs it. → issue-fd547a293d01.

**Open questions:** issue-0a4ad65b8a70

**Evidence:**
- `src/docir/modules/documents/application/services/document_service.py:194-200`

## BR-011

**Statement.** If `--replace-body` is requested without `--force`, or when the file changed on disk since it was indexed, then the system shall refuse the write.

**Pattern:** unwanted · **Flow:** arch-3e305bc76ff0 · **Actor:** — · **Confidence:** observed · **Status:** assumed · **Owner:** repo maintainer

**Notes:** The stale-write guard protects ONLY `--replace-body`. `--append-section` and `--replace-section` apply to the on-disk version, so they are safe by construction; a metadata-only patch silently absorbs an out-of-band body edit into the index.

**Evidence:**
- `src/docir/modules/documents/application/services/document_service.py:369-380`

## BR-012

**Statement.** If more than one body-edit mode is supplied in one call, then the system shall reject the request.

**Pattern:** unwanted · **Flow:** arch-3e305bc76ff0 · **Actor:** — · **Confidence:** observed · **Status:** assumed · **Owner:** repo maintainer

**Evidence:**
- `src/docir/modules/documents/application/services/document_service.py:353-359`

## BR-013

**Statement.** When an update would change nothing, the system shall return the current document unchanged and shall not advance `updated`.

**Pattern:** event · **Flow:** arch-3e305bc76ff0 · **Actor:** — · **Confidence:** observed · **Status:** assumed · **Owner:** repo maintainer

**Evidence:**
- `src/docir/modules/documents/application/services/document_service.py:142-143`

## BR-014

**Statement.** When a document is archived, the system shall remove it from full-text and semantic retrieval while retaining its file, its metadata row and its relation edges.

**Pattern:** event · **Flow:** arch-3e305bc76ff0 · **Actor:** — · **Confidence:** observed · **Status:** assumed · **Owner:** repo maintainer

**Notes:** Unlike delete, archive does not consider incoming references at all.

**Evidence:**
- `src/docir/modules/documents/application/services/document_service.py:161-173`

## BR-015

**Statement.** The system shall fix a document's file path at creation from its id and title slug, and reuse that path for every later write.

**Pattern:** ubiquitous · **Flow:** arch-3e305bc76ff0 · **Actor:** — · **Confidence:** observed · **Status:** assumed · **Owner:** repo maintainer

**Notes:** Consequence: renaming a document's title leaves the old slug in the filename forever. Deliberate (avoids orphaning renamed files) and undocumented for users. Also the mechanism by which the issue-b7ddde3ce860 collision produces two files rather than an overwrite.

**Evidence:**
- `src/docir/platform/filesystem/markdown_store.py:34-39`
- `87-89`

## BR-016

**Statement.** The system shall record an edge as at most one kind per ordered (source, target) pair; if a source lists the same target twice, the last kind shall win.

**Pattern:** ubiquitous · **Flow:** arch-3e305bc76ff0 · **Actor:** — · **Confidence:** observed · **Status:** assumed · **Owner:** repo maintainer

**Notes:** Silent last-wins deduplication; no warning that an edge was discarded.

**Evidence:**
- `src/docir/platform/persistence/repositories.py:81-85`
- `src/docir/platform/persistence/alembic/versions/0002_typed_edges_and_staleness.py:32-35`

## BR-017

**Statement.** When an edge of the default kind (`relates_to`) is written to disk, the system shall render it as a bare id, and any other kind as a `{to, kind}` mapping.

**Pattern:** event · **Flow:** arch-3e305bc76ff0 · **Actor:** — · **Confidence:** observed · **Status:** assumed · **Owner:** repo maintainer

**Evidence:**
- `src/docir/platform/filesystem/markdown_store.py:156-168`
- `docs/adr/adr-599055502f0e-typed-relation-edges.md`

## BR-018

**Statement.** If a read limit of zero or less is requested, then the system shall reject the request.

**Pattern:** unwanted · **Flow:** arch-f220a644d654 · **Actor:** — · **Confidence:** observed · **Status:** assumed · **Owner:** repo maintainer

**Boundaries:** limit = 0 rejected, limit = 1 allowed, limit = -1 rejected, limit above corpus size: allowed, returns everything

**Evidence:**
- `src/docir/modules/documents/application/services/document_service.py:55-63`

## BR-025

**Statement.** When ranked context is requested, the system shall combine a full-text ranking with a semantic ranking by reciprocal rank fusion and order results by the fused score.

**Pattern:** event · **Flow:** arch-f220a644d654 · **Actor:** AI coding agent · **Confidence:** observed · **Status:** assumed · **Owner:** repo maintainer

**Notes:** RRF is rank-based, so the emitted `score` carries no absolute meaning and is not comparable across queries. It is published in the README's agent-facing example (README:90) with no interpretation given. → issue-93152f7b9213.

**Evidence:**
- `src/docir/modules/indexing/domain/scoring.py:44-73`
- `src/docir/modules/documents/application/services/document_service.py:256-258`

## BR-026

**Statement.** The system shall consider at most 25 full-text candidates when fusing rankings.

**Pattern:** ubiquitous · **Flow:** arch-f220a644d654 · **Actor:** — · **Confidence:** observed · **Status:** assumed · **Owner:** repo maintainer

**Notes:** Unexplained magic constant, not configurable, not derived from `limit`. A document ranked 26th lexically can only enter via the semantic side.

**Evidence:**
- `src/docir/modules/documents/application/services/document_service.py:47`

## BR-027

**Statement.** The list read paths (`query`, `search`, `context`) shall return frontmatter, typed edges and a staleness flag, and shall never return a document body.

**Pattern:** ubiquitous · **Flow:** arch-f220a644d654 · **Actor:** AI coding agent · **Confidence:** observed · **Status:** assumed · **Owner:** repo maintainer

**Evidence:**
- `src/docir/modules/documents/application/dto.py:84-108`
- `README.md:103-106`

## BR-028

**Statement.** While a document is archived, the system shall exclude it from `query`, `search` and `context` unless `--include-archived` is given; `get` shall return it regardless.

**Pattern:** state · **Flow:** arch-f220a644d654 · **Actor:** — · **Confidence:** observed · **Status:** assumed · **Owner:** repo maintainer

**Notes:** `search` has no --include-archived flag at all; it relies on archived docs being absent from the FTS table. Reaching the same outcome by a different mechanism.

**Evidence:**
- `src/docir/platform/persistence/repositories.py:115-116`
- `src/docir/modules/documents/application/services/document_service.py:210-214`

## BR-029

**Statement.** While a document's status is one of its type's `inactive_statuses`, the system shall exclude it from *every* read path — `query`, `search`, and both the ranked and graph-expanded halves of `context` — unless `--include-inactive` is given.

**Pattern:** state · **Flow:** arch-f220a644d654 · **Actor:** — · **Confidence:** observed · **Status:** confirmed · **Owner:** repo maintainer

- *Given* a resolved issue reachable only through its decision's edge · *when* `docir context auth --limit 5` · *then* OBSERVED: only the decision is returned; the resolved issue is not.

**Notes:** Was `disputed`: `context`'s graph-expansion step checked `archived` but not inactive status, so a resolved issue came back through a neighbour edge while `search` and `query` correctly hid it. Fixed — `DocumentService._is_visible` is the single visibility predicate, called by the ranked loop *and* by `_augment_with_related`, so the four paths cannot diverge again. The flag was also renamed: `--include-resolved` is deprecated in favour of `--include-inactive`, which covers every inactive status rather than the one named `resolved`. Re-verified 2026-07-30 by replaying the example above against the current CLI.

**Open questions:** issue-9152d83d9f78 (answered)

**Evidence:**
- `src/docir/modules/documents/application/services/document_service.py` (`_is_visible`)

## BR-030

**Statement.** When ranked context is returned, the system shall additionally include every document reachable in one hop along the selected documents' outgoing edges.

**Pattern:** event · **Flow:** arch-f220a644d654 · **Actor:** — · **Confidence:** observed · **Status:** assumed · **Owner:** repo maintainer

- *Given* 3 decisions each with 2 outgoing edges, all ranking above the cut · *when* docir context 'cache invalidation policy' --limit 3 · *then* OBSERVED: 9 documents returned

**Notes:** Applied AFTER the limit is enforced and itself uncapped, so `--limit N` does not bound the response. OBSERVED: --limit 3 returned 9. Incoming edges are never followed, so "what superseded this?" is not answerable from the superseded document. → issue-996b567e5131, issue-5bfbc6f2699d.

**Open questions:** issue-8bcb6b7f8308

**Evidence:**
- `src/docir/modules/documents/application/services/document_service.py:297-307`

## BR-031

**Statement.** `docir context` ranks every document with an active vector, so the fused list is
never empty on a non-empty corpus. "Nothing relevant exists" is expressible only through
`--min-score`, which filters on `similarity` — the raw cosine — and not on `score`.

**Pattern:** ubiquitous · **Flow:** arch-f220a644d654 · **Actor:** — · **Confidence:** observed · **Status:** confirmed · **Owner:** repo maintainer

- *Given* a store containing only 'Postgres connection pooling' · *when* docir context 'how do I bake sourdough bread' --limit 3 · *then* OBSERVED (2026-07-30, v0.2.1): returns the Postgres decision, score 0.0328
- *Given* the same store · *when* the same query with `--min-score 0.5` · *then* the empty result is a real answer: nothing scored close enough

**Notes:** The 2026-07-30 observation recorded this as an emergent rule nobody wrote, with no
similarity floor available. The floor now exists. The distinction it rests on is the
load-bearing part: `score` is RRF, a fusion of *ranks*, so a nonsense query against a
one-document store scored the same ~0.0328 a perfect match does — which is exactly why
"nothing relevant" was inexpressible. `FusedScore.similarity` carries the raw cosine through
and `--min-score` filters on that. Two exemptions are deliberate and remain: graph neighbours
are never filtered (they are present because a selected document links them, not because they
scored), and a hit with **no** `similarity` is kept — absent means *no current vector*, not
zero, so dropping it would filter on embedding-queue staleness rather than relevance.

**Open questions:** resolved — issue-93dd537bbbbb

**Evidence:**
- `src/docir/modules/indexing/domain/scoring.py` (`FusedScore.similarity`)
- `src/docir/modules/documents/application/services/document_service.py`
- `docir context --min-score`

## BR-032

**Statement.** When free text is searched, the system shall match documents containing ANY of the query terms.

**Pattern:** event · **Flow:** arch-f220a644d654 · **Actor:** — · **Confidence:** observed · **Status:** assumed · **Owner:** repo maintainer

**Notes:** OR-of-terms, not AND, and not a phrase. A three-word query matches a document sharing one common word. Undocumented; `docir search` is presented as plain "full-text search". Also means punctuation-only or stopword-only queries return nothing at all (empty MATCH).

**Boundaries:** empty query → no results, not an error, query of only punctuation → no results

**Evidence:**
- `src/docir/platform/persistence/repositories.py:352-357`

## BR-033

**Statement.** While a document's type declares a review cadence, the system shall report it as stale once more days have elapsed since its last verification (or last edit, if never verified) than the cadence allows.

**Pattern:** state · **Flow:** arch-f220a644d654 · **Actor:** — · **Confidence:** observed · **Status:** assumed · **Owner:** repo maintainer

**Boundaries:** exactly at cadence → NOT stale (strict >), cadence 0 → never stale, archived → never stale, unknown type → never stale

**Notes:** Falling back to `updated` means any edit — including an administrative tag rename — resets the clock. → issue-9ed4905e0db8.

**Evidence:**
- `src/docir/modules/documents/domain/services/graph_checks.py:84-111`
- `src/docir/modules/documents/domain/entities/document.py:73-75`
- `docs/adr/adr-bd7c4f3c5764-staleness-as-data.md`

## BR-034

**Statement.** When output is captured (stdout is not a terminal) or `--json` is given, the system shall emit compact JSON with information-free fields omitted; at a terminal it shall render tables.

**Pattern:** complex · **Flow:** arch-f220a644d654 · **Actor:** AI coding agent · **Confidence:** observed · **Status:** assumed · **Owner:** repo maintainer

**Decision table:** {'conditions': ['pretty_flag', 'json_flag', 'stdout_is_tty', 'no_trim_flag'], 'rows': ['[true, any, any, any] -> rich tables', '[false, true, any, false] -> compact trimmed JSON', '[false, true, any, true] -> compact full JSON', '[false, false, true, any] -> rich tables', '[false, false, false, false] -> compact trimmed JSON', '[false, false, false, true] -> compact full JSON'], 'notes': '--no-trim has no effect on the table path. Precedence is identical for --help, which is resolved from argv because Click renders it before CliState exists (runner.py:51-66).\n'}

**Notes:** An omitted key always means the field's default, never a real zero or false.

**Evidence:**
- `src/docir/entry_points/cli/rendering.py:27-57`
- `README.md:85-95`

## BR-041

**Statement.** When the index is rebuilt, the system shall reconstruct it from the markdown files and the tag registry file, which are canonical — including the id counter, which is derived state and not stored in either.

**Pattern:** event · **Flow:** arch-0a3c2d6d54a6 · **Actor:** — · **Confidence:** observed · **Status:** confirmed · **Owner:** repo maintainer

- *Given* a store with two documents and no index (a fresh clone — the index is gitignored) · *when* `docir reindex` then `docir add` · *then* OBSERVED: the new id does not collide and all three documents stay visible.

**Notes:** Was `disputed` as INCOMPLETE, and it was the mechanism behind issue-b7ddde3ce860: `id_sequences` is part of the index and was not reconstructed, so the next `add` re-issued a live id and the older document fell out of every read path. Fixed — `_restore_id_sequences` raises each prefix to `max(numeric suffix on disk) + 1`, monotonically, with two allocation-time backstops. The two related complaints are fixed too: unparseable files are counted as `documents_skipped` rather than skipped silently, and `--changed` now runs the removal sweep. Re-verified 2026-07-30 by replaying the example above against the current CLI.

**Open questions:** issue-88dd653b9f39 (answered)

**Evidence:**
- `src/docir/modules/documents/application/services/maintenance_service.py` (`_restore_id_sequences`)
- `tests/modules/documents/test_merge_safety.py`

## BR-042

**Statement.** When structural checks run, the system shall report unknown types, dangling references, relation cycles, orphans, layering violations, staleness, duplicate ids and malformed files as non-blocking findings.

**Pattern:** event · **Flow:** arch-0a3c2d6d54a6 · **Actor:** CI job · **Confidence:** observed · **Status:** assumed · **Owner:** repo maintainer

**Evidence:**
- `src/docir/modules/documents/domain/services/graph_checks.py:44-58`
- `src/docir/modules/documents/application/services/maintenance_service.py:84-124`

## BR-043

**Statement.** If `--strict` is given and any finding of **error** severity exists, then the system shall exit 1. `--strict-all` restores the fail-on-any-finding behaviour.

**Pattern:** unwanted · **Flow:** arch-0a3c2d6d54a6 · **Actor:** CI job · **Confidence:** observed · **Status:** confirmed · **Owner:** repo maintainer

- *Given* a corpus whose only findings are warnings · *when* `docir check --strict` · *then* OBSERVED: exit 0.

**Notes:** Was `disputed`, and the statement itself was wrong rather than merely unmet: all finding kinds were equal, so `orphan` — which fires for any document with no relations, the default state of a new one — failed the gate on a healthy corpus. The only way to keep CI green was to abandon the gate, which also abandoned duplicate-id detection, its stated purpose. Fixed by giving findings a severity: `ERROR_KINDS` is `duplicate-id`/`dangling`/`malformed` (the corpus is *broken*); everything else is a warning about shape or age. `CheckIssue` derives severity from kind in `__post_init__`, so a new check classifies itself by being added to `ERROR_KINDS` or not. Re-verified 2026-07-30 by replaying the example above against the current CLI.

**Open questions:** issue-9adf57138ea1 (answered)

**Evidence:**
- `src/docir/modules/documents/domain/services/graph_checks.py` (`ERROR_KINDS`, `severity_for`)

## BR-044

**Statement.** When duplicate ids are looked for, the system shall scan the markdown files directly rather than the index, because the index deduplicates by primary key.

**Pattern:** event · **Flow:** arch-0a3c2d6d54a6 · **Actor:** git / branch merge · **Confidence:** observed · **Status:** assumed · **Owner:** repo maintainer

**Notes:** Correct and load-bearing. Note what it implies: when two files share an id the index silently keeps one and drops the other, so the other document is invisible to every read path while its file still exists. That is the actual damage in issue-b7ddde3ce860 and issue-389dc5dac58a.

**Evidence:**
- `src/docir/modules/documents/application/services/maintenance_service.py:109-124`

## BR-045

**Statement.** While a relation of kind `depends_on` or `refines` points from a higher-level type to a lower-level one, the system shall report a layering violation. No other kind is a dependency claim.

**Pattern:** state · **Flow:** arch-0a3c2d6d54a6 · **Actor:** — · **Confidence:** observed · **Status:** confirmed · **Owner:** repo maintainer

- *Given* the software profile and a decision related to the issue that motivated it · *when* `docir check` · *then* OBSERVED: no findings.

**Notes:** Was `disputed`, and the statement was inverted. It was written as an *exemption* list holding `supersedes`/`contradicts`, which made every other kind a dependency claim — including `relates_to`, the default for a bare id. So the most natural thing a user can model, and the pairing in the README's own example output, was a permanent violation no edit could silence. Fixed by naming the dependency kinds instead: `_DEPENDENCY_KINDS = {depends_on, refines}`. Accepted consequence: a relation kind added by a custom schema is not layering-checked until it is named there — silence on an unknown kind is the right default for a heuristic, noise on a correct one is not. Re-verified 2026-07-30 by replaying the example above against the current CLI.

**Open questions:** issue-f2591bdbca13 (answered)

**Evidence:**
- `src/docir/modules/documents/domain/services/graph_checks.py` (`_DEPENDENCY_KINDS`)

## BR-046

**Statement.** When advisory linting is requested with `--deep`, the system shall report document pairs whose embeddings exceed 0.9 cosine similarity, and documents whose body exceeds 8000 characters.

**Pattern:** event · **Flow:** arch-0a3c2d6d54a6 · **Actor:** — · **Confidence:** observed · **Status:** assumed · **Owner:** repo maintainer

**Notes:** Both thresholds are unexplained constants and neither is configurable. The duplicate scan is O(n²) over all active vectors with no cap. 8000 chars is ~2000 tokens — well under a normal ADR for a complex decision.

**Evidence:**
- `src/docir/modules/documents/domain/services/similarity_lint.py:29-69`

## BR-047

**Statement.** When embeddings are recomputed, the system shall drop any queued entry whose document no longer exists, so a deleted document cannot wedge the queue.

**Pattern:** event · **Flow:** arch-0a3c2d6d54a6 · **Actor:** — · **Confidence:** observed · **Status:** assumed · **Owner:** repo maintainer

**Evidence:**
- `src/docir/modules/indexing/infra/scheduler.py:36-39`

## BR-059

**Statement.** The system shall resolve its store as, in order: an explicit `--home`, then `DOCIR_HOME`, then the nearest `.docir` directory found by walking up from the working directory, then a global `~/.docir`.

**Pattern:** ubiquitous · **Flow:** arch-90c90751344f · **Actor:** — · **Confidence:** observed · **Status:** assumed · **Owner:** repo maintainer

**Notes:** The last fallback is silent. A command run in a repo that was never initialised writes into the user's home store with no indication. → issue-34b4f0ca1e13.

**Boundaries:** nested .docir directories → nearest wins, DOCIR_HOME set to empty string → treated as unset

**Evidence:**
- `src/docir/config/settings.py:76-104`
- `docs/adr/adr-20eec6e2e2ca-per-project-store.md`

## BR-060

**Statement.** Where a schema file names profiles, the system shall merge the frozen core, then each named profile in order, then the file's own inline definitions, with later fragments replacing whole types of the same name.

**Pattern:** optional · **Flow:** arch-90c90751344f · **Actor:** — · **Confidence:** observed · **Status:** assumed · **Owner:** repo maintainer

**Notes:** Replacement is whole-type, not field-level; overriding one attribute of a bundled type means restating all of it. Silent — no warning that a type was replaced.

**Boundaries:** no `profiles:` key → inline-only, core NOT merged, relation kinds unconstrained, `profiles: []` → core only, all five profiles → 15 types, no prefix collision (verified)

**Evidence:**
- `src/docir/modules/documents/infra/schema_loader.py:88-117`
- `docs/adr/adr-2a3f625bb2f8-core-plus-profiles.md`

## BR-061

**Statement.** If a schema declares a duplicate type prefix, a default status outside its own status enum, or an `allowed_relations` kind absent from the relation registry, then the system shall refuse to load it.

**Pattern:** unwanted · **Flow:** arch-90c90751344f · **Actor:** — · **Confidence:** observed · **Status:** assumed · **Owner:** repo maintainer

- *Given* type ticket: statuses {open: [closd], closed: []}, inactive_statuses [done] · *when* docir schema validate · *then* OBSERVED: {"valid":true}. At write time: `invalid transition 'open' -> 'closed'` — which names a status that IS declared, misdirecting the reader away from the typo in the schema. `open` has no reachable exit.

**Notes:** These three are checked. Transition targets and `inactive_statuses` are NOT checked for membership in the status enum, so a type can declare an unreachable exit and still validate. → issue-b47a1203baa2.

**Open questions:** issue-2b28fd8b1dfa

**Evidence:**
- `src/docir/modules/documents/domain/schema.py:97-117`

## BR-062

**Statement.** When a store is initialised, the system shall write a schema file and a `.gitignore` covering the derived index, preserving existing files unless `--force` is given.

**Pattern:** event · **Flow:** arch-90c90751344f · **Actor:** — · **Confidence:** observed · **Status:** assumed · **Owner:** repo maintainer

**Notes:** `--force` overwrites the schema and the .gitignore together, with no separate control, no confirmation and no backup. → issue-fde9a7151bd1.

**Evidence:**
- `src/docir/entry_points/composition.py:182-192`

## BR-063

**Statement.** When agent instructions are installed, the system shall write only files it owns, and shall modify a pre-existing `AGENTS.md` only within its own marker block.

**Pattern:** event · **Flow:** arch-90c90751344f · **Actor:** — · **Confidence:** observed · **Status:** assumed · **Owner:** repo maintainer

**Evidence:**
- `src/docir/modules/agents/application/service.py:110-135`
- `docs/adr/adr-3a2d5ee7bc84-agent-instruction-scaffolding.md`

## BR-064

**Statement.** If an unrecognised agent target name is requested, then the system shall refuse and name the available targets.

**Pattern:** unwanted · **Flow:** arch-90c90751344f · **Actor:** — · **Confidence:** observed · **Status:** confirmed · **Owner:** repo maintainer

- *Given* a project directory · *when* `docir agent install --agent claud` · *then* OBSERVED: exit 2, `error: unknown agent target(s): claud; available: claude, agents`, nothing written.

**Notes:** Was `disputed` — and the entry said so plainly: "this is the behaviour, not a defensible rule". A silent no-op on a typo in a once-per-repository onboarding command left the user believing their agent was configured, while `docir init --profiles` two files away raised on the same class of input. Fixed to match. Re-verified 2026-07-30 by replaying the example above against the current CLI.

**Open questions:** issue-bdb7330441e6 (answered)

**Evidence:**
- `src/docir/modules/agents/application/service.py`

## BR-069

**Statement.** When a tag is renamed or force-removed, the system shall rewrite every referencing document's file and index row within the same transaction.

**Pattern:** event · **Flow:** arch-ccfcceeb35eb · **Actor:** — · **Confidence:** observed · **Status:** assumed · **Owner:** repo maintainer

**Notes:** Also sets `updated = today` on each, which resets the staleness clock for any document without an explicit `verified` date. → issue-9ed4905e0db8.

**Evidence:**
- `src/docir/modules/tags/application/services/tag_service.py:62-105`

## BR-070

**Statement.** If a tag removal is requested while documents still carry the key, then the system shall refuse and name them, unless `--force` is given.

**Pattern:** unwanted · **Flow:** arch-ccfcceeb35eb · **Actor:** — · **Confidence:** observed · **Status:** assumed · **Owner:** repo maintainer

**Evidence:**
- `src/docir/modules/tags/application/services/tag_service.py:90-96`

## BR-071

**Statement.** If a tag is registered under a key that already exists, then the system shall refuse it.

**Pattern:** unwanted · **Flow:** arch-ccfcceeb35eb · **Actor:** — · **Confidence:** observed · **Status:** assumed · **Owner:** repo maintainer

**Notes:** The same check blocks renaming a tag onto an existing key, so merging two tags is impossible. → issue-cc61d038cf8f.

**Evidence:**
- `src/docir/modules/tags/application/services/tag_service.py:46-47`

## BR-073

**Statement.** Where a schema declares a top-level `id_style`, the system shall apply it to every type the merged schema contains, including those contributed by the core and by profiles, unless a type declares its own.

**Pattern:** optional · **Flow:** arch-90c90751344f · **Actor:** — · **Confidence:** observed · **Status:** confirmed · **Owner:** repo maintainer

**Boundaries:** no id_style anywhere -> DEFAULT_ID_STYLE (sequential), so a pre-existing docs-schema.yaml keeps minting the ids it always did, schema-wide random + per-type sequential -> that one type stays sequential, unknown value -> SchemaError at load, exit 3

**Notes:** `docir init` writes this key explicitly (default `random`); the resolution happens before any type is parsed, which is what lets one line cover profile-contributed types.

**Evidence:**
- `src/docir/modules/documents/infra/schema_loader.py:105-121`
- `src/docir/modules/documents/infra/schema_loader.py:134-146`
- `src/docir/modules/documents/domain/schema.py:34-42`

## BR-074

**Statement.** When a store is initialised, the system shall write a collision-resistant `random` id style unless the caller asks for `sequential`.

**Pattern:** event · **Flow:** arch-90c90751344f · **Actor:** — · **Confidence:** observed · **Status:** confirmed · **Owner:** repo maintainer

- *Given* an empty project directory · *when* docir init . · *then* docs-schema.yaml carries `id_style: random`; the first decision is adr-eb7ce81f8cd0 (OBSERVED)
- *Given* the same · *when* docir init . --id-style sequential · *then* the first decision is adr-0001 (OBSERVED)

**Notes:** Deliberately differs from DEFAULT_ID_STYLE. `init` scopes docs to a *shared repository*, where two branches can each mint adr-0007; the bare `~/.docir` fallback is a single-user scratch store where readable numbers cost nothing.

**Evidence:**
- `src/docir/entry_points/composition.py:50-56`
- `src/docir/entry_points/cli/app.py:95-140`

## BR-072

**Statement.** The system shall accept any non-empty string as a tag key.

**Pattern:** ubiquitous · **Flow:** arch-ccfcceeb35eb · **Actor:** — · **Confidence:** inferred · **Status:** assumed · **Owner:** repo maintainer

**Notes:** No format rule exists anywhere. Document ids are strictly regex-validated (identifiers.py:21); tag keys are not validated at all. → issue-e71e1ad9b0ef.

**Evidence:**
- `src/docir/modules/tags/application/services/tag_service.py:43-52`

## Verification status (2026-07-30)

Of the 47 rules, **9 are `confirmed`, 38 are `assumed`, none is `disputed`.** The five that were disputed described v0.2.1 behaviour and were re-verified on 2026-07-30 by replaying each rule's own Given/When/Then against the current CLI: every disputed claim is now false, and the entries were rewritten to state what the system does rather than flipped. Two of them (BR-043, BR-045) had a wrong *statement*, not merely an unmet one — the layering rule was written as an exemption list, which made `relates_to` a dependency claim, and `--strict` was specified to fail on any finding, which failed a healthy corpus. The other 38 rules have NOT been re-verified since the v0.2.1 pass. `assumed` means reconstructed from the code and never confirmed by anyone who could say what was intended; it does not mean wrong, and it does not mean checked. That distinction is what archived issue issue-b928ad676595 recorded.