Repository navigation
feat(spec,service-automation,lint)!: a notify node's computed values are value slots and its link a text slot — a CEL envelope or a literal, the {…} token refused (#19939 pass 4, S4) - #22926
Conversation
…ledger (#19939 pass 4, S4) Part of the S4 stage; work in progress. Claude-Session: https://claude.ai/code/session_01KNKBCRDJCu5tGy3TEbvtrF Co-authored-by: Claude <noreply@anthropic.com>
Work in progress. Claude-Session: https://claude.ai/code/session_01KNKBCRDJCu5tGy3TEbvtrF Co-authored-by: Claude <noreply@anthropic.com>
…s 4, S4) Work in progress. Claude-Session: https://claude.ai/code/session_01KNKBCRDJCu5tGy3TEbvtrF Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KNKBCRDJCu5tGy3TEbvtrF Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KNKBCRDJCu5tGy3TEbvtrF Co-authored-by: Claude <noreply@anthropic.com>
… text slot (#19939 pass 4, S4) Claude-Session: https://claude.ai/code/session_01KNKBCRDJCu5tGy3TEbvtrF Co-authored-by: Claude <noreply@anthropic.com>
…9939 pass 4, S4) The os-regen driver kept one side of the generated reference in the merge of origin/main 7de0265; gen:schema then gen:docs re-derive it, carrying both this branch's notify value slots and #22875's template describe. Claude-Session: https://claude.ai/code/session_01KNKBCRDJCu5tGy3TEbvtrF Co-authored-by: Claude <noreply@anthropic.com>
… CEL fault (#19939 pass 4, S4) Claude-Session: https://claude.ai/code/session_01KNKBCRDJCu5tGy3TEbvtrF Co-authored-by: Claude <noreply@anthropic.com>
…fy recipient holds (#19939 pass 4, S4) Claude-Session: https://claude.ai/code/session_01KNKBCRDJCu5tGy3TEbvtrF Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 3 package(s): 22 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 5 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 139 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin b520c42a2725bdc721d081a4b986aac6bf3273a9 && git checkout b520c42a2725bdc721d081a4b986aac6bf3273a9
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 6a40a09ed952f83b1f765d47cd76aee4a41f6672 ef02901d26f2d65d1ce553e99c9cfa42cd9ef871 && git checkout -B drift-repro 6a40a09ed952f83b1f765d47cd76aee4a41f6672 && git merge --no-ff ef02901d26f2d65d1ce553e99c9cfa42cd9ef871
node scripts/docs-audit/affected-docs.mjs --json 6a40a09ed952f83b1f765d47cd76aee4a41f6672
|
… payload are literals, read as written (#19939 pass 4, S4 round 2) Claude-Session: https://claude.ai/code/session_01KNKBCRDJCu5tGy3TEbvtrF Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: PR #22926 ( ① Derived judgmentsEvery accept-set and public-surface change the diff implies, each judged:
② Semver level
③ Boundary flags
Check-runs on this head, read at 2026-10-12T05:49Z42 check-runs: 37 success, 4 skipped, 1 failure.
The verdict below is on the diff's contract; the seat separately requires every check green before the queue, which this head does not yet have (the ceiling red). Implemented-by: VERDICT: PASS Generated by Claude Code |
Contract reviewServed-tier: PR #22926 ( The hop, measured from the fetched diffs, not from the seat's note. The net diff of ① Derived judgmentsEvery accept-set and public-surface change the diff implies, each judged:
② Semver level
③ Boundary flags
Check-runs on this head
Implemented-by: VERDICT: PASS Generated by Claude Code |
Part of #19939
Clause-②: yes (narrowing: a {…} token is refused at notify recipients (the value or a list entry), templateData values, sourceId, actionUrl, sourceObject, actorId and any string of a payload value, a templateData value that names a dialect must be a valid CEL value envelope, and a CEL value envelope written as a payload value is refused; widening: recipients and sourceId accept a CEL value envelope they refused, a recipients list accepts envelope entries, and their published JSON Schema and inferred types gain the envelope arm)
Pass 4, stage S4 of #19939 (stage plan
6096263424, claim6114980625), under ruling D on #11182 and ADR-0032 Decisions 2 and 3. Anotifynode's computed values become value slots:recipients(the value, or each entry of a list),sourceIdand the values oftemplateDatatake a CEL value envelope the executor evaluates in the run's scope, or a literal written as it is. Its link,actionUrl, becomes a text slot with{{ }}holes, liketitle. ItssourceObject,actorIdandpayloadare literals, read as written (round 2, below). A single-brace{…}token at any of them is refused. The family finding is fixed and pinned: a list-valued recipient entry adds each of its recipients, where the template handed it on as one joined recipient. #19939 stays open for stages S5 to S7, so this PR usesPart of.Round 2: the open question answered B (seat order
6117306001)The round-1 report asked whether
sourceObject,actorIdandpayload, which no flow computed (zero{…}tokens in this repository and in hotcrm), should be value slots or literals. The seat answered B in comment6117306001, by the dispatch's own Zone 2 rule ("Do not build a slot nobody computes", with S7's connector inputs as the model). This round's diff,fc5982615c..c200819d1e:sourceObjectandactorIdarez.string()again andpayloadisz.record(z.string(), z.unknown()): their 17.x published shapes, with no envelope arm and no marker. The contract'ssuperRefinerefuses a{…}token insourceObject, inactorIdand in any string of apayloadvalue at any depth, and an envelope written as apayloadvalue, with the literal's remedy, for example: "sourceObjectis the name of the object the notification links to, a literal the executor reads as written: a single-brace token is not resolved here; write the value." An envelope atsourceObjectoractorIdis refused by the string type, with the same lead.builtinValueJudged) leaves a token-carrying value alone, because only the interpolation decides what the run sees there. At a literal, the run reads what the build reads. So the contract marks these issuesreadAsWritten(READ_AS_WRITTEN_PARAM, in the package-internalflow-template-token.ts, which adds no public export;check:api-surfaceis unchanged), and the arm reports them.FlowSchema.parse,objectstack validate, the save door andregisterFlowrefuse them in the contract's words, as the executor's parse does.payload.*,sourceObjectandactorId. The executor reads the three as written;resolveStringSlotkeepssourceIdonly. The descriptor's help strings are restated.flow-double-brace-interpolationandflow-bare-dollar-refhints now say that neither brace dialect resolves there, and they name the value itself. Before, they prescribed{var}, which the build now refuses there. The three keys are a local set inlint-flow-patterns.ts, pinned in step with the contract both ways.Clause-②line (read throughclause2-line.mjs) dropssourceObjectandactorIdfrom the widening arm and adds the payload envelope to the narrowing arm. The generated reference and the dropped-refinements ledger move with it:automation/NotifyConfiglosesactorIdandsourceObject, 713 → 711.mainwas merged throughos-regen-merge.sh(fc5982615c, from base85b4ac634e). It reconciled nothing, and the reference was regenerated after the change.What lands
The contract (
@objectstack/spec).NotifyConfigSchema.recipientstakes a string (one recipient as written), a list whose entries are strings or CEL value envelopes, or one envelope. It is a union, likescreen.recordId's, so the published JSON Schema and inferred type keep the literal arms. A value that is none of these is refused with a sentence naming whatrecipientsis, and the entry at fault.sourceIdtakes a string or an envelope (the same union shape).templateDatatakesFlowValueSlotSchemaper key, like a screen'sdefaults.{…}token in any of their strings is refused by the one value-slot judge (valueSlotTemplateRefusals), naming its CEL spelling. A malformed envelope leads withVALUE_ENVELOPE_REFUSAL.sourceObject,actorIdandpayloadare literals (round 2): their 17.x types, and a token or an envelope there refused with the literal's remedy.actionUrlstaysz.string(), and the contract'ssuperRefinerefuses a single-brace token through the one text-slot judge (textSlotTemplateRefusal), as fortitle/message.describe()of these keys is restated.The ledger.
FLOW_NODE_EXPRESSION_PATHSappends threenotifyvalue rows:recipients,templateData.*,sourceId.recipientscarries a new optional field,eachElement: the slot holds one value or a list of them, and both walks (resolveFlowNodeExpressions,resolveFlowNodeValueSlots) emit each element of a list at its own index (recipients[1]). An envelope entry is validated and evaluated where it sits, and a token in a string entry is refused with the envelope at its place, not as a string inside a literal.NotifyConfigSchemajoinsLEDGER_DECLARED_NODE_CONFIG_SCHEMAS: the descriptor offers these keys with no marker (the Studio form keeps its recipients picker and text inputs), so the markers ride the spec Zod channel, one slot one channel.FLOW_NODE_TEXT_SLOTSgainsnotify.actionUrl, soregisterFlowandobjectstack validatejudge it and compile its holes, as for the other text slots.The judges at the doors.
flowNodeValueTemplateRefusalsreaches the three positions through the ledger with no new judge. Atrecipients(the value or an entry) a refused whole token's remedy ends with the recipient sentence: an envelope that evaluates tonullor a blank string adds no recipient, as a token that resolved to nothing did, and one that evaluates to a list adds each of its recipients.flowNodeConfigRefusals) judges a list slot per element: an issue at an envelope entry is the envelope pass's, and a list whose every entry is a string or envelope-shaped is refused there only for a token or a malformed envelope, which their own judges report. A literal the contract refuses ([42]) stays this arm's.The executor (
@objectstack/service-automation).notifyresolvesrecipientsthroughresolveRecipients: each envelope (the whole value or an entry) through the sharedresolveValueSlotin the run's scope; a list it evaluates to adds each recipient;nulland a blank string add none; anything else (a map, a number, a list holding one) fails the node, naming what it evaluated to and its source, neverString(value).framework#3582's guard, restated for CEL).sourceIdresolves throughresolveValueSlotand must evaluate to a string;nullleaves the key unset; a half-specified click-through pair is still dropped.sourceObject,actorIdandpayloadare read as written.templateDataresolves throughresolveValueSlotMap, like every value-slot map.actionUrlrenders throughrenderTextSlot. A link that renders no text is no link, so the synthesized one still applies downstream, as an empty one did, and an explicit one still wins.notify-node.tsno longer importsinterpolate. The descriptor's descriptions are restated; itstypes are unchanged (the form's controls).The lint (
@objectstack/lint, patch). Theflow-double-brace-interpolationhints nameactionUrlamong the text slots, and the dialect comment no longer listsrecipientsas single-brace.objectstack validaterefuses the positions through the shared judges and the contract, with no rule change. At a notify literal, both wrong-syntax hints name the value itself (round 2).ADR-0087. The step-18 D3 entry
18.flow-value-slot-template-dialect-refusedis amended the S1–S3 way: its surface widens to the notify positions, and its replacement, reason and acceptance criteria say what a recipient evaluates to, that a list adds each recipient andnullnone, why the list token is not rewritten, and that a literal takes the value itself. The two text-slot entries,18.flow-text-slot-single-brace-refusedand18.flow-text-slot-unbound-dollar-root-refused, widen their surface toactionUrl, andSTEP18_RATIONALE's text-slot fragment (registry.ts.template) names the link.registry.tsis generated at build and gitignored. The changeset marker isnot-required (already-registered …)naming the three ids;check-adr-0087-registrationaccepts it.Premises measured at the base
9fdb12752cMeasured through the shipped
AutomationEngine(installBuiltinNodes,registerFlow,execute, a recording messaging service) with a throwaway probe on the base tree, deleted after the run.NotifyConfigSchema(io-node-config.zod.ts:274–:353):recipientswasz.union([z.string(), z.array(z.string())]), required;sourceObject,sourceId,actorId,actionUrlwerez.string();templateDataandpayloadwerez.record(z.string(), z.unknown()).title/messagewere the only text slots (templateExpressionInput, refused a single brace since [v18] flow text slots: read ADR-0032 §3's{{ }}delimiter instead of single-brace{token}(notify title/message and the other flow string slots), converting only what renders the same #22110).notify-node.ts):interpolateran onrecipients(:269, thentoStringList), thetemplateDatavalues (:291),actionUrl(:298, thenString(…)), thepayloadvalues (:301),sourceObject/sourceId(:128–:129, thentoStr) andactorId(:309).template(:289),channels(:293),topic(:294) andseverity(:295) were read raw. The PM's line numbers held.flowNodeConfigRefusalsparsesNotifyConfigSchemaatFlowSchema.parse,objectstack validate,compile, the save door andregisterFlow(the contract map entry,flow-node-config-refusals.ts:125). Its value arm skips any value carrying a{token}, so a token at these keys passed every door; an envelope at the four string-typed keys was refused (measured:registerFlowrefusedrecipients: { dialect: 'cel', … }andsourceId: { dialect: 'cel', … }). No ledger row or judge read these keys.toStringList(:70), measured:recipients'u1'['u1']'u1,u2'['u1,u2']: a string is one recipient, never split['u1', 'u2']['u1', 'u2']['{owners}'],owners = ['u1', 'u2']['u1,u2'], the run succeeds: the family finding holds'{owners}', the same list['u1', 'u2']['{obj}'],obja record['[object Object]'], the run succeeds['{missing}', 'u1']['u1']: the unresolved token is dropped['{missing}']NODE_FAILURE, "every recipient template resolved to nothing: {missing}", with theexpandhintactionUrl: '{obj}'(a map) sent the link[object Object];'/x/{when}'with aDatesent/x/"2026-10-08T09:30:00.000Z"(JSON-quoted);'/x/{{ id }}'became/x/{42};'{missing}'senturl: '', and the synthesized link applied.sourceId: '{obj}'sent the id[object Object], and'{missing}'dropped the pair.templateData: { a: '{missing}' }dropped the key, andtemplateData: { a: { dialect: 'cel', … } }handed the template the envelope object.evaluateValueEnvelope): an absent variable throwsUnknown variable: owner;record.owner.manageron a scalarownerthrowsNo such key: manager; its step-by-stephas(vars.…)guard answersnull;'team:' + ownerwithownernullthrowsno such overload.{x}toxat a recipient: an absentxwas dropped by the template and fails the run under CEL; a list token written as a list entry reached nobody and now reaches each recipient; a record token became[object Object]and now fails the node. AtsourceIdan absent value dropped the link and fails the run under CEL. At atemplateData/payloadkey an absent value dropped the key, and the guarded form writesnull. AtactionUrl,{x}to{{ x }}renders aDateand a whole-token object differently (the [v18] flow text slots: read ADR-0032 §3's{{ }}delimiter instead of single-brace{token}(notify title/message and the other flow string slots), converting only what renders the same #22110 measurement, repeated here). So the three step-18 D3 entries are amended and no conversion is registered.#22875(thetemplatedescribe) landed during this stage;mainwas merged throughos-regen-merge.shand the one generated path it rewrote was regenerated (below).templateis untouched here.The classification
One row per key the executor read through
interpolate, with the token census in shipped metadata and hotcrm (the reach below).{…}tokensrecipients{record.FIELD}templateData.*sourceIdsourceObjectactorIdpayload.*actionUrl{{ }}Flattened, not refused. An entry that evaluates to a list adds each of its recipients. The list is what the author meant (a multi-user field, a team's members, a list variable), the whole-value form already meant a list, and the designer's one-row-per-field picker has no other way to address a multi-user field; refusing it would force every such flow into one hand-written CEL list concatenation. Nested lists, maps and numbers are refused at the node, so a flattened entry can never be a phantom recipient. Pinned (
notify-value-slots.test.ts3).The three zero-census keys are literals (round 2). Round 1 built them as value slots and reported the choice as an open question. The seat answered B (
6117306001) by the dispatch's own rule: no slot is built that nobody computes. Each of them resolved a token in 17.x, so a{…}there is refused with the literal's remedy, not forwarded verbatim.The reach
Re-derived at HEAD with a census of every token-bearing string at the seven positions, in
examples/**,packages/platform-objects,packages/qa/dogfoodandcontent/docs/**(references and releases excluded). Positive control: the base read 41 token strings in 7 files (recipients25,sourceId10,actionUrl4,templateData1, counted by hand for the one map the scan's object regex cuts), and HEAD reads 0.examples/app-showcase(automation/flows/index.ts): 16 recipient strings, 7sourceIds and 4 links. A recipient field that may be unset (assignee,owner,watcher, all optional text fields) is guarded,has(record.assignee) ? record.assignee : null, so an unset one is dropped as the template dropped it and the node fails as before;ownerId(a subflow input) and the ids are plain. The urgent-task alert's two entries are guarded each, and its comment now says what it does: both are notified, and the node fails only when both are absent. The seed's comment and the stale{record.project.owner}/{record.account.owner}comments follow.examples/app-todo(flows/task.flow.ts): 2 recipients (guarded) and 2sourceIds.automation/flows.mdx(the value-slot list, a new paragraph on recipients, the text-slot list, the dialect table, three samples),automation/email-templates.mdx(the sample and thetemplateDatasentence),getting-started/common-patterns.mdx(two samples). The service-automation README's dialect table.seed-ownership-claim-dispatch.dogfood.test.tsandfixtures/schedule-organization-fixture.ts.packages/platform-objectswrites nonotifynode.approval-resume-relation-expand.test.ts(its rolled-back shape is the CEL hop now; see Acceptance notes) and the translation-face and text-slot-list pins.1d7148bf2d, deleted after): 19 notify nodes in 15 flow files, each with one{…}recipient in a list (2 of them{$User.Id}), 35{…}templateDatavalues in 19 maps, and 19 links with holes (/crm_lead/{record.id}); nosourceObject,sourceId,actorIdorpayload. It is on@objectstack/spec17.7.0, so nothing there moves until it upgrades.4997995022(read-only), the designer's notify pickers, re-read at the pin (the stage-0 lines:777,:856,:1103are the S1/S2inputs,defaultsandinputrows there now):flow-node-config.ts:1118:recipientsrendersFlowRecipientsField, which writes a record-field row as{record.FIELD}(FlowRecipientsField.tsx:106) and reads a stored value withreadRecipients(:115–:118), which keeps strings only — so a stored envelope entry, or a whole envelope, is dropped from the rows and lost on the next save.:1132sourceObject,:1133sourceId,:1139actionUrl:textfields, whose defaultrefModeistemplate, so the{x}picker inserts a single-brace token (VariableTextInput.tsx,formatToken).:1120message's placeholder still reads "Supports {var} template references".recipientsuntyped and the other keys with theirtypes, unchanged here, somergeServerFlowFieldskeeps the hand-written picker and the pinned console still builds. No non-test objectui source importsNotifyConfig.inputand scriptinputsfields author a CEL value envelope, not a{var}token (the objectui half of objectstack-ai/objectstack#19939 pass 4, S1) objectui#12101) by the seat.Pins, and the ablations that turn them red
notify-value-slots.test.ts(service-automation, 32 engine pins): each position refused atregisterFlow, located and naming the envelope, and by the executor's contract parse as a guard; the recipient sentence;actionUrlrefused with its hole spelling at both doors; each envelope evaluated, each literal handed on, a recipient string not split; the family finding (an entry evaluating to a list adds each recipient; never'u1,u2'); an absent variable fails withEXECUTION_ERRORnamingrecipients[1]; a guardednullis dropped; a node left with none fails naming the expressions; a map, and a list holding a number, fail the node naming them;sourceIdnulldrops the pair and a map fails;actionUrlrenders a hole, a map as JSON, and a link rendering nothing yields to the synthesized one.notify-node.test.tsre-judged in place: the interpolation fixtures move to envelopes and holes; theframework#3582trio now pins the guarded hop'snulldropped, the node failing with the expression named, and the unguarded hop on a scalar id failing withNo such key: manager.io-node-config.test.ts: every position takes an envelope and a literal and refuses a token at its own path and a malformed envelope; a list mixes strings and envelopes and names the entry at fault; the build doors' value arm judges a list per element; the JSON projection carries the markers and keeps the literal arms; the describes); ledger pins (flow-node-expression-paths.test.ts: the six rows,eachElementonrecipientsonly, element-by-element emission; the census row list); judge pins (flow-value-slot-template.test.ts: located per position, the recipient sentence and where it is absent); the text-slot list (flow-text-slot-template.test.ts); the translation face classifiesnotify.actionUrl(flows-translation-face.test.ts).config-expression-ledger.test.ts): notify declares its three slots on the spec Zod channel alone;notify.recipientsjoins the channel-required non-predicate slots refused absent.validate-expressions.text-slot.test.ts(envelopes pass, a token at the link is refused aterror);lint-flow-patterns.test.ts(the double-brace control moves fromactionUrl, a text slot now, tosourceId, a value slot).notify-value-slots.test.ts, section 6, which replaces those keys' rows in sections 1 and 2): a literal is handed on as written, nestedpayloaddata included; a{…}token and an envelope are refused atregisterFlow(located at the key, in the contract's words, with the literal remedy) and by the executor's contract parse as a guard. The contract (io-node-config.test.ts): a literal parses; a token is refused at its own path with the literal remedy, never the value slots' envelope; an envelope is refused; a token at any depth of apayloadvalue is located at that string, and a deeper envelope is data; the build doors (flowNodeConfigRefusals,FlowSchema.safeParse) refuse a token and an envelope in the contract's words; the published JSON Schema keeps the 17.x shapes, with no marker and no envelope arm.objectstack validate(validate-expressions.test.ts) refuses both atconfig.KEYwith the literal remedy, and a literal is clean. The lint hints (lint-flow-patterns.test.ts): the contract's literal keys, asked of the contract, are exactly the keys the hints read as literals, and a control key keeps its hint. The ledger, judge and ratchet pins count three notify slots.384da59997, each throughscripts/ablation-replace.mjsin WRAP mode under an outertrap … EXIT INT TERM: the anchor hit once, the blob changed, and the restore was proven with the blob equal to HEAD andgit diff HEADempty (the outer check found nothing to restore).recipientsreading restored to the 17.xtoStringList(interpolate(…)): 8 of the 32 engine pins went red — every evaluation pin forrecipients. The refusal pins stayed green, as they must: the contract parse refuses a token before the executor reads anything, so restoring the interpolator cannot reach them. (The dispatch predicted the refusal pins; the measured direction is the evaluation pins.)eachElementdeleted: 5 spec pins went red in 3 files (the element-wise emission, the located recipient refusal and its sentence, and the build doors' per-element value arm).@objectstack/spec'sdist; ablations 2 and 3 turned the same functions red at source.c200819d1e, the same way (anchor hit once, blob changed, restore proven blob-equal toHEAD,git diff HEADempty):4. The value arm's read-as-written line deleted (
flow-node-config-refusals.ts): 3 of 73 pins inio-node-config.test.tswent red, exactly the three build-door pins (sourceObject,actorId,payload); the contract's own refusal pins stayed green.5. The contract's literal refusal removed (its
addIssuereplaced byvoid refusal): 8 of 73 went red: the three token refusals, the payload envelope, the payload depth case and the three build-door pins. ThesourceObject/actorIdenvelope pins stayed green, as they must: the string type refuses those, not the refinement.Measurements
Round 2, at
c200819d1e(the head; each suite took the verify lock once, on a shared box, so no wall-clock figure is an idle-box reading):turbo run build --filter='…^...')automation/NotifyConfigwithoutactorIdandsourceObject)@objectstack/service-automation: thenotify-*set (all six files) plusconfig-expression-ledgerandconfig-parse@objectstack/lint:lint-flow-patterns,validate-expressions,validate-expressions.text-slot@objectstack/specvitest run --project local(full)@objectstack/service-automationvitest run(full)@objectstack/lintvitest run(full)@objectstack/spec/service-automation/linttypecheckcheck:generated,check:api-surface("public API surface + factory signatures unchanged"),check:authorable-surface,check-adr-0087-registration.mjs --base origin/main(not-required (already-registered))c200819d1e:node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackwith no paths derived 116 commands, and 115 were run. 113 exited 0 in the battery.check:skill-examplesandcheck:i18n-coveragerefused with exit 3 (PREREQUISITE NOT MET:client-reactand the CLI and example closures were unbuilt in the re-created worktree). After the one closure build each names, both exited 0, and so did a re-run ofcheck:dts-closure. Reconciled with--ran: "116 derived, 115 run, 0 NOT-MEASURED, 1 UNRUN"; the unrun one ispnpm check:dual-build-cjs-loads, NOT MEASURED by the dispatch's rule.pnpm check:type-check-debt(exit 0, "1 ledger entr(ies) re-measured … none above its recorded number"). That npm script is--re-measure, which the dispatch rules out, and it first runs the whole-workspace buildturbo run build --filter=./packages/* --filter=./packages/*/*, outside the verify lock. That build rewrotedist/while my prerequisite build ran, so its firstclient-reactdeclaration build failed (TS7016) and acheck:dts-closuretaken then namedverify. Both were re-run green once it ended. Round 1's batteries ran the same script. The derivation tree was 8 commits behindorigin/main(51b43a8dc4); 8 gate-input files changed there (workflows and ADR anchors), none of them in this diff.eslint --no-inline-config --format jsonover the 42 changed.tsfiles atc200819d1e: exit 0, 42 files in the JSON report, 0 errors, 0 warnings, none reported ignored. The config enables no type-aware linting (everyparserOptionsis{ ecmaVersion: 'latest', sourceType: 'module' }, noproject/projectService;eslint.config.mjs:327–:328), so this diff cannot move a verdict on an untouched file. The whole-treepnpm lintis CI's.os-dev-reportcomment on [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.origin/mainmoved again after the merge (to51b43a8dc4, 8 commits, none touching a file in this diff).dual-build-cjs-loads(the dispatch rules out the whole-workspace build), and the CI-owned whole-tree lint.Round 1 (the heads named; the round-1 diff, before the literals):
@objectstack/specbuild (the dropped-refinements ledger and the generated references included)384da59997@objectstack/specvitest run --project local(full)4c8cf1558f@objectstack/specvitest run --project repo(test:repo)4c8cf1558f@objectstack/spectypecheck(tsc, scripts, test layer)4c8cf1558f@objectstack/service-automationvitest run(full, against the rebuilt spec)384da59997@objectstack/service-automationtypecheck4c8cf1558f@objectstack/lintvitest run(full)384da59997@objectstack/linttypecheck4c8cf1558fvitest run --project integration test/migrate-meta-engine-guidance.test.ts384da59997@objectstack/example-showcasevitest run(full)d8485a8a50@objectstack/example-todovitest run(full)4c8cf1558f@objectstack/dogfood(closure build, then the three files this diff feeds:seed-ownership-claim-dispatch,schedule-acting-organization,schedule-sweep-organization-scope)4c8cf1558f4c8cf1558fone test failed:inert-wirings.test.ts, "derives recipient fields from the flows (guard is not vacuous)". The finding: showcase seed assigns tasks to non-user emails — a stock reassignment lands an inbox row nobody can read #7746 guard derived recipient fields with a{record.FIELD}regex, so the migrated envelopes left it blind. It now reads the trigger record's direct fields from the envelope sources; at HEAD it derivesshowcase_task.assignee,showcase_invoice.ownerandshowcase_project.owner(the base's fields:{record.assignee}and{record.owner}under the same three start objects), where the old regex derives none. The rolled-back hop's pin inapproval-resume-relation-expand.test.tswas also re-judged (it asserted the node's id, which a thrown evaluation fault does not carry).node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackwith no paths, atd8485a8a50: 116 commands derived, 115 run, every one exit 0. Reconciled with--ran: "116 derived, 115 run, 0 NOT-MEASURED, 1 UNRUN"; the one unrun is the next sentence's.pnpm check:dual-build-cjs-loadsis NOT MEASURED: the dispatch rules out the whole-workspace build it needs. Correction: round 1 said here thatcheck:type-check-debtran without--re-measure. That was wrong. The derived commandpnpm check:type-check-debtIScheck-type-check-coverage.mjs --re-measure(rootpackage.json:195), which also runs the whole-workspace closure build first. So both ⛔s were crossed, in every battery of both rounds, unlocked. See the round-2 measurements.d8485a8a50, every one exit 0 (14 of them are checker-health--self-testruns, not PR verdicts), including the five whose roster sits in a directory this diff touches (check-changeset-fixed,check:meta-url-spelling,check:authz-resolver,check:error-code-casing,check:filter-alias-parity). The three PR-context guards (check-closing-target-claim,check-partof-closing-keyword,check-single-claim-paths) are run wired to this PR once it is open; their readings are in theos-dev-reportcomment on [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.check-adr-0087-registration.mjs --base origin/main(exit 0, "1 declared-breaking changeset(s), each carrying an ADR-0087 disposition",not-required (already-registered)),check:generated(exit 0),check:nul-bytes(exit 0),check:doc-authoring(exit 0).check:migration-registryno longer exists (removed by [Decision] the migration registryregistry.tsis the last committed generated aggregate on the ADR-0087 D3 path: keep it and finish B′, or generate it at build #22554; see the notes).eslint --no-inline-config --format jsonover the 40 changed.tsfiles atd8485a8a50: exit 0, 40 files in the JSON report, 0 errors, 0 warnings, none reported ignored. The population is the changed source files the config'sfilesglobs match. The narrowing excludes nothing that could move:eslint.config.mjsenables no type-aware linting (everyparserOptionsis{ ecmaVersion: 'latest', sourceType: 'module' }, noproject/projectService, no typed rules; the config says so at:327–:328), so this diff cannot move a verdict on an untouched file. The whole-treepnpm lintis CI's.origin/main(85b4ac634e) at the time of the run; none of them touches a file in this diff, and the one gate file among them,scripts/check-verify-stand-in-erasure.mjs, adds two allowlist entries for exports feat(verify): the reference-value shape family, runnable as os verify --reference-shapes (#15953 slice 1) #22907 added topackages/verify, which this diff does not touch.dual-build-cjs-loads(above), and the CI-owned whole-tree lint. Turbo's--affectedset was not used to narrow anything here.Out of the claim's file surface, and why
The claim's file surface (
6114980625) was held, with these files beside it; each one is named here with why.spec/automation/flow-node-config-refusals.ts(18 lines): the build doors' value arm judges a list slot per element when the ledger row carrieseachElement. Without it, the arm reads a list as one value that is not an envelope, so a list holding a malformed envelope entry or a token is refused a second time there, as the contract's union refusing the whole list, beside the envelope pass's or the judge's located refusal (pinned inio-node-config.test.ts, "the build doors").spec/automation/schemaless-node-config.zod.ts(11 lines):LEDGER_DECLARED_NODE_CONFIG_SCHEMASgainsnotify, the channel that carries itsxExpressionmarkers to the reconciliation ratchet, since the descriptor carries none (its form keeps the recipients picker).spec/automation/flow-text-slot-template.ts: the claim scoped it to a notify remedy sentence; the change is one row inFLOW_NODE_TEXT_SLOTS(notify.actionUrl), the list that makes a key a text slot at every door, and its docblocks.18.flow-text-slot-single-brace-refused.tsand18.flow-text-slot-unbound-dollar-root-refused.ts: the claim names only the value-slot entry; their surface lists the text slots, andactionUrljoins it (the plan's "each stage amends the D3 entries").spec/dropped-refinements.baseline.json: the spec build refused until theautomation/NotifyConfigrow named its five new projection-dropped refinements (actorId,recipients,sourceId,sourceObject,templateData.valueType; header 708 to 713).spec/automation/builtin-node-config.zod.ts(two docblocks listing the value-slot positions), service-automationengine.ts(the value-slot docblock),builtin/template.ts(a comment that listedrecipientsas a single-brace position) andREADME.md(the dialect table).api/zod-issues-to-fields.test.ts,flow-builtin-node-config-keys.test.ts,flow-node-config-required.test.ts,flow-region-pause-and-end.test.ts; and the two list pins,builtin-node-config.test.ts(the ledger-declared keys) andsystem/flows-translation-face.test.ts(notify.actionUrlclassified as a text slot with no reader).flows.mdx, as the census found them:automation/email-templates.mdxandgetting-started/common-patterns.mdx; the generatedreferences/automation/io-node-config.mdx.spec/automation/flow-template-token.ts(the package-internal home ofREAD_AS_WRITTEN_PARAM, which is not a public export, beside the token grammar the notify contract now reads),flow-node-config-refusals.tsagain (the value arm reads the marker),lint/src/lint-flow-patterns.ts(the literal hints) andlint/src/validate-expressions.test.ts(theobjectstack validatepins).template,channels,topic,severity; S5 to S7;skills/**; objectui; hotcrm.Acceptance notes
6117306001, in the non-escalated class, with the maintainer's veto window open):sourceObject,actorIdandpayloadare literals, and the round above implements it.record.account.ownerwithout the start node'sexpand) resolved to nothing, and the empty-recipients guard named it with theexpandremedy. As a CEL value the hop fails the evaluation itself:recipients[0]: value expression failed to evaluate as CEL: No such key: owner, with its source. Neither that message nor the restated empty-recipients guard (which now names the expressions that evaluated to nothing, and saysnull, a blank string and an empty list add none) carries the base'sexpandremedy: the fault names the key and the source, and the hint to hydrate the relation is gone, as it is at every value slot since S1, where the fault is the engine's one message. Restoring it is a polish of that shared message, noted here, not filed. The showcase'sapproval-resume-relation-expand.test.tspins the rolled-back shape on the new message. An evaluation fault is thrown, as at every value slot since S1, so the run's message does not carry the node's id (the step and$error.nodeIddo); the returned empty-recipients failure does, as before.inputand scriptinputsfields author a CEL value envelope, not a{var}token (the objectui half of objectstack-ai/objectstack#19939 pass 4, S1) objectui#12101, carried by the seat): the recipients picker writes{record.FIELD}, whichregisterFlow, the save door andobjectstack validatenow refuse, naming its envelope; andreadRecipientskeeps strings only, so a stored envelope entry is dropped from the rows and lost on the next save. ThesourceObject,sourceIdandactionUrltext inputs insert single-brace tokens from the{x}picker.{$User.<path>}as a kept value-slot spelling and the text-slot run-user remedy through it, which #19939 pass 2 (PR #22563) refuses #22585 lands (carried by the seat), measured onmainateab7236ff7:skills/objectstack-automation/SKILL.md:107writesrecipients: '{record.assignee}'and:114sourceId: '{record.id}', both refused after this lands;:115's link is a text slot now;:240listsrecipientsamong the single-brace slots.:107's comment also says a recipient may be a CSV, and at the base a CSV string is one recipient:'u1,u2'was delivered as the one audience entry'u1,u2', which the messaging service's recipient resolver reads as a bare user id (recipient-resolver.ts:143).lint-flow-patterns.ts, because the spec publishes no list of them and a new export would widen the API surface for a hint. A pin asks the contract which keys refuse a token with the literal lead, and holds the set to exactly those.sourceObjecttext input (objectui pin:1132) inserts a single-brace token from its{x}picker. That token is refused at save now, with the literal remedy. This is carried to v18: Studio's subflowinputand scriptinputsfields author a CEL value envelope, not a{var}token (the objectui half of objectstack-ai/objectstack#19939 pass 4, S1) objectui#12101 with the other pickers.resolveValueSlothands a non-envelope value tointerpolate(S1's shape); a literal reaching it carries no token, since the contract parse refused one, so it comes back as written.notify-node.tsitself no longer importsinterpolate.check:migration-registry, named in the dispatch's consumer list, was removed by [Decision] the migration registryregistry.tsis the last committed generated aggregate on the ADR-0087 D3 path: keep it and finish B′, or generate it at build #22554 (the registry is generated at build):gen:migration-registryran inside each spec build, test and typecheck above, andsrc/migrationsis green.mainat19d5a3e6d9, and round 2 atfc5982615c(base85b4ac634e);mainhas moved since (to51b43a8dc4), touching no file in this diff.Generated by Claude Code