Skip to content

S528 census — Lane 1: taxonomy + classification

Lane 1 — Taxonomy + Classification (code + database surfaces)

Section titled “Lane 1 — Taxonomy + Classification (code + database surfaces)”

ROUND 3. S528 census. HEAD c3286753f, Platform staging rbwqewalexrzgxtvcqrh. Every line number opened by hand; every count is a direct SQL measurement.

Revised under GROUNDING §0.5b: a task directive is evidence of intent at that time, never of correctness. Round 2 used id-144 OD-3 as authority. That reasoning is withdrawn. Every verdict below is re-derived from the client’s reported need, measured behaviour, or a DR that explicitly ruled on the question asked — and §2 lists exactly which verdicts were resting on a task file.


1. The search-filtering verdict, re-derived from requirement and behaviour only

Section titled “1. The search-filtering verdict, re-derived from requirement and behaviour only”

Item 2’s failure is under-return: five questions came back “no relevant content” when the answers demonstrably existed in the source libraries. The client names four causes. Three of the four are over-exclusion by a taxonomy facet:

CauseMechanism blamed
1search_qa_library only searches content typed as q_a_pair. A lot of the LMS source content is structured as feature/functionality entries (FUNC-XXX) … and so excluded”content_type exclusion
2semantic ranking misses when terminology differs (PCI-DSS vs “payment providers”) — ranking, not taxonomy
3default limit of 5 hides results — limit, not taxonomy
4”when I tried domain-filtering to security it returned good content but excluded product-feature and implementation entries which is where a lot of the LMS-specific answers live”domain exclusion

The underlying requirement, stripped of proposed solutions per GROUNDING §0.2: the answer that exists in the corpus must be retrievable. Every taxonomy facet in the retrieval path made that worse in the client’s hands. None of the four causes is “we couldn’t narrow enough”.

Measured on staging. These are mechanism properties, not population artefacts — §1.3 separates the two.

FacetMeasured behaviour
filter_subtopicThe q_a_pairs arm projects NULL::text AS "primary_subtopic" (20260710221255_…:250). The migration’s own comment at :241-243: “no subtopic on this grain so a non-null filter_subtopic naturally excludes answers.” Any non-null subtopic filter excludes 100% of the answer grain, at any corpus size.
filter_subtopic, corpus-wideOf the 70 source_documents carrying a real domain, 69 have primary_subtopic = 'unclassified'. Exactly one row in the entire corpus carries a real (domain, subtopic) pair: corporate/company-info.
filter_domain, answer grainq_a_pairs inherit domain via COALESCE(rl.domain,'unclassified'). Measured: 112 of 118 pairs are 'unclassified'; the other 6 carry test-fixture garbage (BI16id144-…-domA). Zero q_a_pairs carry a real taxonomy domain. Any non-null domain filter therefore returns zero answers.
filter_domain, semanticsMatches primary_domain only — single-valued, NOT NULL DEFAULT 'unclassified'. An item whose primary domain is product-feature is invisible to a security filter however security-relevant it is. That is cause 4, exactly, reproduced in the schema.

1.3 Which of those is structural, and which is population

Section titled “1.3 Which of those is structural, and which is population”

I am not repeating trap 2. Separating them honestly:

  • Structural at any population, no caveat: the subtopic projection is NULL on the answer grain by construction. Ten thousand pairs would change nothing.
  • Structural in mechanism, illustrated by the numbers: the answer grain has no independent domain. It inherits one hop from a source document, and the NOT NULL DEFAULT 'unclassified' sentinel guarantees that anything unclassified — or with no linked source document — lands in a bucket that every non-null filter excludes. More rows do not fix that; they enlarge the excluded bucket. The counts illustrate the mechanism; they are not the argument.
  • Genuinely population-dependent, and I make no claim from it: whether ‘unclassified’ would shrink under a real corpus walk. It might. That would not touch either point above.

1.3b Second requirement source — the O1–O9 outcome inventory

Section titled “1.3b Second requirement source — the O1–O9 outcome inventory”

specs/id-71-ai-tooling/research-inputs/lane-a-workflow-inventory-strawman.md (2026-06-09, 550 lines). Requirement-side, not a task directive — its stated unit of analysis is “the concept/outcome, not the tool name … verdicts attach to outcomes, not to tool names” — so §0.5b does not disqualify it.

Status, weighed honestly and both ways. It self-describes as “WORKSHOP STRAWMAN — this document proposes; it does not decide”, every verdict cell deliberately empty, assumptions marked [VALIDATE]. It is content_items-era (W1.1/W1.3/W7.1/W2.x all name content_items, dropped at ID-131 M6). And its Data: lines are inventory shorthand, not exhaustive specs — so absence here is weaker evidence than absence in the gap tracker, which is a line-item action tracker. Evidence of intended outcomes; not a ruling. I am not swapping one false authority for another.

Complete occurrence audit across all 550 lines — every hit, no selection. Terms: domain, subtopic, scope_tag, taxonom.

LineWorkflowTextWhat it is
:191W1.2 (O1, rank 1)Data: `q_a_pairs` (corpus-level, `scope_tag`), `question_matches`, `citations` scope_tag named as the answer-grain retrieval property, in the #1 outcome
:342-343W6.1 (O6)“coverage by domain/template → gaps ranked”; Data: coverage matrix, gaps, taxonomyThe coverage matrix — DR-034 RETIRED
:359W7.1 (O7)“natural-language search (no taxonomy knowledge assumed) — and the doc calls this the test case for ‘you don’t need to remember the tools — just ask‘“An explicit requirement AGAINST taxonomy-dependent retrieval
:365,:367W7.2 (O7)“gather claims citing his domain”; Data: `content_items` by domain/entityThe one genuinely ambiguous hit. “his domain” = Tom’s specialism; could mean primary_domain or subject area generally. Paired with “/entity” (a different mechanism). A claims-audit workflow, not retrieval filtering
:375-376W8.1 (O8)“resolve content by subtopic_filter + expected_layer; Data: guide_sections, content_items, taxonomy layersThe guides anchor, named at column level — direct corroboration of §6
:380W8.2 (O8)“research-layer content arrives in guide domain”guides.domain_filter — guides again, not search
:404W9.1 (O9)Data: company profile, taxonomy priorsOnboarding seeds a taxonomy

Result, stated plainly because it cuts against my round-2 position: across all nine ranked outcomes, domain/subtopic filtering is named as a retrieval mechanism in zero workflows. O1’s three retrieval workflows name content_items, chunks/embeddings, citations, q_a_pairs (corpus-level, scope_tag), question_matches, entities/entity_mentions and ontology CVs — and no domain or subtopic filter. The taxonomy hits land on coverage (retired), guides (twice — my anchor), onboarding priors, and one ambiguous claims-audit line.

And the one retrieval-side mention is a negative requirement: O7’s cold-start test case demands “no taxonomy knowledge assumed”.

This is a second, independent requirement source agreeing with the first. §2.3 found the gap tracker silent on subject classification outside guides; the outcome inventory names guides at column level and never names domain filtering. Two requirement-side documents, same shape.

Also confirmed (the lead’s point 2): W1.1’s “two-step retrieval (metadata preview → verbatim fetch on accept) → answer with citations + confidence posture” is client Item 3’s second ask — strict extraction, cite the source, refuse when no source exists. The scope/citation family is the retrieval requirement the #1 outcome actually describes, and that holds independently of the taxonomy question.

Domain/subtopic filtering as implemented increases the failure the client reported. It is an exclusion predicate over a subject space where the answer grain has no values, sitting in front of a corpus whose retrieval problem is recall. On the evidence available without citing any task file, it does not serve Item 2 — it is a mechanism-level instance of Item 2.

Against that, the genuine value domain filtering delivers today: I looked for one and found narrowing for a user who already knows the subject and wants less noise — a real but unevidenced benefit. No client item requests it. No gap-tracker item requests it (§2.3). No O1–O9 workflow names it (§1.3b). And O7’s cold-start workflow requires the opposite — “natural-language search (no taxonomy knowledge assumed)”.

