Skip to content

ID-114 {114.3} TECH - relocate product functionality out of the private docs-site

ID-114 {114.3} TECH - relocate product functionality out of the private docs-site (de-ID + re-home)

Section titled “ID-114 {114.3} TECH - relocate product functionality out of the private docs-site (de-ID + re-home)”

Date: 15/06/2026 Author: Task Planner (FRESH dispatch - Q-PLANNER-2; read {114.1} RESEARCH + {114.2} PRODUCT fresh, NOT the PRODUCT author). Tier: TECH (of a PRODUCT+TECH+PLAN chain). Maps 1:1 to PRODUCT’s 13 PI-N invariants. Predecessors (read in full this dispatch): {114.1} RESEARCH, {114.2} PRODUCT (specs/id-114-docs-site-relocation/). Builds on (do NOT re-implement): ID-95.5 - lib/ai/skills/classification.md skeleton + tenant_config.classificationDisambiguation. Coordinates with (in-flight, do NOT collide): ID-104 / ID-71.14 on the id71-id104 branch.

Governing principle: “the private docs-site must host docs ONLY - no product functionality.”


This Task re-homes product/coupled content currently mis-homed in the private knowledge-hub-docs-site repo. PRODUCT defines the 13 PI-N behaviour invariants; this TECH grounds each in current file:line reality and supplies the migration plan. The single most consequential finding of this dispatch re-frames the harness relocate-set: the canonical eval infrastructure already exists in MAIN - the docs-site harness/ eval slice is a stale duplicate/fork, so PI-3/PI-4 are predominantly a delete-the-duplicate operation, not a lift-and-move.

Code-intelligence orientation (cited verbatim, this dispatch)

Section titled “Code-intelligence orientation (cited verbatim, this dispatch)”

GitNexus index is STALE (last indexed 69b7bd0); per the dispatch brief I preferred ast-dataflow + grep for exact file:line and grep-swept the .md/.py/.sh surfaces GitNexus/ast-dataflow do not cover.

  • gitnexus query (auto-surfaced for resolveEvalFixture): “resolveEvalFixture (lib/eval/fixtures.ts) Called by: main, main, main; Calls: resolvePrivateDocsDir” - confirms the gold-standard fixture bridge is the only graphed coupling and its three callers are the scripts/eval-*.ts runners’ main. No graphed runtime flow for the classification-prompt load path (build-time codegen only - matches PRODUCT).
  • grep (TS/MD/SH/PY sweep) established the duplicate-fork reality below; cited inline.

Grounded current state (MAIN = knowledge-hub, DS = knowledge-hub-docs-site)

Section titled “Grounded current state (MAIN = knowledge-hub, DS = knowledge-hub-docs-site)”

The eval infra already lives in MAIN (canonical), duplicated in DS harness/ (stale fork):

