Repository navigation
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
Description
Activity
Routing repair only: added
domain:metadata.findinggrade unchanged, no ownership taken.- Landing site: per the card's own analysis, the fix lives in
MetadataPlugin/ the standalone boot (FileSystemRepositoryroot creation at attach time), not inpackages/cli— theos migrate plancommand is only the trigger.packages/metadata*⇒domain:metadataper the domain table. Cross-checked onorigin/main@2f3e793:packages/cli/src/commands/migrate/plan.tsitself performs no mkdir, consistent with the attribution.
本评论来自分诊座位 Routine(#5474 试点),不构成认领。
Generated by Claude Code
- Landing site: per the card's own analysis, the fix lives in
Finding-triage ruling: Promote —
findingtopm:queue.Scoped residual of an already-ruled property (#6743: a dry run leaves nothing behind): os migrate plan still creates the .objectstack/metadata/ tree on a fresh project because MetadataPlugin attaches FileSystemRepository at boot (plugin.ts:386-398) and repository start() mkdirs unconditionally (repository.ts:114-115). The DB half was fixed; leaving the FS half undone is the exact failure mode #6743 exists to prevent.
Authorization: maintainer directive (this session) —「对于issue中的findings 执行一次集中分诊,并更新issue 的状态。」Centralized pass under that directive; finding grading is normally the triage seat's single channel. Round: objectstack#4949 finding-triage, 2026-08-10.
Session: 01JaVVMrSxt7Tgi1uwEuDtH7
Generated by Claude Code
Claim: PM loop round 9 (
domain:metadataseat, sticker #6367)Session:
session_01W6bLax4KMrSfnE1ydFU8Dw
Branch:claude/issue-7000-migrate-plan-no-metadata-root
Worktree:objectstack-issue-7000
Base:origin/main@ currentPromoted 2026-08-10T00:12Z under the maintainer's centralized finding-triage directive. No unmet promotion gate on this card — the triage comment names the mechanism at line level (
plugin.ts:386-398attachesFileSystemRepository,repository.ts:114-115mkdirs unconditionally instart()), and the earlier routing repair independently cross-checked onorigin/main@2f3e793thatpackages/cli/src/commands/migrate/plan.tsperforms no mkdir of its own. So the trigger is the CLI command but the landing site is this lane's.The property being completed is already ruled by #6743 — a dry run leaves nothing behind. Its database half landed (PR #6997); this is the filesystem half, and leaving it undone is the exact failure mode #6743 exists to prevent (#3917's deferral looked complete and was not).
Region:
packages/metadata— disjoint from #6992, dispatched alongside it intometadata-protocol's cold-boot audit.
Generated by Claude Code
os-dev report
Status: done — PR #7152 (DRAFT). Branch
claude/issue-7000-migrate-plan-no-metadata-root, baseorigin/main@3e8e669c0. Premise still valid.The side effect, reproduced
Reproduced at the seam rather than through the CLI, and it comes back byte-identical to the card's
findoutput. With the fix reverted, the new residue sweep prints:AssertionError: expected [ '.objectstack', ...(3) ] to deeply equal [] + [ + ".objectstack", + ".objectstack/metadata", + ".objectstack/metadata/.objectstack", + ".objectstack/metadata/.objectstack/.log", + ]Mechanism confirmed at the symbol, with the file moved as triage warned:
FileSystemRepository.start()mkdir-ed its root and its.objectstack/.logunconditionally, and the file is nowpackages/metadata-fs/src/repository.ts(packages/metadatawas split;plugin.ts:386-398still attaches atREPO_SUBDIR = '.objectstack/metadata', unchanged). No CLI file was needed — triage's attribution held.One correction to the card's framing. The database half is pushed but not merged: PR #6997 is still an open draft,
b70ca0b0cis not an ancestor oforigin/main, andsqliteAbsentFiledoes not exist onmain. So onmaintodayos migrate planstill creates.objectstack/data/too. This PR removes the metadata tree only, and its pin asserts the metadata paths specifically rather than the absence of.objectstack/as a whole — the two halves stay independent and land independently.What changed
The behavioural change is confined to
packages/metadata-fs/src/repository.ts; the rest of the diff is tests, the package README and a changeset.start()creates nothing. Every read path already tolerated an absent root (scanHeadsswallows thereaddirENOENT,JsonlLog.readAll/highestSeqandgetguard onexistsSync).- New private
ensureRoot(), called byput()anddelete()right before they touch the disk — create-on-write, exactly the shape assumption 1 predicted. No read path required the directory, so no read-sideensureRoot()was needed. - Assumption 1, the one measured amendment: a watch path did require it. chokidar cannot watch a path that does not exist yet — measured on chokidar 5.0.0 with the repository's own
usePollingoptions, a root created afterwatch()produces no events at all, ever. Sostart()arms the watcher only when the root exists andensureRoot()arms it when the root appears. Residual case, documented in the README: a root created by a third party while the process runs, with this repository never writing, is not picked up until the nextstart().
Which commands the fix covers (assumption 2)
The property, not the command — the fix is at the repository, so every boot that attaches it without writing metadata is covered: all nine
bootSchemaStackcommands (migrate plan|apply|meta|value-shapes|recorded-by|resume|summary-nulls|files-to-references,meta resync) andserve/devthroughcreateStandaloneStack. Write-performing commands create the root when they write, as before. It does not fix one command by accident.The boot is not weakened (assumption 4)
No DDL, no seed rows, nothing written that was not written before — the only removed behaviour is the
mkdir. Verified positively rather than by absence: the control cases assert the repository is still attached (plugin.repositorydefined, the object the[MetadataPlugin] FileSystemRepository attachedline names), still readable, and that a write through it still materializes root + body + JSONL log.packages/metadata591 tests and themetadata-corerepository contract suite green and untouched.Assumption 3 — the
.objectstack/.lognestingNot a defect on its own: the repository always keeps its log at
.objectstack/.logrelative to its own root, so the nesting is just that documented layout meeting a root that lives under the project's.objectstack/. But chasing it surfaced a real one, filed as #7150: the watcher'signoreddotfile regex is applied by chokidar to the watched root path itself, so a root under a dot-directory is ignored wholesale. Measured side by side on chokidar 5.0.0 — root atmetadata/:getWatched()populated, add/change events fire; same tree at.objectstack/metadata/:getWatched()empty, zero events. The plugin's root is the second shape, so that watcher (andMetadataManager'srepo.watch({})re-emit loop behind it) has never fired in the production layout. Pre-existing onorigin/main, unchanged by this PR in both directions; filed unlabeled for triage to grade, with both the observation-class and defect readings written out.Reverse verification — predictions vs results
Predictions recorded before running. Reverted to my own base
3e8e669c0, never a movingorigin/main, viagit checkout(no stash).Variant 1 — whole fix removed:
# Case Predicted Measured 1 start()creates neither root nor.objectstack/.logRED RED 2 absent root attachable / reads empty RED on the trailing residue assertion only RED exactly there; every read assertion above it passed first 3 first write materializes root/body/log GREEN both directions (guard) RED — missed prediction 4 watcher arming GREEN both directions (guard) RED — missed prediction 5 read-only boot creates nothing RED RED 6 same boot, watcher enabled RED RED 7 attached repository still usable GREEN both directions (guard) GREEN Missed predictions 3 and 4, and their cause: each of those cases opens with an
expect(existsSync(root)).toBe(false)precondition right afterstart()— a line that pins the new behaviour too, which I did not account for when predicting. They went red there (lines 108 and 150) before reaching the assertions the prediction was about, so their substantive halves were left unmeasured by variant 1, not passed. I then measured them: with those two precondition lines removed and the fix still out, both cases pass. So the prediction was right about the substance, wrong about the file — 3 and 4 are guards, 1/2/5/6 are the evidence.Variant 2 — the naive shape of the fix (create-on-write kept, watcher compensation deleted). Predicted: case 4 RED, and
fs-behavior.test.ts's existing external-edit case GREEN because its root exists atstart(). Measured: exactly that — 1 failed / 27 passed, the failure being case 4 alone. That is what makes case 4 load-bearing despite being a guard in variant 1.A false green worth recording: the first attempt at variant 1 reported all 591
packages/metadatatests passing with the fix reverted. Cause — the plugin test imports the built@objectstack/metadata-fs, so reverting the source changed nothing untildistwas rebuilt. Re-run after rebuilding: 2 failed as predicted.Local results
packages/metadata-fs Test Files 3 passed (3) Tests 28 passed (28) packages/metadata Test Files 29 passed (29) Tests 591 passed (591) packages/metadata-fs typecheck (tsc --noEmit) -> Done pnpm lint (eslint . --no-inline-config) -> clean check:nul-bytes / check:doc-authoring / check:published-files / check:engine-double-contract / check:changeset-gate-self-tests / check:release-notes / check:release-body / check:objectui-changeset -> PASS@objectstack/metadatahas notypecheckscript (it is acheck:type-check-debtledger entry), so I measuredtsc --noEmitover the package instead: 89 errors, 0 of them in the new test file. The first draft did add one (TS2835, from the extensionless./pluginimport the neighbouringplugin.test.tsuses); fixed by importing./plugin.js. No ledger drift.CI, per-job conclusions on PR #7152 (head
38b1118)Job Status Conclusion ESLint completed success TypeScript Type Check completed success Check Changeset completed success Build Core completed success Test Core (1/3, 2/3, 3/3) + rollup completed success Dogfood Regression Gate (1/3, 2/3, 3/3) + rollup completed success Dogfood Verify CLI completed success Temporal Conformance (live PG + MySQL) completed success Console Pin Freshness completed success Check PR Size / Documentation Links / ADR maintainer approval / Auto Label / issue-claim guard / docs-flag completed success Build Docs, Console Pin Gate completed skipped (path filter) No
skip-changesetlabel needed or applied — this PR ships a changeset (.changeset/gentle-pears-attach.md) andCheck Changesetis green.Unverified / not done
- Not driven through the real
os migrate planbinary end to end. The pin sits at the plugin/repository seam because that is where the fix is and because the CLI is off this lane's surface; the CLI-level equivalent would be apackages/clitest, which the dispatch put behind a STOP. - Whether anyone edits files under
.objectstack/metadata/out of process today — i.e. the user-facing severity of metadata-fs: the FileSystemRepository chokidar watcher is inert in the production layout — its ownignoreddotfile regex matches the.objectstacksegment of the root path #7150 — is not measured. Left for triage rather than graded here. - Left DRAFT, not marked ready, auto-merge not enabled.
Generated by Claude Code
Generated by Claude Code
- added a commit that references this issue
on Aug 17, 2026
Found while implementing #6743 (PR #6997). Out of that card's scope — #6743's ruling and its pin are scoped to
.objectstack/data/, the database — so this is filed separately and unassigned.Duplicate search:
migrate plan/ dry-run /FileSystemRepository/.objectstack/metadataover open issues and open PRs — 0 hits (other than #6743 / #6997 themselves).Observation
#6743 removed the database half of the write side effect: after PR #6997,
os migrate planon a never-started project leaves no.objectstack/data/at all. The metadata repository half is untouched and still runs — the same command, on the same fresh project, still brings a directory tree into existence:Source:
MetadataPluginattaches itsFileSystemRepositoryatrepoRoot=<projectRoot>/.objectstack/metadataduring the boot thatbootSchemaStackperforms, and the attach creates the root (the boot log line[MetadataPlugin] FileSystemRepository attachednames it).Why it is the same class as #6743, and why it is nonetheless separate
Same class: a command that declares itself a dry run leaves a write side effect behind, and the existence of
.objectstack/stops being a usable signal for "this project has never been started".Separate: it is a different subsystem (metadata repository, not the sqlite driver's open mode), the fix would live in
MetadataPlugin/ the standalone boot rather than in@objectstack/driver-sql, and #6743's ruling — including its "same report, byte for byte" constraint — reasons only about the database. Fixing it inside #6743 would have been scope expansion past a graded card.Severity as observed
Low, and deliberately filed as an observation rather than a defect: what is left behind is an empty repository skeleton plus an empty
.logdirectory. No user-visible failure has been traced to it. Recording it so the "dry run leaves nothing behind" property is either completed deliberately or closed with a reason, rather than left half-done by accident — #6743 exists precisely because the #3917 deferral looked complete and was not.Note the boot is genuinely read-only in the sense #3917 established (no DDL, no seed rows); the question is only whether attaching a metadata repository should create its root during a command that will never write metadata.
Related
os migrate plan自称 dry-run,却仍会在全新项目上创建空数据库文件(#6469 的残余写副作用) #6743 / PR fix(cli,driver-sql):os migrate planstops creating a database on a fresh project (#6743) #6997 — the database half, fixed