Skip to content

metadata-fs: the FileSystemRepository chokidar watcher is inert in the production layout — its own ignored dotfile regex matches the .objectstack segment of the root path #7150

Closed

Description

@os-zhuang

Found while implementing #7000 (PR for the metadata half of #6743's dry-run property). Out of that card's scope — #7000 is about the repository creating its root at attach time — so this is filed separately and unassigned.

Duplicate search over open issues: chokidar ignored watcher, FileSystemRepository watcher metadata-fs, in:title watcher, ".objectstack/metadata" — 0 hits other than #7000 / #6743 themselves.

Observation

FileSystemRepository.startWatcher() passes ignored: [/(^|[\\/])\../] with the comment "skip dotfiles incl. .objectstack" (packages/metadata-fs/src/repository.ts). chokidar applies that matcher to the watched root path itself, not only to entries discovered underneath it. MetadataPlugin attaches the repository at REPO_SUBDIR = '.objectstack/metadata' under the project root (packages/metadata/src/plugin.ts:386-398), so the root path contains a dot segment and the whole watch is ignored.

Measured on chokidar 5.0.0, the version the package depends on, 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; in both, view/a.json is rewritten and view/b.json is added after ready:

A plain root          | root= metadata
   getWatched keys: [ '', 'metadata', 'metadata/view' ]
   events: [["add","view/b.json"],["change","view/a.json"]]
B root under dot-dir  | root= .objectstack/metadata
   getWatched keys: []
   events: []

Case B is the production layout.

Why it may matter

The watcher is not decoration. handleFsChange translates external edits into MetadataEvents with source: 'fs', and MetadataManager.setRepository() subscribes to repo.watch({}) and re-emits every event through notifyWatchers, invalidating the registry entry and the list() cache for the affected type (packages/metadata/src/metadata-manager.ts:2560-2640). So there is a live consumer wired to a source that, in the layout the plugin actually uses, can never fire. Hand edits, a git checkout that brings metadata JSON in, or any out-of-process writer under .objectstack/metadata/ are therefore invisible until the next start().

Whether a user hits this today depends on whether anyone edits files under .objectstack/metadata/ out of process, which I did not measure — I am recording both readings rather than grading it myself:

  • read as observation-class: a shipped mechanism that has never been exercised in the production layout, with no traced user-visible failure;
  • read as a concrete defect: a capability that is wired end to end and silently does nothing, which is exactly the shape that survives review because everything looks connected.

Left unlabeled for the triage round to grade.

Not caused by #7000's fix

Pre-existing on origin/main, and unchanged by that PR in both directions: before it, start() created the root and armed a watcher that was then ignored; after it, ensureRoot() arms a watcher that is still ignored. The #7000 PR's watcher-arming pin deliberately uses a root that is not under a dot-directory, precisely so it measures the arming rather than this.

