Skip to content

Harden card publication and review record - #218

Closed
Deicyde wants to merge 8 commits into
feat/review-on-sitefrom
fix/readback-durability
Closed

Deicyde wants to merge 8 commits into
feat/review-on-sitefrom
fix/readback-durability

Conversation

@Deicyde

@Deicyde Deicyde commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Part 8 of 9 in the split of #75. Draft; it merges after #217, and is based on its branch.

This PR makes card publication fail early and say why. A testimony that GitHub reads differently from the site is refused with a rewrite that both read alike, not only the line. A card write is flushed to stable storage (F_FULLFSYNC on macOS), its directory is flushed too, and a staging file it could not remove is named. review record refuses card conflicts, a stale hash, an unsafe card path, a platform that cannot publish, bad or oversized testimony and a gone article_id before it starts Lean, instead of after a full extraction. A review bundle that cannot be decoded is refused as unreadable instead of raising a traceback.

What it adds

  • readback._github_hint(site, github, at), called from _github_errors: picks the rewrite for the first point where the two readings part (list numbering, nesting and markers, tables, fences, block quotes, rules, $...$ placement, \(/\[, tildes, checkboxes, trailing backslashes, emphasis). Where no rule matches it says "no rewrite is known for this difference". skills/human-review/references/readback.md now says a refusal comes "with the line and a way to fix it".
  • readback._flush(fd): F_FULLFSYNC where fcntl has it; plain fsync only on ENOTSUP, EOPNOTSUPP, ENOTTY or EINVAL. Any other error, such as EIO, fails the flush.
  • readback._publish_card: flushes the staged file through _flush, then the card's directory after os.replace; if only that directory flush fails, the card stays published and a RuntimeWarning says so. A staging file _unlink_quietly cannot remove is named in the ValueError, or in a warning after an interrupt.
  • _open_card_directory calls _flush_new_directory for each directory it makes for a first card, flushing it into its parent; a failure there is a warning.
  • __main__._review_record, before Lean: readback_conflicts([]) refuses a platform that cannot publish; _record_snapshot reports a gone article_id through _record_selection_finding (review-selection-missing), with how to clear it; then _refuse_conflict runs on _planned_card, which builds the card through planned_readback. That refuses a card it would replace without --expected-card-hash, a stale hash, an unsafe existing card (link, directory, FIFO, over the card limit), and invalid or blank testimony.
  • _record_input reads the testimony through _read_regular_file(path, 2 * TESTIMONY_MAX_BYTES + 1) and refuses one over 32 KiB, counting \r\n as one byte. It now returns (prepared, packet, testimony).
  • review.load_review_bundle: a bundle nested past the decoder's recursion limit (RecursionError) or escaping a lone surrogate gives review-bundle-invalid, naming the file. Before this layer the first raised a traceback and the second failed later as a hash mismatch or encode error.
  • autoform_cli/README.md: the write contract (flushes, named leftovers, directory-flush warning), the record's pre-Lean card check, and the advice for a gone article_id.
  • Diff note: the _publish_card hunk re-indents the write block into a nested try, so compare the messages, not the indentation.

Review findings it answers

Item numbers are from the open-items comment on #75. None of #40's blockers is answered here: blocker 1 lands in #75, blocker 3 in #216, and blocker 2 in #75 and #219.

Until the next layers land

Validation

Push run 38302766043 on d351cd9a passed test (3.10), test (3.13), real Lean (fixture toolchain) and the Windows inspection, subprocess, and publication hardening job.

  • tests/test_readback_publication.py: 13 new tests (17 cases). They cover F_FULLFSYNC versus fsync, an I/O error that is not masked, the flush order of file, directory and new parent directories, the warnings, named leftover staging files, and five interrupt tests (an interrupt at any step leaves no lock or staging file).
  • tests/test_readback_testimony.py: 2 new tests (61 cases). 60 testimonies are each refused with the expected hint and their rewrite is accepted; one checks the no-hint fallback. The list-numbering test now expects its hint.
  • tests/test_review_cli.py: 13 new tests (30 cases): pre-Lean refusals (blank testimony, card conflict, stale hash, four unsafe card kinds, a platform without fcntl), gone article_id cases, undecodable bundles, and the testimony cap with CRLF.
  • tests/test_readback.py: drops test_an_interruption_before_the_rename_leaves_the_card_and_no_staging_file, now covered by the interrupt tests above.

Stack

# PR Layer Size
1 #212 Fix roadmap loading, Lean index edge cases and undecodable JSON +608/-42, 20 files
2 #213 Extract Lean skeletons one probe per root module +2,838/-639, 12 files
3 #214 Cache the probe helpers and isolate probe failures +891/-44, 6 files
4 #215 Treat every published page as untrusted +3,751/-311, 27 files
5 #216 Add read-back cards and the testimony validator +6,848/-19, 19 files
6 #75 Add verified statement-review bundles +3,997/-44, 28 files
7 #217 Show statement review on the site +1,374/-57, 12 files
8 #218 Harden card publication and review record +1,401/-90, 9 files
9 #219 Authenticate statement approvals against GitHub code-owner review +7,320/-102, 21 files

Roadmap loading no longer hangs on a README.md link or stops at a chapter
that cannot be listed or a page that cannot be read; the Lean index stops
at #exit and records a name declared in several files; the graph ignores
links in indented code; readers of skeleton reports and publication
manifests refuse undecodable JSON with a message instead of a traceback;
the audit knows irreducible_def; and the Pages build pins the renderer
versions the lock file has.

Part 1 of the split of #75.
A blind packet has the same bytes whichever command extracts it: one probe
per root module, a compiled helper module, a v5 report keyed by root
module with coherence checks, packets without author comments, a captured
blueprint snapshot, and transactional output.

Part 2 of the split of #75.
The compiled probe helpers are cached per toolchain, probes run in a pool,
and a failing root module or declaration is reported without aborting the
rest of the extraction.

Part 3 of the split of #75.
Every page the site publishes is checked as published: raw HTML, link
targets, attribute lists, Mermaid and stateful TeX are refused, render
writes only allowed files and paths, and the site's MathJax runs with
ui/safe and a fixed package set. check reports the same issues render
refuses.

Part 4 of the split of #75.
The autoform-readback/v1 card store (format, loader, record API, locked
compare-and-swap publication) and the validator that holds model-written
testimony to what the site and GitHub both show the same way.

Part 5 of the split of #75.
review prepare, record and check, audit --review, and the CI review check:
a bundle derived from fresh extraction binds each article's statement,
cited passage, declarations and packets; review_approved records its hash
and reads self-approved until a verifier authenticates it.

Part 6 of the split of #75.
render --review shows each reviewed statement's packet, read-back cards
and approval state, with per-card MathJax so no article formula can change
a card.

Part 7 of the split of #75.
Durable card writes (F_FULLFSYNC, directory flushes, leftover and interrupt
warnings), rewrite hints for GitHub/site reading differences, and review
record's pre-Lean checks, input size cap and advice for a gone article.

Part 8 of the split of #75.
@Deicyde

Deicyde commented Oct 13, 2026

Copy link
Copy Markdown
Contributor Author

Review at d351cd9

Verdict: Fold into #216 and #75. This is layer 8 of the statement-review stack (#212 -> #213 -> #214 -> #215 -> #216 -> #75 -> #217 -> #218 -> #219). It moves review record's refusals ahead of the Lean extraction (unpublishable platform, a gone article_id, card conflicts and stale hashes, unsafe existing cards, blank, invalid or oversized testimony), turns a too-deep or lone-surrogate review bundle into review-bundle-invalid instead of a traceback, and hardens card writes. A coordinator learns about these failures before paying for an extraction, and a damaged bundle becomes a finding, not a crash. Static review found no bugs in the new code paths. But every defect it fixes exists only in #216 and #75 (main has no readback.py or review.py), so the fixes belong in those PRs, and main should never carry the bundle traceback or the unbounded testimony read on its own.

Fixes before merge

  1. Split the fold by side. The card-side pieces go into Add read-back cards and the testimony validator #216: durability (file and directory fsyncs), leftover staging-file naming, and flushing new directories into their parents. The record-side pieces go into Add verified statement-review bundles #75: the pre-Lean refusals, the bundle refusals and the testimony bound.
  2. Drop the GitHub-parity hint table (autoform_cli/readback.py:2226-2389). It is 164 lines, about 45% of this PR's product code, plus a 60-case test. The read-back guide (skills/human-review/references/readback.md:68-102) already states nearly every rule it encodes, so the refusal text becomes a second copy to keep in sync with the guide and with _shown's token format. Its catch-all branches also leave the "no rewrite is known" fallback unreached, so an unanticipated difference may get specific advice that does not fit (plausible, from reading). Replace it with a pointer to the guide's GitHub section.
  3. Drop F_FULLFSYNC (autoform_cli/readback.py:3238-3252) and keep plain os.fsync on the card and its directory. Main flushes every other durable write with plain fsync (autoform_cli/render.py:989-996), and Publish blueprint sites transactionally #165 documents the macOS caveat that fsync does not flush the drive's cache. Cards should match that policy, with the same caveat in the read-back README section. Adopting F_FULLFSYNC product-wide would be a separate PR. This also removes the gap where CI (Linux and Windows) only ever exercises that branch through a fake fcntl.
  4. The gone-article_id advice dead-ends when the article was deleted rather than renumbered (autoform_cli/__main__.py:1561-1575, autoform_cli/README.md:858-866). It tells the coordinator to rerun review prepare --packets (a full extraction) and take --article-id from the new manifest. If no article maps the declaration any more, the manifest has no entry and the message offers nothing else. Confirmed: tests/test_review_cli.py:647-682 asserts this exact advice in the deleted-* case, after removing the only article whose lean: names the declaration. Fix (cheap, goes into Add verified statement-review bundles #75): the record already holds the graph; if no node's lean: lists --declaration, say the article was removed, drop the record and delete any card under the old article_id; otherwise name the matching node's article_id.

Optional

  • Document in the write contract (autoform_cli/README.md:676-682) that a first card's new readbacks/ and <article_id>/ directories are flushed into their parents and that a failure there only warns (autoform_cli/readback.py:2990-2991, 3017-3029). Confirmed by reading. Goes into Add read-back cards and the testimony validator #216.
  • autoform_cli/__main__.py:1200 calls readback_conflicts([]) only for its platform refusal; export _require_publication_support and call it by name.
  • Four of the five new interruption tests (tests/test_readback_publication.py:1030, :1052, :1083, :1130) would pass against the base code, because the base _publish_card already removed the staging file in a finally (confirmed by trace, not run). Only :1107 fails if this layer is reverted. Keep them as coverage for Add read-back cards and the testimony validator #216's code, not as evidence for this change.

Landing

This was a static review: no tests were run locally. CI at this head: all 9 checks pass, on its stack base.

Posted by PR swarm: PR Swarm Lead

@Deicyde
Deicyde force-pushed the feat/review-on-site branch from da2cac3 to 13a48e3 Compare October 13, 2026 20:39
@Deicyde

Deicyde commented Oct 13, 2026

Copy link
Copy Markdown
Contributor Author

Closing as part of simplifying the #75 stack. Most of this layer was dropped: the F_FULLFSYNC and directory-flush durability, the GitHub rewrite hints (testimony is now plain text, so there is nothing to rewrite), the testimony size cap, the pre-Lean checks and the gone-article advice. The interrupt-safety pieces that still apply to the simpler card store moved into #216. The previous version of this layer is on branch archive/pr75-split-341af4db.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Meta Open Source bot.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant