Repository navigation
fix(spec/automation): refuse a text-slot {{ $… }} hole whose root the flow engine does not bind - #22499
fix(spec/automation): refuse a text-slot {{ $… }} hole whose root the flow engine does not bind#22499objectstack-fleet[bot] wants to merge 11 commits into
{{ $… }} hole whose root the flow engine does not bind#22499Conversation
…engine does not bind
A `{{ $User.Id }}` hole in a flow text slot compiled (the hole grammar
admits `$` so the engine's own `$error` has a spelling) and rendered a
blank fragment with the run reporting success, while `{$User.Id}` was
refused at the same door with its remedy. The text-slot judge now reads
one enumerated list of the `$` variables the engine binds and refuses a
hole over any other `$` root: `{{ $User.<path> }}` gets the remedy its
single-brace spelling gets, any other root a remedy naming the engine's
variables. A single-brace path token over such a root is no longer
rewritten to a hole the judge would refuse in turn.
Claude-Session: https://claude.ai/code/session_01VZqqwTj2wsihZEbfT6yyYN
Co-authored-by: Claude <noreply@anthropic.com>
…d validate, and the engine-bound list
The registerFlow and `objectstack validate` doors refuse `By {{ $User.Id }}`
in a text slot through the spec judge, with the remedy `{$User.Id}` gets;
`{{ $error.message }}` and `{{ record.name }}` stay clean. In
service-automation, a scan of the package's sources for every `$`-named
variable it binds by literal name asserts the spec judge admits a hole
over each, so a root the engine starts binding without a line in the
spec list reddens here.
Claude-Session: https://claude.ai/code/session_01VZqqwTj2wsihZEbfT6yyYN
Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VZqqwTj2wsihZEbfT6yyYN Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift Check18 anchor(s) derived from 2 changed package(s); no hand-written page names any of them. 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 f7830c31750650db1aed753fa081b5fac2dd7806 && git checkout f7830c31750650db1aed753fa081b5fac2dd7806
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin c76edeb8c623a9996a6706b7f4daf5b5b4acae9b c36b06966d2a5cb97faff4a3cf52a38d0a30ba1e && git checkout -B drift-repro c76edeb8c623a9996a6706b7f4daf5b5b4acae9b && git merge --no-ff c36b06966d2a5cb97faff4a3cf52a38d0a30ba1e
node scripts/docs-audit/affected-docs.mjs --json c76edeb8c623a9996a6706b7f4daf5b5b4acae9b |
Contract reviewServed-tier: Inputs: card #22477 (body; comments 6080591754 triage, 6081163129 claim, 6083552417 os-dev-report), PR #22499 (body, file list, the net diff of the head against ① Derived judgments
② Semver levelMeasured against npm Verdict on the dev's measurement: wrong. "The acceptance this tightens arrived with the No ADR-0087 category carries "the line is unreleased." Pre mode does not change this. The fixed group (69 packages) already carries a pending What the changeset must be (
Service-automation and lint ship no source change and need no entry - right. ③ Boundary flags
Implemented-by: VERDICT: FAIL |
…xt-slot-dollar-root
…edy for a $ root the engine does not bind
In a flow text slot, the hint for a bare `$X.y` written outside the holes
prescribed the hole `{{ $X.y }}` for every root. For a root the flow
engine does not bind (`$User.Id`) that hole is now refused by the spec's
text-slot judge, so the hint asked the author for a refused spelling. The
hint now asks the judge per reference: an admitted hole is prescribed as
before, a refused one gets the judge's own refusal and remedy. The roots
stay listed in one place, the judge.
Claude-Session: https://claude.ai/code/session_01VZqqwTj2wsihZEbfT6yyYN
Co-authored-by: Claude <noreply@anthropic.com>
…-root-refused
The 17.x node contracts typed the flow text slots as plain strings, so a
`{{ $User.Id }}` hole in a notify, screen or end text slot was accepted by
the last published spec; the text-slot judge now refuses it. The D3
semantic entry records the narrowing with its remedy (compute the value
with an assignment node, then write the variable as a hole; a variable
the flow binds itself is named without the `$`), and step 18's rationale
gains its fragment. The registry region is regenerated by
gen:migration-registry; spec-changes.json and the upgrade guide do not
move on this base, where step 18 is not projected yet.
Claude-Session: https://claude.ai/code/session_01VZqqwTj2wsihZEbfT6yyYN
Co-authored-by: Claude <noreply@anthropic.com>
…g of the published spec
`@objectstack/spec@17.7.0` (npm latest) types the flow text slots as plain
strings and accepts `{{ $User.Id }}` there, so the refusal narrows a
published accept set: `minor`, a BREAKING banner, `Clause-②: no
(narrowing)` and the ADR-0087 disposition `registered
flow-text-slot-unbound-dollar-root-refused`. `@objectstack/lint` takes a
patch for the flow-bare-dollar-reference hint.
Claude-Session: https://claude.ai/code/session_01VZqqwTj2wsihZEbfT6yyYN
Co-authored-by: Claude <noreply@anthropic.com>
…xt-slot-dollar-root
…low-text-slot-unbound-dollar-root-refused Pure regeneration after the merge of origin/main 4e9fe9f, which projects protocol step 18: gen:spec-changes and gen:upgrade-guide add the D3 entry flow-text-slot-unbound-dollar-root-refused (step 17 -> 18 semantic count 329 -> 330 in both) and its rationale fragment. No hand edit. Claude-Session: https://claude.ai/code/session_01VZqqwTj2wsihZEbfT6yyYN Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Inputs: card #22477 (body; comments 6080591754 triage, 6081163129 claim, 6083552417 / 6085693344 / 6086613543 the three os-dev reports, 6083820886 the seat's REWORK), PR #22499 (body, its 11-file list, comments 6083515708 and the first record FAIL 6083797903, the net diff of ① Derived judgments
② Semver levelThe changeset
③ Boundary flags
Implemented-by: VERDICT: PASS |
…xt-slot-dollar-root
…e merged tree Pure regeneration after the merge of origin/main c76edeb, which carries the step-18 D3 entry storage-scope-public-retired (ee8751d). The os-regen driver kept one side of both artifacts in the merge; gen:spec-changes and gen:upgrade-guide re-derive them from the merged registry, holding both storage-scope-public-retired and flow-text-slot-unbound-dollar-root-refused: step 17 -> 18 semantic count 331 in both documents. No hand edit. Claude-Session: https://claude.ai/code/session_01VZqqwTj2wsihZEbfT6yyYN Co-authored-by: Claude <noreply@anthropic.com>
Contract review carried over a pure regeneration —
|
Fixes #22477
Clause-②: no (narrowing)
What this does
A
{{ }}hole in a flow text slot (anotifytitle/message, ascreentitle/description, a refusingendmessage) whose root is a$name the flow engine does not bind is now refused by the one text-slot judge,textSlotTemplateRefusalinpackages/spec/src/automation/flow-text-slot-template.ts. Every door that already calls that judge refuses it with no edit of its own:NotifyConfigSchema,ScreenConfigSchema,EndConfigSchema,AutomationEngine.registerFlow, andobjectstack validate(expression-invalid,error).'By {{ $User.Id }}'is refused with the very remedy'By {$User.Id}'already gets ("compute it into a variable with anassignmentnode, whose value slot still reads it (assignments: { v: '{$User.Id}' }), and write{{ v }}here"). The test asserts the hole refusal ends with the single-brace remedy byte for byte, so the two spellings answer alike.$root ({{ $User }},{{ $Error.message }},{{ $org.id }}) gets a remedy that names the root and the variables the engine does bind.'Failed: {$caught.message}') used to be told to write{{ $caught.message }}, which would now be refused in turn. It gets the same remedy instead of a rewrite.{{ $error.message }},{{ record.name }}, a node output{{ lookup.result }}, and every hole over an engine-bound$variable are unchanged.The
$roots are one enumerated list (H1, measured)The runtime has no single declaration of its
$variables. They are bound by literal name in three places:service-automationbinds it (atdee7692f0b)$record,$runId,$flowName,$flowLabelengine.tsseedRunVariables(every run attempt;$recordwhen there is a trigger record)$errorengine.ts, the throw arm and the returned-failure arm of node execution (also thetry_catcherrorVariabledefault)$loopItems,$loopIndexbuiltin/loop-node.ts, the legacy flat-graphloopwith nobodyThe spec has no home for this either.
contracts/automation-service.tsstates only that$names are reserved, in itsINVALID_SIGNALprose. So this follows H1's fallback. The listFLOW_ENGINE_VARIABLESsits beside the judge inpackages/spec. A parity pin inservice-automation'stext-slot-template.test.tsscans that package's non-test sources for every.set('$name'literal and asserts the public judge admits a hole over each one. Today the scan finds exactly those seven, listed with their files. The scan also carries a floor that fails if one of the seven stops being bound, so a removal is caught too.The list is module-private, not exported. A new public export would enlarge the public surface, and that is the question
Clause-②answers; it is ruledno. The parity pin reaches the list throughtextSlotTemplateRefusal, which is already public.check:api-surfaceandcheck:export-originsare unchanged and green.H2 (node outputs): a node output is addressed by its node id (
{{ lookup.result }}). The engine writes it asNODEID.KEY.git grepfinds no node id starting with$. The map node's.$mapState/.$mapItemDoneare.$segments under a node-id root, not$roots. So no node output joins the list.H3 (the validate door):
objectstack validate's text-slot check isvalidate-expressions.ts, and it callstextSlotTemplateRefusal(slot.source)before compiling the slot. The refusal therefore reachesobjectstack validatethrough the same judge, and no lint source edit is needed.validate-flow-template-paths.tsdoes skip$roots, but that is the record-field-path warning rule, not the text-slot door. The validate-door pin is invalidate-expressions.text-slot.test.ts.H4: no
skills/**edit.Boundary, stated
The triage ruling says nothing outside the list is admitted. So a variable an author binds under a
$name, such astry_catcherrorVariable: '$caught'read as{{ $caught.message }}, is refused in a text slot, and the remedy says to name it without the$. Measured:$-namederrorVariables other than$erroroccur only in tests ($caughtinthrow-arm-error-refresh.test.ts,$errin two spec tests). None of them is read in a text slot.Changeset:
@objectstack/specminor,@objectstack/lintpatch,Clause-②: no (narrowing), BREAKINGPatch round 1 (REWORK
6083820886on #22477, after contract review FAIL6083797903) corrected the grade:@objectstack/spec@17.7.0(npmlatest),NotifyConfigSchema.title/.message,ScreenConfigSchema.title/.descriptionandEndConfigSchema.messageare plainz.string(), and no text-slot judge exists. So'By {{ $User.Id }}'is accepted by the published contract and refused after this PR. The unreleased part is the hole semantics of [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, not the acceptance of the string.flow-text-slot-unbound-dollar-root-refused(504b61ad21), with the changeset's markeradr-0087: registered flow-text-slot-unbound-dollar-root-refused. After PR feat(spec)!: PROTOCOL_VERSION 17 → 18 in an ordinary PR — regenerated spec-changes.json and upgrade guide, ^18 handshakes, pre-mode lockstep exception (#22085 Q1 → B) #22215 (protocol 18) landed, the re-sync regeneratedspec-changes.jsonand the upgrade guide in their own commit (52c005e2b1): step 18 has 330 semantic entries, up from 329, the one added being this entry.@objectstack/lintpatch: theflow-bare-dollar-referencehint now asks the judge, so for a$root the engine does not bind it prescribes the judge's remedy, not a refused hole (d9a5ebbb6f).Re-sync after PR #22215 (head
52c005e2b1)origin/maine148ca9842merged in patch round 1 (b3482cb096), andorigin/main4e9fe9ff6a(PR feat(spec)!: PROTOCOL_VERSION 17 → 18 in an ordinary PR — regenerated spec-changes.json and upgrade guide, ^18 handshakes, pre-mode lockstep exception (#22085 Q1 → B) #22215) merged in the re-sync (6e65fb1747). Both went throughos-regen-merge.sh, and neither merge commit adds content of its own.52c005e2b1: speclocal630 files / 18,810, specrepo54 / 915; lint 131 / 5,983; service-automation and the three packages' typechecks exit 0. 94 derived gate families run, 0 not measured.$User.Idpin and the mixed-slot pin red, and the$error.messagecontrol stays green.Tests, round 0 (head
f0b39b02de)packages/specflow-text-slot-template.test.ts: 26 passed. It holds the card's pins at the judge and at the three node contracts (NotifyConfigSchemawith a bare string and with a template envelope,ScreenConfigSchema,EndConfigSchema; each refused at the key withcode: custom), plus the controls.packages/services/service-automationtext-slot-template.test.ts: 18 passed. It holds theregisterFlowpin, the parity scan, and a run that renders{{ $flowName }} / {{ $flowLabel }} / {{ $record.name }}asroots / roots / Acme Corp.packages/lintvalidate-expressions.text-slot.test.ts: 6 passed. It holdsos validate's own sequence (normalize, parse,runAuthoringRules('validate')):By {{ $User.Id }}in a notifymessageand a screentitleis oneerrorfinding carrying the remedy. The control{{ $error.message }}/{{ record.name }}gives[].f0b39b02deand went throughos-verify-lock.sh:@objectstack/specvitest run --project local: 630 files, 18805 passed, 1 todo;@objectstack/specvitest run --project repo(the corpus walk over other packages' sources): 54 files, 915 passed;@objectstack/service-automationtest: 179 files, 2197 passed;@objectstack/lintvitest run: 130 files, 5944 passed;typecheck(tsc --noEmitpluscheck:test-typecheck) is green on spec, service-automation and lint.Ablation (one-shot, nothing left in the tree)
Every leg ran through
scripts/ablation-replace.mjs(anchor must hit, blob hash proven, restore trap armed). The fix was committed first. Service-automation and lint resolve@objectstack/specthroughdist/, so spec was rebuilt inside each leg, andscripts/ablation-dist-preflight.mjsproved the mutation reacheddist/.[singleBraceRefusal(text), unboundRootHoleRefusal(text)]became[singleBraceRefusal(text)]: anchor 1 → 0, blobb022c9d7bcda→1d140ae58225, marker absent from all 98 built files.registerFlow);'$loopIndex'deleted from the list: anchor 1 → 0, blob →8679f7dab66e,"$loopIndex"absent from dist.$loopIndex, bound in builtin/loop-node.ts. This is the parity pin firing;b022c9d7bcdaandgit diff HEADis empty. After a full spec rebuild, the preflight in default mode finds both markers back indist/,git status --porcelainis empty before and after the build, and the three files show 26 / 18 / 6 passed.Gates
node scripts/pm/dispatch-gates.mjs --commandsderived 88 families from this diff: the dispatch list plus 6 the test edits added (check:engine-double-contract,check:objectql-double-limit,check:query-options-erasure,check:type-check-coverage,check:type-check-debt,check:where-matcher). The derivation was re-run on a throwaway tree atorigin/main35ef501e13with this diff applied, and it printed the identical 88.--ranreconciliation: 88 accounted for, 87 run with exit 0, 0 unrun, 1 NOT MEASURED. Among the 87:check:api-surface,check:export-origins,check:authorable-surface,check:docs,check-adr-0087-registration("1 non-breaking changeset"),check-changeset-no-major,check-empty-changeset,check:published-files,check:nul-bytes,check:doc-authoringandcheck:cross-package-test-inputs.pnpm check:dual-build-cjs-loads. Reason: PREREQUISITE NOT MET (exit 3). The gate reads every package's builtdist/, and 36 packages are unbuilt in this worktree. This diff changes no build config, noexportsand no entry point. Left to CI.Acceptance notes
TheFixed in patch round 1 (flow-bare-dollar-referencehint prescribed a refused hole for a bare$User.Id.d9a5ebbb6f).skills/objectstack-automationonmainsays "{{ $User.Id }}renders blank: assign them to a variable first". Once this lands, the parenthetical is outdated: the hole is refused atos validate/registerFlow/ the node contract. The instruction itself is still right.skills/**is governed and outside this card. Carrier: the skills seat.content/docs/automation/flows.mdx's "you wrote / write instead" table could gain a{{ $User.Id }}row. Nothing on the page is made false by this change: it already says holes read "the engine-set$-named ones". Carrier: none.try_catch'serrorVariable/outputVariablestill accept a$-named variable that a text slot now refuses to read: filed as spec(automation): try_catch's errorVariable and a node's outputVariable accept a $-named variable that a flow text slot now refuses to read (two doors of one contract disagree after #22477) #22502.Generated by Claude Code