Skip to content

Publishing-integrity campaign: a published record can be checked against its data - #654

Merged
xmap merged 8 commits into
mainfrom
worktree-record-publishing-integrity
Aug 13, 2026
Merged

Publishing-integrity campaign: a published record can be checked against its data#654
xmap merged 8 commits into
mainfrom
worktree-record-publishing-integrity

Conversation

@xmap

@xmap xmap commented Aug 13, 2026

Copy link
Copy Markdown
Owner

Summary

F6: the published record of the first real 2-BM scan was scientifically
empty. The checksum, name, media type, intent, and conducted-step detail
all dropped, not by a redaction decision but because the redaction
profile was written from a field's DECLARED name and applied against
the STORED wire key, with nothing checking the two agree. Five slices
answer it:

  • 7644b6e1f2 Bind the record bundle to the manifest describing it (P0-A, P0-B, P1-c)
  • c77e531778 Make the manifest state facts that are true (P0-D, watermark, unfired-tier-1 twin)
  • 3be8a8d990 Key the disposition table on what the store writes, not the field name (the checksum, F6 part 1)
  • 7276097f82 Show what the Conductor did, not what a stale docstring guessed (P0-C: address, criterion, reading, result)
  • efc51899eb Type Acquisition evidence as a VO so a record can say what it captured (F6 part 2)
  • 66661aa9c6 a coverage gap the merge itself surfaced (see below)
  • f3ecac7587 a test constant that had drifted into pinning the real production digest (see below)

Slices 6 and 7 were built independently in parallel sessions off the
slice-5 tip and had never run against each other; hash_redaction_profile
(H2) is computed over inputs both of them touch. Merging and running the
full suite plus diff-coverage on the COMBINED tree (this PR) found one
real gap neither branch's own scoped run could see: validate_evidence's
float-field type-rejection branch had no test. Fixed in 66661aa9c6.
Diff-coverage on the full 5-slice diff against main is 100% (139 lines).

Each slice went through a 4-lens gate review (architecture, test
coverage, cross-BC consistency, plus security as declared specialist,
since these slices amend the table deciding what a published record
discloses). Slice 5's panel caught a real privacy regression before
commit (a missing extra=(ValueError,) that would have echoed a raw
payload value on a malformed stored VO).

Live verification, run 2026-08-13

Ran against the real arcturus database (not a test fixture), read-only,
from an isolated worktree so the running service was untouched:

  • redaction_profile_hash matched exactly what this session computed
    locally on the integrated tree: 90e5438654c9...a1f1ee8.
  • The checksum digest is present in the published bundle, on both the
    Dataset and Distribution registration rows, byte_size matching the
    real file exactly. The dataset_id beside it is a per-export random
    surrogate (tier-1 tokenization working as designed), not the real id.
  • Default verify-bundle on the published bundle: exit 2, "use
    --published", not MISMATCH. --published: OK. The full bundle
    in default mode: OK.

Caught by this run: settling which of two identical-looking values (the
plan's cited checksum vs. the slice-5 test's placeholder) was the real
one required a direct, authorized read-only query against arcturus. The
test's placeholder turned out to have silently become the real
production digest, contradicting its own comment. Fixed in f3ecac7587.

The paper claim in object.tex is unblocked: a published record of
the real 2-BM scan is checkable against the data it describes, proven
against the live database.

Known follow-ups, not blocking this PR

  • Slice 6 found a SECOND real writer of activity.payload (direct
    append_activities submissions across ~16 test_2bm_* scenarios),
    left as an open, unbriefed follow-up.
  • Slice 7 found Attestation has the IDENTICAL drop:opaque evidence
    problem, unfixed.
  • The parity guard added in slice 5 has a documented blind spot: 18
    event classes (including AssetRegistered and all Supply lifecycle
    events) build their payload by assign-then-mutate rather than a dict
    literal, so are not statically checked.

Full detail in the campaign memo (project_record_publishing_campaign.md
in the project's auto-memory).

Test plan

  • make lint && make typecheck && make arch-check green on the merged tree
  • Full data/operation/record-export unit + integration surface: 2264 passed
  • diff-cover against main: 100% (139 lines, 0 missing)
  • Gate review (4 lenses) on each of slices 3, 4, 5 individually before merge
  • Live 2-BM re-export on arcturus (read-only): checksum digest confirmed present in a real published bundle, redaction_profile_hash cross-checked against the local build, verifier refusal behavior confirmed correct

🤖 Generated with Claude Code

xmap and others added 7 commits August 12, 2026 13:56
write_bundle took a record and a manifest as unbound arguments with
nothing checking that the manifest was actually built from that record.
The default verifier could print OK on a bundle that claimed to be a
published projection but was byte-for-byte the unredacted record. The
manifest's own expansion_digest_presence_by_run also republished raw
Run stream_id values in plaintext, keyed by the identifiers tier-1
redaction had already replaced with per-export surrogates elsewhere in
the same bundle, and the standalone verifier's default mode against a
genuinely published bundle printed MISMATCH, indistinguishable from
tampering.

write_bundle now recomputes whichever hash (H1 or H3) the manifest
claims over the record it was actually handed, before writing anything,
and refuses on disagreement. build_manifest takes the whole
RedactionResult rather than three independently-omittable parameters,
so a caller cannot supply a token map from an unrelated redaction or
silently drop the unfired-clearances report. The verifier gained a
symmetric structural guard for the case it was missing.
_is_simulated used all(), vacuously True on zero observation rows: the
first genuine beamline-attached export had none and was labelled
simulated. Changed to any(), matching the Run BC's own fold of the same
column (bool_or, empty false).

export_record captured a watermark, bound its query with it, and then
discarded it; build_manifest took watermark as a separate parameter no
caller could actually fill with the value the query used without
capturing a different snapshot. ExportedRecord now carries the
watermark it was bounded by, and build_manifest reads it from there.

hash_redaction_profile (H2) covered the generated per-field disposition
table and tier 2's hand-authored tables, but not tier 1's own
hand-authored fixed-column dispositions (the ones deciding whether
principal_id tokens or signature drops for every event, unconditionally).
Moving signature from DROP to KEEP would not have moved H2 at all. Both
are now in the hashed body.

Added unfired_tier1_fields, the tier-1 twin of the existing
unfired_tier2_clearances: a completeness fact naming declared fields
that never appeared on any row of an event type this export carried, so
a narrow export's coverage gaps are visible on the manifest rather than
silently assumed complete.
The redaction disposition table was generated from each event dataclass's
FIELD NAMES, but redaction looks a field up by the STORED PAYLOAD's actual
JSON key. Nine event types had a field-name/wire-key mismatch, so the real
rule for that field could never fire and the value dropped by the
unlisted-key fail-closed default instead, silently, forever. For
DatasetRegistered and DistributionRegistered this meant the checksum, the
one field that makes a published record checkable against the data it
describes, has never survived into a published bundle.

Fix: reshape the nine event dataclasses so the declared field is the field
actually stored (checksum/encoding as nested VOs, facility_code as
facility_id, etc.); to_payload/from_stored keep the same on-disk shape, only
the Python field names moved to match it. A new static AST test pins
disposition-table keys against to_payload's literal dict keys so a tenth
mismatch cannot ship unnoticed for any event whose payload is a plain dict
literal.

Closing the type gap (str -> DatasetChecksum/Intent) needed one more piece:
the generator resolves a field's disposition from its DECLARED TYPE, so a
value already closed by its own constructor still drops if it is typed
bare str. DatasetChecksum marks itself ClosedValueObject, a new marker in
cora.shared, so the generator keeps the whole VO instead of recursing into
a hex digest as though it were free text. docs/reference/modeling.md's
"primitives in events" rule gets a carve-out for this, scoped to fields
whose value set is genuinely closed.

An end-to-end test registers a Dataset and Distribution through the real
command handlers and confirms the checksum digest is readable in a bundle
written to disk, closing the exact gap the retroactive gate review found in
the first real 2-BM scan's published record.

Gate-review panel (architecture, test coverage, cross-BC consistency, plus
security as declared specialist for this disposition-table amendment)
caught and fixed: from_stored for both reshaped events was missing
extra=(ValueError,) on deserialize_or_raise, so a malformed stored VO would
have raised an uncaught ValueError echoing the raw payload value, defeating
the no-payload-echo privacy guarantee the rest of the codebase relies on.
Also fixed two docstrings that over-attributed encoding's carve-out
eligibility (it is not a ClosedValueObject and still recurses/drops
unchanged), and named the parity guard's actual blind spot (18 event
classes, including Equipment and Supply, whose to_payload builds the
payload by mutation rather than a literal) as a concrete follow-up rather
than a hypothetical caveat.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…e 6, P0-C)

TIER2_JSONB_CLEARED_POINTERS[("activity","payload")] cleared channel/
action_name/units, keys transcribed from append_activities's route
docstring. conductor.py never writes any of the three, on any step
kind, so every clearance was dead and a published record of a real
conducted run couldn't say what address was set, what was checked,
what was read, or whether the step passed.

Replaces the clearance set with pointers verified against conductor.py's
real construction sites (address, result, error_class, criterion/kind,
reading/kind+quality, and the matching compute-measurement fields),
each with a written disclosure rationale. Rewrites the guard test to
derive the real key space via an AST scan of conductor.py instead of a
hand-typed reference list copied from the same stale docstring, so it
can no longer agree with a clearance that fires against nothing. Adds a
fitness test cross-checking migration-created entries_* tables against
the record_export registry (closes the gap that let an envelope-less
table ship twice), and an end-to-end test that conducts a real
Procedure via Conductor+softIOC+Postgres, exports, redacts, writes a
bundle, and verifies it in a subprocess with no cora on the path,
asserting on content for both a passing and a failing step.

Gate review (naming + 4-lens panel) caught a real defect: the first
draft also cleared name/capture_name/capture_ref/steering_ref/
output_ref_name by a wrong analogy to command_name/tool_name. None of
the five is actually closed; reverted to the file's own agent_name
precedent (operator-authored free text, DROP) instead.

Found and deliberately left out of scope: activity.payload has a
second real writer (append_activities's route/MCP tool, used directly
by ~16 test_2bm_*.py scenarios). Its disclosure policy is undecided,
not covered by this table despite sharing the same dispatch mechanism;
documented as an explicit follow-up rather than assumed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
AcquisitionRecorded.evidence was dict[str, Any], so the record exporter
dropped it whole (drop:opaque) on every published record, including
the real 2-BM pilot's frame counts, angle range, and which timestamp
was believed. Type it as AcquisitionEvidence (a new frozen dataclass,
recursing like DatasetEncoding rather than closed like DatasetChecksum)
so the generator discloses the closed/numeric leaves while still
dropping the genuinely open-vocabulary strings.

This required resolving a real conflict, not just transcribing the
brief: record_acquisition's direct decider had a Hypothesis-tested
guarantee to accept any primitive-leaf dict as evidence, while
ingest_scan (the only writer any real scenario exercises) always
produced one fixed shape via a JSON-schema copy of the same rules.
Adopted the one real shape as evidence's contract going forward, per
an explicit decision with the user; ingest_scan's now-redundant
EVIDENCE_SCHEMA is deleted, and validate_evidence in the aggregate is
the sole declarer, used on both the decider and from_stored paths.

settings, the sibling carrier dict on the same event, stays untyped:
no real writer populates it beyond {}, and modeling.md now states the
general rule (type when real content exists, not ahead of demand) so
it's checkable against other carriers.

Naming reviewer and a 4-lens gate panel (architecture, test coverage,
cross-BC consistency, security) ran before this commit; their fixes
are folded in, including deriving the wire-serialization field list
from the dataclass itself rather than a second hand-maintained list,
and pinning the exact disclosure split against the generated table
independent of the generator that produces it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…rity campaign

Integrates slice 7 alongside slice 6. Both were built independently off
the campaign's slice-5 tip and never run against each other; this merge
is the first time DISPOSITIONS (slice 7) and TIER2_JSONB_CLEARED_POINTERS
(slice 6) exist together, which is what redaction_profile_hash (H2) is
computed over. Verification of the combined tree follows in the next
commits/checks, not assumed from either branch's own green run.
Found running the campaign's integrated diff-coverage check across
slices 3-7 together for the first time: validate_evidence's float-field
branch (a bool or non-numeric value for projection_angle_first/_last)
had no test, unlike its sibling int-field and str-field branches which
were already covered. Neither slice 7 alone nor any prior slice's
scoped coverage run would have caught this, since the gate runs against
each slice's own diff; it surfaced only once the combined tree's diff
was measured as one unit.

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/data/aggregates/acquisition
  events.py
  state.py
  apps/api/src/cora/data/aggregates/dataset
  events.py
  evolver.py
  state.py
  apps/api/src/cora/data/aggregates/distribution
  events.py
  evolver.py
  apps/api/src/cora/data/features/ingest_scan
  handler.py 246
  apps/api/src/cora/data/features/record_acquisition
  route.py
  apps/api/src/cora/data/features/register_distribution
  decider.py
  apps/api/src/cora/infrastructure/record_export
  __init__.py
  _bundle.py
  _dispositions.py
  _export.py
  _hashing.py
  _manifest.py
  _redact_tier1.py
  _redact_tier2.py
  _redaction.py
  apps/api/src/cora/shared
  closed_value.py
Project Total  

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

Found during the campaign's live-verification pass on 2026-08-13: this
test's placeholder checksum was, byte for byte, the real test_005.h5
digest, confirmed by an authorized read-only query against the live
2-BM database. Its own comment promised otherwise ("not the live 2-BM
scan's own value... pinning a real production digest into test source
would be a second, unrelated way to leak it"), a promise the code had
stopped keeping. A hex digest discloses nothing about a person, so this
was not itself a privacy incident, but it is the same failure shape the
whole campaign exists to catch: a comment asserting something the code
next to it does not actually do.

Replaced with a digest fabricated from an arbitrary literal, verified
against no real file's bytes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@xmap
xmap merged commit f876a9a into main Aug 13, 2026
19 checks passed
@xmap
xmap deleted the worktree-record-publishing-integrity branch August 13, 2026 18:34
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