Skip to content

Commit 05ab9ef

Browse files
feat(spec)!: the page parse refuses a component data prop naming the retired view-level api source, at every door that parses a page (#23419)
Fixes #23357 Clause-②: yes (narrowing) > The seat amended the claim to `yes (narrowing)` at ACCEPT because of the one published export, `checkPageComponentViewData`. ## What changes The page parse now refuses a page component's `data` prop that names the retired view-level `api` data source, as a view's own `data` is refused. That is triage's runtime half of this card (grade `6159116611`): the narrow judge for `ViewDataSchema` at a component's `data`, enforced at every door that parses a page. It is not the component-props gate's runtime rollout, which stays as decided. - **`packages/spec/src/ui/page.zod.ts`**: a fifth `PageSchema` object-level check, `checkPageComponentViewData`, attached by identifier like its four siblings. For a component of a type whose `data` is a view data source (`object-grid`, `object-map`, `object-gantt`, `object-tree`) whose `properties.data` names the retired provider, it raises the prescription `ViewDataSchema`'s own parse raises, located at the component's `properties.data.provider` (code `custom`, the code of every object-level check on this schema). The prescription is read off `ViewDataSchema`'s issue, not copied. It walks every component under `regions` and `slots` at any depth, a component nested in a `page:tabs` item or a container included. - **`packages/spec/src/conversions/refused-shapes.ts`**: the stored listing's fate for such a component now says what happens: the parse refuses the page; it still loads and is served, registered under `[metadata_spec_invalid]` with the refusal in every read's `_diagnostics`; its next save is refused with 422. - **`packages/spec/src/migrations/entries/semantic/18.view-data-api-provider-retired.ts`**: the acceptance criteria name the page parse, at every door that parses a page, beside the component-props gate. - `view.zod.ts`: a comment pointing from the retirement's prescription to the page check. Generated: `api-surface/ui.json`, `export-origins/ui.json` (the one new export). **The renderer half is objectstack-ai/objectui#12325, which remains open:** until objectui deletes its reader of the retired arm, a page already stored with such a component keeps sending the request. ## Measured (mechanism hypotheses) - **H1, half falsified.** `view.zod.ts` refuses the arm through the discriminated union's `error` map: `invalid_union` at `provider`, message `VIEW_DATA_API_PROVIDER_RETIRED`. `page.zod.ts` does NOT parse a component's `data`: `properties` is `z.record(z.string(), z.unknown())`, and on `origin/main` `PageSchema.safeParse` of an `app` page whose `object-grid` binds the retired provider answered `success: true`. - **H2, holds.** The save door's schema is `getMetadataTypeSchema('page')`, which is `PageSchema` by identity (`resolveOverlaySchema`, `protocol.ts`). Measured through the real `saveMetaItem` in `protocol.save-union-issues.test.ts`: `422 INVALID_METADATA`, nothing persisted, issue at `regions.0.components.0.properties.data.provider`, code `custom`, the prescription. `metadata-protocol/src/protocol.ts` is untouched. - **H3, the four, and only by declaration.** Derived from `ComponentPropsMap` by def identity, the rows whose `data` member is `ViewDataSchema` are exactly `object-grid`, `object-map`, `object-gantt`, `object-tree` (59 rows; `object-kanban`, `object-calendar`, `object-timeline` take a row array). The page check cannot derive that set itself: `component.zod.ts` imports `page.zod.ts`, `packages/spec/src` has zero import cycles (measured: 1383 modules, 0 strongly connected components), and moving `ElementDataSourceSchema` / `RETIRED_PAGE_COMPONENT_TYPES` out of `page.zod.ts` would change the published skill's generated references. So the set is declared in `page.zod.ts` (the `PRINT_REFUSED_PAGE_COMPONENT_TYPES` precedent) and pinned equal to the derivation in both directions (`page-component-view-data.test.ts`: for every props row, the page parse refuses the binding exactly when the row's `data` is `ViewDataSchema`). The same cycle keeps `pageComponentSlotPositions()` out of reach, so the walk descends every object under `regions` and `slots`, a superset of every slot position: it reaches no less than `os migrate meta --stored` lists or the props gate judges, and what it judges at a node stays the one retired binding on those four types. - **H4, stored pages load and are listed; their next save is refused.** Boot hydration (`loadMetaFromDb`, measured with a throwaway probe over a stub engine, not committed): two stored pages, one bound to the retired provider: `{"loaded":2,"errors":0,"invalid":1}`, both registered, one `[metadata_spec_invalid]` warning naming `page/partner_page` and the prescription. Read path (`getMetaItem`, pinned): served, with `_diagnostics.valid: false` and the prescription at the component's position. The fate text states exactly that. ## Tests Every run went through `scripts/pm/os-verify-lock.sh`. Final verdicts are at `63e1e4800`, the branch merged with `origin/main` `c857a93ee`. The newest head, `dce543307`, adds only the changeset's `Clause-②` line. - `@objectstack/spec`, `vitest run --project local`: 675 files, 20507 passed, 1 todo, exit 0. - `@objectstack/spec`, `--project repo`: 59 files, 1011 passed, exit 0. This project holds the tree-scoped absence pin. - `@objectstack/metadata-protocol`: 235 files passed, 3 skipped; 28377 tests passed, exit 0. - `@objectstack/cli`, `--project unit`: 287 files, 4307 tests passed, exit 0. - `@objectstack/lint`: 138 files, 6699 tests passed, exit 0, at `7309ef08c`. The later merge touched nothing in `packages/lint`. - `typecheck` of spec and of metadata-protocol: exit 0 each. - `pnpm --filter @objectstack/spec check:generated`: all 14 artifacts up to date. New pins: - `packages/spec/src/ui/page-component-view-data.test.ts`: - each of the four types is refused at `regions.0.components.0.properties.data.provider` with the retirement's exact message; - a nested component, a container's grandchild and a slot are each refused at their own position; - `definePage`, `getMetadataTypeSchema('page')` and the stack parse all refuse; - controls: `object`, `value`, `schema`, no `data`, another provider, `object-kanban`, a custom type and the element `dataSource` binding are accepted; - a derivation pin over all 59 `ComponentPropsMap` rows. - `object-refinement-check-exports.test.ts`: the fifth export's parity, count, attachment and barrel legs. - `refused-shapes.test.ts`: the new fate text, and the page parse refuses exactly the positions the listing lists. - `protocol.save-union-issues.test.ts`: - `422 INVALID_METADATA` with nothing persisted, top-level and nested; - control saves with `object` and `value` (one row each); - the read path serves a stored page with `_diagnostics.valid: false` and the prescription. Not run locally, and declared to CI: - the cli integration layer: the diff touches no integration file and no spawn entry; - the full `turbo ls --affected` closure, 83 packages. That set comes from the package dependency graph and is blind to tests that read another package's source from disk. The root package's own suites are not on the graph. ## Ablation (negative control) Run on `7309ef08c` through `scripts/ablation-replace.mjs`, which mutates and restores and proves both on disk, and `scripts/ablation-dist-preflight.mjs`. 1. **Pristine.** The dist marker `superRefine(checkPageComponentViewData))` is present in 18 built JS files, and the tree is clean. 2. **Mutate.** The attachment `.superRefine(checkPageComponentViewData));` is replaced by an inert comment. The anchor count goes 1 to 0, and the blob goes `ed11df9efb60` to `b310419cea20`. - The spec source suites go red: 3 files, 17 failed, 268 passed. They are the four type pins, nested/slot, doors, self-reference, the derivation pin, the export parity/count/attachment legs, and the two refused-shapes parity pins. - After the spec rebuild, the preflight reads the marker absent from all 232 built files. - `protocol.save-union-issues.test.ts` goes red: 3 failed, 26 passed. They are the two 422 pins and the read path. The control save stays green. 3. **Restore.** The blob equals HEAD, `git diff HEAD` is empty and the whole-tree status is clean. After the spec rebuild, the marker is present in 18 files and the tree is clean. Spec runs 285/285 and metadata-protocol 29/29. The expected direction: the pins turn red. Two earlier attempts carry no reading: - The first was a no-op. The tool refused it because the replacement `);` already occurred in the file, so the mutation never landed. - The second used a dist marker that the `.d.ts` docblock also carries, so its dist reading was void. It was re-run as above, with a marker only the runtime JS carries. ## Gates `node scripts/pm/dispatch-gates.mjs --ran` on `7309ef08c` reports 90 derived families, all accounted for. 88 exited 0, including `check:adr-0087-registration --base origin/main`, `check:api-surface`, `check:generated` (all 14 artifacts up to date), `check:skill-refs`, `check:doc-authoring`, `check:engine-double-contract`, `check:nul-bytes`, `check:lean-entry-closure` and `check:cross-package-test-inputs`. NOT MEASURED: - `check:dual-build-cjs-loads`: exit 3, PREREQUISITE NOT MET. 9 packages outside this card's build closure have no `dist/`. - `check:type-check-debt`: its re-measure re-runs `tsc` over every DEBT-ledger package repo-wide, which CI owns. The touched packages' own `typecheck` runs are green. Repo-wide `pnpm lint` was not run locally. CI owns it. ## Acceptance notes - One published export added (`checkPageComponentViewData`), required by `object-refinement-check-exports.test.ts` for every object-level `PageSchema` check so a `.shape` mirror can re-attach it; see the `Clause-②` note at the top. - The page check's issue code is `custom`, not the view's `invalid_union`: the object-level-check pin reads `custom` as the code of every check on the schema. The message is the retirement's, byte for byte. - ADR-0087: `not-required (already-registered view-data-api-provider-retired)`; `check-adr-0087-registration` accepts it. - Consumer sweep: `examples/**` holds no `provider: 'api'` (0 hits; control `provider: 'object'` hits in `examples/app-crm/src/views`); the tree-scoped absence pin (`view-data-api-provider-retirement.test.ts`) is green, and every fixture here spells the provider through a constant. --- _Generated by [Claude Code](https://claude.ai/code/session_01YRp5z4kK6vgYkrdH97ztsy)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 61b1258 commit 05ab9ef

11 files changed

Lines changed: 493 additions & 17 deletions

File tree

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
feat(spec)!: the page parse refuses a page component's `data` prop that names the retired view-level `api` data source, so the Studio, REST and MCP save door refuses it as `os validate` does (#23357)
6+
7+
**BREAKING** — a page whose `object-grid`, `object-map`, `object-gantt` or `object-tree` component carries `properties.data` with `provider: 'api'` is now refused by `PageSchema`, at the component's `properties.data.provider`, with the retirement's own prescription. Those four components take a view data source (`ViewDataSchema`) as `data`, so the step-18 retirement of the view-level `api` source reached them through the same schema; but the page parse leaves a component's `properties` an open record, and the component-props gate that refused the binding runs at the CLI doors only. A page saved through the Studio, REST or MCP therefore kept the binding, with its request headers, and the console kept sending it.
8+
9+
Clause-②: yes (narrowing)
10+
11+
The judge is the retirement's own: `ViewDataSchema` parses the component's `data`, and the prescription it raises is raised by the page parse unchanged (one text for a view and a page; the page's issue `code` is `custom`, as every object-level check on `PageSchema` raises). It reaches every component under `regions` and `slots`, at any depth, a component nested in a `page:tabs` item or a container included. It judges nothing else in the bag: another `data` provider, a component whose `data` is a row array (`object-kanban`, `object-calendar`, `object-timeline`), a custom or registered type with no props row, and a component's own `dataSource` binding are untouched. `checkPageComponentViewData` is exported from `@objectstack/spec/ui`, like the four other `PageSchema` checks, so a mirror built from `PageSchema.shape` can re-attach it.
12+
13+
## FROM → TO
14+
15+
| you wrote (accepted by the page parse until this change) | write instead |
16+
| --- | --- |
17+
| `properties.data` with `provider: 'api'` (and its `read` / `write` request blocks) on an `object-grid`, `object-map`, `object-gantt` or `object-tree` page component | `properties.data` with `provider: 'object'` and `object: '<object_name>'`, or `provider: 'value'` with `items: [...]`; an endpoint the page must read is a page component's `dataSource: { provider: 'api', read: { url: '/api/...' }, path? }`, same-origin and with no credential key |
18+
| `read.headers: { Authorization: '...' }` inside that binding | nothing: a credential never lives in metadata, and the console reads an element binding with its own signed-in fetch |
19+
20+
**The one-line fix:** bind each such component to its object or to static rows, and move any endpoint read onto a page component's `dataSource`.
21+
22+
**What an author who still writes it sees.** `definePage()`, `defineStack({ pages })`, `os validate` and `PUT /api/v1/meta/page/:name` refuse the page at `regions[i].components[j].properties.data.provider` (or the nested or slot position) with the prescription. At the save door that is `422 INVALID_METADATA`, nothing persisted.
23+
24+
**A page already stored with such a component.** `os migrate meta --stored` lists it by page and component (the step-18 entry `view-data-api-provider-retired`), and nothing rewrites it: deleting `data` would make the component query its object instead. Left as stored, the page still loads and is served, registered under `[metadata_spec_invalid]` with the refusal in the `_diagnostics` of every read, and its next save is refused with 422. The console stops sending the request when its own reader of the retired arm is deleted (objectstack-ai/objectui#12325); until then a stored page keeps sending it.
25+
26+
<!-- adr-0087: not-required (already-registered view-data-api-provider-retired) the step-18 entry already names the data prop of these four page components in its scope; this change makes the page parse refuse what that entry retired, and its acceptance criteria now name the page parse beside the component-props gate -->

‎packages/metadata-protocol/src/protocol.save-union-issues.test.ts‎

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -491,3 +491,85 @@ describe('#20230 a flattened overlay carrying a retired owner/hidden is refused
491491
}
492492
});
493493
});
494+
495+
/**
496+
* #23357 — a page whose component binds the retired view-level `api` data
497+
* source, at the save door: `saveMetaItem`, the `PUT /api/v1/meta/page/:name`
498+
* write path the Studio, a REST caller and an MCP author reach.
499+
*
500+
* Before: the page parse left a component's `properties` an open record and the
501+
* component-props gate that refuses the binding runs at the CLI doors only, so
502+
* an `object-grid` whose `data` named the retired provider saved — one row
503+
* persisted with its request headers — and the console kept sending it. After:
504+
* `PageSchema`'s `checkPageComponentViewData` refuses it, and this envelope is
505+
* what the author meets. Pinned HERE and not only in spec for the reason the
506+
* #20230 block above gives: a spec refusal that did not reach this envelope
507+
* would be a refusal nobody sees.
508+
*/
509+
describe('#23357 a page whose component binds the retired view-level api source is refused at the save door', () => {
510+
// Spelled through a constant: the spec's tree-scoped absence pin reads
511+
// authored literals, and these bodies are refusals, not authorings.
512+
const RETIRED_PROVIDER = 'api' as const;
513+
const RETIRED_BINDING = {
514+
provider: RETIRED_PROVIDER,
515+
read: { url: 'https://partner.example.com/cases', headers: { Authorization: 'Bearer s3cr3t' } },
516+
};
517+
const PRESCRIPTION = /^`view\.data\.provider: 'api'` was removed in @objectstack\/spec 18 \(ADR-0049/;
518+
const grid = (data: unknown) => ({ id: 'cases', type: 'object-grid', properties: { objectName: 'crm_case', data } });
519+
const pageBody = (components: unknown[]) => ({
520+
name: 'partner_page',
521+
label: 'Partner',
522+
type: 'app',
523+
regions: [{ name: 'main', components }],
524+
});
525+
const savePage = (protocol: any, body: unknown) => save(protocol, body, 'partner_page', 'page');
526+
527+
it('an `object-grid` binding it answers 422 INVALID_METADATA, persists nothing, and locates the prescription at the component', async () => {
528+
const { protocol, rows } = makeProtocol();
529+
530+
const err = await rejection(savePage(protocol, pageBody([grid(RETIRED_BINDING)])));
531+
532+
expect(err.code).toBe('INVALID_METADATA');
533+
expect(err.status).toBe(422);
534+
expect(rows.size).toBe(0);
535+
const at = err.issues.find((i: any) => i.path === 'regions.0.components.0.properties.data.provider');
536+
expect(at, `an issue at the component's data provider; got ${JSON.stringify(err.issues)}`).toBeDefined();
537+
expect(at.code).toBe('custom');
538+
expect(at.message).toMatch(PRESCRIPTION);
539+
});
540+
541+
it('a component nested in a `page:tabs` item is refused at its own position', async () => {
542+
const { protocol, rows } = makeProtocol();
543+
const tabs = { id: 'tabs', type: 'page:tabs', properties: { items: [{ label: 'Cases', children: [grid(RETIRED_BINDING)] }] } };
544+
545+
const err = await rejection(savePage(protocol, pageBody([tabs])));
546+
547+
expect(err.code).toBe('INVALID_METADATA');
548+
expect(err.status).toBe(422);
549+
expect(rows.size).toBe(0);
550+
expect(err.issues.map((i: any) => i.path)).toContain('regions.0.components.0.properties.items.0.children.0.properties.data.provider');
551+
});
552+
553+
it('CONTROL: the same page with the `object` or `value` provider saves, one row', async () => {
554+
for (const data of [{ provider: 'object', object: 'crm_case' }, { provider: 'value', items: [{ name: 'a' }] }]) {
555+
const { protocol, rows } = makeProtocol();
556+
const result = await savePage(protocol, pageBody([grid(data)]));
557+
expect(result.success, JSON.stringify(data)).toBe(true);
558+
expect(rows.size).toBe(1);
559+
}
560+
});
561+
562+
it('READ PATH: a stored page still carrying the binding is served, with the refusal in its `_diagnostics`', async () => {
563+
const { protocol } = makeProtocol([
564+
{ type: 'page', name: 'partner_page', metadata: pageBody([grid(RETIRED_BINDING)]) },
565+
]);
566+
567+
const served = (await protocol.getMetaItem({ type: 'page', name: 'partner_page' })).item;
568+
569+
expect(served.regions[0].components[0].type).toBe('object-grid');
570+
expect(served._diagnostics?.valid).toBe(false);
571+
const at = served._diagnostics.errors.find((e: any) => e.path === 'regions.0.components.0.properties.data.provider');
572+
expect(at, JSON.stringify(served._diagnostics)).toBeDefined();
573+
expect(at.message).toMatch(PRESCRIPTION);
574+
});
575+
});

‎packages/spec/api-surface/ui.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -496,6 +496,7 @@
496496
"checkGlobalFilterDateDefaultValue (function)",
497497
"checkListViewCalendarVisualization (function)",
498498
"checkListViewChartBinding (function)",
499+
"checkPageComponentViewData (function)",
499500
"checkPagePrintComposition (function)",
500501
"checkPageRequiresKind (function)",
501502
"checkPageSlotPair (function)",

‎packages/spec/export-origins/ui.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -481,6 +481,7 @@
481481
"checkGlobalFilterDateDefaultValue": "src/ui/dashboard.zod.ts#checkGlobalFilterDateDefaultValue (function)",
482482
"checkListViewCalendarVisualization": "src/ui/view.zod.ts#checkListViewCalendarVisualization (function)",
483483
"checkListViewChartBinding": "src/ui/view.zod.ts#checkListViewChartBinding (function)",
484+
"checkPageComponentViewData": "src/ui/page.zod.ts#checkPageComponentViewData (function)",
484485
"checkPagePrintComposition": "src/ui/page.zod.ts#checkPagePrintComposition (function)",
485486
"checkPageRequiresKind": "src/ui/page.zod.ts#checkPageRequiresKind (function)",
486487
"checkPageSlotPair": "src/ui/page.zod.ts#checkPageSlotPair (function)",

‎packages/spec/src/conversions/refused-shapes.test.ts‎

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -360,7 +360,8 @@ describe('#23317 — a stored page whose component binds the retired API source
360360
const todo = todos[0]!;
361361
expect(todo.reason).toContain(`The \`${type}\` component \`block\` of page \`partner_page\`, at pages[0].regions[0].components[0], binds the retired view-level API data source.`);
362362
expect(todo.reason).toMatch(/No mechanical move: the successor is a page component's `dataSource` binding/);
363-
expect(todo.reason).toMatch(/Left as stored, the page still loads and saves, because the page parse does not judge a component's properties/);
363+
expect(todo.reason).toMatch(/Left as stored, the parse refuses the page: it still loads and is served, registered under `\[metadata_spec_invalid\]`/);
364+
expect(todo.reason).toMatch(/its next save through the Studio, REST or MCP is refused with 422/);
364365
expect(todo.reason).toMatch(PRESCRIPTION);
365366
});
366367

@@ -374,6 +375,12 @@ describe('#23317 — a stored page whose component binds the retired API source
374375
'pages[0].regions[0].components[0].properties.items[0].children[0].properties.data',
375376
'pages[0].slots.details.properties.data',
376377
]);
378+
// [#23357] …and the page parse refuses exactly those two positions.
379+
const verdict = PageSchema.safeParse(row);
380+
expect(verdict.success ? [] : verdict.error.issues.map((i) => i.path.join('.'))).toEqual([
381+
'regions.0.components.0.properties.items.0.children.0.properties.data.provider',
382+
'slots.details.properties.data.provider',
383+
]);
377384
});
378385

