Tag scope is declared where membership claims are made
Type: ../types/design-proposal.md · Tags: kb-maintenance, document-system
Tags today have no declared scope, and the system's consuming surfaces disagree about what scope they assume: the validator enforces the tag-README marks over one collection, one navigation recipe sweeps three collections, another sweeps one, and the reader-facing wording of the complete mark ("every note carrying the tag") names no scope at all. The build splits the difference within a single page view: a rendered note's tag link routes across collection boundaries to the nearest declared landing page, whose generated listing enumerates only that landing's own collection — so the reader arrives on a page whose "complete" tail omits the note they came from. The disagreement is also a live, validator-invisible falsehood: a note outside kb/notes/ carries a covered tag with none of its declared children.
The reframing this proposal rests on: a bare tag string needs no scope. An rg sweep is always explicit about the paths it searches, so tagging a note commits to nothing scope-shaped. Scope becomes real only where a membership claim is made — a complete or covered_by mark, a documented sweep recipe, a skip rule that lets a consumer trust a mark instead of sweeping, or a built index that presents itself as the tag's full listing. The build is where the stakes are highest: ADR 025 makes the generated tail the completeness surface for human readers ("completeness is the build's job"), so whatever scope the tail enumerates is the tag's de facto extent for the published site, and the tag links that feed traffic into it must agree with that scope. So the design question is not "what scope do tags have" but "what scope do membership claims — marks, recipes, and built indexes — declare and enforce, and how do readers learn it."
Current state (as of 2026-07-24)
- Enforcement is collection-scoped. The validator's
complete/covered_bychecks build membership from a single-collection scan (collect_collection_tag_index(collection_dir)insrc/commonplace/lib/index_generated.py, consumed by thetag-readmetype rule invalidation.py). A note in another collection can never satisfy or violate a mark as far as the validator sees. - The build's two tag surfaces use different scopes. The generated per-tag tail (
## Other tagged notes) enumerates only the landing page's own collection (generated_section_for_indexresolvescollection_for_path(index_path)and scans it alone). But the rendered Tags: footer link on each note resolves by proximity, not collection:_find_tag_indexinsrc/commonplace/docs/properdocs_hooks.pywalks up to four directory levels and one level down, so akb/reference/note taggedkb-maintenancelinks across the boundary tokb/notes/kb-maintenance-README.md. Routing is effectively repo-scoped while enumeration is collection-scoped: the published site funnels cross-collection readers onto a listing that omits the note they arrived from. - Reader-facing surfaces disagree. The
tag-readmetype spec andnavigation.mdstate thecompleteclaim without a scope qualifier ("links every note carrying the tag");AGENTS.md's by-tag recipe sweepskb/notes/ kb/reference/ kb/instructions/;navigation.md's canonical by-tag recipe sweepskb/notes/only; the hubtags-README.mdis the one surface that qualifies its own mark ("complete over the tag pages in this collection"). - Tags already cross collections.
kb-maintenancehas 19 members inkb/notes/and 14 inkb/reference/;computational-model,tool-loop,foundations, and others leak similarly intokb/reference/andkb/agentic-systems/. Converselytrace-learning(102 members) exists only inkb/agent-memory-systems/— a de facto collection-local tag vocabulary. - One repo-scope violation is live and invisible.
kb/agent-memory-systems/trace-learning-techniques-in-related-systems.mdcarrieslearning-theorybut none of the sixcovered_bychildren declared bykb/notes/learning-theory-README.md. At collection scope the mark is true and validates clean; at the scopeAGENTS.md's recipe implies, the reader-facing coverage claim is false. - Tag-READMEs exist only in
kb/notes/(21 files including the hub). Marks currently declared:completeon the hub,discovery-README.md, andartifact-analysis-README.md(neither tag leaks outsidekb/notes/, so their claims are true at either scope today);covered_byonlearning-theory-README.md. cp-skill-connectis already coherent under collection scope. It prospects per destination collection and reads the destination's own<tag>-README.md, skipping the by-tagrgonly for that destination when the mark is present.- ADR 004 made tags freeform strings; ADR 026 made the marks enforced-or-omitted precisely because a claim that tells exhaustive consumers to stop looking must never be silently false — the property the current scope mismatch violates at repo scope.
The design space
-
Formalized collection scope. A tag is
(collection, string); the same string in two collections is two tags. This hardens what the validator, the generated tail, andcp-skill-connectalready do. Beyond prose alignment — qualify the mark's meaning everywhere it is stated ("every note in this collection"), makeAGENTS.md's three-collection recipe either per-collection or explicitly a union of independent scoped sweeps — the build's tag-link routing must be brought in line:_find_tag_indexstops at the collection boundary, and a tag with no landing in the note's own collection renders as plain text (or the collection grows its own README —trace-learninginkb/agent-memory-systems/is the first real candidate outsidekb/notes/). Cross-collection topical routing remains the job of authored links and the hub's editorial prose. Cost: there is no single enforceable surface for "everything about kb-maintenance anywhere" — readers who want it must know to union sweeps, no mark can ever license skipping that union, and cross-collection tag traffic on the published site disappears unless re-authored as links. -
Repo scope. One tag namespace; marks are checked over all collections; the by-tag recipe sweeps the whole
kb/; the generated tail enumerates repo-wide, which makes it consistent with the tag-link routing the build already does. The existinglearning-theoryviolation immediately surfaces as a validator failure (correctly, under this reading). Costs:kb/notes/tag-READMEs become cross-collection artifacts whose entries must link intokb/reference/and beyond — a linking posture the collection contracts would have to authorize; external-coverage collections lose tagging autonomy (their local idioms liketrace-learningjoin a namespace shared with theory tags, sharpening same-string-different-sense risk); every mark's blast radius grows with every new collection, socompletebecomes harder to hold as the KB grows. -
Declared dual scope. Scope is a frontmatter field on the tag-README — the one artifact that reifies a tag — defaulting to collection scope, with an explicit opt-in (working name
scope: kb) that widens that README's mark checks, its generated tail, and its readers' skip rights to the whole repo. This is the ADR 024/026 move again: the claim's parameters live on the artifact that makes the claim, so they cannot drift from it — and because the build already resolves each page's tail from the page itself, the tail generator and the validator read the same field and stay in agreement by construction. Tag-link routing follows the declared scope: links from other collections resolve only to ascope: kblanding, so a collection-scoped README never receives traffic its tail cannot account for. Bare tags without READMEs need no scope at all. Cost: a collision rule is needed (what happens when a kb-scoped<t>-README.mdcoexists with a collection-local<t>-README.mdelsewhere — likely: a kb-scoped tag name is reserved repo-wide), and every consumer — recipes, skip rules,_find_tag_index— must actually consume the field or it decays into decoration.
Under every option, one piece of work is identical and can ship first: align the three disagreeing surfaces (type spec, navigation.md, AGENTS.md) on one stated scope, and either fix or honestly qualify the live learning-theory discrepancy. Option 3 then reduces to a small delta on option 1 — a declared escape hatch whose adoption can wait, per YAGNI, until a tag actually needs a repo-wide claim.
Free choices
- Default scope. Collection scope is the code's current behavior and the cheaper claim to keep true; repo scope matches what
AGENTS.md's recipe has been implying to readers. The default decides which existing surfaces are "wrong." - Where a kb-scoped README lives (option 3 only). In place in
kb/notes/(where all tag-READMEs already are, but then a theoretical-profile collection hosts cross-collection routing artifacts), or a dedicated home outside any collection (mirroringkb/types/as a global surface). - Cross-collection children. Whether a
covered_bylist on a kb-scoped README may name children whose own READMEs live in other collections, or children must share the parent's scope. - Collision policy. Whether a kb-scoped tag name reserves the string repo-wide — which would ride the reserved-term registry proposed in write-time vocabulary collision controls rather than needing its own mechanism.
- Recipe frontloading. Whether
AGENTS.mdcarries one scoped recipe with prose explaining scope, or per-scope recipes; and whethercp-skill-connect's skip rule needs any change (under options 1 and 3-default it needs none). - Tag links with no in-scope landing. When a note's tag has no landing page its scope rules allow linking to, the footer can render plain text (current fallback), link to the hub, or the build can materialize a virtual per-collection tag page — the last re-opens ADR 025's build-time-only machinery for a new page kind.
Adoption criteria
- Every surface that states a mark's meaning names its scope, and the validator enforces exactly that scope — no reader can be licensed to skip a sweep wider than what is checked.
- The build is internally coherent: a tag link never routes a reader onto a landing page whose generated tail's scope excludes the note the link came from.
- The
AGENTS.mdandnavigation.mdby-tag recipes agree with each other and with the enforced semantics. - The live
learning-theorydiscrepancy is resolved (child tag added,covered_bylist revised, or the claim explicitly scoped tokb/notes/). - If dual scope ships: declaring
scope: kbon one README changes only that README's checks and skip rights, with no migration cost to unscoped tags.
Risks
- Silently false marks are the failure mode being repaired — and the easiest to reintroduce. Any partial fix that qualifies some surfaces but not others reproduces today's state: a mark that reads as global but checks locally tells exhaustive consumers to stop looking while members exist elsewhere.
- Same string, different sense. Collection-local namespaces make it legitimate for
agent-memoryinkb/notes/andkb/agent-memory-systems/to drift apart semantically; without at least the collision screen from the vocabulary-controls proposal, the drift is discovered by a confused reader. - Scope as decoration. A declared scope field that recipes and skills do not consume is a derived claim nobody checks — worse than leaving scope implicit, for the same reason unenforced marks are banned.
Relevant Notes:
- Stale indexes are worse than no indexes — rests-on: a mark whose enforced scope is narrower than its stated scope is a marked-but-incomplete head, the exact catastrophic state the marks exist to prevent
- A derived copy of recomputable truth must be checked or absent — rests-on: scope is a parameter of the derived claim, so it must be part of what the check enforces or the claim must not be made
- Load-bearing vocabulary collisions should be prevented or visibly scoped at write time — rests-on: collection-local tag namespaces legitimize same-string-different-sense drift, which is this invariant applied to tag strings