Repository navigation
Commit 78f841b
feat(spec)!: the build doors refuse an undeclared key on a script / subflow node config, with its location (#22129)
Part of #21982
Clause-②: no (narrowing)
**Draft, patch round 1 done** (claim revision `6050287134`). This PR
lands the `script` / `subflow` half of the card. The card stays open for
the remainder named below.
## What changes
The build doors now refuse a key that a `script` or `subflow` node's
executor contract does not declare. They refuse it at its location, in
the existing family of flow slot refusal codes.
- **The doors:** `FlowSchema.parse` / `defineFlow()`, `defineStack`,
`objectstack validate`, `objectstack compile`, an artifact's parse, the
metadata save door's `flow` type schema, and `registerFlow`, which
parses `FlowSchema` first.
- **The code:** the existing `node-config-refused-by-contract`, `params:
{ nodeType, key }`. There is one refusal per undeclared key, anchored at
the key (`nodes.N.config.bogusKey`), and its message is the contract's
own sentence.
- **The edit:**
`packages/spec/src/automation/flow-node-config-refusals.ts`, the builtin
branch of `flowNodeConfigRefusals`. It judges key membership where
`builtinKeysJudged(nodeType)` holds. That is a builtin contract whose
type is in the spec's own schemaless class,
`SCHEMALESS_NODE_CONFIG_SCHEMAS`.
- **Why the narrow lift:** a schemaless descriptor publishes no
`configSchema`, so `registerFlow`'s undeclared-key walk skips it. Its
executor still parses the strict contract, so it refuses the node at
every run.
- **Unchanged:** `getBuiltinNodeConfigContracts()` keeps its 13 entries,
and no new code joins `FLOW_SLOT_REFUSAL_CODES`.
- **Premise corrected:** `builtinValueJudged`'s docblock said
"registration refuses an undeclared key against the descriptor". That
was false for exactly these two types. The docblock now says which door
owns which key.
- **Comments made true:** the `FlowSchema` header in `flow.zod.ts` and
the `node-config-refused-by-contract` docblock in
`flow-node-expression-paths.ts` now name each arm that judges key
membership: approval whole (#21850), builtin values (#21898), and
`script` / `subflow` keys.
- No `service-automation` source line moves, and there is no second key
check anywhere.
## Built-ins: which get the key refusal, and why (measured at
`15ec50e528`)
| type | in `getBuiltinNodeConfigContracts` | descriptor `configSchema`
| contract strict | executor parses it | key refusal here |
|:--|:--|:--|:--|:--|:--|
| `script` | yes | none | `strictObject` | yes (`screen-nodes.ts`
`parseNodeConfig`) | **yes** |
| `subflow` | yes | none | `strictObject` | yes (`subflow-node.ts`
`parseNodeConfig`) | **yes** |
| `decision` | no | none | `strictObject` | no: its executor reads
`conditions[]` raw | no. An undeclared key fails no run.
`decisionShapeRefusals` judges the `conditions` shape, not keys. |
| `wait`, `connector_action` | no | none | their contracts are the
FlowNode sibling blocks `waitEventConfig` / `connectorConfig`,
`strictObject` inside `FlowNodeSchema` | they read the sibling block,
not `config` | no. `FlowSchema` already refuses an unknown key in those
blocks. |
| `get_record`, `create_record`, `update_record`, `delete_record`,
`notify`, `http`, `screen`, `map`, `loop`, `parallel`, `try_catch` | yes
| yes | `strictObject` (all 11) | yes | no: the remainder, below |
| `assignment` | no | yes (keyValue map) | no: open top-level variable
names | no single contract | no |
## The remainder: the card stays open for it
Triage's direction step 2 covers every builtin whose executor contract
is strict. All 13 are. This PR takes the two schemaless ones, by the
seat's ruling `6050287134`.
- **Remainder 1, the 11 descriptor-`configSchema` builtins at the build
doors:** `get_record`, `create_record`, `update_record`,
`delete_record`, `notify`, `http`, `screen`, `map`, `loop`, `parallel`
and `try_catch`.
- They have the same gap at the build doors. Measured at this branch's
round-0 head `f281d801f` on `examples/app-showcase`, flow
`showcase_task_completed`, node `notify`, with `config.bogusKey: 1`:
- `objectstack validate` exits 0;
- `objectstack compile` exits 0, and the artifact carries
`"bogusKey":1`;
- `registerFlow` refuses the same node: "Flow 'p' rejected: 1 undeclared
config key(s). … unknown config key `bogusKey` at config.bogusKey".
- So boot drops the flow with a warning, and the build never says so.
- **Remainder 2, an open design choice, not decided here:** once the
spec arm covers those 11 types, who judges a builtin's undeclared key at
`registerFlow`? The spec arm pre-empts registration's descriptor walk,
and with it that walk's pinned prescriptions (`service-automation`
`config-unknown-keys.test.ts`).
## Census first (triage step 1): no writer found
| corpus | read at | `script` nodes | `subflow` nodes | with a key
outside the contract |
|:--|:--|:--|:--|:--|
| this repo: `examples/**`, `packages/platform-objects/**`,
`packages/apps/**`, `packages/create-objectstack/**` (templates),
`skills/**`, `content/docs/**` | `15ec50e528` | 6 | 2 | 0 |
| hotcrm, whole tree | `c9678036d9` | 0 | 5 | 0 |
| objectui flow designer `FLOW_NODE_CONFIG` | pin `a58626c88d` (same
file at objectui `main` `9990f9e122`) | form writes `function`,
`inputs`, `outputVariable` | form writes `flowName`, `input`,
`outputVariable` | 0 (its `timeoutMs` field writes the node; the five
retired `script` keys sit behind a `showWhen` no field satisfies) |
| objectui designer seeds `defaultNodeExtras` | `a58626c88d` | empty
`config` | empty `config` | 0 |
| objectui console preview samples | `a58626c88d` | 4 | 0 | 0 |
- **Method:** a TypeScript-AST scan for object literals carrying `id`
and `type: 'script'` / `'subflow'`, reading the keys of their `config`.
Code fences in `.md` / `.mdx` and `.json` files were parsed too.
- **Control:** over this whole repo, tests included, the same scan finds
103 nodes and flags 17. All 17 are fixtures:
- 9 in the D2 conversion fixtures (`conversions/registry.ts`);
- 5 in `lint` tests;
- 3 test-double keys in `service-automation` `engine.test.ts`, repaired
below.
## Doors, measured
- **`objectstack validate` / `compile`.** Built CLI at round-0 head
`f281d801f`, `examples/app-showcase` node `summarize` (`script`), one
edit: `function: 'summarizeCompletedTask' , bogusKey: 1,`.
- Control: validate exit 0, compile exit 0, and the artifact has no
`bogusKey`.
- With `bogusKey`: validate **exit 1**, compile **exit 2**, and no
artifact is written. Both print the refusal at path `nodes, 1, config,
bogusKey`.
- The mutation was made with `scripts/ablation-replace.mjs`: anchor 1 →
0, then restored to the HEAD blob, `git diff HEAD` empty.
- **`registerFlow`** (real builtin executors):
- `script` / `subflow` with `bogusKey` are refused at
`nodes.1.config.bogusKey`;
- both controls register;
- a `script` `functionName` alias registers: it is converted before the
parse;
- `http` `bogusKey` is still refused by the descriptor walk.
- **Pinned in `flow-builtin-node-config-keys.test.ts`** (19 tests):
- the refusal at `FlowSchema` (also inside a region body), `defineStack`
(`STACK_SCHEMA_INVALID` 422 at `flows.1.nodes.1.config.bogusKey`),
`ObjectStackDefinitionSchema`, the save door's `flow` type schema and an
artifact parse;
- the controls: no extra key; a descriptor type's key still left to
registration (`http`, `create_record`, `screen`); `decision`; a retired
`script` key keeps its tombstone path.
- **Reverse verification** at round 0, `builtinKeysJudged` mutated to
`return false`: 12 of 19 went red and the 7 controls held. It was
restored to the HEAD blob.
## Cross-lane fixtures repaired (claim revision `6050287134`)
- **`service-automation`, test only:**
- The doubles in `engine.test.ts` ("should execute unconditional
branches in parallel", "should fail when parameter type is wrong") and
in `input-schema-retry-parity.test.ts` now register under the type
`probe_step`, executor and nodes alike, never the builtin `script`.
- The `function: 'noop'` filler went with them.
- `inputSchema` reads top-level config keys, which a real `script`
executor refuses.
- **`lint`:** `validateStackExpressions` keeps the pre-conversion
tolerance it declares.
- The filter in `validate-expressions.ts` also hands the judge's
undeclared-key refusal for a `script` node's `functionName` alias to the
callable check, which already reads that alias.
- A new pin holds that every other undeclared `script` key is still
refused there (`bogusKey`, on a canonical and on an alias source).
- The changeset gains `'@objectstack/lint': patch`.
**Red → green.** Round 0 at `f281d801f` had 4 red in
`service-automation` and 2 red in `lint`. All six now pass at
`ef0dfb44d`:
- `engine.test.ts` › "should execute unconditional branches in parallel"
✓
- `engine.test.ts` › "should fail when parameter type is wrong" ✓
- `input-schema-retry-parity.test.ts` › "never executes a node whose
config mis-types its declared inputSchema — on ANY attempt" ✓
- `input-schema-retry-parity.test.ts` › "still retries a VALID flow
normally …" ✓
- `validate-expressions.test.ts` › "accepts a script node that names a
callable via the functionName alias" ✓
- `validate-expressions.test.ts` › "a `script` with no `function` is ONE
finding, the callable check's …" ✓
## ADR-0087
- **D3 entry:**
`18.flow-script-subflow-config-undeclared-keys-refused.ts`.
- **Rationale fragment:** step 18 `order: 87`, re-read on `origin/main`
`8fc50b764` (the merged base): its highest order is 86, and open PRs
#22103 and #22094 hold 86 and 85 at their heads.
- **Registry:** `registry.ts` was regenerated by
`gen:migration-registry`.
- **Changeset:** `@objectstack/spec` `minor`, BREAKING, with the
`registered` marker, plus `@objectstack/lint` `patch`.
`.changeset/pre.json` is absent on `origin/main`.
## Merge
- `origin/main` `8fc50b764` was merged by `scripts/pm/os-regen-merge.sh`
as merge commit `521e16f1f`, with parents `f281d801f` and `8fc50b764`.
There were no conflicts.
- Step 2 took `main`'s side of the generated artifacts that `main`
moved, and there was nothing more to commit.
- After a spec build on the merged tree, `check:generated` reported all
15 artifacts up to date. The delta against `main` was exactly this PR's
5 round-0 files.
- `gen:schema` was not run.
- Round 1's edits are commit `ef0dfb44d` on top.
## Verification at `ef0dfb44d`
- **Spec:**
- `check:generated`: all 15 artifacts up to date.
- The pin files `flow-builtin-node-config-keys.test.ts` and
`flow-builtin-node-config-values.test.ts`: 63/63.
- Round 0's whole spec suite at `f281d801f`: 624 files, 18628 tests
passed.
- **lint:** the whole suite, 123 files, 5689/5689.
- **service-automation:** the whole suite, 175 files, 2120/2120. The
first attempt collided with a concurrent gate run that left
`@objectstack/spec/automation` unresolvable for 17 files; it was re-run
alone.
- **Typecheck:** `@objectstack/lint` exit 0 and
`@objectstack/service-automation` exit 0, both including
`check:test-typecheck`, over a closure rebuilt with declarations.
- **eslint, narrowed** (`--no-inline-config --format json`) over the
diff's 10 `.ts` files: 10 files, 0 errors, 0 warnings. The population is
read from the json count. `parserOptions.project` and `projectService`
are null for each file, so there is no type-aware linting and no
untouched file's verdict can move.
- **Gates:** `dispatch-gates --commands --repo
objectstack-ai/objectstack` derives 92 commands from the merged head.
All ran, with exit codes captured before any pipe. The `--ran`
reconciliation reads 91 run, 1 NOT-MEASURED, 0 UNRUN
(`check:dts-closure` recorded at its re-run).
- 91 exit 0.
- `check:dual-build-cjs-loads`: exit 3, PREREQUISITE NOT MET (packages
outside this worktree's build closure have no `dist`). It is read from
CI, as are round 0's `cli` published-subpath pins.
- `check:dts-closure` first exited 1, naming exactly the 19 closure
packages built with `OS_SKIP_DTS=1` for the test runs. Re-run after the
closure was rebuilt with declarations, it exits 0: 169/169 declaration
files across 71 built packages.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01RPo7FUd6bSnAfkWMAKi848)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent ef1fcb2 commit 78f841b
11 files changed
Lines changed: 566 additions & 39 deletions
File tree
- .changeset
- packages
- lint/src
- services/service-automation/src
- spec/src
- automation
- migrations
- entries/semantic
Lines changed: 43 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
4535 | 4535 | | |
4536 | 4536 | | |
4537 | 4537 | | |
| 4538 | + | |
| 4539 | + | |
| 4540 | + | |
| 4541 | + | |
| 4542 | + | |
| 4543 | + | |
| 4544 | + | |
| 4545 | + | |
| 4546 | + | |
| 4547 | + | |
| 4548 | + | |
| 4549 | + | |
| 4550 | + | |
| 4551 | + | |
| 4552 | + | |
4538 | 4553 | | |
4539 | 4554 | | |
4540 | 4555 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1714 | 1714 | | |
1715 | 1715 | | |
1716 | 1716 | | |
1717 | | - | |
1718 | | - | |
| 1717 | + | |
| 1718 | + | |
| 1719 | + | |
| 1720 | + | |
| 1721 | + | |
| 1722 | + | |
| 1723 | + | |
| 1724 | + | |
| 1725 | + | |
1719 | 1726 | | |
1720 | 1727 | | |
1721 | 1728 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2337 | 2337 | | |
2338 | 2338 | | |
2339 | 2339 | | |
| 2340 | + | |
| 2341 | + | |
| 2342 | + | |
2340 | 2343 | | |
2341 | | - | |
| 2344 | + | |
2342 | 2345 | | |
2343 | 2346 | | |
2344 | 2347 | | |
| |||
2355 | 2358 | | |
2356 | 2359 | | |
2357 | 2360 | | |
2358 | | - | |
2359 | | - | |
| 2361 | + | |
| 2362 | + | |
2360 | 2363 | | |
2361 | 2364 | | |
2362 | 2365 | | |
| |||
2442 | 2445 | | |
2443 | 2446 | | |
2444 | 2447 | | |
| 2448 | + | |
| 2449 | + | |
| 2450 | + | |
2445 | 2451 | | |
2446 | | - | |
| 2452 | + | |
2447 | 2453 | | |
2448 | 2454 | | |
2449 | 2455 | | |
| |||
2457 | 2463 | | |
2458 | 2464 | | |
2459 | 2465 | | |
2460 | | - | |
| 2466 | + | |
2461 | 2467 | | |
2462 | | - | |
| 2468 | + | |
2463 | 2469 | | |
2464 | 2470 | | |
2465 | 2471 | | |
| |||
Lines changed: 6 additions & 5 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
55 | 55 | | |
56 | 56 | | |
57 | 57 | | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
58 | 61 | | |
59 | | - | |
| 62 | + | |
60 | 63 | | |
61 | 64 | | |
62 | 65 | | |
| |||
72 | 75 | | |
73 | 76 | | |
74 | 77 | | |
75 | | - | |
| 78 | + | |
76 | 79 | | |
77 | | - | |
78 | | - | |
79 | | - | |
| 80 | + | |
80 | 81 | | |
81 | 82 | | |
82 | 83 | | |
| |||
0 commit comments