Repository navigation
feat(cloud-connection,metadata-protocol): an unmanaged copy can take the package's sample data, owned by the installer - #31398
objectstack-fleet[bot] wants to merge 9 commits into
Conversation
…le data An unmanaged install asked for its sample data (seedSampleData) has the import write each seed dataset into the new base as a seed item, the copy's seed receipt in the environment ledger. The install door loads the rows from those stored bodies, owned by the installer, and the purge door removes exactly them for the base's id. Claude-Session: https://claude.ai/code/session_01FEkQ5pQJCx8STEq28nr3Ej Co-Authored-By: Claude <noreply@anthropic.com>
… at the door and on publish Claude-Session: https://claude.ai/code/session_01FEkQ5pQJCx8STEq28nr3Ej Co-Authored-By: Claude <noreply@anthropic.com>
…, pinned on a real boot The list read serves no stored seed item (a seed body has no name), so the receipt is read item by item through the by-name read: the names come from the import's answer on install, and from the base's active seed rows on purge. Claude-Session: https://claude.ai/code/session_01FEkQ5pQJCx8STEq28nr3Ej Co-Authored-By: Claude <noreply@anthropic.com>
…the engine dispatch asserts Claude-Session: https://claude.ai/code/session_01FEkQ5pQJCx8STEq28nr3Ej Co-Authored-By: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 2 package(s): 38 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 15 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 15 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 46dfdc2874fb6da0241f829b502b0137edb7eb43 && git checkout 46dfdc2874fb6da0241f829b502b0137edb7eb43
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 216b18a6ccb47dc23d3fe9f6a8575daf54f25dd5 86246c5b01e81a07ff8d0acd5dddc082ae27b932 && git checkout -B drift-repro 216b18a6ccb47dc23d3fe9f6a8575daf54f25dd5 && git merge --no-ff 86246c5b01e81a07ff8d0acd5dddc082ae27b932
node scripts/docs-audit/affected-docs.mjs --json 216b18a6ccb47dc23d3fe9f6a8575daf54f25dd5
|
|
domain:server#3 · os-project-manager · session_01FEkQ5pXkgNXVpCn2GZDjyy Contract reviewServed-tier: Inputs read: card #30808 (body, triage, unblock, claim, claim amendment, os-dev-report), PR #31398 body and file list (10 files, +1076/−36), the net diff against ① Derived judgmentsGate verdicts on this head (check-runs). Green: Check Changeset, Governed Surface Queue Guard, PR-body guards, Spec property liveness, Lint · PM dispatch-gates, Lint · ESLint, Lint · docs & code guards, Lint · scoped · query-options erasure, Lint · scoped · tooling self-tests, Type Check · debt ledger, Type Check · source gates, Check PR Size, Flag docs, Test UI, Plan Test Core shards, filter. NOT MEASURED (in progress at review time): Test Core 1/8 to 8/8, Build Core, Type Check · workspace, Type Check · consumer gates, Lint · CI, release & engine gates (the family that carries (a) Is the widening limited to the door switch and the verb's in-process type, with no
(b) Can storing the copy's seed items in the environment ledger make any boot, save or publish re-insert rows after a purge?
(c) Are the seed rows owned by the installer, as ruled (#30324 C / #30680 A)? Yes. Card asks.
Correct by reading.
(e) ② Semver level(d) The changeset is Clause-②: Text vs diff. The changeset matches the diff except in three places:
③ Boundary flags
Must-fix: none. Should-fix:
Nits:
Implemented-by: VERDICT: PASS |
…oor made A door-private record of each copy (beside the install ledger, removed by an uninstall cleanup) is now what lets the purge reach a base without a ledger entry; every other writable base keeps the 404. A copy made without sample data says so. Pins for a save after a purge, a publish after a purge, and a dataset that carries files. Claude-Session: https://claude.ai/code/session_01FEkQ5pQJCx8STEq28nr3Ej Co-Authored-By: Claude <noreply@anthropic.com>
…ish-after-purge owner rule Claude-Session: https://claude.ai/code/session_01FEkQ5pQJCx8STEq28nr3Ej Co-Authored-By: Claude <noreply@anthropic.com>
… cleanup's outcome Claude-Session: https://claude.ai/code/session_01FEkQ5pQJCx8STEq28nr3Ej Co-Authored-By: Claude <noreply@anthropic.com>
|
domain:server#3 · os-project-manager · session_01FEkQ5pXkgNXVpCn2GZDjyy Contract reviewServed-tier: Round 2, a delta review against the round-1 record (comment 6718084382, head ① Derived judgmentsGate verdicts on this head (check-runs). Green: Check Changeset, Check PR Size, Governed Surface Queue Guard, PR-body guards, Spec property liveness, Lint · PM dispatch-gates, Lint · docs & code guards, Lint · scoped · tooling self-tests, Flag docs, Test UI, Plan Test Core shards, filter, Auto Label. NOT MEASURED (in progress at review time): Build Core, Test Core 1/8 to 8/8, Lint · CI, release & engine gates (the family that carries The merge. S1 — the purge door's reach. Closed in code. The declaration is closed in the changeset and still open in the PR body.
S2 — the save pin. Closed. The new case saves the receipt item with S3 — publish after purge. Closed in the changeset and pinned. The PR body is open (S8). The changeset now says it: "Published after a purge, it loads its rows again under the publish rule, not the installer's: the publish names no person, so those rows carry no S4 — the persona consequence. Closed in the changeset. The PR body is open (S8). The changeset now carries the card's required sentence: rows under different personas collapse to one owner, "so per-role answers in the customer's environment differ from the gallery's per-persona transcripts." That is the consequence the card's governing-rulings paragraph says to state. S5 — N1 — the message for a copy without sample data. Closed. A recorded copy made without New on this head: the cleanup outcome on every uninstall answer. The door registers
No published response shape changes.
It is not a ② Semver levelThe changeset is still
Clause-②: Text vs diff. The changeset matches the diff except in two places:
③ Boundary flags
Must-fix: none. Should-fix:
Nits:
Implemented-by: VERDICT: PASS |
Requested by Leehom · project thread
Fixes #30808
Clause-②: yes (widening) — the install-local door accepts an optional
seedSampleDatabesideinstallMode: 'unmanaged', and theimportUnmanagedPackageverb takes a door-statedseedData; the publishedImportUnmanagedPackageRequestSchemais unchangedMetadata-protocol: none
维护者速读
seedSampleData,默认关闭;关闭时行为与之前完全一致。seed元数据项写入新的 base(即“种子回执”,存于环境元数据中),再由安装门以安装者身份一次性加载,owner_id与created_by均为安装者;原包里不同角色拥有的行会合并为同一个拥有者。externalId精确删除复制的行,用户自建的行保留;回执读不到时返回 503,不删任何行。targetNamespace只改object名,记录与externalId原样保留。externalId匹配、不重复加载。files),框架本身没有记录配额;卸载前需先清除,否则行会留下。What changes
The switch (
@objectstack/cloud-connection,POST /marketplace/install-local).seedSampleDatais accepted besideinstallMode: 'unmanaged'and is off by default. Without it, an unmanaged copy loads no row and answers the sameseeded: { mode: 'skipped', … }as before. Withtrue, the door loads the copy's datasets once, inline, and answersseeded: { mode: 'inline', inserted, updated, skipped, errors }. A value that is not a boolean, or one sent with a managed install (which always loads its data), answers 400INVALID_REQUESTbefore anything is written.Ownership. Every copied row is owned by the person who installed it: the door loads with the installer as the load's user and stamps
owner_idandcreated_bywith that id (the #30324 option C / #30680 answer A rule). Rows the package wrote as different personas collapse to that one owner; the copy keeps no record of the persona a row had.The seed receipt (
@objectstack/metadata-protocol,importUnmanagedPackage). Asked with the door-statedseedData: true(read off before the strict request parse, likeactor), the verb writes eachdatadataset as aseeditem of the new base, in the environment's ownsys_metadata, after every other copied item:<object>_seed, then<object>_seed_2for a second dataset of the same object. This receipt is what the door loads from and what the purge deletes by. UndertargetNamespaceonly the dataset'sobjectfollows the #30794 rename map; records andexternalIdare copied verbatim, because the rename's free-text rewrite would otherwise reach record values (a probe showed a record value'crm_account'rewritten to'mycrm_account'), and a changed key would break both lookups and the purge. A dataset that cannot be written (noobject, norecordsarray, a name past the storage limit) is refused with 400 before any write; aseeditem the save door refuses rolls the whole copy back, as every copied item does;seednot runtime-creatable on the target is a finding on the keydata. A re-run is still refused with 409.Two receipt designs were weighed. A marker column on every seeded row would need a schema change on every object the package seeds, and a row the person later edits would still carry it. The receipt in the environment ledger needs no column, survives restarts with the base, and lets the purge run the same
externalIdmatch the managed purge already uses. The receipt was chosen.Reading the receipt. Each receipt item is read through the protocol's by-name read (
getMetaItem), scoped to the base. The list read cannot serve it: aseedbody declares noname(SeedSchemais closed), and the list keys every stored item by its body'sname, sogetMetaItems({ type: 'seed' })serves no stored seed item at all (measured on the real showcase boot: 5 active rows insys_metadata, 0 listed, each one served by name). On install the names come from the import's own answer (copied); on purge they come from the base's activeseedrows insys_metadata, read as the system for their names only. An item that is not served, or is not a dataset, fails the whole read.The purge (
POST /marketplace/install-local/:id/purge-sample-data). When the install ledger has no entry for the id, the purge now checks for an installed, writable base under that id, reads itsseeditems, and deletes exactly the rows they name byexternalId; a row the person wrote in the same object survives. A receipt that cannot be read answers 503SERVICE_UNAVAILABLEand deletes nothing: an unread receipt is never taken for "no sample data". An unknown id still answers 404. The managed path is unchanged (the shared body moved intopurgeSeedDatasets).No double load. A boot never applies the receipt: a restart after the copy, or after the purge, inserts no row. A copied
seeditem saved later writes metadata only, and drafted and published it behaves as every environmentseeditem does: create-only, matched byexternalId, so the rows already loaded are matched and none is inserted twice.Tests
packages/metadata-protocol/src/package-unmanaged-install.test.ts, describe "the sample data, copied when the door asks — the seed receipt (An unmanaged install can copy the package's seed data, removable as one batch, owned by the installer (gallery demos need data) #30808)", 11 cases: off by default; planned after every other item, named<object>_seed, records verbatim; undertargetNamespaceonlyobjectrenamed, records andexternalIdverbatim;_seed_2naming; a dataset missingrecordsorobjectanswers 400 with no writes;seednot runtime-creatable is a finding ondata; the verb writes the seed rows into the new base with noorganization_idand the response parses againstImportUnmanagedPackageResponseSchema; a refusedseeditem rolls back the whole copy withfailedItem: { type: 'seed', name: 'crm_lead_seed' }; a re-run answers 409 with no writes; an undeclared key (withSampleData) answers 400; a copiedseeditem re-saved, then drafted and published, inserts no row (matched: ['a1', 'a2'],inserted: 0).packages/cloud-connection/src/marketplace-install-local-install-modes.test.ts, describe "install-local — an unmanaged copy's sample data, asked for withseedSampleData(An unmanaged install can copy the package's seed data, removable as one batch, owned by the installer (gallery demos need data) #30808)", 7 cases, on the realSeedLoaderService: off by default; asked for, 3 rows inserted,owner_idandcreated_bythe installer, a contact'saccountthe id of the account it names, no ledger entry and no registry package; the purge deletes exactly 3 and keeps a user row, a second purge skips 3; a restart after the copy and after the purge inserts nothing; a re-run answers 409 with no new writes; a non-boolean value, or a managed install, answers 400; an unknown id answers 404 and an unreadable receipt answers 503 with no deletes.packages/qa/dogfood/test/install-local-unmanaged-seed-copy.dogfood.test.ts(new), on the real showcase with one database file across three boots: the CRM example's copy answers 200 and loads its 28 rows inline; every row is owned by the signed-in installer; an activity'saccountresolves to the copied account it names; a restart inserts no row; the purge deletes exactly the 28 and keeps a row the person wrote; a restart after the purge inserts none back.Acceptance notes
SeedSchemahas afilesmap (packages/spec/src/data/seed-file.zod.ts), and the loader mints asys_fileper row from it. The copy keeps the dataset'sfilesin the receipt and loads it as a managed install does, so a copied seed with files gets its files.LifecycleService's in objectql, which a seed load does not read. Whether a cloud deployment enforces one on top is not verified here.withSampleDatacount and thereseed-sample-datadoor read the install ledger, which an unmanaged copy never writes, so neither covers a copy yet.refusedwithout one.seedlist.GET /meta/seed(andgetMetaItems({ type: 'seed' })) lists no storedseeditem, whoever wrote it, because the list keys items by a bodynamethatSeedSchemadoes not have. This PR works around it (reads by name) and does not change the list.data: 'door'inpackages/spec/src/kernel/unmanaged-install.ts("a copy of the metadata cannot carry them") is now stale. It is spec text, so it is left for the spec seat.Verification
All on head
da697f3f(origin/main merged once, at5620dd94), unless a line says otherwise.e2e6ec7dand the new tests kept:package-unmanaged-install.test.ts -t 308089 failed, 2 passed (the two that pass guard behaviour main already has: off by default, and an undeclared key answers 400), for exampleAssertionError: expected [] to deeply equal [ 'crm_account_seed', 'crm_lead_seed' ]andError: Invalid unmanaged install request: 1 issue — <root> [unrecognized_keys];marketplace-install-local-install-modes.test.ts -t 308085 failed, 2 passed, for exampleexpected 404 to be 503; the dogfood file 5 failed of 5 (dists rebuilt from base),{"mode":"skipped","reason":"an unmanaged install copies metadata, not data: the package's sample data was not loaded"}andNo marketplace install for com.example.crm-copy.Then green on head.scripts/ablation-replace.mjs, each mutation landed and the tree restored to HEAD after it):recordsarray check inunmanagedSeedItems→if (false): red,expected 'The package\'s sample data cannot be …' to contain '`data[0]` (object \'crm_lead\') has n…';seedruntime-creatable finding →&& false: red,expected undefined to deeply equal [ ObjectContaining {"key": "data"} ];if (rename === undefined): red, the copy rolled back withseed/acme_account_seed failed spec validation: <root>: Unrecognized key(s) on this seed: `name`(the rewrite reaches the body); the plan-level rename pin alone stays green under this mutation, because the skip lives in the verb, so the verb pin is the one that holds it;seedSampleData400 →if (false): red,expected 200 to be 400;seedSampleData400 →if (false): red,expected 200 to be 400;return { datasets: receipt.datasets ?? [] }: red,expected 400 to be 503;userId: null: red,expected { owner_id: undefined, …(1) } to deeply equal { owner_id: 'usr_installer', …(1) }.pnpm --filter @objectstack/metadata-protocol --filter @objectstack/cloud-connection testexit 0 on5620dd94: metadata-protocol 326 files, 22570 passed, 26 skipped; cloud-connection 73 files, 976 passed. The cloud-connection door file was re-run onda697f3fafter the harness fix: 41 passed.pnpm --filter @objectstack/cloud-connection --filter @objectstack/metadata-protocol --filter @objectstack/dogfood run typecheckexit 0.install-local-unmanaged-seed-copy,install-local-unmanaged-modeandinstall-local-purge-sample-datatogether: 3 files, 34 passed, after a@objectstack/dogfood^...closure build on the merged tree.pnpm lint packages/cloud-connection packages/metadata-protocol packages/qa/dogfoodexit 0.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, each command run with its exit captured before any pipe;--ran:83 derived famil(ies) accounted for — 83 run, 0 NOT-MEASURED. The first pass foundcheck:engine-double-contractred (the new door harness'supdate/deletedid not route throughassertEngineUpdateDispatch/assertEngineDeleteDispatch); fixed inda697f3fwith the ledger written by--write, then exit 0.check-plugin-teardown-shape --self-testneeded its pinned fixture commit fetched on the shallow clone, then exit 0.check:dual-build-cjs-loadsfirst exited 3 (a package withoutdist/while another build was running), then exit 0 on rerun. The seven gates the ledger file added were run onda697f3f, all exit 0.Generated by Claude Code