Skip to content

feat(service-automation,lint,spec,verify)!: the single-brace resolver and the legacy condition path are deleted — every condition is CEL, a value-slot literal is handed on as written (#19939 pass 4, S7) - #23252

Merged
objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/issue-19939-p4-s7-close-out
Oct 13, 2026

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #19939
Clause-②: no (narrowing)

Pass 4, stage S7 — the close-out of #19939: the single-brace {…} template dialect's resolver is deleted (ADR-0032 Sequencing 6: "delete the single-brace resolver"), with the engine's legacy condition path, the lint's mirror of the grammar, and the canon and pending release text that still described them. Every earlier stage (S1–S6) retired the dialect position by position; this one removes what was left reading it.

维护者速读(草稿)

改了什么

  • 流程(flow)里不再有任何地方读旧的单花括号模板写法 {…}。负责解析这种写法的运行时代码被整段删除。
  • 流程条件(启动条件、连线条件、分支条件、屏幕字段的显示条件)一律按 CEL 表达式求值。还写着 {amount} > 100 这类旧写法的条件会被明确拒绝,并告诉作者正确写法 amount > 100。
  • 顺带修复一个隐蔽缺陷:代码里直接定义的流程如果把日期对象写进字段值,过去会被悄悄写成空对象 {},现在原样写入。
  • 校验工具(os validate)不再按旧写法报告问题,也不再在旧写法的位置给出旧写法的修改建议。
  • 文档、示例和待发布的版本说明里,凡是"旧写法在某处仍然有效"的句子都已改成与现状一致。

为什么改

  • 架构决策 ADR-0032 规定流程只保留一种模板分隔符 {{ }},并要求删除单花括号解析器。S1–S6 已经逐个位置把旧写法退役,解析器只剩两处只会看到普通字面量的调用,以及一段已注册流程根本走不到的条件旧路径。
  • 两种方言并存,正是 AI 和人写流程时最容易混用、出错又不报错的地方。删掉后只剩一种写法,写错会被大声拒绝。

风险与代价(含回滚)

  • 影响面:已注册的流程不受影响,因为注册时本来就拒绝旧写法的条件。受影响的只有直接调用引擎条件求值方法的代码(插件、@objectstack/verify 的测试入口):旧写法从"按文本比较"变为报错。本仓库源码、示例和文档里没有这类调用。
  • @objectstack/lint 移除 4 个导出(旧语法的分类工具)。本仓库和 objectui 都没有引用。
  • HotCRM 仍有 294 处旧写法分布在 28 个源码文件里,这些位置在 S1–S6 已被拒绝,需要在 HotCRM 下次升级依赖版本时一并迁移(本 PR 不改 HotCRM)。
  • 改动规模:53 个文件,+867 / −2451,共 3318 行,其中约三分之二是删除。超过 3000 行,不能走自动合并队列。
  • 回滚:整 PR revert 即可恢复解析器和条件旧路径。没有数据迁移,也没有存储格式变化。

席位意见

你要做的(一个动作)

  • 本 PR 超过 3000 行,需要一位有授权的账号(os-zhuang 或 hotlong)在 PR 上点 Approve。点完后由席位负责合并。

What changes

  • The resolver. service-automation's builtin/template.ts loses interpolate, interpolateString, resolveToken, the value-expression function table and FlowExpressionFunctionError (none was a package export), with their four unit-test files (template-array-index, template-date-offset-dst, template-functions, template-object-token). The file keeps the text-slot renderer (renderTextSlot, textTemplateScope, FlowTextTemplateError).
  • Its two remaining callers. value-slot-map.ts resolveValueSlot (the literal arm every value slot shares) and the assignment executor (logic-nodes.ts, all three shapes) saw literals only, since the value-slot judge refuses every token. They hand on copyLiteral(value): arrays and plain objects copied, every other value as it is. That fixes interpolate() writing a Date as {} (stage plan 6096263424's S7 item), pinned through registerFlow + execute on the three assignment shapes and on create_record / update_record.
  • The legacy condition path. AutomationEngine.evaluateCondition loses the template branch, its hole discriminator, and its text comparison (TEMPLATE_HOLE, templateHoles, refuseUnresolvedTemplateHole, refuseUnresolvedCelOperand, compareValues, unquoteLiteral). Every condition is CEL. A braced string is refused as the CEL brace trap, naming its source. A template or flow envelope is refused before evaluation. Measured first: registerFlow refused all 12 condition spellings probed (start, edge, decision, visibleWhen, wait; bare braced strings and template / flow envelopes), so only a direct call reached the path.
  • The lint. validateFlowTemplatePaths reads {{ }} holes only and skips a CRUD node's filter operands (value slots since S6), so its filter-position error and every finding on a single-brace token are gone. The value-slot and text-slot judges refuse those tokens as expression-invalid. Its 14 single-brace filter fixtures and the reference-integrity suite's one are re-judged. flow-double-brace-interpolation and flow-bare-dollar-reference, at a string outside every slot and literal, say the value is read as written instead of prescribing a single-brace token. flow-template-grammar.ts, the lint's mirror of the deleted grammar (its test read template.ts from disk), is deleted with its four exports.
  • The canon. The FlowSchema example's connector_action input holds a literal, since nothing reads a token there and no connector-input value slot is built. The step-18 registry text: the text-slot entry says no other flow string reads the dialect, and the value-slot entry (flow-value-slot-template-dialect-refused, order 88) names every value slot S1–S6 retired, where it named only pass 1's. flow-template-token.ts is now the judges' frozen reading of a retired grammar. Docblocks in flow-text-slot-template.ts, builtin-node-config.zod.ts and data/context-tokens.zod.ts are updated, with their two reference pages regenerated. Also updated: the verify handle's doc, a formula stdlib comment, and the flows guide's paragraph that still taught a {var} condition.

FROM → TO

you wrote write instead what changes
engine.evaluateCondition('{amount} > 100', vars) engine.evaluateCondition('amount > 100', vars) the template substituted text and compared it; CEL compares the values and fails the run on an absent variable
"{status} == 'open'", '{status} == open' "status == 'open'" a string literal is quoted
{ dialect: 'template', source: "{flag} == true" }, dialect: 'flow' { dialect: 'cel', source: 'flag == true' } —
classifyFlowTemplateToken(inner) (@objectstack/lint) valueSlotTemplateRefusals(value) / textSlotTemplateRefusal(text) (@objectstack/spec/automation), which refuse a token with the spelling that replaces it the grammar it classified is read by nothing
FLOW_TEMPLATE_DATE_FUNCTIONS, FLOW_TEMPLATE_VALUE_FUNCTIONS the CEL stdlib: isoDate(today()), isoDatetime(now()), round, floor, ceil, abs, min, max —
FlowTemplateTokenVerdict nothing: the judges' reading of the retired grammar is internal to @objectstack/spec —

The retired flow-filter-token-unknown rule (S6) gets its own FROM → TO in its changeset, corrected below: rule id flow-filter-token-unknown → expression-invalid (EXPRESSION_INVALID); validateFlowFilterTokens(stack) → validateStackExpressions(stack); FlowFilterTokenFinding → ExprIssue; FLOW_FILTER_TOKEN_UNKNOWN → EXPRESSION_INVALID.

The new changeset is .changeset/19939-flow-single-brace-resolver-deleted.md. Bumps: @objectstack/service-automation major, @objectstack/lint major, @objectstack/spec patch, @objectstack/verify patch. ADR-0087 disposition: not required, already registered (flow-value-slot-template-dialect-refused, flow-text-slot-single-brace-refused), accepted by check-adr-0087-registration.

check-empty-changeset is red on purpose: DELIBERATE CORRECTION

scripts/check-empty-changeset.mjs refuses a diff that modifies a changeset present on the merge base. It names two classes with opposite remedies. COLLISION means restore theirs and rename yours. DELIBERATE CORRECTION means "do NOT restore it — say so on the PR and get it confirmed". This PR is the second class for all 15 files, exactly as #23126 was: each file is a release note that a later landing on the next train made false (S6, 615c696046) or that this PR makes false. Nothing is restored to turn the gate green. It stays red (exit 1, 15 ::error lines, one per file) until the at-tier contract review on this head confirms the correction.

What moved since #23126. The Version Packages PR #21988 merged as d889448b9 and 18.0.0-next.0 is on npm (next dist-tag). That cut consumed every pending changeset into .changeset/pre/, which is where these 15 corrections now sit. They still decide published text. In @changesets/cli 3.0.3, assemble-release-plan drops pre/ changesets only while pre mode is not exit. So the GA cut that exits pre mode reads them, and the 18.0.0 CHANGELOG entry is built from the corrected sentences. The 18.0.0-next.0 CHANGELOG entries already carry the S6-falsified sentences. Those are released entries, and AGENTS.md routes their amendment to a dedicated docs-only PR, never a rider here. That amendment is the seat's carry.

Why each in-place-fix condition holds. (1) Same defect class: every file is pending or prerelease text describing the single-brace dialect, or the retired flow-filter-token-unknown, as still live. (2) Mechanical, with its shape pinned by #23126, which edited the identical "keeps resolving where it still lives" sentences in place. (3) No other claim holds these files: #22586 is closed, and the next Version Packages refresh regenerates from main. (4) The same gate family, with no new verification surface.

# file (under .changeset/pre/) BEFORE AFTER made false by
1 19939-flow-value-slot-template-dialect-refused.md:21 A filter value keeps the template dialect; a text slot (…) reads {{ }} holes and refuses a single-brace token. A text slot (…) reads {{ }} holes and refuses a single-brace token. S6 615c696046
2 22161-lint-slice-10-one-line.md title names "flow-filter-token"; "10 rule ids" (x3), "all 11 ids"; the flows bullet, the os explain list, the os verify list and the runtime-gate sentence name flow-filter-token-unknown the title and lists drop it; 9 / 10 ids; added: "A tenth id this change shortened, flow-filter-token-unknown, is retired in this same release with its rule: a flow filter operand is a value slot, and a token there is refused as expression-invalid by validateStackExpressions (its ExprIssue), with the CEL envelope that replaces it." S6 (the rule retired)
3 22161-lint-slice-14-one-line-fix.md "17 more author-time rule ids" (x2), "10 of the 17 fire", a flow-filters bullet, a flow write bullet, "the two vocabularies a flow filter token resolves in;" 16 / "9 of the 16 fire"; those three items removed; the same retirement sentence (seventeenth id) added S6
4 19939-flow-crud-filter-operands.md:27, :43 the rule's exports "are removed rather than left reporting…"; "this one supersedes that line: read the user record with filter: { id: { dialect: 'cel', source: 'current_user.id' } }" + the FROM → TO for the retired rule id and exports (above); the supersede sentence dropped, its row corrected in row 11 this PR (row 11)
5–10 19939-flow-callee-input-value-slots.md:32, -collection-value-slots.md:38, -http-slots.md:44, -notify-value-slots.md:44, -screen-value-slots.md:41, -value-slot-date-macros-refused.md:47 "The single-brace dialect (, date macros included,) keeps resolving where it still lives, a CRUD filter value." (sentence removed) S6
11 19939-flow-value-slot-run-user-refused.md:22, :30, :44 "{$User.*} keeps resolving where the single-brace dialect still lives, a CRUD filter value."; the $User.Email row: "an assignment of uid: …, a get_record on sys_user with filter: { id: '{uid}' }"; "and every other position keeps the single brace" (sentence removed); "a get_record on sys_user with filter: { id: { dialect: 'cel', source: 'current_user.id' } }"; "and the hint at a node's literal, or at any other position, names the value itself" S6; this PR (the lint hint)
12 22110-flow-text-slot-double-brace.md:24 Unchanged: a CRUD filter value keeps the single-brace dialect, and the value slots are CEL's. Unchanged: the value slots are CEL's. S6
13 22301-verify-evaluate-condition-door.md:12 …a string, which the engine reads as CEL unless it carries a {var} hole (the legacy template dialect). …a string, which the engine reads as CEL; a {var} hole is refused as the brace trap, and a template or flow envelope is refused too (the legacy template dialect is deleted in this release). this PR
14–15 22502-flow-binding-variable-dollar-name-refused.md:33, 22572-flow-binding-name-dollar-refused.md:41 (a text-slot hole, a CEL expression, a single-brace token in a value position) (a text-slot hole, a CEL expression) S6

The measured lengths slice 10 states (at most 194; 222 to 441) still hold for its nine remaining ids: PR #22828's table puts flow-filter-token-unknown at 267–278 → 181–192.

Acceptance notes

  • HotCRM, for its next pin bump. Census at hotcrm 0f84c9c (read only): a TS-AST walk of 494 code files, judged with the spec's own classifiers (classifyFlowFilterOperandString, the frozen token grammar). In src, 294 strings carry a single-brace token at positions [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 retired, in 28 files: fields.* 94, CRUD filter operands 71 (none a query placeholder), notify templateData.* 35, http body 34, notify recipients 19, notify actionUrl 19, loop collection 10, assignments.* 6, subflow input.* 5, screen description 1. Most are paths (about 260), with 26 date macros and 7 $User paths. There are none at the positions S7 itself retires: 0 braced conditions, 0 connector_action inputs. Tests add 14 more. The seat records this on its ledger.
  • objectui (companion card, filed by the seat): the designer still teaches the single brace. See inspectors/flow-node-config.ts :690 and :706 (collection placeholders, refMode: 'template') and json-schema-to-fields.ts :259 and :295. The simulator re-implements the deleted interpolator (previews/simulator/flow-simulator.ts :455–581). The pinned console imports none of the removed lint exports (git grep exit 1; control exit 0), so the Console Pin Gate is not tripped.
  • Noted, not filed: the header of packages/formula/src/stdlib-timestamp-text.test.ts still describes the deleted resolveToken / interpolateString in the present tense; its pinned bytes stay valid as a frozen record of what they measured. Carrier: none.
  • Not in this PR, by the seat's call: the service-automation: a built-in node's config value its own contract refuses still registers, then fails every run — the built-in half of #21848's class #21898 token carve-out (builtinValueJudged / carriesInterpolationToken). A get_record with limit: '{x}' registers, and every run refuses it (probe). The seat files it as this family's tail card.

Verification

All at head 05539d423 (origin/main 01a3421f4 merged through scripts/pm/os-regen-merge.sh, plus one fold commit), unless stated. Measured size: 53 files, +867 / −2451 = 3318 changed lines against main, generated files included.

  • Merge and fold. b1f1c0ffc merges origin/main 01a3421f4 with a merge commit, through scripts/pm/os-regen-merge.sh: no conflict, nothing regenerated by hand. That merge brought in the 18.0.0-next.0 cut, which is what moved the 15 corrected files under .changeset/pre/. 05539d423 folds S7's canon fix: the order-88 rationale fragment in registry.ts.template, plus one sentence in the new changeset. The fragment's projections (registry.ts, the upgrade-guide pages, spec-changes.json) are gitignored build output, so nothing committed derives from it. The full build regenerates them, and check:generated, check:upgrade-guide and step18-rationale-merge.test.ts read them green.

  • Build. pnpm turbo run build --filter=!@objectstack/docs --concurrency=2, under the verify lock: 74 successful, 74 total. The full dist closure is what makes check:dual-build-cjs-loads measured (exit 0) rather than exit 3.

  • Derived gates. node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, with no paths, on 05539d423 derives 114 commands. Each ran once, with its exit code captured before any pipe.

    • 113 exit 0, among them check:authorable-surface, check:generated, check:upgrade-guide, check:skill-examples, check:dual-build-cjs-loads, check:engine-double-contract, check:nul-bytes, check-adr-0087-registration and check-changeset-no-major.
    • 1 exit 1: check-empty-changeset --base origin/main, with 15 ::error lines. This is by design, as described above.
    • --ran reports: "114 derived famil(ies) accounted for — 114 run, 0 NOT-MEASURED (a DERIVED zero — all 114 recorded an exit code and none of them is 3)".
  • Suites, on the merged head, under the lock:

    • @objectstack/service-automation: 185 files, 2449 tests passed.
    • @objectstack/lint: 137 files, 6662 tests.
    • @objectstack/verify: 38 files, 301 tests.
    • @objectstack/spec: src/automation src/migrations src/data/context-tokens 41 files, 1806 tests; --project repo scripts/step18-rationale-merge.test.ts 1 file, 9 tests.
    • turbo run typecheck on service-automation, lint, verify, formula and spec: 53 successful, 53 total.
  • Downstream consumer sweep. Prefix direction: ...@objectstack/service-automation, ...@objectstack/lint and ...@objectstack/verify together give 55 packages. Each package ran alone with --maxWorkers=2, all exit 0:

    • metadata-protocol: 233 files passed, 3 skipped; 28284 tests passed, 19 skipped.
    • objectql: 412 files, 8041 tests.
    • plugin-approvals: 74 files, 1051 tests.
    • trigger-record-change: 11 files, 114 tests.
    • trigger-schedule: 8 files, 174 tests.
    • example-crm: 6 files, 46 tests.
    • example-showcase: 33 files, 409 tests.
    • example-todo: 7 files, 238 tests.
    • @objectstack/cli --project unit: 285 files, 4283 tests.

    The cli integration tier is declared to CI, since the diff touches no integration-tier file and no spawn entry. The four removed lint exports have zero hits in the tree (git grep exit 1; control exit 0), and the full build compiles every package's declarations.

    NOT MEASURED locally: the other 43 packages of the closure. The reason is the shared container: a first, single turbo run of the sweep was SIGKILLed (exit 137) inside metadata-protocol while the gate battery ran beside it. A git grep over those 43 finds no evaluateCondition call, no deleted-symbol or flow-template-* reference, and no interpolator call. The only hits are service-messaging's own {{ }} renderer and prose in the dogfood ledger. CI runs them.

  • Ablations, at 56811ae93. The three mutated files and five test files are byte-identical at 05539d423 (git diff --quiet exit 0); the suites above re-ran green on the merged head, and the ablations were not re-run there. Each used scripts/ablation-replace.mjs wrap mode under the lock: anchor hits 1 then 0, a blob change proven, then restore with blob == HEAD and git diff HEAD empty, plus a shell trap that restores HEAD by absolute path with a hash-object check.

    • A1, copyLiteral walking every object (value-slot-map.ts): 6 red (the three assignment Date shapes, the copyLiteral unit, the create_record / update_record Date).
    • A2, the template / flow envelope refusal removed (engine.ts): 2 red.
    • A3, the lint reading single-brace tokens again: 1 red.
    • A4, the lint scanning CRUD filter operands again: 9 red.
    • Control on the restored tree: green for both suites. No dist leg is owed, because both suites import their own src.
  • ESLint, a measured narrowing at 05539d423: eslint --no-inline-config --format json over the 47 changed, non-deleted files, read off eslint's own JSON. 27 files are linted with 0 errors and 0 warnings. The other 20 (16 md, 3 mdx, 1 .template) are ignored by the config. eslint.config.mjs enables no type-aware linting (no parserOptions.project), so the diff cannot move the verdict on an untouched file.


Generated by Claude Code

… legacy condition path (#19939 S7)

Claude-Session: https://claude.ai/code/session_01BRQ9Mp1yH7R1az6MaUM18H
Co-authored-by: Claude <noreply@anthropic.com>
…d docblocks follow the deleted resolver (#19939 S7)

Claude-Session: https://claude.ai/code/session_01BRQ9Mp1yH7R1az6MaUM18H
Co-authored-by: Claude <noreply@anthropic.com>
…tirement reached (#19939 S7)

Claude-Session: https://claude.ai/code/session_01BRQ9Mp1yH7R1az6MaUM18H
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 5 package(s): @objectstack/formula, @objectstack/lint, @objectstack/service-automation, @objectstack/spec, @objectstack/verify, touching 58 documentable anchor(s). ⚠️ 5 changed file(s) yielded no anchor (packages/lint/src/index.ts, packages/spec/src/automation/builtin-node-config.zod.ts, packages/spec/src/automation/flow-text-slot-template.ts, …), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/automation/flows.mdx (via evaluateCondition (symbol, a method of class AutomationEngine))

⛔ 4 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v16.mdx (via AutomationEngine (symbol, a top-level class))
  • content/docs/releases/v17/17-0.mdx (via AutomationEngine (symbol, a top-level class))
  • content/docs/releases/v17/17-4.mdx (via evaluateCondition (symbol, a method of class AutomationEngine))
  • content/docs/releases/v17/17-6.mdx (via AutomationEngine (symbol, a top-level class))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 5 changed file(s) yielded no anchor (packages/lint/src/index.ts, packages/spec/src/automation/builtin-node-config.zod.ts, packages/spec/src/automation/flow-text-slot-template.ts, …) — pages documenting those are invisible to this run
  • 11 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 142 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json afbf960c2649d87ff6503721b9c290ec679356c6 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 239173ff7a08ef0c4dd7167b94c38343f3fa1026 — the merge of head 05539d4230bb5487498d664af0ea563f79950f76 into base afbf960c2649d87ff6503721b9c290ec679356c6, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 239173ff7a08ef0c4dd7167b94c38343f3fa1026 && git checkout 239173ff7a08ef0c4dd7167b94c38343f3fa1026
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin afbf960c2649d87ff6503721b9c290ec679356c6 05539d4230bb5487498d664af0ea563f79950f76 && git checkout -B drift-repro afbf960c2649d87ff6503721b9c290ec679356c6 && git merge --no-ff 05539d4230bb5487498d664af0ea563f79950f76

node scripts/docs-audit/affected-docs.mjs --json afbf960c2649d87ff6503721b9c290ec679356c6

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs afbf960c2649d87ff6503721b9c290ec679356c6 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 05539d4230bb5487498d664af0ea563f79950f76
Local-runs: none

Read: card #19939 (body and all 76 comments; the dispatching seat's amendment 6143270685 was read as the file-surface order, never as evidence), PR #23252 (body, its one comment, the 53-row file list, the net diff origin/main...05539d423, merge base 01a3421f4), the 41 check-runs on the head with every annotation, the Check Changeset job log, PR #23126 with card #22586 and its 13 comments (the deliberate-correction pattern and its contract review 6136739998), origin/main at 4f5679c26 read-only through git show and git grep, objectui origin/main 866763bda and the pinned 1fb1ced36, and the read-only hotcrm clone at 0f84c9c. Nothing was built, run or re-run. This record is the written confirmation check-empty-changeset asks for on its 15 DELIBERATE CORRECTION rows (landing-operations.md: a same-head at-tier PASS record is that confirmation).

① Derived judgments

A. What leaves @objectstack/service-automation, and who could reach it.

  • AutomationEngine.evaluateCondition loses its template path: a bare string carrying a {var} hole was substituted and compared as text; it is now the CEL brace trap, refused with its source, and a template or flow envelope is refused before evaluation where it used to be taken "at the author's word". RIGHT, declared, and named with a FROM → TO. The method is public on the engine class and on @objectstack/verify's stack.automation.evaluateCondition; it is not in @objectstack/spec's contracts (git grep evaluateCondition origin/main -- packages/spec/src/contracts/: 0 hits). No registered flow reaches the deleted path: registration judges every structural predicate (start and edge condition) and every ledger-declared predicate slot (decision expression, screen visibleWhen) as bare CEL, where {…} is the Record-change trigger plugin loads but flows never fire on data writes (7.4.1) #1491 brace trap (engine.ts validateFlowExpressions docblock on origin/main); that a template or flow envelope is refused at those same slots is the dev's 12-spelling probe (12 of 12 refused), consistent with ADR-0032 §1a and not re-run here. Callers outside this repository: objectui at the pin and at main call no engine evaluateCondition (its hits are @object-ui/core's own evaluator); hotcrm's seven test call sites pass a { dialect: 'cel', source } envelope or a brace-free string (test/helpers/verify-stack.ts wraps a string into the CEL envelope itself), and hotcrm src and test carry 0 braced conditions and 0 template / flow condition envelopes. The direct-call pins (engine.test.ts, nested-region-parity.test.ts, verify's door test) are re-pinned to the refusal; the structural shape gate (service-automation: evaluateCondition still throws a raw TypeError: exprStr.trim is not a function on a non-string envelope source — registration refuses the shape, evaluation faults unattributed #16038) is untouched and its test file is not in the diff.
  • interpolate, interpolateString, resolveToken, stringifyForTemplate, the function table and FlowExpressionFunctionError are deleted with four unit files. Unreachable from any published accept set: src/index.ts on origin/main exports nothing from builtin/template.ts, and no file outside the package imports it (git grep "builtin/template" origin/main -- packages/ outside service-automation: 0 hits). RIGHT, no FROM → TO owed.
  • copyLiteral replaces the two residual interpolate() calls (resolveValueSlot's literal arm, the assignment executor's three shapes). For a string the judge admits the two are the identity alike: the interpolator's token regex and the value-slot judge's TEMPLATE_TOKEN are the same /\{([^{}]+)\}/, so every string the interpolator would have rewritten (a double-brace hole included, since the inner { x } matches) is refused at every door first; a lone { or {} passes both unchanged. For an object the difference is the declared fix: the interpolator walked every object's own entries and wrote a Date as {}; copyLiteral walks plain objects and arrays only. Pinned through registerFlow + execute on the three assignment shapes and on create_record / update_record. RIGHT, and the stage plan's S7 item.

B. What leaves @objectstack/lint.

  • classifyFlowTemplateToken, FLOW_TEMPLATE_DATE_FUNCTIONS, FLOW_TEMPLATE_VALUE_FUNCTIONS and the type FlowTemplateTokenVerdict are removed with flow-template-grammar.ts (whose test read template.ts from disk, so the deletion forces it). Consumers measured: this repository 0 outside the lint package (the only reader, lint-flow-patterns.ts, now asks $User. directly); objectui origin/main and the pin 0; hotcrm 0. Each is named with a FROM → TO in the changeset. RIGHT.
  • validateFlowTemplatePaths reads double-brace holes only and skips a CRUD node's filter operands, so its error at a filter operand and every single-brace finding are gone, and flow-template-unknown-field / flow-template-lookup-traversal report warning only. A narrowing of findings whose replacement is named: a token at a filter operand is expression-invalid from the value-slot judge (validateStackExpressions walks flowNodeValueTemplateRefusals over the ledger's filterOperands on origin/main), stricter than the dropped error. RIGHT.
  • flow-double-brace-interpolation and flow-bare-dollar-reference, at a string outside every value slot, text slot and declared literal, say the value is read as written where they prescribed a {var} token. RIGHT: nothing interpolates there (no executor outside the package ever imported the interpolator, and plugin nodes receive their config raw).

C. @objectstack/spec, @objectstack/verify, @objectstack/formula, docs. Spec moves docblocks (flow-text-slot-template.ts, builtin-node-config.zod.ts, data/context-tokens.zod.ts, the package-internal flow-template-token.ts), the FlowSchema example's connector_action input (a literal; no connector-input value slot is built, as the stage plan orders), and two step-18 rationale fragments in registry.ts.template (the text-slot entry no longer says every other flow string keeps the dialect; the order-88 value-slot entry names every slot S1–S6 retired). No export, schema or authorable key moves; the two reference pages are regenerated and check:docs / check:upgrade-guide are green on the head. Verify: a docblock. Formula: two comments, on the module-private isoTimestampText and inside registerStdLib. content/docs/automation/flows.mdx replaces the paragraph that still taught a {var} start condition; the four release-owned pages the drift check names are untouched. All RIGHT.

D. Scope against the order. The stage plan 6096263424's S7 row (resolver deletion, the engine's legacy condition path, the canon fixes; the FlowSchema example; the Date literal; Fixes #19939), S6's carry line 6138926915 (the three changeset corrections; the lint's single-brace filter fixtures and the bare-$ hint's single-brace arm; the step-18 text-slot line) and the seat's amendment 6143270685 together name every one of the 53 files; nothing in the diff falls outside them, and the two things the order excludes are excluded (the #21898 carve-out is untouched; no connector-input slot is built). The carry line's closing condition holds: every position is under a judge with executor evaluation, and the resolver behind them is gone.

E. The 15 in-place corrections, each judged on this head. Invariants measured on all 15 between origin/main and the head: frontmatter bump block identical 15/15, Clause-② line identical 15/15, ADR-0087 marker identical 15/15 (present in 12, absent in the three patch/minor notes on both sides), no control byte, every hunk below the frontmatter. TRUE means the AFTER states this head; DROP-OK means the removed text is false on this head and nothing true went with it.

  1. 19939-flow-value-slot-template-dialect-refused.md:21 — "A filter value keeps the template dialect" dropped. DROP-OK (S6 615c696046: a filter operand is a value slot; this head deletes the last reader).
  2. 22161-lint-slice-10-one-line.md — title and lists drop flow-filter-token-unknown; 10 → 9 ids, 11 → 10; the retirement sentence added. TRUE: the rule, its exports and its RULE_EXPLANATIONS entry are absent on origin/main (git grep flow-filter-token-unknown -- packages/lint/src packages/cli/src: only the grammar mirror's comment, deleted here); validateStackExpressions, ExprIssue and EXPRESSION_INVALID = 'expression-invalid' exist. Counting the listed ids gives 9 and the os verify list drops the one error-severity id. The length claim (at most 194; 222 to 441) is unchanged for the nine.
  3. 22161-lint-slice-14-one-line-fix.md — 17 → 16, "10 of the 17" → "9 of the 16", the flow-filters bullet, the flow write bullet and the "two vocabularies" clause removed, the same retirement sentence added. TRUE by the same evidence; the "Never at the runtime gate" seven plus the nine at the door are the sixteen.
  4. 19939-flow-crud-filter-operands.md:27, :43 — the retired rule gains its FROM → TO (rule id, validateFlowFilterTokens → validateStackExpressions, the finding and constant types) and the supersede sentence quoting the run-user {uid} row is dropped. TRUE / DROP-OK: the quoted row is corrected in its own file (item 11), which is docs(changeset): correct pending v18 changesets that later train landings made false, in place (deliberate correction) #23126's H3 rule.
    5–10. callee-input-value-slots.md:32, collection-value-slots.md:38, http-slots.md:44, notify-value-slots.md:44, screen-value-slots.md:41, value-slot-date-macros-refused.md:47 — "The single-brace dialect (, date macros included,) keeps resolving where it still lives, a CRUD filter value." dropped. DROP-OK, false since S6.
  5. 19939-flow-value-slot-run-user-refused.md:22, :30, :44 — the same sentence dropped; the $User.Email row reads the user record with filter: { id: { dialect: 'cel', source: 'current_user.id' } } (the assignment hop gone); the lint line says the hint at a node's literal, or at any other position, names the value itself. TRUE: the filter envelope is evaluated in the flow's CEL scope, which binds current_user; the lint's LITERAL_HINTS end "Write the value itself." and the new ELSEWHERE_HINT does too.
  6. 22110-flow-text-slot-double-brace.md:24 — "a CRUD filter value keeps the single-brace dialect" dropped from the Unchanged line. DROP-OK.
  7. 22301-verify-evaluate-condition-door.md:12 — the string is read as CEL; a {var} hole is refused as the brace trap and a template / flow envelope is refused too. TRUE on this head (the engine diff), and false before it, which is why it rides here.
    14–15. 22502-…:33, 22572-…:41 — "a single-brace token in a value position" dropped from the dialects a flow string speaks. DROP-OK.
    Cross-reference sweep over every changeset at the head (root and pre/, 522 files) for each removed sentence and for the removed symbols: 0 remaining clauses quote a corrected sentence; flow-filter-token-unknown appears only in the S6 note and the two corrected slices; the four removed lint exports appear only in the new changeset's FROM → TO. The corrections are the docs(changeset): correct pending v18 changesets that later train landings made false, in place (deliberate correction) #23126 shape: pending release text a later landing on the same next train made false (S6) or this head makes false (items 11's lint line and 13), rewritten in place, no restoration, each row naming the landing, confirmed by this record.

F. Keeping the corrected files under .changeset/pre/: RIGHT. The merge of origin/main brought in #21988 (18.0.0-next.0, merged 2026-10-13T08:04Z), whose v3 cut moved the 15 files into .changeset/pre/; pre.json is {"mode":"pre","tag":"next"} and tracks no list. The repository's own release tooling treats that directory as the input the next version step re-applies: release.yml's post-version validation counts .changeset/pre/*.md as pending, check-changeset-no-major.mjs records the v2 mechanism the move replaces (consumed notes kept "so the final release could re-apply them, and only the POST-EXIT changeset version deleted them"), and check-adr-0087-registration still judges an edited pre/ row. So the GA 18.0.0 CHANGELOG is built from these files, and restoring them from base would print the false sentences there. The 18.0.0-next.0 entries already compiled carry the falsified text (③ item 5): released, so not this PR's to edit.

G. The red Check Changeset is the designed refusal and only that. The job log: the count step passes (1 changeset added), check-empty-changeset exits 1 on exactly the 15 .changeset/pre/ rows, each a DELIBERATE CORRECTION, check-adr-0087-registration passes (1 declared-breaking changeset, disposition not-required / already-registered), the launch-window major guard stands aside in pre mode, and the LEVEL AXIS passes (no (narrowing) accounted by the two majors; spec and verify graded patch, not refused). The 10 failure annotations are the runner's per-step cap on the 15 lines. The context is not one of the required five. No other red on the head.

H. Check-runs on 05539d423: 41, all completed — 35 success, 5 skipped, 1 failure. Required: Lint & Repo Gates success, TypeScript Type Check success, Build Core success, Governed Surface Queue Guard success, Test Core success (the aggregate and all six shards). Advisory: every Lint · and Type Check · slice success (Type Check · source gates notices the ADR-0087 projection moved, as the registry edit intends), Dogfood Regression Gate success, Build Docs, Spec property liveness, Check PR Size, the five claim and single-writer guards, Check Documentation Links success; Console Pin Gate, Temporal Conformance, Dogfood Verify CLI, the dogfood shards and the packed-tarball smoke skipped by path filter; Check Changeset failure as in G. The PR is not governed (no .claude/**, skills/**, docs/adr/**, AGENTS.md, CLAUDE.md, docs/NORTH-STAR.md path), and at +867 / −2451 = 3318 changed lines it lands only on an authorized APPROVED review and the owning seat, or a human merge, as the seat's route B records; this record does not stand in for that approval.

② Semver level

  • .changeset/19939-flow-single-brace-resolver-deleted.md: @objectstack/service-automation major (a public method narrows), @objectstack/lint major (four exports removed, two rules narrowed), @objectstack/spec patch (docblocks, an example, rationale text; no surface moves), @objectstack/verify patch (a docblock). Each matches what the diff publishes. @objectstack/formula is touched by two comments on a module-private function and carries no bump: nothing publishes from it, so none is owed.
  • Clause-②: no (narrowing) on the PR and in the changeset: RIGHT. No accept set and no public surface widens — the Date now reaching a write was accepted before and mangled, a fix, not an admission; the lint's dropped error is a narrowing of findings. The narrowing arm is the BREAKING, carried by the two majors and the **BREAKING** banner.
  • ADR-0087 disposition not-required (already-registered flow-value-slot-template-dialect-refused, flow-text-slot-single-brace-refused): RIGHT. No authorable surface changes: every value and text slot already refuses the token under those two step-18 D3 entries, registration already refuses a braced predicate at every condition slot (the engine's own docblock) and, by the dev's probe, a template or flow envelope there, and the residual calls saw literals only; the order-88 rationale is amended to name every slot the retirement reached, so the upgrade guide's entry stops naming pass 1's positions alone. No D2 conversion is right: nothing here is a lossless rewrite.
  • skip-changeset absent: RIGHT, a changeset ships and the correction class bars the label.

③ Boundary flags

Newest report 6145295543, every item; round 1's 6143212658 where it still stands.

  1. The service-automation: a built-in node's config value its own contract refuses still registers, then fails every run — the built-in half of #21848's class #21898 token carve-out (builtinValueJudged / carriesInterpolationToken; a get_record with limit: '{x}' registers and every run refuses it) — a contract change, rightly not a rider; the seat files it as the family's tail card (6143270685). Carrier named; not folded here.
  2. objectui's designer and simulator still teach and emulate the single brace (flow-node-config.ts, json-schema-to-fields.ts, flow-simulator.ts on objectui main); the pin imports none of the removed exports (measured here too: 0 hits at 1fb1ced36 and at 866763bda). Carrier: the companion card the stage plan orders the seat to file.
  3. hotcrm: 294 single-brace strings at positions S1–S6 retired, in 28 src files at 0f84c9c, none at a position S7 retires; its tests reach evaluateCondition with CEL envelopes or brace-free strings only (measured here). Carrier: the seat's ledger for the pin bump; nothing S7 adds breaks hotcrm beyond what S1–S6 did.
  4. packages/formula/src/stdlib-timestamp-text.test.ts's header still describes resolveToken / interpolateString in the present tense — a frozen record of what it measured; noted, not filed, rightly.
  5. The 18.0.0-next.0 CHANGELOG entries of @objectstack/lint, @objectstack/service-automation and @objectstack/spec carry the S6-falsified sentences (six "keeps resolving where it still lives" rows in service-automation's alone). Cause: S6 landed 2026-10-13T04:56Z and chore: version packages (next) #21988 merged 2026-10-13T08:04Z, so S6's carry "lands before the first next publish that carries S6" was overtaken by the cut. Those are released entries; AGENTS.md routes their amendment to a dedicated docs-only PR, never a rider on code — this PR rightly leaves CHANGELOG.md untouched. ESCALATED as the seat's carry: one docs-only amendment PR on the three 18.0.0-next.0 entries, before GA.
  6. Deviation: the merge brought in chore: version packages (next) #21988 and the corrections are kept under pre/ — judged RIGHT in ① F.
  7. Deviation: the ESLint narrowing re-measured against the merge base — the reported run is the right one; not a contract matter.
  8. Deviation: the harness attribution reminder — the PR body carries the session-URL footer AGENTS.md prescribes and no commit was made this round; not a contract matter.
  9. Deviation: the relay's proxy address line — the tool's own output; nothing to answer.
  10. Round 1, still standing: the file surface widened past the claim — amended by the seat (6143270685) and every file of the head is inside the amended surface (① D); the shared-checkout cd slip — both commands were reads or no-ops and were redone in the worktree, no write landed; the size line — answered B by the seat, the end-game stated in ① H; round 1's order-88 finding — folded into the fold commit 05539d423 and verified in ① C.

No FAIL item. The one escalation is item 5, a carry on released text that this PR could not carry itself.

Implemented-by: claude/issue-19939-p4-s7-close-out
Reviewed-by: session_01BRQ9Mp1yH7R1az6MaUM18H

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

维护者速读(终稿)

domain:spec seat 2 · os-justin · session session_01BRQ9Mp1yH7R1az6MaUM18H · 2026-10-13T12:50Z · head 05539d423。契约复审档复核 PASS:6145835958;席位验收 6145940355(在 #19939)。

改了什么: 这是 #19939 系列(退役流程里的单花括号模板写法 {…})的收尾一步。

  • 负责解析这种写法的运行时代码整段删除。
  • 流程的各类条件(启动、连线、分支、屏幕显示)一律按 CEL 表达式求值。还写着 {amount} > 100 这类旧写法的条件会被明确拒绝,并给出正确写法。
  • 顺带修好一个隐蔽缺陷:直接在代码里定义的流程,如果把日期对象写进字段值,过去会被悄悄存成空对象 {},现在原样写入。
  • 校验工具不再按旧写法报告问题,@objectstack/lint 移除 4 个只为旧写法服务的导出。
  • 15 份尚未发布 GA 的版本说明里,有些句子说"旧写法在某处仍然有效",这些句子已就地改成与现状一致。

为什么改: ADR-0032 规定流程只保留一种模板分隔符 {{ }},并要求删除单花括号解析器。前六步(S1–S6)已经逐个位置把旧写法退役:注册时就拒绝旧写法,解析器实际上只剩两处只会遇到普通值的调用。两种写法并存,正是 AI 和人写流程时最容易混用、出错又不报错的地方。删掉后只剩一种写法,写错会被当场拒绝。

风险与代价(含回滚):

席位意见: 建议批准。

你要做的: 在本 PR 上点 Approve(请审已发给 os-zhuang 与 hotlong)。批准后由席位过落地前检、摘标、翻 ready 并入合并队列。


Generated by Claude Code

@os-zhuang
os-zhuang marked this pull request as ready for review October 13, 2026 13:03
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:data size/xl tests tooling

Projects

None yet

3 participants