Repository navigation
fix(cloud-connection): the install-local listing answers withSampleData from the caller's own organization's rows - #21820
Conversation
… from the caller's own rows The listing answered each entry's withSampleData from the install-wide ledger record, so after a purge in organization A it told organization B 'no sample data' while B held all its seed rows. It now derives the flag per request: true when the purge's own identification (each dataset's externalId, read-only, scoped to the caller's active organization under a wall) finds at least one seed row. The identification pass is split out of the purge as matchSeedRows; the purge deletes what it returns, with its counts and log text unchanged. Claude-Session: https://claude.ai/code/session_01RWZbGvPFcRKvUqASZtunCU Co-authored-by: Claude <noreply@anthropic.com>
…Data and the read-only matcher Claude-Session: https://claude.ai/code/session_01RWZbGvPFcRKvUqASZtunCU Co-authored-by: Claude <noreply@anthropic.com>
…ation across a restart, and off-wall beside the ledger Claude-Session: https://claude.ai/code/session_01RWZbGvPFcRKvUqASZtunCU Co-authored-by: Claude <noreply@anthropic.com>
…ting withSampleData Claude-Session: https://claude.ai/code/session_01RWZbGvPFcRKvUqASZtunCU Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RWZbGvPFcRKvUqASZtunCU Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 15 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 3 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 3 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 b7fabc72fe76480d06956e11236277ad272d0f58 && git checkout b7fabc72fe76480d06956e11236277ad272d0f58
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 18c7dfd2e68cd2630420080b49a5f6a62fe60a6a c668f551be27c05c70026fc90f2cf733f2c507cd && git checkout -B drift-repro 18c7dfd2e68cd2630420080b49a5f6a62fe60a6a && git merge --no-ff c668f551be27c05c70026fc90f2cf733f2c507cd
node scripts/docs-audit/affected-docs.mjs --json 18c7dfd2e68cd2630420080b49a5f6a62fe60a6a
|
Fixes #21775
Clause-②: no
What was wrong
GET /api/v1/marketplace/install-localanswered each entry'swithSampleDatafrom the install ledger's record (withSampleData: e.withSampleData ?? false), which holds one value per install. Under an organization wall, sample data is per organization: the install, the reseed and the purge each act in the caller's active organization. So after a purge in organization A, the listing read as organization B answeredfalsewhile B held all 28 of its CRM seed rows, and a restart kept that answer.What changed
This follows the triage ruling (
5984286272): the listing deriveswithSampleDatafrom the caller's own organization's rows, matched by the seed key the purge already uses (externalId). The ledger gains no field, and the purge and the reseed behave as before.marketplace-install-local-purge.ts. The purge's identification pass is now a separate read-only export,matchSeedRows.purgeSeedRowsdeletes exactly what it returns, child before parent. Counts, order, write context and every log line are byte-for-byte what they were: the matcher reports problems as data (SeedMatchProblem), and the purge words them the same way as before. The existing purge pins pass unchanged.firstOnlystops at the first identified row.marketplace-install-local-plugin.ts.handleListanswerswithSampleDatafromsampleDataInScope, which runsmatchSeedRows(firstOnly) per seeded entry, under the purge's scope rule (organizationWallActive+resolveActiveOrgId) and over the loader's own dependency graph. The newseedDatasetsOfgives the purge and the listing one definition of what a seed dataset is.local-manifest-source.ts. The docblock of the two ledger fields now says they are install-time records, one per install and not per organization, and that the listing does not readwithSampleData. The docblock lives here rather than in the plugin file the claim named.Readings that decided the shape (all on
origin/main75ddcd1b41)handleListsits at:1348, and:1367iswithSampleData: e.withSampleData ?? false.externalIdunderREAD_CONTEXT = { isSystem: true }, with reference key parts translated through the matched parents. That loop moved intomatchSeedRowsunchanged, and the listing calls it. Nothing is restated.resolveActiveOrgIdanswers the session's active organization ornull. D2: "Tenant-scoped reads resolve to nothing ... zero rows, HTTP 200, no error." So on a walled session with no active organization, every entry answersfalsewith200, and no seed row is read. The ADR answers this case, so there is no open question.packages/app-shell/src/console/marketplace/MarketplacePackagePage.tsx(read at objectui9dfaca6). It picks the reseed item's label (reseedAgainwhen true,addSampleDatawhen false) and disables the purge item when the flag is false. "Any" is exactly when the purge has a row to delete. "All" would disable the purge while 27 of 28 rows remain. A count would change the response shape, and Clause-② isno. A seed record the purge cannot identify either (no key value, or a key that two rows carry) is not evidence of presence.Tests (head
c668f551)pnpm --filter @objectstack/cloud-connection typecheck(both tsconfigs): exit 0.pnpm --filter @objectstack/cloud-connection exec vitest run --maxWorkers=2: 39 files, 485 passed. That is 477 before, plus 8 new pins inmarketplace-install-local-purge.test.ts, which reuse that file's existing engine double, so no new double was pinned:firstOnlyreads one object when the first parent matches; with no seed row in scope it reads every seeded object; a failed read is reported asreaddata;true(reads pinned toorg_bonly) and read as A answersfalse(A keeps a user-authored row); walled with no active organization,false, 200, zero seed-object reads; off-wall, the derived answer equals the ledger's before and after a purge; unreadable rows answerfalsewith awarnnaming the package and the cause.marketplace-install-local-reseed.test.tsread the reseed's ledger write back through the listing, and now reads the ledger itself, since its seed loader is a stub that writes no rows. Inmarketplace-install-local-list-posture.test.ts, the fixture's ledger saystruebut its manifest bundles no seed dataset, so the expected value is nowfalse.pnpm --filter @objectstack/dogfood typecheck: exit 0.dist/-resolved;withSampleData.has(e.manifestId)counted once indist/index.jsand once indist/index.cjsbefore the run): 3 files, 20 passed.install-local-listing-sample-data.dogfood.test.tsis the ruling's pin. On a walled boot with A and B (28 seed rows each), after a purge in A the listing read as B answerstrue(28 rows) and read as A answersfalse(0 rows), while the ledger record saysfalse. A second boot over the samedatabaseFileand ledger keeps both answers.install-local-purge-sample-data.dogfood.test.tsgains the off-wall pin: listing beside the ledger record after install, purge and reseed,true/true,false/false,true/true.install-local-no-active-organization.dogfood.test.tspasses unchanged.Reverse verification (committed fix first,
node scripts/ablation-replace.mjs, wrap mode)withSampleData: withSampleData.has(e.manifestId),becamewithSampleData: e.withSampleData ?? false,, which is origin/main's read. The anchor went from 1 to 0, and the blob from50a71c2ftoeecd9640.ablation-dist-preflight.mjs @objectstack/cloud-connection 'e.withSampleData ?? false'exited 0, with the marker present indist/index.jsanddist/index.cjs.false, which is the card's measured defect, and the restart pin also gotfalse.50a71c2f, andgit diff HEADis empty. The rebuild exited 0, and--absentpreflight exited 0 with the marker gone from all 6 built files and the tree clean.Gates
dispatch-gates --commands, run without paths. Its 67 commands ran atd46b1a3d, and every one exited 0.check:dual-build-cjs-loadsfirst exited 3 (PREREQUISITE NOT MET: 8 packages had nodist/). It passed after those were built.dispatch-gates --ranreported: 67 derived, 67 run, 0 NOT-MEASURED, 0 UNRUN.c668f551, a type annotation in the new dogfood test), these ran again and exited 0:check:type-check-debt,check:type-check-coverage,check:test-source-alias,check:cross-package-test-inputs,check:engine-double-contract,check:nul-bytes,check:doc-authoringandcheck:issue-citations.pnpm lint(eslint . --no-inline-config, not narrowed) exited 0 atc668f551.Acceptance notes
withSampleData. The install, the heal, the reseed and the purge still write it, as the ruling keeps it as an install-time record.sampleDataPurgedkeeps its one reader, the heal, which heals nothing under a wall.findper seeded object for the key columns in scope, and stops at the first seed row found. With no seed row in scope, it reads every seeded object. That is the same read the purge makes, once per request.falsewith awarnonce per entry per request, the way this door already reports a corrupt ledger entry on every GET. The wire shape is unchanged.Carried measurement (from the #21762 review, not acted on here)
databaseFile. The CRM package was installed (28 rows), and then the ledger entry'smanifest.engines.protocolwas set to^16on a protocol-17 runtime. After a restart, the rehydrate loggedOS_PROTOCOL_INCOMPATIBLE ... is NOT loaded, andGET /api/v1/marketplace/install-localran once.total: 1, and the entry is listed with every field (packageId,versionId,manifestId,version,installedAt,installedByfor the operator) andwithSampleData: false. Each such GET logs onewarnnaming the five seeded objects asObject '...' not found.withSampleData: truefrom the ledger record. On main itself nowarnis logged, because the derivation does not exist there.Generated by Claude Code