Skip to content

ID-68 {68.13} — Pre-Flip Client De-Identification + Private-Repo Maturation Plan

ID-68 {68.13} — Pre-Flip Client De-Identification + Private-Repo Maturation Plan

Section titled “ID-68 {68.13} — Pre-Flip Client De-Identification + Private-Repo Maturation Plan”

Type: Implementation design (RESEARCH + TECH, no PRODUCT — ID-54/ID-68 precedent). Authored: 2026-06-03 (S301a) by the relocation Orchestrator, from the S301a de-identification-status investigation (3 read-only agents: spec-corpus, live-triage, memory). Status: RATIFIED (S301a, Liam) with notes — D3 adopted + a ledger-CLI client-name guard added (§2 D, §4); C largely resolved (classification-prompt is kb_pipeline-only and retires with it — §2 C). No code/ledger/migration edits made by this document. Governing inputs: RELOCATION-IMPLEMENTATION-PLAN.md (S-B routing, ratified S301); RELOCATION-INVESTIGATION-V2.md (OQ-R10/R12); ledger docs/reference/tasks/ID-68.md ({68.11} deferral journal); the S291 “Pre-Flip De-Identification + Client-Configurability Scoping Report” (MemPalace, sessions/technical, 2026-06-01). Implements: a NEW subtask {68.13} to be opened under Task ID-68, inserted as a dependency of {68.9} (flip) — see §7.


0. Why this exists — the flip-blocking gap

Section titled “0. Why this exists — the flip-blocking gap”

The S301a investigation established that the ratified ID-68 design is sound and ~60% executed, but the pre-flip client de-identification is incomplete, and the remaining work was explicitly owner-deferred and never ratified or re-homed into a tracked subtask.

Terminology (ratified, RESEARCH §11): Phew (Phew Design Limited) = the first CLIENT. The platform is Knowledge Hub; the company is AI Solution Hub (ai-solution-hub org). Client identity must not leak into the public application repo; client-name references inside the private knowledge-hub-internal repo are accepted (sensitivity routing, S-B).