AssetMAIN (canonical, present)DS harness/ (stale fork)
Runnersscripts/eval-{classification,entity-classification,procurement-drafting,summarisation,search}.tsharness/scripts/eval-*.ts
Vitest eval suites__tests__/eval/{classification,entity-classification,procurement-drafting,summarisation,search}-eval.test.tsharness/__tests__/eval/*-eval.test.ts
Public de-ID’d fixtures__tests__/fixtures/eval-gold/{classification,entity}-*.json (+ entity-*.README.md)harness/__tests__/fixtures/{classification,entity}-*.json
Metric baselines__tests__/fixtures/eval-baselines/{classification,entity-classification,search,summarisation}.baseline.jsonharness/__tests__/fixtures/eval-baselines/*.baseline.json
Shared eval liblib/eval/{metrics,baseline,reporter,types}.ts; lib/ai/pricing.ts; lib/anthropic.tsharness/lib/eval/*; harness/lib/ai/pricing.ts; harness/lib/anthropic.ts (the fork)

diff -q confirms MAIN scripts/eval-classification.ts and DS harness/scripts/eval-classification.ts differ by exactly one structural line: MAIN line 31 import { resolveEvalFixture } from '../lib/eval/fixtures' (plus canonical ../lib/ai/pricing, ../lib/anthropic) vs the harness fork resolving fixtures locally against ../lib/*. MAIN imports resolve to the real modules; the harness fork’s ../lib/ is the workaround copy.

The latent --live break (PI-4) lives in the DS fork, not MAIN. harness/lib/ contains ONLY: anthropic.ts, ai/pricing.ts, eval/{baseline,metrics,reporter,types}.ts. It does NOT contain ai/classify.ts, ai/skills/loader.ts, client-config.ts, or ai-parse.ts. So harness/scripts/eval-classification.ts:383 and harness/scripts/eval-entity-classification.ts:309 (await import('../lib/ai/classify')) resolve into a directory that lacks the module -> a --live module-not-found. MAIN’s scripts/eval-classification.ts:383 / eval-entity-classification.ts resolve to the real lib/ai/classify.ts and are not broken.

PI-4 5-runner scope CORRECTED (carried nit 1). Of the five runners, only TWO import ai/classify: eval-classification.ts and eval-entity-classification.ts (grep -c "ai/classify": classification=5 refs/1 import, entity=1 import; procurement-drafting=0, summarisation=0, search=0). The other three import only ../lib/eval/* + ../lib/anthropic - all present in the fork, so they are NOT subject to the --live classify break. The “all 5 harness eval runners” framing in the carried nit is wrong; the import-break surface is exactly 2 runners (both in the fork being deleted).

KEEP slice inside harness/ (PI-12). harness/__tests__/{workflows,skills,actions,scripts,dependencies,decommission}/* and harness/scripts/{docubot,skills}/* test docs-site operators (docubot/run-skill/SEO-audit) and are wired into DS CI (harness/.github/.../*.yml use working-directory: harness; harness/.github/workflows/ci.yml runs bunx vitest run). These are NOT the eval duplicate and stay.

The classification-prompt build bridge (PI-8/9). ops/classification-prompt.md (1877 L; 24 “Phew” occurrences, worked-examples L194-809; TAXONOMY markers at L33/L255). Two build-time call sites resolve it via the KH_PRIVATE_DOCS_DIR bridge (lib/private-docs.ts:resolvePrivateDocsDir, fail-loud Inv 29):

  • scripts/generate-classification-prompt-taxonomy.ts:26-30 - fetchTaxonomy() (L93) selects taxonomy_domains{id,name,description,key_signal} + taxonomy_subtopics{name,domain_id,description} (both is_active, ordered display_order), generateTaxonomySection() (L143) renders markdown, inject() (L190) writes it back between the markers. The taxonomy is already 100% DB-derived; the markdown is only an injection target.
  • scripts/bundle-plugin.ts:172-186 - validate() reads the markdown via parseCanonicalTaxonomy(CANONICAL_PATH) (scripts/lib/taxonomy-parser.ts:7, regex over the TAXONOMY_START..END block -> Map<domain, {slug,desc}[]>) and compares it against the plugin skills/classification/SKILL.md TAXONOMY_INJECT block.

The denylists (PI-6). Two DS files + four MAIN consumers (worktree copies excluded):

  • ops/identity-denylist.json (3466 B, PC-31 canonical) and .config/ip-denylist.txt (381 B, “Phew”).
  • .github/workflows/identity-guard.yml:78 reads the existing KH_CLIENT_NAME_DENYLIST GitHub secret (L19-25: “holds the FULL canonical denylist JSON … synced manually from ops/identity-denylist.json”). ZERO docs-site dependency at the public required check.
  • scripts/sweep-identity-relocation.ts:184-207 resolves KH_PRIVATE_DOCS_DIR/ops/identity-denylist.json (then a sibling-checkout default). Bridge-coupled.
  • .claude/hooks/ip-leak-filename-guard.sh:23 reads ${KH_PRIVATE_DOCS_DIR}/.config/ip-denylist.txt; silent no-op if absent (L24). Bridge-coupled.
  • scripts/generate-purge-path-inventory.ts, __tests__/setup.ts also reference the term set.

The eval-fixtures (PI-5). DS root eval-fixtures/{procurement-drafting,summarisation}-eval-gold-standard.json (72 KB / 75 KB; Phew-saturated). lib/eval/fixtures.ts:47-50,76-78 (resolveEvalFixture) routes the private names (summarisation, procurement-drafting) to resolvePrivateDocsDir()/eval-fixtures/<file>; public names (classification, entity) already resolve in-repo to __tests__/fixtures/eval-gold/. Consumers: scripts/eval-{procurement-drafting,summarisation}.ts, __tests__/eval/*-eval.test.ts, __tests__/validation/eval-fixture-sync.test.ts (describe.skipIf(!KH_PRIVATE_DOCS_DIR) for the private pair - the parity guard).

check-token-parity (PI-7). scripts/check-token-parity.ts:172 resolve(here, '../../app/globals.css')

  • the parent-dir escape - vs :173 ../src/styles/warm-meridian.css (the DS mirror). :180-184 skip-cleans when MAIN CSS is absent (so it is a no-op in the standalone DS build and is not currently wired into any DS workflow). MAIN app/globals.css is the source of truth; the DS file is the mirror.

In-flight collision surface (OQ-B). git log origin/id71-id104 + git diff --name-only main...origin/id71-id104: the eval work there is the NEW ID-104 engine (lib/eval/{contract,regression,severity}.ts, supabase/migrations/20260615170612_id104_eval_engine.sql, scripts/mcp-eval/fixtures.ts, __tests__/lib/eval/{contract,regression,severity}.test.ts) and ID-71 MCP-tool collapses. It does NOT touch lib/eval/fixtures.ts, eval-fixtures/*.json, resolveEvalFixture, or the gold-standard runners. scripts/mcp-eval/fixtures.ts is a DISTINCT module from lib/eval/fixtures.ts. The shared file the brief named, scripts/mcp-eval/functional-correctness.ts (83 KB), is the bid->form rename surface - orthogonal to the gold-standard fixture path. So the PI-5 rewire and the id71-id104 eval-engine work occupy different files; they coordinate on sequencing, not on a shared edit.


Each PI-N maps to a proposed change; verification lives in Testing and validation.

For every item entering MAIN (PI-5 fixtures, PI-8 worked-examples), de-ID in the moving commit, before it lands tracked in MAIN: swap first-client organisation/product strings for Example Client Limited + the {CLIENT_*} placeholders the ID-95.5 skeleton already uses (the public __tests__/fixtures/eval-gold/classification-*.json is the de-ID precedent - 21 neutral refs, zero “Phew”). Run scripts/sweep-identity-relocation.ts (PC-31 term set) over the relocated MAIN paths as the gate. De-ID is a relocation precondition, NOT a flip precondition.

No code change. ID-114 adds no dependency to/from the flip Task; identity-guard.yml stays scoped to the public repo. The de-ID gate (PI-1) fires on “about to enter MAIN”, because at that point identity-guard scans it. Asserted by review of the workflow config + the Task dependency edges.

PI-3 - harness/ eval slice -> MAIN (already canonical) -> DELETE the DS duplicate

Section titled “PI-3 - harness/ eval slice -> MAIN (already canonical) -> DELETE the DS duplicate”

Because MAIN already holds the canonical runners, suites, public fixtures, baselines, and lib/eval/*, the action is delete the duplicate eval slice from DS harness/, not move it: harness/scripts/eval-*.ts, harness/__tests__/eval/*, harness/__tests__/fixtures/{classification,entity}-*.json, harness/__tests__/fixtures/eval-baselines/*, and the eval-only parts of harness/lib/ (see PI-4). Before deleting, diff each DS harness eval file against its MAIN twin and forward-port any genuine divergence into MAIN (the only known divergence is the resolveEvalFixture import line, already canonical in MAIN). The KEEP slice of harness/ (operators/docubot/skills - PI-12) stays; the DS harness/.github/workflows/ci.yml bunx vitest run keeps passing once the eval *.test.ts

  • fixtures are removed (its remaining suites are the operator tests). Decision: ID-114 owns the DS deletion + the divergence forward-port; ID-68 remains de-ID policy owner.

PI-4 - eliminate the harness/lib fork; fix the latent --live break

Section titled “PI-4 - eliminate the harness/lib fork; fix the latent --live break”

Resolved BY PI-3’s deletion. Deleting harness/lib/{anthropic.ts,ai/pricing.ts,eval/*} and the two classify-importing runners removes the fork and the broken import('../lib/ai/classify') sites (harness/scripts/eval-classification.ts:383, harness/scripts/eval-entity-classification.ts:309) in one move - MAIN’s twin already imports the real lib/ai/classify.ts. Scope correction baked in: only those 2 runners carry the classify import; the other 3 do not. No new MAIN code; this is a duplicate-removal that incidentally retires the break.

PI-5 - root eval-fixtures/*.json -> MAIN private-gated, de-ID’d; move-then-rewire-lockstep

Section titled “PI-5 - root eval-fixtures/*.json -> MAIN private-gated, de-ID’d; move-then-rewire-lockstep”

Three separate commits, strict order:

  1. (MAIN, ID-114) Add de-ID’d copies of procurement-drafting/summarisation gold standards to the MAIN private-gated home __tests__/fixtures/eval-gold/ (alongside the existing public pair), de-ID’d to Example Client Limited per PI-1. Do NOT yet flip resolveEvalFixture. DS originals untouched - the bridge route still resolves them.
  2. (MAIN, lockstep-coordinated with ID-104 / ID-71.14) Flip lib/eval/fixtures.ts: move summarisation + procurement-drafting from PRIVATE_FIXTURES (bridge route) to PUBLIC_FIXTURES (in-repo PUBLIC_FIXTURE_DIR), and update __tests__/validation/eval-fixture-sync.test.ts so the pair is no longer skipIf(!KH_PRIVATE_DOCS_DIR)-gated. This is the rewire commit; it lands in the Task that next touches these fixtures (per OQ-B resolution below), not ahead of it.
  3. (DS, after the rewire commit lands) Delete eval-fixtures/{procurement-drafting,summarisation}-eval-gold-standard.json from DS. The bridge keeps resolving the unmoved copy until step 2 is live, so there is never a broken-path window. The UUID re-key of expected_kb_items_used (RESEARCH OQ-4) is flagged to ID-104, not owned here.

PI-6 - denylists -> secret/config store; evaluate the EXISTING secret FIRST (carried nit 2)

Section titled “PI-6 - denylists -> secret/config store; evaluate the EXISTING secret FIRST (carried nit 2)”

The KH_CLIENT_NAME_DENYLIST GitHub-encrypted secret already exists and is already the canonical store for the public required check (identity-guard.yml:78). So the net-new question is only for the LOCAL consumers (sweep-identity-relocation.ts, ip-leak-filename-guard.sh, generate-purge-path-inventory.ts) that still bridge to a DS path. Proposed end-state: a single private secret/config home (the existing KH_CLIENT_NAME_DENYLIST JSON as the canonical source; the .config/ip-denylist.txt term-set derivable from it), with local consumers resolving from a MAIN-checkout-relative config path (or the secret-materialised file in CI) rather than KH_PRIVATE_DOCS_DIR. The sweep already has a sibling-checkout default fallback (:191-207); the move is to make a MAIN-resident path the primary, not the bridge. Final home + whether to consolidate the two denylists into one source is OQ-C (Liam) - do not stand up a NEW secret; the existing one is the store.

PI-7 - check-token-parity: keep the function, re-home the coupling

Section titled “PI-7 - check-token-parity: keep the function, re-home the coupling”

Remove the ../../app/globals.css parent-dir escape (check-token-parity.ts:172). Recommended shape: re-home the guard to MAIN, where it reads MAIN app/globals.css directly (no escape) and fetches the DS warm-meridian.css mirror via the existing GitHub-App-token bridge (the docubot / sync-source-docs pattern) - flipping the coupling so it points docs-site-ward, not MAIN-ward. The check still runs in CI and still fails on a real token drift; the DS skip-clean branch (:180-184) becomes unnecessary once MAIN owns the source side. MAIN-homed guard vs shared composite action is OQ-D (Liam) (RESEARCH OQ-5).

In ops/classification-prompt.md L194-809 (24 “Phew” occurrences), replace first-client organisation/product prose with the {CLIENT_*} placeholders the ID-95.5 skeleton already uses, OR delete the region as a duplicate of tenant_config.classificationDisambiguation. Gate with sweep-identity-relocation.ts -> zero hits. This is independent of the OQ-A mechanism choice and can land first.

PI-9 - TAXONOMY/classification codegen regenerates from DB direct; bridge retired at both sites

Section titled “PI-9 - TAXONOMY/classification codegen regenerates from DB direct; bridge retired at both sites”

OQ-A RESOLVED -> option (a): build-step codegen that materialises the DB-derived taxonomy into a MAIN-homed artefact (not the no-intermediary runtime fetch). Justification: bundle-plugin.ts:validate() needs a canonical taxonomy to diff the plugin SKILL.md against, and its reader is parseCanonicalTaxonomy() (markdown-block regex). A materialised MAIN artefact (a) keeps validate()’s contract intact with a one-line path change, (b) preserves a reviewable diff of the canonical taxonomy in git, (c) avoids giving bundle-plugin a DB/network dependency at plugin-build time (it currently needs none). The no-intermediary fetch would force a live DB round-trip into every bundle-plugin run and rewrite parseCanonicalTaxonomy - more surface, less reviewability, for no gain since the taxonomy is small and already materialised today. Mechanism:

  1. Source of truth: taxonomy_domains + taxonomy_subtopics (the DB columns fetchTaxonomy() already reads) - the canonical taxonomy. No DS markdown is load-bearing.
  2. Codegen target: rewire generate-classification-prompt-taxonomy.ts to write the generateTaxonomySection() output to a MAIN-homed artefact (e.g. lib/ai/taxonomy/canonical-taxonomy.generated.md or .json - exact path is an impl detail for PLAN), dropping resolvePrivateDocsDir() (L20,26-30). The script keeps fetchTaxonomy + generateTaxonomySection; only the write target + path import change.
  3. Validation reader: point bundle-plugin.ts:validate() CANONICAL_PATH (L175-179) at the same MAIN artefact; parseCanonicalTaxonomy() is reused unchanged if the artefact keeps the TAXONOMY_START..END
    • #### N. DOMAIN shape, or gets a trivial JSON-reader variant if .json is chosen (PLAN decides).
  4. Retire the bridge at both sites: remove resolvePrivateDocsDir() import/use from generate-classification-prompt-taxonomy.ts and bundle-plugin.ts. After this, with KH_PRIVATE_DOCS_DIR unset, both codegen and plugin-bundle validation succeed. No migration/DDL: the taxonomy tables already exist and are read-only here. No new REVOKE/search_path (no new function). --env=prod opt-in (assertEnvFlag, L83) is preserved for the codegen’s DB fetch.

PI-10 - public MAIN carries only the generic skeleton

Section titled “PI-10 - public MAIN carries only the generic skeleton”

Ensured by PI-8 (worked-examples de-ID’d) + PI-9 (the materialised artefact is the generic DB-regenerated taxonomy only). No per-client disambiguation values enter tracked MAIN; those stay in tenant_config.classificationDisambiguation (ID-95.5). No code beyond PI-8/9.

No change to lib/ai/classify.ts runtime prompt-assembly, buildDisambiguationBlock(), the skeleton, or tenant_config. ID-114 touches only build-time codegen + the docs-site-resident artefacts. Asserted by git diff showing zero edits to the runtime classifier path.

PI-12 - KEEP set; wire DS root __tests__/ into DS CI

Section titled “PI-12 - KEEP set; wire DS root __tests__/ into DS CI”

No relocation of workflow-evaluation/sessions/**, the docubot lane, the workflow-evaluator operators, the KEEP slice of harness/, or the DS root __tests__/ + vitest.config.ts. Additionally, wire the DS root __tests__/ into DS CI so it executes (currently harness/.github/workflows/ci.yml runs only the harness/ vitest from working-directory: harness; the DS root tests are unwired). Add a CI job / extend the existing one to run the root suite from the repo root. No MAIN dependency added to any KEEP item.

PI-13 - no new external-library API surface

Section titled “PI-13 - no new external-library API surface”

No new dependency in package.json/requirements.txt. The only external-library calls in touched code are pre-existing: @supabase/supabase-js createClient/.from().select() in the codegen (carried unchanged) and the Anthropic SDK in the (being-deleted) harness fork. The Q-EX2 import-and-call gate is not triggered (PRODUCT PI-13 scope). No empirical-verification block required for this TECH.


Maps each invariant to a concrete check. KH bars throughout: bun run test (NEVER bun test); behaviour-first tests (reference/test-philosophy.md); UK English; no barrel re-exports; semantic tokens; sb()/tryQuery() where Supabase is touched (codegen uses the existing service-role createClient pattern, unchanged).

  • PI-1 / PI-8 / PI-10: bun run scripts/sweep-identity-relocation.ts over the relocated MAIN paths
    • the de-ID’d ops/classification-prompt.md -> zero hits; grep -ci phew over each relocated MAIN file -> 0. No first-client name in any tracked MAIN file introduced by this Task.
  • PI-2: review identity-guard.yml still scopes to the public repo; ID-114 has no flip-Task edge; DS-resident identity (KEEP items) trips no required check.
  • PI-3 / PI-4: post-deletion, no harness/scripts/eval-*.ts, harness/__tests__/eval/*, or harness/lib/{anthropic,ai/pricing,eval/*} remain in DS; grep "import('../lib/ai/classify')" over DS harness/ -> 0; MAIN bun run test green (the canonical __tests__/eval/* already exist); MAIN --live classify path (scripts/eval-classification.ts) resolves lib/ai/classify.ts (smoke: dry import). DS harness/.github/workflows/ci.yml bunx vitest run still green on the remaining operator suites.
  • PI-5: after step 1, MAIN holds de-ID’d procurement-drafting/summarisation fixtures; after step 2, resolveEvalFixture('procurement-drafting'|'summarisation') returns the in-repo path and __tests__/validation/eval-fixture-sync.test.ts runs them un-skipped; at no commit between move and rewire does any consumer resolve a missing path (the bridge resolves the unmoved DS copy until step 2); DS delete (step 3) only after step 2 lands. bun run test green across each commit.
  • PI-6: no MAIN consumer reads either denylist from a DS path after the move; KH_CLIENT_NAME_DENYLIST sync + the identity sweep produce the SAME term set as before (diff the resolved term set pre/post). ip-leak-filename-guard.sh still blocks a denylisted term.
  • PI-7: no ../../app/globals.css escape remains in the re-homed guard; introduce a deliberate token mismatch -> the check fails; revert -> passes; the check runs in CI.
  • PI-9: with KH_PRIVATE_DOCS_DIR UNSET, bun run scripts/generate-classification-prompt-taxonomy.ts and bun run build:plugin (-> bundle-plugin.ts:validate()) both succeed; no build path resolves ops/classification-prompt.md via the bridge (grep "resolvePrivateDocsDir" scripts/{generate-classification-prompt-taxonomy,bundle-plugin}.ts -> 0); the generated artefact’s domains/subtopics match the active DB taxonomy_domains/taxonomy_subtopics rows.
  • PI-11: git diff shows zero edits to lib/ai/classify.ts / buildDisambiguationBlock / the skeleton / tenant_config; no new per-client store; classification eval accuracy unchanged on the gold standard (scripts/eval-classification.ts baseline parity).
  • PI-12: KEEP items still in DS post-Task; DS root __tests__/ executes in DS CI (was unexecuted); docubot/evaluator lanes operate on the DS corpus unchanged.
  • PI-13: git diff package.json requirements.txt shows no new dependency.

Sibling-only Subtask deps within ID-114; cross-Task coordination (ID-104 / ID-71.14 for the PI-5 step-2 rewire) is a Task-level handshake, not a Subtask dep (no cross-Task Subtask dependency is created). Proposed wave order:

  1. PI-6 denylists + PI-8 de-ID worked-examples - low-coupling, unblock nothing, can run parallel.
  2. PI-3/PI-4 harness duplicate deletion (forward-port divergence first) - atomic; retires the fork+break.
  3. PI-9 codegen regenerate-from-DB + bridge retirement - depends on PI-8 landing in the same file is NOT required (different region), but sequence PI-8 first to keep the de-ID gate clean.
  4. PI-5 step 1 (MAIN add de-ID’d fixtures) - independent; step 2 (rewire) lands lockstep in the OQ-B-resolved Task; step 3 (DS delete) strictly after step 2.
  5. PI-7 check-token-parity re-home - independent; resolve OQ-D shape first.
  6. PI-12 wire DS root __tests__/ into CI - DS-only, independent.

OQ-B resolution (eval-fixture rewire sequencing handshake)

Section titled “OQ-B resolution (eval-fixture rewire sequencing handshake)”

The id71-id104 branch does NOT touch lib/eval/fixtures.ts / eval-fixtures / resolveEvalFixture (grounded above), so there is no file collision - only a sequencing courtesy. Recommended handshake: ID-114 owns PI-5 step 1 (add de-ID’d MAIN fixtures) and step 3 (DS delete). PI-5 step 2 (the resolveEvalFixture flip) is a ~10-line change ID-114 also authors, but it MERGES only after the id71-id104 eval work has landed on the shared trunk, to avoid two eval-touching branches racing the same bun run test gate - i.e. ID-114 does not merge step 2 ahead of id71-id104, it rebases onto it. Because the files are disjoint, no rebase conflict is expected; the handshake is purely “land after, not before”. The UUID re-key (OQ-4) is deferred to ID-104. If Liam prefers ID-71.14 to own the flip entirely (fold step 2 into the fixture-rename lane), that is the cleaner ownership boundary - flagged as OQ-E.


  • Deleting the harness eval duplicate loses unique history/divergence. Mitigate: diff every DS harness eval file against its MAIN twin and forward-port real divergence into MAIN BEFORE deleting; the only known divergence is the resolveEvalFixture import (already canonical in MAIN).
  • PI-5 broken-path window if the 3-commit order is violated. Mitigate: enforce move(1) -> rewire(2) -> delete(3); the skipIf(!KH_PRIVATE_DOCS_DIR) guard in eval-fixture-sync.test.ts keeps the private pair green during transition. Never git mv the fixtures and delete the DS copy in one commit.
  • PI-9 bridge retirement could break bundle-plugin validation if the MAIN artefact shape drifts from parseCanonicalTaxonomy’s regex. Mitigate: keep the TAXONOMY_START..END + #### N. DOMAIN / - \slug`: desc` shape in the materialised artefact, OR add a JSON-reader variant in the same PR; the PI-9 validation test (KH_PRIVATE_DOCS_DIR unset) catches drift.
  • Denylist regression (PI-6) if the term set differs post-move. Mitigate: diff the resolved term set pre/post; the KH_CLIENT_NAME_DENYLIST secret stays the canonical source.
  • De-ID miss lands client identity in MAIN (PI-1). Mitigate: the sweep gate is the relocation precondition; identity-guard will scan MAIN once landed, so a miss is caught - but the gate must run pre-commit, not post.

  • OQ-C - denylist canonical home (PI-6). The existing KH_CLIENT_NAME_DENYLIST secret is confirmed as the store for the public check; do the LOCAL consumers (sweep, ip-leak-filename-guard.sh, generate-purge-path-inventory) resolve from a MAIN-checkout config path, or a secret-materialised file in CI? And: consolidate .config/ip-denylist.txt + ops/identity-denylist.json into one source? (Do NOT stand up a new secret.)
  • OQ-D - check-token-parity re-home shape (PI-7). MAIN-homed guard (reads MAIN CSS, fetches DS mirror via GitHub-App bridge) vs shared composite action invoked from both repos?
  • OQ-E - PI-5 step-2 ownership. ID-114 authors the resolveEvalFixture flip and merges-after-id71-id104, OR fold the flip into ID-71.14’s fixture-rename lane entirely (cleaner ownership)? (OQ-A/OQ-B are resolved in this TECH; these three home/ownership choices remain.)

  • {114.1} RESEARCH + {114.2} PRODUCT (specs/id-114-docs-site-relocation/), read in full.
  • MAIN: scripts/generate-classification-prompt-taxonomy.ts (full), scripts/bundle-plugin.ts:140-280, scripts/lib/taxonomy-parser.ts (full), lib/eval/fixtures.ts (full), lib/private-docs.ts (full), scripts/eval-{classification,entity-classification,procurement-drafting,summarisation,search}.ts (import scope), __tests__/eval/*, __tests__/fixtures/eval-gold/*, __tests__/fixtures/eval-baselines/*, scripts/sweep-identity-relocation.ts:180-219, .claude/hooks/ip-leak-filename-guard.sh, .github/workflows/identity-guard.yml, __tests__/validation/eval-fixture-sync.test.ts, vitest.config.ts.
  • DS: harness/{scripts,lib,__tests__,package.json,tsconfig.json,vitest.config.ts}, harness/.github/workflows/ci.yml, eval-fixtures/*.json, ops/{classification-prompt.md,identity-denylist.json}, .config/ip-denylist.txt, scripts/check-token-parity.ts.
  • git log/diff origin/id71-id104 (OQ-B collision check) - eval work is disjoint from the gold-standard fixture path.