fix(metadata-fs): scope the watcher's dotfile ignore to paths relative to the root (#7150) - #7208
Conversation
…e to the root (#7150) `MetadataPlugin` attaches the FileSystemRepository at `<project>/.objectstack/metadata` (REPO_SUBDIR). The watcher's `ignored` matcher was a bare dotfile regex, and chokidar applies that matcher to the watched root path itself, not only to entries discovered underneath it — so the `.objectstack` segment of the root matched and the entire watch was inert. Measured on chokidar 5 with the repository's own watch options, two identical trees differing only in whether the root sits under a dot-directory: plain root getWatched: ['<root>', 'view'] events: add + change dot root getWatched: [] events: none The matcher is now a function evaluated against the path RELATIVE to the watch root, so dot segments belonging to the root itself are never considered while dotfiles under the root — including the repository's own `.objectstack/` bookkeeping subtree — stay ignored as before. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016R9de1FqP7NvwKvqXi92Gh
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 1 package(s): 1 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 31351599963 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 失败的 job(日志抽取,best effort):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
|
Merge-queue ejection triage (lane PM, session Signature: Verdict: load-timing flake, not a regression from this diff. Three readings:
Action: re-queueing once (re-arm auto-merge). Standing rule honored: same signature on a second ejection ⇒ escalate + file the flaky-test card, no further re-queues — each blind re-queue rebuilds every PR behind this one. One forward-looking note: this PR's own new tests add more watcher-timing surface (4s/8s waits) to the same suite; if queue-load flakes recur in this family, the fix direction is deadline-widening or event-promise waits in the pre-existing test, filed as its own card, not silent re-queues. Generated by Claude Code |
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 31352802812 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 失败的 job(日志抽取,best effort):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
…l-suite queue load (#7150) Test-only. No production code changes, no new cases, no assertion changes. PR #7208 was ejected from the merge queue twice, on a different watcher case each time and on the identical SHA `c47cc79e0`: 1. run 31351599963 — `test/fs-behavior.test.ts > chokidar: external file change emits an update event` (pre-existing test, 3s event deadline). 2. run 31352802812 — `test/watch-dot-root.test.ts > sees an external edit when the root is under a dot-directory` (this PR's own test, 8s deadline). Same family both times. The queue runs the FULL suite while PR-side CI runs only the affected subset, and `FileSystemRepository`'s watcher rides `usePolling: 1000ms` plus an `awaitWriteFinish` stability window — wall-clock timers that stretch under a saturated runner while nothing the assertions look at changes. Both cases were green on that same SHA in PR CI and locally, so this is load-timing, not semantics. The lane PM's ejection triage named deadline-widening in the pre-existing test as the sanctioned direction. Every positive event wait in the family now goes through a named `EVENT_WAIT_MS`, raced against the event promise as before, so a healthy run still costs about one poll interval and the widened number is only paid on the way to a failure: - `metadata-fs/test/fs-behavior.test.ts` 3s -> 20s (case cap 10s -> 45s) - `metadata-fs/test/watch-dot-root.test.ts` 8s -> 20s (case cap 30s -> 60s, both cases) - `metadata-fs/test/no-root-on-attach.test.ts` 8s -> 20s (case cap 20s -> 45s) - `metadata/src/metadata-repository-fs-dot-root.test.ts` 15s -> 25s (case cap 40s -> 60s) The last two are beyond the two ejected files and are disclosed as such: both carry the identical event-wait shape against the same watcher in the same shard, so they were the next candidates to eject rather than anything the diff's semantics touch. Case caps were raised alongside each deadline. A cap that no longer clears `EVENT_WAIT_MS` plus setup kills the case on the vitest timeout before its own deadline is reached, which reports as a timeout instead of as the missing event — reintroducing the flake from the other side. `watch-dot-root.test.ts`'s 4s quiet window is unchanged and marked never to be shortened: a too-short quiet window cannot fail, it can only produce a false pass on `toEqual([])`. Its liveness control is a positive assertion and was widened with the rest. Reverse verification (direction predicted first): setting `EVENT_WAIT_MS = 1` must turn every positive wait red while leaving the quiet-window assertion untouched. Observed exactly that — 4 failed / 26 passed, and the negative case failed at its CONTROL, not at its `toEqual([])`: × chokidar: external file change emits an update event 319ms × arms the watcher on the first write, so external edits are still detected 429ms × sees an external edit when the root is under a dot-directory 433ms × still ignores dot entries UNDER the root, including its own bookkeeping 4425ms AssertionError: expected [] to have a length of 1 but got +0 Restored, both suites green: metadata-fs 4 files / 30 tests, metadata 30 files / 592 tests. `typecheck` clean, ESLint clean on all four files, `check-nul-bytes` OK. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016R9de1FqP7NvwKvqXi92Gh
|
Patch round accepted — re-queueing (third and final attempt under the standing rule). Session
Verification: PR CI re-converged at Honest caveat from the patch report, endorsed: local green + PR-CI green is what the previous SHA also had — queue load can only be tested in the queue. This lowers ejection probability rather than proving it away. If this attempt ejects again on a watcher-timing signature, the standing rule fires: no fourth attempt — the flaky-family card gets filed and the PR escalates to the maintainer. Generated by Claude Code |
Fixes #7150
MetadataPluginattaches theFileSystemRepositoryat<project>/.objectstack/metadata(REPO_SUBDIR). The watcher'signoredmatcher was a bare dotfile regex, and chokidar applies that matcher to the watched root path itself, not only to entries discovered underneath it — so the.objectstacksegment of the root matched and the entire watch was inert.MetadataManager.setRepository()subscribes torepo.watch({})and invalidates the registry entry and thelist()cache per event, so a live consumer was wired to a source that could never fire in the layout the product ships.Premise verification
Re-verified on
origin/main@aeadbd61c(which already contains #7000 / PR #7152):ignored: [/(^|[\\/])\../]live atrepository.ts:438,REPO_SUBDIR = '.objectstack/metadata'atpackages/metadata/src/plugin.ts:57, joined at:389. Premise holds.Re-ran the filer's A/B harness on chokidar 5.0.0 with the repository's own watch options (
ignoreInitial: true,depth: 2,awaitWriteFinish,usePolling: true,interval: 1000), two identical trees differing only in whether the root sits under a dot-directory.Legend for the
getWatcheddumps below: keys are printed relative to the watch root;(root)is the watch root itself and..is chokidar's entry for its parent. (Spelled(root)rather than in angle brackets because GitHub's body sanitizer strips<+letter as an HTML tag and silently deleted it from the first revision of this body.)The shape choice, and why (triage asked for this to be recorded)
Triage ruled this implementation-layer rather than decision-box, with the choice to be made by measurement. Both candidate shapes were measured on the same harness.
Shape A — function matcher scoped to the path relative to the root (chosen):
Identical to the plain-root control. The
.objectstackbookkeeping subtree never enters the poll set.Shape B — drop the regex, rely on the
parseItemPathguard (rejected):Shape B does restore event delivery, but the body's premise that
parseItemPathmakes the matcher redundant does not survive measurement.parseItemPathrejects exactly one name,.objectstack— so other dot entries leak straight through it:Both would be published as
MetadataEvents withsource: 'fs'and re-emitted throughnotifyWatchers, whilescanHeadsskips every dot entry on boot (entry.name.startsWith('.')). That leaves the boot scan and the watcher disagreeing about what the repository contains — a worse defect than the one being fixed. Shape B also puts.objectstack/.log/in the poll set, so every one of the repository's own log appends wakeshandleFsChangeonly to be discarded.So the fix keeps the original intent verbatim and corrects only the frame of reference: judge the path relative to the root, so dot segments belonging to the root itself are never considered.
Note on the harness: an editor
.swpartifact is a poor probe here — chokidar's built-inatomicoption filters.*.sw[px]before any matcher runs, so it is suppressed under Shape B too and proves nothing..cache/x.jsonandview/.scratch.jsonare the probes that actually separate the shapes.Tests
Two new pins in
packages/metadata-fs/test/watch-dot-root.test.ts, which are the two halves of the watcher's promise — a fix that merely widened the matcher would pass the first and fail the second:sees an external edit when the root is under a dot-directory— the production layout, asserting the event fires.still ignores dot entries UNDER the root, including its own bookkeeping— asserts.cache/x.json,view/.scratch.jsonand appends to.objectstack/.log/main.jsonlproduce nothing, then carries a control write to a real item proving the watcher is alive.no-root-on-attach.test.tsis untouched: its watcher-arming pin deliberately uses a non-dot root to measure arming (#7000), and it keeps measuring that.End-to-end consumer proof in
packages/metadata/src/metadata-repository-fs-dot-root.test.ts: a realMetadataManager+FileSystemRepositoryon a real dot-rooted temp dir, an out-of-process write, asserted atsubscribe(). Cheap in the end — 1.1 s.pnpm --filter @objectstack/metadata-fs typecheckclean (packages/metadatahas notypecheckscript). ESLint clean on all three touched files.node scripts/check-nul-bytes.mjsOK.Reverse verification
Direction predicted before running: restoring the old regex must turn the positive cases red, and must leave case 2's
toEqual([])green — because that assertion is satisfied by a watcher that emits nothing at all. That is exactly what happened, which is why case 2 carries a control:The consumer test was reverse-verified separately, and required rebuilding
metadata-fs'sdistfrom the reverted source first —packages/metadataconsumes the built artifact, so reverting onlysrcleft it green against a staledist(recording it because it is the trap that makes this kind of check silently meaningless). Against the correctly reverted build it burned its full 15 s deadline and failed:Scope note
The claim comment declared the file surface as
packages/metadata-fs/src/repository.ts+ its tests +.changeset/. This PR additionally adds one test-only file underpackages/metadata/src/, which is the end-to-end assertion the dispatch asked for; no non-test file outside the declared surface is touched.Changeset: patch on
@objectstack/metadata-fs(user-visible — external edits are now seen).Generated by Claude Code