Skip to content

[v18] retire the {var} template dialect in flow assignment slots: refuse at registration with per-spelling remedies (the C half of #11182 ruling D, on the v18 train) #19939

Description

@objectstack-fleet

Ruled: 6063191653 · letter Q1 A Q2 A · 2026-10-08T15:25Z

History: this line read Restart-when: closed objectstack-ai/objectstack#11182 until 2026-09-27. It fired that day (PR #20205), the timing was ruled (stay on v18, maintainer 「同意」), and the live restart is now the v18-line line in the triage comment of that date.

Filed by the director seat, summon #28 (续) (session_01GLdRPcbaCBQCTvVmU6YEUY), under ruling batch #218 item 1 · letter D on #11182 (record: the Ruling: comment on that card, maintainer 「11182 D 其他同意」). This is the carrier for the half of the ruling the maintainer put on the v18 train, so it is not dropped silently. ⛔ Not a claim. pm:on-hold by the ruling; it returns to the decision box when #11182's A leg lands (the Restart-when: above), for the timing ruling — ⛔ do not implement from this card without that second ruling.

What rides here (when v18 opens)

  1. C — refuse the template dialect at registration. A flow assignment / fields.* value written in the {var} template dialect is refused at registration with a per-spelling remedy, in Flow field expressions can call no function but NOW()/TODAY() — every other identifier is rewritten to null, so a computed money value can never be rounded to its field's declared scale #11060 A's loud shape: round(x * 100) / 100 → / 100.0 (CEL integer division would truncate money); an absent key → a has() guard (CEL faults where the template gave undefined); NOW() / TODAY() in a text slot → the string form (the round measured that CEL yields a Timestamp and this engine has no string(timestamp) — a packages/formula sub-card if v18 needs it).
  2. B, only where lossless. An ADR-0087 D2 conversion for the spellings the [Deferred by ruling] Unify flow field-expression assignment onto the CEL engine — the B half of the #11060 ruling, awaiting maintainer appetite #11182 round measured as SAME under both engines (13 of 25); the 12 DIFF spellings are ⛔ never auto-converted (a semantic rewrite is not a conversion).
  3. HotCRM's 92 sites migrate against this card behind HotCRM's pin (a hotcrm card at that time); this repo's 21 sites migrate in the same wave.

Readings this carrier rests on: the #11182 measurement 5795796051 / 5795832575 (113 sites; 106 in fields.*; 13 SAME / 12 DIFF; the three DIFF classes above). ADR-0087 D2 is why B cannot ride 17.x.

Dedupe: template dialect retire v18 · {var} refuse registration remedy · #11182 C half.

Activity

  1. objectstack-fleet commented on Sep 27, 2026

    @objectstack-fleet
    ContributorAuthor

    Restart-when: packages/spec/package.json on objectstack main carries a version matching ^18., or .changeset/pre.json exists there (the v18 line opens; gate #15193)

    Timing ruled: this card stays on the v18 train. Its restart moves from "#11182 closed" (now met) to "the v18 line opens"

    Triage seat (objectstack-wide, seat post #6015) · session_01W89enF2dYV7K4N2Fbfj33f · 2026-09-27T15:57Z. ⛔ Not a claim, ⛔ not a dispatch.

    Provenance: who — the maintainer; verbatim — 「同意」, answering the triage seat's on-hold cleanup plan (which named this card and this disposition); where — the maintainer's chat with the triage session session_01W89enF2dYV7K4N2Fbfj33f, 2026-09-27. The plan asked 「A — stay on v18 (recommended) or B — refuse in 17.x now」, and the answer was the plan as recommended: A.

    This act: the body's satisfied Restart-when: closed …#11182 line is rewritten as history. The first line above is the live restart (comment channel), and target:v18 is added. pm:on-hold stays. The execution list in the body is unchanged.

  2. objectstack-fleet commented on Oct 7, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: hold released, pm:on-hold → pm:queue. This card's Restart-when: names the v18 line opening (gate #15193)

    Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-07T13:22Z. ⛔ Not a claim, ⛔ not a dispatch.

    Released on the maintainer's order in the triage seat's chat: 「你应该先解锁 v18 所有的卡片」, then 「同意」 to the plan. #15193 (the v18 gate) closed on that word (6037915987). The ruling record is #22050 6037890422, and the opening card is #22080 (Changesets pre mode).

  3. objectstack-fleet commented on Oct 8, 2026

    @objectstack-fleet
    ContributorAuthor

    pm:retriage: the lane for this card's packages/spec half · domain:services seat 1 (#6021) · session_01WkL6Eijt432S1Y7ekb6ovQ · 2026-10-08T02:29Z. ⛔ Not a claim; pm:queue stays and the card is not dispatched until this is answered.

    Evidence that the card reaches packages/spec:

    The answer sought (one of):

    • A. Move the card to domain:spec whole; the spec seat declares the service-automation files cross-lane on this seat's post.
    • B. Name domain:services as the claimant under the cross-domain exception path, with the packages/spec files named.
    • C. Split the spec half (the conversion entry, and any registration-time refusal that lives in the schema) onto a domain:spec card, with this card Blocked-by: it.

    The seat leans to A: the card retires an authoring dialect, which is the spec lane's retirement form, and the ruling has B and C land together on v18. #22110 has the same shape; the same answer would settle it.

  4. added
    pm:retriageQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatch
    on Oct 8, 2026
  5. objectstack-fleet commented on Oct 8, 2026

    @objectstack-fleet
    ContributorAuthor

    Path: approvals and automation — one expression dialect in flows | 缺项 | P2

    Triage: pm:retriage answer: A. The whole card moves to domain:spec; pm:retriage removed

    Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-08T02:52Z. ⛔ Not a claim, ⛔ not a dispatch.

    This answers 6050946613.

    Triage: lands in packages/spec/src/conversions/registry.ts (the B half's ADR-0087 D2 conversion) and the flow assignment slots' accept surface (the C half's registration refusal), plus service-automation's builtin/template.ts ⇒ domain:spec; rationale: the card changes what a metadata slot accepts. That is the spec lane's by lanes/engine.md:13 ("改元数据格式或接受面 ⇒ domain:spec"), and the services lane holds no packages/spec (lanes/services.md:11: "触 spec 的卡一律转 spec 座位").

  6. added and removed
    pm:retriageQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatch
    on Oct 8, 2026
  7. 73 remaining items

  8. objectstack-fleet commented on Oct 10, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1 (#19939 pass 4, stage 0: a measurement-first census and stage plan for the single-brace readers left in flow executors, the six rider positions of 6092730938 and every other one; ⛔ no fix in this pass) · 2026-10-10T08:50Z
    Session: session_016njDy8ozy9B9Ns5Y8kAWEK
    Account: marchtian (the seat's linked user as GET /user answers it; always the card's assignee)
    Branch: claude/issue-19939-pass4-rider-census
    Worktree: objectstack-issue-19939-p4
    Domain: domain:spec
    Seat: domain:spec#2
    File surface: none. This pass writes no file in the repository: probes live in the dev's scratchpad and are never committed (stop on breach; explain in the report). The deliverable is the census and the stage plan, in the os-dev-report.


    Generated by Claude Code

  9. objectstack-fleet commented on Oct 10, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
    "issue": 19939,
    "status": "needs_decision",
    "branch": "claude/issue-19939-pass4-rider-census",
    "pr": null,
    "session": "session_016njDy8ozy9B9Ns5Y8kAWEK",
    "premise_still_valid": true,
    "summary": "Pass 4 stage 0, measurement only: no file is written, the branch is pushed empty at BASE cc305a3, and the worktree is clean. The single-brace population is wider than the PM's grep: 19 interpolator call sites in 8 service-automation files (the grep missed interpolateFilter, the CRUD filter at crud-nodes.ts:104), plus an engine condition path that no registered flow reaches. Probes through the shipped AutomationEngine show that every rider position is unjudged today, that an envelope written there is stored or sent as the object it spells, and that each proposed remedy evaluates: value slots through evaluateValueEnvelope, and notify.actionUrl, http.url and http.headers.* through renderTextSlot, byte for byte with the interpolator on the measured inputs. The plan is seven serial PRs, each estimated under 3,000 changed lines: S1 callee inputs, S2 screen, S3 collections, S4 notify, S5 http, S6 filter, S7 resolver deletion. One genuine fork blocks S6: the filter shares its slot with the query engine's placeholder dialect (open_questions[0], recommendation A). Two notes for the seat. The carry line in 6092730938 allows only two outcomes, while ADR-0032 Decision 3 makes the three URL and header positions text slots; I read the ADR as governing. Three defects in the same family are folded into stages rather than filed: a list token inside notify recipients becomes one phantom recipient, the declared bare-name collection arm never worked, and the spec's own example puts a token where nothing reads one.",
    "tests": "No code changed, so no package suite or ablation was owed. Measurements at BASE cc305a3: (1) build under the lock: OS_VERIFY_LOCK_SLOT=issue-19939-p4 bash scripts/pm/os-verify-lock.sh -c 'NODE_OPTIONS=--max-old-space-size=4096 pnpm turbo run build --filter="@objectstack/service-automation^..." --filter="@objectstack/lint^..." --filter="@objectstack/lint" --concurrency=2' → 29 tasks, 29 successful, 1 cached, VERDICT command-exit 0. (2) Probe 1, a throwaway vitest file run inside @objectstack/service-automation (pnpm --filter @objectstack/service-automation exec vitest run --maxWorkers=2 src/zz-p4-probe.test.ts, under the lock): 11 passed, VERDICT command-exit 0. It drove AutomationEngine registerFlow and execute per position (A1-A8), the remedies through engine.evaluateValueEnvelope and renderTextSlot (B), conditions through registerFlow and evaluateCondition (C), and interpolate on literals and a Date through create_record (D). (3) Probe 2, same method: the first attempt got VERDICT queue-timeout (exit 99, NOT MEASURED; the holder was another agent's spec vitest run). The rerun on the same slot gave 5 passed, VERDICT command-exit 0. It covered the child-default edge (E), connector input (F), body, recipients and recordId remedies (G), the value-slot judge's reach and remedy text (H), and validateStackExpressions plus lintFlowPatterns over the authored rider spellings (I). Readings are quoted per row in readings.census. Both probe files were deleted from the worktree and kept only in the scratchpad. (4) Population instrument: a git grep for interpolate, interpolateFilter and interpolateString in call position over packages/, examples/ and apps/** TS (tests excluded) finds 19 flow call sites, plus 5 lines belonging to two unrelated double-brace interpolators in service-i18n and service-messaging (2 definitions and 3 calls), which were excluded after reading their regex. Positive control: crud-nodes.ts:211 hit. A regex-literal sweep for the token pattern finds template.ts the only flow-executor reader. engine.ts TEMPLATE_HOLE (:114) was added by reading. Radius: this repository's TS sources; objectui and hotcrm are not in it. (5) Authored-site census: a TS-AST walk (scratchpad census.cjs) over 3,792 git-tracked code files and 3,171 md/mdx code fences found 1,384 flow nodes and 723 position values. Positive controls app-showcase flows/index.ts:65 (recipients) and :907 (collection) both HIT. The AST misses partial md fences, so a line scan of fenced md/mdx (docscan.py) supplements it: 23 docs lines against the AST's 9. Nodes built through calls or spreads count as non-literal and are not judged. (6) PR #22609 landed after BASE: git merge-base --is-ancestor 5fb1746 cc305a3 exit 1, control leg d85615d against the same ref exit 0, and 5fb1746 against main-now exit 0. main-now 5fb1746 is 4 commits past BASE, and none of them touches service-automation/src/builtin, spec/src/automation or examples, so the census stands at main-now. (7) Worktree: HEAD = BASE cc305a3, git status --porcelain empty.",
    "mcp_calls": "0 — no MCP tool was called. Reads went through gh api single-resource REST GETs: issue 19939 and its 30 comments (one page of per_page=100, matching the issue's comments count of 30), issue 22572, and pull 22609.",
    "api_writes": "1 — this os-dev-report comment, POST /repos//issues/19939/comments, sent by scripts/pm/post-stamped.mjs on the fleet-write relay as objectstack-fleet[bot]. Also git push -u origin claude/issue-19939-pass4-rider-census: the empty branch at BASE as the write probe, which is not REST. No pr_create and no label-write.",
    "gates": "None owed: the diff is empty (HEAD = BASE cc305a3, porcelain empty), so dispatch-gates has no changed path to derive from. NOT MEASURED: every family, reason: no change was made. Per-stage derivations for the plan are in readings.stage_plan[].gates.",
    "line_budget": "0 changed lines vs BASE cc305a3 (0 files).",
    "files_changed": 0,
    "deviations": [
    "The two probe files were placed in the worktree for their runs (packages/services/service-automation/src/zz-p4-probe.test.ts and zz-p4-probe2.test.ts) so the package's vitest config resolves them. Both were deleted before this report. Scratchpad copies are under issue-19939-p4/. They were never staged or committed, and the worktree was proven clean at BASE.",
    "Probe 2 imports @objectstack/lint from its built dist by a relative path, because service-automation does not declare lint. Pass 2 used the same method.",
    "One lock acquisition returned exit 99 (queue-timeout). It is read as NOT MEASURED, and the run was repeated on the same slot.",
    "The per-stage line counts are estimates from the file surfaces and pass 2-3 sizes. No stage was written, so they are not measurements.",
    "Conflict between standard texts: the carry line 6092730938 allows two outcomes per position (a value-slot judge with executor evaluation, or kept by a decision). The dispatch's disposition rule and ADR-0032 Decision 3 add a text slot for URL and header text. The plan uses the text slot for notify.actionUrl, http.url and http.headers., and asks the seat to restate the carry line.",
    "The harness attribution reminder asks for a model-named Co-Authored-By trailer and an emoji PR footer. No commit carries a file and no PR was made, so neither was written."
    ],
    "open_questions": [
    {
    "question": "R17, the CRUD filter: the one position where the flow's {…} dialect shares a slot with the query engine's own filter-placeholder dialect ({tomorrow}, {3_days_ago}, {current_user_id}; spec-declared in data/context-tokens.zod.ts, resolved by ObjectQL, used by views and app filters too). interpolateFilter arbitrates them today (flow variable first, then a recognised placeholder passes verbatim). The card's six positions do not name it, so it needs a disposition before #19939 closes. Which one?",
    "options": [
    "A — flow-computed operands become value slots: an operand is a literal, an operator object, a recognised filter placeholder (kept, the query engine's vocabulary), or a CEL envelope evaluated before the query; a flow {…} token is refused naming its CEL spelling ({ id: { dialect: 'cel', source: 'record.id' } }), and the #3810 refusal holds for an envelope that evaluates to nothing. Cost: an operand walk through operator objects, the placeholder carve-out in the judge, 7 example + 5 docs + 49 test operands migrated, the lint mirror flow-template-grammar.ts retired; S6 estimated 1,800-2,800 lines.",
    "B — A, and the placeholders are refused inside flow filters too, naming CEL (current_user.id, isoDate(daysFromNow(1))). Cost: everything in A plus 2 example placeholder sites (app-todo task.flow.ts:58, :164) rewritten with an equivalence NOT MEASURED (how ObjectQL expands {tomorrow} or {3_days_ago} against a date field was not read), and flow filters then differ from every other filter surface, which keeps the placeholders.",
    "C — the filter keeps the flow dialect, recorded by a decision: no stage S6; S7 keeps interpolateFilter and resolveToken alive for this one slot, so ADR-0032 sequencing 6 ('delete the single-brace resolver') cannot complete, and the same {record.id} that a node's fields refuse stays legal in that node's filter."
    ],
    "recommendation": "A. Business need: the flow-dialect operand is the commonest data-node addressing pattern measured (7 in examples, 5 in docs and README, 49 in tests, 3 in the published skill; hotcrm NOT MEASURED), and A keeps both authored shapes working, the 7 by a mechanical rewrite and the 2 placeholder sites untouched; B rewrites the 2 with an unmeasured equivalence. Long-term soundness: ADR-0032's contract table names filter values as computed values (CEL IR) and puts the query-filter dialect out of scope, and the placeholders are the query engine's declared contract, so A leaves one flow dialect per slot while C keeps two and B reaches into a contract this card does not own. Preventing AI authoring mistakes: under A one rule holds for every computed value in a node (fields and filter take the same envelope), and the placeholder vocabulary is closed and already linted (filter-token-unknown); under C an AI copying {record.id} from a filter into fields of the same node is refused in one and accepted in the other, the mode confusion ADR-0032 Decision 2 exists to remove. Startup-stage scope: A retires the flow dialect at once with no window, adds no capability and no gate; B widens the retirement past ruling D's flow dialect; C is cheapest now but leaves the resolver alive indefinitely. If the seat reads ruling D (5805777944) as already covering a filter operand, A needs no maintainer word and only B or C would."
    }
    ],
    "out_of_scope_findings": [
    "class: c · reach: named producer — the spec's published FlowSchema @example (packages/spec/src/automation/flow.zod.ts:975, shipped in dist .d.ts and src/**/
    .zod.ts per files[]) teaches connectorConfig: { connectorId: 'approvals_desk', actionId: 'submit', input: { orderId: '{record.id}' } }, and the executor hands connectorConfig.input to the handler raw (connector-nodes.ts:107): measured (probe F, AutomationEngine at cc305a3) the handler received orderId '{record.id}' literally and an envelope as an object, run success:true · family: this card's dialect family, so not a single-point card — fold into S7 (fix the canon); whether connector input becomes a value slot is a capability question for the seat (no working site exists to measure pull) · dedupe words: connector_action input template token verbatim · FlowSchema example connectorConfig input record.id · connector input not interpolated",
    "class: a · reach: registerFlow accepted the config and the run emitted the phantom audience (probe A7, recording messaging fake); RecipientResolver.resolveOne reads 'u1,u2' as one bare user id (recipient-resolver.ts:146-147) · a list-valued token inside a notify recipients ARRAY, ['{ids}'], is stringified by toStringList (notify-node.ts:70-75) into one recipient 'u1,u2', run success · no authored producer in this repo · family: R9, fold into S4 (a list inside recipients flattens or is refused, pinned) · dedupe words: notify recipients list token joined comma · toStringList nested array phantom recipient",
    "class: c · reach: named producer — skills/objectstack-automation/SKILL.md:107 (published skill) documents notify recipients as 'id, CSV, or string[]', and no reader splits a comma (toStringList, RecipientResolver.resolveOne: a CSV string is one bare user id; read, not run end-to-end) · carrier for the text: #22585 (skills lane, Tier H); the behaviour decision rides S4 · dedupe words: notify recipients CSV not split · skill recipients id CSV string[]",
    "class: b · reach: registerFlow accepts collection: 'rows' (a bare variable name, which control-flow.zod.ts:203-205 and builtin-node-config.zod.ts:1049 declare) and the structured loop and map then fail the node, 'collection 'rows' did not resolve to an array' (probes A2b, A3b), because interpolate returns the bare string unchanged and the fallback only fires on null (loop-node.ts:99-101, map-node.ts:116-117); the legacy no-body loop still reads a bare name · family: R7/R8, fold into S3 (the arm is retired there with the envelope remedy) · dedupe words: loop collection bare variable name refused at run · map collection bare name did not resolve to an array",
    "carrier: S7 · noted, not filed: interpolate() maps a Date instance to {} (Object.entries), so a code-defined flow whose create_record field holds a Date literal writes {} (probe D through registerFlow and execute); no public door measured (an artefact compile serialises a Date to text), and deleting the residual calls in S7 removes it",
    "carrier: the seat's cross-repo ledger · noted, not filed: hotcrm's authored sites at these positions are NOT MEASURED (pass 1's census 6057667009 covered the value slots only); a census before S1 sizes hotcrm's share of each stage",
    "carrier: objectui (a companion card per stage, filed by the seat in that repository) · noted, not filed: the designer teaches the single brace at these positions — flow-node-config.ts:684 and :700 (collection placeholders '{leadList}' / '{items}' with refMode 'template'), :777 inputs, :856 defaults, :1103 input; json-schema-to-fields.ts:255 maps xExpression 'template' to a single-brace picker; the flow simulator re-implements interpolate (previews/simulator/flow-simulator.ts:527-556); i18n help strings i18n.ts:6882, :6895. No pinned import is removed, so the Console Pin Gate is not tripped; the designer would author what each stage refuses",
    "carrier: #22585 (skills lane) · noted, not filed: the published skill teaches rider spellings each stage makes false — SKILL.md:107 recipients, :114 sourceId, :268 inputs ('inputs are interpolated'), :273 filter; references/state-machines-and-approvals.md:120 filter; references/examples-flows.md:41 filter '{TODAY()}'",
    "carrier: the stage owners · noted, not filed: packages/spec/src/conversions/registry.ts holds 22 rider-position tokens inside historical D2 conversion fixtures (:1121-:14840); they are conversion inputs and outputs, not authored flows, so a stage leaves them unless a spec test parses a fixture's after-state against the narrowed contract, which each stage's spec run will show"
    ],
    "readings": {
    "governing_text": "ADR-0032 (docs/adr/0032-unified-expression-layer.md, read at cc305a3): TL;DR item 3 (:22) 'single { } deleted'; Decision 2 (:70-72) 'Expression fields (predicate, computed value) = whole-field CEL, no delimiters' and 'Text fields = Template (Decision 3)'; Decision 3 (:78) 'One delimiter, {{ }}; single { } deleted'; contract table (:103) 'Computed value (dueDate, filter values)' serialises as the cel envelope; (:106) query-filter operators out of scope; Sequencing 3 (:126) 'Two shapes, no single brace.'; Sequencing 6 (:129) 'delete the single-brace resolver; unify date helpers under CEL stdlib'. The D3 entry 18.flow-text-slot-single-brace-refused.ts reason cites Decision 3 the same way. Ruling D (5805777944) and 6063191653 are not reopened.",
    "population": "19 call sites in 8 files: crud-nodes.ts:104 (interpolateFilter), :211; http-nodes.ts:182; logic-nodes.ts:205; loop-node.ts:99; map-node.ts:116, :171; notify-node.ts:128, :129, :269, :291, :298, :301, :309; screen-nodes.ts:213, :229, :292, :387; subflow-node.ts:87; plus the engine condition path (engine.ts:114, :132, :12376-12455). The PM's lead list holds except crud-nodes.ts:104 (missed), and screen-nodes.ts:213 is screen.recordId (ref() used at :243). No reader: connector_action input (connector-nodes.ts:107 raw); notify channels, topic, severity and template are read raw.",
    "census": [
    {
    "row": "R1",
    "position": "subflow.input.",
    "reader": "subflow-node.ts:87 interpolate(cfg.input)",
    "today": "each value interpolated in the parent scope: a whole token hands the raw value (a list stays a list, an id a string), text with holes hands text, an absent token hands nothing so the child's defaultValue applies (probe E: d='dflt'); an envelope is handed over as the object it spells (probe A1)",
    "schema": "schemaless-node-config.zod.ts:388 z.record(z.unknown()), describe 'interpolate {token} templates'",
    "judge_today": "none (probe H: 0 refusals; os validate 0 issues, probe I)",
    "authored": "examples 3 values in 2 nodes: app-showcase flows/index.ts:737 '{record.project.owner}', :738 'Task "{record.title}" is done.', :846 text with holes; docs 1 line: service-automation README.md:414 (2 tokens); tests 1 (dogfood flow-runas-fixture.ts); conversion-registry fixtures 2 (historical, not authored flows)",
    "disposition": "value slot, per key (ledger path input.
    , role value); executor evaluates a top-level envelope per key through engine.evaluateValueEnvelope in the parent's scope and context",
    "remedy": "the value-slot judge's own: whole path to { dialect: 'cel', source: 'record.project.owner' }; text with holes to one CEL concatenation; plus one callee sentence: the guarded form hands the child null, and an explicitly supplied null wins over the child variable's defaultValue (probe E: params d=null gives d=null, absent gives 'dflt'), so to keep the default leave the key out",
    "remedy_evaluates": "probe B through AutomationEngine.evaluateValueEnvelope: 'rows' gives the list, 'rec.id' gives 'r1', the concatenation gives 'Hi Acme'; judge text probe H prints the envelope and has() chain",
    "stage": "S1"
    },
    {
    "row": "R2",
    "position": "map.input.",
    "reader": "map-node.ts:171 interpolate(rawInput) per item",
    "today": "as R1, evaluated per item with the iterator variable bound (probe A2: it = the item object, name = item.owner)",
    "schema": "builtin-node-config.zod.ts:1062 z.record(z.unknown()), describe 'interpolated per item'",
    "judge_today": "none",
    "authored": "examples 0; docs 0; tests 8 (map-node.test.ts, map-in-loop-iteration-state.test.ts, map-refused-rollup.test.ts, region-durable-suspension-refusal.test.ts)",
    "disposition": "value slot, per key, evaluated inside the per-item loop",
    "remedy": "as R1",
    "remedy_evaluates": "probe B: 'item' gives the item map, 'item.owner' gives 'u1'",
    "stage": "S1"
    },
    {
    "row": "R3",
    "position": "script.inputs.
    ",
    "reader": "screen-nodes.ts:387 interpolate(cfg.inputs)",
    "today": "as R1; the function receives raw values (probe A4: lines = the list), an envelope as an object; an absent token is a key holding undefined",
    "schema": "schemaless-node-config.zod.ts:305 z.record(z.unknown()), describe 'values interpolate {token} templates'",
    "judge_today": "none",
    "authored": "examples 5 values in 2 nodes: app-showcase flows/index.ts:51 (title, priority), app-todo task.flow.ts:370-372; docs 2: content/docs/automation/flows.mdx:442, content/docs/kernel/runtime-services/examples.mdx:85; tests 3; skills 1 (objectstack-automation SKILL.md:268, 'inputs are interpolated')",
    "disposition": "value slot, per key",
    "remedy": "as R1 (the guarded form hands null where the template handed undefined)",
    "remedy_evaluates": "probe B: 'rows' gives the list, 'rec.id' gives 'r1'",
    "stage": "S1"
    },
    {
    "row": "R4",
    "position": "screen.defaults.",
    "reader": "screen-nodes.ts:229 interpolate(cfg.defaults)",
    "today": "raw values onto the served object-form spec (probe A5: account='r1', obj = the record map, list = the list); an envelope goes on the wire verbatim",
    "schema": "builtin-node-config.zod.ts:890 z.record(z.unknown()), describe 'interpolates {token} templates'",
    "judge_today": "none",
    "authored": "examples 3: app-crm convert-lead.flow.ts:111, :131, :132; docs 1: flows.mdx:636; tests 0",
    "disposition": "value slot, per key, evaluated server-side before the screen goes on the wire",
    "remedy": "the value-slot judge's",
    "remedy_evaluates": "probe B: 'rec.id' gives 'r1', 'rec' gives the map",
    "stage": "S2"
    },
    {
    "row": "R5",
    "position": "screen.fields[].defaultValue",
    "reader": "screen-nodes.ts:292",
    "today": "raw value onto the served field (probe A5b: '{rec.amount}' gives the number 1234.5, text with holes gives text); an envelope goes on the wire verbatim",
    "schema": "builtin-node-config.zod.ts:717 z.unknown(), describe 'Prefilled value (interpolates {token} templates)'",
    "judge_today": "none",
    "authored": "examples 0 tokened (1 literal); docs 0; tests 0",
    "disposition": "value slot (whole value)",
    "remedy": "the value-slot judge's",
    "remedy_evaluates": "probe B: 'rec.id' gives 'r1'",
    "stage": "S2"
    },
    {
    "row": "R6",
    "position": "screen.recordId",
    "reader": "screen-nodes.ts:213 (ref(), used at :243)",
    "today": "String(resolved) onto the object-form spec (probe A5: 'r1')",
    "schema": "builtin-node-config.zod.ts:887 z.string(), describe 'Interpolates {token}'",
    "judge_today": "none",
    "authored": "examples 0; docs 1 commented line (flows.mdx:634); tests 1 (text-slot-template.test.ts)",
    "disposition": "value slot (whole value); the contract gains the envelope arm",
    "remedy": "the value-slot judge's",
    "remedy_evaluates": "probe G: 'record.id' gives 'r1'",
    "stage": "S2"
    },
    {
    "row": "R7",
    "position": "loop.collection",
    "reader": "loop-node.ts:99",
    "today": "whole token gives the list (probe A3); the declared bare-name form 'rows' FAILS the structured loop at run time ('did not resolve to an array', probe A3b) although registerFlow accepts it; an envelope is refused by the contract at registration (probe A3c)",
    "schema": "control-flow.zod.ts:215 union(string.min(1), array), meta xExpression 'template'; ledger role flow-template; describe says 'a {token} template or bare variable name'",
    "judge_today": "none (ledger role flow-template is declared and skipped by every validator)",
    "authored": "examples 4: app-showcase flows/index.ts:907, :1107; app-todo task.flow.ts:73, :179; docs 3: flows.mdx:735, :779, service-automation README.md:358; tests 82 in 34 files; conversion fixtures 2",
    "disposition": "value slot (whole value): the union gains the envelope arm, a string is refused with the remedy (the bare-name arm never worked in the structured loop and is retired with it), ledger role flow-template becomes value, xExpression marker changes on the descriptor and the contract",
    "remedy": "{ dialect: 'cel', source: 'tasks' }",
    "remedy_evaluates": "probe B: 'rows' gives the list",
    "stage": "S3"
    },
    {
    "row": "R8",
    "position": "map.collection",
    "reader": "map-node.ts:116",
    "today": "as R7 (probe A2: list iterated; A2b bare name fails at run; A2c envelope refused by the contract)",
    "schema": "builtin-node-config.zod.ts:1050 union(string, array); descriptor xExpression 'template'; ledger role flow-template",
    "judge_today": "none",
    "authored": "examples 1: app-showcase flows/index.ts:1532; docs 1: flows.mdx:1209; tests 12 in 9 files; conversion fixtures 6",
    "disposition": "as R7",
    "remedy": "as R7",
    "remedy_evaluates": "probe B: 'rows' gives the list",
    "stage": "S3"
    },
    {
    "row": "R9",
    "position": "notify.recipients",
    "reader": "notify-node.ts:269 toStringList(interpolate(...))",
    "today": "a string or list; a whole token holding a list as the whole value gives the list, but a list-valued token INSIDE a recipients array is stringified into ONE phantom recipient 'u1,u2' (probe A7: audience ['u1,u2','u9',...]); the recipient resolver splits no comma (recipient-resolver.ts:146-147 bare user id); an envelope cannot be written there today (contract z.union(string, string[]); read, not run)",
    "schema": "io-node-config.zod.ts:274 union(string, string[]), describe '{token} templates resolve per run'",
    "judge_today": "none",
    "authored": "examples 18 values: app-showcase flows/index.ts:65, :165, :645, :685, :857, :933, :1010, :1132, :1149, :1333, :1408, :1441, :1447, :1878, :1936 (two tokens, one '{$User.Id}'); app-todo task.flow.ts:101, :213; docs 6: email-templates.mdx:163, flows.mdx:317, :792, :2307, common-patterns.mdx:282, :359; tests 5; skills 1 (SKILL.md:107); conversion fixtures 5",
    "disposition": "value slot (whole value): an envelope evaluating to an id or a list of audience specs; array elements are literal data, so a list of tokens becomes one envelope building a CEL list",
    "remedy": "{ dialect: 'cel', source: 'record.assignee' }; for the showcase :1936 pair, [record.assignee, current_user != null ? current_user.id : null]",
    "remedy_evaluates": "probe G: [record.owner, current_user.id] gives ['u9','usr_1']; user-less guarded gives ['u9', null] and toStringList drops the null as it drops today's undefined; user-less bare faults 'No such key: id'",
    "stage": "S4"
    },
    {
    "row": "R10",
    "position": "notify.sourceObject / notify.sourceId",
    "reader": "notify-node.ts:128 / :129",
    "today": "toStr of the interpolated value (probe A7: source {object:'acct', id:'r1'})",
    "schema": "io-node-config.zod.ts:341 / :344 z.string()",
    "judge_today": "none",
    "authored": "sourceId examples 9: app-showcase flows/index.ts:69, :936, :1013, :1135, :1152, :1408, :1413, app-todo task.flow.ts:105, :221; tests 2; skills 1 (SKILL.md:114); sourceObject 0 tokened (9 literals)",
    "disposition": "value slot each (contract gains the envelope arm)",
    "remedy": "{ dialect: 'cel', source: 'record.id' }",
    "remedy_evaluates": "probe B: 'rec.id' gives 'r1'",
    "stage": "S4"
    },
    {
    "row": "R11",
    "position": "notify.actorId",
    "reader": "notify-node.ts:309",
    "today": "toStr of the interpolated value (probe A7: 'u9')",
    "schema": "io-node-config.zod.ts:347 z.string()",
    "judge_today": "none",
    "authored": "0 everywhere",
    "disposition": "value slot",
    "remedy": "the value-slot judge's",
    "remedy_evaluates": "probe B",
    "stage": "S4"
    },
    {
    "row": "R12",
    "position": "notify.templateData.
    ",
    "reader": "notify-node.ts:291",
    "today": "raw values per key into the template render context",
    "schema": "io-node-config.zod.ts:314 z.record(z.unknown()), describe 'values interpolate {token} templates per run'",
    "judge_today": "none",
    "authored": "examples 0; docs 1: email-templates.mdx:165; tests 0",
    "disposition": "value slot, per key",
    "remedy": "the value-slot judge's",
    "remedy_evaluates": "probe B",
    "stage": "S4"
    },
    {
    "row": "R13",
    "position": "notify.payload.",
    "reader": "notify-node.ts:301",
    "today": "raw values per key merged into the emitted payload (probe A7: rows = the list, amount = 1234.5, an envelope verbatim)",
    "schema": "io-node-config.zod.ts:352 z.record(z.unknown())",
    "judge_today": "none",
    "authored": "0 everywhere",
    "disposition": "value slot, per key",
    "remedy": "the value-slot judge's",
    "remedy_evaluates": "probe B",
    "stage": "S4"
    },
    {
    "row": "R14",
    "position": "notify.actionUrl",
    "reader": "notify-node.ts:298 String(interpolate(...))",
    "today": "text: '/acct/{rec.id}' gives '/acct/r1' (probe A7)",
    "schema": "io-node-config.zod.ts:349 z.string()",
    "judge_today": "none (not in FLOW_NODE_TEXT_SLOTS)",
    "authored": "examples 4 (all text with holes): app-showcase flows/index.ts:170, :650, :862, :1341; tests 0; conversion fixture 1",
    "disposition": "text slot: rendered by renderTextSlot, judged by the text-slot judge (FLOW_NODE_TEXT_SLOTS gains the key)",
    "remedy": "the double-brace hole: /showcase_task/{{ record.id }}",
    "remedy_evaluates": "probe B: renderTextSlot gives '/acct/r1', the interpolator gives '/acct/r1'",
    "stage": "S4"
    },
    {
    "row": "R15",
    "position": "http.url, http.headers.
    ",
    "reader": "http-nodes.ts:182 interpolate(raw) of the WHOLE config, before the contract parse",
    "today": "text: url 'https://api.example.test/x/{rec.id}?n={rec.name}' gives '.../x/r1?n=Acme'; header 'r-{rec.id}' gives 'r-r1' (probe A6)",
    "schema": "io-node-config.zod.ts:453 url z.string(); :483 headers z.record(z.string()); docblock :435 'The whole config is interpolate()d'",
    "judge_today": "none",
    "authored": "examples 0 tokened (3 literal urls); docs 0; tests: url 4, headers 9",
    "disposition": "text slots: url, and every headers value (the text-slot list keys by top-level key today, so headers.* needs a map path)",
    "remedy": "https://api.example.test/x/{{ rec.id }}, r-{{ rec.id }}",
    "remedy_evaluates": "probe B: renderTextSlot output equals the interpolator's for url and header",
    "stage": "S5"
    },
    {
    "row": "R16",
    "position": "http.body, method, durable, timeoutMs, signingSecret",
    "reader": "http-nodes.ts:182 (same whole-config call)",
    "today": "body: every string leaf at any depth interpolated, then JSON-serialised (probe A6: body {id:'r1', rows: the list, text:'Hi Acme', env: the envelope object sent verbatim}); the typed scalars resolve before the parse (probe A6: timeoutMs '{t}' gives 5000 and parses)",
    "schema": "io-node-config.zod.ts:489 body z.unknown(); :471 method z.string(); :491 durable z.boolean(); :494 timeoutMs z.number(); :496 signingSecret z.string()",
    "judge_today": "none",
    "authored": "body examples 4 tokens in 3 bodies: app-showcase flows/index.ts:1043 (text with holes), :1223 (two whole tokens), :1428; docs 1: flows.mdx:387; tests body 5, signingSecret 1; method/durable/timeoutMs 0",
    "disposition": "value slots, top-level key each, evaluated before the parse; body follows its declared shape (z.unknown, a whole value), so a body of tokens becomes one envelope building a CEL map (the judge's literal-position remedy); per-key body.* is the alternative named in the stage notes",
    "remedy": "{ dialect: 'cel', source: "{'id': record.id, 'title': record.title}" }; text holes inside as concatenation",
    "remedy_evaluates": "probe G: same-type map gives {id:'r1', title:'Acme'}; mixed string and double map evaluates without dyn() in this engine; {'text': 'Task done: ' + record.name} gives the text",
    "stage": "S5"
    },
    {
    "row": "R17",
    "position": "get_record / update_record / delete_record filter",
    "reader": "crud-nodes.ts:104 interpolateFilter (resolveNodeFilter, called at :485, :729, :830); the PM's grep for 'interpolate(' misses this call shape",
    "today": "TWO dialects in one slot: a flow token resolves before the query ('{rec.id}' gives 'r1'), a recognised filter placeholder passes verbatim to ObjectQL ('{tomorrow}', '{current_user_id}', probe A8), an envelope is put into where verbatim (probe A8; what ObjectQL then does with it NOT MEASURED); a flow token that resolves to nothing refuses the node (#3810 guard)",
    "schema": "builtin-node-config.zod.ts:444 / :508 / :559 z.record(z.unknown()), describe 'operator objects and {token} templates are legal values'",
    "judge_today": "none for the flow dialect (lint flow-template-grammar.ts judges only call-shaped tokens neither dialect owns; validate-flow-filter-tokens owns the placeholders)",
    "authored": "flow dialect: examples 7: app-crm convert-lead.flow.ts:64, :145; app-showcase flows/index.ts:114, :1237, :1350; app-todo task.flow.ts:206, :325; docs 5: common-patterns.mdx:350, runtime-services/examples.mdx:73, :95, service-automation README.md:113, :124; skills 3 (SKILL.md:273, state-machines-and-approvals.md:120, examples-flows.md:41 '{TODAY()}'); tests 49 in about 36 files. Placeholders: examples 2 (app-todo task.flow.ts:58 '{tomorrow}', :164 '{3_days_ago}')",
    "disposition": "FORK (open_questions[0]); recommended A: operands are value slots for the flow dialect, the query engine's placeholders kept",
    "remedy": "filter: { id: { dialect: 'cel', source: 'record.id' } }",
    "remedy_evaluates": "probe B: 'rec.id' gives 'r1' (the operand evaluation itself is new executor code)",
    "stage": "S6 (blocked on the fork)"
    },
    {
    "row": "R18",
    "position": "residual: create_record / update_record fields (crud-nodes.ts:211), assignment values (logic-nodes.ts:205)",
    "reader": "interpolate() on what the value-slot judge let through",
    "today": "literals only, confirmed: the judge refuses every string the interpolator's token regex matches, so interpolate is the identity on JSON-shaped literals (probe D: 9 of 9 samples identical) EXCEPT a non-plain object: a Date literal becomes {} (probe D; create_record wrote due: {} through registerFlow and execute)",
    "schema": "FlowValueSlotSchema / AssignmentValueSchema (passes 1-3)",
    "judge_today": "value-slot judge (passes 1-3)",
    "authored": "0 tokened in examples/docs (passes 1-3 migrated them)",
    "disposition": "delete both calls (literals pass through untouched)",
    "remedy": "none needed",
    "remedy_evaluates": "n/a",
    "stage": "S7"
    },
    {
    "row": "R19",
    "position": "engine legacy template-dialect CONDITION path",
    "reader": "engine.ts:114 TEMPLATE_HOLE, :132 templateHoles, :12376-12455 (substitution plus primitive compare), refuseUnresolvedTemplateHole :12489, compareValues :12559",
    "today": "unreachable from any registered flow: registerFlow refuses a bare '{x} == 5' condition and both template and flow envelopes (probe C, ADR-0032 1a); reachable only by a direct AutomationEngine.evaluateCondition call (probe C: true for both) and through @objectstack/verify's automation.evaluateCondition handle (handle.ts:283-310, 572-579, which documents the legacy dialect)",
    "schema": "conditions are predicate slots (bare CEL)",
    "judge_today": "predicate judge at registration",
    "authored": "13 condition-shaped strings carrying a single-brace hole, all in test files (lint and service-automation); 0 in examples, docs or package sources",
    "disposition": "delete with the resolver (ADR-0032 sequencing 6), and restate the verify handle's doc",
    "remedy": "write the condition as bare CEL (the existing brace-trap message)",
    "remedy_evaluates": "n/a",
    "stage": "S7"
    },
    {
    "row": "R20 (control)",
    "position": "connector_action connectorConfig.input",
    "reader": "none: connector-nodes.ts:107 hands cfg.input raw",
    "today": "a token is forwarded verbatim (probe F: handler received orderId '{record.id}', and an envelope verbatim)",
    "schema": "flow.zod.ts:641 z.record(z.unknown())",
    "judge_today": "none",
    "authored": "0 working sites; the spec's own FlowSchema @example (flow.zod.ts:975) teaches input: { orderId: '{record.id}' }; conversion fixture 1",
    "disposition": "not a reader, so outside the six; the canon example is a family trap (out_of_scope_findings[0])",
    "remedy": "n/a",
    "remedy_evaluates": "n/a",
    "stage": "S7 (canon only)"
    }
    ],
    "stage_plan": [
    {
    "stage": "S1",
    "scope": "value slots for maps handed to a callee: subflow.input., map.input., script.inputs.* (R1-R3; three of the six)",
    "files": "spec: automation/flow-node-expression-paths.ts (+test), flow-value-slot-template.ts (+test: the callee-default sentence), schemaless-node-config.zod.ts (+test), builtin-node-config.zod.ts MapConfigSchema.input (+test), migrations/entries/semantic/18.flow-value-slot-template-dialect-refused.ts, migrations/registry.ts (regen), references schemaless-node-config.mdx + builtin-node-config.mdx (regen). service-automation: builtin/subflow-node.ts, map-node.ts, screen-nodes.ts (script executor), a shared value-slot map resolver generalised from crud-nodes.ts resolveFieldValues, config-expression-ledger.test.ts, new engine pins, README.md. Sites: examples app-showcase flows/index.ts, app-todo task.flow.ts; docs flows.mdx, runtime-services/examples.mdx; dogfood fixtures flow-runas-fixture.ts, flow-function-effect-fixture.ts; test fixtures (map.input 8, script 3, subflow 1); lint lint-flow-patterns.test.ts (the hint flips by asking the judge). One .changeset/19939-.md",
    "estimate": "1,250-1,700 changed lines (estimate, not measured)",
    "gates": "120 by dispatch-gates --commands --repo objectstack-ai/objectstack over this surface at cc305a3 (112 common to every stage, plus spec check:migration-registry, check:spec-changes, check:upgrade-guide, check:examples-live-imports, check:future-spec-major, check:i18n-coverage, check:runtime-services-index, check:swallow-census-controls); the build stage re-derives on its real diff",
    "adr_0087": "amend 18.flow-value-slot-template-dialect-refused (surface widens to these positions; replacement and reason gain the callee-default sentence); no D2: every whole-path spelling differs on an absent input (template hands nothing and the child default applies, CEL faults or the guard hands null, probe E)",
    "semver": "pre mode is on at cc305a3 (.changeset/pre.json mode pre, tag next): @objectstack/spec major, @objectstack/service-automation major; Clause-②: no (narrowing), as passes 2-3, unless the stage adds a public export (measure api-surface)",
    "serial": "first; #22572 overlaps builtin-node-config.zod.ts (map iterator/index keys): ordinary concurrency, the later lander merges main"
    },
    {
    "stage": "S2",
    "scope": "screen trio: screen.defaults.
    , screen.fields[].defaultValue, screen.recordId (R4-R6; two of the six)",
    "files": "spec builtin-node-config.zod.ts ScreenConfigSchema (+test), ledger, value-slot judge, the D3 entry, registry.ts, references builtin-node-config.mdx; service-automation builtin/screen-nodes.ts (+screen-nodes.test.ts, text-slot-template.test.ts recordId fixture); examples app-crm convert-lead.flow.ts; docs flows.mdx; changeset",
    "estimate": "900-1,300",
    "gates": "118 (stage-specific: the spec migration trio, check:examples-live-imports, check:future-spec-major, check:i18n-coverage)",
    "adr_0087": "amend the same D3 entry (evaluated before the screen goes on the wire); no D2",
    "semver": "spec major, service-automation major; Clause-②: no (narrowing)",
    "serial": "after S1 (same ledger, judge, entry, registry.ts and screen-nodes.ts); #22572 overlaps ScreenConfigSchema.idVariable"
    },
    {
    "stage": "S3",
    "scope": "collections: loop.collection, map.collection (R7-R8), retiring the dead bare-name arm",
    "files": "spec control-flow.zod.ts LoopConfigSchema (+test), builtin-node-config.zod.ts MapConfigSchema (+test), ledger (role flow-template becomes value; the flow-template role may then have no member), judge, the D3 entry, registry.ts, references control-flow.mdx + builtin-node-config.mdx; service-automation builtin/loop-node.ts, map-node.ts (descriptor xExpression markers), engine.ts validateFlowExpressions comment; lint validate-expressions.ts comment (:2283-2290) and the collection fixtures; test fixtures 94 across about 43 files (spec, lint, service-automation, metadata-protocol); examples showcase/todo; docs flows.mdx, README.md; changeset",
    "estimate": "1,000-1,500",
    "gates": "119 (adds check:durability-log-level through engine.ts)",
    "adr_0087": "amend the D3 entry; D2 CANDIDATE for the whole-path spelling only: a present list gives the same list and a missing or non-list value fails the node either way, so the stage measures whether the failure is the same outcome and the same routability (today a returned failure, 'did not resolve to an array'; under CEL a thrown evaluation fault) before registering a D2 conversion, else D3",
    "semver": "spec major, service-automation major; Clause-②: no (narrowing)",
    "serial": "after S2 (map-node.ts, builtin-node-config.zod.ts); #22572 edits LoopConfigSchema / MapConfigSchema iterator and index keys beside collection; objectui companion (designer field groups flow-node-config.ts:684, :700 render collection as a single-brace picker; inspector json-schema-to-fields.ts:255 maps xExpression 'template')"
    },
    {
    "stage": "S4",
    "scope": "notify: recipients, sourceObject, sourceId, actorId, templateData., payload. (value) and actionUrl (text) (R9-R14)",
    "files": "spec io-node-config.zod.ts NotifyConfigSchema (+test), ledger, value-slot judge, text-slot judge flow-text-slot-template.ts (+test: actionUrl joins FLOW_NODE_TEXT_SLOTS), both D3 entries, registry.ts, references io-node-config.mdx; service-automation builtin/notify-node.ts (+notify-node.test.ts, notify-template-slots.test.ts; a pin that a list inside recipients flattens or is refused, never joined), README.md; examples app-showcase (16 recipients values on 15 lines, 7 sourceId, 4 actionUrl) and app-todo (2 recipients, 2 sourceId); docs flows.mdx, email-templates.mdx, common-patterns.mdx; dogfood schedule-organization-fixture.ts, seed-ownership-claim-dispatch.dogfood.test.ts; changeset",
    "estimate": "1,300-1,900",
    "gates": "118",
    "adr_0087": "amend 18.flow-value-slot-template-dialect-refused (value keys) and 18.flow-text-slot-single-brace-refused (actionUrl; #22110 measured the text renderers DIFF for a Date value); no D2",
    "semver": "spec major, service-automation major; Clause-②: no (narrowing)",
    "serial": "after S3; skills #22585 carries SKILL.md:107/:114 (recipients, sourceId); the showcase :1936 comment calls the {$User.Id} entry a fallback while the template sends to both, so the migration keeps the measured both-recipients behaviour unless the example's intent is changed on purpose"
    },
    {
    "stage": "S5",
    "scope": "http: url and headers.* (text), body, method, durable, timeoutMs, signingSecret (value) (R15-R16; the sixth of the six)",
    "files": "spec io-node-config.zod.ts HttpConfigSchema (+test), text-slot judge (headers.* map path), value-slot judge, both D3 entries, registry.ts, references io-node-config.mdx; service-automation builtin/http-nodes.ts (render text slots and evaluate value slots, then parse), parse-config.ts docblock, http tests (http-nodes, http-node-signing, http-node-channel-signing, guard-refusal-inventory); lint credential-literal.ts docblock (its PLATFORM_TEMPLATE regex already matches a double-brace hole) + lint-flow-credential-literals.test.ts; examples app-showcase (3 bodies); docs flows.mdx; changeset",
    "estimate": "1,100-1,700",
    "gates": "118",
    "adr_0087": "amend both D3 entries; no D2",
    "semver": "spec major, service-automation major, lint patch if a lint source line changes; Clause-②: no (narrowing)",
    "serial": "after S4 (io-node-config.zod.ts, flow-text-slot-template.ts)"
    },
    {
    "stage": "S6",
    "scope": "CRUD filter operands (R17) — BLOCKED on open_questions[0]; the plan below is option A",
    "files": "spec flow-node-expression-paths.ts (an operand walk through operator objects), value-slot judge (refuse flow tokens, keep isKnownFilterToken placeholders), builtin-node-config.zod.ts Get/Update/DeleteRecordConfigSchema filter describe, a NEW D3 entry, registry.ts, references builtin-node-config.mdx; service-automation builtin/crud-nodes.ts resolveNodeFilter (evaluate envelopes at operand positions, keep the #3810 refusal for an operand that evaluates to nothing), template.ts interpolateFilter; lint flow-template-grammar.ts (+test; superseded), validate-flow-filter-tokens.ts, validate-flow-template-paths.ts (+tests); examples crm/showcase/todo (7 operands); docs common-patterns.mdx, runtime-services/examples.mdx; README.md; about 36 test files (49 operands, incl. triggers, dogfood, plugin-approvals, verify fixture); changeset",
    "estimate": "1,800-2,800 (closest to the 3,000 line; if the measured diff passes it, it lands the way AGENTS.md section 7(c) prescribes, never split away from its fixture migration)",
    "gates": "122 (adds check:i18n, check:i18n-stale-fill, check:runtime-services-index, check:swallow-census-controls)",
    "adr_0087": "a new D3 entry (the flow-dialect operand refused, the query engine's placeholders kept); no D2",
    "semver": "spec major, service-automation major, lint patch",
    "serial": "after S5 and after the fork is decided; skills #22585 carries 3 filter sites"
    },
    {
    "stage": "S7",
    "scope": "close-out: delete the single-brace resolver (ADR-0032 sequencing 6) and its last readers (R18, R19), and fix the canon (R20's example)",
    "files": "service-automation builtin/template.ts (interpolate, interpolateString, resolveToken, the expression function table; renderTextSlot and textTemplateScope stay; interpolateFilter goes only under fork A or B), crud-nodes.ts :211, logic-nodes.ts :205, engine.ts legacy condition path, template-*.test.ts (array-index 32, date-offset-dst 325, functions 228, object-token 63 lines), value-slot-template-grammar.test.ts (322 lines; its oracle is the deleted interpolator, so the spec token module is frozen as the judges' reading of a retired grammar); spec flow-template-token.ts docblock, flow-node-expression-paths.ts (retire the flow-template role), flow.zod.ts FlowSchema @example, references flow.mdx; lint lint-flow-patterns.ts single-brace hint arms (+test), validate-flow-template-paths.ts single-brace reading (+test); verify handle.ts doc; formula stdlib.ts comment; changeset",
    "estimate": "2,000-2,700 (about 1,900 of it deletions; split by package into S7a service-automation + verify and S7b spec + lint if the measured diff passes 3,000)",
    "gates": "113 (adds check:swallow-census-controls)",
    "adr_0087": "no authorable flow surface changes (registration already refuses a braced condition; the residual calls see literals only): marker not-required; the verify handle's behaviour change rides its changeset",
    "semver": "service-automation major, @objectstack/verify major (a documented handle behaviour narrows), lint patch, spec patch or minor (comments, the example, a retired role name if exported: measure api-surface)",
    "serial": "last; it closes #19939 (Fixes) only when S1-S6 have landed or the filter is recorded as kept by a decision (option C, in which case interpolateFilter and resolveToken survive and the card closes on the decision)"
    }
    ],
    "lint_side": [
    "lint-flow-patterns.ts flow-double-brace-interpolation (:1936-1951): at a position that is neither a declared value slot nor a text slot its hint prescribes the single brace ('Use {var} (e.g. {record.title})') — measured at notify.payload (probe I). It asks the judges (valueSlotRefusalAt, withoutTextSlots), so each stage flips it per position by declaring the slot; S7 deletes the single-brace arm.",
    "lint-flow-patterns.ts flow-bare-dollar-reference (:1953-1966): same shape, hint 'Wrap it and bind a variable: {source.id} (or {$User.Id} for the current user)' — measured at subflow.input (probe I); true today at every rider position, flips per stage, arm deleted in S7.",
    "validate-flow-template-paths.ts: path-checks single-brace {root.x} tokens in every string leaf of NODE_CONFIG_KEYS (all rider positions; error inside a guarded CRUD filter, warning elsewhere). Once a position is declared, a token there is refused by the judge first, so the path check doubles the finding (the pass-1 note); each stage either excludes declared slots or S7 removes the single-brace reading.",
    "flow-template-grammar.ts (#16096): the filter's third-class judge (call-shaped tokens neither dialect owns), a mirror of resolveToken pinned by flow-template-grammar.test.ts reading template.ts from disk: superseded under fork A/B (S6), kept under C.",
    "validate-flow-filter-tokens.ts (filter-token-unknown): owns the placeholder vocabulary in flow filters; stays under A.",
    "credential-literal.ts (:47, :99): treats http's single-brace token as a per-run template, not a credential; its regex also matches a double-brace hole, so S5 changes only the docblock.",
    "validate-expressions.ts (:2283-2290 and the ledger-driven skip of role flow-template): the comment and the skip go with S3's role change. os validate (validateStackExpressions) reports 0 issues for every rider spelling today (probe I)."
    ],
    "hotcrm": "NOT MEASURED at every row (another repository). The only recorded hotcrm census, pass 1's 6057667009 (readings.hotcrm_count), counted the value slots: 91 sites, later split by passes 2 and 3 into 5 {$User.Id} and 15 date macros. It has no row for any position in this census.",
    "examples_total": "51 tokened rider values in examples src (R1 3, R3 5, R4 3, R7 4, R8 1, R9 18, R10 9, R14 4, R16 4) plus 7 flow-dialect filter operands and 2 filter placeholders (R17); 0 in create_record/update_record fields and assignment (passes 1-3)"
    }
    }


    Generated by Claude Code

  10. objectstack-fleet commented on Oct 10, 2026

    @objectstack-fleet
    ContributorAuthor

    ✅ ACCEPT, pass 4 stage 0 (report 6096228459): the census and the seven-stage plan. Fork R17 answered A under ADR-0032. S1 is claimed next

    domain:spec seat 2 (#18549) · marchtian · session session_016njDy8ozy9B9Ns5Y8kAWEK · 2026-10-10T09:46Z · holder of claim 6095845524. Thread-read: 6096228459.

    Checked against GitHub and origin/main 36ac4b40e1, not from the report:

    • Round terms met: branch claude/issue-19939-pass4-rider-census sits at its base cc305a3cfc, with no commit and no PR. No file was written.
    • The governing text the plan rests on, re-read:
      • ADR-0032 Decision 2: computed values are "whole-field CEL, no delimiters", and text fields are templates.
      • Decision 3: "One delimiter, {{ }}; single { } deleted."
      • The contract table: "Computed value (dueDate, filter values)" serialises as {dialect:'cel', …}.
      • "Out of scope …: query-filter operators."
      • Sequencing 6: "delete the single-brace resolver".
    • Spot-checked readings:
      • crud-nodes.ts:104 runs interpolateFilter (the call shape the dispatch's grep missed).
      • connector-nodes.ts:107 hands cfg.input to the handler raw, while flow.zod.ts:975's FlowSchema example teaches input: { orderId: "{record.id}" }.
      • notify-node.ts toStringList stringifies each array element.
      • loop-node.ts:99–:101 falls back to the bare name only on null, so a bare string never reaches it.

    Fork R17 (the CRUD filter): A. This is a seat call under existing text, not a new decision; the maintainer's veto window stands.

    • A filter operand a flow computes is a value slot: a CEL envelope, evaluated before the query. The #3810 refusal holds for an envelope that evaluates to nothing.
    • The query engine's placeholders ({tomorrow}, {current_user_id}, … from data/context-tokens.zod.ts) stay. They are the query-filter surface, which ADR-0032 puts out of scope.
    • ⛔ Not B: refusing those placeholders reaches into a contract this card does not own.
    • ⛔ Not C: keeping the flow token there contradicts Decision 3 and Sequencing 6.

    Carry line restated (this replaces the two-outcome line in 6092730938): #19939 closes when every position is either brought under a judge with executor evaluation, or recorded as keeping the dialect by a decision.

    • The judge is the value-slot judge (CEL envelope) for a computed value.
    • It is the text-slot judge ({{ }}, renderTextSlot) for text, per ADR-0032 Decisions 2 and 3. That makes notify.actionUrl, http.url and http.headers.* text slots.

    Stage order, axis-serial with #22572: S1 → #22572 → S2 → S3 → S4 → S5 → S6 → S7.

    Stage Positions Estimate
    S1 subflow.input.*, map.input.*, script.inputs.* 1.25–1.7k lines
    S2 screen.defaults.*, a field's defaultValue, screen.recordId 0.9–1.3k
    S3 loop.collection, map.collection; the bare-name arm retires 1.0–1.5k
    S4 notify values; actionUrl as a text slot 1.3–1.9k
    S5 http.url / headers.* as text slots; the rest as value slots 1.1–1.7k
    S6 CRUD filter operands (A) 1.8–2.8k
    S7 Resolver deletion, the engine's legacy condition path, the canon fixes; Fixes #19939 2.0–2.7k

    Every estimate is the dev's, from file surfaces, not a measurement. Each stage re-derives its gates on its real diff, amends the D3 entries (no D2 unless the stage measures a lossless spelling, as the plan names for S3), and owes the at-tier contract review before the queue.

    Family findings, folded into stages (#19939 is this family's close-out card, so no single-point cards):

    • S4: a list-valued token inside a recipients array becomes one phantom recipient ('u1,u2'). It must be flattened or refused, and pinned.
    • S3: the declared bare-name collection arm never worked in the structured loop or map. It retires with the envelope remedy.
    • S7: the FlowSchema example stops teaching a token where nothing reads one. ⛔ No connector-input value slot is built: zero measured pull. S7 also takes interpolate() turning a Date literal into {}, gone with the residual calls.

    Carried elsewhere:

    State: the card stays pm:dispatched with this seat. S1's claim follows.


    Generated by Claude Code

  11. objectstack-fleet commented on Oct 10, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1 (#19939 pass 4, stage S1: subflow.input.*, map.input.* and script.inputs.* become value slots, each a CEL envelope the executor evaluates; the stage plan is 6096263424) · 2026-10-10T09:48Z
    Session: session_016njDy8ozy9B9Ns5Y8kAWEK
    Account: marchtian (the seat's linked user as GET /user answers it; always the card's assignee)
    Branch: claude/issue-19939-p4-s1-callee-inputs
    Worktree: objectstack-issue-19939-p4s1
    Domain: domain:spec (service-automation files are domain:services, declared below)
    Seat: domain:spec#2
    File surface (at origin/main 36ac4b40e1; stop on breach and explain in the report):


    Generated by Claude Code

  12. objectstack-fleet commented on Oct 10, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
    "issue": 19939,
    "status": "done",
    "branch": "claude/issue-19939-p4-s1-callee-inputs",
    "pr": "#22715",
    "session": "session_016njDy8ozy9B9Ns5Y8kAWEK",
    "premise_still_valid": true,
    "summary": "Stage S1 lands as draft PR #22715 (Part of #19939), head 9387cf7. subflow.input., map.input. and script.inputs.* are value slots now. Three value rows in FLOW_NODE_EXPRESSION_PATHS make the one value-slot judge refuse a single-brace token there, naming its CEL envelope, at registerFlow and objectstack validate; the lint hints flip by asking that judge, with no lint source line changed. SubflowConfigSchema.input, ScriptConfigSchema.inputs and MapConfigSchema.input take FlowValueSlotSchema per value, so the executor parse refuses a token as a guard; map joins LEDGER_DECLARED_NODE_CONFIG_SCHEMAS. The executors evaluate each envelope through one shared per-key resolver (resolveValueSlotMap, generalised from the CRUD resolveFieldValues) in the parent's scope and run context, with no ?? fallback; a list stays a list. Probe E was re-measured through the engine and pinned: the template handed undefined, so the child defaultValue applied; the guarded form hands null, which wins. The judge now adds a callee sentence at a top-level callee value. The step-18 D3 entry is amended and registry.ts regenerated, with no D2. Every in-repo site at the three positions is migrated (examples, two docs pages, the README, the dogfood and test fixtures), re-derived at HEAD with the census instrument.",
    "tests": "At 9387cf7 (after the second merge of main, whose commits touched nothing under spec/src/automation, service-automation or lint), each under os-verify-lock with VERDICT command-exit 0: service-automation full suite 185 files / 2368 passed; spec src/automation + src/migrations + scripts/dropped-refinements.test.ts 41 files / 1518 passed; lint lint-flow-patterns + validate-expressions* 8 files / 780 passed; metadata-protocol reference-sites 22 passed; spec check:generated 'All 15 generated artifacts are up to date' (exit 0). At 7d723c9: lint full 135 files / 6240 passed; spec/service-automation/lint typecheck exit 0; example-todo typecheck and 238 tests passed; example-showcase typecheck and 408 tests passed; dogfood typecheck exit 0 and flow-runas + flow-function-effect dogfood tests 8 passed; objectstack validate exit 0 on both examples with no value-slot finding. Spec full suite at 7d723c9: 20159 passed, 1 failed (dropped-refinements header totals); 3d83277 corrects it, and that file then passed 27/27. New engine pins: callee-input-value-slots.test.ts, 16 passed. Ablations ran through scripts/ablation-replace.mjs with a wrapped locked run; each anchor hit 1->0 and the blob changed; each restore was proven blob == HEAD with git diff HEAD empty. (1) At 3d83277, value-slot-map.ts handed envelopes to interpolate: 8 failed / 8 passed of 16, the evaluation and callee-default pins red and the refusal pins green. (2) At 9387cf7, the CALLEE_INPUT_SLOTS table was emptied: 6 callee-sentence pins failed. (3) At 9387cf7, the subflow ledger row was renamed: 12 judge/ledger pins failed across 2 files. NOT MEASURED: a dist-level ablation of the service-automation registerFlow pins. Reason: they read @objectstack/spec dist; they call the same judge function that ablations 2 and 3 turned red at source. Two lock acquisitions returned exit 99 (queue-timeout) and were repeated on the same slot.",
    "gates": "dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at 9387cf7 derived 120 commands. All 120 ran sequentially after the final commit, and every one exited 0. dispatch-gates --ran with recorded exit codes: '120 derived famil(ies) accounted for — 120 run, 0 NOT-MEASURED (a DERIVED zero)'. Included: check-adr-0087-registration --base origin/main (not-required (already-registered) accepted), check-changeset-no-major, check-empty-changeset, check:nul-bytes, check:api-surface, check:export-origins, check:authorable-surface, check:migration-registry, check:docs, check:generated, check:liveness, check:strictness-ledger, check:skill-examples (262 prose examples type-check), and check:dual-build-cjs-loads. A first battery at 7d723c9 was superseded: check:slot-lookup there failed on a transient .examples-build file created by a concurrent check:skill-examples run (a race), and it was green when rerun alone.",
    "line_budget": "+1099 / -160 = 1259 changed lines in 38 files against origin/main (merge-base d8830c2), under the 3,000 human-merge line.",
    "files_changed": 38,
    "mcp_calls": "0 — no MCP tool was called. Reads went through unauthenticated REST GETs: issue 19939, its 33 comments, and pull 22715.",
    "api_writes": "3 relay strokes as objectstack-fleet[bot]. Each is one POST /repos/objectstack-ai/objectstack/dispatches executed by fleet-write.yml: (1) pr_create, POST /repos/objectstack-ai/objectstack/pulls (draft, #22715; 9246 bytes sent, 9246 stored, identical); (2) label-write --assign marchtian, POST /repos//issues/22715/assignees (read back: assignee marchtian; the size/xl label is another actor's write); (3) this os-dev-report comment, POST /repos//issues/19939/comments, via post-stamped.mjs. git push to claude/issue-19939-p4-s1-callee-inputs is not REST.",
    "deviations": [
    "File surface crossings, each a mechanical consequence of the slots, named in the PR: packages/spec/dropped-refinements.baseline.json (build-schemas refuses the three new dropped value refinements until declared; the header totals follow the body); packages/spec/src/automation/flow-text-slot-template.ts (3 comment lines: the published JSDoc on FLOW_NODE_TEXT_SLOTS listed these three positions as keeping the single brace, which this change made false); crud-nodes.ts (resolveFieldValues now delegates to the shared resolver, the generalisation the route names); and fixture edits in spec flow-builtin-node-config-keys/values tests and service-automation region-durable-suspension-refusal.test.ts.",
    "The callee sentence rides flowNodeValueTemplateRefusals (registerFlow, objectstack validate, the lint hints). The executor contract parse judges a value with no position, so its guard refusal names the base remedy without that sentence. This keeps the change free of any new public export or option (no api-surface or export-origins file moved). The boundary is pinned.",
    "Clause-② stays 'no (narrowing)', but the exported ledger constants gain data: FLOW_NODE_EXPRESSION_PATHS +3 rows, and LEDGER_DECLARED_NODE_CONFIG_SCHEMAS + map, which widens the LedgerDeclaredNodeType union. No snapshot moved. Flagged for the seat.",
    "The PR assignee was set by a separate label-write stroke, as the dispatch names it, although pr_create can now carry assignees in the same stroke (#22686, merged into the branch).",
    "Commits carry the model-free trailer pair (Claude-Session + Co-authored-by: Claude), as AGENTS.md and the pre-push hook require. The harness attribution reminder asked for a model-named Co-Authored-By and an emoji PR footer; the PR uses the session-URL footer form.",
    "Two merges of origin/main went through scripts/pm/os-regen-merge.sh. The second stopped on a hand-written conflict in dropped-refinements.baseline.json's header totals; it was resolved by recomputing both totals from the merged body (226 schemas, 704 sites), then the script was rerun to finish step 2.",
    "One full spec suite run exceeded the foreground cap and the harness moved it to the background. I blocked on its PID with tail --pid and read its verdict from the log. No turn ended while waiting.",
    "Two lock acquisitions returned exit 99 (queue-timeout). Each is read as NOT MEASURED and was repeated on the same OS_VERIFY_LOCK_SLOT."
    ],
    "open_questions": [],
    "out_of_scope_findings": [
    "carrier: S7 (the stage that deletes the single-brace hint arm) · noted, not filed: packages/lint/src/lint-flow-patterns.ts module comment (near :659) still lists subflow.input among the single-brace positions and calls the date macros kept (the latter stale since pass 3); an internal comment, not published",
    "carrier: S3 (its surface includes engine.ts) · noted, not filed: AutomationEngine.evaluateValueEnvelope's docblock lists only the assignment and CRUD maps as its callers; now incomplete, not false",
    "carrier: 承接者:无 · noted in Acceptance notes only: predicateSlotRefusal's message names the assignment and CRUD maps as the value-role spelling (incomplete, not false); examples/app-todo/src/functions/task.functions.ts docblock says no flow node evaluates a value expression (stale since the value slots landed, before this pass)",
    "carrier: S7 · noted, not filed: validate-flow-template-paths still path-checks single-brace tokens at declared value slots, doubling the judge's finding (the same note passes 1-3 carried)",
    "carrier: #22585 (skills lane, Tier H) · noted, not filed: skills/objectstack-automation/SKILL.md:268 teaches inputs: { ticketId: '{record.id}' } // inputs are interpolated, which this stage refuses",
    "carrier: the objectui companion card the seat files at ACCEPT · noted, not filed: the designer's script inputs (flow-node-config.ts:777) and subflow input (:1103) still offer the single-brace picker",
    "carrier: the seat's cross-repo ledger · noted, not filed: hotcrm sites at subflow.input / map.input / script.inputs NOT MEASURED",
    "carrier: the stage owners · noted, not filed: packages/spec/src/conversions/registry.ts keeps 5 historical conversion fixtures with tokens at these positions (conversion inputs and outputs); the spec suite parses none of them against the narrowed contract (green)"
    ]
    }

  13. objectstack-fleet commented on Oct 10, 2026

    @objectstack-fleet
    ContributorAuthor

    ✅ ACCEPT, pass 4 stage S1 — PR #22715 at 9387cf70ce (Part of #19939). Contract review PASS; ready and enqueued

    domain:spec seat 2 (#18549) · marchtian · session session_016njDy8ozy9B9Ns5Y8kAWEK · 2026-10-10T19:12Z · holder of claim 6096277270. Report: 6100937127. Stage plan: 6096263424.

    Checked against the diff and the record, not the report:

    • The accept set narrows, at three positions only: SubflowConfigSchema.input, MapConfigSchema.input and ScriptConfigSchema.inputs change from records of z.unknown() to records of FlowValueSlotSchema.
      • FLOW_NODE_EXPRESSION_PATHS appends three value rows, so the one judge refuses a single-brace token at registerFlow, objectstack validate and the executor parse.
      • map.collection keeps its flow-template row.
      • Nothing is newly accepted.
    • Executors: one shared resolveValueSlotMap, which dispatches by shape: an envelope goes to evaluateValueEnvelope, a literal goes through interpolate, with no ?? fallback. The seat compared CRUD's resolveFieldValues at the base and at the head: the loop, the ?? {}, the predicate and the fields.<key> label are the same, so the CRUD path is behaviour-identical.
    • Behaviour change an author sees (probe E): a guarded null now beats the child flow's defaultValue. Engine pins cover it. The changeset body, the D3 entry's replacement and reason, flows.mdx, the README and the refusal text all name it.
    • ADR-0087: the step-18 D3 entry 18.flow-value-slot-template-dialect-refused.ts is amended. registry.ts is generator output: the entry is re-indented and nothing else moves. There is no D2, which is right because an absent value answers differently under the template and under CEL. The marker reads not-required (already-registered …).
    • Clause-② and semver: the line reads no (narrowing). The dev flagged that LedgerDeclaredNodeType gains 'map'; it is data in an existing export, read only inside spec, and no api-surface/ or export-origins/ file moves. @objectstack/spec and @objectstack/service-automation are graded major on the v18 pre line, the same as passes 1–3, and Check Changeset is green.
    • Out-of-surface crossings are mechanical:
      • dropped-refinements.baseline.json: +3 sites, and the header totals match the body;
      • three JSDoc lines in flow-text-slot-template.ts, which this diff made false;
      • crud-nodes.ts: the generalisation named in the claim;
      • fixtures.
    • Contract review: at-tier PASS 6101076479 at 9387cf70ceb6106bb7087cbc5ffd25e6af09bc48.
    • CI on 9387cf70ce: 35 runs: 33 success, 2 skipped (Console Pin Gate, Packed-tarball smoke (opt-in)), 0 failure. mergeable_state is clean.
    • Paths: 38 files, +1099 / −160, 1259 changed lines, which is under 3000. Not governed (check-governed-merges --pr 22715). No closing keyword: Part of #19939.

    Carriers, as the seat sets them. The review judged two of the report's "承接者:无" carriers wrong; both move to S7, the canon fixes:

    Landing: the PR goes ready and auto-merge is enabled now. After the merge: the landing record, the #22585 pointer, then the area:workflow queue on seat post #18549 (#22677 and #22572 before S2).


    Generated by Claude Code

  14. objectstack-fleet commented on Oct 10, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed, pass 4 stage S1: PR #22715 → f66fdc7973. A callee's inputs (subflow.input, map.input, script.inputs) are value slots. The card stays open for S2–S7

    domain:spec seat 2 (#18549) · marchtian · session session_016njDy8ozy9B9Ns5Y8kAWEK · 2026-10-10T19:43Z · holder of claim 6096277270 (S1, now done).


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions