Skip to content

ID-114 {114.2} PRODUCT — relocate product functionality out of the private docs-site

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

Section titled “ID-114 {114.2} PRODUCT — 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 fresh, NOT the RESEARCH author) Tier: PRODUCT (of a PRODUCT+TECH+PLAN chain — RESEARCH recommended PRODUCT+PLAN / TECH+PLAN; the regenerate-from-DB rework + lockstep eval rewire warrant a TECH leg too). Predecessor: {114.1} RESEARCH (specs/id-114-docs-site-relocation/RESEARCH.md, docs-site a232d3b4). Builds on (do NOT re-implement): ID-95.5 — lib/ai/skills/classification.md skeleton + tenant_config.classificationDisambiguation (shipped + verified).

Governing principle (verbatim, prior audit 2026-06-15): “the private docs-site must host docs ONLY — no product functionality.” This Task closes the gaps where product / coupled content sits in the private docs-site as an ID-68 Option S-B split workaround.


Relocate every piece of product functionality currently mis-homed in the private knowledge-hub-docs-site repo to its proper home (mostly the public-bound MAIN repo, private-gated, de-identified first), and re-architect the build-time TAXONOMY/classification codegen to regenerate directly from the database rather than from a relocated markdown artefact — eliminating the docs-site as load-bearing build infrastructure. De-identification of any first-client identity is the mandatory precondition for an item entering the MAIN repo. The Task is NOT flip-blocking; it is hygiene, correctness, and de-fragilising cross-repo coupling.

The audit (RESEARCH §2, 14-row map) found seven classes of product/coupled content living in the docs-site as ID-68-era workarounds: the harness/ test infra + its harness/lib fork, root eval-fixtures/*.json gold-standards, ops/classification-prompt.md (build-time codegen source), two denylists, and check-token-parity.ts. Two stale premises from the original ticket were corrected by RESEARCH and re-verified this dispatch:

  • Code-intelligence orientation (cited verbatim, this dispatch, repo knowledge-hub):
    • gitnexus_query({query: 'classification taxonomy codegen branding tenant_config'})"processes": [] (no graphed runtime flow); definitions returned were build-time codegen only: scripts/generate-classification-prompt-taxonomy.ts (main, assertEnvFlag), scripts/fetch-client-branding.ts (runClientBrandingFetch, writeBrandingJson), scripts/generate-client-branding-map.ts (renderClientBrandingMap), plus scripts/cocoindex_pipeline/extraction.py:ClassificationExtraction (Python pipeline, DB-taxonomy consumer). The classifier-prompt load path is not a graphed runtime flow.
    • gitnexus_context({name: 'main', file_path: 'scripts/generate-classification-prompt-taxonomy.ts'})"processes": []; outgoing calls parseEnvFlag → fetchTaxonomy → generateTaxonomySection → inject. The codegen already fetches the taxonomy from the DB (fetchTaxonomy selects taxonomy_domains + taxonomy_subtopics) but then injects it into the docs-site markdown intermediary between <!-- TAXONOMY_START --> / <!-- TAXONOMY_END --> markers.
    • grep (gitnexus/ast-dataflow are TS-only): MAIN scripts/kb_pipeline/ is empty (__pycache__/ + .DS_Store only) — classify.py is GONE; the ticket’s premise that “scripts/kb_pipeline/classify.py loads ops/classification-prompt.md at RUNTIME” is STALE. The KH_PRIVATE_DOCS_DIR build bridge resolves at exactly two build-time call sites: scripts/bundle-plugin.ts:174 (validate() reads back the markdown) and scripts/generate-classification-prompt-taxonomy.ts:27 (read source + write target), both via lib/private-docs.ts:resolvePrivateDocsDir() (fail-loud, Inv 28/29).
    • The runtime classifier prompt is already MAIN-homed + skeletonised: lib/ai/skills/classification.md (53 KB) carries {TAXONOMY}, {CLIENT_DISAMBIGUATION}, {CLIENT_*} placeholders; lib/ai/classify.ts resolves them via .replaceAll + buildDisambiguationBlock(); per-client data lives in tenant_config.config (BrandingConfigSchema.classificationDisambiguation, migration 20260613090000_id95_5_tenant_config.sql, service-role-only, set out-of-band, never committed — ID-95 PI-10). ID-114 consumes this; it does not rebuild it.

The residual problem is therefore four-fold: (1) the build-time codegen + plugin-bundle validation depend on a client-identity-bearing docs-site markdown intermediary (24 “Phew” occurrences in ops/classification-prompt.md, worked-examples L194–809); (2) the live eval gold-standards (eval-fixtures/*.json, client-identity-saturated) are reached out of the docs-site via the bridge from lib/eval/fixtures.ts:resolveEvalFixture(); (3) the harness/lib fork makes harness/scripts/eval-classification.ts:383 (await import('../lib/ai/classify')) resolve into a fork that lacks ai/classify, a latent --live break; (4) two denylists + a fragile ../../app/globals.css parent-dir escape in check-token-parity.ts sit in the wrong repo.

Goals. Re-home each audited item to its proper home with de-ID as the entry precondition; land the regenerate-from-DB end-state for the TAXONOMY/classification codegen so no docs-site build/runtime dependency remains; preserve live-eval correctness across the move via a move-then-rewire-lockstep contract; eliminate the harness/lib fork; give the denylists a proper secret/config home.

Non-goals (explicit). (a) Re-doing the ID-95.5 split — the skeleton + tenant_config.classificationDisambiguation runtime path is shipped and verified; ID-114 builds on it and verifies against it, never duplicates it. (b) Tier-3 BYOC / any new per-client topology mechanism. (c) The public-repo flip itself — this Task is not a flip prerequisite (PI-2). (d) Re-keying the eval seed corpus end-to-end (UUID re-key is flagged for ID-104 coordination, not owned here).

Invariants are numbered PI-N, each with a *Verify:* means the Checker uses as the acceptance criterion. “Home” = the destination repo/path; “de-ID gate” = the identity-removal precondition. UK English throughout.

  1. PI-1 (de-ID is the mandatory precondition for MAIN entry). No item carrying first-client identity (verbatim client prose, organisation/product names, real content UUIDs) lands in the MAIN repo until it has been de-identified — first-client identity is replaced with the canonical neutral placeholder (Example Client Limited and the {CLIENT_*} placeholders the ID-95.5 skeleton already uses) or relocated into tenant_config/secret store. De-ID happens BEFORE the MAIN commit, not after. Verify: after relocation, an identity sweep over the relocated MAIN paths (scripts/sweep-identity-relocation.ts + the ops/identity-denylist.json term set) reports zero hits on the de-ID’d items; no first-client name appears in any tracked MAIN file introduced by this Task.

  2. PI-2 (NOT flip-blocking; identity-guard scans the public repo only). This Task does not gate the public-repo flip: the identity-guard required check scans the public knowledge-hub repo only, never the private docs-site, so client identity resident in the docs-site is not a public leak. The de-ID gate (PI-1) is a relocation precondition, not a flip precondition — its trigger is “item is about to enter MAIN”, because at that point identity-guard will scan it. Verify: ID-114 carries no dependency on, and is not a blocker of, the flip Task; the identity-guard workflow config still scopes to the public repo only after this Task lands; docs-site-resident identity (in KEEP items) does not trip any required check.

  1. PI-3 (harness/ → MAIN __tests__/eval/, private-gated). The private vitest/eval product test infrastructure (the knowledge-hub-internal package: own package.json/tsconfig/vitest.config, plus harness/__tests__/fixtures/*.json + eval-baselines/*.baseline.json) is re-homed under the MAIN repo as private-gated eval infrastructure; it no longer lives in the docs-site. Gold-standard fixtures and metric baselines moved with it pass the PI-1 de-ID gate. Verify: no harness/ product-test tree remains in the docs-site; the eval infra resolves under MAIN __tests__/eval/; relocated fixtures/baselines pass the identity sweep.

  2. PI-4 (harness/lib fork is ELIMINATED; imports resolve to canonical @/lib/*). After relocation no forked copy of MAIN libraries (anthropic.ts, ai/pricing.ts, eval/*) exists; the relocated eval runners import the canonical MAIN modules. The latent --live import break is fixed: the relocated eval-classification.ts dynamic import('.../lib/ai/classify') now resolves to the real lib/ai/classify.ts (the fork lacked it), so --live mode loads classifyContent without a module-resolution failure. Verify: no harness/lib (or equivalent fork) directory exists post-relocation; the relocated eval runner’s classify import resolves to canonical lib/ai/classify.ts; the --live classification path no longer throws a module-not-found on ai/classify.

  3. PI-5 (root eval-fixtures/*.json → MAIN private-gated, de-ID’d; live eval never breaks — move-then-rewire-lockstep contract). The two client-identity-saturated root gold standards (procurement-drafting-eval-gold-standard.json, summarisation-eval-gold-standard.json) are physically moved into the MAIN private-gated fixture home and de-ID’d to Example Client Limited (PI-1). The physical move + de-ID is owned by ID-114; the consuming-path rewire (flipping resolveEvalFixture()’s private names from the ${KH_PRIVATE_DOCS_DIR}/eval-fixtures/ bridge route to the in-repo route) lands lockstep-coordinated with whichever of the ID-104 eval-runner / ID-71.14 fixture-lane owners next touches these fixtures. At no point between move and rewire does a consumer (the scripts/eval-*.ts runners or the __tests__/eval/* suites) resolve a missing or wrong fixture: either the bridge route still resolves the unmoved copy, or the rewire and move land atomically together. The docs-site originals (${KH_PRIVATE_DOCS_DIR}/eval-fixtures/*.json) MUST NOT be deleted until the lockstep rewire commit has landed in its owning Task (ID-104 or ID-71.14) — the physical move into MAIN and the docs-site deletion are SEPARATE commits, so the bridge route keeps resolving the unmoved copy until the rewire is live. Verify: the two fixtures resolve from the de-ID’d MAIN private-gated home after the move; resolveEvalFixture() and its consumers never resolve a non-existent path during the transition (no broken-path window); the live eval suites green across the move; the rewire is recorded as coordinated with ID-104/ID-71.14 (Task-level handshake, not a Subtask dep).

  4. PI-6 (denylists → secret/config store, not docs-site). .config/ip-denylist.txt and ops/identity-denylist.json are re-homed to a dedicated private secret/config store (their proper home), not the docs-site. The MAIN consumers that read them — the public KH_CLIENT_NAME_DENYLIST secret sync, the identity sweep (scripts/sweep-identity-relocation.ts), and the purge-path inventory (scripts/generate-purge-path-inventory.ts) — resolve the denylist from its new home, not from a KH_PRIVATE_DOCS_DIR/docs-site path. Verify: neither denylist is read from a docs-site path by any MAIN consumer after the move; the denylist consumers resolve from the new secret/config home; the KH_CLIENT_NAME_DENYLIST sync and the identity sweep produce the same term set as before (no denylist regression).

  5. PI-7 (check-token-parity.ts — keep the function, re-home the coupling). The token-parity capability (asserting the docs-site app’s CSS matches MAIN app/globals.css) is preserved, but the fragile ../../app/globals.css parent-dir escape is removed: the coupling is re-homed so it no longer assumes MAIN is the docs-site’s parent directory. The check still runs in CI and still fails on a genuine token drift between the two repos. Verify: no ../../app/globals.css (or equivalent parent-dir escape out of the docs-site repo) remains in the re-homed check; the token-parity check still runs and still fails on an introduced token mismatch.

Regenerate-from-DB classification/TAXONOMY codegen end-state

Section titled “Regenerate-from-DB classification/TAXONOMY codegen end-state”
  1. PI-8 (de-ID the residual worked-examples). The first-client worked-examples region of the build-time prompt source (ops/classification-prompt.md L194–809; 24 first-client occurrences) carries no first-client identity in the de-ID’d end-state: it is either replaced with the {CLIENT_*} placeholders the ID-95.5 skeleton already uses, or removed as a duplicate of the tenant_config.classificationDisambiguation rules. No first-client worked-example prose survives into MAIN. Verify: the de-ID’d codegen source contains no first-client organisation/product name; an identity sweep over it reports zero hits; any disambiguation examples present are generic placeholders or sourced from tenant_config, not hardcoded client prose.

  2. PI-9 (TAXONOMY/classification codegen REGENERATES FROM THE DB DIRECT — no docs-site build/runtime dependency remains). In the end-state the build-time TAXONOMY/classification codegen produces the canonical taxonomy artefact directly from the database (taxonomy_domains + taxonomy_subtopics) with no docs-site markdown intermediary as the load-bearing source, and the plugin-bundle taxonomy validation (scripts/bundle-plugin.ts:validate()) reads its canonical taxonomy from a DB-derived / MAIN-homed source — not from ${KH_PRIVATE_DOCS_DIR}/ops/classification-prompt.md. The KH_PRIVATE_DOCS_DIR build bridge for the classification prompt is retired at both call sites (generate-classification-prompt-taxonomy.ts + bundle-plugin.ts). The docs-site is no longer load-bearing build infrastructure for classification/taxonomy. Verify: with KH_PRIVATE_DOCS_DIR unset, the taxonomy codegen and the plugin-bundle taxonomy validation both succeed (they no longer require the docs-site checkout); no build-time code path resolves ops/classification-prompt.md via the bridge; the generated taxonomy matches the active DB taxonomy_domains/taxonomy_subtopics rows. (Open decision deferred to TECH — see Open questions: the exact mechanism, build-step codegen-from-DB-to-a-MAIN-artefact vs runtime fetch.)

  3. PI-10 (public MAIN carries only the generic skeleton; per-client specifics stay in tenant_config). The classification/taxonomy assets that land in the public-bound MAIN repo are generic only: the {TAXONOMY}/{CLIENT_*} skeleton (lib/ai/skills/classification.md, already shipped) plus the generic DB-regenerated taxonomy. All per-client classification specifics (disambiguation entity-examples, self-reference rules) remain exclusively in tenant_config.classificationDisambiguation in the client’s own Supabase project (ID-95 PI-10), never in tracked MAIN source. Verify: no per-client classification disambiguation values appear in any tracked MAIN file; the only classification/taxonomy assets in MAIN are the generic skeleton + the DB-regenerated generic taxonomy; per-client specifics resolve from tenant_config at runtime exactly as ID-95.5 shipped (no behavioural change to the runtime classifier).

  4. PI-11 (ID-95.5 runtime split is NOT rebuilt — consumed + verified, not duplicated). The ID-95.5 runtime split (skeleton + tenant_config.classificationDisambiguation + buildDisambiguationBlock() resolution in lib/ai/classify.ts) is left intact; ID-114 introduces no second copy of the disambiguation model, no alternative per-client store, and no change to the runtime prompt-assembly path. The classifier’s runtime behaviour is byte-for-byte unchanged by this Task. Verify: lib/ai/classify.ts runtime prompt-assembly + buildDisambiguationBlock() are unchanged; no new per-client config table/store is introduced; the classification runtime behaviour is identical before and after this Task (no eval-accuracy regression on the classification gold standard).

  1. PI-12 (KEEP items remain in the docs-site; one is newly CI-wired). The following stay in the docs-site and are NOT relocated by this Task: workflow-evaluation/sessions/** (ID-48 evaluator telemetry), the docubot lane (.github/workflows/{docubot,sync-source-docs}.yml + its action + harness docubot/skills scripts), the workflow-evaluator operators (.claude/{workflows,agents,skills}), and the docs-site’s OWN root __tests__/ + vitest.config.ts. The docs-site root __tests__/ (currently unwired — ci.yml runs only the harness/ vitest) is wired into the docs-site CI so it actually executes. None of the KEEP items gain a new MAIN dependency. Verify: the KEEP items still reside in the docs-site after this Task; the docs-site root __tests__/ runs in docs-site CI (it was previously unexecuted); no KEEP item was moved to MAIN; the docubot/evaluator lanes still operate ON the docs-site corpus unchanged.

Empirical-verification note (OQ-3 / Q-EX2 scope)

Section titled “Empirical-verification note (OQ-3 / Q-EX2 scope)”
  1. PI-13 (no new external-library API surface introduced). This Task relocates and de-IDs existing assets and re-points existing internal symbols; it introduces no new external third-party library API. The only external-library calls in the touched code (@supabase/supabase-js in the codegen, the Anthropic SDK in the eval runners) are already in use against their pinned versions and are not newly relied upon by this PRODUCT spec, so the import-and-call empirical-verification gate (external-library symbols only) is not triggered here. (Any externally-sourced claim that does surface during TECH/implementation must still pass that gate.) Verify: the relocation introduces no new dependency in package.json/requirements.txt; all relied-upon library symbols are pre-existing call sites carried unchanged through the move.

Open questions (deferred to TECH — do NOT resolve in PRODUCT)

Section titled “Open questions (deferred to TECH — do NOT resolve in PRODUCT)”
  • OQ-A — regenerate-from-DB mechanism (PI-9). Build-step codegen that materialises the DB-derived taxonomy into a MAIN-homed artefact (markdown or otherwise) that bundle-plugin.ts:validate() reads, vs a runtime/build fetch with no materialised intermediary at all. TECH to choose, weighing the existing fetchTaxonomy() + parseCanonicalTaxonomy() shapes and the plugin-bundle validation contract.
  • OQ-B — eval-fixture rewire sequencing handshake (PI-5). The exact lockstep handshake with ID-104 (eval-runner build) and ID-71.14 (fixture-rename lane): who flips resolveEvalFixture()’s private entries, in which Task, and the trigger ordering so the move and rewire never leave a broken-path window. (Carries the related ID-104 UUID-re-key flag — RESEARCH OQ-4.)
  • Borderline (for Liam, not blocking PRODUCT): the denylist canonical home (PI-6) — stand up a dedicated secret/config store now vs interim-keep — and whether to consolidate the two denylists into one source (RESEARCH OQ-6); plus the check-token-parity re-home shape (PI-7) MAIN-homed guard vs shared composite action (RESEARCH OQ-5). These are home-selection choices TECH can carry; none changes the invariants above.

  • {114.1} RESEARCH (specs/id-114-docs-site-relocation/RESEARCH.md) — 14-row relocation map, read in full.
  • ID-95 PRODUCT.md / TECH.mdtenant_config + classificationDisambiguation model (PI-10, OQ-5 resolution tenant_config.config holds the whole client document including classificationDisambiguation).
  • MAIN code (verified this dispatch): scripts/generate-classification-prompt-taxonomy.ts (full), scripts/bundle-plugin.ts:160–200 (the validate() bridge), lib/eval/fixtures.ts (resolveEvalFixture consuming path + consumer list), harness/scripts/eval-classification.ts (the --live fork import break, L383), lib/private-docs.ts (the bridge helper), scripts/kb_pipeline/ (empty — classify.py gone).
  • gitnexus (repo knowledge-hub): query('classification taxonomy codegen branding tenant_config') → no runtime flow, build-time codegen definitions only; context('main', generate-classification-prompt-taxonomy.ts) → DB-fetch + markdown-inject codegen shape.