Skip to content

records(export): structured record exports and the export format - #46

Merged
TusanHomichi merged 7 commits into
mainfrom
claude/milestone-2-org-rename-q8b2q8
Sep 1, 2026
Merged

records(export): structured record exports and the export format#46
TusanHomichi merged 7 commits into
mainfrom
claude/milestone-2-org-rename-q8b2q8

Conversation

@TusanHomichi

@TusanHomichi TusanHomichi commented Sep 1, 2026

Copy link
Copy Markdown
Member

Primary Issue

Closes #45
Refs #44 (Milestone 5 design; decision 1 and the slice plan)

Problem And Outcome

Finalized records existed only inside the installation's database: readable and hash-verifiable over the API, but nothing produced a portable artifact whose integrity could be checked anywhere else. After merge, a version, a record (every retained version), an enrollment, or the whole installation exports as a ZIP archive whose record.json entries are the stored canonical bytes, byte for byte, beside canonical-JSON manifests; the same scope exported at the same instant is byte-identical; and consolebook-server export verify <archive> proves an export consistent with its stated fingerprints from the file alone, opening no data directory. The format is normative in docs/formats/record-export.md, the decisions are ADR 0014.

Changes

  • record_export.rs (production and the format vocabulary): Scope (version, record, enrollment, installation), typed ExportRefusal (including nothing_to_export — an empty scope is refused, never exported as a complete-looking archive), deterministic ZIP writer (stored entries, fixed order, export instant as entry mtime, 0644; rows are consumed so each version's bytes are released once written), archive and unit manifests as canonical JSON, record_exported audit events with actor and subject and never content.
  • record_envelope.rs: the typed reading side of the record envelope (ADR 0011 schema 1, ADR 0013 schema 2) — every named member with its type, deny_unknown_fields throughout, nullable members required to be present, daily_reports present exactly for schema 2, attachments an array of an uninhabited element type (no known schema carries attachments). Verification reads through it; packets and rendering will too.
  • export_verify.rs (verification from the archive alone, split out when the module crossed the 1,000-line threshold): typed Findings covering content hash over the bytes, canonical re-serialization, the bytes being a valid envelope of their declared schema, envelope identity cross-check (record.*, instance, canonicalization), lowercase 64-hex hash members, chain hash from the carried predecessor hash, positive identity, lineage shape, predecessor linking within the archive, declared scope against the listed units (no_units, scope_cardinality, unit_outside_scope), unlisted and missing entries, non-canonical manifests, unsupported format versions, and duplicate entry names found by walking the central directory itself (the zip reader collapses duplicates by name).
  • Authorization reuses existing read contracts (ADR 0010): the record read rule for version/record scopes (workflow readers and the trainee's own finalized record, every retained version per ADR 0012), the training-history read rule for enrollments, and the existing export_records capability for the installation.
  • exports_http.rs: downloads for all four scopes plus GET /api/exports/summary for export_records holders; main.rs: export verify <archive>.
  • Web: "Export this version" / "Export all versions" on the record page's Integrity panel, an Export panel on the enrollment page, and a "Record exports" card on Home for export_records holders (counts, one-click installation export). Downloads go through fetch so a refusal surfaces as an error rather than a saved error document.
  • Docs: docs/formats/record-export.md (normative), docs/decisions/0014-record-export-format.md, a RecordExport entry in docs/domain-model.md.
  • Dependency: zip 8.6 with default features off (stored entries only; adds crc32fast and typed-path).

Scope

  • In scope: export units and archives, four scopes, verification from the export alone (library + CLI), audit, HTTP downloads and summary, web controls, format document, ADR.
  • Out of scope: trainee packets (slice 2), retention/holds/disposition (slice 3), PDFs, backup retention, restore verification (slice 4); persisted exports under data/exports/ (this slice streams and retains nothing, recorded in ADR 0014); importing record exports; streaming the export body instead of assembling it in memory (Stream installation exports instead of assembling the archive in memory #47, raised in review — the peak is bounded near one copy of the corpus here).

Verification

  • Listed the exact verification commands run below
  • Added or updated tests when behavior changed
  • Added or updated an ADR when a durable decision changed
  • All fixtures and examples are invented; no real agency data
- cargo fmt --check
- cargo clippy --workspace --all-targets -- -D warnings
- cargo test --workspace   (110 tests; new tests/record_export.rs: 6)
- web/: npm run check, npm run build
- web/: CONSOLEBOOK_E2E_CHROMIUM=/opt/pw-browsers/chromium npm run e2e   (5 specs; drafts.spec downloads a version export in the browser and verifies it with the CLI)

tests/record_export.rs proves: record.json equals the stored canonical_bytes; both manifests carry the documented fields and are canonical; two exports at one instant are byte-identical; the record scope links v2 to v1 while a lone v2 reports its predecessor as not in the export; a repacked archive with altered bytes, a swapped manifest hash, a replaced record, a missing entry, an unlisted entry, a pretty-printed manifest, an unknown format version, reordered units, a bad path, a broken lineage, an emptied unit list, a mislabeled version scope, a foreign record scope, and a hand-assembled container with a duplicated entry each produce the named finding; a correctly hashed counterfeit carrying only the identity members, a schema-2 envelope without daily_reports, an unnamed member, a mistyped member, an absent nullable member, an attachment element, and an unknown schema are each envelope_invalid while the same content shaped as schema 1 verifies; a renumbered version 0 fails on identity alone and an uppercase predecessor hash on hash shape alone; a raw byte flip fails; every scope's authorization and typed refusals; the API's headers, bodies, and error codes; and the CLI's exit codes and output with a never-created --data-dir left untouched.

Review And Merge Notes

  • Review focus: the verifier's check list against the format document (they are meant to be one-to-one); the typed envelope reader against finalization::envelope (same member set, same types); the authorization line (read rules for scoped exports, export_records for breadth) recorded in ADR 0014's rejected alternatives; determinism assumptions on the zip writer (no extended-timestamp extra fields without the time feature).
  • User or operator impact: three new download controls and one Home card; a new CLI subcommand; a new record_exported audit kind. No migration.

🤖 Generated with Claude Code

https://claude.ai/code/session_01DNP3dkn9c84v2YoNwx7KTg


Generated by Claude Code

Deliver Milestone 5 slice 1 (#45; #44 decision 1): finalized versions
leave the installation as verifiable archives whose record bytes are
the stored canonical bytes, byte for byte.

- record_export: units (stored bytes beside a canonical-JSON unit
  manifest), ZIP archives with stored entries in a fixed order and
  metadata (deterministic per scope and instant), four scopes
  (version, record, enrollment, installation), read-rule authorization
  with export_records for the installation, typed refusals including
  nothing_to_export, record_exported audit events, and verification
  from the archive alone with typed findings and predecessor linking.
- exports_http: downloads for every scope and the export summary;
  main: `export verify <archive>` opens no data directory.
- web: export buttons on the record page (this version, all versions),
  the enrollment page, and an installation-export card on Home;
  downloads surface refusals as errors instead of saved error bodies.
- docs: docs/formats/record-export.md (normative), ADR 0014, the
  RecordExport domain-model entry; zip added without default features.
- tests: verbatim bytes, determinism, every verification finding,
  scope authorization, API delivery, CLI verification; the drafts e2e
  downloads a version export from the browser and verifies it with
  the CLI.

Refs #44

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DNP3dkn9c84v2YoNwx7KTg
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 1, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-01T20:18:41.395342Z f1c7aa3 New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1e0d2770f0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/consolebook-server/src/record_export.rs Outdated
Comment thread crates/consolebook-server/src/record_export.rs Outdated
Two verifier gaps from review: the declared scope was copied into the
report without checking the listed units against it, so a version
scope listing another version, several units, or none at all still
verified; and the zip reader indexes entries by name, so a name written
twice — where extraction tools disagree on which copy they take — was
invisible.

- scope_findings: no_units, scope_cardinality (a version scope is
  exactly one unit), unit_outside_scope (version and record scopes);
  enrollment and installation scopes state nothing the bytes confirm.
- duplicate_entry_findings walks the central directory itself
  (APPNOTE 6.3, ZIP64 fields included) and reports every name written
  more than once; an unreadable directory is its own finding.
- docs/formats/record-export.md and ADR 0014 name both checks.
- tests: empty units, mislabeled version scope, foreign record scope,
  and a hand-assembled container with a duplicated record entry whose
  genuine copy the reader resolves.

Refs #45, #44

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DNP3dkn9c84v2YoNwx7KTg

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 37efc28ec6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/consolebook-server/src/record_export.rs Outdated
record_export crossed the 1,000-line reorganization threshold
(AGENTS.md) once it owned both production and the independent check.
Ownership now splits along the format's two sides:

- record_export keeps the format vocabulary (constants, Scope, both
  manifests, unit_path, canonical_json) and production: authorization,
  row collection, the deterministic archive writer, audit, summary.
- export_verify owns verification from the archive alone: findings,
  per-unit and per-archive reports, the unit and scope checks, the
  container walk including the central directory.

No public behavior changes; the CLI, tests, and format document point
at the new owner. Nothing persisted changes.

Refs #45, #44

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DNP3dkn9c84v2YoNwx7KTg

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b2571d0a39

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/consolebook-server/src/export_verify.rs Outdated
The verifier parsed record.json as an arbitrary document and checked
six identity members, so a canonical, correctly hashed document that
was not a record — only those members, or a mistyped or missing
presentation member — still verified.

- record_envelope: the typed reading side of ADR 0011's shape (schema
  1) and ADR 0013's (schema 2): every named member with its type, no
  unnamed member anywhere, every nullable member present, and
  daily_reports present exactly for schema 2.
- export_verify reads record.json through it before the identity
  cross-check; a document that is not an envelope of a known schema is
  a typed `envelope_invalid` finding.
- docs/formats/record-export.md names the check (unit check 4); ADR
  0014's verification bullet points at the reader.
- tests: a six-member counterfeit with matching hashes, a schema-2
  envelope without daily_reports, an unnamed member, a mistyped member,
  an absent nullable member, an unknown schema — each invalid and
  nothing else; the same content shaped as schema 1 verifies; the
  genuine bytes read as the typed envelope they are.

Refs #45, #44

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DNP3dkn9c84v2YoNwx7KTg

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b02e19e698

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/consolebook-server/src/record_envelope.rs Outdated
Comment thread crates/consolebook-server/src/export_verify.rs
Two gaps in the new validation surface from review:

- The typed envelope reader accepted "attachments":[{}] because the
  element type was an empty struct; both known schemas require the
  array empty. The element type is now uninhabited, so no value of any
  shape deserializes into it.
- The lineage rule (predecessor null exactly for version 1) is
  satisfied trivially by version 0 or a negative number with a
  predecessor, and the predecessor lookup then reports "not in export"
  without objecting. Identity is positive by the format; a zero or
  negative record_id or version_number is the typed finding
  identity_out_of_range.

docs/formats/record-export.md names both (unit checks 4 and 7);
tests cover an attachment element and a renumbered version 0 unit whose
only objection is the range check.

Refs #45, #44

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DNP3dkn9c84v2YoNwx7KTg

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6fe27df352

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/consolebook-server/src/export_verify.rs
Comment thread crates/consolebook-server/src/record_export.rs
- The chain recomputation decodes hex case-insensitively, so an
  uppercase predecessor hash carried consistently through both
  manifests and the envelope verified for a lone successor and would
  then fail to link beside its lowercase predecessor. Hash members are
  64 lowercase hex characters by the format; a deviation is the typed
  finding hash_not_canonical (docs: unit check 2).
- build_archive consumes its rows so each version's bytes are released
  once written: the assembled archive is the one copy held to the end,
  bounding the peak near one copy of the corpus instead of two.
  Streaming the response itself is #47; ADR 0014's cost note now says
  what an installation export costs until then.

Refs #45, #44, #47

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DNP3dkn9c84v2YoNwx7KTg

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f1c7aa3e81

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/consolebook-server/src/export_verify.rs
Entry order, modification times, and permissions are production rules
that make a fresh export deterministic; they carry no record content,
and an intact archive stays verified after a tool repacks it. The
format document now states this so the verifier's scope is explicit.

Refs #45, #44

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DNP3dkn9c84v2YoNwx7KTg
@TusanHomichi
TusanHomichi merged commit 97050ae into main Sep 1, 2026
1 check 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.

Milestone 5 slice 1: structured record exports and the export format

2 participants