Skip to content

fix(cloud-connection): the install-local listing answers withSampleData from the caller's own organization's rows - #21820

Merged
objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-21775-listing-per-org-sample-data
Oct 5, 2026
Merged

objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-21775-listing-per-org-sample-data

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

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

claude added 5 commits October 5, 2026 04:14
… 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>
@github-actions github-actions Bot added the size/l label Oct 5, 2026
@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Oct 5, 2026
@github-actions

github-actions Bot commented Oct 5, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/cloud-connection, touching 18 documentable anchor(s).

15 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/automation/flows.mdx (via organizationId (symbol, a field of interface SeedRowMatchInput))
  • content/docs/automation/hooks.mdx (via organizationId (symbol, a field of interface SeedRowMatchInput))
  • content/docs/data-modeling/field-types.mdx (via externalId (literal, a string literal in SeedMatchProblem; a string literal in describePurgeProblem; a string literal in matchSeedRows))
  • content/docs/data-modeling/fields.mdx (via externalId (literal, a string literal in SeedMatchProblem; a string literal in describePurgeProblem; a string literal in matchSeedRows))
  • content/docs/data-modeling/seed-data.mdx (via organizationId (symbol, a field of interface SeedRowMatchInput), externalId (literal, a string literal in SeedMatchProblem; a string literal in describePurgeProblem; a string literal in matchSeedRows))
  • content/docs/data-modeling/validation-rules.mdx (via externalId (literal, a string literal in SeedMatchProblem; a string literal in describePurgeProblem; a string literal in matchSeedRows))
  • content/docs/deployment/cli.mdx (via MarketplaceInstallLocalPlugin (symbol, a top-level class))
  • content/docs/deployment/seed-tenancy-repair.mdx (via organizationId (symbol, a field of interface SeedRowMatchInput))
  • content/docs/getting-started/examples.mdx (via externalId (literal, a string literal in SeedMatchProblem; a string literal in describePurgeProblem; a string literal in matchSeedRows))
  • content/docs/kernel/events.mdx (via organizationId (symbol, a field of interface SeedRowMatchInput))
  • content/docs/kernel/runtime-services/audit-service.mdx (via organizationId (symbol, a field of interface SeedRowMatchInput))
  • content/docs/kernel/runtime-services/sharing-service.mdx (via organizationId (symbol, a field of interface SeedRowMatchInput))
  • content/docs/permissions/authentication.mdx (via organizationId (symbol, a field of interface SeedRowMatchInput))
  • content/docs/permissions/system-context.mdx (via organizationId (symbol, a field of interface SeedRowMatchInput))
  • content/docs/protocol/kernel/config-resolution.mdx (via organizationId (symbol, a field of interface SeedRowMatchInput))

⛔ 3 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/index.mdx (via organizationId (symbol, a field of interface SeedRowMatchInput))
  • content/docs/releases/v16.mdx (via organizationId (symbol, a field of interface SeedRowMatchInput))
  • content/docs/releases/v17/17-5.mdx (via organizationId (symbol, a field of interface SeedRowMatchInput))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 cross-cutting symbol(s) contributed no route anchor: organizationId (5 routes)
  • 8 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 3 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 18c7dfd2e68cd2630420080b49a5f6a62fe60a6a → packageMentionDocs.

Which tree this was computed on

This run read content/docs from b7fabc72fe76480d06956e11236277ad272d0f58 — the merge of head c668f551be27c05c70026fc90f2cf733f2c507cd into base 18c7dfd2e68cd2630420080b49a5f6a62fe60a6a, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 18c7dfd2e68cd2630420080b49a5f6a62fe60a6a → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 5, 2026 05:33
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 5, 2026 05:33
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 5, 2026
Merged via the queue into main with commit c4d5713 Oct 5, 2026
37 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-21775-listing-per-org-sample-data branch October 5, 2026 06:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

2 participants