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 7db1697
Browse filesBrowse the repository at this point in the historyBrowse files
fix(spec): os migrate meta --stored lists a stored object bound external to a stored or registered managed datasource (#25299)
<!-- claude-projects-attribution: {"github_login":"os-bill"} -->
_Requested by **Bill** · [project
thread](https://claude.ai/code/project/chan_016iMXWSZDzqWRWvWzdNo8wg?thread=cmsg_016iMXWSZDzqWRWvWzdNo8wg6zwDuV2XvoDkqC7bXgqnpc)_Fixes#25279
Clause-②: yes (widening)
Metadata-protocol: none
The stored-shape listing for
`object-external-binding-managed-datasource-refused` resolved an
object's named datasource only from the stack being converted. `os
migrate meta --stored` hands the chain one stored row at a time, so for
a stored object row the listing could judge only a binding on the
primary datasource (`'default'`). The object save door from #25257 also
refuses a binding on a datasource stored or registered beside the
object, so the pass missed exactly the rows whose next save is refused.
This takes the card's first branch: the pass hands down the datasource
the door would read, and the detector judges it through the same
predicate.
- **One read for both:** a new private
`externalBindingDatasourceDefinition(name)` in `protocol.ts` is the
`readMetaItem({ type: 'datasource', name })` read that
`externalBindingSaveRefusal` already did inline (the environment's
stored row, else the definition the code layer registers). The object
half of the save door now calls it, and so does the pass, so the two
cannot judge a binding against different datasources.
- **The pass:** `migrateStoredMetadata`, for each non-flow row, asks
`storedRowKnownDatasources(singular, body)`. For an `object` row with
`external` and a named `datasource`, it resolves that one datasource and
passes it down as a one-entry map through `convertStoredItemDetailed` →
`applyConversionsToStoredItem`. Any other row passes nothing. A failed
read propagates, as it does at the door.
- **`@objectstack/spec`:** an optional
`ApplyConversionsOptions.knownDatasources`, a read-only map from
datasource name to that datasource's definition (only its `schemaMode`
is read), which `StoredConversionOptions` inherits. `applyConversions`
hands it to `reportRefusedStoredShapes` as a `RefusedShapeContext`, so
only the refused-shape listing reads it (only with an `onTodo` sink).
The `external` detector reads the stack's own `datasources` first, then
the map. `refused-shapes.ts` stays internal (not re-exported).
- **`surface` string** now reads "…on the primary datasource, or on a
managed one the stack declares or the caller resolves (a stored or
registered datasource)".
## Measure first (base `c9be979dd`)
1. **The save door** (`externalBindingSaveRefusal`, object half): one
read, `readMetaItem({ type: 'datasource', name })`. That read takes the
environment's active stored row first and falls back to the code layer's
definition (the MetadataService registration, then the registry). A
code-defined datasource name declines a stored row
(`declinesStoredRow`), so the code definition wins there.
2. **The pass** (`migrateStoredMetadata`): it reads every `active` and
`draft` `sys_metadata` row in two queries before the loop. So the stored
datasource rows are in reach before any object row is converted. The
pass now uses the door's own read for each object row, so registered
datasources and the code-wins rule are covered too.
3. **Census of `onTodo` callers:** `applyConversionsToStoredItem` is
called with a sink only by `convertStoredItemDetailed` (the pass). The
other sink, `canonicalizeStoredFlow` in `service-automation`, goes
through `applyConversionsToFlow` over `{ flows: [...] }`, which the
`external` detector never reads. Neither changes behaviour without the
new option.
## Tests
- `packages/spec/src/conversions/refused-shapes.test.ts` (+7): a stored
row on a known datasource declared `'managed'`, or with no `schemaMode`,
is listed once and not rewritten. A known `'external'` or
`'validate-only'` datasource, or one the row does not name, is not
listed. A datasource the stack declares is read before the caller's.
Without an `onTodo` sink the map is never read.
-
`packages/metadata-protocol/src/protocol.external-binding-named-datasource-refusal.test.ts`
(+6). An object row is planted straight into the `sys_metadata` double
(stored before the door existed), and then `migrateStoredMetadata` runs
in preview. Pinned cases:
- on a stored managed datasource (both shapes), the row is listed
`skipped` with this entry's TODO at `objects[0].external`, and the same
body offered to `saveMetaItem` is refused `422 INVALID_METADATA` at
`external`;
- on a registered managed datasource, the row is listed;
- when a stored federated row overlays a registered managed definition,
the row is not listed and the save goes through;
- on a federated stored datasource, or one no layer holds, the row is
not listed (the card's control).
- **Full runs:** `@objectstack/spec` test 708 files / 21527 tests
passed, typecheck exit 0. `@objectstack/metadata-protocol` test 258
files / 20921 tests passed, typecheck exit 0.
- **Ablation, protocol half:** the pass's `await
this.storedRowKnownDatasources(singular, body)` was replaced with
`undefined` via `scripts/ablation-replace.mjs` (anchor 1 → 0, blob
`919137f3` → `cad58c17`). Result: 3 failed / 25 passed, with the three
listing pins red and every control green. This is the base behaviour of
the pass. Restore proven: blob == HEAD `919137f3`, `git diff HEAD`
empty. `@objectstack/spec` resolved to a `dist/` built from this branch.
- **Ablation, spec half:** the detector's `??
context.knownDatasources?.get(name)` was dropped (anchor 1 → 0, blob
`a387f79e` → `39524e03`). Result: 2 failed / 60 passed, the two listing
pins red. Restore proven: blob == HEAD `a387f79e`, `git diff HEAD`
empty. The test imports `src` relatively.
## Gates
All local readings were taken on head `aa0ca3a33` (the final commit,
`origin/main` `49185cdb8` merged in).
- **Derived subset:** `node scripts/pm/dispatch-gates.mjs --commands
--repo objectstack-ai/objectstack` derived 88 families. `--ran`
reconciliation: "88 derived famil(ies) accounted for — 87 run, 1
NOT-MEASURED". 87 exit 0. `pnpm check:dual-build-cjs-loads` exits 3
(PREREQUISITE NOT MET: it loads every package's `dist`, and this
worktree built only the closure), so it is NOT MEASURED locally and left
to CI.
- **Two re-runs after their prerequisites:** on the first pass
`check-plugin-teardown-shape.mjs --self-test` (shallow history; the
named commit was fetched) and `pnpm check:lean-entry-closure`
(`@objectstack/objectql` not yet built) exited 3. Both exit 0 on re-run.
- `pnpm --filter @objectstack/spec check:generated`: "All 14 generated
artifacts are up to date", against a `dist` built from this branch
without `OS_SKIP_DTS`.
- CI families: this PR's check-runs. They were in progress when it
opened.
## Acceptance notes
- **The datasource side is listed only through its objects.** The
datasource save door also refuses re-saving a stored datasource as
managed while objects bind it `external`. The pass lists the pair at
each object row, not at the datasource row. Either fix the owner chooses
(federate the datasource, or drop `external`) clears both. A
datasource-row TODO would need its own D3 id and was not asked for.
- **Draft object rows** are judged against the active datasource, as the
door judges a draft save. Organization-scoped rows are skipped before
this step, as before.
- **One read per object row with `external`**, not cached across rows.
The read is the door's own, and an `apply` run may rewrite rows between
two object rows.
- **The semantic entry's acceptance criteria** already said `os migrate
meta --stored` lists each such row. That sentence is now true for stored
and registered datasources, so the entry is unchanged and the second
branch's guidance edit is not needed.
- **File surface:** `packages/spec/src/conversions/apply.ts` was added
to the claim's declared surface. The option has to pass through
`applyConversions` to reach the detector. The seat was told before the
edit.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01P6EWuSSDC4q1RNpSTNbyCU)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
fix(spec): `os migrate meta --stored` lists a stored object bound `external` to a stored or registered managed datasource
7
+
8
+
Clause-②: yes (widening)
9
+
10
+
Metadata-protocol: none
11
+
12
+
The stored-shape listing for `object-external-binding-managed-datasource-refused` resolved an object's named datasource only from the stack being converted. A stored object row reaches the conversion chain alone, so the listing judged only a binding on the primary datasource (`'default'`), while the object save door also refuses one on a datasource stored or registered beside the object. The stored-metadata pass therefore did not list a stored object row bound `external` to a managed datasource, the row whose next save is refused with `422 INVALID_METADATA`.
13
+
14
+
-**Now:** for each stored `object` row carrying `external` and naming a datasource, the pass reads that datasource the way the object save door does (the environment's stored row, else the definition the code layer registers; both go through one read) and hands it to the chain. The row is listed under `rows[].todos` with this entry's id exactly when that door refuses its next save. A datasource declared `schemaMode: 'external'` or `'validate-only'`, and a datasource no layer holds, give no listing, as at the door. Nothing is rewritten.
15
+
-**`@objectstack/spec`:**`ApplyConversionsOptions` (and so `StoredConversionOptions` for `applyConversionsToStoredItem`) gains an optional `knownDatasources`, a map of datasource definitions by name that the caller's door can see beyond the stack. Only the refused-shape listing reads it, so only when an `onTodo` sink is passed. A datasource the stack declares is read from the stack first. Without it, behaviour is unchanged.
0 commit comments