fix(sync): stop pinning file mtime to Notion last_edited_time#75
Conversation
After every page write, FreezePage called os.Chtimes to roll the file's mtime back to Notion's last_edited_time. This defeats git's stat-cache: when a refresh produces a same-length rewrite AND Notion's timestamp did not advance, (size, mtime, inode) are unchanged from the cached entry, so `git status` reports the file clean despite the on-disk content having changed. Workflows that run `notion-sync refresh && git commit` silently lose the update. Both trigger conditions hold for routine same-length API edits (typo fixes, equal-length renames, certain property toggles whose last_edited_time does not bump). Observed in production on a typo correction that did not surface in `git status`. The authoritative timestamp is preserved in the `notion-last-edited` YAML frontmatter field, which is git-tracked and human-readable; the filesystem mtime was a redundant copy that no internal caller reads. Letting the OS set mtime to wall-clock-now restores stat-cache correctness with no loss of provenance. Adds a regression test asserting mtime is not pinned to a past Notion timestamp after FreezePage. Co-Authored-By: Claude <noreply@anthropic.com>
Issue EvaluationVerdict: real bug, valuable fix, trivial change. Verified. No internal consumer of mtime. The skip-if-unchanged check at Feasibility: trivial. Source change is 4 lines deleted; Caveats worth flagging: anyone downstream depending on mtime ordering (rare) gets a behavior change. Reproducibility is preserved — |
TDD Cycle SummaryCycle: RED → GREEN → full suite. RED — failing regression testAdded Conditions under test:
Result: failed as expected GREEN — minimal fixDeleted the Result: Full suite
Code changes
Net: −4 source / +41 test. |
The test-single-datasource-db and test-double-datasource-db skills had explicit pass criteria that "file mtime matches notion-last-edited within 1s." That criterion encoded the very behavior PR #75 removes — pinning mtime backwards defeats git's stat-cache and causes silent loss on same-length rewrites. Inverts both checks to the new contract: mtime must NOT be within 1s of notion-last-edited. Provides defense in depth alongside the unit-level TestFreezePage_DoesNotPinMtimeToNotionTimestamp regression guard. Updates testing-README pyramid bullet from "mtime" to "no-pin" to match. Co-Authored-By: Claude <noreply@anthropic.com>
Code Review (Pass 1)Note Verdict: Approve SuggestionsNone — author confirmed remaining items are sharpening, not no-brainers. Notes
Review feedback assisted by the critical-code-reviewer skill. |
E2E Verification:
|
Context
notion-sync refresh --idsleft the corrected content on disk, butgit statusreported the file clean and the commit captured the pre-fix state.FreezePagecalledos.Chtimesafter every write to set the file's mtime backwards to Notion'slast_edited_time. Git's stat-cache skips re-hashing when(size, mtime, inode)are unchanged from the cached entry, so a same-length rewrite where Notion's timestamp also did not advance is invisible togit status.last_edited_timedoes not bump) — not a rare corner.notion-last-edited), which is git-tracked and human-readable. Filesystem mtime was a redundant copy, and no internal caller readsModTime()for any decision.Changes
os.Chtimesmtime-pin block ininternal/sync/page.goso the OS sets mtime to wall-clock-now at write time. Git's stat-cache then invalidates correctly on every rewrite.TestFreezePage_DoesNotPinMtimeToNotionTimestampininternal/sync/page_test.go— freezes a page with a 2020last_edited_timeand asserts the resulting file's mtime is not in the past. Encodes the contract so the pin cannot silently regress.notion-last-editedfrontmatter remains the authoritative source of truth for "when did Notion last touch this entry"; behavior of skip-if-unchanged checks (which read frontmatter, not mtime) is unaffected.go test ./...) passes.🤖 Generated with Claude Code