Repository navigation
Commit dc39326
fix(spec,service-automation): the v18 stored preview lists page components on the retired API source and every refusal registerFlow makes after the parse (#23356)
Fixes #23317
Clause-②: yes (widening)
## What this does
`os migrate meta --stored` listed two families of stored rows as already
on protocol while a v18 door refuses them. Both are now **refused and
listed**, through the preview's existing TODO lines. Nothing is
rewritten, on preview or on `--apply`, and `storedMigrationClean` does
not move. No door accepts or refuses anything it did not before.
| item | shape | listed by | TODO id |
|:--|:--|:--|:--|
| 1 | a page component whose `data` prop binds the retired view-level
API source: `object-grid`, `object-map`, `object-gantt`, `object-tree` |
page, component and position; request headers withheld |
`view-data-api-provider-retired` (the existing D3 entry, its scope
extended) |
| 2 | every site `registerFlow` refuses in a stored flow the parse
accepts: a text slot's template hole, a decision's `mode`, a malformed
node or edge predicate, a plugin node's undeclared config key, an `api`
flow with no secret | flow and site, in the check's own words |
`registerFlow` (the refusing door: these sites have no step-18 entry of
their own) |
## Public surface: Clause ②, yes (widening)
The card's prediction was `Clause-②: no` (the preview lists more, no
door accepts more). That holds for every door. The diff also widens
published outputs, the same class contract review record 6152721854
named for PR #23315, so this line is `yes (widening)`, and the changeset
is `minor` for both packages:
- `ConversionTodoNotice.conversionId` (`@objectstack/spec`): may now
name `registerFlow`, the refusing door, for a site no conversion and no
D3 entry covers. Declared in its docblock.
- `IAutomationService.canonicalizeStoredFlow(...).todos?`
(`@objectstack/spec`) and
`AutomationEngine.canonicalizeStoredFlow(...).todos`
(`@objectstack/service-automation`): now also carry each site flow
registration refuses. Declared in the contract's docblock. No type
changes.
- The `onTodo` sink of `applyConversions` (`@objectstack/spec`) now also
receives a page component bound to the retired API source, under the
existing D3 id.
Nothing narrows. No export name changes: `check:api-surface` is green
and nothing was regenerated.
## Zone 2 assumptions, each measured
1. **The components: partly disproved.**
- There are **four**, not two. `ComponentPropsMap` rows whose `data` is
`ViewDataSchema`: `object-grid`, `object-map`, `object-gantt`,
`object-tree`. `object-kanban`, `object-calendar` and `object-timeline`
take a row array under `data` and refuse a provider object for its
shape, never with this retirement's prescription.
- **A stored page carrying one is not refused at rehydration.**
`PageSchema.safeParse` and `getMetadataTypeSchema('page')` both accept
the page, because `PageComponent.properties` is an open record. The door
that refuses the binding is the component-props gate
(`validateComponentProps` in `@objectstack/lint`). It is declared
`surfaces: CLI_ONLY`, so `os validate`, `os build` and `os lint` refuse
it in source, and the save door and the boot do not.
- So the TODO's fate sentence says what is true: the page still loads
and saves, and the CLI refuses the binding in source. The ruling's
listing stands as written.
- **Extending the existing D3 entry is the truthful record, not a
sibling entry.** It is one retirement: the same arm of the same schema,
under the same ruling. The entry's `surface` / `replacement` / `reason`
/ `acceptanceCriteria` now name the four components and the
component-props gate.
- **The judge is the door's own predicate.** Each stored component is
judged by its own `ComponentPropsMap` row's `data` member, the one the
component-props gate parses. It is listed only when that row refuses the
binding with the prescription `ViewDataSchema` gives. No list of types
is kept in step: a pin derives the four from `ComponentPropsMap` and
holds the D3 entry's and the detector's `surface` text to them.
2. **The whole validation: holds, with one deviation from the Zone 3
route.**
- `canonicalizeStoredFlow` was `registerFlow`'s first half: convert,
parse, cycle check, region validation. The four registration checks
(`validateNodeConfigKeys`, `validateDecisionModes`,
`validateFlowExpressions`, `validateApiTriggerSecret`) ran only inside
`registerFlow`, and each threw.
- Measured on a probe at base `688a39d77`. Canonicalize passed, and
`registerFlow` refused: a decision `mode: 'bogus'`, a malformed node
predicate, a braced edge predicate, an `api` flow with no secret, and a
notify `title` on the single-brace dialect or with an unknown formatter.
A builtin's undeclared key and #23260's value arm (`get_record` `limit:
'{x}'`) are refused inside `FlowSchema.parse`. So
`canonicalizeStoredFlow` throws them, and the stored pass already
reports the row `failed` with the parse's words.
- **Deviation from Zone 3:** the checks cannot run from a module-scope
function. They read the live engine: the action registry (plugin node
`configSchema`), the trigger binding and its credential channel
(`resolveSecret`), and the object-schema resolver. So the shared list is
an instance method.
- Each check now **returns** its refusal (the exact message it threw,
plus its sites) instead of throwing. `registrationChecks()` is the one
ordered list. `registerFlow` iterates it and throws the first refusal,
so the same flows are refused with byte-identical messages.
`canonicalizeStoredFlow` runs the same list and lists every site. A
check added to that list later is listed with no second change.
- The listing never throws: a check that throws instead of refusing is
listed as one site at the flow. It runs only when `todos` is first read,
once. So the save door, a package duplication and the sync's held-flow
comparison (`holdsFlowDefinition` in `plugin.ts`), which read `parsed` /
`storable` only, do no more work than before.
- The schema-aware advisory pass (logged, never thrown) runs for
registration only, so the doors that canonicalize log nothing new.
3. **No double listing: holds, and dedupe is needed.** A value-slot
`{…}` site is refused by both the spec detector (D3 id) and
`validateFlowExpressions`. Registration sites carry stack paths spelled
as the conversion walk spells them
(`flows[0].nodes[1].config.body.nodes[0].config.title`), and a site a
chain TODO already names is skipped. Ablation A4 below: without the
skip, the existing #23269 engine pin goes red.
4. **The report: holds; the line names the door.** A step-18 refused
shape keeps its D3 id. A registration refusal has no step-18 entry, so
its line reads `TODO registerFlow: SOURCE left as stored at PATH — No
conversion rewrites it, and registerFlow refuses it: left as stored, the
flow is not registered at boot and never runs. Flow NAME, WHERE: the
check's message`. Here `SOURCE`, `PATH`, `NAME` and `WHERE` are
placeholders. `from` shows the source text, the key, or the declared
value. For the `api` secret, it shows only a blank string or `null`, so
no secret is printed.
## Where it lands, and why outside the claim's file surface
The claim's surface was taken whole: `refused-shapes.ts` and its test,
the D3 entry, `engine.ts`, `stored-migration.ts` (docblocks only) and
one changeset. Outside it:
- `packages/spec/src/conversions/types.ts` and
`packages/spec/src/contracts/automation-service.ts`: docblocks only.
They declare the widened domain and content above.
- `packages/runtime/src/dispatcher-error-vocabulary.ts`: one
`foreign-vocabulary` row. `check:dispatcher-error-vocabulary` refuses
any unclassified site that stamps `OS_METADATA_CONVERSION_TODO`, and the
engine now builds that TODO. The row sits beside the `apply.ts` row and
has the same reasoning. The file is read by the gate and by a test, and
it is not in runtime's `dist`.
- `content/docs/deployment/cli.mdx`: the `--stored` table. Its row said
each TODO names a step-18 entry and that the parse or the boot refuses
the row, and both would have become false. It now lists both families,
each with its fate.
-
`packages/services/service-automation/src/builtin/config-unknown-keys.test.ts`:
one pin called the private `validateNodeConfigKeys` and expected a
throw. It now reads the returned refusal.
- `packages/metadata-protocol/src/protocol.stored-migration.test.ts`:
one preview pin for a page row.
- Not touched: `packages/metadata-protocol/src/protocol.ts` (fenced for
PR #23345). The stored pass already reads `flowResult.todos ?? []`, so
no edit there was needed.
## Pins
- **Item 1** (`refused-shapes.test.ts`, 14 new):
- each of the four components is listed once at
`pages[0].regions[0].components[0].properties.data`, naming the page,
the component and its position, with the fate and the prescription; the
row is not rewritten;
- reach: a component nested in a `page:tabs` panel, and one in a slot;
- no credential: headers are withheld;
- **negative controls**, none listed: the `object` and `value`
providers, `object-kanban` (a row-array `data`), a custom type with no
props row, and the governed `dataSource` element binding;
- **door agreement, both directions:** the listed set equals the props
rows whose `data` refuses the binding with the prescription;
- the fate holds: `PageSchema` accepts the page, and each row refuses
the binding;
- the D3 entry and the detector name the four.
- `metadata-protocol`: the preview lists the page row `skipped` with its
TODO, without the header. An `object` provider page counts as
`canonical`. `--apply` writes nothing.
- **Item 2** (`canonicalize-stored-flow.test.ts`, 23 new):
- eight refused flows, one per check (two text-slot shapes, a text slot
in a loop body, a decision mode, a node predicate, an edge predicate, a
plugin node's undeclared key, an `api` flow with no secret). Each is
listed under `registerFlow` at its site, in the check's words, and
`registerFlow` refuses it with the same words;
- **negative controls:** the same eight on protocol list nothing and
register;
- **door agreement, both directions:** over the corpus, a flow is listed
exactly when `registerFlow` refuses it;
- several checks in one flow are all listed, in `registerFlow`'s order,
and `registerFlow` still throws the first;
- dedupe: a braced filter operand plus a single-brace title gives one D3
TODO and one `registerFlow` TODO;
- a parse refusal (#23260's value arm) throws from
`canonicalizeStoredFlow` as from `registerFlow`;
- nothing is rewritten: `storable` is the input reference;
- laziness: reading `parsed` / `storable` / `conflicts` runs no check,
and `todos` runs them once;
- no new logs: the advisory pass logs at `registerFlow` and not at
`canonicalizeStoredFlow`.
## Verification
At `HEAD` `ac768f1d5` unless a line names another commit. `origin/main`
was merged at `ac768f1d5`: 5 commits, none under `packages/spec`.
`service-automation`'s `notify-node.ts` changed there, so that suite
re-ran after the merge.
- Build: the closures of `@objectstack/service-automation^...`,
`@objectstack/metadata-protocol^...` and `@objectstack/runtime^...`
(turbo, 30 of 30 tasks), then `@objectstack/spec` again; every `VERDICT
command-exit 0`.
- `pnpm --filter @objectstack/spec check:generated`: "All 14 generated
artifacts are up to date". No tracked file moved.
- `typecheck`, exit 0 each: `@objectstack/spec` (test layer held),
`@objectstack/service-automation` (after the merge),
`@objectstack/metadata-protocol`, `@objectstack/runtime`.
- Tests:
- `@objectstack/service-automation`, the whole suite after the merge:
185 files, 2481 tests, exit 0.
- `@objectstack/spec` `local` project in four locked runs: 102 files /
2735 tests, 170 / 5918, 310 / 9524 (+1 todo), and 88 / 2245. That is 670
files, the whole project (729 test files minus the 59 `repo` ones). All
passed, at `706df2da0`; no spec file changed after it.
- `@objectstack/spec` `test:repo`: 59 files, 1011 tests, exit 0, at
`706df2da0`.
- `@objectstack/metadata-protocol`: `protocol.stored-migration`,
`protocol.flow-canonicalizer` and `protocol.save-flow-canonicalization`,
80 tests, exit 0. This is a declared narrowing: the package's code is
unchanged (docblocks only), and CI runs the suite.
- `@objectstack/runtime`: `error-envelope.conformance.test.ts` (the test
that imports the vocabulary file), 53 tests, exit 0. Also a narrowing:
runtime's code is unchanged.
- **Ablations**, one-shot, through `scripts/ablation-replace.mjs` in
wrap mode under the lock. Every anchor went 1 to 0, and every restore
was proven: blob equal to `HEAD`, empty `git diff HEAD`. The spec and
engine pins import their subject by relative path, so no `dist` leg was
needed.
- A1, the page-component judge made to return nothing: 7 failed, 38
passed, predicted red. The four listings, reach, the credential pin and
door agreement failed; the five negative controls and two text pins
stayed green.
- A2, the props-row judge skipped (any API binding listed): 3 failed, 42
passed. `object-kanban`, the custom type and door agreement failed. This
is the proof that the row is the judge.
- A3, the registration listing removed from `canonicalizeStoredFlow`,
re-run on the final lazy form: 13 failed, 21 passed. Every listing pin
failed; the on-protocol controls, the parse-throw pin and the log pin
stayed green.
- A4, the per-site dedupe removed (final form): 2 failed, 32 passed. The
dedupe pin and the existing #23269 "exactly one TODO" pin failed.
- Lint, a measured narrowing, not `pnpm lint`:
- `eslint --no-inline-config --format json` over the 13 changed files
gave 13 entries. 11 were linted, with 0 errors and 0 rule warnings.
eslint itself ignores the changeset and `cli.mdx` ("File ignored").
- Invariance: `eslint.config.mjs` enables no type-aware linting (no
`parserOptions.project`), so this diff cannot move an untouched file's
verdict.
- Gates: `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived 114 commands at `ac768f1d5`, with no
stale-tree warning. Each ran with its exit code recorded before any
pipe.
- First pass: 112 exited 0. `check:skill-examples` and
`check:dual-build-cjs-loads` exited 3 (PREREQUISITE NOT MET: an unbuilt
`client-react`, and 30 unbuilt packages).
- Both exited 0 once `turbo run build` filled their prerequisites (35 of
35 and 74 of 74 tasks, cached).
- Reconcile: "✓ dispatch-gates --ran: 114 derived famil(ies) accounted
for — 114 run, 0 NOT-MEASURED (a DERIVED zero — all 114 recorded an exit
code and none of them is 3)."
- On the way, `check:dispatcher-error-vocabulary` caught one real
finding: the engine's TODO stamp was unclassified. The vocabulary row
above fixes it.
## Serial note: #23316
#23316 (branch `claude/issue-23316-refused-flow-runtime-state`, no PR
yet) edits `engine.ts`, `plugin.ts` and
`contracts/automation-service.ts`. `git merge-tree --write-tree HEAD
origin/claude/issue-23316-refused-flow-runtime-state` gave exit 0 and
tree `5069a5cc8` (no conflict), at this head and that branch's
`2b84fe555`.
- That branch imports a type named `FlowRegistrationRefusal` into
`engine.ts`, so this branch's internal types are named
`RegistrationCheckRefusal` / `RegistrationCheckSite`. None of the
identifiers this diff adds appears on that branch.
- This diff does not touch `plugin.ts`, the cold-boot bind,
`withdrawFlow`, or what `registerFlow` does after its checks.
- **Whichever of the two PRs reaches the queue second merges `main` and
re-runs the automation suite and the contract gates.** A joint typecheck
of the merged tree was not measured here.
## Acceptance notes
- **The retired API binding still works on a stored page at runtime.**
The component-props gate is CLI-only by its declared `surfaceReason`
("Gating rule held off the runtime door pending the crossing discipline
… Crossing is its own rollout card"). The save door and the boot accept
a page component whose `data` carries `provider: 'api'` with `headers`.
At objectui `origin/main` `72594f29`, `resolveDataSource` still builds
an `ApiDataSource` from such a config (`ObjectGantt.tsx` routes through
it). So a stored page keeps sending the request as stored, headers
included, until the row is rewritten. This PR lists those pages and does
not change a door. It is reported as a finding.
- The prescription the component-props gate prints at a component's
`data` says `view.data.provider`, because it is `ViewDataSchema`'s own
text. This is wording only, and its carrier is the retirement's owner.
- Cost: when a caller reads `todos`, the stored pass runs registration's
checks once per flow row. Nothing else pays for them.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01BRQ9Mp1yH7R1az6MaUM18H)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 18cd410 commit dc39326
13 files changed
Lines changed: 834 additions & 125 deletions
File tree
- .changeset
- content/docs/deployment
- packages
- metadata-protocol/src
- runtime/src
- services/service-automation/src
- builtin
- spec/src
- contracts
- conversions
- migrations/entries/semantic
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1473 | 1473 | | |
1474 | 1474 | | |
1475 | 1475 | | |
1476 | | - | |
| 1476 | + | |
| 1477 | + | |
1477 | 1478 | | |
1478 | 1479 | | |
1479 | 1480 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1184 | 1184 | | |
1185 | 1185 | | |
1186 | 1186 | | |
| 1187 | + | |
| 1188 | + | |
| 1189 | + | |
| 1190 | + | |
| 1191 | + | |
| 1192 | + | |
| 1193 | + | |
| 1194 | + | |
| 1195 | + | |
| 1196 | + | |
| 1197 | + | |
| 1198 | + | |
| 1199 | + | |
| 1200 | + | |
| 1201 | + | |
| 1202 | + | |
| 1203 | + | |
| 1204 | + | |
| 1205 | + | |
| 1206 | + | |
| 1207 | + | |
| 1208 | + | |
| 1209 | + | |
| 1210 | + | |
| 1211 | + | |
| 1212 | + | |
| 1213 | + | |
| 1214 | + | |
| 1215 | + | |
| 1216 | + | |
| 1217 | + | |
1187 | 1218 | | |
1188 | 1219 | | |
1189 | 1220 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
61 | 61 | | |
62 | 62 | | |
63 | 63 | | |
64 | | - | |
65 | | - | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
66 | 68 | | |
67 | 69 | | |
68 | 70 | | |
| |||
119 | 121 | | |
120 | 122 | | |
121 | 123 | | |
122 | | - | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
123 | 129 | | |
124 | 130 | | |
125 | 131 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
617 | 617 | | |
618 | 618 | | |
619 | 619 | | |
| 620 | + | |
| 621 | + | |
| 622 | + | |
| 623 | + | |
| 624 | + | |
| 625 | + | |
| 626 | + | |
| 627 | + | |
| 628 | + | |
| 629 | + | |
| 630 | + | |
| 631 | + | |
| 632 | + | |
| 633 | + | |
620 | 634 | | |
621 | 635 | | |
622 | 636 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
159 | 159 | | |
160 | 160 | | |
161 | 161 | | |
162 | | - | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
163 | 165 | | |
164 | 166 | | |
165 | 167 | | |
| |||
169 | 171 | | |
170 | 172 | | |
171 | 173 | | |
172 | | - | |
| 174 | + | |
173 | 175 | | |
174 | 176 | | |
175 | 177 | | |
176 | | - | |
| 178 | + | |
177 | 179 | | |
178 | 180 | | |
179 | 181 | | |
| |||
0 commit comments