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
Commit c4d5713
Browse filesBrowse the repository at this point in the historyBrowse files
fix(cloud-connection): the install-local listing answers withSampleData from the caller's own organization's rows (#21820)
Fixes#21775
Clause-②: no
## What was wrong
`GET /api/v1/marketplace/install-local` answered each entry's
`withSampleData` from 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
answered `false` while 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 derives
`withSampleData` from 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`.
`purgeSeedRows` deletes 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. `firstOnly` stops at the first identified row.
- **`marketplace-install-local-plugin.ts`.** `handleList` answers
`withSampleData` from `sampleDataInScope`, which runs `matchSeedRows`
(`firstOnly`) per seeded entry, under the purge's scope rule
(`organizationWallActive` + `resolveActiveOrgId`) and over the loader's
own dependency graph. The new `seedDatasetsOf` gives 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 read `withSampleData`. The
docblock lives here rather than in the plugin file the claim named.
## Readings that decided the shape (all on `origin/main` `75ddcd1b41`)
- **H1 holds.** `handleList` sits at `:1348`, and `:1367` is
`withSampleData: e.withSampleData ?? false`.
- **H2 holds, and the matcher is reused.** The purge matched seed rows
by each dataset's `externalId` under `READ_CONTEXT = { isSystem: true
}`, with reference key parts translated through the matched parents.
That loop moved into `matchSeedRows` unchanged, and the listing calls
it. Nothing is restated.
- **H3: ADR-0123 D2 governs, and it is pinned.** `resolveActiveOrgId`
answers the session's active organization or `null`. D2: "Tenant-scoped
reads resolve to nothing ... zero rows, HTTP 200, no error." So on a
walled session with no active organization, every entry answers `false`
with `200`, and no seed row is read. The ADR answers this case, so there
is no open question.
- **H4: "present" means at least one seed row in scope.** The console
reads the flag in one place: objectui
`packages/app-shell/src/console/marketplace/MarketplacePackagePage.tsx`
(read at objectui `9dfaca6`). It picks the reseed item's label
(`reseedAgain` when true, `addSampleData` when 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-② is `no`. 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 in `marketplace-install-local-purge.test.ts`, which reuse that
file's existing engine double, so no new double was pinned:
- matcher: identifies exactly what the purge deletes and deletes
nothing; `firstOnly` reads one object when the first parent matches;
with no seed row in scope it reads every seeded object; a failed read is
reported as `read` data;
- listing: walled, after a purge in A, read as B answers `true` (reads
pinned to `org_b` only) and read as A answers `false` (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 answer `false` with a `warn`
naming the package and the cause.
- Fixture triage, two cases. `marketplace-install-local-reseed.test.ts`
read 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.
In `marketplace-install-local-list-posture.test.ts`, the fixture's
ledger says `true` but its manifest bundles no seed dataset, so the
expected value is now `false`.
- `pnpm --filter @objectstack/dogfood typecheck`: exit 0.
- Dogfood, on real boots (`dist/`-resolved;
`withSampleData.has(e.manifestId)` counted once in `dist/index.js` and
once in `dist/index.cjs` before the run): **3 files, 20 passed**.
- The new `install-local-listing-sample-data.dogfood.test.ts` is 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 answers `true` (28 rows) and read as A
answers `false` (0 rows), while the ledger record says `false`. A second
boot over the same `databaseFile` and ledger keeps both answers.
- `install-local-purge-sample-data.dogfood.test.ts` gains 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.ts` passes
unchanged.
## Reverse verification (committed fix first, `node
scripts/ablation-replace.mjs`, wrap mode)
- **Mutation.** `withSampleData: withSampleData.has(e.manifestId),`
became `withSampleData: e.withSampleData ?? false,`, which is
origin/main's read. The anchor went from 1 to 0, and the blob from
`50a71c2f` to `eecd9640`.
- **Build.** JS was emitted. DTS failed on TS6133 (the now-unused set),
which does not affect the JS the suites load.
`ablation-dist-preflight.mjs @objectstack/cloud-connection
'e.withSampleData ?? false'` exited 0, with the marker present in
`dist/index.js` and `dist/index.cjs`.
- **Expected direction: red. Observed: red.**
- Unit: 4 failed, 25 passed. The four are the walled A/B pin, the
no-active-organization pin, the unreadable-rows pin and the list-posture
member pin. The off-wall pin stays green by design.
- Dogfood: 2 failed, 11 passed. "after the purge in A, the listing read
as B answers withSampleData: true" got `false`, which is the card's
measured defect, and the restart pin also got `false`.
- **Restore.** The blob equals the HEAD blob `50a71c2f`, and `git diff
HEAD` is empty. The rebuild exited 0, and `--absent` preflight exited 0
with the marker gone from all 6 built files and the tree clean.
## Gates
- **The derived set** came from `dispatch-gates --commands`, run without
paths. Its 67 commands ran at `d46b1a3d`, and every one exited 0.
- `check:dual-build-cjs-loads` first exited 3 (PREREQUISITE NOT MET: 8
packages had no `dist/`). It passed after those were built.
- `dispatch-gates --ran` reported: 67 derived, 67 run, 0 NOT-MEASURED, 0
UNRUN.
- **After the last commit** (`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-authoring` and `check:issue-citations`.
- **Full `pnpm lint`** (`eslint . --no-inline-config`, not narrowed)
exited 0 at `c668f551`.
## Acceptance notes
- After this PR, nothing in the runtime reads the ledger's
`withSampleData`. The install, the heal, the reseed and the purge still
write it, as the ruling keeps it as an install-time record.
`sampleDataPurged` keeps its one reader, the heal, which heals nothing
under a wall.
- **Cost.** Each listing request now builds one dependency graph per
seeded package. It also runs one `find` per 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.
- **Off-wall, the rows win over the record** where the two disagree. A
purge the engine refused for some rows, rows deleted by hand, or a heal
that landed nothing now answers from the rows. In the flows that keep
them in step (install, purge, reseed), both answers are equal, as
pinned.
- A rows read that fails answers `false` with a `warn` once 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)
- **Setup.** An unwalled real boot with `databaseFile`. The CRM package
was installed (28 rows), and then the ledger entry's
`manifest.engines.protocol` was set to `^16` on a protocol-17 runtime.
After a restart, the rehydrate logged `OS_PROTOCOL_INCOMPATIBLE ... is
NOT loaded`, and `GET /api/v1/marketplace/install-local` ran once.
- **This PR.** 200, `total: 1`, and the entry is listed with every field
(`packageId`, `versionId`, `manifestId`, `version`, `installedAt`,
`installedBy` for the operator) and `withSampleData: false`. Each such
GET logs one `warn` naming the five seeded objects as `Object '...' not
found`.
- **origin/main's listing read**, measured through the same mutation as
above: the same entry and fields, with `withSampleData: true` from the
ledger record. On main itself no `warn` is logged, because the
derivation does not exist there.
- In both cases nothing on the entry says the runtime did not load it.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01RWZbGvPFcRKvUqASZtunCU)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
`GET /api/v1/marketplace/install-local` now answers each entry's `withSampleData` for the caller's own organization. Before, after a purge in organization A, the listing read as organization B answered `withSampleData: false` while B still held every one of its seed rows.
6
+
7
+
Clause-②: no
8
+
9
+
-**What was wrong.** The listing served the install ledger's `withSampleData`, 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. A purge in A flipped the one value for every organization, and a restart kept it.
10
+
-**What it does now.** The listing reads the rows. An entry answers `true` when at least one of the package's seed rows is in the caller's scope. Rows are matched the way the purge matches them, by each dataset's `externalId`. "At least one" is exactly when the purge has something to delete, and it decides whether the console labels its reseed action "Add sample data" or "Reseed again". The purge's matching is now a separate read-only step, and the purge deletes what it returns, with the same counts and log lines as before.
11
+
-**Scope.** Under a wall, the scope is the caller's active organization. A session with no active organization reads nothing, so every entry answers `false` with `200`, and no row is read. Without a wall, the match covers the whole table, and the answer is the one the ledger records after an install, a purge and a reseed.
12
+
-**When the rows cannot be read** (for example, a package the runtime did not load), the entry answers `false`, and the server log says why at `warn`, once per entry per request.
13
+
-**The response keeps its shape.** The ledger keeps its shape too. Its `withSampleData` and `sampleDataPurged` stay as install-time records, and their docs now say they are not per organization.
0 commit comments