The gap, precisely: {68.11} (de-identify shipped code — DONE, S292 f917f88d) scoped itself to confidential worked-examples + parameterising lib/client-config.ts defaults, and its journal (tasks/ID-68.md:149) listed under “DEFERRED (separate, not code-scope)”: phew.json + public/clients/phew/* untracking; the capture_phew_domain_hook migration; the kh.phew.org.uk sweep (~40 files); export-phew-articles.ts filename; classification-eval-gold-standard.json; cloudrun/*phew*.yaml. The {68.9} flip AC (tasks/ID-68.md:113-116) gates only on Dependabot clearance + Liam GO — it carries none of these. Flipping today would publish the real client name (Phew Design Limited), support@phew.org.uk, the client domain, and the domain-gate migration — the exact IP-separation concern ID-68 exists to prevent.

Cross-session note: a concurrent session is implementing the canonical pipeline on canonical-pipeline-setup. Item C below (classification-prompt scrub) and the ledger writes intersect that work — execution must be coordinated, not raced.


1. Current status — what is done vs what remains

Section titled “1. Current status — what is done vs what remains”
AreaStatusEvidence
Channel-B client content HEAD-removed✅ DONE{68.5} S290 b6e35683
scripts/ client literals parameterised → client-config/CLIENT_* env✅ DONE{68.6} S291
ITT one-shot seeds relocated out of build context✅ DONE{68.7}
ghcr build context clean post-scrub✅ DONE{68.8}
Classification worked-examples scrubbed in lib/ai/skills/classification*.md + inlined.generated.ts; client-config defaults genericised✅ DONE{68.11} S292 f917f88d
Cloud Run *phew* yamls✅ DONE (0 tracked)Cloud Run retired S298 — deferred-item 6 already resolved
Dependabot high-sev gate✅ GREEN (re-verify live pre-flip)S291: 0 open high
A — phew.json + public/clients/phew/* (5 assets)❌ OPEN — tracked in public HEADlib/branding/clients/phew.json, public/clients/phew/*
B — capture_phew_domain_hook migration + config.toml❌ OPEN — names real client domainsupabase/migrations/20260424202806_*.sql, supabase/config.toml:72-78
B2 — 2nd migration seeds real client name❌ OPEN20260419212103_*.sql:48 ("…for Phew Design Limited")
C — confidential client content in fixtures + classification-prompt.md❌ OPEN — real bid prose4 eval gold-standards; docs/reference/classification-prompt.md:194-758
D — staying-public ledgers/specs/ontology name the client❌ OPEN — ~82 docs filesdocs/specs/ID-68-*, docs/reference/*.json + mirrors, docs/ontology/18-entity-aliases.md
F — kh.phew.org.uk prod domain in public source❌ OPENCLAUDE.md, scripts/tests/*.py (×7 webhook URLs)
Operator-move (docs-site + operators → private; remove from public)⏸ NOT STARTED{68.12} Phase 2e/3 remainder — see §3
Private repo (knowledge-hub-internal) maturation⏸ NOT a functioning repono package.json/vitest/CI; bridges unwired

Already shipped this session (S301a): client-IP scrub of the mammoth test (synthetic .docx fixture), quality-gate.test.ts filenames, and an ICO-number placeholder — commit 83525b84 on id68-relocation-phase23.


2. Decisions A–F (Liam-steered) — design + open investigations

Section titled “2. Decisions A–F (Liam-steered) — design + open investigations”

Each carries Liam’s ratified steer (verbatim intent), the proposed design, the open investigation, and risk.

A — phew.json + brand assets → untrack → deploy-inject

Section titled “A — phew.json + brand assets → untrack → deploy-inject”
  • Steer: “untrack → deploy-inject.”
  • Current: lib/client-config.ts statically imports phew.json and registers it in CLIENT_BRANDING_MAP = { default, phew } — so the real client config is compiled into the bundle regardless of NEXT_PUBLIC_CLIENT_ID. default.json is already a generic fallback.
  • Design: (1) git rm lib/branding/clients/phew.json + public/clients/phew/*; (2) drop the static phew import + map entry from client-config.ts, so the map ships { default } only and the loader resolves default when no client overlay is present; (3) provide phew.json + assets at deploy time via a private overlay (mounted/copied into lib/branding/clients/ + public/clients/ during the client build) — the path/loader mechanism is unchanged, only the data is injected. NEXT_PUBLIC_CLIENT_ID=phew at deploy selects it.
  • Open investigation: confirm the loader’s behaviour when CLIENT_BRANDING_MAP[id] is absent — it already falls back to default (verified L584-607), so this is low-risk; verify no other static import of phew.json exists and that public/clients/phew/* is referenced only via the config-driven asset paths.
  • Risk: LOW. Reversible. Deploy overlay mechanism must exist before the public asset removal lands in a deployed build (else the client deploy loses branding).

B — capture_phew_domain_hook migration → rename + parameterise

Section titled “B — capture_phew_domain_hook migration → rename + parameterise”
  • Steer: “rename + parameterise.”
  • Current: 20260424202806_capture_phew_domain_hook.sql hardcodes hook_restrict_signup_to_phew_domain, IF domain = 'phew.org.uk', the user-facing error message, and a prod project-ref. config.toml:72-78 wires the hook by that name. A 2nd migration (20260419212103) seeds a DB row naming Phew Design Limited.
  • Design: migrations are append-only history — do not edit the applied file. Author a new migration that (a) CREATE OR REPLACEs the function under a generic name (hook_restrict_signup_to_allowed_domain) reading the allowed domain from a config value (a app_settings/GUC row or env-injected setting), (b) drops/renames the old client-named function, (c) repoints config.toml to the generic hook name. The real domain becomes deploy-config, not source. The 2nd migration’s seeded client-name row: supersede via a follow-up migration or accept (it is data, not identity in the function surface) — decide in ratification.
  • Open investigation: whether Supabase auth-hook wiring (config.toml) can reference a config-driven domain without a redeploy; whether the old migration’s text must also be history-purged (§E) since it names the domain in all history.
  • Risk: MEDIUM. Touches live auth (signup domain gate). Must be tested against staging before the public-app change. The migration is the single highest-signal leak — it proves Phew is the live production tenant (memory: “this connection is the IP-separation concern ID-68 exists to prevent”).

C — confidential client content (fixtures + classification-prompt.md) → scrub to synthetic, AFTER confirming live-usage

Section titled “C — confidential client content (fixtures + classification-prompt.md) → scrub to synthetic, AFTER confirming live-usage”
  • Steer: “Scrub to synthetic, but first check whether the classification prompt in its current form is still used once the canonical pipeline implementation is complete.”
  • Current — two prompt sources:
    • docs/reference/classification-prompt.md — read by scripts/kb_pipeline/config.py:15 (PROMPT_PATH), bundled into the MCP plugin (bundle-plugin.ts:174), taxonomy-codegen target (generate-classification-prompt-taxonomy.ts). Contains real client bid content at L194–758 (Phew portfolio, CREST pen-test, Phew Audit System, PI insurance, G-Cloud positioning, client list Phew Design Limited, NHS, HMRC, …) — the same confidential worked-examples {68.11} already scrubbed from lib/ai/skills/classification.md.
    • scripts/cocoindex_pipeline/prompts.py — the canonical pipeline’s own prompt source.
  • The coordination question (Liam’s caveat): if the canonical pipeline (cocoindex_pipeline) supersedes kb_pipeline and inlines its own prompts, then classification-prompt.md may be retired by the canonical work — in which case it should be relocated-private/deleted rather than scrubbed-in-place, and the scrub effort should target the canonical prompt source if that carries any client examples.
  • RESOLVED (S301a): classification-prompt.md is consumed by kb_pipeline only (config.py:15, bundle-plugin.ts:174, taxonomy codegen) — the canonical cocoindex_pipeline does NOT reference it (verified: no classification-prompt/PROMPT_PATH/ get_system_prompt in scripts/cocoindex_pipeline/**). And kb_pipeline is due to retire (ID-66 PRODUCT :93,277 “kb_pipeline retirement — a separate Curator carry”; Liam: see 07-collapse-list.md / the v1-sequencing doc for the exact timing). Therefore classification-prompt.md retires WITH kb_pipeline — its client bid content leaves the public surface when kb_pipeline is removed, not via an in-place scrub.
  • Design: (1) Do NOT scrub classification-prompt.md in place — confirm the kb_pipeline retirement timing (v1-sequencing) and either (a) gate the {68.9} flip on kb_pipeline (+ classification-prompt.md + the MCP-plugin classification surface) retirement, or (b) if the flip must precede retirement, relocate classification-prompt.md (and repoint the plugin bundle/codegen) rather than scrub. (2) The 4 eval gold-standards (classification/entity/procurement-drafting/summarisation) carry real bid prose (ISO 27001, Telehouse data centre, DBS, employee counts) and are not kb_pipeline-coupled → scrub to synthetic regardless.
  • Risk: MEDIUM. classification-prompt.md is the MCP plugin’s classification surface + reference-doc-paths.json carve-out; scrubbing must keep codegen markers + path-manifest valid. Coordination risk with s302 if scrubbed prematurely.

D — staying-public ledgers / specs / ontology → RATIFIED: D3 hybrid + ledger-CLI guard (§4)

Section titled “D — staying-public ledgers / specs / ontology → RATIFIED: D3 hybrid + ledger-CLI guard (§4)”
  • Steer: “My preference would be to move these across to the private repo … the implications relate to our dev workflow … ledgers and specs are internal workflow related, not platform related. For ontology this should be sanitised, but the ontology itself is core platform functionality … clients [will] customise/configure it.” Liam explicitly requested my recommendation. See §4.

E — {68.10} history purge → avoid sharing any client-specific content

Section titled “E — {68.10} history purge → avoid sharing any client-specific content”
  • Steer: “Whichever approach avoids unnecessarily sharing any client specific content.”
  • Current: {68.10} as specced is path-removal only (git filter-repo --invert-paths) — it does not content-scrub the string “phew” from blobs that stay (the migration, Python tests, fixtures). So identity strings scrubbed at HEAD would still survive in history.
  • Design: extend {68.10} with git filter-repo --replace-text (a redaction map: phew.org.uk→client.example, Phew Design Limited→Example Client Ltd, support@phew.org.uk, kh.phew.org.uk, etc.) applied across all history, in addition to the path-removal set. Mandatory first action: a pre-purge mirror backup (git clone --mirror + tag pre-id68-purge) — the only safety net (purge is irreversible; memory Q1). Run on a fresh mirror; verify zero residual matches before force-push.
  • Open investigation: the exact redaction map (must not break staying code that legitimately needs a value — e.g. generic default config); whether HEAD-scrub-only is acceptable for any low-sensitivity strings (memory: config strings “may be acceptable to leave in history if scrubbed at HEAD”).
  • Risk: HIGH (irreversible). Backup-first is non-negotiable.

F — kh.phew.org.uk prod domain → genericise public refs; keep a private/deploy-side reference

Section titled “F — kh.phew.org.uk prod domain → genericise public refs; keep a private/deploy-side reference”
  • Steer: “Genericise, but keep a reference to the URL given that it’s the current focus for all implementation work … I’d benefit from you knowing at the start of each session the current production domain … unless there’s a better way more closely aligned to how we’ll deploy to clients, which would make the URL a non-issue.”
  • Design: (1) genericise hardcoded public refs — scripts/tests/*.py webhook URLs and any CLAUDE.md prod-URL line read from an env/config value (APP_URL / NEXT_PUBLIC_APP_URL) with a generic example in public source; (2) the deploy-aligned answer Liam intuits: the production domain is inherently per-client deploy config — each client deploy sets its own APP_URL, so the URL is a deploy variable, not source → a non-issue publicly; (3) session-start awareness: record the current live domain in a private/local location that start-session reads — options: the gitignored .env.local (APP_URL=https://kh.phew.org.uk), or a private-repo ops doc (knowledge-hub-internal/docs/runbooks/current-deploy.md). Recommend .env.local + start-session surfacing it, so it tracks the active deploy without any public reference.
  • Risk: LOW. Mostly mechanical genericisation + one start-session convention.

3. Operator-move true scope + private-repo maturation (folds {68.12} remainder)

Section titled “3. Operator-move true scope + private-repo maturation (folds {68.12} remainder)”

The continuation under-scoped the operator move. The actual move-set (verified S301a):

  • 7 workflows (not 6): docs-site-deploy, missing-docs, docs-seo-audit, review-docs-pr, sync-source-docs, docubot, check-for-broken-links.
  • .github/actions/docubot/ (composite action: action.yml + prompt.txt) — used only by docubot.yml, moves with it. Absent from private repo — must copy first.
  • 8 skills: the 6 docs skills + evaluate-workflow + evaluate-findings.
  • workflow-evaluator agent, scripts/skills/run-skill.ts, scripts/docubot/run-agent.ts.
  • .claude/workflows/evaluator-efficiency-sweep.js — dispatches the (moving) evaluator agent; moves with the eval lane (overrides PLAN §2d’s stale “repoint”). Absent from private.
  • ~15 coupled tests — relocate (pure operator-shape/loading) or edit (mixed): skill-workflow-shape, skill-inventory, docubot-action-shape, docubot-workflow-shape, docubot-prompt-loading, run-skill-loading, docs-seo-audit-cron, docubot-prompt-shape, session-a-acceptance, check-for-broken-links, docs-seo-audit, review-docs-pr, sync-source-docs, missing-docs-audit, keep-docs-in-sync-shape, claude-agent-sdk (driver-import test). (umbrellas-schema.test.ts is a false positive — comment only.)
  • tsconfig.json docs-site exclude line — remove (dead after move).
  • @anthropic-ai/claude-agent-sdk — after the drivers move, it is an unused public devDependency (its only consumers are the moving drivers + docubot action). Either remove from package.json (+ lockfile regen) or knip-baseline it.

Private-repo maturation (the real blocker for the move’s gate): knowledge-hub-internal is not a functioning repo — no package.json, no vitest, no CI, bridges unwired. To make the move’s gate (private docs-site dry-run + relocated-guard coverage) real, the private repo needs: (1) a package.json + vitest harness + the test helpers the relocated guards import; (2) the 2b/2f bridges wired (the moved sync-source-docs/docubot/docs-site-deploy workflows mint a GitHub App installation token to checkout the public repo — App + repo-level secrets are installed); (3) a buildable docs-site/ (node_modules + resolvable ../docs spaces) for the dry-run gate; (4) its own CI to actually run the relocated guards.

Recommendation: treat the operator-move as two tracked phases: (i) public-side removal (clean, reversible, low-risk — everything copied to private first) and (ii) private-repo standup (harness + CI + bridges + docs-site build). Phase (i) can land independently; phase (ii) is the larger “make knowledge-hub-internal a real repo” effort, and it is entangled with D (if ledgers/specs also move private, the private repo must also support the SDLC write-path).


4. The D decision — ledgers / specs / ontology routing (recommendation)

Section titled “4. The D decision — ledgers / specs / ontology routing (recommendation)”

Liam asked for my recommendation. The tension: S-B (ratified) deliberately kept ledgers + raw specs public-sanitised precisely to avoid re-introducing the S299 SDLC write-path parameterisation cost. Moving them private is the PLAN’s named “max-security variant” (RELOCATION-IMPLEMENTATION-PLAN.md §0.2.5 sub-note, §3.2 note) and re-incurs that cost.

The cost of move-private (what S-B avoided): scripts/ledger-cli.ts --ledger-dir threading; scripts/regen-mirrors.sh parameterisation; ~39 ledger-cli skill invocations across .claude/skills/*/SKILL.md; 4 ledger roundtrip guards. The KH_DOCS_DIR read-knob already exists (Phase 0) and the GitHub App is installed (cross-repo CI writes possible) — so the residual cost is the write-path parameterisation + the per-session discipline of the orchestrator writing to the sibling/private checkout.

Three options:

OptionWhatProCon
D1 — move ledgers + specs privateRelocate docs/reference/*.json + mirrors + docs/specs/ to knowledge-hub-internal; SDLC writes via KH_DOCS_DIR/AppAligns with “internal workflow, not platform”; true separation; specs keep full fidelityRe-incurs S299 write-path parameterisation; high-churn ledgers written every session → ongoing cross-repo writes; collides with concurrent ledger writers until they settle
D2 — sanitise ledgers + specs in publicReplace client name with a placeholder (Client-1/Example Client) in-place; keep publicLowest infra cost; no write-path changeOngoing discipline (every new entry must be client-clean); specs that structurally describe client work lose fidelity; still exposes internal SDLC process/IP
D3 — hybrid (recommended)Specs → private (lower churn, structurally client-named, genuinely internal IP). Ledgers → sanitise-in-public with a placeholder (high churn; client name is incidental to process-tracking).Balances cost vs separation; avoids parameterising the hottest write-path; removes identity from bothTwo mechanisms; specs move needs the private repo standup (§3.ii)

RATIFIED: D3 (hybrid) — S301a, Liam. Specs are the genuine internal-IP/process artefacts and are low-churn → clean fit for the private repo (and they already mirror there). Ledgers are written continuously and by concurrent sessions → moving them private now is operationally risky and costly; sanitising the client name to a placeholder in-place removes the identity at a fraction of the cost. Either way, execution must wait for concurrent ledger writers to settle (the s302 canonical session is actively writing task-list.json).

Ledger-CLI client-name guard (Liam, S301a) — the forcing function for D3’s ledger half. Add a check to scripts/ledger-cli.ts that rejects (or warns on) any write whose content contains the first client’s name (Phew/Phew Design Limited), at the mutation entry points (journalBlock :2108, promote :3551, updateUmbrella :4023, and the open/add/status paths). This ensures nothing re-introduces the client name after the one-time sanitise sweep. Implement as a small denylist (single value now, list-shaped for future multi-client) so the guard is trivially adaptable. Order: sanitise sweep → land the guard in the same change, so the ledger stays clean by construction.

Ontology: stays public, sanitised — Liam confirms it is core platform functionality (not internal), with client-configurability in scope (clients customise/configure the ontology at deploy or in-use). So: sanitise the residual client reference in docs/ontology/18-entity-aliases.md (and confirm 03-layer-vocabulary/14-bid-states prose done in Phase-0 0f), and design the de-ID so the ontology’s client-customisation seam is preserved (per-client ontology overlays, analogous to the client-config mechanism) — flag as a forward design note, not a pre-flip blocker.


[done] {68.5,6,7,8,11} (de-ID code-scope, content removal, Cloud Run teardown)
{68.13} pre-flip de-ID ── A (phew.json untrack) ─┐
│ B (migration rename) │ (all worker-safe, reversible,
│ C (scrub — AFTER s302 │ land on id68-relocation-phase23)
│ canonical coord) │
│ D3 (specs→private; │
│ ledgers sanitise) │
│ F (domain genericise) │
│ ▼
{68.12} operator-move ── (i) public-side removal ──► Gate: public test+build green
│ (ii) private-repo standup (harness+CI+bridges+docs-site)
│ │
└──────────────────────┬──────────────────────────┘
Pre-flip sanitisation AUDIT (git grep clean on staying public set)
{68.9} FLIP (HUMAN GO) — reversible
{68.10} PURGE (HUMAN GO, LAST) — backup-first + --invert-paths + --replace-text (E)

Coordination gates: C (classification-prompt) and D (ledgers) both intersect the concurrent canonical-pipeline-setup session — sequence them after that work settles, not concurrently.


6. Open investigations to resolve before execution

Section titled “6. Open investigations to resolve before execution”
  1. C / canonical-pipeline — RESOLVED in principle: classification-prompt.md is kb_pipeline-only (cocoindex doesn’t touch it) and kb_pipeline is due to retire. Remaining: confirm the retirement timing in the v1-sequencing / 07-collapse-list.md doc and decide whether {68.9} flip waits for that retirement or relocates the prompt if it must precede it. (Eval gold-standards scrub regardless.)
  2. A / loader: confirm the only static consumer of phew.json is client-config.ts and that public/clients/phew/* is reached only via config-driven paths; design the deploy overlay injection.
  3. B / auth hook: can config.toml reference a config-driven signup domain without redeploy; staging-test the superseding migration before the public-app change.
  4. D / write-path: if specs move private, confirm KH_DOCS_DIR covers the spec read-paths the spec-driven skills use; scope the ledger placeholder-sanitise sweep.
  5. E / redaction map: finalise the --replace-text map; confirm no staying code depends on a redacted literal.
  6. Operator-move: decide claude-agent-sdk dep removal vs knip-baseline; design the private-repo vitest harness for the relocated guards.

7. {68.9} flip-gate update (the AC this plan adds)

Section titled “7. {68.9} flip-gate update (the AC this plan adds)”

Add to {68.9}’s acceptance criteria (currently Dependabot + Liam GO only):

AC-DEID (new): the public HEAD is client-de-identified — git grep -i 'phew' over the public flip-surface returns only (a) parameterised-mechanism config keys (default config, slug examples) and (b) intentionally-retained generic placeholders. Specifically: phew.json

  • public/clients/phew/* untracked (A); the signup-domain migration genericised (B); the classification prompt + eval fixtures scrubbed-or-retired (C); ledgers sanitised + specs relocated-private (D3); kh.phew.org.uk genericised in public source (F); and the operator move-set (docs-site + operators) absent from public HEAD ({68.12}).

{68.10} extends with the --replace-text redaction map + mandatory pre-purge mirror backup (E).


  • S301a investigation (this session): spec-corpus agent, live-triage agent, memory agent — three read-only reports synthesised in the S301a handoff.
  • RELOCATION-IMPLEMENTATION-PLAN.md (S-B routing, Phase 4 gate, §3.2 purge), RELOCATION-INVESTIGATION-V2.md (OQ-R10/R12), ledger docs/reference/tasks/ID-68.md ({68.11} deferral journal :149, cross-cutting flag :60), RESEARCH.md §11 (Phew=client terminology).
  • MemPalace sessions/technical 2026-06-01: “Pre-Flip De-Identification + Client-Configurability Scoping Report” (owner framing; NOW/LATER split) + “Three Decision-Gating Questions” (flip reversible / purge irreversible; eval-fixture-vs-live-tenant distinction).
  • Live verification S301a: lib/client-config.ts:580-607 (static phew.json import + map), scripts/kb_pipeline/config.py:15 (classification-prompt.md PROMPT_PATH), docs/reference/classification-prompt.md:194-758 (client bid content), supabase/config.toml:72-78.