Skip to content

Persist ingest invariant verification metadata - #1030

Merged
kans merged 3 commits into
mainfrom
kans/ingest-invariant-marker
Jul 24, 2026
Merged

Persist ingest invariant verification metadata#1030
kans merged 3 commits into
mainfrom
kans/ingest-invariant-marker

Conversation

@kans

@kans kans commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Persist ingest invariant verification metadata

Adds a durable verification marker to sync-run metadata recording that a sync
passed the post-collection ingestion invariants: which contract generation ran
(ingest_invariant_generation), which invariant IDs actually executed
(ingest_invariant_coverage), and under which verdict policy
(ingest_invariant_mode: connector/compaction_merge, with _fail_fast
variants). Downstream consumers (e.g. compaction choosing a base sync) can now
distinguish "validated" from "predates validation" instead of assuming.

Semantics

  • The marker is only ever readable on a sealed sync. The syncer stages the
    verification after a successful invariant pass and persists it only after
    EndSync succeeds — a crash anywhere before the seal leaves the artifact
    unverified (fail-closed). Both engines' writers refuse to mark an unfinished
    sync.
  • Marker persistence failure after a successful seal is log-only: the artifact
    is complete and an absent marker is safe, while a returned error would make
    IsSyncPreservable callers discard a finished sync.
  • Any run that reaches collection clears prior verification up front, so a
    rebound sealed sync (WithSyncID — the compactor's expansion pass, a reused
    syncer) can't read as verified while collection rewrites its data.
  • SQLite records its engine-independent coverage (I5, plus I4 under
    fail-fast); Pebble full syncs record the referential family (I5, I7, I3,
    I8, I9). Partial syncs record only all-sync-type checks.
  • Coverage is what RAN, not what passed-by-skipping: consumers compare
    generation + required coverage against their own contract.
  • Fold compaction clears inherited markers (merged output ≠ verified base);
    the post-fold expansion sync re-verifies and re-marks. The c1z sanitizer
    intentionally does not carry the marker (it rewrites record contents).
  • SQLite→Pebble conversion preserves the marker for finished, verified
    sources (byte-identical data).
  • Third-party SyncMeta implementations are untouched: the writer is a
    separate optional interface (IngestInvariantVerificationWriter); stores
    without it just produce unmarked (truthfully unverified) syncs.

Pre-existing bugs fixed along the way

  • Pebble sync-run metadata was silently dropped across lifecycle updates:
    CheckpointSync/EndSync rebuilt the record from a partial field list;
    they now clone-and-mutate, preserving all fields.
  • Pebble SyncMeta mutations skipped the store dirty bit: a standalone
    metadata stamp on a reopened c1z was dropped at Close (the envelope save
    is dirty-gated). pebbleStore now wraps SyncMeta() in the same
    dirty-marking pattern as Grants()/FileOps() — this also covers the
    pre-existing MarkSyncSupportsDiff/RecalculateStats.

Testing

  • Crash seam: TestCrashBetweenInvariantPassAndSealLeavesNoMarker aborts a
    real sync between the invariant pass and the seal on both engines and
    asserts the unfinished sync reads unverified; the resume then seals and
    marks.
  • Rebind: TestRebindingSealedSyncClearsStaleVerification rebinds a sealed
    verified sync, aborts mid-collection, and asserts the marker is gone
    (fails without the clear, both engines).
  • Coverage/mode by engine and sync type; failed-pass-writes-nothing;
    SQLite column migration from pre-marker files; conversion round-trip;
    fold non-inheritance (the marking helper now proves the marker persisted
    into the input file — it previously didn't, making the assertion vacuous);
    standalone Pebble stamp persistence (TestPebbleStoreSyncMetaMarksDirty).
  • Two adversarial review rounds (independent models); all confirmed findings
    fixed: post-seal error semantics, dirty-bit escape, unfinished-sync
    marking, rebind staleness.

Co-authored-by: Cursor <cursoragent@cursor.com>
@kans
kans requested a review from a team July 22, 2026 22:51
@github-actions

github-actions Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

General PR Review: Persist ingest invariant verification metadata

Blocking Issues: 0 | Suggestions: 0 | Threads Resolved: 0
Criteria: Criteria status: loaded .claude/skills/ci-review.md from trusted base dd39a54.
Review mode: incremental since ac04b28
View review run: https://github.com/ConductorOne/baton-sdk/actions/runs/29969742224

Review Summary

The full PR diff was scanned for security and correctness. This commit hardens the verification marker against prior review findings: MarkIngestInvariantsVerified now refuses unfinished syncs in both the SQLite ended_at-is-not-null predicate plus FailedPrecondition and Pebble GetEndedAt-nil guard paths; a pebbleStore.SyncMeta override flips the dirty bit so a standalone metadata stamp on a reopened c1z survives Close; stale markers are cleared at sync start before any collection write; persistIngestInvariantVerification failure is now log-only, fail-closed to unverified rather than failing a sealed sync; and ToPebble no longer converts an unfinished-but-marked source into a sealed verified destination. Proto source matches the regenerated pb output, and the SQLite read path short-circuits on empty generation so a cleared marker never triggers a malformed-JSON parse. No new issues found.

Security Issues

None found.

Correctness Issues

None found.

Suggestions

None.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

No blocking issues found.

A crash between the invariant pass and the final checkpoint/EndSync
left the marker readable on an UNFINISHED sync: the resume machinery
pushes InitOp and re-collects over the same sync while the stale
marker claims the data verified, and any consumer reading the crash
image meanwhile would trust an artifact that never sealed.

runIngestionInvariants now only stages the verification on the syncer;
persistIngestInvariantVerification writes it after EndSync succeeds.
Both built-in writers accept metadata stamps on a sealed sync (SQLite
updates by sync_id; Pebble's PutSyncRunRecord is AllowSealed). The
inverse crash window — sealed but not yet marked — reads as an
unverified legacy artifact: fail-closed.

TestCrashBetweenInvariantPassAndSealLeavesNoMarker pins the promise on
both engines: crash at haltStageInvariantsComplete leaves the
unfinished sync unverified, and the resumed run seals and marks it.

Co-authored-by: Cursor <cursoragent@cursor.com>

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

No blocking issues found.

Four fixes from the second adversarial review round:

1. Marker persistence after EndSync is log-only. EndSync stays the
   last mutation that can fail the sync; a Mark failure on a sealed,
   complete artifact must not read as a FAILED sync (IsSyncPreservable
   callers would discard the artifact and a retry would re-collect
   from scratch over it).

2. pebbleStore overrides SyncMeta() with a dirty-marking wrapper.
   Mark/Clear (and MarkSyncSupportsDiff / RecalculateStats) previously
   wrote to the engine without flipping the store's dirty bit, so a
   standalone metadata stamp on a reopened c1z was silently dropped at
   Close. This also made the fold test's inherit-nothing assertions
   vacuous: markFoldInputVerified never actually persisted the marker
   into the input file. The helper now reopens and asserts IsVerified.

3. Mark writers refuse unfinished syncs on both engines, and ToPebble
   only carries the marker alongside a finished source. The marker is
   now unrepresentable on an unfinished sync through the public API.

4. Sync() clears any prior verification right after binding the sync,
   before collection writes. A rebound, already-sealed VERIFIED sync
   (WithSyncID - the compactor's expansion pass, or a reused syncer)
   otherwise reads as verified while collection rewrites the data the
   marker vouched for. Pinned by
   TestRebindingSealedSyncClearsStaleVerification, which fails without
   the clear on both engines.

Not changed: the c1z sanitizer intentionally does not carry the marker
- sanitize rewrites record contents, so an unverified output is
truthful.

Co-authored-by: Cursor <cursoragent@cursor.com>

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

No blocking issues found.

@kans
kans merged commit 1e06515 into main Jul 24, 2026
10 checks passed
@kans
kans deleted the kans/ingest-invariant-marker branch July 24, 2026 00:16
kans added a commit that referenced this pull request Jul 24, 2026
A conflict-free rebase is not a semantically current one: main's new
verification-metadata functions assigned the dbUpdated flag this branch
made atomic, the persisted coverage lists gained I10, and the pass now
requires the active sync ID to clear prior verification before
re-evaluating.

Co-authored-by: Cursor <cursoragent@cursor.com>
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.

2 participants