Skip to content

fix(service-datasource,metadata-core)!: the datasource-admin plugin reads environment rows only; a legacy organization-scoped datasource row is neither restored nor served (ADR-0131 D6) - #22862

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-22835-datasource-restore-environment-rows
Oct 11, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-22835-datasource-restore-environment-rows

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #22835
Clause-②: no (narrowing)

Summary

DatasourceAdminServicePlugin now reads the environment's stored datasource rows only. This is the ADR-0131 D6 read narrowing that #15206's C5 applied to every metadata read, carried to @objectstack/service-datasource, which S5's enumeration missed. A row stored in a legacy organization's layer is no longer restored at boot, served by the datasource-admin door or the metadata read door, pooled, updated or deleted by this plugin. The boot report [metadata_org_scoped_unserved] keeps naming it, and it stays at rest: neither deleted nor rewritten (#22011 A).

This makes the pending v18 note .changeset/15206-reads-environment-only.md ("served by no read and loaded by no boot") true for datasource. That note is not edited. #21922's "code wins on collision" restore design is unchanged.

Classes, positions and function names only (security card).

What changed

  • One predicate, moved, not copied. isLegacyOrganizationRow was module-private in packages/objectql/src/plugin.ts. It moves verbatim to packages/metadata-core/src/legacy-organization-row.ts and is exported from @objectstack/metadata-core, which both objectql and service-datasource already depend on (no new edge). metadata-core had no equivalent (searched for organization-row predicates). objectql's three binder call sites now import it, unchanged, and the private copy is deleted.
  • Every read of a stored datasource row in the plugin goes through one helper. The new private findEnvironmentRows (datasource-admin-plugin.ts) runs a find on sys_metadata (type datasource, state active, optionally one name, system context) and drops each row isLegacyOrganizationRow names. Census of the plugin's reads of stored rows, each now behind it:
    • loadDatasourceRows: the boot restore (restoreRuntimeDatasources);
    • loadDatasourceRow: the by-name read behind cluster convergence (convergePool) and the /meta datasource projection (applyMetadataDoorWrite);
    • persistDatasourceRow: the existence probe behind admin create and update;
    • deleteDatasourceRow: the existence probe behind admin remove.
  • By-name reads moved from findOne to a filtered find. A single row picked by the store could be the legacy one while the environment's row exists. findOne leaves the plugin's private engine slice type (DataEngineLike), where it is now unused.
  • The plugin's other reads (listDatasourceRecords, getDatasourceRecord, countBoundObjects*) read the MetadataService, not sys_metadata. They stop serving the legacy row because the restore no longer registers it. The measurement below shows this; hypothesis 3 holds.

Measurement: real boot, before and after

Showcase via bootShowcase over a database file. Two datasource rows were stored with system context between two boots: one legacy organization-scoped, one environment. Readings come from the second boot.

Reading Before (d489318714) After (fccd98aea0)
datasource-admin door: by name, list 200, listed 404, absent
metadata read door: by name, list 200 (the legacy body), listed 404, absent
live pool from the legacy row opened none
environment row: both doors, pool 200, 200, pooled 200, 200, pooled (unchanged)
boot report [metadata_org_scoped_unserved] names the legacy row names the legacy row
admin create under the legacy row's name 400 "already exists" 201; writes an environment row, legacy row untouched
admin remove under that name 204, and the legacy row was deleted 204; removes the environment row only, legacy row kept

The last row is the same missing predicate on a write path's existence probe (deleteDatasourceRow). The same fix closes it.

Pins

  • packages/qa/dogfood/test/datasource-restore-environment-rows.dogfood.test.ts (new): a real boot, a restart and both doors. Three cases:
    • Premise and control: both rows are at rest, and the boot report names the legacy row.
    • Neither door serves the legacy row (404 by name, absent from both lists) and no pool opens from it. The environment row is served and pooled.
    • The writes' by-name reads never reach the legacy row: an admin create and remove under its name write and remove an environment row of their own, and a /meta save under the name is what the admin door then serves. The legacy row equals its stored bytes at the end.
  • packages/services/service-datasource/src/__tests__/datasource-admin-plugin.test.ts, describe [#22835] (two cases, on the file's existing sys_metadata double):
    • The restore registers and pools environment rows only. A row whose organization_id is the empty string counts as the environment's, per the one predicate.
    • With a legacy row inserted first under the same name as an environment row, the /meta projection, admin update, remove and create all reach the environment row, and a legacy-only name is neither registered nor pooled. The legacy row stays byte-equal.
  • datasource-system-context.pin.test.ts: population count moved from findOne to find (5 reads); the system-context assertion is unchanged.
  • packages/metadata-core/src/legacy-organization-row.test.ts (new): the predicate's truth table.

Ablation (direction observed: red, as expected)

  • Committed state fccd98aea0, mutated with scripts/ablation-replace.mjs. In findEnvironmentRows, the anchor .filter((row) => !isLegacyOrganizationRow(row)); was replaced by a filter that keeps every row, which reproduces the pre-fix reads.
  • The anchor count went 1 to 0 and the marker 0 to 1. Blob ed525af25cfd became f7140c73d786.
  • pnpm --filter @objectstack/service-datasource build, then ablation-dist-preflight.mjs @objectstack/service-datasource ABLATION-22835 found the marker present in dist/index.js and dist/index.cjs.
  • Unit pins (-t 22835): 2 failed. Readings: the legacy row was registered at restore, and the projection served the legacy body.
  • Dogfood pin: 2 failed, 1 passed (the premise control). Readings: the admin door answered 200 for the legacy row, and the admin create answered 400 "already exists".
  • Restore: ablation-replace --restore then git checkout HEAD -- PATH. The blob equals HEAD (ed525af25cfd), git diff HEAD is empty and the tree is clean. After a rebuild, the preflight with --absent confirms the marker is absent from all 24 built files.

Tests and gates

Head b02b3f8c16 is a merge of origin/main eed637c09f (four commits, none touching these packages), plus a comment-only trim of the dogfood header.

  • @objectstack/service-datasource: vitest run, 42 files and 765 tests passed. typecheck exit 0, and tsc --listFiles includes both edited test files.
  • @objectstack/metadata-core: vitest run, 17 files and 386 tests passed. typecheck exit 0, covering the new test through both tsconfigs.
  • @objectstack/objectql: the 16 src/plugin*.test.ts files plus hook-binder-package-scope.test.ts, 17 files and 158 tests passed. typecheck (including check:test-typecheck) exit 0.
  • @objectstack/dogfood, isolated project: this pin plus the four sibling datasource files (datasource-restore-code-wins, datasource-meta-door-reaches-admin-door, meta-door-code-datasource, external-import-code-datasource-namespace), 5 files and 28 tests passed at 6e19243562. The pin was re-run at b02b3f8c16: 3 of 3 passed. Dogfood tsc --noEmit exit 0 and lists the new file.
  • Build: the dogfood dependency closure was built with turbo (63 tasks) after the merge.
  • Lint: eslint --no-inline-config --format json on the 8 edited TS files reported 8 files, 0 errors and 0 warnings. eslint.config.mjs enables no type-aware linting (no parserOptions.project), so untouched files cannot move. A repo-wide pnpm lint is left to CI.
  • Gates: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 70 commands at b02b3f8c16, a superset of the 46 named at dispatch. All 70 ran sequentially, with the exit code captured before any pipe. --ran reconciliation: 70 derived, 70 run, 0 NOT-MEASURED, 0 UNRUN.
    • check:dual-build-cjs-loads first exited 3 (PREREQUISITE NOT MET: eight unrelated packages unbuilt). After building them it re-ran and exited 0.
    • Verdict lines: check-adr-0087-registration reports 1 declared-breaking changeset, carrying its disposition. check-empty-changeset reports no foreign changeset modified. check-changeset-no-major reports no major.

Changeset

.changeset/22835-datasource-reads-environment-only.md:

  • Bumps: @objectstack/service-datasource minor (the narrowing), @objectstack/metadata-core minor (the new export), @objectstack/objectql patch (the import, no behaviour change).
  • Carries Clause-②: no (narrowing) with a FROM to TO row.
  • ADR-0087 disposition: not-required (already-registered metadata-read-organization-scope-retired). That entry already retires every read of an organization-scoped row, boot hydration included.

Line budget

+325 / -38 across 9 files against the merge base. Source is +59 / -36: metadata-core +24, objectql +4 / -11, datasource-admin-plugin.ts +31 / -25, mostly comments.

The advisory total of 300 is exceeded by the real-boot pin, 164 lines. It boots twice, mounts the admin routes and reads both doors and the store; its sibling datasource dogfood files run 250 to 300 lines.

Acceptance notes (observations, not filed)

  • metadata-protocol's reportUnhydratableOrgScopedRows re-checks the same row value inline instead of importing the predicate. That is a second spelling in a third package, though metadata-protocol depends on metadata-core. It is outside this claim's file surface. Carrier: none.
  • No objectql test pins the hook and action binders' skip of a legacy organization row. This PR moves that predicate without changing it. Carrier: none.

Contract review

This PR owes one contract-review-tier review before the queue (Clause-② narrowing), to be arranged by the seat.


Generated by Claude Code

…t rows only (ADR-0131 D6)

Every read of a stored datasource row in DatasourceAdminServicePlugin --
the boot restore, the by-name read behind convergence and the /meta
projection, and the existence probes behind its own writes -- goes through
one find filtered by isLegacyOrganizationRow, now exported from
@objectstack/metadata-core; objectql's binders import the same predicate.

Claude-Session: https://claude.ai/code/session_01JfJfBUC3cQ6hhgm9MQK76T
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Oct 11, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/metadata-core, @objectstack/objectql, @objectstack/service-datasource, touching 7 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/metadata-core/src/index.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

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

  • content/docs/api/data-flow.mdx (via findOne (literal, a string literal in Pick))
  • content/docs/automation/hook-bodies.mdx (via findOne (literal, a string literal in Pick))
  • content/docs/automation/webhooks.mdx (via findOne (literal, a string literal in Pick))
  • content/docs/kernel/contracts/data-engine.mdx (via findOne (literal, a string literal in Pick))
  • content/docs/kernel/contracts/index.mdx (via findOne (literal, a string literal in Pick))
  • content/docs/kernel/events.mdx (via findOne (literal, a string literal in Pick))
  • content/docs/permissions/attachments-access.mdx (via findOne (literal, a string literal in Pick))
  • content/docs/permissions/field-level-security.mdx (via findOne (literal, a string literal in Pick))
  • content/docs/permissions/record-view-auditing.mdx (via findOne (literal, a string literal in Pick))
  • content/docs/permissions/rls.mdx (via findOne (literal, a string literal in Pick))
  • content/docs/permissions/system-context.mdx (via findOne (literal, a string literal in Pick))
  • content/docs/protocol/objectql/query-syntax.mdx (via findOne (literal, a string literal in Pick))
  • content/docs/protocol/objectql/schema.mdx (via findOne (literal, a string literal in Pick))
  • content/docs/ui/react-pages.mdx (via findOne (literal, a string literal in Pick))

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

  • content/docs/releases/v15.mdx (via findOne (literal, a string literal in Pick))
  • content/docs/releases/v16.mdx (via findOne (literal, a string literal in Pick))
  • content/docs/releases/v17/17-0.mdx (via findOne (literal, a string literal in Pick))
  • content/docs/releases/v17/17-5.mdx (via findOne (literal, a string literal in Pick))
  • content/docs/releases/v17/17-6.mdx (via findOne (literal, a string literal in Pick))
  • content/docs/releases/v17/17-7.mdx (via findOne (literal, a string literal in Pick))
  • content/docs/releases/v17/index.mdx (via findOne (literal, a string literal in Pick))

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 changed file(s) yielded no anchor (packages/metadata-core/src/index.ts) — pages documenting those are invisible to this run
  • 1 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 — 19 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 55382dc02a2168585a024ba142b98d8bce9910e6 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 3a78f8bf4e6c14b90e02f888429c7746bd7bc465 — the merge of head b02b3f8c16ea80da01d042242fd60bf83764f03b into base 55382dc02a2168585a024ba142b98d8bce9910e6, 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 3a78f8bf4e6c14b90e02f888429c7746bd7bc465 && git checkout 3a78f8bf4e6c14b90e02f888429c7746bd7bc465
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 55382dc02a2168585a024ba142b98d8bce9910e6 b02b3f8c16ea80da01d042242fd60bf83764f03b && git checkout -B drift-repro 55382dc02a2168585a024ba142b98d8bce9910e6 && git merge --no-ff b02b3f8c16ea80da01d042242fd60bf83764f03b

node scripts/docs-audit/affected-docs.mjs --json 55382dc02a2168585a024ba142b98d8bce9910e6

⚠️ 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 55382dc02a2168585a024ba142b98d8bce9910e6 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: b02b3f8c16ea80da01d042242fd60bf83764f03b
Local-runs: none

Inputs: card #22835's body and both of its comments (claim 6109766787, os-dev-report 6110648560); #15206's os-dev-report 6108953446 (finding 1) and PR #22833's contract review 6109504501 (③); PR #22862 (its body, its 9-file list, and the net diff against the merge base eed637c09f: 9 files, +325 / −38, equal to the file list, over the branch's three commits fccd98aea0, the merge 6e19243562 and b02b3f8c16); .changeset/15206-reads-environment-only.md on origin/main (unchanged since the merge base and untouched by this diff); the 41 check-runs and the one commit status on the head, read at 2026-10-11T15:51Z; git merge-tree of the head against origin/main at 55382dc02a. Source at the head, read by symbol: datasource-admin-plugin.ts (findEnvironmentRows, persistDatasourceRow, deleteDatasourceRow, loadDatasourceRows, loadDatasourceRow, restoreRuntimeDatasources, applyMetadataDoorWrite, convergePool, listDatasourceRecords, getDatasourceRecord, putDatasourceRecord, deleteDatasourceRecord, countBoundObjects, countBoundObjectsDiagnosed, DataEngineLike), datasource-admin-service.ts (createDatasource, updateDatasource, removeDatasource), legacy-organization-row.ts and the metadata-core entry, objectql's plugin.ts (the hook, action and object-action binder reads) at the head and at the merge base, metadata-protocol's protocol.ts (reportUnhydratableOrgScopedRows, loadMetaFromDb's per-type read, the by-name overlay read), the cli's secret-reference-union.ts (readStoredDatasourceArtefacts), the three edited test files and their doubles, the dogfood harness (bootShowcase, BootOptions, the isolated project's population), the ADR-0087 ledger entry 18.metadata-read-organization-scope-retired.ts on main and at the merge base, check-adr-0087-registration.mjs's already-registered rule, .changeset/config.json and pre.json on main, the ruleset's required contexts, the governed register in check-governed-merges.mjs, and the size job in pr-automation.yml. ⛔ Not the dispatch order, not the dispatching seat's conclusions. Nothing was checked out, built, run or re-run; the dev's gate run is read as the dev's report, never as a gate verdict. This is a security card: classes, positions and functions only.

① Derived judgments

Gate verdicts on this head, as read at 2026-10-11T15:51Z. 41 check-runs: 31 success, 5 skipped, 5 in_progress, none red. Of the seven required contexts the ruleset names, six are success: Build Core, Dogfood Regression Gate (the rollup and 1/3 to 3/3), Temporal Conformance (live PG + MySQL), Lint & Repo Gates, Governed Surface Queue Guard, and TypeScript Type Check (the rollup and its four legs — source gates, consumer gates, debt ledger, workspace). One has not concluded: Test Core — shard 1/6 is success, shards 2/6 to 6/6 are in progress, and no rollup has reported. This record judges what has concluded and does not wait; the landing's "every check green" is the seat's re-read at arming, outside this verdict. Also green: Check Changeset twice (no foreign changeset is modified — the pending note is not in the diff), Dogfood Verify CLI, Check PR Size, Check Documentation Links, Flag docs affected by code changes, Spec property liveness, filter, Auto Label, and the four claim and closing guards, twice each. Skipped by contract: Build Docs, Console Pin Gate (the pin is unchanged), Packed-tarball smoke (opt-in), and the edited-run twins of Auto Label and Check PR Size. The one commit status, Vercel, is success (ignored build step). Not governed: none of the nine paths is a register row (docs/adr/, .claude/, skills/, AGENTS.md, CLAUDE.md, docs/NORTH-STAR.md); head repo is the base repo; draft; auto_merge unset; labels documentation, size/m, tests, tooling, no skip-changeset; mergeable: true, mergeable_state: blocked (the draft and the running checks, not a conflict).

  1. The census of the plugin's reads of stored datasource rows. At the head datasource-admin-plugin.ts calls engine.find on sys_metadata in exactly one place, inside the new private findEnvironmentRows (type datasource, state active, one name when given, the explicit system context, then a filter dropping every row isLegacyOrganizationRow names); findOne appears nowhere in the file and leaves the private DataEngineLike slice. The helper's four callers are the whole population of stored-row reads: loadDatasourceRows (the boot restore, restoreRuntimeDatasources, whose own engine.find precondition matches); loadDatasourceRow (behind applyMetadataDoorWrite, the /meta door's datasource projection, and convergePool, cluster convergence); persistDatasourceRow (the existence probe behind putDatasourceRecord, which admin create and update reach); deleteDatasourceRow (the probe behind deleteDatasourceRecord, admin remove). The plugin's other reads — listDatasourceRecords, getDatasourceRecord, countBoundObjects, countBoundObjectsDiagnosed — read the MetadataService registry, never sys_metadata; they stop serving the legacy row because the restore no longer registers it (the dev's hypothesis 3, measured on the real boot and pinned on both doors). RIGHT, and complete within the package: no other file under service-datasource/src reads sys_metadata (admin-routes.ts, datasource-admin-service.ts and plugin.ts read the registry). Outside the package, one read of stored datasource rows exists and is rightly left outside the helper: readStoredDatasourceArtefacts in packages/cli/src/utils/secret-reference-union.ts, a driver-level find by type, deliberately unfiltered by state because it is the liveness sweep for secret handles (a handle a stored row still holds must not be collected); it serves nothing, and narrowing it would let a handle a legacy row at rest still carries be collected — the opposite of decision: ADR-0131 C5 — under single, Studio saves are stored organization-scoped today. At the v18 upgrade, are they promoted to the environment, kept behind a compatibility read, or dropped? #22011 A. metadata-protocol's own hydration and by-name overlay reads lower organization_id: null into the store (C5) and are environment-only already. metadata-core/src/types.ts, the spec's kernel schemas and the lint rules name the table without reading it.

  2. findOne → filtered find for the by-name reads. Right, and necessary: a store-picked single row under a name held twice could be the legacy one while the environment's row exists. The unit pin inserts the legacy row FIRST under the shared name, so a first-match read would pick it, and the test double's find honours every where key by equality, so the organization work is done by the predicate alone. The [#21913] system-context pin's population count moves from one find plus four findOne to five find (two probes, the restore, convergence, the remove probe); its expectAllSystem assertion is unchanged, and the helper passes SYSTEM_CTX on the one call.

  3. The admin door's writes now. Create under a legacy row's name: createDatasource's existence check reads the registry, which no longer holds the row, and persistDatasourceRow's probe finds no environment row, so an environment row is inserted; the legacy row is not touched. Update under such a name reaches the environment row only — persistDatasourceRow's update is keyed on the id the filtered probe returned. Remove: deleteDatasourceRow deletes (or inactivates) only the environment row's id. Before, deleteDatasourceRow's findOne by type, name and state could answer the legacy row and engine.delete took its id — the dev measured that remove DELETED the legacy row — and the same unfiltered probe in persistDatasourceRow could have rewritten one in place; both against decision: ADR-0131 C5 — under single, Studio saves are stored organization-scoped today. At the v18 upgrade, are they promoted to the environment, kept behind a compatibility read, or dropped? #22011 A ("neither deleted nor rewritten"). Pinned twice: the unit case (update, remove and create under the shared name; the legacy row equals its inserted value at the end) and the dogfood case (an admin create and remove under the name write and remove an environment row of their own; a /meta save under the name is what the admin door then serves; the legacy row equals its planted bytes). RIGHT.

  4. The predicate move. At the merge base isLegacyOrganizationRow was module-private in packages/objectql/src/plugin.ts; at the head it is packages/metadata-core/src/legacy-organization-row.ts, exported through the package entry. The signature and the two-line body are byte-equal to the deleted private copy (export added, TSDoc extended); the truth table is pinned in legacy-organization-row.test.ts (an organization id is legacy; absent, undefined, null, the empty string, a null row and an undefined row are the environment's). objectql's three binder sites — the hook binder, the action binder and the object-action read — import it from @objectstack/metadata-core and are otherwise unchanged; git grep at the head finds no other definition in the tree. No new dependency edge: objectql, service-datasource and metadata-protocol already depend on metadata-core (workspace:*); metadata-core has no API-surface baseline to regenerate, and its tsconfig (src/**/*) compiles the new test under both of its typecheck programs. The inline re-check in reportUnhydratableOrgScopedRows (metadata-protocol): a filter of the same three comparisons (not null, not undefined, not the empty string) over the rows a driver that cannot lower the $and predicate hands back. It is the same truth table spelled a second time, not a second rule: it decides what a boot LOG LINE names, serves nothing, pre-dates this PR, is outside the claim's surface, and cannot make any read disagree about which rows are the environment's. Not a ground; a one-line import swap there is a follow-on for the seat if wanted (carrier: none, as the dev says). Also not a ground, and C5's own: metadata-protocol's serving reads use the where-form organization_id: null, whose empty-string edge differs from the row-value form; the card's direction names the row-value form for this plugin, which is what landed, and it agrees with the binders and with the boot report.

  5. The boot report still names the row. protocol.ts is not in the diff; reportUnhydratableOrgScopedRows is byte-unchanged from the merge base. Pinned: the dogfood premise case captures console.warn across the measured boot and asserts one [metadata_org_scoped_unserved] line naming the legacy row at its organization; the dev's before and after table reads the same. RIGHT.

  6. The pending note's sentence. "A row stored organization-scoped by an earlier release is served by no read and loaded by no boot" — for datasource rows, now true at the head in the shipped compositions: the restore does not load it (item 1), neither door answers it by name and neither lists it (measured on a real boot and pinned), no pool opens from it, and the /meta projection and convergence cannot reach it; the one remaining reader, the cli's secret-handle sweep, serves nothing (item 1). The note is untouched by the diff (right: it is the sentence to make true, not to edit) and unchanged on origin/main since the merge base. The restore's [#21922] "code wins on collision" warning is untouched: codeNames is still read from the kernel's code-datasource set and the warning fires on the same condition, after the filter.

Nothing is deleted or rewritten. No added line calls a store write; the inserts, updates and deletes are the same calls as before, now keyed on an environment row's id only.

② Semver level

.changeset/22835-datasource-reads-environment-only.md (new, this PR's own): @objectstack/service-datasource: minor, @objectstack/metadata-core: minor, @objectstack/objectql: patch; a feat(...)!: title; Clause-②: no (narrowing); one FROM → TO row with the one-line fix (re-create through the admin door or save through the /meta door, or wait for the v18 ceremony); a closing line naming the new export. Each level judged: service-datasource — a BREAKING narrowing of a published door's answer, graded minor on the v18 prerelease line exactly as the pending note grades its own (.changeset/pre.json is pre with tag next on main; all three packages are in the fixed group; check-changeset-no-major refuses major): right. metadata-core — a new public export is additive: minor, right. objectql — an import swap with no change in what it binds, a changed published artifact: patch, right (none is skip-changeset, which ruling D refuses for this class). ADR-0087 marker not-required (already-registered metadata-read-organization-scope-retired): the id resolves on main — packages/spec/src/migrations/entries/semantic/18.metadata-read-organization-scope-retired.ts and the pending note's registered marker — and both existed at the merge base eed637c09f, which is the gate's two-sided test for this category; Check Changeset is green twice. Honest: that entry's replacement already carries this change's prescription (a legacy organization-scoped row is served by no read; boot names each one; the v18 ceremony carries it; nothing is deleted or rewritten before it), the same one the changeset's row gives for a datasource row, so a new entry would duplicate a prescription the ledger holds. A precision note, not a ground: the entry's surface names @objectstack/metadata-protocol's reads "and the boot hydration" and not the datasource-admin plugin; widening that wording is a spec edit with its regenerated artifacts, outside this diff. Clause-②: no (narrowing) on PR body line 2, in the changeset and in the claim: right. No read refused today is admitted and no door answers more; a published door's answer narrows (the legacy row leaves both doors' answers and the pool). The new metadata-core export is not a widening in Clause-②'s sense — it admits no input and changes no door's answer — and it is carried by the minor level, which is the floor yes would set anyway; the closed pair admits one arm, and the narrowing is the breaking one. no (widening) would be malformed and yes would claim an admitted read that does not exist.

③ Boundary flags

Deviations (a) to (h), each answered. (a) file surface: the metadata-core module, entry export and truth-table test, and objectql's import swap — the claim names the move as part of this claim if the predicate moves to a package both sides depend on, which it does; stated in the report and the body; accepted (①4). (b) DataEngineLike drops findOne — a private type slice in the same file, now unused; the 21 doc pages the drift check lists are all reached through that findOne string literal in a Pick, so none documents what moved; Flag docs affected by code changes green; accepted. (c) origin/main merged into the branch at 6e19243562 before gating — four main commits, none in these packages; the net diff against the merge base eed637c09f equals the file list; accepted. (d) the changeset's three levels and the marker — ②; accepted. (e) line budget: +325 / −38, 363 changed lines against the 300 advisory, size/m by the labeler's bands; over by the 164-line real-boot pin, which is the shortest of the five datasource dogfood files at the head (its siblings run 178 to 323 lines) and is the pin the card asks for ("ablation-verified, on a real boot"); source is +59 / −36; advisory, not a gate; accepted. (f) zero label writes — right. (g) a temporary measurement copy of the dogfood file, run at the base and at the fix and deleted — nothing of it is in the diff; accepted. (h) the model-free trailer pair on both non-merge commits (the session trailer and the co-author trailer with no model identifier), the merge commit carrying none — what AGENTS.md requires; the harness reminder yields to it; right.

The two out-of-scope findings. (1) reportUnhydratableOrgScopedRows' inline re-check — judged in ①4: the same truth table, a log-line filter, not a second rule; carrier none is right. (2) no objectql test pins the hook and action binders' skip of a legacy organization row — true at the head (protocol-meta.test.ts's ADR-0131 D6 cases cover the protocol reads, not the binders); the predicate itself is now pinned in metadata-core, the binder sites are unchanged, and the gap pre-dates this PR; carrier none, not a ground; the seat may file it against the objectql lane.

main has moved. origin/main is at 55382dc02a, six commits past the merge base. git merge-tree --write-tree origin/main b02b3f8c16 writes a tree with no conflict; no file main touched since the base is among the nine this PR touches; .changeset/15206-reads-environment-only.md is unchanged on main since the base. The nine paths are hand-written files, so no regenerated artifact is in the merge. The queue rebuilds on the merge ref; nothing asks for a pre-merge of main.

Check-runs red or in progress. Red: none. In progress at the reading: Test Core shards 2/6 to 6/6 (1/6 green), so the required Test Core context has not reported; the dev's own vitest runs of the three packages are the dev's report. The seat arms only on a re-read showing it green; a red there re-opens this record on the same head.

For the seat at landing. The PR's Fixes #22835 is right (the card is this gap); #15206 is not closed by keyword and is closed by hand on MERGED, as its carry-over review 6109504501 left it open on exactly this sentence. The body's +325 / −38 across 9 files matches the diff. skip-changeset stays off.

Verdict grounds, all on this head: every read of a stored datasource row in the plugin goes through one helper carrying the one predicate C5's binders apply; the by-name reads are a filtered find; the writes under a legacy name touch an environment row only and the legacy row stays byte-equal; the predicate moved verbatim with its private copy deleted and no second definition in the tree; the boot report is untouched and still names the row; the pending note's sentence is now true for datasource; the three levels and the marker are right and Clause-②: no (narrowing) matches the diff; no concluded check is red and no governed path is touched. The one required context still running, Test Core, is the seat's arming condition, not this record's.

Implemented-by: claude/issue-22835-datasource-restore-environment-rows
Reviewed-by: session_01JfJfBUC3cQ6hhgm9MQK76T

VERDICT: PASS


Generated by Claude Code

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/m tests tooling

Projects

None yet

2 participants