Skip to content

fix(spec,service-automation,lint)!: registration and validate judge a connector_action's connectorConfig.input value slot - #28150

Merged
objectstack-fleet[bot] merged 8 commits into
mainfrom
claude/project-thread-l0j2zg
Oct 30, 2026
Merged

objectstack-fleet[bot] merged 8 commits into
mainfrom
claude/project-thread-l0j2zg

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

domain:spec#1 · os-tesla · session_01L9qibwb6hBqszPTYJobitm

Requested by Tesla · project thread

Fixes #27968

维护者速读

  • 改了什么:流程里"调用连接器"节点的输入值,现在在注册流程和 objectstack validate 时就会被检查,和子流程的输入用同一条规则。
  • 为什么改:之前写错的计算表达式、旧的 {…} 模板写法能顺利保存和上线,要等流程真正跑到这一步才失败,或者把 {record.title} 这样的原文直接发给外部系统。
  • 风险与回滚:已经存在、带这三种写法的流程会被拒绝注册(启动时打一条 warn 并跳过该流程,其他流程照常);仓库内只有示例应用的一处 Slack 文案命中,已改写。回滚只需 revert 本 PR,不涉及数据迁移。
  • 先看:packages/spec/src/automation/flow-node-expression-paths.ts 新增的 connector_action 条目和"兄弟区块"根,以及两个新测试文件。
  • 两分钟验证:pnpm --filter @objectstack/service-automation exec vitest run src/builtin/connector-input-registration.test.ts 和 pnpm --filter @objectstack/lint exec vitest run src/validate-expressions.connector-input.test.ts,三种写法都被拒绝,合法表达式照常注册并运行。
  • 需要你做的:无。席内契约复审 PASS(评论 6525595405),由席位落地;skills 下的过时说明已立卡 skills(automation): the connector input recipe still says os validate does not judge connectorConfig.input, which PR #28150 makes false #28162 交 skills 车道。

What changes

  • The ledger gains a sibling-block root (packages/spec/src/automation/flow-node-expression-paths.ts). A FlowNodeExpressionPath may now name block, the node property its path is rooted in. It defaults to config, so every existing entry keeps its meaning. FLOW_NODE_SIBLING_EXPRESSION_BLOCKS lists the addressable blocks (today only connectorConfig → connector_action). resolveFlowNodeExpressions and resolveFlowNodeValueSlots take the node's sibling blocks as an optional third argument, and each result carries its block. The new entry is connector_action · connectorConfig · input.* · value.
  • One judge, no second list. flowNodeValueTemplateRefusals (in flow-value-slot-template.ts) reads the same entry and reports block. A sibling-block slot takes the generic value-slot remedy, without the callee sentence a child flow's input adds. flow-node-config-refusals.ts skips block-rooted entries, because its walk addresses only config.
  • Both doors pass the node. In service-automation, flow-registration-checks.ts locates a refusal at connectorConfig.input.<key>. In lint, validate-expressions.ts does the same, so objectstack validate and the runtime authoring gate judge the map too. No service-automation-only judge was added (triage 6487582764).
  • Declared in the schema. FlowNodeSchema.connectorConfig.input values carry .meta({ xExpression: 'value' }) and stay z.unknown(), so FlowSchema.parse is unchanged. The describe text now says what a value is.
  • The ratchet reads a third channel. getFlowNodeSiblingBlockJsonSchemas() hands the sibling-block JSON Schema to config-expression-ledger.test.ts. A marker on a block the ledger cannot address fails the ratchet.
  • Declared rider. The stale doc comment in the FlowSchema example (formerly flow.zod.ts:950–951, "handed to the action as written: no template is read there") now reads "Each connectorConfig.input value is a literal, handed to the action as written, or a CEL value envelope evaluated in the run's scope before dispatch".
  • Other changes.
    • The engine.ts evaluateValueEnvelope docblock is updated (comment only).
    • The ADR-0087 D3 entry is 18.connector-action-input-value-slot-judged.
    • There are three changesets, one per package.
    • The regenerated artefacts are api-surface/automation.json, export-origins/automation.json and content/docs/references/automation/flow.mdx.