Possible directions (not a ruling)

  • Scope the matcher to paths relative to the root instead of absolute paths (chokidar's matcher receives absolute paths, so this needs an explicit function matcher).
  • Or drop the regex and ignore only the repository's own .objectstack/ bookkeeping subtree, which is what the comment says the intent is — parseItemPath already rejects anything under .objectstack, so the matcher may be redundant with the parse guard.

Either shape is a contract-ish decision about what the watcher promises to see; it should be ruled, not guessed.

Related


Generated by Claude Code

Activity

  1. os-zhuang commented on Aug 10, 2026

    @os-zhuang
    ContributorAuthor

    Triage: pm:queue + domain:metadata.

    • Classification: concrete defect — a capability wired end to end that silently does nothing in the production layout, with a measured A/B reproduction on the package's own chokidar version and watch options. This is the restore-invariant shape, not an observation: the consumer side (MetadataManager.setRepository() subscribing to repo.watch({}) and re-emitting through notifyWatchers) is live code whose source can never fire.
    • Landing site: packages/metadata-fs/src/repository.ts — verified on origin/main @ 3e8e669: ignored: [/(^|[\\/])\../] with the "skip dotfiles incl. .objectstack" comment is live at repository.ts:398, and REPO_SUBDIR = '.objectstack/metadata' at packages/metadata/src/plugin.ts:57 (joined at :389) puts the watched root under a dot segment. packages/metadata* ⇒ domain:metadata.
    • On the filer's "should be ruled, not guessed" note: judged implementation-layer, not decision-box. The watcher's promise is already stated by its own comment and its consumer wiring — both proposed shapes converge on "see everything under the root, ignore the repository's own bookkeeping", and the body's own evidence that parseItemPath already rejects .objectstack paths gives the implementer the measurement to pick between them. Record the choice and its rationale in the PR.
    • Dedup: repo-scoped search (chokidar watcher) — only this card; cli/metadata: os migrate plan still creates .objectstack/metadata/ on a fresh project (the residual half of #6743's dry-run write side effect) #7000 / cli/driver-sql: os migrate plan 自称 dry-run,却仍会在全新项目上创建空数据库文件(#6469 的残余写副作用) #6743 are the origin cards and explicitly exclude this scope.
    • Not target:v17: the mechanism has never worked in this layout, so no shipped behaviour regresses; whether any user edits .objectstack/metadata/ out of process today is unmeasured (the filer says so). Class ① unproven ⇒ default not blocking. Re-grade if a user-visible stale-metadata report ties back to this.

    本评论来自分诊座位 Routine(#5474 试点),不构成认领。


    Generated by Claude Code

  2. self-assigned this
    on Aug 10, 2026
  3. os-help commented on Aug 10, 2026

    @os-help
    Collaborator

    Claim: PM loop round 2 (spec-surface seat, cross-lane under maintainer directive)
    Session: session_016R9de1FqP7NvwKvqXi92Gh
    Branch: claude/issue-7150-watcher-dot-root
    Worktree: objectstack-issue-7150
    Domain: domain:metadata
    File surface: packages/metadata-fs/src/repository.ts + its tests, .changeset/ (stop on breach; explain in the report). packages/metadata/src/plugin.ts is read-only context (REPO_SUBDIR), not expected to change.
    Container & model: M card, mode:subagent, model: opus
    Serial constraints cleared: origin card #7000's PR explicitly excludes this scope (its watcher-arming pin deliberately avoids a dot-rooted path); no open PR touches metadata-fs per triage's premise check @ 3e8e669 (2026-08-10T01:22Z). Nothing else in flight in packages/metadata*.

    Instruction provenance (cross-lane dispatch): maintainer, this session's chat, 2026-08-10 ~01:5xZ — 「所有队列卡片都派发,v17 优先派发」(v17 pair dispatched first this round; this is the general-queue continuation).


    Generated by Claude Code

  4. os-help commented on Aug 10, 2026

    @os-help
    Collaborator

    ACCEPT — PR #7208 (review of record, session session_016R9de1FqP7NvwKvqXi92Gh).

    What shipped: the watcher's ignored matcher becomes a function evaluated against the path relative to the watch root — the root's own dot segments are never judged, dotfiles under the root (including .objectstack/ bookkeeping) stay ignored. 4 files, +295/−2, changeset patch, the consumer-side test file being the end-to-end assertion the dispatch asked for (test-only, disclosed in the scope note — in scope).

    Verified against GitHub: draft targeting main, first line Fixes #7150; CI converged at c47cc79e0 — 26 checks, zero failures; ESLint success (02:51:47Z) and TypeScript Type Check success (02:59:34Z) personally confirmed.

    Review credits:

    1. The shape choice is a measurement, not a preference — exactly what triage's ruling asked to be recorded. Shape B's premise ("parseItemPath makes the matcher redundant") was falsified: the guard rejects the single literal .objectstack, so .cache/x.json parses as type .cache and view/.scratch.json as an item named .scratch, and Shape B would put the repo's own log in the poll set. The follow-up defence-in-depth gap is filed as metadata-fs: parseItemPath's dot guard rejects only the literal .objectstack, not dot segments generally — narrower than its callers read #7216 (observation-class) rather than folded in.
    2. The reverse verification's per-case direction was predicted first, including the subtle half: case 2's toEqual([]) stays green on an inert watcher, which is why it carries a liveness control — and on revert it was the control that failed, as predicted.
    3. A false green was caught and named: the consumer test initially stayed green against a stale dist after reverting only src — the [skill] 新 worktree 里第一次验证之前必须先 build 依赖闭包 —— AGENTS.md §9 的陈旧产物陷阱当日连咬三个 dev(假红 + 假绿两个方向) #6371 trap, self-caught, rebuilt, re-measured red. This is the stale-artefact discipline working at the dev, not the reviewer.
    4. Probe selection knowledge worth keeping: an editor .swp is a useless discriminator here (chokidar's atomic filters .*.sw[px] before any matcher runs).

    Also recorded: the GitHub body sanitizer ate a bare angle-bracket token in the PR body's first revision — the dev caught it on read-back and re-spelled it. Consistent with the seat's G-notes.

    Next: ready → merge queue.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions