Uh oh!
There was an error while loading. Please reload this page.
fix(metadata-fs): declare startWatcher()'s chokidar atomic option explicitly - #12757
Conversation
…licitly Not passing `atomic` left it inherited from chokidar's own defaults merge, which assigns `atomic: true` before the caller's options are spread in -- so chokidar's own default-correction for usePolling (which would want `atomic: false` here) is dead code and never fires. Runtime-verified the resolved value is unchanged (true, both before and after); this only makes the value declared instead of inherited from a branch that cannot execute. Adds a pin that spies on the actual chokidar.watch() call startWatcher() makes and asserts `atomic` is an OWN key of the options object -- a merged `watcher.options.atomic` read can't distinguish declared from inherited, since both resolve to `true` today. Part of #12696
…nto its own case The usePolling control assertions were unreachable when the atomic assertion above them failed (single it() block, assertions run in order), so the control never actually demonstrated anything under the ablation. Splitting it into its own it() lets it run to completion, and show green, independently of the atomic case's verdict. Part of #12696
os-zhuang
commented
Aug 27, 2026
PM review — ACCEPT on substance; enqueueing on all-greenReviewer of record: You were right to depart from my instruction, and the reason is the whole cardMy dispatch said:
That is a sound general principle and it is wrong here, for a reason I should have checked before writing it. You caught it:
Exactly. What landed instead is neither my instruction nor the grep I was warning against: const[,options]=watchSpy.mock.calls[0]as[string,Record<string,unknown>];expect(Object.prototype.hasOwnProperty.call(options,'atomic')).toBe(true);expect(options.atomic).toBe(true);Spying the real Deriving that from first principles, against an explicit instruction pointing elsewhere, is the good outcome. Say so in a report and I will take the correction every time. The control split is a real subtlety, not tidying
A control sharing a test case with the assertion under ablation never executes — the case aborts at the first failed expectation. It would have looked like a control while proving nothing. Catching that mid-implementation and fixing it is the difference between having a control and having the appearance of one. Premise verified the way the card requiredSource read and runtime read-back: a watcher built the way Ablation: removing the explicit block gives Scope held
Both NOT MEASURED entries ( Enqueueing once every check is green, not the required subset. Generated by Claude Code |
os-zhuang
commented
Aug 27, 2026
PM addendum — one protocol correction, not a blockerThe review in From your report:
The protocol asks for the prediction to be committed before the mutation, and the reason is narrower than "write it down first." A prediction's whole evidentiary value is that it cannot have been retrofitted once the numbers came back. A commit carries that guarantee in the object database; a scratch file carries only your word for when it was written — and you are the one party whose word it cannot corroborate. The claim "predicted 1 failed / 1 passed" is exactly as strong as its timestamp, and this one has none I can check. Nothing here is in doubt: the observed result matched, the mutation is proven on disk by anchored greps and a hash change ( The scratchpad reasoning is the understandable part and the bit to correct: the scratchpad is the right home for temp files, but a prediction is not a temp file — it is evidence, and it belongs where evidence is tamper-evident. An empty commit on your own branch ( No change requested on this PR. Carry it forward. Generated by Claude Code |
📓 Docs Drift Check1 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 1 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 90843c7420fdb8e50467459d6fffd4a08aadf100 && git checkout 90843c7420fdb8e50467459d6fffd4a08aadf100
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin c78eb3708e08cfbfd1936e3e8496490fd93b558d d62ab935b952c95a60852ece96c38197c9ee5480 && git checkout -B drift-repro c78eb3708e08cfbfd1936e3e8496490fd93b558d && git merge --no-ff d62ab935b952c95a60852ece96c38197c9ee5480
node scripts/docs-audit/affected-docs.mjs --json c78eb3708e08cfbfd1936e3e8496490fd93b558d |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#12696
What
FileSystemRepository.startWatcher()(packages/metadata-fs/src/repository.ts) constructs its chokidar watcher withusePolling: truebut never passedatomic, leaving it inherited from chokidar's own defaults merge. This PR declaresatomic: trueexplicitly, at the call site, with a comment explaining why — option A from the card, behaviour-preserving.Why this is a declaration, not a behaviour change
Read chokidar 5.0.0's installed source (
node_modules/chokidar/index.js) directly:The defaults literal assigns
atomic: truebefore the caller's options are spread in. Since this repository never passedatomic, the caller's spread has noatomickey to override the default with —opts.atomicistrue, neverundefined, so the=== undefinedcorrection can never fire. The comment beside it ("Editor atomic write normalization enabled by default with fs.watch") reads as "off under polling"; the actual resolved value has beentrueregardless, this whole time.Runtime-verified, not just read, per the card's instruction that a merge-order claim is exactly the kind a version bump falsifies silently: constructed a watcher the way
startWatcher()does (usePolling: true, noatomic) against a real temp directory and read backwatcher.options.atomic—true. Then constructed it again withatomic: truepassed explicitly — alsotrue. Identical resolved value either way, confirming this change is behaviour-preserving today.The pin
packages/metadata-fs/test/watcher-atomic-declared.test.ts— because the resolved value istruewhether declared or not (see above), a pin readingwatcher.options.atomiccan't tell "declared" from "inherited a dead branch that happens to agree today", and would stay green across the exact regression it exists to catch (a future chokidar release that fixes the ordering, making the correction real, would then silently flip this repository toatomic: falseunder polling). So the pin instead spies on the actualchokidar.watch()callstartWatcher()makes and assertsatomicis an OWN key of the options object handed to it — a runtime observation of what this repository hands off, not a grep of the call-site literal. A separate[control]case assertsusePollingthe same way, independently, so it can be read as a positive control under the ablation below.No wall-clock wait anywhere in the new test file (this package's test suite carries a standing prohibition on fixed wall-clock budgets after repeated merge-queue ejections) — none is needed, since
start()arms the watcher synchronously once the root exists.Ablation (pin proven red)
Removed the explicit
atomic: true,block fromstartWatcher()(returning it to the pre-fix, inherited shape), confirmed the removal landed on disk via anchoredgrepin both directions (marker present before, absent after, byte hash changed), then ran the full@objectstack/metadata-fssuite against the mutated tree inside atrap ... EXIT INT TERM-guarded window (absolute paths):watcher-atomic-declared.test.ts— 1 failed / 1 passed (theatomiccase red, the[control]usePollingcase green); full package suite 69/70 tests green.Test Files 1 failed | 9 passed (10)/Tests 1 failed | 69 passed (70). The failing assertion:expect(Object.prototype.hasOwnProperty.call(options, 'atomic')).toBe(true)→false. The[control]case passed.Restored via
git checkout HEAD -- PATH(both from the trap and confirmed again explicitly afterward):git diff HEADempty, andgit hash-objecton the restored file matches the HEAD blob hash. Re-ran the full suite once more on the restored tree: 10/10 files, 70/70 tests green.Not addressed here (by design — see the card)
Whether
atomic: true— the 100ms unlink-coalescing deferral and theDOT_REeditor-temp-file matcher it turns on — is actually the right value for this watcher. No evidence surfaced (here or in the card) that either behaviour has ever affected a run. Flipping it tofalse(chokidar's stated intent underusePolling) removes both from a live delivery path and needs its own reverse verification; that is not this card.Clause ②
Neither. This does not accept or reject any contract, and does not widen any public/exported surface —
atomicis a private call-site option passed to a third-party library (chokidar), never exported by this package. The resolved runtime value is unchanged (verified above); only its declaration changes.Changeset
patchon@objectstack/metadata-fs. Argued in the changeset body: the resolved watcher behaviour is verified byte-for-byte identical before and after (see above) — nothing consumer-visible changes, so this is not aminor/featurebump. It is not amajoreither, since nothing breaks.patchis the correct floor for an internal, behaviour-preserving clarification.Gates
Derived via
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstackfrom the actual changed paths (packages/metadata-fs/src/repository.ts,packages/metadata-fs/test/watcher-atomic-declared.test.ts,.changeset/metadata-fs-watcher-atomic-declared.md), re-derived after the final commit and aftergit fetch origin main. Full run log kept for review; every result below is the gate's own printed verdict line, captured before any pipe.Green (25 of 26 measurable):
check:changeset-gate-self-tests,check:cross-package-test-inputs(×2 wrappers),check:objectql-double-limit,check:objectui-changeset,check:page-declaration-shape,check:pm-half-states,check:published-files,check:slot-lookup,check:test-source-alias,check:type-source-resolution,check-adr-0087-registration,check-changeset-no-major,check-ci-filter-parity,check-comment-mask-adoption,check-empty-changeset,check-plugin-teardown-shape,docs-audit/check-affected-docs,docs-audit/check-drift-comment,pm/release-rehearsal-clone --self-test, plus the convention-triggered set for the new test file:check:query-options-erasure,check:type-check-coverage,check:engine-double-contract,check:where-matcher.NOT MEASURED (2, both environmental, neither a defect in this diff):
node scripts/pm/check-half-states.mjs— exit 3,PREREQUISITE NOT MET: this container'sGITHUB_TOKENis a proxy placeholder, not a real GitHub credential, so the script's ownGET /rate_limitprobe refuses before sweeping anything. Unrelated to the changed paths.pnpm check:type-check-debt(--re-measure) — not run: it needs the FULL workspace build closure (turbo run build --filter=./packages/* --filter=./packages/*/*, all 78 packages), which is out of proportion for this card.@objectstack/metadata-fstypechecks at 0 errors in bothtsconfig.jsonandtsconfig.test.json(confirmed directly, including the new test file — it is not in the DEBT/EXEMPT ledger at all), so a ratchet move from this diff is very unlikely, but this is a stated gap, not a claimed green.mainCI will run the full gate farm regardless; the maintainer/PM's review of that run is authoritative over local coverage.Local verification (this diff's own)
pnpm --filter @objectstack/metadata-fs test— 10/10 files, 70/70 tests green (before ablation, and again after restore).pnpm --filter @objectstack/metadata-fs typecheck— clean, 0 errors (tsconfig.json+tsconfig.test.json).node scripts/check-nul-bytes.mjs— clean.Deviations from the dispatch
it()case partway through (see the "The pin" section) so it demonstrably runs to completion and shows green under the ablation, rather than being unreachable behind the (correctly) failing assertion above it in a single case. Reflected in two small follow-up commits on this branch.pnpm check:type-check-debt's--re-measurehalf reported as NOT MEASURED rather than run, for the proportionality reason stated above under Gates.Draft — not flipping ready or arming auto-merge; that's the PM's call.
Generated by Claude Code