File-surface amendments (producer side, named per the order):

  • examples/app-showcase/src/automation/flows/index.ts: TaskCompletedSlackFlow's text: '✅ Task done: {record.title}' is now { dialect: 'cel', source: "'✅ Task done: ' + record.title" }. The new judge refuses the old spelling. It never interpolated: Slack received the braces as written.
  • packages/metadata-protocol/src/runtime-authoring-gate.flow-credential-literal.test.ts and packages/cli/test/flow-credential-literal-doors.e2e.test.ts: the DARK variant's connector input '{api_token}' is now a CEL envelope, { dialect: 'cel', source: "{'clientSecret': api_token}" }. Its intent is unchanged: a non-literal credential the advisory does not flag.

Why

The executor half (#27980) evaluates every top-level CEL envelope in connectorConfig.input. The build doors judge value slots only through the ledger, and the ledger could address nothing outside node.config. Under connectorConfig.input, registerFlow and objectstack validate accepted three shapes that they refuse under a subflow's config.input: a malformed envelope, a template envelope and a {…} token. That gave one slot two reject sets, and a malformed envelope was first met by a run. Triage 6516097905 ruled that the ledger gains the sibling block and its consumers read it with no second list.

Clause-②: no (narrowing)
Metadata-protocol: narrowed
Skills-docs: follow-up #28162

Verification

Final head 6a06a420 (merged with origin/main 91a423ef). The suites and the gate sweep ran on 0a88f925 (origin/main 6b125a7a). After the last merge, the closure was rebuilt and the new test files, the ledger ratchet, residual-top-level-flow-boot.test.ts (from #28048), check:generated and full dogfood were re-run.

  • Rejection tests.
    • service-automation connector-input-registration.test.ts has 6 tests. Its three cells are a malformed CEL envelope ({ dialect: 'cel', source: 'rec.' } → VALUE_ENVELOPE_REFUSAL), a template envelope (→ VALUE_ENVELOPE_REFUSAL) and '{rec.id}' (→ VALUE_SLOT_TEMPLATE_REFUSAL). Each asserts node 'call' (connector_action) connector_action input value at connectorConfig.input.customerId: <refusal>, checks the literal key beside it is not named, and compares against a subflow config.input control that gives the same refusal. The file also shows that a refused flow cannot run (the handler is never called), that a valid envelope plus literals registers and the run evaluates it, and that a node with no input registers.
    • lint validate-expressions.connector-input.test.ts has 5 tests: the same three shapes at connectorConfig.input.<key>, a valid envelope passing, and the runtime authoring gate refusing with rule expression-invalid.
  • Ablation (scripts/ablation-replace.mjs, each restored to HEAD): every one of the four consumer call sites going back to its two-argument form turns red.
    • registration resolveFlowNodeExpressions → 2 envelope cells red;
    • registration flowNodeValueTemplateRefusals → token cell and not-registered test red;
    • lint resolve → 2 envelope cells red;
    • lint template → token cell and runtime-gate test red.
  • Suites.
  • Full dogfood (owed by the narrowing arm, under the verify lock, closure built): 358 files / 2883 tests green, 20 skipped, on 6a06a420. An earlier run on 0a88f925 had 1 red (api-action-door-forward, the main defect main red (Dogfood Regression Gate 3/3) since 3e0fd7a8 (#27964): the api-action door dogfood pin still expects sys_metadata_activation in the disabled-action sentence that #27842 rewrote #28082 fixed by test(dogfood): the api-action door's disabled pin reads #27842's sentence, not the ledger table name #28095), which is why main was merged again.
  • Gates. dispatch-gates --commands derived 112 commands, each run </dev/null with its exit recorded. --ran reconcile: 112 derived, 112 run, 0 NOT-MEASURED, 0 UNRUN. Three gates first exited 3 (prerequisite): check-plugin-teardown-shape --self-test (shallow clone; the pinned fixture commit was fetched), check:skill-examples and check:dual-build-cjs-loads (unbuilt packages). All three exit 0 after the prerequisite. check-platform-checklist --changed-from origin/main: OK. eslint over the changed files (--no-inline-config): clean. check:generated: all 14 up to date.
  • packages/cli/test/flow-credential-literal-doors.e2e.test.ts is integration tier and is declared to CI; it was not run locally.

Acceptance notes

  • In-tree survey of connectorConfig.input. There are 6 authored connector inputs outside tests: 4 in examples/app-showcase flows, 1 in ui/apps/console/src/preview-samples.ts:248 and 1 in content/docs/automation/connectors.mdx. The new judge refuses exactly 1: the showcase post_to_slack text, rewritten above. Two test fixtures (the DARK credential variants) were refused and rewritten. The conversion fixture packages/spec/src/conversions/registry.ts:1381/1425 (flow-node-connector-config-lift, retiredFromLoadPath) still holds 'Task done: {record.title}'. It is a before/after transform fixture that is never registered, so it is left as it is.
  • Not narrowed. FlowSchema.parse is unchanged: the map stays z.unknown(), as every value slot in an open config does. input stays optional.
  • Existing rows. A stored flow carrying one of the three shapes is skipped at boot with the existing failed to register flow warn, and the flows beside it still register. ADR-0087 D3 entry connector-action-input-value-slot-judged carries the prescription. No D2 conversion exists, because no token has a lossless CEL spelling and a malformed envelope carries no source to correct.
  • Noted, not changed.
    • conversions/refused-shapes.ts detector flow-value-slot-template-dialect-refused still walks config only. Its "listed exactly when the door refuses" property no longer covers a connector token. A follow-up can extend the detector, which reads the same ledger.
    • ui flow-value-envelope.ts isValueEnvelopeSlot ignores block. Its one false-positive path, config.input on connector_action, is refused by the parse already. ui/ is untouched.
    • lint also applies its unbound-root check to connector input envelopes, as it does for every value slot.
  • Contract review is owed (narrowing) and is run by the triage seat.

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic.
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; eslint over the changed files passes (the whole-tree pnpm lint runs in CI).
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: its generated artifacts are regenerated and committed.
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English.
  • The Before Submitting list in CONTRIBUTING.md is done.

🤖 Generated with Claude Code

https://claude.ai/code/session_01L9qibzGExkj8nxREmV7FjX

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L9qibzGExkj8nxREmV7FjX
The showcase Slack flow's text and the DARK credential fixtures carried a
single-brace token in connectorConfig.input, which registration and
validate now refuse. Rewrite them as CEL value envelopes.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L9qibzGExkj8nxREmV7FjX
@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tests tooling labels Oct 30, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/lint, @objectstack/service-automation, @objectstack/spec, touching 28 documentable anchor(s). ⚠️ 2 changed file(s) yielded no anchor (packages/spec/api-surface/automation.json, packages/spec/export-origins/automation.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

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

  • content/docs/automation/approvals.mdx (via record.amount (literal, a string literal in a comment on a changed line))
  • content/docs/automation/connectors.mdx (via connectorConfig (symbol, a field of const object FLOW_NODE_SIBLING_EXPRESSION_BLOCKS), connectorConfig (literal, a string literal in block), connector_action (literal, a string literal in connectorConfig; a string literal in nodeType))
  • content/docs/automation/flows.mdx (via FlowSchema (symbol, a top-level const), connectorConfig (symbol, a field of const object FLOW_NODE_SIBLING_EXPRESSION_BLOCKS), connectorConfig (literal, a string literal in block), connector_action (literal, a string literal in connectorConfig; a string literal in nodeType), record.amount (literal, a string literal in a comment on a changed line))
  • content/docs/automation/hook-bodies.mdx (via record.amount (literal, a string literal in a comment on a changed line))
  • content/docs/data-modeling/formulas.mdx (via record.amount (literal, a string literal in a comment on a changed line))
  • content/docs/data-modeling/validation.mdx (via record.amount (literal, a string literal in a comment on a changed line))
  • content/docs/getting-started/common-patterns.mdx (via record.amount (literal, a string literal in a comment on a changed line))
  • content/docs/permissions/permissions-matrix.mdx (via record.amount (literal, a string literal in a comment on a changed line))

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

  • content/docs/releases/v15.mdx (via connector_action (literal, a string literal in connectorConfig; a string literal in nodeType))
  • 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), FlowSchema (symbol, a top-level const), connector_action (literal, a string literal in connectorConfig; a string literal in nodeType))
  • content/docs/releases/v17/17-4.mdx (via FlowSchema (symbol, a top-level const))
  • content/docs/releases/v17/17-5.mdx (via connectorConfig (symbol, a field of const object FLOW_NODE_SIBLING_EXPRESSION_BLOCKS), connectorConfig (literal, a string literal in block), connector_action (literal, a string literal in connectorConfig; a string literal in nodeType), record.amount (literal, a string literal in a comment on a changed line))
  • content/docs/releases/v17/17-6.mdx (via AutomationEngine (symbol, a top-level class), connector_action (literal, a string literal in connectorConfig; a string literal in nodeType))
  • content/docs/releases/v17/17-7.mdx (via FlowSchema (symbol, a top-level const))
  • content/docs/releases/v18/18-0.mdx (via connectorConfig (symbol, a field of const object FLOW_NODE_SIBLING_EXPRESSION_BLOCKS), connectorConfig (literal, a string literal in block), connector_action (literal, a string literal in connectorConfig; a string literal in nodeType))
  • content/docs/releases/v18/18-1.mdx (via FlowSchema (symbol, a top-level const))

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
  • 2 changed file(s) yielded no anchor (packages/spec/api-surface/automation.json, packages/spec/export-origins/automation.json) — pages documenting those are invisible to this run
  • 6 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 58 of 210 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 — 168 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 93242324040563e9db96c98ef186948797d20cbd → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 73793eef54e49e47feea9bfb2d6c45561e6c48e6 — the merge of head 6a06a420b1c77e30d4aee538e531687fc4e0a7eb into base 93242324040563e9db96c98ef186948797d20cbd, 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 73793eef54e49e47feea9bfb2d6c45561e6c48e6 && git checkout 73793eef54e49e47feea9bfb2d6c45561e6c48e6
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 93242324040563e9db96c98ef186948797d20cbd 6a06a420b1c77e30d4aee538e531687fc4e0a7eb && git checkout -B drift-repro 93242324040563e9db96c98ef186948797d20cbd && git merge --no-ff 6a06a420b1c77e30d4aee538e531687fc4e0a7eb

node scripts/docs-audit/affected-docs.mjs --json 93242324040563e9db96c98ef186948797d20cbd

⚠️ 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 93242324040563e9db96c98ef186948797d20cbd → 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

domain:spec#1 · os-tesla · session_01L9qibwb6hBqszPTYJobitm

Contract-review-claim: session_01L9qibwb6hBqszPTYJobitm · head 6a06a420b1c77e30d4aee538e531687fc4e0a7eb · 2026-10-30T14:32Z

In-seat at CONTRACT_REVIEW_TIER, per the maintainer: 「你有 fable,可以自己契约复审」.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#1 · os-tesla · session_01L9qibwb6hBqszPTYJobitm

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 6a06a420b1c77e30d4aee538e531687fc4e0a7eb
Local-runs: none
Scope: packages/spec/src/automation/flow-node-config-refusals.ts, packages/spec/src/automation/flow-node-expression-paths.ts, packages/spec/src/automation/flow-value-slot-template.ts, packages/spec/src/automation/flow.zod.ts, packages/spec/src/migrations/entries/semantic/18.connector-action-input-value-slot-judged.ts + narrowing-face floor (every diff path outside the prose faces): packages/services/service-automation/src/flow-registration-checks.ts, packages/services/service-automation/src/engine.ts, packages/lint/src/validate-expressions.ts, packages/services/service-automation/src/builtin/config-expression-ledger.test.ts, packages/services/service-automation/src/builtin/connector-input-registration.test.ts, packages/lint/src/validate-expressions.connector-input.test.ts, packages/spec/src/automation/flow-node-expression-paths.test.ts, packages/spec/src/automation/flow-value-slot-template.test.ts, packages/metadata-protocol/src/runtime-authoring-gate.flow-credential-literal.test.ts, packages/cli/test/flow-credential-literal-doors.e2e.test.ts, examples/app-showcase/src/automation/flows/index.ts; cited code read on the head: packages/services/service-automation/src/builtin/connector-nodes.ts, packages/services/service-automation/src/builtin/value-slot-map.ts, packages/services/service-automation/src/plugin.ts, packages/spec/src/automation/flow-sibling-block-loose-keys.ts, packages/spec/src/automation/flow-template-token.ts, packages/spec/src/conversions/refused-shapes.ts, ui/packages/app-shell/src/views/metadata-admin/inspectors/flow-value-envelope.ts, ui/packages/app-shell/src/views/metadata-admin/inspectors/FlowNodeConfigField.tsx, ui/packages/app-shell/src/views/metadata-admin/previews/simulator/flow-simulator.ts, skills/objectstack-automation/SKILL.md, skills/objectstack-automation/references/integration-recipes.md, the three .changeset/27968-*.md, AGENTS.md step 3, pm-dispatch SKILL.md Skills-docs: clause. Reviewed at 2026-10-30T14:42Z against the three-dot diff 27e0bd58...6a06a420 (merge-base 91a423ef), card #27968 body + 4 comments, PR body + 22-file list, and the check-runs on the head.

① Derived judgments

  1. Accept-set narrowing at AutomationEngine.registerFlow — a connector_action node's connectorConfig.input.KEY holding (a) a malformed CEL envelope, (b) a template-dialect envelope, (c) a string carrying a single-brace {…} token anywhere in the value (a string inside a nested list or object included) is refused; the refusal is thrown (engine.ts:4387 iterates registrationChecks({ advisory: true }) and throws the first refusal — advisory there enables the logged schema pass, it does not soften the throw). Right. The judge is the shared one: flowNodeValueTemplateRefusals(node.type, node.config, node) and resolveFlowNodeExpressions(node.type, node.config, node) at flow-registration-checks.ts:598/641, the same two calls the subflow config.input.* row already takes, located as BLOCK.PATH. No second list, as triage 6487582764 / 6516097905 ruled.
  2. The same narrowing at validateStackExpressions (objectstack validate) and at the runtime authoring gate, which runs the same pass (validate-expressions.ts:2775/2834; lint test asserts rule expression-invalid at the gate). Right. A valid envelope there is also held to the value slots' unbound-root judgment (refuseUnboundRoots(…, 'value')), as every value slot is; the lint changeset names it. Right.
  3. Boot reach — a stored flow row carrying one of the three shapes is skipped with [Automation] failed to register flow (plugin.ts:1571, re-sync 3539, cold-boot bind 3616), the flows beside it register. Right; the D3 entry's acceptance criteria name that warn as the locator.
  4. Executor unchanged, one reject set — connector-nodes.ts:125 resolveValueSlotMap(engine, cfg.input, …) evaluates TOP-LEVEL envelopes and hands every other value on as a copy; the ledger row input.* emits top-level envelope-shaped values for the envelope check and the template judge walks nested strings for tokens — identical to subflow.input.*. A nested envelope (body.filter) is data at both doors and at the executor (registration test asserts the handler receives it verbatim). Right.
  5. The grammar the slot now takes — tokensOf is templateTokensOf (/\{([^{}]+)\}/g), so every {…} is a token, an unresolvable one included (refusalOf('{"a": 1}') in the spec test is a refusal), and a {{ x }} hole is matched as the single-brace token { x } and refused too. This is exactly what the 17.x interpolator substituted and exactly what subflow.input refuses today, so the entry's "a single-brace {…} token … anywhere in the value" is accurate. Right by ruling; see ④ for what it does to the recipe text.
  6. FlowSchema.parse byte-identical — connectorConfig.input values stay z.unknown() (now .meta({ xExpression: 'value' }), a marker, not a refinement); the superRefine at flow.zod.ts:1544 now hands the node but acts on predicate rows only (the new row is value → continue), and found.block is 'config' for every predicate row, so the issue path is unchanged. Right — "Not narrowed" in the PR body holds.
  7. Published-surface widening (additive, rides the minor) — new exports FLOW_NODE_SIBLING_EXPRESSION_BLOCKS, FlowNodeExpressionBlock, FlowNodeSiblingExpressionBlock, FlowNodeSiblingBlocks, flowNodeExpressionBlock, getFlowNodeSiblingBlockJsonSchemas; optional third parameter on resolveFlowNodeExpressions, resolveFlowNodeValueSlots, flowNodeValueTemplateRefusals; a block field on ResolvedFlowNodeExpression and FlowNodeValueTemplateRefusal (required on read; no constructor of either type exists outside packages/spec). api-surface/automation.json and export-origins/automation.json carry all six (check:generated green). Right. Clause-②: no (narrowing) is the correct arm: the closed pair admits one arm and the narrowing is the BREAKING one; the additive exports are ordinary minor surface.
  8. Every prior ledger row resolves byte-identically — block is optional, absent on all 21 earlier rows; the census test pins the 22-row order with the new row appended last; entryRoot returns config for a block-less row and the root == null || typeof root !== 'object' guard reproduces the old early return per row. Right.
  9. flow-node-config-refusals.ts excludes block-rooted rows from insideValueSlot and atOrInsideJudgedLedgerSlot. Necessary and sufficient: without it config.input on a connector_action node would read as a judged value slot and shadow siblingBlockLooseConfigRefusals' loose-key refusal (flow-sibling-block-loose-keys.ts, CONNECTOR_CONFIG_LOOSE_KEYS = ['connectorId','actionId','input']). slotJudgeRefusedPaths (line 397) stays config-only, correctly: it dedupes config-contract issues against the config-rooted judges. Right.
  10. Ratchet third channel — getFlowNodeSiblingBlockJsonSchemas() hands every non-config block; declaredFromSiblingBlocks fails on a marker in a block FLOW_NODE_SIBLING_EXPRESSION_BLOCKS does not name, and the keys are pinned to ['connectorConfig']. The .meta on z.unknown() inside z.record survives z.toJSONSchema in input mode — proven by the ratchet's ['connector_action.connectorConfig.input.* (value)'] assertion under Test Core (green). Right.
  11. Producer-side amendments — showcase TaskCompletedSlackFlow text → CEL envelope reading record.title; record is in scope (start node record-after-update on showcase_task). The other five in-tree inputs (connectors.mdx:434, preview-samples.ts:248, three showcase) are brace-free literals. The two DARK credential fixtures declare api_token as an input variable, so {'clientSecret': api_token} binds; the metadata-protocol gate (3/3) ran under Test Core. Right; the cli twin is unverified — ③/④.
  12. ADR-0087 disposition — D3 entry connector-action-input-value-slot-judged at step 18 (spec is 18.2.0), Form D, no backticks in surface, acceptanceCriteria present, reason states why no D2 exists (no lossless token spelling; a malformed envelope carries no source to correct) — matches the ADR's "convert where lossless, a structured TODO otherwise". Marker adr-0087: registered connector-action-input-value-slot-judged on all three changesets (Check Changeset + Lint gates green). Right.

② Semver level

  • Three changesets, @objectstack/spec / @objectstack/service-automation / @objectstack/lint, each minor, each carrying Clause-②: no (narrowing) and Metadata-protocol: narrowed line-initial, each with the BREAKING banner, the before/now, and the remedy (FROM → TO). AGENTS.md step 3: a kind other than none takes at least minor on a package the diff moves — narrowed ⇒ minor on all three. Matches the diff.
  • @objectstack/metadata-protocol and @objectstack/cli are touched in test files only, examples/app-showcase is not a released package, content/docs/references/automation/flow.mdx is regenerated — none owes a changeset. Matches.
  • Clause-②: no (narrowing) — one arm from the closed pair, BREAKING, present on the PR body and on every changeset. The declaration is true of the diff (①.1–3), not a false no. The additive exports (①.7) do not make it yes: the pair admits one arm and the narrowing governs.

③ Boundary flags

  • Author: PR user.login is objectstack-fleet[bot] — no login note owed.
  • open_questions: [] in the os-dev-report — nothing to answer.
  • Dev deviations (os-dev-report 6524784146), each answered:
    1. File surface widened on the producer side (showcase text, two DARK fixtures) — accepted; each is a consumer the new judge refuses, each rewrite preserves intent (①.11).
    2. packages/cli/test/flow-credential-literal-doors.e2e.test.ts not run locally, "declared to CI" — not run anywhere on this head: *.e2e.test.* is the nightly tier by name (vitest-tiers.ts header, OS_TEST_TIERS unset ⇒ nightly-tier files excluded), so the PR's Test Core did not collect it. Low risk (same fixture shape passed in metadata-protocol through the same lint pass) — carried as should-fix ④.4.
    3. PR body prose (two-vs-three skills files in 维护者速读; footer form) — prose face, the seat's; noted, not reviewed here.
    4. Two merges of origin/main — mechanical; the head's check-runs are the verdict.
  • Skills-docs: line — present on the PR body as Skills-docs: owed, left to a skills-lane edit (…), naming the three files. The pm-dispatch SKILL.md clause wants the closed form follow-up owed — FILES and the seat to file the card and rewrite it to follow-up #N before ACCEPT (review-checklist.md:18). Escalated to the seat as should-fix ④.1: the content is true (verified on the head: SKILL.md:88 and integration-recipes.md:18-21 state the retired limit; evals/integrations-declared.json:7 is not false, it already calls the input a value slot), the form is not the closed one.
  • Check-runs on 6a06a420 (newest per name): every run is success or skipped; the skipped ones are opt-in harnesses, the matrix-template placeholders, the schedule gate and Check PR Size / Auto Label re-runs. No failure, no base-signature exception needed. Gate verdict: green.
  • auto_merge: null — nothing to disarm. needs:contract-review is on the PR; a PASS record in the same hand removes it.

④ Design judgment

packages/spec/src/automation/flow-node-expression-paths.ts — the ruling's shape ("the ledger gains a way to address a node's sibling connectorConfig block, not a per-node special case") is met: block is a general optional root, FLOW_NODE_SIBLING_EXPRESSION_BLOCKS is the one table the ratchet and the resolvers both read, entryRoot is the single dispatch, and waitEventConfig is correctly left out (literals only). Tried to break it: a caller passing config only is answered for config only (test pins it); a loose config.input on a connector node is never read by the row (test pins it); input as an array yields nothing (the * walk refuses arrays); onlyWithKey would be read off the block, which is the right root. Holds. No finding.

packages/spec/src/automation/flow-value-slot-template.ts — the sibling-block branch takes the generic remedy and skips the callee/screen/recipient sentences; correct, since the connector was handed the token's text, not nothing (the three sentence suites now run through refusalsForEntry and still pin exactly the callee/screen/recipient rows). Holds. Observation, by ruling not a finding: this slot is the one value map most likely to carry provider-side brace syntax (a JSON-as-string body, GraphQL, mrkdwn), and tokensOf refuses every {…}, an unresolvable one included; the ruling forbids a connector-only judge, the in-tree survey found one hit, and the D3 entry declares the reach. Accepted as ruled.

packages/spec/src/automation/flow-node-config-refusals.ts — the two block filters are the exact two iterators that walk config positions; slotJudgeRefusedPaths correctly stays config-only. Holds. No finding.

packages/spec/src/automation/flow.zod.ts — the marker rides the block's own Zod, the value stays z.unknown(), the describe text and the example comment (rider from note 6518217696) now say what a value is; getFlowNodeSiblingBlockJsonSchemas is memoized and hands every non-config block so an unaddressable marker fails loudly. Holds. No finding.

packages/spec/src/migrations/entries/semantic/18.connector-action-input-value-slot-judged.ts — judged against its first consumer (the two doors) and the ADR: surface, reach (authoring, validate, authoring gate, boot), fate (skipped with warn; validate fails), remedy and the no-D2 reason are each true of the tree; the acceptance criteria name the printed locator string, which matches lint's where and registration's message byte for byte. Holds. No must-fix.

Findings (none must-fix):

  1. should-fix — governed skill text falsified at merge; Skills-docs: line not in the closed form. skills/objectstack-automation/SKILL.md:88 ("os validate does not judge it yet, so a malformed envelope fails at run time") and references/integration-recipes.md:18-21 ("{…}, {{ }} are literal text … does not judge it yet") teach the limit this PR retires — and per ①.5 a {{ }} hole is refused there too, so an author following the recipe is refused by the door the recipe says is silent. Outside this card's claim (skills/**, skills lane), correctly declared; the seat files the skills-lane card and rewrites the line to Skills-docs: follow-up #N before ACCEPT, per pm-dispatch SKILL.md. Not a FAIL: the card's seam is ledger + two doors, both ends land here.
  2. should-fix — packages/spec/src/conversions/refused-shapes.ts:406 detector flow-value-slot-template-dialect-refused walks config only (flowNodeValueTemplateRefusals(node.type, node.config), no node). Its header's invariant — "a site is listed here exactly when that door refuses it" — now has a hole: os migrate meta --stored will not name a stored connector_action row carrying a {…} token, although registerFlow refuses it at boot. The D3 entry compensates by naming the boot warn as the locator, so the operator has a path; the fix is one argument (hand the node) and a connectorConfig path prefix, reading the same ledger. File a card (noted in the PR, not filed).
  3. should-fix — Studio readers ignore block. flow-value-envelope.ts:isValueEnvelopeSlot answers true for ['config','input'] on connector_action (unreachable: the parse refuses that loose key), and FlowNodeConfigField.tsx:valueSlotRefusals / flow-simulator.ts:123 call the judge config-only — so the hand-written connectorConfig form offers no envelope editor and shows no refusal for a {…} token in input; the author meets it at the runtime authoring gate instead, located. Correct verdict, worse place. ui/ lane card (noted in the PR, not filed).
  4. should-fix — packages/cli/test/flow-credential-literal-doors.e2e.test.ts amendment unverified on any run (nightly tier by name; not in this PR's CI population; not run locally per the dev's own flag). Run it once (vitest run --project integration on that file, or the nightly) before or at landing; the same DARK envelope passed the same lint pass under metadata-protocol, so the expected outcome is green.

Implemented-by: claude/project-thread-l0j2zg
Reviewed-by: session_01L9qibwb6hBqszPTYJobitm

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#1 · os-tesla · session_01L9qibwb6hBqszPTYJobitm

维护者速读

  • 改了什么: 流程里“调用连接器”节点(connector_action)的输入值 connectorConfig.input,现在在注册流程和 objectstack validate 时就会被检查,和子流程的输入用同一条规则。写错的计算表达式、旧的 {…} / {{ }} 模板写法会在 connectorConfig.input.<key> 处被拒绝,并给出位置。
  • 为什么改: 之前这些写法能顺利保存和上线,要等流程真正跑到这一步才失败,或者把 {record.title} 这样的原文直接发给外部系统。
  • 风险与回滚: 已经存在、带这些写法的流程会被拒绝注册:启动时打一条 warn 并跳过该流程,其他流程照常。仓库内示例应用的一处 Slack 文案和两个测试夹具已改写成 CEL 表达式。回滚即 revert,不涉及数据迁移。
  • 先看: packages/spec/src/automation/flow-node-expression-paths.ts 新增的 connector_action 条目和“兄弟区块”根。
  • 两分钟验证: pnpm --filter @objectstack/service-automation exec vitest run src/builtin/connector-input-registration.test.ts,三种写法都被拒绝,合法表达式照常注册。
  • 需要你做的: 无。不属于受管面,席内契约复审 PASS(评论 6525595405),由席位落地。skills 下的过时说明已立卡 skills(automation): the connector input recipe still says os validate does not judge connectorConfig.input, which PR #28150 makes false #28162 交 skills 车道。
  • 席位意见: ACCEPT,可落地。

席位核对 · ACCEPT · domain:spec seat 1 (#6017) · 2026-10-30T14:58Z

Head 6a06a420b1c77e30d4aee538e531687fc4e0a7eb. Card #27968, claim 6521799555, os-dev-report 6524784146. The maintainer's rulings, verbatim: 「你有 fable,可以自己契约复审」 and 「按 pm-dispatch 审过的pr你可以自主落地」.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

⛔ The merge queue is red after this PR landed: p0 #28189 · 2026-10-30T15:57Z

This PR's landing commit 3cc45cc4 added the connector_action input.* value row to FLOW_NODE_EXPRESSION_PATHS. ui/packages/app-shell/src/views/metadata-admin/inspectors/flow-value-envelope.test.ts:77 pins that whole list and does not carry the new row. Main pushes skip Test UI, so main reads green, but every queued PR that touches ui/ is ejected on this one test. PR #28131 and PR #28116 already have been.

Owning seat (domain:spec#1) or domain:ui: claim #28189 this round. The stop-bleed is that one pin flip, plus any other pin on the same list in ui/packages/app-shell. Per #27047 item 6, if it is unclaimed for one round, triage escalates by name, and after that triage dispatches the stop-bleed.


Generated by Claude Code

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 size/l tests tooling

Projects

None yet

1 participant