Keep real closure dates and transaction identity when re-importing closed issues

Merged
UN
12d ago
+514-17

Summary

Repeating a gated import is already byte-identical since v2026.10.30 (#198). This PR fixes the two defects that remained when an already-linked closed issue is imported again:

  • Invented closure instants. The atomic plan forced a native re-close on items that were already closed. The SDK close router then stamped the import time as closed_at before the fidelity finalizer restored the real date. Source lifecycle is now applied once, in the fenced finalizer, and unchanged values are skipped (index.ts buildAtomicImportMutations, import-fidelity.ts persistImportFidelity).
  • Lost transaction identity on replay. After an interrupted governed update, the SDK's content update could omit the message. Context selection then treated that entry as an external edit. selectImportContextIndex now also recognizes the governed item_transaction_step context, and only entries that actually change status count as a lifecycle generation.

Details and measurements: docs/repeat-import-noop.md

Evidence

  • test/repeat-import-noop.test.ts: 9/9 pass. With the production change reverted, 0/9 pass. The interrupted-replay case fails with only the context recognition reverted.
  • test/import-fidelity.test.ts still imports half the recorded fixtures into an empty tracker. A new check asserts that a fresh atomic mirror never writes an import-time closed_at into history. That check fails on the previous code for pm-context-81 and pm-csv-134. Full file: 54/54.
  • Repeat import against real trackers (pm-slack, pm-ado, pm-review): 0 items updated on the second run, empty git diff, no untracked files.
  • The two closeout decision-matrix failures seen during the disk-full incident pass 2/2 on this branch and 2/2 on 9586643; they did not reproduce once space was recovered.
  • release:check: 11/11 gates, 1,609/1,609 tests, coverage 100% on all 28 modules. Changelog check passes.

The local coverage run needed a percentage heap override, because c8 aggregation for the 1,609-case suite exceeded the 12 GiB cap on this host. Thresholds, inventory and the checked-in heap default are unchanged. CI on this PR shows whether the default cap holds on hosted runners.

pm items

Closes #175

Summary by Sourcery

Make repeated gated imports of closed GitHub issues fully idempotent while preserving source lifecycle dates and transaction identity.

Bug Fixes:

  • Preserve source closure dates when re-importing already-closed issues instead of recording transient import-time closures.
  • Restore governed transaction identity during interrupted import replay and avoid treating unchanged lifecycle events as new generations.
  • Ensure repeated gated imports produce no item or history changes after the initial synchronization.

Enhancements:

  • Apply source lifecycle fidelity through a single finalization step while skipping unchanged metadata values.

Documentation:

  • Add evidence and regression documentation for byte-identical repeat imports and closure-date fidelity.

Tests:

  • Add regression coverage for repeated imports, source lifecycle transitions, interrupted transaction recovery, and immutable closure history.

Summary by cubic

Fixes re-importing an already-linked closed issue so repeated gated imports stay byte-identical and history keeps the real closure date.

  • Stops the atomic plan from forcing a native re-close on already-closed items, which stamped the import time as closed_at before the finalizer restored the source value. Source lifecycle is now applied once in the fenced finalizer, and unchanged values are skipped.
  • Restores transaction identity on replay after an interrupted governed update: selectImportContextIndex now recognizes the item_transaction_step context, and only entries that actually change status count as a lifecycle generation.

Evidence

  • test/repeat-import-noop.test.ts: 9/9 pass; 0/9 with the production change reverted.
  • test/import-fidelity.test.ts: new check requires the real GitHub closed_at to appear in a fresh atomic mirror's history and rejects any import-time closed_at; 54/54 pass.
  • Repeat imports against real trackers (pm-slack, pm-ado, pm-review) update 0 items and produce empty git diff.
  • release:check: 11/11 gates, 1,609/1,609 tests, 100% coverage.

Migration

  • None. Known follow-up: the plain (non-atomic) fresh import path still records a transient import-time closed_at; this predates the PR and is tracked as pm-github-ldc1.