TECH — {427.3} Grain registry, residual grain, and the retirement of the closed type vocabulary
TECH — {427.3} OKF producer inversion
Section titled “TECH — {427.3} OKF producer inversion”Task: id-427. Artefact: {427.3} TECH. Date: 09/08/2026 (S546).
Reads: RESEARCH.md {427.1}, PRODUCT.md {427.2}.
Paths: producer scripts/cocoindex_pipeline/producer/; Source adapters
scripts/cocoindex_pipeline/sources/. All line numbers are main d1e6e14ad.
1. Architecture — the one structural move
Section titled “1. Architecture — the one structural move”Today the producer has seven enumeration branches and three separate dispatchers
keyed on concept_type (read_concept l_records.py:993-1017,
_source_documents_for_key :1056-1085, sample_rows :1185-1210), plus a directory
literal embedded in each branch, plus a write-time redirect for the one case where two
branches collided. Adding a grain means editing four places and, today, also editing four
type registries. That structure is why nobody added a catch-all.
The move: a grain is a declared object, and everything about it lives in one place.
@dataclass(frozen=True)class GrainSpec: name: str # dispatch key, e.g. "topic_scope_tag" directory: str # bundle directory this grain owns, e.g. "topics" type_label: str # the `type:` value concepts of this grain carry list: Callable[[Pool], Awaitable[GrainEnumeration]] read: Callable[[Pool, ConceptKey], Awaitable[ConceptRaw]] sample: Callable[[Pool, ConceptKey, int], Awaitable[list[Mapping[str, Any]]]] drafts_via: Literal["pass1", "template"] = "pass1"
@dataclass(frozen=True)class GrainEnumeration: keys: tuple[ConceptKey, ...] covers: Coverage # the unit ids this grain's concepts reach
@dataclass(frozen=True)class Coverage: source_document_ids: frozenset[str] = frozenset() q_a_pair_ids: frozenset[str] = frozenset()ConceptKey gains grain: str (the dispatch key) alongside concept_type (the emitted
label). list_concepts() iterates the registry, unions every grain’s Coverage, and then
runs the residual grain last, handed that union. read_concept/sample_rows become
self._grains[key.grain].read(...) / .sample(...). _source_documents_for_key stops
sniffing concept_type and takes an explicit patterns argument from its grain.
Consequences, all deliberate:
- Adding a grain is one registry entry. No dispatcher edit. This is the property whose absence produced the inversion.
concept_typeis no longer read by any control flow — only written into frontmatter (enrich.py:939) and into the type filter the viewer derives from the bundle (bundle-graph.ts:515). That is what “a label, not a gate” means mechanically.- The directory is the grain’s, not the type’s (RESEARCH M5/C2). Relabelling a concept cannot move its file — PI-5, and BI-2’s memo key stays put.
repo_docs.RepoDocsSourceuses the same registry shape for its two pillars, so the two Sources stop being parallel implementations of the same idea (id-362 F1).
2. Decisions
Section titled “2. Decisions”2.1 The residual grain — coverage, not a catch-all list
Section titled “2.1 The residual grain — coverage, not a catch-all list”Coverage cannot be computed by a standalone SQL predicate. The product / certification /
case_study grains select documents by %<entity canonical_name>% ILIKE patterns that are
data-dependent (l_records.py:455-466), so “the set the preferred grains reach” is not
expressible as a static predicate. Therefore:
Every grain declares what it covers, and the residual grain is the complement.
Each preferred grain’s list returns, alongside its keys, one set-based query’s worth of
covered unit ids (six extra set-based queries per run — no per-concept round-trips, MD-5’s
existing discipline). The residual grain then issues two anti-joins:
-- residual documentsSELECT id, filename, logical_path, created_at, updated_atFROM source_documentsWHERE publication_status = 'published' AND id <> ALL($1::uuid[]) ORDER BY id;
-- residual pairs (attribution inputs only)SELECT id, source_document_id, source_form_instance_idFROM q_a_pairsWHERE publication_status = 'published' AND id <> ALL($1::uuid[]) ORDER BY id;The invariant is coverage (≥1), not partition (=1). Measured (RESEARCH C4), a published
pair with a scope_tag and a pattern-matched parent already lands in two concepts today,
and that overlap is good — the same knowledge reachable two ways. DR-141’s Decision says
“exactly one”; the implementable and product-load-bearing invariant is “at least one”.
Action: recommend a DR-141 rider recording coverage-not-partition. This spec does not
silently reinterpret the register.
2.2 The attribution cascade — three homes, one rule
Section titled “2.2 The attribution cascade — three homes, one rule”A residual document is its own concept. A residual pair is attributed by a cascade,
because q_a_pairs.source_document_id is nullable and is NULL for every
derived_from_form_response pair (RESEARCH M6):
residual_home(unit) = unit is a source_document → documents/ unit.source_document_id → documents/ (joins that document's concept) unit.source_form_instance_id → questionnaire-responses/ otherwise → unattributed-answers/ (one bundle-wide concept)| Home | Key | rel_path | type | Populated by |
|---|---|---|---|---|
documents/ | source_document_id | documents/<slug>-<sd_uuid[:8]>.md | document | hole 2, and hole-1 pairs that have a parent document |
questionnaire-responses/ | form_instance_id | questionnaire-responses/<slug>-<fi_uuid[:8]>.md | questionnaire_response | published derived_from_form_response pairs from non-won forms |
unattributed-answers/ | — (singleton) | unattributed-answers/published-answers.md | answer_set | published pairs with neither lineage; omitted entirely when empty |
Why document is the type label. OKF §4.1 asks for descriptive and self-explanatory.
residual/unrouted describe our pipeline, not the knowledge. source_document borrows
a table name and invites exactly the content_type/concept-type conflation DR-050 forbids.
document is what the thing is, and a generic OKF consumer reads it without a glossary.
Why the -<uuid[:8]> suffix is unconditional. write_bundle refuses a physical
write-path collision before any write in the run (bundle_writer.py:1310-1320), a
residual concept has no curated name to disambiguate with, and a conditional suffix would
make one concept’s identity depend on the existence of an unrelated row (deleting the
collider would rename the survivor — a spurious moved). An 8-hex prefix is not a
record pointer: contains_record_pointer matches only the full 8-4-4-4-12 form
(resource_uri.py:65-68), so BI-10 is untouched.
Title guard. The residual title derives from filename. If that filename embeds a full
uuid (pipeline sidecars are minted from sd:<rel_path>), build_concept_frontmatter would
raise on the BI-10 check (frontmatter.py:258-264). The renderer runs
contains_record_pointer on the candidate title first and falls back to a neutral
"Undistilled source document" when it hits.
2.3 What Pass-1 receives, and when it does not run
Section titled “2.3 What Pass-1 receives, and when it does not run”read_concept for a residual document returns the document row, its published q_a_pairs
(possibly none), its record_lifecycle, entity_mentions and entity_relationships. It
does not return body text: source_documents.extracted_text is permanently NULL on the
pipeline path and composing content_chunks bodies is ruled out of scope
(l_records.py:383-388).
So the grain declares its drafter:
drafts_via="pass1"when the cluster has ≥1 publishedq_a_pair— real content, the ordinary agent loop, no special case.drafts_via="template"when it has none —render_undistilled_draft(key, raw)returns aConceptDraftdirectly, bypassing the agent loop entirely:
# {title}
No published answer has been distilled from this document. It is held in the corpus andis reachable here so that its absence from the answer set is visible rather than silent.
## What is known* Document: {filename} ({content_type})* Held since: {created_at}; last updated: {updated_at}* Extraction: {extraction_method}* Entities mentioned: {canonical_names} <!-- omitted when none -->
## What is not knownNo question–answer pair published from this document exists. Any question this documentmight answer is unanswered by this bundle. Escalate to a subject-matter expert.Frontmatter: type: document; deterministic description; confidence: no-content —
the A19 value that has been unreachable since it was ratified (RESEARCH M8), and the honest
one here; tags: []; no top-level resource: (id-426 F2-B); sources[] carrying
canonical://source_documents/<uuid>; generated.by = the producer actor string per §7 —
the producer is the actor, there is no human: claim to make.
Rejected — drafting it through Pass-1 anyway. The model would receive a filename and metadata and be asked for a title, description and body. It would produce plausible prose about a document it has not read. That is the exact failure the negative answer exists to prevent (PRODUCT PI-4). It also spends an Anthropic call per undistilled document.
2.4 Directory scheme, and the id-429 interface
Section titled “2.4 Directory scheme, and the id-429 interface”Rule: the directory is declared by the grain. There is no type→directory function.
The existing five directories are already grain constants (RESEARCH M5) and are therefore
unchanged: topics/, products/, company/, certifications/, case-studies/. The
won-bid grain owns case-studies/won-bid/ (§2.5). Residual grains own the three
directories in §2.2. A feeder-declared grain names its own directory (§2.7).
Rejected — directory = slug(type). It re-instates the inversion at the filesystem layer
(the label would decide the location), it is not a total function over an open vocabulary
(the feeder’s own docstring, l_records.py:805-812, already ruled that an arbitrary
client-chosen type name has no principled English-plural rule), and it would make a
relabel a file move — churning BI-2 identity, the cocoindex memo key, every BI-9 cross-link
and the client’s git history.
Against id-429’s interface assumptions:
| id-429 | Status here |
|---|---|
| IA-1 every concept has a directory | Satisfied — every grain declares one; nothing is written at the bundle root. |
| IA-2 the residual grain gets a named directory | Satisfied — three of them (§2.2). |
IA-3 index/log refused as concept slugs | Specified — §2.6. Net-new, created by this task. |
| IA-4 deterministic, stable, filesystem-safe directory names; many-to-one fine | Satisfied — they are module constants, not derived values. |
| IA-5 every intermediate directory is real | Satisfied — case-studies/won-bid/ is the only nesting, and it is a real directory today. |
id-429 Q2 is answered, not carried. The residual directory names are readable words, so
id-429 D6’s sentence-cased basenames give “Documents”, “Questionnaire responses”,
“Unattributed answers” with no additional label. The opaque -<uuid[:8]> suffix appears
only in file slugs, never in a directory name, so no heading is ever derived from it.
2.5 The won-bid redirect collapses
Section titled “2.5 The won-bid redirect collapses”_won_bid_case_study_redirect / bundle_write_path / bundle_write_path_for_key
(bundle_writer.py:224-284) exist solely because two case_study grains were forced to
share one directory (RESEARCH M13). Once the won-bid grain declares
directory="case-studies/won-bid", identity rel_path is the physical path and all
three functions delete; flow_def.py’s BI-28 map and the embed lookup key on rel_path
directly.
The physical path does not change (case-studies/won-bid/<slug>.md already). What
changes is the concept’s identity, and therefore its memo key and its BI-9 citation key.
No file moves; RunSummary.moved stays empty. The affected concepts re-draft once — folded
into the single wave-wide version= bump (§5).
2.6 Reserved-name slug guard (id-429 IA-3 — net-new)
Section titled “2.6 Reserved-name slug guard (id-429 IA-3 — net-new)”OKF §3.1 reserves index.md and log.md at any directory level; they MUST NOT be
concept documents. Under a closed vocabulary this was unreachable. Opening it makes a
scope_tag, entity name or filename that slugifies to index or log reachable for the
first time, and such a concept would silently overwrite that directory’s index and then be
reconciled away on the next run.
Rule — deterministic rename, not refusal. A new mint_concept_slug(value) -> str wraps
_slugify (l_records.py:676) and appends -concept when the result case-folds to a
reserved stem:
_RESERVED_CONCEPT_STEMS = frozenset({"index", "log"})mint_concept_slug("Index") -> "index-concept"mint_concept_slug("log") -> "log-concept"mint_concept_slug("iso-9001") -> "iso-9001" # identity on everything elseWhy rename rather than refuse: a client document legitimately called “Index” is a data
fact, not a configuration error; aborting a whole producer run over it violates DR-047’s
narrowly-scoped degrade posture. Why not a _ prefix: leading-underscore files are
skipped or hidden by enough tooling that it trades one silent invisibility for another.
Residual collision: a directory holding both index and a genuine index-concept trips
write_bundle’s existing pre-write collision guard (:1310-1320) and fails loud before any
write — the correct outcome for a genuine ambiguity, and it costs no new machinery.
Directory names are guarded differently. index/log as a feeder-declared directory
is a configuration error, not a data fact, so _validate_concept_feeder_schema
(bundle_writer.py:918) rejects it at read time with the existing fail-loud posture.
2.7 The feeder branch under the open model
Section titled “2.7 The feeder branch under the open model”_list_feeder_concepts (l_records.py:755-821) had two halves. The type-widening half
(concept-feeder.json declares a type, _permit_overlay_concept_types lets it past the
ConceptKey guard) dies with the guard. The grain half — enumerate concepts over
entity_mentions of entity type X — survives, and it is now the only thing the feeder is:
a client-declared preferred-routing grain, which is precisely DR-141’s model.
Config gains an explicit directory key, because type and directory decouple:
{ "framework": { "grain": "entity_mention", "entity_type": "framework", "directory": "frameworks" } }directory defaults to slug(type) when omitted, for compatibility. No shipped bundle
carries a concept-feeder.json (checked: none in canonical-okf-showcase/), so there is
no config to migrate — this is an artefact fact, not a “0 rows” argument about correctness.
2.8 type shape validation — the exact rule
Section titled “2.8 type shape validation — the exact rule”check_type_membership (validator.py:406-420) becomes check_type_shape. The name must
change: an identifier that says “membership” is the inversion.
_TYPE_SHAPE_RE = re.compile(r"^[a-z][a-z0-9]*(?:_[a-z0-9]+)*$")_TYPE_MIN_LEN, _TYPE_MAX_LEN, _TYPE_MAX_WORDS = 3, 40, 4_RESERVED_TYPES = frozenset({"q_a_pair"}) # BI-3, unconditionalA type is valid iff: it is a str; it matches _TYPE_SHAPE_RE (lowercase ASCII
snake_case, leading letter); its length is 3–40; it has at most 4 underscore-separated
words; and it is not in _RESERVED_TYPES.
Why this shape. (i) iri_projection.slug() is identity on it (:132-148), so no
term is silently rewritten downstream; (ii) all ten types the platform emits today —
topic, product, company, certification, case_study, schema, tool, api,
navigation, playbook — already match, so the rule ratifies existing output rather than
inventing a migration; (iii) api is 3 characters, which sets the floor and excludes the
uninformative 1–2 character token; (iv) the word cap keeps a type a label rather than a
sentence, which is what “self-explanatory” buys.
Deliberately not checked: singular-vs-plural (not objectively decidable, and it would
reject nothing a human would call wrong); collision with a content_type value (DR-050
forbids merging the vocabularies, not string coincidence — adding that check would be
the overreach it warns against).
The effective_ontology parameter is dropped from this check: there is no set to compose
against. check_concept/validate_concept still take effective_ontology for
lint_entity_relation_mentions, which is unchanged.
2.9 The four registries + the facet vocabulary
Section titled “2.9 The four registries + the facet vocabulary”| Registry | Disposition | Requirement it served, and why that ends |
|---|---|---|
ALLOWED_CONCEPT_TYPES validator.py:116-118 | DELETED | BI-4’s write gate. Withdrawn by DR-141 / DR-019 amendment. |
CONCEPT_TYPES l_records.py:115-117 + guard :273-284 | DELETED | Source-side defence-in-depth on the same set. Guard replaced by the §2.8 shape check + the BI-3 q_a_pair refusal, which stays unconditional. |
_CLASS_CONCEPT_TYPES validator.py:242-248 | DELETED | DR-079’s per-class type sets. Owner ruling (iii), S546 — uniform across bundle classes. Downstream: EffectiveOntology.base_for_class and repo_docs.SYSTEM_BASELINE_CONCEPT_TYPES delete with it. |
CONCEPT_TYPE_VALUES lib/ontology/concept-schema.ts:70 | DELETED | Its own docstring sources it only to “documentary/UI purposes (e.g. a future type-legend)”. Measured (RESEARCH M4): no runtime consumer, and the legend duty is already discharged by concept-type-tokens.ts KNOWN_TYPES. A future use is not a current source. |
KNOWN_TYPES lib/okf/concept-type-tokens.ts:38-48 | KEPT — the one surviving legend | Colour tokens with an explicit default fallback for unmapped types. Extended with document, questionnaire_response, answer_set, reference. Additive; never a gate. |
RECOGNISED_FACET_TAGS validator.py:147-149 | DELETED | Its own two stated requirements (:120-133): (a) name first-class facets for downstream consumers — discharged by KNOWN_TYPES; (b) stop the type set and facet vocabulary diverging — subject stops existing. Facet tags themselves survive as ordinary free-form tags: entries; they never needed a registry (check_concept has never rejected an unregistered tag). |
FACET_TAG_ALIASES + canonical_facet_tag + normalise_facet_tags :157-175 | DELETED | The methodology → playbook fold existed because the type set would not admit a sixth type — DR-141’s headline evidence. No production caller (RESEARCH M3). |
_permitted_overlay_concept_types + _permit_overlay_concept_types l_records.py:152-168 | DELETED | Existed only to lift the ConceptKey gate for feeder types. Gate gone, widener gone. |
The three bends, resolved as types (AC 3):
reference— the only one with a live emitter.web_pass.py:674emitstype="topic", tags=(…, "reference"); it becomestype="reference"with the tag dropped and_REFERENCE_CONCEPT_TAG(:169) deleted. The judgement-call paragraph at:85-92goes with it.policy/capability— no code path has ever emitted them (RESEARCH M2). Resolving them as types is the deletion of the registry entries that expressed them as facets; nothing starts emitting them until a grain does. Saying this plainly matters: an implementer expecting an emission change will go looking for one that does not exist.methodology→playbook— the alias deletes.playbookalso leaves the facet registry: it is already atypein the system-baseline lane (validator.py:246), and carrying one term in both vocabularies is the double-bookkeeping DR-141 diagnoses.
2.10 EffectiveOntology and the ontology artefacts (DR-027 as amended, F6)
Section titled “2.10 EffectiveOntology and the ontology artefacts (DR-027 as amended, F6)”EffectiveOntology survives with two dimensions. entity_types and
relationship_types remain closed platform CVs — DR-027’s platform-repo half is explicitly
unchanged by the S546 amendment, and lint_entity_relation_mentions is not in DR-141’s
scope. concept_types leaves the dataclass; base_for_class deletes; base_only() returns
the base pair; compose(overlay) unions the two remaining dimensions and ignores an
overlay’s concept_types key.
_OVERLAY_DIMENSIONS (bundle_writer.py:817) keeps all three keys: the overlay schema
is closed (OV-2), so removing concept_types would turn a valid client file into a
validation failure. The key is still meaningful — it declares the client’s own vocabulary
for the overlay echo, which bundle-graph.ts:338-350 reads as a styling signal. It simply
no longer feeds a gate.
write_ontology_artefact emits overlay-only. _base_ontology_snapshot (:699-716)
deletes; the payload becomes {"overlay": <mapping or null>}. Source: DR-027’s S546
amendment, executed here per its own text (“Implementation rides id-427’s wave”).
context.jsonld drops its concept_types dimension. iri_projection._DIMENSIONS
(:76-80) loses that row; project_context (:227-245) projects entity_types +
relationship_types only; the 5/12/10 counts in the slug() docstring (:138-139)
correct to 12/10.
SUPERSEDED, in this paragraph only, by §6’s TQ-1 — RULED (S546), amended (S548), executed by {427.14}.
project_contextprojects client-overlay terms only: the base entity/relationship half retires alongside thebasenamespace-prefix key, so a bundle composing no overlay emits{"@context": {}}. The_DIMENSIONSchange above still stands; what changed is that its registers became an EXCLUSION filter rather than a mint list. The rest of §2.10 (EffectiveOntology,_OVERLAY_DIMENSIONS,write_ontology_artefact) is UNAFFECTED and landed as written at {427.11}.Two riders {427.14} added that this section did not anticipate:
_RESERVED_PREFIX_SLUGSdrops"base", which was reserved solely to protect the prefix key that is now gone; keeping it would silently drop a client whose own overlay declares a term namedbase.- The claim below that the
overlayecho’sconcept_typeskey is read bybundle-graph.ts:338-350was measured WRONG at S550 —readBundleClassSignalonly testedoverlayfor null-ness and never openedconcept_types. {427.14} made the claim true rather than correcting it downward: that key is now the source of the re-sourcedtypeDeclarationborder channel.
Why not re-derive the concept-type projection from the observed bundle instead. The
classifier that made the projection meaningful was ALLOWED_CONCEPT_TYPES — the register
that said which terms were base. With it gone there is no base/overlay distinction for
that dimension, and minting a w3id.org/canonical/ontology IRI for an organically-minted
label would assert exactly the registration DR-027-as-amended just retired ontology.json.base
for. Carried as TQ-2 below.
2.11 Corpus census — making PI-2 mechanical
Section titled “2.11 Corpus census — making PI-2 mechanical”@dataclass(frozen=True)class CorpusCensus: considered: tuple[tuple[str, int], ...] = () # ("source_documents", 42), ("q_a_pairs", 310) routed: tuple[tuple[str, int], ...] = () # unrouted derived per kind; unrouted_total is the property PI-8 keys onSource gains census() -> CorpusCensus (both implementations). RunSummary gains
census: CorpusCensus = CorpusCensus(). _render_run_bullets (bundle_writer.py:577)
emits, unconditionally, including on a no-op run:
* **Run <ISO-ts> — Considered (2):** source_documents 42 (routed 42), q_a_pairs 310 (routed 310)and, only when non-zero, an Unrouted (N) line. RunSummary.is_no_op (:562-570) gains
or self.census.unrouted_total — a run that leaves knowledge unrouted is never a silent
no-op, exactly the precedent failed already sets.
AMENDED 2026-08-10 by {427.13}‘s re-derivation (§3) — “unconditionally” is one case too broad, and the IMPLEMENTATION is the correct half. {427.9} shipped the
Consideredline guarded byif not census.considered: return []. The case this paragraph did not consider is a caller with no corpus at all — a directwrite_bundlecall, which is handed drafts rather than a Source and so takes no census. EmittingConsidered (0)there would report a measurement nobody took, which is precisely the failure {427.7} named when it warned that its own emptyCoverage“is not a measurement”;CorpusCensus()’s empty default means “no census taken”, not “a census that found nothing”.The rule as amended:
Consideredis emitted on every run that took a census, including a no-op one — which is the guarantee this section actually wanted, since a no-op run over a real corpus is exactly the case the census exists to make speak.is_no_opis unaffected and landed as written.
Counts only — no uuids in log.md. BI-10 keeps record pointers to resource:/citations,
and a run log is neither.
Rejected — aborting the run when unrouted > 0. The residual grain makes it structurally
impossible, so a non-zero value means a grain regressed, not that the data is bad; DR-047’s
degrade posture says report rather than destroy an otherwise-correct bundle. Silence is what
is forbidden, and the census removes it. The hard guarantee lives in the behaviour test
({427.10}), where it belongs.
2.12 id-358 — the rename
Section titled “2.12 id-358 — the rename”ConceptKey.workspace_id → form_instance_id (l_records.py:219-236, and its
__post_init__ check :285-290); ProposedChange.source_workspace_id →
source_form_instance_id (git_sync.py:208-235); sync_bundle(source_workspace_ids=) →
source_form_instance_ids= (:628,:653-656,:722-733); flow_def.py:617-638’s map variable
follows. The BI-28 slot stays (owner, S546). The field has held a form_instances.id
since {145.24}; the only thing wrong with it is the name, and the reason it was not renamed
then — cross-file blast radius — is dissolved by this task rewriting those files anyway.
3. The audit — wired-sites disposition table (AC 5)
Section titled “3. The audit — wired-sites disposition table (AC 5)”Substrate §2’s blast-radius list, each site dispositioned. Changed / kept / deleted / UNDECIDABLE. Sites the substrate listed that measurement moved are flagged [C]* with the challenge that moved them.
Producer — scripts/cocoindex_pipeline/producer/
Section titled “Producer — scripts/cocoindex_pipeline/producer/”| Site | Disposition |
|---|---|
validator.py:116-118 ALLOWED_CONCEPT_TYPES | DELETED (§2.9) |
validator.py:120-157 RECOGNISED_FACET_TAGS, FACET_TAG_ALIASES | DELETED (§2.9) [C3] |
validator.py:160-175 canonical_facet_tag, normalise_facet_tags | DELETED — no production caller (M3) |
validator.py:242-248 _CLASS_CONCEPT_TYPES | DELETED — owner ruling (iii) |
validator.py:263-331 EffectiveOntology | CHANGED — two dimensions; base_for_class deleted (§2.10) |
validator.py:406-420 check_type_membership | CHANGED → check_type_shape (§2.8) |
validator.py:596-617 check_concept / validate_concept | CHANGED — calls the shape check; stops threading effective_ontology into it |
l_records.py:115-117 CONCEPT_TYPES | DELETED |
l_records.py:152-168 contextvars overlay widener | DELETED |
l_records.py:219-236, 285-290 ConceptKey.workspace_id | CHANGED — renamed form_instance_id (id-358) |
l_records.py:260-290 ConceptKey.__post_init__ | CHANGED — shape + BI-3 + non-empty grain |
l_records.py:340-351 local Source protocol | MOVED to sources/base.py; gains census() (id-362 F1) |
l_records.py:676 _slugify | KEPT, wrapped by mint_concept_slug (§2.6) |
l_records.py:736-753 list_concepts | CHANGED — registry iteration + coverage union + residual last |
l_records.py:755-821 _list_feeder_concepts | KEPT, re-described — client-declared routing grain; config gains directory (§2.7) |
l_records.py:823-984 the six grain methods | CHANGED — each returns GrainEnumeration; each declares its directory |
l_records.py:993-1017 read_concept five-way dispatch | CHANGED — grain-keyed (§1) |
l_records.py:1056-1085 _source_documents_for_key | CHANGED — takes patterns; stops sniffing concept_type; its topic-branch ValueError deletes |
l_records.py:1185-1210 sample_rows | CHANGED — grain-keyed |
bundle_writer.py:158 validator import | CHANGED |
bundle_writer.py:224-284 won-bid redirect, bundle_write_path, bundle_write_path_for_key | DELETED (§2.5) |
bundle_writer.py:528-570 RunSummary | CHANGED — census; is_no_op accounts for unrouted (§2.11) |
bundle_writer.py:577-620 _render_run_bullets | CHANGED — unconditional Considered line |
bundle_writer.py:699-716 _base_ontology_snapshot | DELETED — DR-027 S546 amendment |
bundle_writer.py:745-747, 787-812 overlay class gate | KEPT — DR-054/DR-079, a different live requirement |
bundle_writer.py:918 _validate_concept_feeder_schema | CHANGED — accepts directory; rejects index/log (§2.6) |
bundle_writer.py:1025-1054 write_ontology_artefact | CHANGED — overlay-only |
bundle_writer.py:1266-1282 bundle-class → effective ontology | CHANGED — no concept-type dimension; the internal_dev fail-loud deletes (its requirement was “no ratified type set yet”) |
bundle_writer.py:394-519 IndexTheme / build_index_themes / theme_config / unthemed_heading | RETIRED BY id-429 (D3) — not dispositioned here; shared-file coordination only |
iri_projection.py:57-62, 76-80, 227-245 | CHANGED — concept_types dimension dropped (§2.10) |
iri_projection.py:138-139 slug() docstring 5/12/10 | CHANGED — docstring counts correct to 12/10 |
enrich.py:246 imports from sources.l_records | CHANGED — imports from sources/base.py (id-362 F1) |
enrich.py:328-341 _qa_pairs_anchor | CHANGED — grain-keyed, not type-keyed |
enrich.py:344-353 _resource_from_raw | KEPT mechanically; its output relocates to sources[] — id-426 owns that move |
enrich.py:380-395 _annotate_raw_with_anchors | KEPT — rename ripple only |
enrich.py:847-880 enrich_concept | CHANGED — protocol-typed; version= bump (§5) — see the S550 correction under this table |
enrich.py:938-951 frontmatter assembly | CHANGED — template path bypasses; confidence: no-content reachable |
web_pass.py:85-92 judgement-call docstring, :169 _REFERENCE_CONCEPT_TAG, :670-674 | CHANGED — type="reference", tag dropped (§2.9) |
flow_def.py:138, 519-533 _REPO_DOCS_BUNDLE_CLASSES | KEPT [C1] — source selection (bundle-doctrine’s two paths), not type gating |
flow_def.py:617-638 BI-28 provenance map | CHANGED — rename ripple; keys on rel_path once the redirect goes. Residual stamping: TQ-3 / PQ-3 |
git_sync.py:208-235, 628-656, 712-733 | CHANGED — id-358 rename (§2.12) |
Sources — scripts/cocoindex_pipeline/sources/
Section titled “Sources — scripts/cocoindex_pipeline/sources/”| Site | Disposition |
|---|---|
repo_docs.py:105-119 SYSTEM_BASELINE_CONCEPT_TYPES | DELETED — with _CLASS_CONCEPT_TYPES |
repo_docs.py:122-201 RepoConceptKey | CHANGED — shape check replaces membership; converges on the shared ConceptKey (id-362 F1) |
repo_docs.py:234-242 duplicate Source protocol | DELETED [M14] — imports the shared one |
repo_docs.py:549-680 grains at :584/:594/:628 | CHANGED — declare directories + coverage; census() added — see the S550 correction under this table |
Correction (2026-08-10, S550, from id-427 {427.16}) — two rows of this table asserted an end state as a change
Section titled “Correction (2026-08-10, S550, from id-427 {427.16}) — two rows of this table asserted an end state as a change”Both rows above were read as delivered and neither was. The S548 independent
spec-compliance audit measured them; {427.16} executed the fix. Recorded here because the
table itself was the mechanism — “the disposition table asserted the end state as a change,
so both the implementer and the ledger read it as delivered” (tasks/id-362.md, S548).
enrich_concept— “CHANGED — protocol-typed” described a state that ALREADY HELD at the base SHA. The function was byte-identical across the whole wave apart fromversion=3; it was typedkey: ConceptKey, which is precisely NOT protocol-typed. It is nowkey: ConceptKeyLike. And the row understates the work even as an instruction: typing alone leaves the call raisingAttributeErroron aRepoConceptRaw, because §3 nowhere notes thatConceptRawandRepoConceptRawshare no field. The row should have read “CHANGED — protocol-typed AND the anchor-annotation path taught the artefact raw”.repo_docs.pygrains — “declare directories + coverage; census() added” was HALF done. {427.9} addedCoverageandcensus()and disclosed the rest as an open gap; the registry and the declared directories landed only at {427.16}. A row conjoining four deliverables with+cannot record a partial landing, which is how the disclosed gap went unowned between {427.9} and the S548 audit.
Every line number in both rows is stale (measured S550): the hardcoded pillar tuple sat at
repo_docs.py:610-613, the directory literals at :707/:709/:728, and enrich_concept at
:932-938. Line numbers in this table are pre-{427.4} and should be treated as locators to
re-derive, never as coordinates.
A third site this table never named: flow_def.run_producer_flow’s BI-28 provenance map
read key.form_instance_id directly, crashing every staging system_baseline/internal_dev
run. §3’s flow_def.py:617-638 row calls the BI-28 map “CHANGED — rename ripple; keys on
rel_path once the redirect goes” — true as far as it goes, and blind to the fact that the
line runs for both key models.
RE-DERIVATION (2026-08-10, S550, id-427 {427.13} — AC 5) — every row measured against 20b644468
Section titled “RE-DERIVATION (2026-08-10, S550, id-427 {427.13} — AC 5) — every row measured against 20b644468”Method, and why it is not a re-read. This table had been measured wrong four times across
two sessions, every time by walking it as written. So no row below was settled from the row
above it. Each was settled by measuring the symbol at the base SHA and diffing its site
against the pre-wave commit 2b21fdc05 (the parent of {427.4}, 932b3b784) — because
enrich_concept taught that “CHANGED” can describe a state that already held, and only a diff
against the pre-wave tree distinguishes a change from a description.
Coverage: 56 of 56 table rows re-derived (41 Producer, 4 Sources, 7 TypeScript, 4 Bundle
artefacts), plus the Tests block and the “Explicitly not swept” block — 58 dispositions, none
left blank. What this pass did NOT do: it did not run the producer, execute any SQL, or
open a database; it did not re-read the vendored OKF SPEC; it did not count test assertions
(the substrate’s caveat still stands); and it did not verify the four rows the owner ruled
flag-and-route beyond establishing their current state. GitNexus’s index was measured stale
against this SHA — it surfaced test_an_overlay_declaring_concept_types_stays_schema_valid_and_ composes_nothing, a test that has since been inverted — so every verdict here rests on grep +
git diff + direct reads, not on the code-intelligence graph.
Seven rows were wrong. Each is stated with what it asserted.
-
bundle_writer.py:577-620_render_run_bullets— “CHANGED — unconditionalConsideredline” is FALSE, and the row is not where the error starts. The line is explicitly CONDITIONAL:_render_census_bulletsopensif not census.considered: return [], and its docstring gives the reason — a run with no census (a directwrite_bundlecall, which never saw a corpus) emits neither line, becauseConsidered (0)there “would report a measurement nobody took, which is the error {427.7} named when it warned that its own emptyCoverage‘is not a measurement’”. What holds unconditionally is narrower:Consideredis emitted on every run that took a census, including a no-op one.The row faithfully restates §2.11, so §2.11 is the stale carrier. §2.11 specifies that
_render_run_bulletsemits “unconditionally, including on a no-op run”. {427.9} deliberately departed from that when it met the case §2.11 had not considered — a caller with no corpus at all — and the departure is right: the implementation distinguishes “measured, and found nothing” from “never measured”, which the spec’s blanket rule cannot. This is an undocumented spec departure where the CODE is correct and the SPEC was never updated, which is the reverse of this table’s other six errors. §2.11 is annotated accordingly. -
enrich.py:380-395_annotate_raw_with_anchors— “KEPT — rename ripple only” is FALSE. It gained an entire artefact branch (text-discriminated, minting through the same_mintledger so BI-17 holds) and is the change that closed id-362 F1 leg 2. This is the SAME defect the S550 correction block above already diagnosed one row later: that block correctedenrich_concept’s row to read “protocol-typed AND the anchor-annotation path taught the artefact raw” — and left the anchor-annotation path’s OWN row still saying “rename ripple only”. The correction named the work and did not follow it to the row that owned it. -
enrich.py:328-341_qa_pairs_anchor— “CHANGED — grain-keyed, not type-keyed” is wrong at BOTH ends. It is not grain-keyed now: it readsgetattr(key, "scope_tag", None), a locator presence test that consults no grain registry. And it was not type-keyed before: pre-wave it readkey.scope_tag is not None, the same presence test, unguarded. The row describes a transition between two states that never held. The function DID change ({427.16}:ConceptKeyLike+getattr, so aRepoConceptKeywith noscope_taggets the honestNone), so the disposition word survives and its stated mechanism does not. -
Tests — “All CHANGED” is FALSE for one of the ten named modules.
scripts/tests/test_producer_frontmatter.pyis byte-identical across the entire wave (git diff 2b21fdc05..20b644468returns empty for it). The other nine did change. This is theenrich_conceptfailure mode in the test block: a blanket disposition that reads as delivered for a module nothing touched. -
l_records.py:755-821_list_feeder_concepts— “KEPT, re-described” understates a rename and a re-signature. It is now_list_feeder_grain(spec: GrainSpec) -> GrainEnumeration; pre-wave it was_list_feeder_concepts() -> list[ConceptKey]and carried thegrain == "entity_mention"dispatch itself. That dispatch and itsValueErrormoved out to the registry-build path (_feeder_grains). The row’s substantive claim — config gainsdirectory— is delivered (directory=grain_config.get("directory") or concept_type). -
l_records.py:676_slugify— “KEPT, wrapped bymint_concept_slug” is true but mislocates it. Both functions live insources/base.pynow, notl_records.py. The same applies to three neighbouring rows:ConceptKey.workspace_id,ConceptKey.__post_init__and the localSourceprotocol are all dispositioned againstl_records.pyline ranges, and all three symbols now live insources/base.py. Only theSourcerow says so. -
Bundle artefacts — showcase
ontology.json“REGENERATED overlay-only on the next run” has NOT landed, and cannot have. The on-disk artefact atokf-bundles/canonical-okf-showcase/ontology.jsonstill carries abasekey (withentity_types+relationship_types; noconcept_types). The row is a forecast contingent on a producer run that has not happened, which is a legitimate state — but it is PENDING, not delivered, and a table walked for “landed” would have ticked it.
Two more sites this table never named (the pattern the S550 block opened with flow_def):
bundle_writer._render_census_bullets— net-new at {427.9}, and the function that actually renders the census. §3 dispositions only its caller, which is how correction 1 above went unnoticed.enrich._samples_source_documents— net-new, and the genuinely grain-keyed function (getattr(grain_for(key), "sample_kind", "")). It is the mechanism correction 3’s row wrongly attributed to_qa_pairs_anchor.
The remaining 49 rows are CONFIRMED as written — each measured, not read across. In summary, by disposition:
| Disposition | Rows | Verification executed |
|---|---|---|
| DELETED (11 symbols across 9 rows) | ALLOWED_CONCEPT_TYPES, RECOGNISED_FACET_TAGS, FACET_TAG_ALIASES, canonical_facet_tag, normalise_facet_tags, _CLASS_CONCEPT_TYPES, CONCEPT_TYPES, contextvars overlay widener, won-bid redirect + bundle_write_path + bundle_write_path_for_key, _base_ontology_snapshot, SYSTEM_BASELINE_CONCEPT_TYPES, duplicate Source protocol, CONCEPT_TYPE_VALUES | Each confirmed a REAL definition at 2b21fdc05 (git grep at that rev) and absent at 20b644468 — surviving only as prose in comments, and as assert not hasattr(...) guards in test_producer_validator.py:697-700/799-801 and test_producer_bundle_writer.py:1535. Absence alone was NOT taken as evidence; the pre-wave definition is what makes each a deletion rather than a thing that never existed. |
| CHANGED (confirmed) | EffectiveOntology (3 fields → 2; base_for_class gone), check_type_membership→check_type_shape (takes no effective ontology), check_concept/validate_concept (calls the shape check; stops threading effective_ontology into it — still threads it into lint_entity_relation_mentions, which is what the row means), ConceptKey.workspace_id→form_instance_id, __post_init__ (shape + BI-3 + non-empty grain), list_concepts (registry iteration, coverage union, runs_last ordering), the six grain methods, read_concept (registry lookup), _source_documents_for_key, sample_rows, validator import, RunSummary (census field; is_no_op reads census.unrouted_total), _validate_concept_feeder_schema (accepts directory, refuses index/log), write_ontology_artefact (payload is exactly {"overlay": …}), bundle-class→effective ontology, iri_projection dimensions + slug() docstring counts, enrich.py imports, enrich_concept, frontmatter assembly, web_pass (_REFERENCE_CONCEPT_TYPE = "reference"; no _REFERENCE_CONCEPT_TAG), flow_def BI-28 map, git_sync (id-358 rename), RepoConceptKey, repo_docs grains, concept-schema.ts parity docstring | Symbol located at HEAD, then git diff 2b21fdc05..20b644468 over its site to prove the change is real rather than a description of the status quo ante. Two carry riders: _source_documents_for_key is now _source_documents_by_patterns(patterns) — the key parameter is dropped ENTIRELY, not merely supplemented, and its own docstring records the deleted topic-branch ValueError; bundle-class → effective ontology no longer reads bundle_class at all (EffectiveOntology.compose(overlay) is the single path), so the internal_dev fail-loud is deleted rather than relocated. The six grain methods return GrainEnumeration as stated, but their directories are declared on GrainSpec in _BUILTIN_GRAINS, not in the methods. |
| KEPT (confirmed, with its live requirement named) | overlay class gate (requirement: only client_business may compose a client overlay; source: DR-054/DR-079, OV-10 — live, asserted at bundle_writer.py:1722 and its own docstring calls itself UNCHANGED), flow_def._REPO_DOCS_BUNDLE_CLASSES [C1] (requirement: source selection between bundle-doctrine’s two paths; source: ID-163 PC-2 — live, read at :559), concept-schema.ts type: z.string().min(1) (already correct pre-wave), KNOWN_TYPES as legend (extended past the base 5; 018fc2270 made the domain-tokens guard enumerate it rather than a copy), bundle-graph.ts type derivation, bundle-graph.ts ontology.json overlay read (readBundleClassSignal reads the key’s PRESENCE only), the five type-named directories [C2] (all five present and unmoved on disk) | Each verdict states the requirement and its current source, per the requirement-first rule. A “KEPT” row with no nameable live requirement would have been UNDECIDABLE; none was. |
| UNSWEPT / NOT DISPOSITIONED (unchanged status, restated) | parse-index.ts/parse-log.ts; IndexTheme/build_index_themes/theme_config/unthemed_heading; enrich._resource_from_raw; CONFORMANCE.md rows | See the routing block below. The id-429 theme row is worth one measured note: it says “RETIRED BY id-429 (D3) — not dispositioned here”, and that retirement has since landed (9037022e3) — IndexTheme is renamed, theme_config and unthemed_heading are gone. Still not dispositioned by this task; recorded so the row is not read as outstanding. |
No row in this table is UNDECIDABLE. Every one had a nameable requirement with a current
source, or a measurable presence/absence at two SHAs. The four TQ-* UNDECIDABLEs in §6 are
questions about the DESIGN, not about a row’s disposition, and they are carried verbatim into
tasks/id-427.md by {427.13} rather than resolved here.
Flagged and routed, not fixed — the owner’s S550 directive is that {427.13} must not complete work that an in-flight task will change. Each carries its owning task id:
| Site | Owner | State measured at 20b644468 |
|---|---|---|
showcase CONFORMANCE.md superset 1 | id-431 | AC 6 is discharged at the register, not in the file — DR-019’s S545/S546 amendment already reads “Divergence 1 — the closed, validator-enforced type taxonomy — is WITHDRAWN”, and authority for a conformance posture lives on the register. DR-019 itself records the CONFORMANCE.md house rule as “re-opened with id-431”, whose entire goal is deciding whether a bundle should carry the file at all. Editing a file id-431 may delete is the churn the directive rules out. One brief-supplied ground was measured FALSE and is corrected here: the bundle directory okf-bundles/canonical-okf-showcase/ IS a git repository (git rev-parse --is-inside-work-tree → true, history back through bc5a80a) and CONFORMANCE.md IS tracked (git ls-files returns it). The other two grounds stand and are sufficient. |
showcase CONFORMANCE.md superset 3 (ontology.json) | id-430 / id-431 | Unchanged — id-430 rules the artefact, id-431 the file. |
enrich._resource_from_raw | id-426 | Mechanically KEPT as the row says; re-typed to (key: ConceptKeyLike, raw: Any). Its output’s relocation to sources[] is id-426’s. |
bundle_writer IndexTheme / build_index_themes / theme_config / unthemed_heading | id-429 | Retirement HAS landed (9037022e3) — see the table above. |
Anything v0.2, okf_version, frontmatter shape, sources[] | id-426 (producer) / id-439 (consumers) | Both doing; untouched here. |
enrich._seed_user_message’s “backing L-records” prompt text | {427.16} flagged it | Changing prompt text is a DR-060 drafting-config change requiring a version= bump, and this wave has already spent its one bump (version=3, TECH §5). Flag only. |
__tests__/lib/okf/bundle-graph.test.ts fixtures shipping concept_types inside ontology.json | id-439 | 4 sites measured — :377, :427, :451, :463. Left deliberately by the S550 overlay retirement; the consumer-side parser work is id-439’s. |
_RETIRED_OVERLAY_DIMENSIONS tombstone branch | future subtask | Its stated retirement condition is now MET (see §OV-2’s amendment); retiring it is a behaviour change, so {427.13} recorded the discharge in the docstring and left the branch standing. |
TypeScript
Section titled “TypeScript”| Site | Disposition |
|---|---|
lib/ontology/concept-schema.ts:70 CONCEPT_TYPE_VALUES | DELETED [M4] |
lib/ontology/concept-schema.ts:25-46 type-parity docstring | CHANGED — restated against DR-141 |
lib/ontology/concept-schema.ts:136 type: z.string().min(1) | KEPT — already correct |
lib/okf/concept-type-tokens.ts:38-48 KNOWN_TYPES | KEPT AS LEGEND, extended (§2.9) |
lib/okf/bundle-graph.ts:515 type derivation | KEPT — already derives from the bundle |
lib/okf/bundle-graph.ts:338-350 ontology.json overlay read | KEPT — F6 keeps overlay |
lib/okf/parse-index.ts, parse-log.ts | UNSWEPT — the census adds a log.md line whose parser this spec did not read; verified in {427.9}, not assumed |
test_producer_validator, test_l_records_source, test_producer_bundle_writer,
test_producer_iri_projection, test_producer_enrich, test_producer_flow_def,
test_producer_frontmatter, test_producer_bi28_bidoutcome_proposal;
__tests__/lib/ontology/concept-schema.test.ts, concept-type-tokens.test.ts.
All CHANGED. Assertion counts unswept (substrate’s own caveat stands); the four
assertions grepped this pass are test_producer_validator.py:449-493 (facet registry) and
concept-schema.test.ts:73 (CONCEPT_TYPE_VALUES), all of which delete with their subjects.
Bundle artefacts
Section titled “Bundle artefacts”| Site | Disposition |
|---|---|
showcase CONFORMANCE.md:35 superset 1 | CHANGED — withdrawn in step (AC 6); coordinate id-431 |
showcase CONFORMANCE.md:41 superset 3 (ontology.json) | NOT THIS TASK — id-430 ruled the artefact, id-431 rules the file |
showcase ontology.json | REGENERATED overlay-only on the next run |
| the five type-named directories | KEPT UNCHANGED [C2] — they are grain constants, not a type materialisation |
Explicitly not swept
Section titled “Explicitly not swept”.claude/worktrees/agent-* stale producer copies; supabase/migrations; specs/id-132-*
DR-141 propagation. All three carried forward from the substrate, all three still open.
4. Migration and compatibility — the first post-inversion run
Section titled “4. Migration and compatibility — the first post-inversion run”Measured surface: 19 concept files across five directories plus case-studies/won-bid/
(RESEARCH M11).
| Question | Answer |
|---|---|
| Do the five directories move? | No. They are grain constants and the grains keep them (§2.4). |
| Does any existing file move? | No. RunSummary.moved stays empty. The won-bid change is to identity, not to the physical path (§2.5). |
| Do the 19 concepts re-draft? | Yes, once — ConceptKey gains grain and loses/renames workspace_id, and cocoindex fingerprints every field unconditionally (l_records.py:135-145). This is a memo miss, not a move. |
Does any concept’s type change? | No for all 19 — each grain keeps the label it already emits. reference concepts change type, and none is shipped (M11: no references/ directory exists). |
| What is new on disk? | documents/, and questionnaire-responses/ / unattributed-answers/ if the corpus has residue. Every new file lands in RunSummary.added, which is the correct report. |
What changes in ontology.json? | base disappears; overlay: null stays. |
What changes in context.jsonld? | No published instance exists to change (M11 / substrate §4). |
Does CONFORMANCE.md change? | It is hand-authored and producer-preserved (bundle_writer.py:186-202); superset 1’s withdrawal is a manual edit in {427.13}. |
RunSummary.moved semantics are unchanged and stay caller-supplied (bundle_writer.py:1172-1180
— a move cannot be inferred from a flat path diff). Nothing in this task supplies one.
5. Memo, fingerprint and DR-060
Section titled “5. Memo, fingerprint and DR-060”ConceptKey is the @coco.fn(memo=True) key and every field fingerprints unconditionally
(enrich.py:863-871, l_records.py:135-145). This task changes the field set twice
(grain added, workspace_id renamed) and changes the output shape (template drafts,
confidence: no-content). Per DR-060 that is a manual version= bump on
enrich_concept, recorded in the bundle log.md at the producer run — one bump for the
whole wave, not one per subtask. grain is added immediately after concept_type and the
“keep new fields last for positional compatibility” convention (l_records.py:253-258) is
explicitly retired for this wave: every construction site is being rewritten anyway, and a
silent positional shift is worse than a compile-time break.
6. UNDECIDABLE (carried verbatim)
Section titled “6. UNDECIDABLE (carried verbatim)”TQ-1 — RULED (S546), amended (S548), EXECUTED by {427.14} (S550). No longer open.
“What live document requires the bundle to declare a vocabulary at all, now that
ontology.json.base retires and context.jsonld loses its concept-type dimension — or is
the remaining entity_types/relationship_types projection surviving on DR-027’s
platform-repo half, which says nothing about what is asserted to consumers?” — DR-027’s
S546 amendment is explicit that base CVs “are simply no longer asserted to bundle
consumers”, yet context.jsonld continues to assert two of the three dimensions to exactly
those consumers. This spec keeps them because DR-141 scopes only the concept-type
vocabulary and unasked-for retirement is not this task’s to take. The question belongs to
id-430’s ruling family, and it is not closed.
Resolution. RULED S546 (owner-delegated): overlay-driven emission only; the base-only projection retires. The ruling’s stated ground — “no consumer” — was measured FALSE at S548 (
bundle-graph.tsrenderedcontext.jsonldinto a node border colour) and the ruling was re-grounded on DR-027’s other and stronger ground; see DR-027’s S548 amendment, which exists so the “no consumer” claim is never re-derived. Executed S550 by {427.14}, which also repaired the channel the artefact fed (re-sourced ontoontology.json’soverlay.concept_types) rather than leaving it vestigial.TQ-1a — still OPEN, carried verbatim in
tasks/id-427.md{427.14}. Which of the ruling’s two emission readings is meant (artefact not written at all on a no-overlay run, vs always written carrying only overlay terms). {427.14} shipped the second as a REVERSIBLE DEFAULT, not as an answer: it could not name a requirement whose current source discriminates them, since both satisfy “declare the client’s overlay vocabulary” vacuously when there is no overlay. OV-10 does not settle it by analogy — that precedent rests on a consumer that distinguishes present-and-null from absent, and no reader distinguishes an empty@contextfrom a missing file.
TQ-2. “If a future RDF/JSON-LD export needs a dereferenceable rdf:type IRI per
concept, what mints it once concept-type labels are unregistered — and does that requirement
exist today, with a current source?” — DR-082 ratifies the namespace authority; nothing
found this pass names an export that consumes it. §2.10 drops the projection on the ground
that minting an IRI for an unregistered label asserts registration. If the requirement is
later shown live, the honest re-entry is a per-concept IRI derived from the concept’s own
bundle path, not a vocabulary declaration.
TQ-3. “Should a residual questionnaire_response concept carry BI-28 form-instance
provenance the way a won-bid proposal does, and what is the current source for the rule that
decides?” — BI-28’s “never an unattributed proposal” argues yes; the owner’s S546 caveat
that “the entire procurement functionality is under review as it’s clear that there is
confusion and duplication currently” argues for touching nothing. Not stamped by this
spec. Mirrors PRODUCT PQ-3.
TQ-4 (carried from PRODUCT PQ-2). “On the day id-422’s publication gate lands and an
empty-scope_tag published pair becomes impossible, is the residual cascade’s defensive
branch retired or kept permanently?” — the S546 ruling says “enumerated defensively until
that gate lands” and stops there. Kept behind one grain-registry entry so either answer is
a one-line change.
7. What this spec did not cover
Section titled “7. What this spec did not cover”- No producer run and no database query. Every SQL shape in §2.1 is specified, not executed; the anti-join plans are unmeasured against a real corpus.
parse-index.ts/parse-log.tsinternals — the census adds alog.mdline and this spec did not read the log parser. {427.9} verifies rather than assumes.- Test bodies beyond the four assertions in RESEARCH M3/M4.
- The vendored OKF SPEC was not re-read; id-426’s S546 delta note is the sole authority for every v0.2 claim here.
.claude/worktrees/agent-*,specs/id-132-*DR-141 propagation — unswept, carried from the substrate.- id-429’s own surfaces —
IndexTheme/build_index_themes/theme_config/unthemed_headingare named in the disposition table as retired by id-429 and are deliberately not dispositioned here, though both tasks editbundle_writer.py.
S546 post-authoring rulings — the five carried questions (owner-delegated)
Section titled “S546 post-authoring rulings — the five carried questions (owner-delegated)”The owner reviewed the spec report and delegated these five to staff-engineer judgment (S546). Rulings, each with its ground:
- PQ-1 — RULED: document existence is disclosable; un-admitted content is not. The
bundle is the client’s own repo, and every existing concept already cites
source_documentsby pointer — a residual concept adds no new disclosure class. Rider binding the residual-template subtask: the template renders only admission-surface metadata (title/path/dates/counts — the same fields citations already expose) and never quotes content that has not passed the knowledge-admission gate (R2). Onward-sharing risk is governed by the client’s own sharing decision, exactly as it already is for every concept. - PQ-2 / TQ-4 — RULED: retire the defensive branch when id-422’s gate lands. The invariant moves to the DB CHECK (enforced by construction at the write layer); an unreachable branch kept as belt-and-braces is the exact DR-139 pattern this programme retires. Routed: an AC added to id-422 — the gate-landing change removes the branch and its test asserts the CHECK instead. The residual complement needs no special case; anti-join coverage is unaffected.
- PQ-3 / TQ-3 — RULED: yes — carry
form_instance_idprovenance on residualquestionnaire_responseconcepts. The attribution key IS the form instance (cascade step 2), so the value is already in hand; BI-28 is ruled live (QC3-A); two grains attributing by the same key carry the same provenance shape. - TQ-1 — RULED:
context.jsonldfollowsontology.json’s F6 fate — overlay-driven emission only; the base-only projection retires. The same principle applied uniformly: a consumer-asserted vocabulary artefact with no consumer and no requirement source is not emitted. The entity/relationship vocabulary remains platform-internal (DR-027’s unchanged half). Supersedes {132.44}‘s base-only-plus-advisory behaviour (pre-launch schema/behaviour change per DR-139). Folds into PLAN’s F6 subtask. - TQ-2 — RULED: no live requirement; defer, documenting the door. RDF export is a
benefit note in DR-027, not a requirement with a current source. If it materialises,
a label type mints a deterministic IRI via the existing
slug()under the DR-082 namespace — the mechanism needs no registry. Nothing is built now.
Also executed: the DR-141 rider (coverage ≥1, not partition) — recorded on the register.