Documents / Decisions / adr-289e788719a7
A platform.naming leaf for shared name grammars
Why the tag-key grammar lives in platform rather than being written twice or crossing a module boundary.
Context¶
issue-e71e1ad9b0ef asked for a format rule on tag keys: any non-empty string was accepted,
so auth, Auth and authentication could all exist and nothing objected.
The agreed shape (option 1 of three) was to reject a non-conforming key on
the write paths that mint one, and warn about keys already in a store — no
silent normalisation, because lowercasing somebody's key rewrites their data.
That splits the rule across two modules:
tagsapplies it ontag add/tag rename(Tier 0, a hard rejection).documentsapplies it indocir check, whoseGraphCheckeralready receives the registry as data (known_tags) and now reportstag-key-format.
Neither module may import the other. The dependency graph allows only
tags -> documents -> indexing, so a documents -> tags import would close a
cycle, and tach fails the build on it. The alternatives were:
- Write the regex twice. Two definitions of one grammar, guaranteed to drift — and the entire point of a controlled vocabulary is that there is one rule.
- Have
tagsproduce theCheckIssueand let the dispatcher merge two result lists. This needs a newtags -> documents.apiedge for theCheckIssuetype, and puts the decision of what counts as a finding into the wiring layer, which is meant to hold no business logic. - Move the shared thing into
platform— one of the three sanctioned responses to a boundary error, alongside "route through the module's api" and "merge the modules".
Decision¶
Add docir.platform.naming, a pure leaf (depends_on = []) holding name
grammars — rules about the shape of a key, not its meaning. It currently
exports TAG_KEY_PATTERN, TAG_KEY_RE, TAG_KEY_RULE (the human-readable
form, so the error message and the check finding cannot describe different
rules) and is_valid_tag_key.
tags.application and documents.domain both depend on it. Both edges point
the sanctioned direction (module -> platform); no cross-module edge is
created and the deprecated baseline is untouched.
The module is pure — no I/O, no framework — which is what makes it importable
from a domain layer at all, the same property that lets platform.errors and
the Embedding value object be imported there.
Consequences¶
- One grammar, named once. The rejection message and the
checkfinding quote the sameTAG_KEY_RULEstring. platformgrows a fifth pure capability. This is the ADR the architecture rules require for a platform addition (§14).- Validation is a write-path concern only.
is_valid_tag_keymust never be called to reject on a read: a store written before the rule can hold keys that fail it, and refusing to load them would make an old corpus unreadable rather than merely untidy.tag renametherefore validates the new key and not the old one — renaming away from a legacy key is the migration path, and validatingoldwould trap exactly the keys the rule wants gone. tag-key-formatis a warning and must stay one.tag addrejects bad keys now, so the only way to hold one is to predate the rule; failing a--strictbuild for that would repeatissue-9cb85759076d, where a gate that fired on a healthy corpus got switched off and took duplicate-id detection with it.check --fixdoes not repair it either: the fix is a rename, and deciding whetherAuthmeantauthorauthnis a reading of the corpus, not a transformation of it.- Scoped out: the document-id grammar stays in
documents.domain.value_objects.identifiers. It is used by one module, so it has no reason to move; if a second module ever needs it, this is where it goes.