Skip to content

Ledger migration can silently drop a gate: line and pass validation #519

Description

@pbean

A legacy-ledger migration sweep can drop a gate: line from a pre-existing canonical entry, pass deterministic validation, and commit the rewrite. The next dispatch then runs the story that entry was there to hold back.

_ensure_migration snapshots the pre-existing canonical entries as id → status only (sweep.py:717):

pre_canonical = {e.id: e.status for e in deferredwork.parse_ledger(text)}

and validate_migration checks only that the id survived and its status first-word is unchanged (sweep.py:279-284). Nothing in that function reads gate: — the other gate matches in sweep.py are the unrelated gates notification module.

Repro

Migrate a ledger whose canonical half carries a gate, and hand back a rewrite with the gate: line removed and everything else intact:

DW-1 gate tokens BEFORE: ('3-2',)
DW-1 gate tokens AFTER : ()
validate_migration errors: []

The rewrite is accepted. _refuse_gated_story then sees no gate, and story 3-2 dispatches.

Why it matters

This is the failure the field was added to prevent, reached by a route the field's own guard does not cover. deferred-work-format.md states the contract the migration breaks:

Like source_spec:, a gate: line is never edited or dropped when an entry is otherwise touched: removing it un-gates the story silently, which is the exact failure this field exists to prevent.

That sentence is instruction to the session, not a check. Migration is the one path where an LLM rewrites entries it did not author, so it is exactly where the instruction should not be the only thing holding.

Severity is bounded by reachability: it needs a project with a pre-DW-format ledger and a gated canonical entry, so it cannot affect a project that never carried legacy content. It is silent when it does happen, which is what makes it worth a check rather than a note.

Suggested shape

Extend the snapshot beyond status — the minimal form is id → (status, gate tokens), with validate_migration reporting a dropped or altered token the same way it reports a changed status. Preserving whole entry bodies would be stricter but would refuse the reformatting a migration legitimately does.

Worth deciding at the same time whether origin: and location: deserve the same treatment; source_spec: carries a comparable never-drop instruction and has no check either.

Provenance

Found by Codex reviewing #502, which adds the gate: field. Filed separately rather than folded in: sweep.py is not in that PR's diff, and the fix needs its own tests against the migration path.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions