Repository navigation
Commit 12b9daf
Part of #19939
Clause-②: yes (narrowing: a {…} token is refused at screen.defaults.*, a
field's defaultValue and screen.recordId; widening: screen.recordId
accepts a CEL value envelope it refused, and its published JSON Schema
and inferred type gain the envelope arm)
Pass 4, stage S2 of #19939 (stage plan `6096263424`, claim
`6107940231`), under ruling D on #11182 and ADR-0032 Decisions 2 and 3.
A `screen` node's three positions that hand a value to the client become
value slots: an object form's `defaults.*`, a field's `defaultValue`,
and an object form's `recordId`. Each takes a CEL value envelope, which
the executor evaluates in the run's scope before the screen goes on the
wire, or a literal. A single-brace `{…}` token is refused at all three.
#19939 stays open for stages S3 to S7, so this PR uses `Part of`.
## What lands
**The contract and the ledger (`@objectstack/spec`).**
- `ScreenConfigSchema.defaults` takes `FlowValueSlotSchema` per key, and
a field's `defaultValue` takes `FlowValueSlotSchema`. Each `describe()`
is restated.
- `ScreenConfigSchema.recordId` gains the envelope arm: a string, or a
CEL value envelope (a union, so the published JSON Schema and the
inferred type keep the string literal). A `{token}` in the string is
refused by the one value-slot judge. Any other value (`42`, `null`, a
list) is refused with a sentence naming what the id is for.
- `FLOW_NODE_EXPRESSION_PATHS` appends three `value` rows:
`screen.defaults.*`, `screen.fields[].defaultValue`, `screen.recordId`.
`LEDGER_DECLARED_NODE_CONFIG_SCHEMAS` carries `ScreenConfigSchema`, as
it carries `MapConfigSchema` since S1, because the descriptor offers
these positions with no marker. The ratchet finds the three markers on
the spec Zod channel and `fields[].visibleWhen` on the descriptor
channel alone.
- So the one judge (`flowNodeValueTemplateRefusals`) refuses a token at
these positions at `registerFlow` and `objectstack validate`, and the
envelope pass validates an envelope there. No second judge was added.
- **The screen sentence.** At a top-level value, the refusal ends with
what the client is served where the template served nothing. The table
is keyed by the ledger entry, like S1's callee sentence.
- `dropped-refinements.baseline.json` declares the three new dropped
refinements (`defaults.valueType`, `recordId`, `defaultValue`). The
header total moves 705 to 708.
**The executor (`@objectstack/service-automation`).**
- `screen-nodes.ts` resolves the three readers through S1's value-slot
resolver. `defaults` goes through `resolveValueSlotMap`. `defaultValue`
and `recordId` go through the new single-value `resolveValueSlot` in
`value-slot-map.ts`, which the map resolver now calls per key, so the
dispatch by shape is spelled once.
- An envelope goes to `AutomationEngine.evaluateValueEnvelope` in the
run's scope and context. A literal is served as written. There is no
`??` fallback to the interpolator.
- `recordId` must evaluate to a non-blank string. Anything else fails
the node there, naming its source; it is never `String(value)`.
- A screen that passes through without pausing goes on no wire and
evaluates nothing, as before.
- The descriptor's three descriptions, which said "Interpolates {var}
references", are restated.
**ADR-0087.** The step-18 D3 entry
`18.flow-value-slot-template-dialect-refused` is amended:
- the surface widens to the three screen positions;
- `replacement` and `reason` say what each position serves where the
template served nothing;
- `acceptanceCriteria` gains the screen check, and also the callee
caveat the S1 ACCEPT carried to this stage.
There is no D2: every whole-path spelling answers differently for an
absent value. `registry.ts` is generated at build from the entries and
gitignored at this base (`.gitignore:68`), so no regenerated registry
file is in the diff. The typecheck's `gen:migration-registry` reports it
current (422 semantic entries). The changeset marker is `not-required
(already-registered flow-value-slot-template-dialect-refused)`, and
`check-adr-0087-registration` accepts it.
**Sites migrated**, re-derived at HEAD with the stage-0 census
instrument. Positive control: the 3 tokened `screen.defaults` values and
the 1 tokened `screen.recordId` value it found at base read 0 tokened at
HEAD.
- `examples/app-crm` `crm_convert_lead_wizard`: 3 `defaults` values.
`company` is optional on a lead, so it is guarded.
- `content/docs/automation/flows.mdx`: the object-form example, the
value-slot list, and a new paragraph on what each screen position
serves.
- The `text-slot-template.test.ts` `recordId` fixture, and test fixtures
in spec and lint.
## Measured: what each position serves
Every reading is through the shipped `AutomationEngine`, on the screen
the paused run returns (`screen-value-slots.test.ts`).
| position | envelope evaluates to a value | absent, unguarded | guarded
`has(…) ? … : null` | the template, before |
|:--|:--|:--|:--|:--|
| `defaults.*` | the value: a record as a map, a list as a list | the
run fails at the screen, naming the source | `null`, served as a key
with value `null` | nothing: the key left the wire |
| `fields[].defaultValue` | the value | the run fails at the screen |
`null` | nothing |
| `recordId` | the string id | the run fails at the screen | the run
fails: `null` is not a record id | `String(resolved)`: nothing, or
`[object Object]` for a record |
What the client does with it, read from objectui at the pinned
`.objectui-sha` `20c6d351ad`:
- `initialRecord.ts` keeps a `null` prefill member as a value, and
`seedCreateValues` puts the caller layer over the object schema's
declared defaults. So `defaults: { k: null }` opens the field blank over
the object field's `defaultValue`, which applied when the key was
absent. The refusal and the D3 entry say: write that default in the
guard.
- `ScreenView.initialScreenValues` seeds a field from any `defaultValue`
that is not `undefined`, `null` included, and `resumeWith(values)`
submits it.
- `ObjectForm` with `mode: 'edit'` and no `recordId` reads no record,
and its save throws "Invalid form mode or missing record ID". This is
why `recordId` refuses a `null` rather than serving such a form. The
refusal tells the author to route around the screen with a `decision`.
## Premises checked against `f313fbcc99`
- **Assumption 1 is false for `recordId`.** At base an envelope there
was refused (`z.string()`): the parse refused it with `invalid_type`,
and the build doors with `node-config-refused-by-contract@recordId`. It
was not served verbatim. It held for `defaults` and `defaultValue`.
Measured by a probe through `flowNodeConfigRefusals`,
`flowNodeValueTemplateRefusals` and `ScreenConfigSchema`.
- The reader lines moved since the census: `ref()` was at `:234` and
used at `:247`, `defaults` was at `:250`, and `defaultValue` at `:313`.
- **Out of the claim's file surface, and why.**
- `flow-node-config-refusals.ts`. The build doors' value arm skipped
every issue at or inside a `value` slot. That would have stopped
refusing `recordId: 42` at `FlowSchema.parse`, `registerFlow` and
`objectstack validate`: a widening of the doors, because no other judge
reads a value slot's literal type. It now skips a `value` slot only
where the slot holds an envelope (the envelope pass owns it); a token is
skipped by the existing carve-out. Every other value slot's literal arm
refuses nothing but a token, so no other verdict moves.
- In the same file, `insideValueSlot` matched only map paths. A
malformed envelope at `fields[0].defaultValue` was also reported as
`node-config-key-required-by-rule` at `fields[0].defaultValue.source`.
It now matches `[]` segments too.
- Both are pinned (`flow-builtin-node-config-values.test.ts`), and
ablations 4 and 5 below turn them red.
- `schemaless-node-config.zod.ts`: `LEDGER_DECLARED_NODE_CONFIG_SCHEMAS`
gains `screen` (the S1 precedent for `map`).
- `value-slot-map.ts`: `resolveValueSlot`. It is not exported from the
package entry.
## Pins, and the ablations that turn them red
- `screen-value-slots.test.ts` holds 25 engine pins:
- refusal at `registerFlow`, at the executor's guard and at the envelope
pass, per position;
- evaluation on the served spec, with no `dialect` on the wire;
- absent and `null` per position;
- `recordId` refusing `null`, a map, a list and a blank string.
- In spec: contract pins (`builtin-node-config.test.ts`), ledger pins,
screen-sentence pins (`flow-value-slot-template.test.ts`), the build
doors' value arm, and the text-slot test re-judged in place.
`config-expression-ledger.test.ts` covers the ratchet.
- The lint hints flip by asking the judge, with no lint source change: 6
pins in `lint-flow-patterns.test.ts`.
- **Ablations**, each through `scripts/ablation-replace.mjs`: the anchor
hit once, the blob changed, and the restore was proven with blob equal
to HEAD and `git diff HEAD` empty. Each was run at `528f5ca363`.
1. `resolveValueSlot` never evaluates (`false &&
isExpressionEnvelopeShaped(value)`): 11 of the 25 engine pins went red,
the evaluation and absent/`null` pins. The refusal pins stayed green, as
expected.
2. `recordId` falls back to `String(id)`: the 5 `recordId` refusal pins
went red.
3. The `screen.recordId` ledger row renamed: 10 spec pins in 3 files
went red.
4. The value-arm carve-out back to whole slots: `screen {"recordId":42}
is refused at recordId` went red.
5. `insideValueSlot` back to map-only matching: the "slot another judge
owns" control went red. It showed the `node-config-key-required-by-rule`
double report.
6. The screen-sentence table emptied: 6 judge pins went red.
- Not ablated at dist level: the service-automation and lint pins read
`@objectstack/spec`'s `dist`. Ablations 3 and 6 turned the same
functions red at source.
## Measurements
- **Gates** at `d693848a79`. `dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived 115 commands from the 23 changed
paths. All 115 exited 0. `--ran` with the recorded exit codes reads 115
run, 0 NOT-MEASURED (a derived zero).
- This includes `check-adr-0087-registration`,
`check-changeset-no-major`, `check-empty-changeset`,
`check:doc-authoring`, `check:i18n-coverage`,
`check:examples-live-imports`, `check:nul-bytes`,
`check:type-check-debt`, and the spec gates `check:generated`,
`check:api-surface`, `check:export-origins`, `check:docs`,
`check:spec-changes`, `check:upgrade-guide`, `check:authorable-surface`
and `check:skill-examples`.
- On the first run, at `528f5ca363`, three gates exited 3 (`PREREQUISITE
NOT MET`): `check:skill-examples`, `check:dual-build-cjs-loads` and
`check:i18n-coverage`. They passed once the packages they read were
built, and passed again in the full second run.
- The dispatch also named `check:future-spec-major`, which exited 0.
There is no `check:migration-registry` script at this base.
- **Tests:**
- service-automation full suite at `528f5ca363`: 186 files, 2397 tests
pass;
- spec local project, 648 files, 19370 pass; spec repo project, 55
files, 971 pass;
- lint full suite, 137 files, 6422 pass;
- example-crm, 6 files, 46 pass.
- At `d693848a79`, after the second merge (which touched only `core`,
`mcp` and `metadata-protocol`): the 5 touched service-automation files
(109 tests), spec `src/automation` (1406) and the two touched lint files
(227) pass.
- Typecheck is green for spec, service-automation, lint and example-crm.
- **The CRM example through every judge.** `validateStackExpressions`
reports 0 issues, the flow lint's value-slot hints 0, and `registerFlow`
accepts `crm_convert_lead_wizard`. Its screens serve `{"name":"Acme"}`
for a lead with a company and `{"name":null}` for one without.
- **Semver.** `@objectstack/spec` and `@objectstack/service-automation`
are `major` (pre mode `next`, re-read at push). The `Clause-②` line
reads `yes (narrowing: …; widening: …)`, on this body and in the
changeset (patch round 1, after the contract review `6109203893`):
- the narrowing: a `{…}` token is refused at `screen.defaults.*`, a
field's `defaultValue` and `screen.recordId`;
- the widening: `screen.recordId` accepts a CEL value envelope that
`z.string()` refused, and its published JSON Schema (`anyOf: [string,
object]`) and inferred type gain the arm: `ScreenConfig['recordId']`
infers `string | AssignmentExpressionValue` where it inferred `string`.
- No `api-surface/**` or `export-origins/**` file moved.
`LEDGER_DECLARED_NODE_CONFIG_SCHEMAS` gains `screen`, which widens
`LedgerDeclaredNodeType` and `ReconciledNodeConfigType`; that is data in
existing exports, as S1's `map` was.
- **Size:** 23 files, +1185 / −145 against `main` (1330 changed lines).
## Acceptance notes
- **The executor's parse of a blank-`source` envelope at `recordId`**
returns the envelope arm's own sentence, without the
`VALUE_ENVELOPE_REFUSAL` lead: the union reports that arm's issue
directly. `registerFlow`'s envelope pass, which runs first, leads with
it.
- **A literal `recordId: ''`** still parses, as it did. Only an
evaluated id must be non-blank.
- **The Studio form.** The descriptor keeps `recordId` as `type:
'string'` and `defaults` as an open key-value map, so the designer
cannot author an envelope at these positions. Carrier: the objectui
companion card objectstack-ai/objectui#12101, which is for the seat to
extend. Sites there:
- `flow-node-config.ts:856`, whose `defaults` help text names
`{account_id}`;
- `previews/screen-spec.ts`, which interpolates `defaultValue` and
`defaults` with the single brace.
- `validate-flow-template-paths` still path-checks single-brace tokens
at declared value slots, which doubles the judge's finding. This is the
note passes 1 to 3 carried; S7 removes it.
- `flows.mdx` named a `subflow` input as a single-brace position in two
places, stale since S1. Both are fixed here, in the file this PR already
edits: the surrounding examples are now `notify` recipients and `http`
bodies.
- **Skills.** `skills/**` carries no text about these three positions
(measured, zero hits), so this stage owes no #22585 pointer.
- **hotcrm:** NOT MEASURED, as for the earlier stages.
---
_Generated by [Claude
Code](https://claude.ai/code/session_016njDy8ozy9B9Ns5Y8kAWEK)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent 8558668 commit 12b9daf
26 files changed
Lines changed: 1199 additions & 154 deletions
File tree
- .changeset
- content/docs
- automation
- references/automation
- examples/app-crm/src/flows
- packages
- lint/src
- runtime/src/domains
- 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 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
230 | 230 | | |
231 | 231 | | |
232 | 232 | | |
233 | | - | |
234 | | - | |
235 | | - | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
236 | 239 | | |
237 | 240 | | |
238 | 241 | | |
| |||
295 | 298 | | |
296 | 299 | | |
297 | 300 | | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
| 309 | + | |
| 310 | + | |
| 311 | + | |
| 312 | + | |
| 313 | + | |
| 314 | + | |
| 315 | + | |
| 316 | + | |
| 317 | + | |
| 318 | + | |
| 319 | + | |
298 | 320 | | |
299 | | - | |
300 | | - | |
| 321 | + | |
| 322 | + | |
| 323 | + | |
301 | 324 | | |
302 | 325 | | |
303 | 326 | | |
| |||
645 | 668 | | |
646 | 669 | | |
647 | 670 | | |
648 | | - | |
649 | | - | |
650 | | - | |
| 671 | + | |
| 672 | + | |
| 673 | + | |
651 | 674 | | |
652 | 675 | | |
653 | 676 | | |
| |||
2027 | 2050 | | |
2028 | 2051 | | |
2029 | 2052 | | |
2030 | | - | |
2031 | | - | |
| 2053 | + | |
| 2054 | + | |
2032 | 2055 | | |
2033 | 2056 | | |
2034 | 2057 | | |
| |||
2069 | 2092 | | |
2070 | 2093 | | |
2071 | 2094 | | |
2072 | | - | |
| 2095 | + | |
2073 | 2096 | | |
2074 | 2097 | | |
2075 | 2098 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
77 | 77 | | |
78 | 78 | | |
79 | 79 | | |
80 | | - | |
81 | | - | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
82 | 83 | | |
83 | 84 | | |
84 | 85 | | |
| |||
230 | 231 | | |
231 | 232 | | |
232 | 233 | | |
233 | | - | |
234 | | - | |
| 234 | + | |
| 235 | + | |
235 | 236 | | |
236 | 237 | | |
237 | 238 | | |
| |||
242 | 243 | | |
243 | 244 | | |
244 | 245 | | |
245 | | - | |
| 246 | + | |
246 | 247 | | |
247 | 248 | | |
248 | 249 | | |
| |||
264 | 265 | | |
265 | 266 | | |
266 | 267 | | |
267 | | - | |
| 268 | + | |
268 | 269 | | |
269 | 270 | | |
270 | 271 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
107 | 107 | | |
108 | 108 | | |
109 | 109 | | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
110 | 115 | | |
111 | | - | |
| 116 | + | |
112 | 117 | | |
113 | 118 | | |
114 | 119 | | |
| |||
127 | 132 | | |
128 | 133 | | |
129 | 134 | | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
130 | 138 | | |
131 | | - | |
132 | | - | |
133 | | - | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
134 | 142 | | |
135 | 143 | | |
136 | 144 | | |
| |||
0 commit comments