Is the client’s Item 2 sentence settleable? The lead asked directly. “Either the filter logic needs to be more permissive or the ingest needs to apply secondary tags” — read alone, it cannot be settled in either direction; it is a user proposing fixes, and both readings (presupposes the mechanism / works around a mechanism that did not fit) are available. But it does not stand alone. The surrounding context settles it: the sentence sits as the fourth named cause of a failure, in a list where three of four causes are over-exclusion by a taxonomy facet, and the client’s own framing of the exercise is “pull existing approved answers” — a recall goal. Read in its paragraph, it is a workaround report, not an endorsement. Medium confidence on that reading; the sentence in isolation genuinely is ambiguous, and I am not resting the verdict on it — §1.2’s measurements and §1.3b’s independent silence carry the weight.

TargetVerdictConf.EvidenceOwner action
hybrid_search filter_subtopicRETIRE-CANDIDATEhighExcludes 100% of the answer grain by construction; 1 row corpus-wide carries a real subtopicOwner ruling — see §1.5 conflict
hybrid_search filter_domainRETIRE-CANDIDATEmediumReturns zero answers; single-valued match reproduces client cause 4as above
MCP find scope enum (lib/mcp/tools/search.ts:58-80)falls with the abovemediumPurely derivative — it advertises the domain axis to the AI consumerdies with filter_domain if that goes

RETIRE-CANDIDATE, not RETIRE — because §1.5 is a genuine conflict I am not entitled to resolve.

1.5 THE FINDING — requirement and task directive point different ways

Section titled “1.5 THE FINDING — requirement and task directive point different ways”

Stated as a finding, not a resolution, per GROUNDING §0.5b:

  • The requirement evidence (§0.2) points at retiring domain/subtopic filtering. Item 2 reports it over-excluding; the measured mechanism over-excludes structurally.
  • id-144 OD-3 points the other way. It reads “Wire kind/domain/subtopic/date filters server-side”, it shipped (12 args live), and the task cites kh-client-feedback.md 'Item 3' by name — so its author had the client evidence in hand and wired the filters anyway.

I no longer claim the second settles it. But I note the conflict is sharper than “someone decided this once”: id-144 read the same client feedback this census is reading and reached the opposite conclusion. Either it re-derived and I cannot see the reasoning, or it wired the filters because they were the available mechanism. The task text does not distinguish those. This needs an owner ruling, and it is the single highest-value question in my lane.

Where the evidence now stands, after §1.3b. Two independent requirement-side sources — the gap tracker (silent on subject classification outside guides, §2.3) and the O1–O9 outcome inventory (names guides at column level, never names domain filtering, and requires “no taxonomy knowledge assumed” for cold-start retrieval) — agree with the measured behaviour in §1.2. One task directive points the other way. That is the whole of the disagreement.

I am still not resolving it, and I want to be explicit about why rather than hedging. The lead has twice pushed the superseded read and asked me not to comply toward it, so the bar for me to flip to RETIRE is higher, not lower. Three things hold me at RETIRE-CANDIDATE: the outcome inventory is a self-declared strawman whose Data: lines are shorthand, so its silence is suggestive rather than dispositive; the Item 2 sentence is ambiguous in isolation (§1.4); and id-144’s author had the client evidence and chose otherwise, which is a fact I cannot explain away. The evidence has moved substantially toward retirement and I say so — but it is the owner’s ruling, not mine.

One round-2 argument I should retire explicitly. I framed domain and scope as “complementary axes, therefore both wanted”. The semantic distinction survives on its own reasoning (§1.6) — they answer different questions. The “therefore both wanted” half was propped by id-144 and does not survive §0.5b. Two axes being semantically distinct says nothing about whether both are required.

1.6 Could scope filtering serve what domain filtering is being asked to serve?

Section titled “1.6 Could scope filtering serve what domain filtering is being asked to serve?”

Genuinely answered, not steered.

Partly — and for the part it cannot serve, nothing filter-shaped can.

The two facets answer different questions: domain asks what subject is this about; scope asks what system or premises does this describe. Splitting the client’s needs by that line:

  • Item 3 (safety / precision). Scope serves it; domain provably cannot. The Bitdefender entry is correctly classified under security/malware — a security-domain filter would include it. Only a scope boundary (internal-it vs production-infrastructure) excludes it. So the wrong-context failure the client called the most damaging is structurally out of domain filtering’s reach.
  • Item 2 (recall). Scope does not serve it, and neither does domain. Recall failures are ranking (cause 2), limit (cause 3) and grain coverage (cause 1). No narrowing predicate improves recall; both can only reduce it.

The asymmetry that matters: scope is an exclusion predicate over a small closed boundary set — it removes items that declare they do not apply. Domain is a narrowing predicate over a large open subject space — it removes everything that has not declared the target subject, which on this corpus is nearly everything. That is why one is recall-safe and the other is not.

So: domain filtering is currently being asked to serve a safety need it cannot serve, while damaging the recall need it directly harms. If retrieval effort goes anywhere, the evidence points at scope — §4. That is not a recommendation to build scope filtering instead; it is the observation that Item 3’s mechanism is absent while Item 2’s blamed mechanism is present.


2. Audit — which verdicts were resting on a task file

Section titled “2. Audit — which verdicts were resting on a task file”

The lead asked for this explicitly. Full pass over every round-2 verdict.

VerdictRested on a task file?Round-3 disposition
Search filter_domain/filter_subtopic KEEPYES — id-144 OD-3, load-bearingREVERSED to RETIRE-CANDIDATE + surfaced conflict (§1)
MCP find scope enum KEEPYES — derivative of the abovefalls with it (§1.4)
record_lifecycle.domain / governance KEEPNO task file — but no requirement either. I asserted “governance domain policy” from code (quality-score cron reads governance_config.domain). That is consumer-counting, trap 1DOWNGRADED to UNDECIDABLE (§3)
secondary_domain/secondary_subtopic WIRENo — client Item 2 verbatimNARROWED — I over-read the client’s solution as the requirement (§5)
Guides: domain_filter/subtopic_filter/content_type_filter/expected_layer KEEPNo — B.1/B.2/B.4/B.6, owner decisions recorded in a requirements doc, plus a shipped migrationUNCHANGED — strongest anchor in the lane (§6)
taxonomy_domains/taxonomy_subtopics tables KEEPIndirectly — its support included the search KEEPNARROWED to a guides-only anchor (§6)
review_assignments filters UNDECIDABLENounchanged — this was the right shape
coverage_targets RETIREtasks/id-373.md cited only as “tracked”not load-bearing. The anchor is DR-034, a DR that explicitly ruled on this exact featureUNCHANGED
scope_tag findingsNo — client Item 3 + measured counts + mechanical reasoningUNCHANGED (§4)
§7 delete list (3 dead modules, coverage_gap, platforms, classification_disputes, classification-quality route)No — zero-caller measurements, the files’ own comments, owner rulingsUNCHANGED
Sync machine dead since 28/04, bid FK drift, five-copy duplication, mirror 1:1, 01-vision diff, classification promptNo — all measuredUNCHANGED
(round 2) “domain and scope are complementary axes, therefore both wantedYES — the “therefore” half was propped by id-144SPLIT — the semantic distinction survives on its own reasoning (§1.6); the “both wanted” inference is withdrawn (§1.5)

Two verdicts reversed, two narrowed, one downgraded, one split. Everything measured survived.

I grepped all 497 lines of kb-hub-gap-analysis-action-tracker.md for domain|taxonom|classif. One hit, and it is the word “domain” meaning domain-knowledge about plugin-reuse policy. Combined with the client feedback, where domain filtering appears only as a reported failure: across both requirement documents there is no requirement for subject-domain classification at all, except inside the guides feature (§6). That is the most load-bearing negative finding in this lane, and I could not have stated it in round 2 because I had not looked for absence.

Now corroborated by a third document. §1.3b’s complete occurrence audit of the O1–O9 outcome inventory finds the same shape independently: domain/subtopic filtering named in zero ranked-outcome workflows; taxonomy appearing only under coverage (DR-034-retired), guides (twice, at column level), onboarding priors, and one ambiguous claims-audit line. Three requirement-side sources, none of which asks for subject classification outside guides.


3. record_lifecycle.domain and per-domain governance — DOWNGRADED

Section titled “3. record_lifecycle.domain and per-domain governance — DOWNGRADED”

Round 2 kept this on the strength of a live trigger and a live cron. That is trap 1: the mechanism exists, which is not evidence a requirement chose it.

What exists (measured): trigger trg_record_lifecycle_domain_sync syncs record_lifecycle.domain from the owning source_documents.primary_domain. The scheduled weekly quality-score cron (vercel.json 0 5 * * 0) reads governance_config and builds per-domain quality thresholds + auto-flag policy (app/api/cron/quality-score/route.ts:99-130).