379386
it('never prints a credential: the URL and the method are shown, every other request key is withheld', () => {
@@ -414,9 +421,24 @@ describe('#23317 — a stored page whose component binds the retired API source
414421
}
415422
});
416423

417-
it('the fate it states holds: the page parse accepts the page, and the component props row refuses the binding', () => {
424+
// [#23357] Until the page parse judged this binding the fate read "the page
425+
// still loads and saves", and this pin asserted `PageSchema` ACCEPTED the
426+
// row. Since `checkPageComponentViewData` the page parse refuses each listed
427+
// component at the listed position with the same prescription, which is
428+
// what the save door's 422 and the read / boot `_diagnostics` resolve.
429+
it('the fate it states holds: the page parse refuses each listed component at the listed position, and so does its props row', () => {
418430
const row = pageRow(VIEW_DATA_COMPONENTS.map((type, i) => component(type, apiData(), `b${i}`)));
419-
expect(PageSchema.safeParse(row).success).toBe(true);
431+
const { todos } = storedTodos('page', row);
432+
const verdict = PageSchema.safeParse(row);
433+
expect(verdict.success).toBe(false);
434+
const refused = verdict.success ? [] : verdict.error.issues;
435+
// The parse locates the issue at `…properties.data.provider`; the listing
436+
// names the `data` prop it would rewrite, in the conversion walker's spelling.
437+
const listedAt = (path: readonly PropertyKey[]) =>
438+
`pages[0]${path.slice(0, -1).map((k) => (typeof k === 'number' ? `[${k}]` : `.${String(k)}`)).join('')}`;
439+
expect(refused.map((i) => listedAt(i.path))).toEqual(todos.map((t) => t.path));
440+
expect(todos).toHaveLength(VIEW_DATA_COMPONENTS.length);
441+
for (const issue of refused) expect(issue.message).toMatch(PRESCRIPTION);
420442
for (const type of VIEW_DATA_COMPONENTS) {
421443
expect(ComponentPropsMap[type].safeParse({ objectName: 'crm_case', data: apiData() }).success, type).toBe(false);
422444
}

‎packages/spec/src/conversions/refused-shapes.ts‎

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,9 @@
3535
* `controlled_by_parent` refinement's
3636
* ({@link controlledByParentMasterRefusal}), `ViewDataSchema`'s parse — for
3737
* a page component, through the component's own props row, which the
38-
* component-props gate parses — and the value-slot judge
38+
* component-props gate parses and whose verdict on this one retirement the
39+
* page parse shares (`checkPageComponentViewData`, #23357) — and the
40+
* value-slot judge
3941
* `AutomationEngine.registerFlow` and `objectstack validate` run
4042
* ({@link flowNodeValueTemplateRefusals}). A site is listed here exactly
4143
* when that door refuses it.
@@ -205,13 +207,18 @@ interface SafeParseable {
205207
* when that refusal is this retirement's — `undefined` otherwise (#23317).
206208
*
207209
* The page parse leaves a component's `properties` an open record; the door
208-
* that judges them is the component-props gate, which parses the bag against
210+
* that judges the bag is the component-props gate, which parses it against
209211
* the type's `ComponentPropsMap` row. So the judge here is that row's own
210212
* `data` member, and the site is listed only when it refuses the binding with
211213
* the prescription `ViewDataSchema` refuses it with. That holds for every
212214
* component whose `data` is a view data source and for no other, without a
213215
* list of types to keep in step: a row whose `data` is a bare array refuses
214216
* the binding for its shape, never with this prescription.
217+
*
218+
* Since #23357 the page parse refuses this one binding too
219+
* (`checkPageComponentViewData`, `ui/page.zod.ts`), on the same four types and
220+
* with the same prescription, so a listed site is refused at the save door as
221+
* well; `refused-shapes.test.ts` pins the two judges to one set of types.
215222
*/
216223
function componentApiBindingRefusal(type: unknown, data: unknown): string | undefined {
217224
const prescription = apiBindingRefusal(data);
@@ -237,10 +244,11 @@ const VIEW_API_BINDING_FATE =
237244
const COMPONENT_API_BINDING_FATE =
238245
'No mechanical move: the successor is a page component\'s `dataSource` binding, which takes a same-origin '
239246
+ '`url` and no other request key, so the request cannot be carried over as written; and deleting `data` '
240-
+ 'would make the component query its object instead and change what it shows. Left as stored, the page '
241-
+ 'still loads and saves, because the page parse does not judge a component\'s properties, while '
242-
+ '`os validate`, `os build` and `os lint` refuse the binding in source. Decide what the component binds now, '
243-
+ 'and which same-origin endpoint, if any, replaces the request.';
247+
+ 'would make the component query its object instead and change what it shows. Left as stored, the parse '
248+
+ 'refuses the page: it still loads and is served, registered under `[metadata_spec_invalid]` with the '
249+
+ 'refusal in the `_diagnostics` of every read, and its next save through the Studio, REST or MCP is refused '
250+
+ 'with 422, as `os validate`, `os build` and `os lint` refuse the binding in source. Decide what the component '
251+
+ 'binds now, and which same-origin endpoint, if any, replaces the request.';
244252

245253
const viewDataApiProvider: RefusedStoredShape = {
246254
id: 'view-data-api-provider-retired',

‎packages/spec/src/migrations/entries/semantic/18.view-data-api-provider-retired.ts‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,9 @@ export const entry: SemanticMigration = {
3737
+ 'move either: deleting its `data` would make it query its object instead.',
3838
acceptanceCriteria: "No view in `stack.views[]`, in any object's `listViews` / `formViews` map or in a stored "
3939
+ "`sys_metadata` view row carries `data.provider: 'api'`, and no page component's `data` prop does; the "
40-
+ 'parse refuses one in a view, and the component-props gate of `os validate`, `os build` and `os lint` '
41-
+ 'refuses one in a page component, with the prescription. '
40+
+ 'parse refuses one in a view and in a page component, at every door that parses a page (the metadata '
41+
+ 'save door\'s 422 included), and the component-props gate of `os validate`, `os build` and `os lint` '
42+
+ 'refuses one in a page component too, with the prescription. '
4243
+ "Every endpoint such a view read is either a page component's API binding on a same-origin path "
4344
+ 'with no credential key, or dropped on purpose; and every converted view binds its object or static '
4445
+ 'rows and loads. `os migrate meta --stored` lists each stored view, and each page component of a stored '

0 commit comments

Comments
 (0)