Skip to content

S2: parser conformance across 18 harness versions - #21

Merged
JumpTechCode merged 6 commits into
mainfrom
spike/s2-parser-conformance
Jul 26, 2026
Merged

S2: parser conformance across 18 harness versions#21
JumpTechCode merged 6 commits into
mainfrom
spike/s2-parser-conformance

Conversation

@JumpTechCode

Copy link
Copy Markdown
Collaborator

The last outstanding Phase 0 item. Both published measurements run through the layer that reads a harness's own transcripts, so its ability to read real sessions across releases is upstream of them.

All four pre-registered clauses met — and the fourth was mis-specified.

clause bar result
1 — coverage ≥99% parsed, refusals named 100.00% (1681/1681), 0 refused
2 — no silent misparse invariants hold or reported with version 44 files reported, 0 silent
3 — fail closed on unknown, open on versionless both directions both
4 — redaction planted corpus at 100% 17/17, 0 FPs — and the wrong gate

One parser spans every version

All 18 versions (2.1.1422.1.220), 1,681 files, nothing refused. Every key the parser reads is present in every version for every record type; churn is additive and lands in system metadata a replay never reads. Tool-call pairing is exact — zero dangling tool_results across 35,510. The Session IR can rest on that.

Three assumptions that did not survive

  • session_id is not sessionId. In all 15 disagreeing files it names a sibling transcript — it is a resume/fork pointer. Treating them as synonyms merges unrelated sessions. S3's subject session was one of these files.
  • Sessions move between working directories. 29 files, 15 of 18 versions. Workspace pinning cannot assume one cwd per session.
  • Subagent attribution is not total. 891 of 932 sidecars (95.6%) resolve to a spawning tool call; 41 resolve to none anywhere in their project, across 5 versions, and compaction explains at most 19.

The fail-closed rule needs restating

35% of records (113,911) carry no version field — twelve bookkeeping types that never carry one, four content types that always do, no type inconsistent. Selecting parsers by "the version field each record carries", implemented literally, rejects every real transcript.

And versions arrive every 3.9 days and overlap. Strict per-version allowlisting breaks capture roughly twice a week while buying little, since every field needed was stable across all 18. Recommendation in the summary: fail closed on unknown record type and missing core keys, parse unknown versions, and mark capture fidelity unverified — vocabulary the design already has.

Clause 4 passes and is the wrong gate

17/17 on plants, reached only after three corrections the awkward plants forced. But the clause never measured precision, and scanning the real store fires on 50.3% of files. A masked sample showed the biggest contributors are code: a pricing module's input_micro_per_token= / output_micro_per_token= / cached_input_micro_per_token= field names, matched for ending in "token". A redactor that flags one file in two either blocks every distillation or trains users to waive findings.

Three FP classes found against the real corpus and fixed — risk-disclosure matching an OpenAI key, ${VAR:?must be set} shell defaults, ${VAR}@host templated URLs — each now a permanent benign case, because the invented benign set contained none of them.

No fixture corpus was produced, for that reason.

Two findings from mistakes made while measuring

  • Redaction must be whole-document. My sampling tool masked the matched value but not its context, and printed an adjacent credential in the clear. A per-match redactor exports a fixture with the secret next to the one it removed. (Reported to the user out-of-band with its location; unrelated to this repo.)
  • The planted corpus must be synthesised, never committed. GitHub push protection refused this branch's first push on the Slack plant — correctly. A committed corpus fights push protection, repo scanning, and local scanners, and the escape hatches are worse than the corpus. Plants are now assembled from fragments; the detector finds nothing in its own source. History was rewritten rather than allowlisting a real detection.

Review notes

  • README gains a short section; it leads with the result and ends on redaction not being ready, which is the most limiting statement in it.
  • No transcript content committed — outputs are counts, versions, record types, key names.
  • Corpus is one machine, one user, all 2.1.x over 66 days. The design mentions a "2.0 inline-sidechain era"; no such transcripts exist here, so that era is unmeasured.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Ew64uotJqtvVDN3twMAVEQ

JumpTechCode and others added 6 commits July 26, 2026 10:15
The capture adapter bets on two things: that the transcript format is stable
enough for one parser to span releases, and that the version key is there to
key on. S1 and S3 both run through that adapter, so it is upstream of results
already published.

The schema census that chose the subject is committed alongside, and its
findings are recorded in the pre-registration as inputs rather than results.
18 harness versions, 323,430 records; the four content-bearing record types
appear in all 18 with stable core key sets, and the churn is additive and
concentrated in `system` metadata the IR does not need.

One census finding shapes the bar. 35% of records carry no `version` field —
twelve bookkeeping types that never carry one, against four content types that
always do, with no type inconsistent. The design selects parsers by "the
version field each record carries", so a fail-closed rule written literally
would reject every real transcript in the corpus. Clause 3 requires both
directions: refuse an unknown version, admit a versionless bookkeeping record.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ew64uotJqtvVDN3twMAVEQ
A version-keyed parser that extracts exactly what S3 showed the Distiller
needs — human turns, tool calls with their inputs, tool results, model, cwd,
and the link from a subagent sidecar back to the tool call that spawned it.
A measuring instrument, not the product's capture adapter.

Conformance over the local store: 1681 files, 100.00% parsed, none refused,
all 18 versions. Zero dangling tool_results across 209,701 content records,
so the pairing invariant the Session IR wants to rest on is a property of
real transcripts rather than an assumption.

The redaction pass needed three fixes before it met its clause, and the
planted corpus earned its keep by being awkward on purpose:

- a password containing `&` escaped a value class that stopped at punctuation;
- `AWS_SECRET_ACCESS_KEY=` never matched, because `_` is a word character so
  `\bsecret\b` cannot match a keyword buried in an env-var name — that one
  would have missed real env dumps, not just the plant;
- base64-wrapped secrets decoded to an invalid length, because anchoring `\b`
  after the `=` padding excluded the padding from the match.

Now 17/17 caught, 0 false positives on the benign corpus, and every caught
value verifiably gone from the redacted text.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ew64uotJqtvVDN3twMAVEQ
The planted corpus scored 17/17 with zero false positives on a benign set,
and the benign set was the problem. Scanning the real store fired on 50.9% of
files, and a masked sample of those hits showed the detector was largely
finding its own shadow:

- `openai-key` matched the word "risk-": `sk-` followed by 32 word characters
  is satisfied by every `2026-03-27-risk-disclosure.md` in the corpus;
- `assigned-secret` matched `${POSTGRES_PASSWORD:?must be set}`, a shell
  default that references a secret rather than containing one;
- `url-credentials` matched `${POSTGRES_PASSWORD}@host`, the same mistake in a
  connection string.

Narrowing those required one correction of its own: an initial "a path is not a
credential" rule matched any `word/word` and swallowed
`wJalrXUtnFEMI/K7MDENG/…`, because an AWS secret key contains slashes and looks
exactly like a relative path. The rule now needs a leading separator or a
trailing extension.

Every one of those false positives is now a permanent case in the benign
corpus. The invented benign set contained not one of them, which is the point:
a detector is only as honest as its hard cases, and these came from data the
author did not choose.

Also reports invariant violations per version, which clause 2 asked for and
which separates standing format properties from release churn.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ew64uotJqtvVDN3twMAVEQ
All four pre-registered clauses met, and the fourth was mis-specified.

One parser read 1,681 files across all 18 harness versions with nothing
refused, because every key it reads is present in every version for every
record type. Tool-call pairing is exact: zero dangling tool_results across
35,510. The Session IR can rest on that.

Three things it cannot rest on. `session_id` is not a duplicate of `sessionId`
— in all 15 disagreeing files it names a sibling transcript, so it is a
resume pointer and treating the two as synonyms misattributes sessions. 29
files move between working directories mid-session, across 15 of 18 versions,
so workspace pinning cannot assume one. And 4.4% of subagent sidecars resolve
to no tool call anywhere in their project, so subagent work has to be reported
as unattributed rather than dropped.

The fail-closed rule needs restating. 35% of records carry no version field at
all, so selecting parsers by "the version field each record carries" rejects
every real transcript when implemented literally. And versions arrive every
3.9 days and overlap, so strict per-version allowlisting breaks capture about
twice a week while buying little — every field the parser needs was stable
across all 18.

Clause 4 passes and is the wrong gate. 17/17 on plants, but scanning the real
store fires on 50.3% of files, and the largest contributors are code: a
pricing module's `*_micro_per_token=` field names, matched for ending in
"token". Recall against secrets the author planted is easy; precision on real
transcripts is the hard part, and a redactor that flags one file in two either
blocks every distillation or trains users to waive findings.

No fixture corpus was produced, for that reason.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ew64uotJqtvVDN3twMAVEQ
Both published measurements run through the transcript-reading layer, so its
ability to read real sessions across harness versions is upstream of them and
belongs in public.

Leads with the result and then with what did not survive: one parser read all
1,681 local transcripts across all 18 versions, and three assumptions failed —
a session id that is really a resume pointer, sessions that move between
working directories, and subagent transcripts that cannot be traced to the call
that started them.

The redaction paragraph is deliberately the most limiting thing in the section.
Exams are meant to be shareable and the scanner is not trustworthy enough yet,
so no shareable fixtures were produced. Saying that plainly now is cheaper than
discovering it after someone exports one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ew64uotJqtvVDN3twMAVEQ
GitHub's push protection refused the first push of this branch, correctly, on
the Slack plant in the planted-secret corpus. The literals are now assembled
from fragments at run time, so no contiguous string in the file matches a
scanner — the detector finds nothing in its own source — and the plants still
score 17/17.

The block is itself a finding, recorded in the results. A committed
planted-secret corpus fights every scanner it meets: the host's push
protection, the repository's own secret scanning, and whatever the developer
runs locally. The only ways through are to allowlist real detections or to
turn scanning off, and both are worse than the corpus is worth. The design
proposes exactly this corpus running in CI, so it has to be synthesised at
test time.

History was rewritten to remove the literal from the two commits that carried
it, rather than allowlisting a real detection.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ew64uotJqtvVDN3twMAVEQ
@codecov

codecov Bot commented Jul 26, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@JumpTechCode
JumpTechCode merged commit dd1b324 into main Jul 26, 2026
6 checks passed
@JumpTechCode
JumpTechCode deleted the spike/s2-parser-conformance branch July 26, 2026 16:37
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