You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
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
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/metadata over 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 plan on 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:
$ ls -d .objectstack
ls: cannot access '.objectstack': No such file or directory
$ os migrate plan # succeeds, prints the plan
$ find .objectstack -maxdepth 3
.objectstack
.objectstack/metadata
.objectstack/metadata/.objectstack
.objectstack/metadata/.objectstack/.log
Source: MetadataPlugin attaches its FileSystemRepository at repoRoot=<projectRoot>/.objectstack/metadata during the boot that bootSchemaStack performs, and the attach creates the root (the boot log line [MetadataPlugin] FileSystemRepository attached names 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 .log directory. 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.
Landing site: per the card's own analysis, the fix lives in MetadataPlugin / the standalone boot (FileSystemRepository root creation at attach time), not in packages/cli — the os migrate plan command is only the trigger. packages/metadata* ⇒ domain:metadata per the domain table. Cross-checked on origin/main @ 2f3e793: packages/cli/src/commands/migrate/plan.ts itself performs no mkdir, consistent with the attribution.
Finding-triage ruling: Promote — finding to pm: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
Session: session_01W6bLax4KMrSfnE1ydFU8Dw
Branch: claude/issue-7000-migrate-plan-no-metadata-root
Worktree: objectstack-issue-7000
Base: origin/main @ current
Promoted 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-398 attaches FileSystemRepository, repository.ts:114-115 mkdirs unconditionally in start()), and the earlier routing repair independently cross-checked on origin/main @ 2f3e793 that packages/cli/src/commands/migrate/plan.ts performs 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 into metadata-protocol's cold-boot audit.
Status: done — PR #7152 (DRAFT). Branch claude/issue-7000-migrate-plan-no-metadata-root, base origin/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 find output. With the fix reverted, the new residue sweep prints:
Mechanism confirmed at the symbol, with the file moved as triage warned: FileSystemRepository.start()mkdir-ed its root and its .objectstack/.log unconditionally, and the file is now packages/metadata-fs/src/repository.ts (packages/metadata was split; plugin.ts:386-398 still attaches at REPO_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, b70ca0b0c is not an ancestor of origin/main, and sqliteAbsentFile does not exist on main. So on main today os migrate plan still 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 (scanHeads swallows the readdir ENOENT, JsonlLog.readAll / highestSeq and get guard on existsSync).
New private ensureRoot(), called by put() and delete() right before they touch the disk — create-on-write, exactly the shape assumption 1 predicted. No read path required the directory, so no read-side ensureRoot() 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 usePolling options, a root created after watch() produces no events at all, ever. So start() arms the watcher only when the root exists and ensureRoot() 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 next start().
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 bootSchemaStack commands (migrate plan|apply|meta|value-shapes|recorded-by|resume|summary-nulls|files-to-references, meta resync) and serve / dev through createStandaloneStack. 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.repository defined, the object the [MetadataPlugin] FileSystemRepository attached line names), still readable, and that a write through it still materializes root + body + JSONL log. packages/metadata 591 tests and the metadata-core repository contract suite green and untouched.
Assumption 3 — the .objectstack/.log nesting
Not 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's ignored dotfile 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 at metadata/: 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 (and MetadataManager's repo.watch({}) re-emit loop behind it) has never fired in the production layout. Pre-existing on origin/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 moving origin/main, via git checkout (no stash).
Variant 1 — whole fix removed:
#
Case
Predicted
Measured
1
start() creates neither root nor .objectstack/.log
RED
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 after start() — 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 at start(). 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/metadata tests passing with the fix reverted. Cause — the plugin test imports the built@objectstack/metadata-fs, so reverting the source changed nothing until dist was rebuilt. Re-run after rebuilding: 2 failed as predicted.
@objectstack/metadata has no typecheck script (it is a check:type-check-debt ledger entry), so I measured tsc --noEmit over the package instead: 89 errors, 0 of them in the new test file. The first draft did add one (TS2835, from the extensionless ./plugin import the neighbouring plugin.test.ts uses); fixed by importing ./plugin.js. No ledger drift.
CI, per-job conclusions on PR #7152 (head 38b1118)
No skip-changeset label needed or applied — this PR ships a changeset (.changeset/gentle-pears-attach.md) and Check Changeset is green.
Unverified / not done
Not driven through the real os migrate plan binary 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 a packages/cli test, which the dispatch put behind a STOP.
Activity
os-zhuang commentedon Aug 9, 2026
Routing repair only: added
domain:metadata.findinggrade unchanged, no ownership taken.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
os-zhuang commentedon Aug 10, 2026
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
os-zhuang commentedon Aug 10, 2026
Claim: PM loop round 9 (
domain:metadataseat, sticker #6367)Session:
session_01W6bLax4KMrSfnE1ydFU8DwBranch:
claude/issue-7000-migrate-plan-no-metadata-rootWorktree:
objectstack-issue-7000Base:
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-zhuang commentedon Aug 10, 2026
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: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).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.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:
start()creates neither root nor.objectstack/.logMissed 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
@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)No
skip-changesetlabel needed or applied — this PR ships a changeset (.changeset/gentle-pears-attach.md) andCheck Changesetis green.Unverified / not done
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..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.Generated by Claude Code
Generated by Claude Code
fix(metadata-fs): attaching a FileSystemRepository no longer creates …