What I could not find: any requirement, in either requirements document, for governance policy to vary by subject domain. B.5 (Document Control) is the recorded owner decision in this area and its axis is timenext_review_date, review_cadence_days, recurring review — not domain.

And the same structural defect applies: with 112 of 118 pairs and 49 of 85 source documents at 'unclassified', per-domain thresholds collapse to DEFAULT_THRESHOLD for the bulk of the corpus, because the sentinel default guarantees a large bucket no governance_config row will ever be authored for.

TargetVerdictConf.EvidenceWhat would settle it
record_lifecycle.domain + governance_config.domain policyUNDECIDABLELive trigger + scheduled cron, but no requirement in either requirements doc; B.5’s axis is time, not domainOwner: should governance thresholds and auto-flagging vary by subject domain, or is document-control cadence (B.5) the whole of the governance requirement?

4. scope_tag — the client’s evidence is unambiguous, and the mechanism is half-built

Section titled “4. scope_tag — the client’s evidence is unambiguous, and the mechanism is half-built”

Unchanged from round 2 and confirmed by the lead as standing on its own. Restated compactly.

Item 3’s four asks against measured state:

Client asked forBuilt?
“a mandatory scope tag on every content item”NO — 9 of 118 pairs carry one; not mandatory, not defaulted
”surfaced in every search result”YES — projection, hybrid_search col 22
Filtering / excluding by scopeNO — there is no filter_scope_tag argument. The function’s own COMMENT separates them: scope_tag is a projection; the five filters are kind/domain/subtopic/date
”the Bitdefender entry should carry an explicit ‘does not apply to: production infrastructure’ flag”NO — the anti_scope_tag column exists; 0 of 118 rows carry one

The producer dead-ends at promotion. flow.py writes q_a_extractions.scope_tags (:1237,2352,3398-3438); lib/q-a-pairs/promote-corpus.ts:770 states “answer_advanced / scope_tag / anti_scope_tag have no extraction source and are NOT in the payload.” The only writer reaching q_a_pairs.scope_tag is the manual PATCH at app/api/q-a-pairs/[id]/route.ts:72-89.

Net: the exact failure the client called the most damaging is not prevented today.

4.1 Taxonomy bleed threatens the scope mechanism’s correctness — mechanically

Section titled “4.1 Taxonomy bleed threatens the scope mechanism’s correctness — mechanically”

The client’s scope vocabulary is a premises/system boundary: internal-it, production-infrastructure, application-layer, office-physical, data-centre-physical.

The four values actually present: data-protection, internal-it, quality-management, social-value. Only internal-it is a boundary. data-protection is a taxonomy_subtopics name (verified by join against the live table).

Scope filtering works by exclusion. An item tagged with a subject has a non-empty scope_tag — so it looks scoped to any presence check — while carrying no boundary at all. It will not be excluded from any scope. Subject values in this column do not dilute the signal; they manufacture false negatives in the exclusion path, which is precisely the Bitdefender failure mode the column exists to prevent.

Two vocabularies with opposite meanings share one untyped text[], and nothing rejects the mix. This is the strongest argument in the lane for enforcement, and it is independent of every verdict above.


5. secondary_domain / secondary_subtopic — narrowed

Section titled “5. secondary_domain / secondary_subtopic — narrowed”

Round 2 said WIRE, citing Item 2. I over-read the client’s proposed solution as the requirement, which GROUNDING §0.2 warns against: “it’s not the approach outlined in the docs that’s important, but the underlying requirement.”

The requirement is: security-adjacent product content must be reachable from a security search. Item 2 offers two candidate mechanisms — “either the filter logic needs to be more permissive or the ingest needs to apply secondary tags” — and a third exists that the client did not name: do not filter by domain at all (§1).

These are not independent. Secondary tags only matter if domain filtering survives §1.5. If it is retired, the requirement dissolves into ranking and the id-162 keyword facet, and secondary tags have no consumer.

TargetVerdictConf.EvidenceOwner action
source_documents.secondary_domain / .secondary_subtopicCONDITIONAL on §1.5highClient Item 2 names them as one of two candidate remedies for reachabilityIf domain filtering is retained → wire (4 steps below). If retired → these columns have no consumer and go with it

If retained, the wiring is four changes and no migration: add both fields to the ClassificationExtraction Pydantic model (extraction.py); extend the Python prompt (prompts.py) — note the TS skill already asks for a secondary pair (lib/ai/skills/classification.md:16-18), so this is Python catching up, not new design; thread them through flow.py:2178-2182, deleting the “no Path-A source today” comment; and make filter_domain match either column — without that last step, populating them changes nothing, and it is a behaviour change to a shipped RPC.

Round-2 correction retained: flow.py’s omission reason is “no Path-A source today” — an absence, not a design ruling. So the read-never-written discriminator does point at writer missing. That part was right; the scope of the conclusion was not.


6. What survives on requirement evidence — and it is narrow

Section titled “6. What survives on requirement evidence — and it is narrow”

Guides, and on current evidence only guides.

AnchorWhat it specifies
B.1 (recorded “Liam decision: Option (ii)”, spec pending)Layer becomes a UI nesting rule; extends get_guide_content to return layer-grouped buckets — built on guide_sections.expected_layer
B.2 (“Liam decision: Align naming for ALL sections + add ALL missing”)Next-action names the insert shape verbatim: “(section_name, expected_layer, display_order, subtopic_filter, content_type_filter)”, and “Impact on existing subtopic_filter wiring (S189 WP4 populated 14 subtopics across the 19 existing sections)“
B.4 (“Liam decision: Required”)“child sections inherit parent’s subtopic_filter
B.6SHIPPED — migration 20260422174117_add_research_feed_to_product_guides.sql, expected_layer='research'. Measured, not intended
W8.1 (O1–O9 inventory, §1.3b)“resolve content by subtopic_filter + expected_layer; Data: guide_sections, content_items, taxonomy layers. A second, independent requirement source naming the same columns

These are owner decisions recorded in a requirements document, a shipped migration, and an outcome-first workflow inventory — §0.2 evidence, not task directives. The distinction is the whole point of §0.5b.

This anchor got stronger this round while every other one weakened. The guides columns are now the only taxonomy mechanism named by two independent requirement-side sources, at column level, in both.

TargetVerdictConf.EvidenceOwner action
guides.domain_filter, guide_sections.subtopic_filter / content_type_filter / expected_layerKEEPhighB.1/B.2/B.4 owner decisions + B.6 shippednone — build the pending specs
taxonomy_domains / taxonomy_subtopics tablesKEEP the vocabulary; REWRITE the bindingmediumThe guides anchor requires a domain/subtopic/layer vocabulary to exist. It does not require the other consumers, and §2.3 found no requirement for subject classification outside guidesKeep the tables; §8 for the binding. If §1.5 retires search filtering, the surviving footprint is guides + the ontology CV register, which is much smaller than the current spread

Honest statement of the narrowing: round 2 offered four anchors. Two (search filters, governance) have fallen to RETIRE-CANDIDATE and UNDECIDABLE. One (secondary tags) is conditional on the first. Guides is the only unconditional requirement anchor left, and after §1.3b it is the only one with two independent sources. The owner’s starting position — that the taxonomy tier is IMS-inherited and the Layer-1 concepts are known-incorrect — survives this round considerably better than my round-2 report implied.

What that implies for the vocabulary’s shape, flagged not asserted. If guides is the whole requirement, then what is needed is a controlled vocabulary the guides resolver filters against — which is exactly the Layer-1/Layer-3 CV the ontology register already declares. It is not obviously a per-document LLM classification pass over the whole corpus, nor two bespoke tables replicated into five snapshot copies. That is the “taxonomy as an ontology layer” shape from GROUNDING §4, reached from requirements rather than from doctrine. It depends on §1.5 and §3 and I am not asserting it as a verdict.


7. DELETE NOW — zero ambiguity (all measured; none rested on a task file)

