Skip to content

BF-07 — Cited Context Projection (buildCcp): span-cited compact serialization + U-shape + offline token measurement - #31

Merged
AJ112103 merged 6 commits into
mainfrom
bonfire-BF-07
Jul 10, 2026
Merged

BF-07 — Cited Context Projection (buildCcp): span-cited compact serialization + U-shape + offline token measurement#31
AJ112103 merged 6 commits into
mainfrom
bonfire-BF-07

Conversation

@AJ112103

Copy link
Copy Markdown
Contributor

BF-07 — Cited Context Projection (CCP)

buildCcp is the agent's default read surface: it serializes one policy-scoped BF-06 SearchResponse into 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

  • a64e937 prep (gate base) — offline gpt-tokenizer@3.4.0 dep; BP-035 no-egress sgrule widened to ccp/** (documented sibling-helper follow-up); contract-drift reconcile (removed the dead loop/evals/corpus/bf-07/** allowedPath — loop/** is GLOBAL_FORBIDDEN); ledger BF-07 active.
  • c0414ad makerccp/** (schemas, leaf-paths, ushape, serialize, token-count, receipt, content-digest, build-ccp) + index.ts exports + ADR + 47 co-located tests.
  • 1aa90d2 panel wave — fixes the two real breaks the review panel found (below).

Security spine (each guard inversion-proven load-bearing)

  • Single RLS-bounded id-set read is the whole cross-tenant guarantee — one where id = any(<result ids>::uuid[]), no practice_id predicate; FORCE RLS + NOBYPASSRLS bound the tenant. No second fhir_resources/search_doc query exists in ccp/** (fhirReads==1 asserted on execution).
  • Fail-closed on unresolved ids (count-only, no id oracle), type confusion (TYPE_MISMATCH), policy-excluded types (re-derived deriveScope — closes the fail-open-authz class the contract's dangerChecks dropped), and receipt laundering (RECEIPT_MISMATCH).
  • Audit: pre-parse malformed appends no row; every post-parse path appends exactly one (T8); span.auditHash == row_hash; the content digest over {spans, text, sourceAuditEventId} is folded into the audit reason (inside the RFC-8785 preimage) so value-, prose-, and replay-tamper all break the chain.
  • No PHI egress — nothing under ccp/** makes an off-box call; the tokenizer is bundled/offline (BP-035 sgrule covers ccp/**).

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:

  • Finding A (HIGH) — serializer line-injection. The untrusted header sourceAuditEventId and withheld-type resourceType/reason were interpolated raw, so a hostile excludedByPolicy.reason with 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.
  • Finding B (MED) — non-scalar leaf threw past the boundary. The write path accepts arbitrary nested JSON; resolvePath threw 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 gate23/23; bun test packages/core/src/ccp51/51; strict slice gate loop gate --slice BF-07 --base a64e937 --strict14/14 at HEAD.
  • Clean-room fresh-volume boot (migrate→seed→terminology→projections): ccp 47→51/0, core 247/0 (hermetic, BP-024).
  • Reproduced CI-only static stages: biome clean · jscpd 0 clones · knip exit 0 · semgrep 0 findings.
  • 6 operator adversarial inversions (audit-bypass, fail-open scope, receipt laundering, injection, + the two panel-fix inversions) each reddened only the owning tests, restored green.

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 + ledger done + a ccp-text-losslessly-invertible eval + 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

AJ112103 and others added 5 commits July 10, 2026 05:33
…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>
@AJ112103

Copy link
Copy Markdown
Contributor Author

CI follow-up: hardened a pre-existing planner-fragile BF-06 eval

The first CI run went red on eval:bf06-hybrid-index-used (a BF-06 eval), while every actual test suite passed (core 251/0 incl. all ccp tests, sql-on-fhir 133/133). Investigation:

  • Not a BF-07 regression. BF-07's diff is byte-identical to main for packages/core/src/search/**, loop/src/evals/**, and scripts/search-demo/** (0 lines). The eval fails identically on main against the same stack.
  • Root cause (reproduced locally). The eval is RLS-scoped to its own 20-doc practice, but the Postgres planner chooses the plan on table-level search_doc stats. BF-07 adds DB tests that commit rows to that shared table; as it grows, the planner reshapes the plan and renders the fused RRF score in the final Sort Key as either the alias fused.rrf (no 1.0) or the inlined sum((1.0/((60+rank))...)) (no rrf alias). The old check keyed on the literal 1.0 and Hash|GroupAggregate, so it went falsely red on the alias form. This is the documented planner-cost fragility from the BF-06 handoff.
  • Fix (34ca01d, disclosed off-floor operator work per the BF-06 race-fix precedent 9874a80): assert the group-by aggregate node (/Aggregate/ — the only aggregate; the row_number WindowAgg doesn't 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. The other five plan assertions (HNSW node, GIN content_tsv @@ predicate, inline scope, RLS InitPlan, no fhir_resources scan) are untouched and remain load-bearing. Reproduced the red locally at ~1150 shared rows; the fix passes clean, at 1150, and at 2000+, and the loop package typechecks + 199 tests pass.

The maker+wave diff (through 1aa90d2) remains pure ccp/** (strict slice gate 14/14); this eval hardening is loop/** operator/harness territory, disclosed here. It follows a pre-existing, non-blocking loop-package lint warning (allowed-paths.ts prefer-includes on a segment-membership check — 0-error under CI's eslint) that the local strict gate escalates once loop/** is in scope; not introduced by this PR.

…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.
@AJ112103

Copy link
Copy Markdown
Contributor Author

All 12 CI checks green. The two follow-up fixes resolved it: (1) the planner-fragile bf06-hybrid-index-used eval hardening (above), and (2) a prefer-includes lint error I introduced with the fix — /Aggregate/.test(plan) is a plain-string regex that @typescript-eslint/prefer-includes flags under full-program typed lint (the original /GroupAggregate|HashAggregate/ was exempt via its |), which eslint's cache masked on the first run; plan.includes("Aggregate") is semantically identical and clean.

The maker + panel-wave diff (through 1aa90d2) remains pure ccp/** (strict slice gate 14/14); the loop/** eval hardening is disclosed operator/harness work per the BF-06 race-fix precedent. Ready for review — stopping for human merge.

@AJ112103
AJ112103 merged commit 5a622ea into main Jul 10, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant