§1 — What is actually missing, and why a field is not enough
The published schema carries 41 properties and no tag field. Its only
grouping axes — sprint, milestone, tier,
north_star — are single-valued and lifecycle-shaped. None expresses "this is
standard-names work". So directories are the only topical grouping in the system, and the
layout migration deletes them: 73 standard-names documents flatten, 71 of whose
slugs do not carry the topic, leaving 09-llm-compose.html beside
gpu-cluster-scoping.html with nothing marking it. Three independent numbered
sequences each run their own 01..46 and interleave into meaninglessness.
The minimal answer is a comma-separated meta field, and it would fail within a
week. An author who cannot enumerate the existing tags invents one, and the corpus
fragments into standard-names, standard_names, sn and
standardnames as four unrelated groupings — which is not hypothetical, it is the
same shape already paid for twice in a consuming project. A grouping mechanism whose values
diverge is worse than none, because it looks like it is working.
So the design problem is not "how do we store a list of strings". It is: what
makes a vocabulary stay coherent when every author is a different agent or session, none of
them coordinates with the others, and nobody will perform governance. Four
properties answer that, and each is a section below: one canonical identity per tag enforced
where writes happen (§2); the vocabulary visible with usage counts at the moment of
authoring, which is the whole design (§3); and a rename that is one command rather than N
hand edits, because otherwise the first typo is permanent (§5). §4 records the thing this
plan deliberately does not build.
Two decisions on this are already locked on sprint-scope-and-surface and are
inherited rather than re-opened: the inventory is observed usage with counts, not a
curated allow-list, and tags and their backfill precede the layout migration.
This plan builds the system those decisions assume.
§2 — One canonical identity, enforced at write
This is the load-bearing mechanism and the one that makes the vocabulary
stable. A tag's stored identity is derived, not accepted: casefold, trim, collapse
internal whitespace and underscores to a single hyphen. Divergent spellings of the same
intent converge before they are ever stored, so they cannot become separate groupings.
Normalisation collapses spelling variance, and only spelling variance. An
abbreviation is a different word, so it survives as its own identity — and is surfaced as a
singleton rather than silently merged, because merging it would be a guess about intent.
Normalisation is applied, not merely validated. Rejecting
Standard_Names with an error teaches the author nothing and blocks a write that
had no ambiguity in it. Silently accepting it creates the second grouping. Applying the
canonical form and reporting what was stored does both jobs. A value that cannot be
normalised into a non-empty identity — punctuation only, or empty after trimming — is a
genuine error and is refused.
The grammar reserves what it does not yet use. Every tag known today is
a topic, so a flat vocabulary is sufficient now. But : is reserved from the
first commit, so a later faceted form such as machine:iter is additive rather
than a migration across the whole corpus. Reserving a separator costs one validation rule
today; retrofitting one costs a rewrite of every tagged document. This is the difference
between a system and a patch, and it is cheap only if it is done now.
Done when a property test asserts that normalising any two spelling
variants of the same intent yields byte-identical identities, that normalisation is
idempotent, that an unnormalisable value is refused with a message naming the offending
input, and that : is refused with a message stating it is reserved.
§3 — Tags on every typed resource, and the inventory in front of the author
Tags belong on plans, research, evidence and sprints alike. The 73
documents that motivate this plan are research, not plans. A tag field that only
works on plans would leave the exact corpus that needs grouping without it — and would make
a cross-type query, which is where topical grouping earns its keep, impossible to express.
The field follows the established comma-separated meta pattern that
depends_on and informs already use, so it needs no new parsing
convention and travels through the same typed-resource machinery.
The inventory is computed, never stored.read_plan(project) already returns a discovery envelope; the tag inventory joins
it, derived live across plans, research and evidence with a usage count per tag. A count is
what makes a one-off typo visible beside an established tag rather than equal to it
— standard-names (73) next to standrd-names (1) tells the reader
everything without any governance having happened.
And it must be in front of the author at the moment of authoring.reckon-create is where a new resource's metadata is chosen. A skill that says
"add tags" without showing which ones exist is precisely the instruction that produces four
spellings. This is the cheap half of the whole plan and the half most likely to be dropped
as a nicety.
Done when the discovery response carries every tag in use across all
typed resources with its count; a test asserts the inventory is derived from the corpus
rather than from any literal list, so a newly invented tag appears with no code change; and
reckon-create surfaces the inventory at authoring time.
§4 — Why there is no tag filter, and what replaces the one you would build
An earlier draft of this plan proposed tag filtering in discovery, in
roadmap, and as an SPA facet. That was scope, and it is cut. The
reasoning is worth keeping so the next author does not add it back by reflex.
Retrieval is already free.read_plan(project) returns a
discovery envelope carrying every resource with its parsed metadata, so once resources carry
tags, the tags arrive with them. An agent wanting everything in a programme already holds the
whole corpus and selects over it — a list comprehension, not a request. A server-side filter
parameter would be a second way to obtain what the payload already contains, and a second
way is a second thing to keep consistent with the first.
The authoring moment is the one that needed help, and it is a different
problem. Directories did two jobs: they told an author where a thing belonged, and
they let a reader enumerate a programme. Only the first was ever hard. Enumeration was
always cheap and stays cheap. What was expensive — and what produced four spellings of the
same tag — is an author choosing a value with no view of what the corpus already uses. The
inventory with counts (§3) is the whole answer to that, and adding query surface does not
improve it.
One property from that draft is real and moves to §3 rather than being
lost: a resource must appear under every tag it carries, not only the
first. That is a multivalued-ness assertion on the field and the inventory, and it is the
failure a single-valued implementation produces — one that reads as working on any document
that happens to carry exactly one tag, which most will.
Done when this plan ships no tag query parameter and no SPA tag facet,
and the multivalued assertion is carried by §3's tests.
§5 — Rename, because the first typo is otherwise permanent
This is what separates a stable vocabulary from a frozen one. Observed
usage with counts deliberately permits invention, which means mistakes and second thoughts
are permitted too. Without a rename, correcting standrd-names means editing
every carrying document by hand, so nobody does it and the corpus accumulates near-duplicates
forever — the allow-list's failure mode arriving by a different road.
Deliverable: one command that renames a tag across every typed resource
in a project, reporting the resources it will touch before touching them, and writing through
the same version-safe path as any other state edit so a concurrent writer cannot be
clobbered. It merges into an existing tag when the target already exists, since that is
exactly the near-duplicate repair.
And the audit closes the loop. Singletons are surfaced as findings —
the near-zero-cost governance the locked decision names — alongside pairs whose canonical
identities differ only by a small edit distance, which catches
standrd-names beside standard-names without forbidding either.
Reported, never auto-merged: the system corrects spelling variance mechanically because that
is decidable, and surfaces suspected intent variance to a human because that is not.
Done when renaming a tag rewrites every carrying resource in one
version-safe pass, renaming onto an existing tag merges rather than duplicating, a dry run
lists the affected resources without writing, and the audit reports both singletons and
near-duplicate pairs with their counts.
§6 — Backfill, and the one thing that must happen before the layout migration
The backfill needs no judgement, which is the whole reason it must happen while the
information still exists: the source directory is the tag. A
document at research/standard-names/09-llm-compose.html carries the tag
standard-names because that is what its location already asserted.
The pre-image is preserved at
~/.local/share/imas-codex/receipts/reckon-migration/layout-moves-preimage.txt,
so the mapping is recoverable whenever it is taken. After the flattening it is recoverable
only from git history, which is a materially harder and more error-prone read.
Done when every document the layout migration would move carries a tag
derived from its pre-migration path, and a comparison against the saved pre-image proves
zero grouping loss — an assertion in the suite rather than a judgement, failing if a single
document would lose a programme it previously had.
A document can carry two tags and cannot live in two directories, so what replaces the
directories is strictly more expressive than what it replaces. That is worth stating plainly,
because the migration is otherwise easy to read as a loss being mitigated rather than an
improvement being unlocked.
§ Decisions
Which resource types carry tags?
Locked 2026-08-24 by the corpus rather than by preference. The 73 documents that motivate this plan are research, not plans, so a plans-only field would leave the exact material that needs grouping without it — and would make the cross-type query, which is where topical grouping earns its keep, inexpressible. The field follows the existing comma-separated meta pattern used by depends_on and informs, so extending it across types costs no new parsing convention.
What happens when an authored tag is not in canonical form?
The four-spellings failure is the central risk. The choice is between refusing a non-canonical value, silently accepting it, or deriving the canonical identity from it.
Locked 2026-08-24. Accepting verbatim creates the second grouping, which is the failure being prevented. Refusing blocks a write that contained no ambiguity and teaches nothing at the moment it matters. Applying the canonical form and reporting it does both jobs: the corpus cannot fragment on spelling, and the author learns the convention from the response rather than from documentation. A value that normalises to nothing is still a genuine error and is refused. Read-time reconciliation was rejected outright because it makes stored identity ambiguous, so any consumer that does not run the same reconciliation sees a different corpus.
Does the tagging system add a query or filter surface, or is the inventory with counts the whole design?
An earlier draft of this plan proposed tag filtering in discovery, in roadmap, and as an SPA facet.
Locked by the lead 2026-08-24, cutting the filter as scope. Retrieval is already free: read_plan(project) returns a discovery envelope carrying every resource with its parsed metadata, so once resources carry tags an agent already holds the whole corpus and selects over it - a list comprehension, not a request. A filter parameter would be a second way to obtain what the payload already contains, and a second way is a second thing to keep consistent. Directories did two jobs, telling an author where a thing belonged and letting a reader enumerate a programme, and only the first was ever hard: enumeration was always cheap and stays cheap, while an author choosing a value with no view of existing usage is what produced four spellings. The inventory with counts answers that completely and query surface does not improve it. One property from the cut draft is retained in §3 rather than lost - a resource must appear under every tag it carries, not only the first.
Does the grammar reserve a facet separator now, and does it accept facets on day one?
Every tag known today is a topic, so a flat vocabulary is sufficient. The question is headroom: reserving : now costs one validation rule, while retrofitting a separator later costs a rewrite of every tagged document. Accepting facets on day one costs more than reserving and may never be used.
Locked by the lead 2026-08-24. Reserving the separator costs one validation rule today and makes a faceted form purely additive later; retrofitting a separator once the corpus is tagged means rewriting every tagged document, so this is cheap now and never cheap again. Accepting facets on day one was rejected as design paid for before any evidence a second facet is wanted, and because a facet vocabulary needs its own inventory and governance, doubling the surface this plan is trying to keep small. A tag containing a colon is refused with a message stating the character is reserved, so the refusal teaches the rule rather than merely failing.
When the audit finds two tags whose identities differ by a small edit distance, what does it do?
Normalisation handles spelling variance mechanically because that is decidable. A near-duplicate such as standrd-names beside standard-names is a suspected typo, which is a guess about intent rather than a derivable fact.
Locked by the lead 2026-08-24. A warning with no adjacent action is the kind that gets scrolled past, and the entire argument for observed-usage-with-counts over a curated allow-list was that governance would be near-zero cost - which only holds if the repair is one paste rather than a research task. Emitting the exact rename invocation beside both tags and their counts turns a finding into a fix. Auto-merge was rejected because it is a guess about intent with no undo, and it fails precisely where the author was right: a genuinely new tag that happens to resemble an established one.
§ Followups
Build the vocabulary mechanism before the field, then backfill before the flatten
Normalisation first, since it is what keeps the vocabulary coherent when every author is a different session and nobody performs governance — applied and reported rather than refused or accepted verbatim. Then the field across every typed resource, because the 73 documents that motivate this are research and a plans-only field misses them entirely. Then the computed inventory with counts, in the discovery envelope and in front of the author at reckon-create. Then filtering, because a grouping you cannot query is decoration. Then rename plus the audit's singleton and near-duplicate findings, which is what stops the first typo becoming permanent. The backfill from the saved pre-image lands last but must precede the layout migration, and the zero-grouping-loss comparison is an assertion in the suite rather than a judgement. Two decisions are open for the lead: whether the grammar reserves a facet separator now, and what the audit does with a near-duplicate pair.