Section titled “7. DELETE NOW — zero ambiguity (all measured; none rested on a task file)”
TargetVerdictConf.EvidenceBlast radius
lib/layer-inference.tsRETIREhighMemtrace get_impact(inferLayer)total_affected: 0. Its retention comment :196-202 names two callers — both gone (app/api/items/ deleted fc326f008; batch-reclassify.ts:944-950 says the inference “is DROPPED here”)its test; the vi.mock at batch-reclassify.test.ts:139; reference-doc-paths.json:28; the kh_code_sources entry in reference/classification-architecture.md
lib/topic-inference.tsRETIREhighget_impact(suggestTopic) → 0. Own docstring :130-141: “always returns null … a follow-up may want to retire suggestTopic outright”its test; 3 comment-only refs in eslint-rules/tests/
lib/guide-section-mapping.tsRETIREhighget_impact(suggestGuideSections) → 0; no non-test reference. The dead library, not the guides feature — §6 keeps guides2 test files
coverage_targets + MCP matrix surfaceRETIREhighDR-034, which explicitly ruled on this feature; S499 adjudicates the MCP surface as in scope, not a carve-out. Route half already executedtable; lib/mcp/tools/apps.ts:47,92-400; lib/mcp/resources.ts:439-490,710-745,950; mcp-apps/coverage-matrix/**; CoverageMatrixData in lib/mcp/formatters/apps.ts + index re-export; lib/mcp/app-bundles.ts constant; generate-api-views.ts:130; export-user-data.ts:543,825,844; 2 test files; scripts/mcp-eval/fixtures.ts
'coverage_gap' residueRETIREhighlib/attention.ts:90-96 keeps it as an explicit “unreachable-but-harmless union member” purely so lib/mcp/tools/review.ts:63 needs no editlib/attention.ts:39; lib/mcp/tools/review.ts:63
platforms vocabularyRETIREhighgenerate-taxonomy-snapshot.ts:111-116: “no live table enforces this CHECK today; retained here for continuity”. Only repo-wide consumer is markdown-parity.test.ts:73, comparing it to itselfgenerator array; snapshot key; fixture CV; test mapping; docs-site ontology/05-platform.md (Lane 3)
classification_disputesRETIREhighOwner ruled. Only consumer is components/provenance/disputes-tab-stub.tsx, a stub; no writertable; the stub; export-user-data.ts + generate-api-views.ts entries
/api/cron/classification-qualityRETIREhighOwner ruled: delete the route. Also absent from vercel.json cronsroute dir; its vercel.json:52 entry. classifyContent’s last non-eval caller goes with it

Do not sweep up: layer_vocabulary (4 rows) — enforced by the validate_layer_key trigger on reference_items.layer, and B.1 and B.6 depend on it. It is the audience/depth axis (sales_brief/bid_detail/company_reference/research), not the OKF L-layer model.


8. Findings independent of every verdict above

Section titled “8. Findings independent of every verdict above”

All measured; none touched by the round-3 correction.

No CV enforcement anywhere. Zero FK, zero CHECK, zero validating trigger on any taxonomy column (pg_constraint + pg_trigger both empty). Measured consequence: source_documents.primary_domain holds 15 distinct values, 12 of them test-fixture garbage (APILAYERid144-…, BI16id144-…). reference_items.layer is the sole counter-example and proves the pattern is available. Per GROUNDING §0.4 the fix in the ratified direction is the never-implemented register → DB sync (scripts/ontology-sync/ contains only parse-flow.py), with CI gated on DB-conforms-to-register — not a repair of the sync trigger the owner has already ruled retired.

The sync machine has been dead since 28 April 2026. taxonomy_sync_state.last_sync_at = 2026-04-28T10:39:45Z; git log --all --grep="automated sync" returns exactly two bot commits, both April. Likely mechanism: lib/taxonomy/sync-trigger.ts:29-42 fetches a relative URL from a Node runtime into a deliberately-swallowing .catch (outcome DB-measured, high; mechanism inferred, medium). Its integration test vi.mocks the function under test.

Five copies of content_type, one of them — sync-plugin-taxonomy.ts:122-175 — still listing all eight values ID-133 BI-3 removed and omitting document, which BI-3 added.

bid form_type drift with a latent FK failure. DB has 7 keys without bid; the snapshot, the frozen fixture and the docs-site CV all still carry it, so Python’s _VALID_FORM_TYPES accepts a value the form_types(key) ON DELETE RESTRICT FKs reject at insert. form-type-parity.test.ts:106-130 compares fixture ↔ snapshot, never the DB.

A parity test whose framing is false. markdown-parity.test.ts:124 names its block “live DB CHECK parity” and asserts the snapshot is DB-derived; for content_types/platforms/ requirement_type it is hardcoded in the generator, and source_documents.content_type has no DB CHECK at all.

Doc provenance. 01-vision.md’s classification language is evidence under investigation, proven by diff: d2b3f5f2 is 15 insertions / 15 deletions, all path re-points or DR-038 noun swaps; §1.1 and §3.2 untouched, §5.2’s only edit "An intelligence workspace tracks""The intelligence application tracks". Corroboration: §3.2 still lists the DR-034-retired coverage matrix. Same failure mode in reference/classification-architecture.md — frontmatter claims kh_last_verified: 27/07/2026, body says 28/04/2026, kh_code_sources still lists the dead lib/layer-inference.ts.

The live production prompt is content_items/bid-era. lib/ai/skills/classification.md:1-8: “You are an expert knowledge base classifier for a UK SMB bid management platform. Your task is to classify content items …”. content_items was dropped at ID-131 M6.

Provenance of the shape. The 'unclassified' sentinel reaches source_documents at 20260628191700_id131_sd_classification_cols.sql:34-35 under that migration’s stated rule — “Column TYPES mirror content_items exactly / “Types mirror content_items 1:1. The port was deliberated (the header lists what was dropped), but mirror 1:1 is type-preservation, not model re-derivation. This is also the direct origin of §1.2’s over-exclusion: the sentinel default is what guarantees a permanently-excluded 'unclassified' bucket.

Classification’s remaining purpose, honestly. Flow A (cocoindex nightly) and Flow B (/api/ingest/url) produce primary_domain, primary_subtopic and content_type. If §1.5 retires search filtering and §3 resolves against domain-keyed governance, the only surviving requirement consumer of those values is guides section routing — which needs a vocabulary, not a per-document LLM classification pass. That would make the classification stage itself a retire-candidate, and I flag it rather than assert it: it depends entirely on §1.5 and §3.


9. What I could NOT cover, and what I need from the owner

Section titled “9. What I could NOT cover, and what I need from the owner”

Three questions, each of which one line settles. I will not guess any of them.

  1. §1.5 — the id-144 conflict. Three requirement-side sources (client feedback, gap tracker, O1–O9 outcome inventory) plus the measured behaviour point at retirement; one task directive — id-144 OD-3 — points the other way, and its author had the client feedback in hand. Should domain/subtopic filtering be retired from hybrid_search? This cascades: it decides §5 (secondary tags), the MCP find scope enum, and — with §3 — whether the classification stage survives at all. (If it helps: the only thing holding me at RETIRE-CANDIDATE rather than RETIRE is that id-144 fact and the strawman’s self-declared non-authority. Everything else has converged.)
  2. §3 — governance. Should governance thresholds and auto-flagging vary by subject domain, or is document-control cadence (B.5) the whole of the governance requirement?
  3. §2 — review assignments. Should a reviewer be assignable to a slice of the corpus by subject, and is domain the right axis? No requirement found in either document.

Coverage limits:

  • The O1–O9 inventory is a strawman. §1.3b weighs it both ways: outcome-first and requirement-side (so §0.5b does not disqualify it), but self-declared as “proposes; it does not decide”, content_items-era, and its Data: lines are shorthand. Absence in an inventory is weaker than absence in an action tracker. I have used it as corroboration, never as the load-bearing evidence.
  • GitNexus still unavailable — index version-broken (Database file version: 42, Current build storage version: 41). §7 rests on Memtrace get_impact plus hand-opened sites. DR-071 makes GitNexus the authority; re-confirm §7 when the index rebuilds.
  • Recall pass run (lock-free FTS, double-quoted terms per §0.6) across taxonomy_domains AND retire, taxonomy AND requirement, secondary_domain OR secondary_tags, domain AND filter AND search. Returned code fragments already in this report; no prior ruling contradicts anything here. Recall and code do not disagree on this lane.
  • Prod DB not queried — per GROUNDING §1 the split carries zero weight.
  • id-364 F1 noted closed; not load-bearing here — I make no claim about that FK.
  • Out of lane: docs-site ontology/** (Lane 3), cited only as requirement evidence; seeding/propagation (Lane 2), with scripts/propagation/payload-contract.ts:79-105 flagged as theirs; test debt (Lane 4).