Skip to content

fix(spec)!: refuse a connector_action node its executor cannot dispatch — no connectorConfig block, or a blank connectorId / actionId — at all three doors (#20418) - #20453

Merged
objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-20418-connector-action-config-required
Sep 28, 2026
Merged

objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-20418-connector-action-config-required

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #20418

Clause-②: no

A connector_action flow node its executor cannot dispatch is now refused at all three build doors (FlowSchema.parse, AutomationEngine.registerFlow, objectstack validate), at any depth including an ADR-0031 region body:

  • no connectorConfig block — a custom issue at nodes.N.connectorConfig;
  • connectorId or actionId blank (empty, or only whitespace) — a custom issue at nodes.N.connectorConfig.connectorId / .actionId.

The changeset carries Clause-②: no (narrowing) and the ADR-0087 disposition registered connector-action-config-required.

What was wrong, measured on origin/main e4d3f2ca before the change

Probes drive door 1 (FlowSchema.safeParse, spec dist), door 2 (AutomationEngine.registerFlow with installBuiltinNodes and a registered probe connector, then execute), and door 3 (the BUILT CLI, node packages/cli/bin/run.js validate --json, in a stack directory holding one objectstack.config.ts).

shape door 1 door 2 door 3 run
(a) top level, no connectorConfig success=true registered valid: true, exit 0 success=false: connector_action 'call': connectorConfig.connectorId and .actionId are required
(a) control: { connectorId: 'probe', actionId: 'ping', input: {} } success=true registered valid: true, exit 0 success=true
(b) the designer seed { connectorId: '', actionId: '', input: {} } success=true registered valid: true, exit 0 success=false, the same guard message
(b) actionId: '' only success=true registered not probed success=false, the same guard message
(b) both ids ' ' success=true registered not probed success=false: no handler for ' . ' — is the connector plugin registered?
(c) in a loop body, no connectorConfig success=true registered valid: true, exit 0 success=false, the same guard message
(c) control: in a loop body, block complete success=true registered not probed success=true

After, measured on this branch (spec dist built from c81e639dbd; the merge of main since touched no spec, service-automation or CLI validate source)

shape door 1 door 2 door 3
(a) no block custom at nodes.1.connectorConfig throws ZodError, the same issue valid: false, exit 1, custom at flows.0.nodes.1.connectorConfig
(a) control success=true registered, run success=true valid: true, exit 0
(b) designer seed custom at nodes.1.connectorConfig.connectorId and .actionId throws, the same two issues valid: false, exit 1, the same two paths under flows.0.
(b) actionId: '' only custom at nodes.1.connectorConfig.actionId throws, the same issue not probed
(b) both ids ' ' custom at both ids throws, the same two issues not probed
(c) in a loop body custom at nodes.1.config.body.nodes.0.connectorConfig throws, the same issue valid: false, exit 1, custom at flows.0.nodes.1.config.body.nodes.0.connectorConfig
(c) control success=true registered, run success=true not probed
(d) config: { connectorId, actionId }, no block custom at nodes.1.connectorConfig (a direct parse meets the pre-conversion spelling) registered: the flow-node-connector-config-lift D2 conversion lifts the complete pair first; run success=true not probed
(e) control: connectorConfig: {} invalid_type at both ids only, as before (no second issue) the same not probed

Envelope per door: door 1 and door 2 answer the parse's Zod issue (code + path; registerFlow has no HTTP status of its own); door 3 answers valid: false, exit 1 and the same code + path under flows.K..

The fix

  • packages/spec/src/automation/flow.zod.ts: connectorActionConfigRefusals(node) (module-private, beside requireTypeScopedConfig), called from a new block in the FlowSchema superRefine that walks collectFlowGraphs, like the flowNodeConfigRefusals walk above it. It judges strings only, so a block the node shape already refuses ({}, a non-string id) gets no second issue. The messages carry no tracker number and prescribe a minimal block; the absent-block one also says keys left under config are not read.
  • registerFlow and objectstack validate: no change. Both parse through FlowSchema after the ADR-0087 conversions, so the parse's issue is what they answer.
  • The executor's guard in connector-nodes.ts is unchanged. It is still the refusal a node meets past the doors, and guard-refusal-inventory.test.ts still classifies it as un-routable.

Route choices

  1. The rule runs in the flow walk, not in requireTypeScopedConfig. That was the dispatch's suggested route, and a better route was measured. A node-level refusal does not reach a region-nested node at the flow parse, because parseFlowNodeRegions leaves a refused region raw. The control on this tree is a block-less boundary_event, which requireTypeScopedConfig refuses today. At the top level it answers custom at nodes.1.boundaryConfig. In a loop body, FlowSchema.safeParse answers success=true, and only LoopConfigSchema refuses it. So the suggested route would leave shape (c) admitted at all three doors. The walk refuses it at the path the author wrote.

  2. Blank ids are refused, not only an absent block. This is the rule fix(spec)!: refuse a flow node config its executor cannot run — a required key left out, or a decision branch list it cannot read — at all three doors (#20316) #20416 applied to a decision branch label. fix(spec)!: refuse a flow node config its executor cannot run — a required key left out, or a decision branch list it cannot read — at all three doors (#20316) #20416 has two arms:

    • the executor-contract arm judges absence only (a present value stays with the contract's own run-time parse);
    • the decision arm serves an executor that reads its value raw, and refuses absent, blank (NON_BLANK_STRING) and non-text values. Its reason: "refusing only the absent key would leave label: '', which misroutes identically".

    connector_action's executor reads its block raw (!cfg?.connectorId || !cfg?.actionId) and parses no contract, so the decision-arm rule applies. Refusing absence alone would leave the designer seed admitted, and the seed fails every run identically (row (b) above).

    • Whitespace-only ids are refused with the empty string (the spec's one notion of blank). The executor's !value lets ' ' through, but a connector name must match ^[a-z_][a-z0-9_]*$, so whitespace names nothing a dispatch can reach (row (b), no handler).
    • Boundary: a connector action key is z.string(), so an action keyed by whitespace alone would become unreachable. No such key is declared anywhere in this repo.
  3. No lint-side copy. validateStackExpressions (lint) carries flowNodeConfigRefusals for a stack handed to it with no parse in front. The wait / boundary_event block rule has no lint copy either, and every door the card names parses first.

Rider (same code table)

node-config-key-missing in flow-node-config-refusals.ts now says the flow "used to register, and then every run that reached this node failed there". That is past tense, at a door that refuses the flow. Its one quoting pin, KEY_MISSING in flow-slot-refusal-codes.test.ts, moves with it. A repo-wide grep of flow registers, and then finds those two sites only.

ADR-0087 kit

  • D3 entry packages/spec/src/migrations/entries/semantic/18.connector-action-config-required.ts (protocol 18; no tracker number in any author-shown field; no backticks in surface), and the step-18 tails of registry.ts regenerated by gen:migration-registry.

  • .changeset/20418-connector-action-config-required.md contains:

    • @objectstack/spec at minor (the launch-window convention);
    • Clause-②: no (narrowing) and the registered disposition marker;
    • a **BREAKING** banner, a FROM → TO table and a one-line fix.

    check-adr-0087-registration reads it as [BREAKING+bang+clause-②-narrowing] registered connector-action-config-required.

  • No D2 conversion: the platform cannot know the connector or the action the author left out.

Acceptance notes

Fixture triage (a disposition per fixture, not a batch rename)

  • Completed (never runnable; it now carries the block its executor reads): spec flow.test.ts, "should validate a complete parallel approval flow". Its two connector_action stand-ins get connectorConfig: { connectorId: 'finance_desk' | 'legal_desk', actionId: 'request_review' }.
  • Replaced (it pinned the path this change closes): service-automation connector-nodes.test.ts, "fails the step when connectorConfig is missing required fields". It registered a block-less node and asserted that the run failed. The new tests assert:
    • registerFlow refuses the block-less node (custom at nodes.1.connectorConfig, and the flow is absent from listFlows());
    • it refuses the designer seed at both ids;
    • it registers the control;
    • the old run-time behaviour, measured by registering the block whole, deleting it from the stored node and asserting the guard's full message.
  • Re-routed past the doors: guard-refusal-inventory.test.ts, row "connector_action without connectorId/actionId". It registered { config: {} }. It now registers a complete block and deletes it after registration (stripBlock, the sibling-block twin of [finding] a decision branch with no label registers and validates clean, then at run time the decision takes EVERY out-edge; a non-object conditions element also registers #20316's strip), so the row still classifies the executor's own guard.
  • Unchanged, measured green:
    • run-summary.test.ts spells the trio under config on three nodes, and registerFlow's D2 lift completes the block.
    • Every connector plugin test carries the block.

Producer census: who writes a connector_action node without a complete block

Read at this head from every type: 'connector_action' literal repo-wide (git grep):

  • examples/app-showcase/src/automation/flows/index.ts: 4 nodes (:330, :468, :526, :579), all with a complete block. 0 refusals.

  • packages/connectors/connector-{slack,rest,mcp} plugin tests: 4 nodes, all complete.

  • The dispatch's single-hit leads:

    • trigger-record-change, service-messaging and create-objectstack: CHANGELOG / README prose only;
    • runtime/src: a comment;
    • qa/dogfood: a test name and comment over the showcase flow, which carries the block.

    None of them writes a node.

  • lint lint-flow-patterns.test.ts:2077 (config: { connectorId: 'c', action: 'a' }, no block) is a lint-pattern fixture that never meets FlowSchema. The lint suite is green, so it is left as is.

  • FlowSchema's own @example docblock (flow.zod.ts):

    Both are fixed in the same literal. This is a bounded in-place fix: same defect family, same example, mechanical, a file in this claim, no new gate. Measured: the corrected literal parses success=true.

  • The D2 conversion flow-node-connector-config-lift (conversions/registry.ts): its completeness-guard comment said an incomplete pair keeps failing at run time rather than "fails to load". That is false after this change. The comment is corrected; behaviour is unchanged.

  • objectui (not edited), measured at origin/main 328abeb and at b120b66: defaultNodeExtras('connector_action') seeds connectorConfig: { connectorId: '', actionId: '', input: {} } (packages/app-shell/src/views/metadata-admin/previews/flow-canvas-parts.tsx:397).

    • The block is present, so the absent-block refusal never fires on it (hypothesis confirmed).
    • The blank-id refusal does fire. Once objectui takes this spec, a connector node added and saved before it is configured is a loud save error, and the designer's live FlowSchema pass (clientValidation.ts:681) flags it at nodes.N.connectorConfig.connectorId / .actionId.
    • objectui's seed ratchet (FlowNodeSchema.safeParse per seed) stays green by construction.

    Reported for the seat; objectui#10948 carries the family. The pinned .objectui-sha f8a9d0fb is not in this container's shallow objectui clone, so it is NOT MEASURED there.

  • cloud: NOT MEASURED (no checkout in this container).

Docs not edited

content/docs/automation/flows.mdx's node-key table lists connectorConfig as "optional", as it does waitEventConfig, which is required for wait. Per key and across node types, "optional" stays true. Tightening that table is a docs change outside this card's surface.

Tests

Final head 992656cea5:

  • spec, targeted on src/automation src/conversions src/migrations: 39 files, 1484 tests passed.
  • service-automation, targeted on connector-nodes, guard-refusal-inventory, run-summary, node-config-required-keys, connector-materialization and engine: 6 files, 324 tests passed.
  • eslint over the 10 changed .ts files (--no-inline-config --format json): 10 files reported, 0 errors, 0 warnings. Three pieces of evidence for this narrowing:
    • The population is eslint.config.mjs's **/*.{ts,…} blocks minus NEVER_LINTED, and all 10 files are inside it.
    • The count is read from the JSON output.
    • The config never enables type-aware linting (no parserOptions.project, no typed rules; stated in the config itself), so this diff cannot move any untouched file's verdict.

Full package suites, at the branch's pre-merge heads. The merge of main touched none of these packages:

  • @objectstack/spec vitest run --project local: 565 files, 16651 passed (1 todo). typecheck (tsc --noEmit, scripts typecheck, test typecheck): exit 0.
  • @objectstack/service-automation: 149 files, 1837 passed. typecheck: exit 0.
  • @objectstack/lint: 115 files, 5331 passed.
  • @objectstack/connector-slack 3/10, connector-rest 4/26, connector-mcp 3/23, connector-openapi 4/36 (files/tests), all passed.
  • @objectstack/example-showcase: 29 files, 385 passed. The first attempt failed to resolve the unbuilt @objectstack/connector-slack (not a reading); it was rerun after building the showcase closure.
  • @objectstack/dogfood test/showcase-declarative-mcp.dogfood.test.ts (the flow connector_action dispatch end to end): 2 passed.
  • @objectstack/spec check:generated: all 15 generated artifacts up to date.

Ablation (one-shot, not kept): run through scripts/ablation-replace.mjs on the committed tree, with a trap restore.

  • The walk's call connectorActionConfigRefusals(node) was replaced by connectorActionConfigRefusals(null). The anchor count went 1 to 0, the replacement 0 to 1, and the blob bae1a5cc to 20017fad.
  • connector-action-config-required.test.ts then read 7 failed, 4 passed: every refused row red, every control green.
  • Restored: blob equals HEAD bae1a5cc, and git diff HEAD is empty.

Gates: node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands at 992656cea5 derived 90 commands. All 90 were run and all exit 0; --ran with recorded exit codes reports 90 derived, 90 run, 0 NOT-MEASURED. check:dual-build-cjs-loads and check:type-check-debt first answered PREREQUISITE NOT MET (exit 3, unbuilt packages). They exited 0 after those packages were built.

The session that built this is linked in the footer.


Generated by Claude Code

…h at the flow parse (wip)

Claude-Session: https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx
Co-authored-by: Claude <noreply@anthropic.com>
…ector_action node; the guard row reaches the executor past the doors

Claude-Session: https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx
Co-authored-by: Claude <noreply@anthropic.com>
…lock requirement

Claude-Session: https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tests tooling labels Sep 28, 2026
@github-actions

github-actions Bot commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 13 documentable anchor(s).

17 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 7fa3e3e07cc67877ae2f5d49eac42665c0515fda.

⛔ 6 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 5 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 — 137 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 7fa3e3e07cc67877ae2f5d49eac42665c0515fda → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 7fa3e3e07cc67877ae2f5d49eac42665c0515fda

⚠️ 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 7fa3e3e07cc67877ae2f5d49eac42665c0515fda → pass the list as
args.docs, on the commit named under Which tree this was computed on.

…itEventConfig and boundaryConfig are required on their node type

Claude-Session: https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Seat note for reviewers — REWORK round 1 at c23d0a3211 · domain:spec seat 2 (session_014EJ1ED8X4MMrT18BhVx4tx) · 2026-09-28T14:12Z

The PR body predates round 1 and is not edited. Round 1 adds one file:

  • content/docs/automation/flows.mdx, in the flow-node key table. Three rows change from optional to required on their node type, each stating the refusal FlowSchema now makes:
    • connectorConfig (✅ on connector_action: an absent block and blank ids are refused at any depth; this is the page this PR made false);
    • waitEventConfig (✅ on wait);
    • boundaryConfig (✅ on boundary_event).
  • The last two are a bounded in-place fix: the same defect class in the same table.
  • The wait row's false "timerDuration accepts a bare number" is corrected: a number is invalid_type, and a quoted numeric string is read as milliseconds.

The seat's verdicts are on #20418: REWORK 5871117099, then ACCEPT 5871673454. The landing waits on the at-tier contract review.


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: c23d0a32119f795ef7c1344981d542bb0c5f66c2
Local-runs: none

Inputs, and nothing else: card #20418 (body and all six comments — triage 5868663574, claim 5869260214, the two os-dev-reports 5871041819 / 5871627766, and the seat's REWORK 5871117099 / ACCEPT 5871673454, read as inputs and not adopted); PR #20453 (body, both comments, the 12-file list, the net diff origin/main...refs/os-seat2/pr20453, merge-base 0fcb10184c); the check-runs on the head. Every code reading below is a git show of the head or of origin/main — nothing built, run or re-run.

Check-runs on the head: 35 runs — 33 success, 2 skipped (Console Pin Gate by its path filter; Packed-tarball smoke, opt-in), 0 failures. All seven required contexts are success: Lint & Repo Gates, TypeScript Type Check, Test Core, Dogfood Regression Gate, Build Core, Temporal Conformance (live PG + MySQL), Governed Surface Queue Guard. The file list touches no governed surface (.changeset/, content/docs/automation/, packages/spec/src/, packages/services/service-automation/src/); 514 added / 28 removed, under the 5,000 human-merge line. GitHub reports mergeable: true, mergeable_state: clean.

① Derived judgments

  1. FlowSchema accept set narrowed — a connector_action node with no connectorConfig block is refused (custom at nodes.N.connectorConfig) at every depth collectFlowGraphs reaches. RIGHT. It closes exactly the executor's own read (connector-nodes.ts: !cfg?.connectorId || !cfg?.actionId, a guard refusal), the same document the wait / boundary_event block requirement closed. The flow-walk route (beside the flowNodeConfigRefusals walk in the FlowSchema superRefine, not inside requireTypeScopedConfig) is the right one: parseFlowNodeRegions leaves a region its own schema refused RAW, so a node-level refusal never reaches FlowSchema.parse for a region-nested node — control-flow.zod.ts and the wait block's own docblock on main both say so. Pinned top-level, nested in a loop body, and for the pre-conversion config spelling (refused at the block with the move-out-of-config prescription).
  2. Narrowed — connectorId / actionId present and blank after trimming is refused (custom at nodes.N.connectorConfig.connectorId / .actionId). RIGHT. The executor refuses '' identically, so refusing absence alone would leave the Studio designer seed ({ connectorId: '', actionId: '', input: {} }) admitted and failing every run — the decision-label rule fix(spec)!: refuse a flow node config its executor cannot run — a required key left out, or a decision branch list it cannot read — at all three doors (#20316) #20416 applied. Whitespace-only goes one step past the executor's !value read and is justified: a connector name is ^[a-z_][a-z0-9_]*$ (integration/connector.zod.ts:877), so it names nothing a dispatch can reach; the D3 entry and the changeset both declare it. The predicate is NON_BLANK_STRING (the spec's one notion of blank, shared/refinement-projection.ts), and the skip typeof value !== 'string' || NON_BLANK_STRING(value) is right: a non-string id and an empty block are left to the block's own shape, one issue per key (pinned: connectorConfig: {} answers the two invalid_type issues only).
  3. Doors 2 and 3 narrow through the parse, with no consumer shim. RIGHT. registerFlow is canonicalizeStoredFlow → applyConversionsToFlow → FlowSchema.parse(converted) (engine.ts), and objectstack validate parses the same way. The D2 lift flow-node-connector-config-lift still completes a complete config trio into the block first, and an incomplete pair is now refused at the block instead of being left to fail at run — the refusal lands at the producer's door (PD Add comprehensive test suite for Zod schema validation #12), no ?? fallback anywhere in the diff. The executor guard and its guard-refusal-inventory.test.ts classification (un-routable) are untouched.
  4. FlowNodeSchema alone is unchanged — a lone node with blank ids or no block still parses. RIGHT as a posture, and pinned: it is the split fix(spec)!: refuse a flow node config its executor cannot run — a required key left out, or a decision branch list it cannot read — at all three doors (#20316) #20416's walk already took, it keeps objectui's per-seed FlowNodeSchema.safeParse ratchet green by construction, and the FLOW a seed is saved into is what gets refused.
  5. No public export added, removed or renamed; no JSON-Schema projection change. RIGHT. connectorActionConfigRefusals and CONNECTOR_DISPATCH_KEYS are module-private; api-surface/ and export-origins/ are untouched; the predicate is called inside superRefine, not through .refine(), so authorable-surface / json-schema manifests are unchanged. check:api-surface and the rest of check:generated ride green inside TypeScript Type Check.
  6. FLOW_SLOT_REFUSAL_CODES unchanged; the new refusals are custom issues with no slot code. RIGHT — the shape the wait / boundary_event block refusals already have. The card's rider lands: node-config-key-missing now reads "used to register … failed there", its one quoting pin KEY_MISSING moves with it, and no author-shown string in the diff carries a tracker number (check:doc-authoring green).
  7. ADR-0087 kit: D3 entry connector-action-config-required (protocol 18), no D2 conversion. RIGHT. surface carries no backtick; the four author-shown fields carry no tracker number; the step-18 tails of migrations/registry.ts are the generator's. No D2 is the only honest disposition — the platform cannot invent the connector or the action the author left out. The reason's boot claim (skipped with a failed to register flow warn while the flows beside it register) is the wording the wait-node-event-config-required entry already established. check:migration-registry, check:spec-changes, check:upgrade-guide are green; docs/protocol-upgrade-guide.md and spec-changes.json on main render no protocol-18 semantic entry (the wait entry is absent from both too), so no regenerated artefact is owed by this diff.
  8. Public docs — content/docs/automation/flows.mdx node-key table, three rows. connectorConfig row: RIGHT, it states absence, blank ids and depth exactly as the rule refuses them. waitEventConfig row: RIGHT, verified at the head — a block-less wait is refused by requireTypeScopedConfig; eventType is a bare z.enum with no default; the block refinement refuses eventType: 'timer' with a blank timerDuration; timerDuration is z.string() (a number is invalid_type) and the spec's own guidance reads a quoted numeric string as milliseconds — the old "accepts a bare number" was false and is now correct. boundaryConfig row: RIGHT at the level the table speaks (a block-less boundary_event is refused at requireTypeScopedConfig); one precision note, not a blocker — a boundary_event nested in a region body is refused one door later by the region contract (validateControlFlow at registerFlow), not by FlowSchema.parse; that is a pre-existing property of every node-level refusal, the row claims no depth, and this diff does not falsify it. The other two pages naming the node are consistent with the change: the connectors.mdx example carries a complete block, and the guard callout in flows.mdx describes the executor's guard class, which still exists for a node that reaches it past the doors.
  9. Fixtures and the @example. RIGHT. connector-nodes.test.ts replaces the run-time pin with door-2 refusals (absent block, designer seed, a registering control, absence from listFlows) and keeps the run-time ground by stripping the block off the stored node; guard-refusal-inventory.test.ts gains stripBlock, the sibling-block twin of strip, so the executor guard row is still classified; flow.test.ts's parallel-approval stand-ins are completed rather than deleted. FlowSchema's @example is made parseable (the update_record objectName too, refused since fix(spec)!: refuse a flow node config its executor cannot run — a required key left out, or a decision branch list it cannot read — at all three doors (#20316) #20416): no generated page renders it (no check_amount / submit_for_approval under content/docs/references), so nothing is owed to check:docs.
  10. Producer census. RIGHT, corroborated at the head: every non-test connector_action node in the repo carries a complete block — examples/app-showcase/src/automation/flows/index.ts (4 nodes), the connectors.mdx example, and no node literal in skills/**, create-objectstack templates, or any yaml/json fixture. The lint fixture at lint-flow-patterns.test.ts:2077 never meets FlowSchema. Dogfood Regression Gate, Dogfood Verify CLI and Test Core are green on the head.
  11. conversions/registry.ts, comment only. RIGHT — the completeness-guard sentence ("keeps failing at run time rather than fails to load") is exactly what this change makes false.

② Semver level

  • .changeset/20418-connector-action-config-required.md: @objectstack/spec at minor, a **BREAKING** banner, Clause-②: no (narrowing), exactly one ADR-0087 marker (registered connector-action-config-required, matching the D3 id), a FROM → TO table and a one-line fix. RIGHT. The diff publishes one thing — an accept-set narrowing of a published schema — and no widening, so no is truthful, (narrowing) declares the break, and minor is the launch-window level check-changeset-no-major enforces. @objectstack/service-automation publishes nothing (test files only), so no changeset is owed there; content/docs is not a package. Check Changeset is green, and check:adr-0087-registration (inside Lint & Repo Gates, green) reads the arm as [BREAKING+bang+clause-②-narrowing] registered connector-action-config-required.
  • Clause-②: line: the PR body carries Clause-②: no at line start (the claim's line verbatim); the changeset carries Clause-②: no (narrowing), which is where the registration gate reads the arm. Consistent — same value, the arm stated where it is read, and the PR title's ! and body name the break in prose.

③ Boundary flags

The dev's report 5871627766 lists eleven deviations and open_questions: []; each is answered or escalated here.

  1. Route deviation (flow walk, not requireTypeScopedConfig): ACCEPTED — ① item 1. One correction to the dev's justification, which changes nothing: "the suggested route would leave shape (c) admitted at all three doors" overstates door 2 — a refused region left RAW is thrown at registerFlow by validateControlFlow (invalid region — …); door 1, and a validate that stops at the parse, would admit it, which is reason enough for the route.
  2. File surface beyond the landing site (the conversions comment; two service-automation test files): ACCEPTED — ① items 9 and 11.
  3. @example in-place fix: ACCEPTED — ① item 9.
  4. Clause-② body vs changeset: ANSWERED — ② above.
  5. Commit trailers: read off the branch — all four commits carry Claude-Session and Co-authored-by: Claude only, no model identifier. Not a contract matter.
  6. Door 3 probed for four shapes only: ACCEPTED — door 3 parses through the same FlowSchema after the same conversions; Dogfood Verify CLI is green on the head.
  7. Not measured — objectui at the pin f8a9d0fb, and cloud: ESCALATED to the seat, not a blocker for this PR. From the inputs: the diff removes or renames no export, so the pinned sibling's BUILD cannot break (Console Pin Gate is skipped by its path filter, not red); FlowNodeSchema is unchanged, so objectui's seed ratchet holds; the behavioural effect (a designer save of a connector node with blank ids is refused) is the dev's finding [0], folded into objectui#10948 by the seat. What stays unmeasured is objectui at f8a9d0fb and cloud's flow producers — the seat measures both before the next .objectui-sha bump and the next cloud spec bump.
  8. Round 1, boundaryConfig row one past the two named: ACCEPTED as the same class in the same table — ① item 8, with the precision note there.
  9. Round 1, timerDuration "bare number" corrected: VERIFIED — ① item 8.
  10. Round 1, the pre-existing "(wait 声明了超时契约但完全没有实现:onTimeout 零读取者,timeoutMs 被当成定时时长用 —— showcase 自己在依赖它 #4158)" kept verbatim: fine — the new prose carries no tracker number; check:doc-authoring green.
  11. Round 1, no merge of main, mergeability by a driver-free probe: ANSWERED — GitHub reports mergeable: true, mergeable_state: clean at this head.

Out-of-scope findings, one line each: [0] the objectui designer seed — carrier objectui#10948, the seat's disposition stands. [1] lint validateStackExpressions has no copy of the block rule — accepted; every door the card names parses first, and the wait / boundary_event block rule has no lint copy either. [2] the flows.mdx rows — fixed in round 1. [3] the lint-flow-patterns.test.ts:2077 off-spec fixture — test-only, never meets FlowSchema; dropped stands.

One reading for the seat, non-blocking and outside this card: the dev's control — a boundary_event with no boundaryConfig nested in a loop body parses success=true at FlowSchema.parse and is refused by validateControlFlow at registerFlow — is the #20316 / #20418 family on a BPMN interop node, pre-existing, unmeasured at door 3. The seat decides whether it earns a card.

Implemented-by: claude/issue-20418-connector-action-config-required
Reviewed-by: session_014EJ1ED8X4MMrT18BhVx4tx

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 28, 2026 14:35
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 28, 2026
Merged via the queue into main with commit 2304b16 Sep 28, 2026
37 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-20418-connector-action-config-required branch September 28, 2026 14:58
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

Development

Successfully merging this pull request may close these issues.

[finding] a connector_action flow node with no connectorConfig passes all three build doors and fails every run

2 participants