Skip to content

metadata-fs: parseItemPath's dot guard rejects only the literal .objectstack, not dot segments generally — narrower than its callers read #7216

Description

@os-help

Found while implementing #7150 (PR #7208), measuring the two candidate fix shapes. Out of that card's scope — #7150 fixes the watcher's ignored matcher — so filed separately and unassigned.

Duplicate search over open issues: parseItemPath, metadata-fs layout dot guard — 0 hits other than #7150 itself.

Observation

parseItemPath (packages/metadata-fs/src/layout.ts:37-50) guards with:

const rel = path.relative(layout.root, absPath);
if (rel.startsWith('..') || rel.startsWith('.objectstack')) return null;

That rejects exactly one name. Any other dot segment parses as ordinary metadata. Measured directly against the real function, feeding it the paths a matcher-less watcher delivers:

add    .cache/x.json          parseItemPath => {"type":".cache","name":"x"}
add    view/.scratch.json     parseItemPath => {"type":"view","name":".scratch"}

So a dot-directory at the type level becomes a metadata type called .cache, and a dotfile at the item level becomes an item named .scratch.

This disagrees with the sibling boot-scan invariant. scanHeads (repository.ts:397-399) skips every dot entry:

if (entry.name.startsWith('.')) continue;

So the boot scan and the parse guard hold different ideas of what counts as repository content. The guard's name and its single call site (handleFsChange) both read as "reject the repository's own bookkeeping", which is what it does for .objectstack and only .objectstack.

Why this is observation-class, not a defect today

It is currently unreachable. #7150's fix filters dot entries at the chokidar matcher, on the path relative to the watch root, so nothing with a dot segment reaches handleFsChange in the first place — PR #7208 pins exactly that (still ignores dot entries UNDER the root, including its own bookkeeping). Before that fix the whole watcher was inert in the production layout, so it was unreachable then too, for a different reason.

What it costs is defence in depth: the invariant now lives in exactly one place (the matcher), and parseItemPath will not catch a regression in it. Anyone who later widens or replaces the matcher — the "drop the regex, parseItemPath already rejects .objectstack" reading that #7150's body proposed and #7208 rejected on this evidence — reintroduces the leak silently, because the guard looks like it covers the case.

No user-visible failure is traced to this, and I did not measure one. Filed plainly for the triage round to grade rather than graded here.

Possible direction (not a ruling)

Make the guard match the boot scan — reject any rel whose first segment starts with ., and reject a basename starting with . — so "dot entries are not repository content" is stated once and enforced at both seams. Cheap, no consumer changes, and it would fail closed if the watcher matcher ever regresses.

Related


Generated by Claude Code

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions