Repository navigation
feat: the package upgrade plan — compare the installed and incoming versions, an install dry run, os package install --dry-run - #30983
Conversation
…ith the incoming one The package upgrade plan lists objects and fields added and removed, type changes with their drift category, renames a field declares with `renamedFrom`, suspected renames nothing declares, and seed datasets added, removed and changed with their modes. A declared rename whose type changes is also a type change, judged on its own. Claude-Session: https://claude.ai/code/session_016WV4cyPqWLpc3AxaZrs98n Co-authored-by: Claude <noreply@anthropic.com>
…ade plan `diffStacks` lifts the command's comparison out of `run()` unchanged, and the new test pins its output byte for byte (entry order and each entry's key order) over every shape it reports today, plus a type-level pin that the upgrade plan's three drift words equal driver-sql's `DriftCategory`. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cyPqWLpc3AxaZrs98n
…pgrade plan `os diff` now builds its object and field entries from `comparePackageVersions` (`@objectstack/metadata-core`), the comparison the install door's dry run and the `manifest` service read, so the CLI and an install cannot disagree about one pair of versions. The output for every shape it reported before is unchanged (pinned before the move). New with the plan: a declared `renamedFrom` rename, a suspected rename on the removed key, and seed datasets added, removed and changed, each with its mode. `seedModeOverwritesMatchedRows` names the two modes whose replay writes over a matched row, for the CLI's wording and the environment half's count. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cyPqWLpc3AxaZrs98n
…ironment knows The `manifest` service gains `planUpgrade(installed, incoming)`: the package upgrade plan (`comparePackageVersions`, each side folded with core's `resolveStackCollection`) plus two read-only readings, across every organization with the system context: - rows holding a value in each stored column the upgrade removes, renames (declared or suspected) or retypes, and rows of each object it removes; - for each `upsert`/`update` dataset both versions declare, the rows a customer edited that the seed replay overwrites: a column the replay writes whose stored value differs from the installed seed value and from the incoming one, compared as the replay compares (temporal and blank storage forms; credential, file, reference, expression and `applyOn: 'create'` columns never compared). A failed reading answers null, never zero. metadata-core exports the plan's dataset reader (`upgradePlanSeedDatasets`) so both halves pair datasets by one key. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cyPqWLpc3AxaZrs98n
… and writes nothing `POST /api/v1/marketplace/install-local` with `dryRun: true` answers 200 with the `manifest` service's `planUpgrade` of the installed version (its ledger entry, converted forward) against the incoming one, after every refusal that says the package cannot install at all and before the first confirmation ceremony or write. A `dryRun` that is not a boolean, a dry run of an `unmanaged` install and one of a customization pair are refused 400; a runtime whose `manifest` service answers no `planUpgrade` answers 503. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cyPqWLpc3AxaZrs98n
`--dry-run` sends `dryRun: true` and prints the runtime's plan, each finding with the environment's reading beside it. A runtime that ignored the key and installed is printed as a failure, exit 1. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cyPqWLpc3AxaZrs98n
…driver double `check:objectql-double-limit` drives every `find` double it finds with its own rows; the in-memory driver's per-object failure switch threw under that probe and was left unjudged. The failure case now rejects `engine.find` itself, and the driver double is plain. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cyPqWLpc3AxaZrs98n
📓 Docs Drift CheckThis PR changes 4 package(s): 42 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 11 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 54 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 81faed8b9569db4470bcdff29fec07fb7006bcdc && git checkout 81faed8b9569db4470bcdff29fec07fb7006bcdc
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 1aeeafd9d371be1fd5afa637c4f0ae6b6f4c1404 3100b841e32027af5f3d910cba6c6b70c4ca1b19 && git checkout -B drift-repro 1aeeafd9d371be1fd5afa637c4f0ae6b6f4c1404 && git merge --no-ff 3100b841e32027af5f3d910cba6c6b70c4ca1b19
node scripts/docs-audit/affected-docs.mjs --json 1aeeafd9d371be1fd5afa637c4f0ae6b6f4c1404
|
|
domain:server#1 · os-elon-musk · session_016WV4cytLmJxfAmgPVjUbbW Contract reviewServed-tier: PR 1 of card #30884, the widening half. The Q3 refusal is PR 2 and is not judged here. Head ① Derived judgmentsMust-fix: none. Should-fix (does not block this PR):
Confirmed, by file and line: Gate 1, dryRun writes nothing.
Gate 2, the comparison against Expected 1.
Gate 3,
Gate 4, the environment half.
Gate 5, who may call.
Gate 7, tests and ablations, read not run.
Gate 8, check-runs on ② Semver level
③ Boundary flags
④ Design judgment
Implemented-by: VERDICT: PASS |
|
domain:server#1 · os-elon-musk · session_016WV4cytLmJxfAmgPVjUbbW 席位核对 · ACCEPT · 2026-11-06T15:32Z · #30884Head-sha: 3100b84
|
Requested by Elon · project thread
domain:server#1 · os-elon-musk · session_016WV4cyPqWLpc3AxaZrs98n
Refs #30884 (PR 1 of 2: widening only. PR 2 carries
Fixes #30884and the Q3 refusal.)Clause-②: yes (widening) — new exports from
@objectstack/metadata-core(comparePackageVersionsand its types,typeChangeCategory,isEmptyPackageUpgradePlan,upgradePlanSeedDatasets,seedModeOverwritesMatchedRows), a newplanUpgrademethod on objectql'smanifestservice, a newdryRunkey on thePOST /api/v1/marketplace/install-localbody, a new--dry-runflag onos package install, and newos diffentries (declared and suspected renames, seed datasets). Nothing a request did before answers differently.Metadata-protocol: none
Claim: 6686492425 (re-posted with this branch) · Branch:
claude/project-thread-89x8tb· Scope note: 6685166393维护者速读
renamedFrom声明的改名、没声明却像改名的「疑似改名」,以及种子数据集的增删改和各自的模式。入口有三个:安装接口带dryRun: true只返回预览、什么都不写;os package install --dry-run打印预览;os diff改用同一个比较。renamedFromis planned, an undeclared rename or type change is refused unless confirmed (epic cloud#3652 Q3) #30884,史诗 cloud#3652 Q3)。预览还读当前环境:每个受影响的列有多少行数据,以及 upsert 种子重放会覆盖多少行客户改过的数据。dryRun的旧运行时会直接安装,CLI 会认出来并报错退出,但那次安装已经发生。回滚即撤 PR。packages/metadata-core/src/package-upgrade-plan.ts(比较本身),packages/objectql/src/package-upgrade-environment.ts(行数和「被覆盖的客户编辑」怎么数),install-local 的 2a′ 步(干跑在哪一步回答、为什么在任何写入之前)。pnpm --filter @objectstack/objectql exec vitest run src/package-upgrade-environment.test.ts,预期 7 条全绿,其中「a seeded upsert dataset with one row edited reports that row」断言三行种子里只有客户改过的won被报出。What changes
@objectstack/metadata-core: the comparison (package-upgrade-plan.ts, new)comparePackageVersions(installed, incoming)takes two flat package bodies (objects,data) and returns:requiredandstored;typeChangeCategory's drift word (safewhen the installed field had no stored column,destructivewhen the new one has none,needs_confirmwhen both are stored — the column keeps its old type under the additive sync);renamedFromnames an installed key the new version no longer declares. The pair is listed as a rename, not as a removal and an addition. A declared rename whose type changes is also a type change, judged on its own (scope note 6685166393), carryingrenamedFrom;records,mode,columns,env), each with its mode (the spec defaultignorewhen none is declared), keyed by object plus a non-default match key and locales.It is pure and decides nothing. The drift vocabulary is restated as
UpgradeDriftCategory, because this package cannot depend on a driver; a type-level pin in the CLI's tests holds it equal to driver-sql'sDriftCategory. Also exported:upgradePlanSeedDatasets(so the environment half pairs datasets by the plan's own keys) andseedModeOverwritesMatchedRows(upsert,update).@objectstack/objectql: the environment half (package-upgrade-environment.ts, new;planUpgradeon themanifestservice inplugin.ts)planUpgrade(installed, incoming)folds each side with core'sresolveStackCollection, runs the comparison, and adds two read-only readings, with the system context, across every organization:$ne: null) in each stored column the upgrade removes, renames (declared or suspected) or retypes (not asaferetype), and the rows of each object it removes;upsertorupdatedataset both versions declare, the rows the replay matches byexternalId, and of those the rows a customer edited that the replay overwrites: some column the replay writes holds a value other than the installed seed value and other than the incoming one. Values are compared as the replay compares them (temporal storage form, blank-as-null form). Credential, file, reference, computed and expression columns,applyOn: 'create'columns and the match key are never compared. Up to 20 edited keys are named.A reading that fails is
nulland is logged, never0.@objectstack/cloud-connection: the dry run (marketplace-install-local-plugin.ts)dryRunmust be a boolean (400 INVALID_REQUESTotherwise). A dry run of anunmanagedinstall is refused400, since that branch writes and a copy follows no upgrade.400.dryRun: trueanswers200with{ dryRun, manifestId, version, installedVersion, plan, environment, message }. The installed side is the ledger entry, converted forward as the incoming one is. Amanifestservice with noplanUpgradeanswers503 SERVICE_UNAVAILABLE, never an empty plan. A ledger entry that does not parse is reported (installedRecordUnreadable).@objectstack/clios diffbuilds its object and field entries fromcomparePackageVersions. Every shape it printed before prints byte for byte as before (pinned before the move). New: arenamedFromrename prints as onemodifiedentry (renamed from X, declared by renamedFrom) instead of a removal plus an addition (renamedFromhas not shipped yet: changeset30880-field-renamed-fromis pending); a suspected rename prints amodifiedentry on the removed key beside its removal and addition; seed datasets print underdata, after every other category, with their mode.os package install --dry-runsendsdryRun: trueand prints the plan, each finding with the environment's reading beside it. A runtime that answers withoutdryRun(one that predates the key, and so installed) is printed as a failure, exit 1.Why
Card #30884 (epic objectstack-ai/cloud#3652, Phase 2 "change preview before install"; rulings Q3 and D3). This PR is the preview. The refusal ruled in Q3 is PR 2.
Re-measured on
616311c1, the card's "Today" items 1–6 still hold (diff.ts:162,:223-224;migrate/plan.ts:62,:137-139;schema-drift.ts:6-9,:135; install-local:85-97,:454,:3456, ledger:1319,install.ts:174,:307;upgrade-impact-report.ts:4-17;seed.zod.ts:193and the18.seed-mode-default-create-onlymigration). Item 7 no longer holds:renamedFromlanded with #30934 (field.zod.ts:2610,object.zod.ts'srefuseDishonestRenamedFrom).Tests
The card's Acceptance bullets this PR carries, each with its test. The other three (the 409 with its token, the declared-rename refusal, the ledger code) belong to PR 2.
renamedFrom→ one suspected renamemetadata-corepackage-upgrade-plan.test.ts› renames › "a field removed and one added of the same type in one object, no renamedFrom → one suspected rename"renamedFrom: ['old']→ one declared rename and no suspected renameupsertdataset change and anignoredataset change are both listed with their modesos diffoutput is unchanged for its existing fixturesclidiff-upgrade-plan.test.ts› "every shape, in order, with each entry's key order" (pinned onmainbefore the move, then held)dryRun: truewrites nothing (ledger file and registry unchanged)cloud-connectionmarketplace-install-local-upgrade-plan.test.ts› "a re-install: the installed ledger body against the incoming one; registry, schema and ledger untouched" (ledger bytes compared file by file;registerandsyncSchemasnever called)upsertdataset, one row edited, reports that rowobjectqlpackage-upgrade-environment.test.ts› "a seeded upsert dataset with one row edited reports that row" (real ObjectQL engine; of three seeded rows only the editedwonis named)package-upgrade-plan.test.ts› "a declared rename whose type changes is ALSO a type change, judged on its own";diff-upgrade-plan.test.ts› "a declared rename that changes type prints the type change too"New tests: 42 (
metadata-core17,objectql7,cloud-connection6,cli7 + 5).Ablations, on head
3100b841, each throughscripts/ablation-replace.mjs(anchor hit ×1, the mutation proven on disk by blob hash, restored to the HEAD blob withgit diff HEADempty):const storedAdded = added.filter(…)→const storedAdded: string[] = []target === key(a field that kept its key)if (dryRun) return this.answerUpgradeDryRun(…)) removedAcceptance notes
updateas well asupsert. An unchangedupsertdataset writes the same edits back, andupdateoverwrites a matched row likeupsert. Each entry says whether the plan lists it as changed.dryRunand installs. The CLI detects this from the answer and exits 1, but by then the install has happened. No guard exists before the send.seedValueEquals. That function is private to@objectstack/metadata-protocol; its temporal and blank rules come from the same@objectstack/corefunctions here. Sinking the comparison to one shared home would be a follow-up. carrier: none (noted, not filed).replacedataset (it deletes every row on replay); a dataset'slocaleorenvfilter (the count may include a dataset a locale filter would skip, which over-reports, never under-reports).content/docs/deployment/cli.mdxhas no row for--dry-runyet. That page is outside this claim's file surface; the seat can card it.planPackageUpgradeis not exported from@objectstack/objectql's entry. A host reaches it throughctx.getService('manifest').planUpgrade, the service method the card names.Verification
Head
3100b841, base616311c1. Every test run below went throughscripts/pm/os-verify-lock.shand endedVERDICT command-exit 0.pnpm --filter @objectstack/metadata-core exec vitest run --maxWorkers=2pnpm --filter @objectstack/objectql exec vitest run --project local --maxWorkers=23dba4119;3100b841changes onlypackage-upgrade-environment.test.ts, 7/7 on head)pnpm --filter @objectstack/cloud-connection exec vitest run --maxWorkers=2pnpm --filter @objectstack/cli exec vitest run --project unit --maxWorkers=2… --project integration test/package-install-local-… --project integration test/package-owned-command-readers.test.tsmetadata-core,cloud-connection,objectql(on head),clicli3 files / 28 errors,objectql39 / 233).clineedsNODE_OPTIONS=--max-old-space-size=8192; at the default heap it runs out of memory before any verdictpnpm lintGates.
node scripts/pm/dispatch-gates.mjs --commandsderives 66 families from this diff. All 66 ran with their exit codes recorded before any pipe, and--ranreconciles: "66 derived famil(ies) accounted for — 66 run, 0 NOT-MEASURED". Four needed a second run, each for a reason outside the diff, and each then exited 0:check:objectql-double-limitexited 1 on the first run: the probe drives everyfinddouble in the objectql tests, and my driver double's failure switch threw under it. Fixed in3100b841(the failure case now stubsengine.find); the gate then reports "none new".check-plugin-teardown-shape --self-testexited 3 on a shallow clone; fetching the commit its self-test names (621a4876) fixed it.check:dual-build-cjs-loadsandcheck:i18n-coverageexited 3 for want ofdist/; after a full build (pnpm turbo run build --filter=!@objectstack/docs, exit 0) both exited 0.check:nul-bytesexits 0. A self-scan for control bytes over the changed files finds none (the environment half spells its key separator as an escape).Today, re-measured on
616311c1: see Why. Items 1–6 hold; item 7 does not.Human Minimum
packages/specholds no runtime logic. (packages/specuntouched.)pnpm lintpasses.main, one pull request (PR 1 of the claim's two).packages/spectouched: n/a.content/docs/references/,packages/*/CHANGELOG.mdorcontent/docs/releases/.Generated by Claude Code