Skip to content

fix(metadata): MetadataPlugin.watch defaults to false, matching its documented contract (#9770) - #9812

Merged
os-elon merged 2 commits into
mainfrom
claude/issue-9770-metadata-plugin-watch-default
Aug 19, 2026
Merged

fix(metadata): MetadataPlugin.watch defaults to false, matching its documented contract (#9770)#9812
os-elon merged 2 commits into
mainfrom
claude/issue-9770-metadata-plugin-watch-default

Conversation

@os-elon

Copy link
Copy Markdown
Collaborator

Fixes#9770

MetadataPluginOptions.watch documents its own default directly above the field
(packages/metadata/src/plugin.ts):

Default: false (post PR-10e — was previously true).

The constructor implemented the opposite, and it did so in two places, so both
entry shapes resolved true:

entry shapeold spellingresolved
caller omits the keythis.options = { watch: true, ...options }true
caller passes an explicit undefinedthis.options.watch ?? truetrue

Both non-test construction sites in this repo — packages/runtime/src/standalone-stack.ts
and packages/cli/src/commands/serve.ts — pass watch: false explicitly, each citing an
EMFILE hazard, which is exactly why the drift was invisible: no in-repo configuration
exercised the default, so no test and no gate could see it. MetadataPlugin is a public
export of @objectstack/metadata and @objectstack/metadata/node, so the consumers who
did reach the default were external ones, reaching it by doing the documented-safe
thing and not naming the key at all. What they got was a recursive chokidar poll over the
entire project root — the pre-fix reverse-verification leg below prints the live watcher
object, usePolling: true, interval: 1000, with node_modules excluded only by
chokidar's default ignored.

The change

  • the options literal now normalizes the flag: watch: options.watch ?? false
  • the effective-watch fallback is (this.options.watch ?? false), as adjudicated

This is a default flip, not a capability removal: an explicit watch: true still
attaches the scanner and its watcher. The sealed-runtime carve-out is untouched —
bootstrap: 'artifact-only' still forces watching off even against an explicit
watch: true.

One deviation from the adjudicated spelling, and why

The ruling prescribed { watch: true, ...options }{ watch: false, ...options }. This
PR instead spells the same default as { ...options, watch: options.watch ?? false }. The
ruling's substance is unchanged — the default is false — but the prescribed spelling
would have left the two entry shapes diverging again, one layer further down:

  • a spread preserves an explicitly-passed watch: undefinedverbatim, so
    this.options.watch would stay undefined for that entry shape;
  • and not every read of the flag routes through the nullish fallback. The start()-time
    FileSystemRepository attachment reads disableWatch: this.options.watch === false and
    logs watch: this.options.watch !== false — identity checks, not nullish ones.

So under the literal spelling, an omitted key would disable both watchers while an explicit
undefined would disable the source-file scanner but still attach the repository watcher,
and the log line would claim watching was on. Normalizing once in the constructor makes
both entry shapes resolve identically at every downstream read. The ?? false
fallback is kept exactly as adjudicated, now as a defensive spelling.

Pins

Six pins in packages/metadata/src/plugin.test.ts, all asserting on the observable
whether a watcher object exists on the manager — rather than on the resolved options value
alone, so an options field reading false while a watcher is still constructed cannot pass
them:

  1. no options argument at all — no watcher
  2. watch key omitted — no watcher
  3. watch: undefined explicitly — no watcher, and the resolved value is normalized false
  4. bootstrap: 'lazy' with the key omitted — no watcher
  5. control: explicit watch: true — watcher is attached
  6. bootstrap: 'artifact-only' against an explicit watch: true — no watcher

Verification

Full package suite, on the merge commit: pnpm --filter @objectstack/metadata test
31 files, 609 tests passed.

Reverse verification, both legs run against the built dependency closure, each restored and
proven byte-identical afterwards (git diff --quiet → YES). The subject resolves through a
relative same-package import, so vitest transforms the mutated source directly; no dist
staleness can mask either leg, and both legs demonstrably changed the outcome:

  • leg Aplugin.ts reverted to its pre-fix version: 4 failed | 13 passed. The
    four failures are exactly the four default pins; the control and the artifact-only pin
    stayed green. The failure output prints the attached chokidar watcher with
    usePolling: true, interval: 1000.
  • leg B — the bootstrapMode === 'artifact-only' limb deleted from the fixed file:
    2 failed | 15 passed, the two artifact-only pins. That is how the carve-out was
    verified still short-circuiting: it is load-bearing, and removing it is detected.
  • restored control — 17 passed (17), tree clean.

Gates

origin/main was merged first (it had moved to 4c260cda5, carrying #9657's change to
check-durability-degradation-log-level). Every reading below is post-merge, taken on
167beb665 with a clean tree, re-derived from the real diff with
node scripts/pm/dispatch-gates.mjs rather than from the dispatch's lead:

gatereading
check:changeset-gate-self-testsOK
check:cross-package-test-inputsOK — 12 packages read outside themselves, all declared
check:durability-log-levelOK — 29 durability-critical seams, all loud; 66 read seams, none inventing an answer (post-merge)
check:objectui-changesetOK
check:stack-collection-mapsOK — 7 enumerations reconciled against 32 declared collections
check:engine-double-contractOK — 321 pinned, 133 in DEBT, 2 exempt
check:where-matcherOK — 255 matchers, 0 silently-wrong, none new
check:query-options-erasureOK — 67 unswept non-test sites, none new
check:type-check-coverageOK — 64/77 packages type-checked, 13 in DEBT, 1 exempt
check:type-check-debt --re-measureOK — 33 ledger entries re-measured (328.3s), 1926 raw tsc errors, none above its recorded number, surplus none
check-adr-0087-registration / check-changeset-no-major / check-empty-changesetOK
docs-audit/check-affected-docsOK
check:nul-bytesOK — 6252 files, no raw control bytes

check:type-check-debt --re-measure was run with the workspace closure built
(turbo run build --filter='./packages/*' --filter='./packages/*/*', 70 tasks successful),
so it is a real measurement, not a refusal. No baseline was raised.

@objectstack/metadata declares no typecheck script — the invocation fails loudly with
ERR_PNPM_RECURSIVE_RUN_NO_SCRIPT rather than passing vacuously — so its type coverage is
accounted for through the ledger the re-measure above exercises.

Consequence for #9701

#9701 was closed completed on the verdict that its watcher exposure was acceptable
because no shipped configuration reaches the watcher. This default was the single thing
that made it reachable, and only for external consumers. No evidence was found that the
true default was deliberate — the doc comment, both call sites, and README.md's own
bootstrap: 'lazy' example (captioned "no FS scan", with the key omitted) all point the
other way, and no test asserted a true default. With the flip, that verdict stands
permanently; #9701 does not need reopening.

Changeset: patch on @objectstack/metadata.

Generated by Claude Code


Generated by Claude Code

…ocumented contract (#9770)
`MetadataPluginOptions.watch` documents `Default: false (post PR-10e — was
previously true)` directly above the field, but the constructor implemented the
opposite in two places: the options literal `{ watch: true, ...options }` (the
omitted-key entry shape) and the `this.options.watch ?? true` fallback (the
explicit-`undefined` entry shape). Both resolved `true`, so an external consumer
constructing the public export without naming the key got the recursive
project-root polling watcher that both in-repo call sites explicitly refuse,
citing an EMFILE hazard at each.
The flag is now normalized once in the constructor (`watch: options.watch ??
false`) instead of `{ watch: false, ...options }`: a spread preserves an
explicitly-passed `undefined` verbatim, and not every read routes through a
nullish fallback — the start()-time FileSystemRepository `disableWatch` keys on
`=== false`. Coercing once makes both entry shapes resolve identically at every
downstream read. The `?? false` fallback is kept as the adjudicated defensive
spelling.
This is a default flip, not a capability removal: an explicit `watch: true`
still attaches the watcher, and the `bootstrap: 'artifact-only'` carve-out still
forces watching off against an explicit `watch: true`. Pins assert on the
observable (whether a watcher object exists on the manager), not on the resolved
options value alone.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019yDEhPBC3tcGkW9bkce1HM
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 1 changed package(s)), so this run has no opinion about the docs.

What this run could not see
  • 1 name(s) were too generic to anchor anything (single lowercase words)

Coarse fallback — 11 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 06f9848f91fc787e67af32b2f6f88e55b5d93e86packageMentionDocs.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Aug 19, 2026
@os-elon
os-elon marked this pull request as ready for review August 19, 2026 01:37
@os-elon
os-elon added this pull request to the merge queueAug 19, 2026
Merged via the queue into main with commit 2a9752cAug 19, 2026
26 checks passed
@os-elon
os-elon deleted the claude/issue-9770-metadata-plugin-watch-default branch August 19, 2026 01:52
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[finding] MetadataPlugin.watch documents its default as false but implements true, attaching a project-root polling watcher for external consumers

2 participants

@os-elon@claude