Repository navigation
Commit ca0bdbc
Fixes #23260
Clause-②: no (narrowing)
## What changes
The build doors' value arm (`flowNodeConfigRefusals` in
`packages/spec/src/automation/flow-node-config-refusals.ts`) judged a
builtin node's present config value against its executor contract,
except a value carrying a `{token}`: `builtinValueJudged` returned false
when `carriesInterpolationToken` found one, because "only the
interpolation decides what the run will see there". #19939 deleted the
flow interpolator and every call site it had, and every builtin executor
parses its config as authored (an `http` node after its credential
channel's `signingSecret`). So every value the carve-out admitted was
refused at every run that reached the node. This PR deletes the
carve-out:
- **A token-carrying value is judged for its type like any literal.**
`get_record` with `limit: '{x}'` is refused at `nodes.N.config.limit`
("expected number, received string") by `FlowSchema.parse`,
`defineStack` (`STACK_SCHEMA_INVALID`, 422, at
`flows.N.nodes.M.config.limit`), `objectstack validate`, the save door
and `AutomationEngine.registerFlow`.
- **The value-slot and text-slot judges keep their own located token
refusals, and the arm exempts them explicitly.** It asks the two judges
which paths they refuse (`slotJudgeRefusedPaths`:
`flowNodeValueTemplateRefusals`, and `textSlotTemplateRefusal` over
`flowNodeTextSlotSources`) and skips a contract issue at one of those
paths. A token at a value or text slot is therefore reported once, by
its judge, with its label and remedy. The judges are asked, not re-read:
no second copy of the dialect's grammar.
- **A literal read as written keeps its own words.** At a key whose
contract issue is marked `READ_AS_WRITTEN_PARAM` (a `notify`
`sourceObject` / `actorId` / `payload`, an `http` `method` / `durable` /
`timeoutMs` / whole `headers` map), the marked issue speaks for the key.
With the carve-out gone, the contract's type issue beside it
(`timeoutMs: '{t}'` is also not a number) would otherwise reach the arm
first and replace the literal's remedy. The key is refused once, as
before.
- **The rationale says what holds.** It is corrected in five places: the
`READ_AS_WRITTEN_PARAM` docblock (`flow-template-token.ts`), the value
arm's docblocks, the `FlowSchema` comment that restates the arm, the
step-18 D3 entry `flow-builtin-node-config-values-refused`, and its
rationale fragment in `registry.ts.template`.
`carriesInterpolationToken` and `INTERPOLATION_TOKEN` are deleted:
nothing else read them, and neither was exported. `check:api-surface`
reads "public API surface + factory signatures unchanged".
## Measured first (the dispatch's four assumptions)
- **A1, nothing interpolates any more: HOLDS.** At base `175e8d414`,
`service-automation/src/builtin/template.ts` exports only `VariableMap`,
`FlowTextTemplateError`, `textTemplateScope` and `renderTextSlot`. The
deleted interpolator survives only in comments. Every contract-carrying
executor calls `parseNodeConfig(type, id, Schema, node.config)` on the
authored config: crud x4, `http` (on `{ ...authored, signingSecret: held
}`), `loop` (with a `body`), `map`, `notify`, `parallel`, `screen`,
`script`, `subflow` and `try_catch`. Text slots are rendered and
value-slot envelopes evaluated only after the parse. Searching
`service-automation/src` and `spec/src/automation` for `interpolate`,
`interpolateString` and `resolveToken` finds no code caller.
- **A2, the build doors share the one judge: HOLDS, and both are
pinned.** `FlowSchema`'s superRefine calls `flowNodeConfigRefusals`
(`flow.zod.ts`). `defineStack` parses `ObjectStackDefinitionSchema`,
whose `flows` are `FlowSchema`. `registerFlow` parses first, in
`canonicalizeStoredFlow`. Pins: `defineStack` in the spec test,
`registerFlow` in `service-automation`'s `config-parse.test.ts`.
- **A3, the double-report risk: HOLDS, with one addition.** A throwaway
probe at base (39 configs, deleted after the run) measured one token at
each kind of slot. At a value slot, the contract raised one issue at the
slot path: a `custom` issue with the value-slot judge's message, or at a
`loop` / `map` `collection` an `invalid_union` carrying the same
message. The value-slot judge refused the same path, and the base arm
reported nothing. Text slots measured the same way with the text-slot
judge (a single brace, or a `{{ $User.Id }}` hole). Without the
exemption, the arm adds `node-config-refused-by-contract` at that same
path (ablation A2 below). **The addition:** at the `http` literal keys,
the contract also raises a type issue beside the `READ_AS_WRITTEN_PARAM`
one. Only the carve-out had kept that type issue from reaching the arm
first, so the arm now prefers the marked issue (ablation A3).
- **A4, lint and docs: HOLDS.** No rule in `packages/lint/src` restates
the carve-out: the lint calls `flowNodeConfigRefusals`. A grep of
`content/docs`, `skills` and `docs` for a token at a typed key finds
nothing. Control: the same pattern finds the spec test's `limit:
'{page.size}'`. The only hits are release-owned `CHANGELOG.md` entries,
including `@objectstack/spec`'s `18.0.0-next.0` entry describing the
carve-out. They are not edited here; the changeset carries the FROM →
TO.
**At the doors, after the change** (throwaway probes through the shipped
`AutomationEngine.registerFlow` and `@objectstack/lint`
`validateStackExpressions`, on a rebuilt `dist` at `f88ed85f6`):
- The probe is refused at `config.limit`.
- `limit: 10` registers.
- Each token at a value slot (`fields.title`, `collection`, `recordId`)
and at a text slot (`notify` `title`, `http` `url`) draws exactly one
finding, the slot judge's located one.
- `http` `timeoutMs: '{t}'` draws one finding, in the literal's words.
**Who is affected.** The census was a TS-AST pass. It statically
evaluated every builtin node literal's `config` in `examples/`,
`packages/` and `apps/` at base, plus hotcrm at `0f84c9c`: 1010 nodes,
244 of them carrying a `{…}` token, 266 partly dynamic. It compared the
base arm with the new one and found 0 verdict changes. Control: a
synthetic `get_record` with `limit: '{x}'` moves.
## Tests (at `f88ed85f6`, the head this PR opens on)
- `@objectstack/spec`, the whole `local` project in four locked runs:
`src/automation` + `src/migrations` 40 files / 1769 tests; `src/ui
src/ai src/security src/integration src/identity src/marketplace
src/studio src/qa src/meta-spelling` 170 / 5824; `src/data src/kernel
src/conversions` 191 / 5691 (+1 todo); `src/system src/api src/contracts
src/shared scripts` + the root files 268 / 6998. All passed. `repo`
project: `scripts/step18-rationale-merge.test.ts` 1 / 9 passed.
- `@objectstack/spec` `typecheck`: exit 0. `tsc --noEmit`,
`check:scripts-typecheck` and `check:test-typecheck` all pass, and the
test layer is held in its debt ledger.
- `@objectstack/service-automation`: 185 files / 2450 tests passed;
`typecheck` exit 0.
- `@objectstack/lint`: 138 files / 6683 tests passed.
- New pins in `flow-builtin-node-config-values.test.ts`:
- the probe, at `nodes.1.config.limit`, in the contract's words;
- the probe through `defineStack`, in the ADR-0112 envelope;
- a literal of the right type, unchanged;
- every spelling (`{x}`, `{{ x }}`, `${x}`) at seven typed keys;
- one token at each of six value slots, reported once, by the value-slot
judge;
- one token at each of six text slots, reported once, by the text-slot
judge;
- `http` `timeoutMs` and whole `headers`, refused once, in the literal's
words.
The old CONTROL "a token value in a typed slot is never refused for its
pre-interpolation type" is removed: it pinned the carve-out.
- New pin in `config-parse.test.ts`: `get_record` `limit: '{x}'` is
refused at registration (`refused at \`limit\` by the get_record
contract`) and at the run (`config.limit`), and `limit: 10` registers.
## The negative control and two ablations (one-time proofs, not
committed)
Each leg ran through `scripts/ablation-replace.mjs` in wrap mode under
`os-verify-lock`. Each anchor's count dropped and the blob changed. A
shell trap restored by absolute path, and each restore was proved: blob
equals HEAD `7199602764fe`, and `git diff HEAD` is empty. No `dist` leg
was needed, because the spec pins import the source by relative path.
- **A1, the carve-out restored (the negative control).** Expected: the
probe pins go red. Observed: 3 failed / 52 passed: the probe, the
`defineStack` probe, and the every-spelling pin. ⚠ The first A1 attempt
was an **empty operation**: its replacement carried its own anchor, so
the anchor count stayed 1 → 1, and the tool refused before running the
test. It was re-run with a two-line anchor, and the marker count on disk
read 1.
- **A2, the slot-judge exemption deleted.** 4 failed / 51 passed: both
"reported once" pins, plus the existing CONTROLs "a slot another judge
owns is not re-judged here" and "a token-free sibling does not shield a
refused value".
- **A3, the read-as-written preference deleted.** 1 failed / 54 passed:
the literal's-words pin.
- **Control, the restored tree:** 55 / 55 passed.
## Gates
`node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` at `f88ed85f6` derived 90 commands. Every
one was run, with its exit code captured before any pipe: 89 exited 0.
`pnpm check:dual-build-cjs-loads` exited 3, PREREQUISITE NOT MET (it
needs every package's `dist`, and this tree built only the
`service-automation` and `lint` closures), so it is NOT MEASURED.
Reconcile: `✓ dispatch-gates --ran: 90 derived famil(ies) accounted for
— 89 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3)`.
`pnpm --filter @objectstack/spec check:generated` reads "All 14
generated artifacts are up to date", and the build changed no tracked
file. `check-adr-0087-registration` accepts `not-required
(already-registered flow-builtin-node-config-values-refused)`.
The tree is 4 commits behind `origin/main` `404fc477e`. None of those
commits touches a file this diff touches, but three files the derivation
reads changed there: `scripts/pm/dispatch-gates.mjs`, its self-test, and
`scripts/tenant-audit-census.mjs`. CI's merge-ref run is the reading on
current `main`.
ESLint, as a measured narrowing: `eslint --no-inline-config --format
json` over the 8 changed files. The config ignores two of them (the
changeset and `registry.ts.template`, per eslint's own JSON). It linted
the other 6 with 0 errors and 0 warnings. `eslint.config.mjs` enables no
type-aware linting (no `parserOptions.project`), so this diff cannot
move a verdict on an untouched file.
## File surface
The claim's surface, plus three files that restate the arm's rationale.
Leaving them would publish a sentence this change makes false:
- `flow.zod.ts`: one comment;
-
`migrations/entries/semantic/18.flow-builtin-node-config-values-refused.ts`:
the D3 entry's `surface`, `replacement`, `reason` and
`acceptanceCriteria`;
- `migrations/registry.ts.template`: the step-18 fragment, order 85.
Both registry files feed the generated upgrade guide, which is
gitignored, so nothing generated is committed. The PR also adds one pin
to `service-automation`'s `config-parse.test.ts`, the `registerFlow`
door the dispatch asked to pin. No consumer fixture asserted the old
carve-out: the census found 0 moved verdicts, and the only test that
pinned it was the spec CONTROL above.
## Acceptance notes
- **A co-located type issue at a slot its judge refuses keeps the
judge's verdict first.** Measured once: a `screen` `title` written as a
`{ dialect: 'template', source: 'Hi {name}' }` envelope. The contract
types it as a string, and the text-slot judge refuses the token. The arm
exempts the path, as the carve-out did. The type refusal appears once
the token is fixed.
- **One more rule finding now reaches the arm.** A token used to hide
it: a `notify` `templateData` with no `template` (its rule refuses that
whatever the values hold).
- **The changeset's level.** It is `minor`, following the convention
#21898's own changeset used for this arm's accept-set narrowing. The
line is `18.0.0-next.*` in pre mode.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01BRQ9Mp1yH7R1az6MaUM18H)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent 572ecfb commit ca0bdbc
8 files changed
Lines changed: 339 additions & 103 deletions
File tree
- .changeset
- packages
- services/service-automation/src/builtin
- spec/src
- automation
- 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 | + | |
| 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 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
210 | 210 | | |
211 | 211 | | |
212 | 212 | | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
213 | 227 | | |
214 | 228 | | |
215 | 229 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
12 | 12 | | |
13 | 13 | | |
14 | 14 | | |
15 | | - | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
16 | 18 | | |
17 | | - | |
18 | | - | |
19 | | - | |
20 | | - | |
| 19 | + | |
| 20 | + | |
21 | 21 | | |
22 | 22 | | |
23 | | - | |
24 | | - | |
| 23 | + | |
| 24 | + | |
25 | 25 | | |
26 | 26 | | |
27 | 27 | | |
| |||
30 | 30 | | |
31 | 31 | | |
32 | 32 | | |
| 33 | + | |
| 34 | + | |
33 | 35 | | |
34 | 36 | | |
35 | 37 | | |
| |||
207 | 209 | | |
208 | 210 | | |
209 | 211 | | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
| 309 | + | |
| 310 | + | |
| 311 | + | |
| 312 | + | |
| 313 | + | |
| 314 | + | |
| 315 | + | |
| 316 | + | |
| 317 | + | |
| 318 | + | |
| 319 | + | |
| 320 | + | |
| 321 | + | |
| 322 | + | |
| 323 | + | |
| 324 | + | |
| 325 | + | |
| 326 | + | |
| 327 | + | |
| 328 | + | |
| 329 | + | |
| 330 | + | |
| 331 | + | |
| 332 | + | |
| 333 | + | |
| 334 | + | |
| 335 | + | |
| 336 | + | |
| 337 | + | |
| 338 | + | |
210 | 339 | | |
211 | 340 | | |
212 | 341 | | |
| |||
217 | 346 | | |
218 | 347 | | |
219 | 348 | | |
220 | | - | |
221 | | - | |
222 | | - | |
223 | | - | |
224 | | - | |
225 | | - | |
226 | | - | |
227 | | - | |
228 | | - | |
229 | | - | |
230 | | - | |
231 | | - | |
232 | | - | |
233 | | - | |
234 | | - | |
235 | | - | |
236 | | - | |
237 | | - | |
238 | | - | |
239 | | - | |
240 | 349 | | |
241 | 350 | | |
242 | 351 | | |
| |||
0 commit comments