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-Bsplit workaround.
Summary
Section titled “Summary”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.
Problem
Section titled “Problem”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), plusscripts/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 callsparseEnvFlag → fetchTaxonomy → generateTaxonomySection → inject. The codegen already fetches the taxonomy from the DB (fetchTaxonomyselectstaxonomy_domains+taxonomy_subtopics) but theninjects 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_Storeonly) —classify.pyis GONE; the ticket’s premise that “scripts/kb_pipeline/classify.pyloadsops/classification-prompt.mdat RUNTIME” is STALE. TheKH_PRIVATE_DOCS_DIRbuild bridge resolves at exactly two build-time call sites:scripts/bundle-plugin.ts:174(validate()reads back the markdown) andscripts/generate-classification-prompt-taxonomy.ts:27(read source + write target), both vialib/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.tsresolves them via.replaceAll+buildDisambiguationBlock(); per-client data lives intenant_config.config(BrandingConfigSchema.classificationDisambiguation, migration20260613090000_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 / Non-goals
Section titled “Goals / Non-goals”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).
Behavior
Section titled “Behavior”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.
De-ID precondition (cross-cutting)
Section titled “De-ID precondition (cross-cutting)”-
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 Limitedand the{CLIENT_*}placeholders the ID-95.5 skeleton already uses) or relocated intotenant_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+ theops/identity-denylist.jsonterm set) reports zero hits on the de-ID’d items; no first-client name appears in any tracked MAIN file introduced by this Task. -
PI-2 (NOT flip-blocking; identity-guard scans the public repo only). This Task does not gate the public-repo flip: the
identity-guardrequired check scans the publicknowledge-hubrepo 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.
Per-item proper home (relocate set)
Section titled “Per-item proper home (relocate set)”-
PI-3 (
harness/→ MAIN__tests__/eval/, private-gated). The private vitest/eval product test infrastructure (theknowledge-hub-internalpackage: ownpackage.json/tsconfig/vitest.config, plusharness/__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: noharness/product-test tree remains in the docs-site; the eval infra resolves under MAIN__tests__/eval/; relocated fixtures/baselines pass the identity sweep. -
PI-4 (
harness/libfork 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--liveimport break is fixed: the relocatedeval-classification.tsdynamicimport('.../lib/ai/classify')now resolves to the reallib/ai/classify.ts(the fork lacked it), so--livemode loadsclassifyContentwithout a module-resolution failure. Verify: noharness/lib(or equivalent fork) directory exists post-relocation; the relocated eval runner’s classify import resolves to canonicallib/ai/classify.ts; the--liveclassification path no longer throws a module-not-found onai/classify. -
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 toExample Client Limited(PI-1). The physical move + de-ID is owned by ID-114; the consuming-path rewire (flippingresolveEvalFixture()’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 (thescripts/eval-*.tsrunners 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). -
PI-6 (denylists → secret/config store, not docs-site).
.config/ip-denylist.txtandops/identity-denylist.jsonare re-homed to a dedicated private secret/config store (their proper home), not the docs-site. The MAIN consumers that read them — the publicKH_CLIENT_NAME_DENYLISTsecret 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 aKH_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; theKH_CLIENT_NAME_DENYLISTsync and the identity sweep produce the same term set as before (no denylist regression). -
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 MAINapp/globals.css) is preserved, but the fragile../../app/globals.cssparent-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”-
PI-8 (de-ID the residual worked-examples). The first-client worked-examples region of the build-time prompt source (
ops/classification-prompt.mdL194–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 thetenant_config.classificationDisambiguationrules. 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 fromtenant_config, not hardcoded client prose. -
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. TheKH_PRIVATE_DOCS_DIRbuild 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: withKH_PRIVATE_DOCS_DIRunset, the taxonomy codegen and the plugin-bundle taxonomy validation both succeed (they no longer require the docs-site checkout); no build-time code path resolvesops/classification-prompt.mdvia the bridge; the generated taxonomy matches the active DBtaxonomy_domains/taxonomy_subtopicsrows. (Open decision deferred to TECH — see Open questions: the exact mechanism, build-step codegen-from-DB-to-a-MAIN-artefact vs runtime fetch.) -
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 intenant_config.classificationDisambiguationin 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 fromtenant_configat runtime exactly as ID-95.5 shipped (no behavioural change to the runtime classifier). -
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 inlib/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.tsruntime 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).
KEEP set (must NOT relocate)
Section titled “KEEP set (must NOT relocate)”- 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.ymlruns only theharness/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)”- 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-jsin 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 inpackage.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 existingfetchTaxonomy()+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-parityre-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.
Sources (read this dispatch)
Section titled “Sources (read this dispatch)”- {114.1} RESEARCH (
specs/id-114-docs-site-relocation/RESEARCH.md) — 14-row relocation map, read in full. - ID-95
PRODUCT.md/TECH.md—tenant_config+classificationDisambiguationmodel (PI-10, OQ-5 resolutiontenant_config.configholds the whole client document includingclassificationDisambiguation). - MAIN code (verified this dispatch):
scripts/generate-classification-prompt-taxonomy.ts(full),scripts/bundle-plugin.ts:160–200(thevalidate()bridge),lib/eval/fixtures.ts(resolveEvalFixtureconsuming path + consumer list),harness/scripts/eval-classification.ts(the--livefork 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.