Skip to content

Write the record to disk as a bundle a stranger can verify - #650

Merged
xmap merged 2 commits into
mainfrom
worktree-record-bundle-writer
Aug 12, 2026
Merged

Write the record to disk as a bundle a stranger can verify#650
xmap merged 2 commits into
mainfrom
worktree-record-bundle-writer

Conversation

@xmap

@xmap xmap commented Aug 12, 2026

Copy link
Copy Markdown
Owner

Two commits finishing the record exporter's Definition of Done. Seven
merged steps produced structures in memory and never an artifact, so
nothing had ever verified end to end.

Bundle writer + H3 (5699dfd293). Writes manifest.json,
streams.jsonl, logbooks/<kind>.jsonl. The line format is NOT what
the hashes cover, so a verifier must reassemble the body before
hashing; that reassembly deliberately exists twice, once in the package
and once in scripts/verify_record_hash.py, because a checker
importing CORA's reassembly would only confirm CORA's own idea of what
the bundle says. A fitness test pins the script's zero-import property.
H3, the published record's own hash, lands here with a payload type
distinct from H1's so the two can never collide; None means "not a
published projection", and --published refuses rather than falling
back to H1. Writing into a non-empty directory refuses, and the
manifest is written last so an interrupted export leaves something
read_bundle_body rejects.

Unfired tier-2 clearances (9144168891). redact_record used to
raise unless every declared activity/payload clearance fired. Those
keys live on different step kinds and two are optional, so no narrow
export reliably fires all three. The check applied a denylist's threat
model to tier 2's allowlist, where an unfired rule means a field was
published LESS than the profile permits, never more. It now reports the
(kind, column, pointer) triples onto the manifest as a completeness
fact. The genuine worry underneath, a misspelled pointer that can never
fire, gets its own build-time check instead.

Ready for the first live rehearsal against the 2-BM record.

Refs: project_record_export_v3.md F5, project_record_export_build_brief.md

🤖 Generated with Claude Code

xmap and others added 2 commits August 11, 2026 19:14
Seven steps built an exporter that produced structures in memory and
never an artifact. The Definition of Done was "a rehearsal runs, the
exporter writes a bundle, the standalone verifier passes on it in a
subprocess" and nothing wrote the bundle, so nothing had ever verified
end to end.

The layout is the one the design fixed: manifest.json, streams.jsonl,
logbooks/<kind>.jsonl. JSON Lines because a reviewer can stream, grep
and diff it per row. The trap that carries is that the line format is
NOT what the hashes cover, so a verifier must REASSEMBLE the body
before hashing. Reassembly therefore exists twice, once in the package
and once in the standalone script, and the duplication is the point: a
checker importing CORA's reassembly would confirm CORA's own idea of
what the bundle says. That is not a check. A new fitness test pins the
script's zero-import property, because the duplication is a standing
invitation to deduplicate it away and its failure mode is silent.

The gap that only appears end to end: a bundle whose files are each
individually well-formed can still be missing a whole logbook kind.
Per-file verification passes; only the reassembled body's hash catches
it. Both suites assert exactly that, alongside tamper and line-reorder
sensitivity, because order is part of the record rather than
presentation.

H3 lands here too, as step 7 asked, not as an afterthought: the
published record's own hash, the one a paper prints beside its locator
and the only one covering bytes a reader can hold. Its payload type is
deliberately distinct from H1's, so an export whose redaction happened
to drop nothing still hashes differently as a published record than as
a full one. They must never collide. The manifest carries it as
optional, and None means "not a published projection" rather than
"redaction found nothing": the absence is the signal, and the verifier
refuses --published against a bundle lacking it rather than falling
back to H1.

Writing into a non-empty directory refuses, and the manifest is written
last, so an interrupted export leaves something read_bundle_body
rejects rather than a complete-looking bundle with truncated rows.

Found while writing the end-to-end test and NOT fixed here, because
loosening a fail-closed check is a security decision rather than a
bundle writer's business: ensure_all_clearances_fired refuses unless
every declared activity/payload clearance fires, and those three keys
live on different step kinds, two of them optional. redact_record
therefore refuses any export whose activities lack a setpoint-with-units
AND an action AND a check. It fails closed, so nothing leaks, but the
first small rehearsal bundle would hit it and read as a config error.
Step 6's own fixture already works around it with a comment reasoning
that a real export "would plausibly exercise every step kind" -- an
assumption about data holding up a hard refusal. Documented at both
sites; owed a decision.

Refs: project_record_export_v3.md F4/F5, project_record_export_build_brief.md

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`redact_record` used to raise unless every declared activity/payload
clearance (channel, action_name, units) fired. Those three keys live on
different step kinds and two are optional, so no narrow export -- most
likely the first rehearsal bundle -- reliably fires all three. The
check reasoned from a denylist's threat model (an unfired rule that
should have hidden something is a leak) and applied it backwards to
tier 2's allowlist, where an unfired rule means a field was published
LESS than the profile permits, never more. There is no mechanism by
which that leaks anything.

unfired_clearances now returns the (kind, column, pointer) triples
instead of raising, and RedactionResult / build_manifest thread the
result onto the manifest as a completeness fact a reader can see, not
a reason to refuse. A new regression test seeds the exact shape that
used to abort (one setpoint, no units, no action, no check) and asserts
it now redacts, verifies against H3, and the manifest names the two
clearances the export was too narrow to exercise.

The genuine worry the old check was reaching for -- a misspelled
pointer in TIER2_JSONB_CLEARED_POINTERS that can never fire against any
real payload -- is a fact about this file's code, not about any one
export, and now has its own build-time check: test_tier2_jsonb_
clearances_are_real_keys.py. outcome.measurements gets a real
introspection check against Measurement's actual dataclass fields;
activity.payload has no typed contract, so it hand-encodes the known
keys from append_activities/route.py's docstring plus the envelope
keys conductor.py's _append_step merges into every kind. Both checks
verified to fire on an injected typo and restore clean.

Writing that hand-encoded key list surfaced two things beyond the
completeness check's own scope, recorded in its docstring rather than
acted on here: `STEP_KIND_VALUES` has five members and the payload
docstring documents three (capture/compute are undocumented), and
`result` -- a string closed in practice to exactly three constants
(_RESULT_OK/_RESULT_FAILED/_RESULT_IN_FLIGHT, verified by grepping
every call site) -- is written on every conductor-driven activity row
and is not in the clearance set, so the published record cannot
currently distinguish a step that succeeded from one that failed.
Not added here: changing what gets published is a content decision,
not a typo check.

Refs: project_record_export_v3.md F5, project_record_export_build_brief.md

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

Coverage report

Click to see where and how coverage changed

FileStatementsMissingCoverageCoverage
(new stmts)
Lines missing
  apps/api/src/cora/infrastructure/record_export
  __init__.py
  _bundle.py 92-93, 170-171
  _hashing.py
  _manifest.py
  _redact_tier2.py
  _redaction.py
Project Total  

This report was generated by python-coverage-comment-action

@xmap
xmap merged commit 1be3770 into main Aug 12, 2026
19 checks passed
@xmap
xmap deleted the worktree-record-bundle-writer branch August 12, 2026 11:28
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