BF-07 — Cited Context Projection (buildCcp): span-cited compact serialization + U-shape + offline token measurement - #31
Conversation
…o ccp/** + contract reconcile Operator prep for the Cited Context Projection slice — the off-maker-floor changes the maker diff builds ON, so HEAD-vs-base stays inside allowedPaths. - packages/core: add gpt-tokenizer@3.4.0 (MIT, zero transitive deps, bundled ranks, fully offline) for the CCP token-measurement hook (acceptance #6). - sgrules/no-egress-in-search-path.yml: widen `files` scope to packages/core/src/ccp/** so the CCP serializer/tokenizer inherits the no-PHI-egress structural floor (BP-035 documented sibling-helper follow-up); ast-grep 10/10. - contract reconcile: drop the DEAD loop/evals/corpus/bf-07/** allowedPath (GLOBAL_FORBIDDEN loop/** shadows it) — the 6 bf-07-* Stage-2 evals are the operator post-merge wave; golden fixtures self-seed hermetically in-test. - ledger: BF-07 active (de-risk + merged Opus design-review deltas summarized). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… serialization
Implements the Cited Context Projection over a BF-06 SearchResponse: buildCcp
runs inside the caller's withTenant tx and returns a Result<CcpDocument,CcpError>.
- schemas/errors/leaf-paths/ushape/serialize/token-count/content-digest/receipt/
build-ccp under packages/core/src/ccp/** + co-located tests (47 pass).
- Security spine (each guard inversion-proven load-bearing):
* ONE RLS-bounded id-set read is the whole cross-tenant guarantee; no other
fhir_resources/search_doc query exists in ccp/** (fhirReads==1, Class 2).
* pre-parse MALFORMED_INPUT appends no audit (nothing read); every post-parse
path appends exactly one audit row (T8); span.auditHash == that row_hash.
* receipt cross-check (tenant/purpose/actor/decision) → RECEIPT_MISMATCH;
count-only UNRESOLVED_RESULT (no id oracle); TYPE_MISMATCH; and a re-derived
deriveScope() closes the latent fail-open the contract's dangerChecks dropped.
* every span value JSON-encoded (injection-safe, losslessly invertible);
declared leaf paths are scalar-only (throw on non-scalar).
* content digest over {spans, text, sourceAuditEventId} binds value + prose +
replay(versionId) + provenance; folded into the audit row reason.
- token-measurement hook (gpt-tokenizer o200k, offline): CCP >= 1.4x leaner than
compact-JSON of the identical span set (measured 4.31x live / 4.89x golden).
- docs/adr/0004 records the 8 decisions + accepted limits.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…w (B) Disclosed operator wave applying the adversarial review panel's findings (3 independent agents converged on both breaks). All within ccp/** + docs/adr/**. - Finding A (HIGH): serialize.ts interpolated the untrusted header sourceAuditEventId and the withheld-type resourceType/reason RAW, so a hostile excludedByPolicy reason with newlines could forge group-header + span lines into the agent-consumed text (indirect prompt injection) and the content digest would notarize the forgery. Fix: JSON-encode all three, matching the span-value Class-5 pattern; document stays losslessly invertible. +2 injection guards. - Finding B (MED): resolvePath threw on a non-scalar stored leaf (the write path accepts arbitrary nested JSON), escaping buildCcp's Result boundary before the audit append (violating acceptance #1 + T8). Fix: fail-closed SKIP (return undefined — value still never emitted). +1 DB guard (ok + audited, no throw), leaf-paths throw-tests reframed to skip. - ADR 0004: records both fixes + accepted residuals (app-level type check rationale, subject/receipt caller-contract trust, golden-set token ratio). Both fixes inversion-proven load-bearing (raw-interp -> injection test RED; throw -> boundary test RED). ccp 51/51, gate 23/23. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…inst planner reshape CI build-test/test went red on eval:bf06-hybrid-index-used — a PRE-EXISTING BF-06 eval fragility (documented planner-cost dependence), surfaced by BF-07 adding DB tests that commit rows to the SHARED search_doc table. The eval is RLS-scoped to its own 20-doc practice, but the planner chooses the plan on TABLE-LEVEL stats, so as total search_doc grows the plan reshapes: the "RRF float fusion" check keyed on the literal `1.0` AND `Hash|GroupAggregate`, but the planner renders the fused score in the final Sort Key either as the alias `fused.rrf` (no `1.0`) or the inlined `sum((1.0/((60+rank))...))` (no `rrf` alias) — a false red on the alias form. Fix: assert the group-by aggregate node (`/Aggregate/` — the ONLY aggregate; the row_number WindowAgg does not match) plus either render of the fused score (`rrf` OR `1.0`). Corpus-independent. Float-vs-integer RRF correctness (T1) stays covered by the functional ranking + determinism evals. Reproduced the red locally by growing search_doc to ~1150 rows; the fix passes clean, at 1150, and at 2000+. Off the BF-07 maker floor (loop/**), disclosed operator work per the BF-06 race-fix precedent (9874a80). No product-code change; the six other plan assertions (HNSW index, GIN content_tsv predicate, inline scope, RLS InitPlan, no fhir_resources scan) are untouched and remain load-bearing. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CI follow-up: hardened a pre-existing planner-fragile BF-06 evalThe first CI run went red on
The maker+wave diff (through |
…efer-includes)
My eval-hardening introduced /Aggregate/.test(plan); @typescript-eslint/prefer-includes
flags a plain-string regex (the original /GroupAggregate|HashAggregate/ was exempt via
the | alternation), which surfaced only under full-program typed lint. plan.includes
('Aggregate') is semantically identical (covers Aggregate/Group-/HashAggregate) and clean.
|
✅ All 12 CI checks green. The two follow-up fixes resolved it: (1) the planner-fragile The maker + panel-wave diff (through |
BF-07 — Cited Context Projection (CCP)
buildCcpis the agent's default read surface: it serializes one policy-scoped BF-06SearchResponseinto a compact, span-cited document where every span carries(resourceId, jsonPath, auditHash, value), ordered U-shape, with an offline token-measurement hook. Raw FHIR stays an explicit escape hatch. dependsOn BF-05 + BF-06 (both merged).Commit chain
a64e937prep (gate base) — offlinegpt-tokenizer@3.4.0dep; BP-035 no-egress sgrule widened toccp/**(documented sibling-helper follow-up); contract-drift reconcile (removed the deadloop/evals/corpus/bf-07/**allowedPath —loop/**is GLOBAL_FORBIDDEN); ledger BF-07 active.c0414admaker —ccp/**(schemas, leaf-paths, ushape, serialize, token-count, receipt, content-digest, build-ccp) +index.tsexports + ADR + 47 co-located tests.1aa90d2panel wave — fixes the two real breaks the review panel found (below).Security spine (each guard inversion-proven load-bearing)
where id = any(<result ids>::uuid[]), nopractice_idpredicate; FORCE RLS + NOBYPASSRLS bound the tenant. No secondfhir_resources/search_docquery exists inccp/**(fhirReads==1asserted on execution).TYPE_MISMATCH), policy-excluded types (re-derivedderiveScope— closes the fail-open-authz class the contract's dangerChecks dropped), and receipt laundering (RECEIPT_MISMATCH).span.auditHash == row_hash; the content digest over{spans, text, sourceAuditEventId}is folded into the auditreason(inside the RFC-8785 preimage) so value-, prose-, and replay-tamper all break the chain.ccp/**makes an off-box call; the tokenizer is bundled/offline (BP-035 sgrule coversccp/**).Adversarial review panel (5 agents)
Verifier PASS (no defect). Cross-tenant/scope refuter HOLD (live-probed). Injection refuter + audit-bypass refuter + security-auditor (3 independent) converged on two real breaks, both fixed on this branch in
1aa90d2:sourceAuditEventIdand withheld-typeresourceType/reasonwere interpolated raw, so a hostileexcludedByPolicy.reasonwith newlines could forge group-header/span lines into the agent-consumed text (indirect prompt injection), which the digest would then notarize. Fixed: JSON-encode all three (the existing span-value pattern); document stays losslessly invertible. Guarded by two injection tests.resolvePaththrew on a non-scalar stored leaf, escaping the Result boundary (acceptance BF-01: Workspace and Docker boot scaffold #1) before the audit append (T8). Fixed: fail-closed skip (return undefined — value still never emitted). Guarded by a DB test (audited ok, span skipped, no throw).Accepted residuals (disclosed in ADR 0004): app-level canonical-type scope check (deliberate — binds to stored type, keeps distinct error codes; same-tenant, never emitted, fails closed); CCP trusts the in-process
subject/receipt (a forged response can only affect its OWN tenant — subject-from-membership is the caller contract BF-06 also owes); the ≥1.4× ratio is a golden-set property.Verification (local, DB_HOST_PORT=55432)
bun run gate→ 23/23;bun test packages/core/src/ccp→ 51/51; strict slice gateloop gate --slice BF-07 --base a64e937 --strict→ 14/14 at HEAD.Token residual (gpt-tokenizer o200k, offline)
≈4.3× live build / 4.9× golden vs the 1.4× floor; ADR discloses the ~2.5× hash-neutral baseline so the metric can't be called gamed. Serialization-residual lever only — the 10–100× slice reduction is BF-06's, not re-claimed.
Out of scope (documented follow-ups)
The 6
bf-07-*Stage-2 evals + ledgerdone+ accp-text-losslessly-invertibleeval + an ast-grep rule banning raw response-string interpolation are the operator post-merge wave (loop/**is off the maker floor), matching every prior slice. Greptile waived (not wired).The local allowed-paths gate is clean (diff base→HEAD is entirely
ccp/**+index.ts+docs/adr/**); the off-floor prep artifacts (dep, sgrule, contract reconcile, ledger) are the disclosed gate base per BF-04/05/06/13 precedent. CI runs standard static+build+test on a fresh runner.🤖 Generated with Claude Code