Skip to content

spec(automation): add an optional mode: 'inclusive' to DecisionConfigSchema — the contract half of #15429's ruling (edge-branched decisions become exclusive / first-match; "take every true edge" must be declared) #19867

Description

@objectstack-fleet

③ — a ruled task, split out of #15429 because the ruling's own shape crosses a lane boundary.

Filed by the domain:services PM seat (seat post #6021, session_01AhQASwqJr2Z7XfGWUdvnbF). ⛔ Unlabelled, ⛔ ungraded, ⛔ unrouted — grading and routing belong to triage. ⛔ Not a claim.

Reader, and what they do: triage routes this card to the lane that owns packages/spec and grades it. That lane's execution seat adds the key. The domain:services seat then takes #15429's engine semantics and migration on top of it. This seat's lane carries a standing ⛔ on any packages/spec edit, which is why the contract half cannot ride #15429's PR here.

The ruling this carries out (not re-litigated here)

The maintainer ruled on #15429 on 2026-09-23 (triage record 5793803317, maintainer verbatim 「跟主流对齐」). Item 2 of that record:

显式包容:要「所有成立的分支都走」,作者必须在判断节点上显式声明(工作名 mode: 'inclusive',对应 BPMN 包容网关、n8n 的那个开关)。这是 DecisionConfigSchema 上一个可选键 ⇒ packages/spec 契约变更,Clause-②: yes,实现 PR 走 needs:contract-review。

The same record's routing note sets the split order:

按 spec 键先落 → 引擎语义 + os migrate meta 迁移同一个 PR 落 → 文档的顺序拆;⛔ 语义变更与迁移不能分开落地

What this card is, and is not

Dedupe — query run by this seat, hit count including closed

Query (semantic, repo-scoped, closed included): decision node mode inclusive DecisionConfigSchema exclusive first match out-edges spec key → 3 hits, 0 duplicates. The hits were #15429 (the parent), #8593 (closed; dashboard component schema, unrelated) and #4027 (closed; node config keys validated at author time, adjacent but a different question).


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Sep 25, 2026

    @objectstack-fleet
    ContributorAuthor

    Path: approvals and automation | automation.flow-node-type-matrix | P2

    Triage: first grade — domain:spec · enhancement · priority:p2 · area:workflow · pm:queue · pm:blocking

    Triage: lands in packages/spec (DecisionConfigSchema, packages/spec/src/automation/schemaless-node-config.zod.ts:409 on origin/main a8bcce6) ⇒ domain:spec; rationale: a ruled task, so execution rather than a decision — the maintainer's ruling on #15429 (record 5793803317, re-read in this act: 「跟主流对齐」, and its item 2 「显式包容」 names mode: 'inclusive' as an optional key on this schema); enhancement because the key widens the accept set of a published schema (Clause-②: yes, needs:contract-review, as the ruling says); priority:p2 inherited from #15429, which this card unblocks (Blocked-by: #19867 in 5795620752) ⇒ pm:blocking.

    Triage seat #6015 · session_01CRZSc7dU8oDStbTbSwhuZe · 2026-09-25T04:36Z. ⛔ Not a claim, ⛔ not a dispatch. Read: this card (no comments yet), ruling record 5793803317 and the blocker line on #15429, and origin/main.

    Prior rulings read: thread none on this card; the governing record is the #15429 ruling above. The keyword ADR hits check-prior-rulings.mjs prints for these terms are generic and none rules on this key.

    Execution notes

    1. The split order is the ruling's: this key first, then A decision node with no declared config.conditions takes EVERY out-edge whose condition holds, in parallel — nothing enforces or warns that intended-exclusive edges partition #15429's engine semantics and the os migrate meta conversion in one PR, then docs. ⛔ Do not fold A decision node with no declared config.conditions takes EVERY out-edge whose condition holds, in parallel — nothing enforces or warns that intended-exclusive edges partition #15429's engine change into this PR.
    2. The card's own caution holds: the key alone changes no behaviour. Word the .describe() text and the docblock so they do not claim first-match before the engine does.
    3. Pins: mode absent and 'exclusive' and 'inclusive' parse; any other value is refused with a prescription.
  2. objectstack-fleet commented on Sep 27, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1 · 2026-09-27T02:17Z
    Session: session_01Rjy9MeetSfq34PKn81CRiN
    Account: os-zhuang
    Branch: claude/issue-19867-decision-mode-inclusive
    Worktree: objectstack-issue-19867
    Domain: domain:spec
    Seat: domain:spec#1
    File surface: packages/spec/src/automation/schemaless-node-config.zod.ts (the DecisionConfigSchema block and its docblock only) and its tests under packages/spec/src/automation/; the liveness ledger row the new key owes under packages/spec/liveness/, if the gate asks for one; the generated artefacts that follow (authorable surface, JSON Schema, content/docs/references/**), regenerated and never hand-edited; .changeset/. ⛔ Not packages/services/**, ⛔ not the os migrate meta conversion, ⛔ not content/docs/automation/flows.mdx: those are #15429's, per the ruling's split order. (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: default judgment tier (dispatch-gates.mjs --tier at 8d1f7ab7: 「no path-derived mandate … floor · default · ceiling」, the default slot taken; build at the default tier, review at CONTRACT_REVIEW_TIER before enqueue).
    Clause-②: yes
    Thread-read: 5826828035
    Serial constraints cleared: Ruling-ref: 5793803317 (#15429, item 2; retrieved this act). Same file: no open PR touches schemaless-node-config.zod.ts. Same package dir packages/spec/src/automation/: #19938 (seat 5, claim 5811386948, surface builtin-node-config.zod.ts + flow-node-expression-paths.ts, branch tip = a main commit, no work pushed), files disjoint; shared generated artefacts are regenerated on base merge. #19961 (queue, DecisionConditionSchema in this same file) is NOT taken and serialises after this card. #15429 (domain:services, pm:blocked on this card) is the consumer. No pm:epic territory intersects.

  3. objectstack-fleet commented on Sep 27, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 19867,
    "status": "done",
    "branch": "claude/issue-19867-decision-mode-inclusive",
    "pr": "#20162",
    "session": "session_01Rjy9MeetSfq34PKn81CRiN — the PM session id (mode:subagent); identity = the branch named by claim 5851910762, re-read before the first edit",
    "premise_still_valid": true,
    "summary": "DecisionConfigSchema now declares an optional mode: z.enum(['exclusive','inclusive']) with no .default() (parse({}) still returns {}), and every other value is refused at ['mode'] with invalid_value and one schema-level prescription naming both members' meanings and what an omitted key means. The .describe(), the DecisionConfigSchema docblock, the module header and the changeset state what each value DECLARES and say in plain words that nothing reads mode yet (today an edge-branched decision takes every out-edge whose condition holds). They also rewrite, instead of repeating, the two existing sentences that called the edge-branched shape 'a plain BPMN exclusive gateway' and the now-false 'conditions is its only key'. Zone 2 measured: (1) holds at 8d1f7ab, since only conditions was declared and the engine half has not landed; (2) holds, since logic-nodes.ts reads only config.conditions, the traversal reads no decision config key, and the status-quo pin stays green; (3) the key is outside the liveness walk (check:liveness green, no row owed), the authorable surface gains exactly +automation/DecisionConfig:mode, and the base is untouched; (4) objectui's reconciliation test at pin f8a9d0fb WILL red for decision once objectui installs a spec carrying mode, until its form offers the key. This repo's Console Pin Gate builds only, so this PR's CI is unaffected. Draft PR #20162, spec surface only; nothing in packages/services, os migrate meta or flows.mdx was touched.",
    "tests": "All heavy runs went through os-verify-lock; exit codes were captured before any pipe. All results are at HEAD 5fbb7ed, the final commit. (1) spec full suite pnpm --filter @objectstack/spec exec vitest run --project local --maxWorkers=2: 540 files passed, 15807 tests passed, 2 todo, VERDICT command-exit 0. (2) pnpm --filter @objectstack/spec typecheck: exit 0. The edited test file is in tsconfig.test.json's program (--listFilesOnly: 1 hit, 511 test files), and check:test-typecheck holds it at its 1 pre-existing pinned signature. (3) consumers: service-automation full, 145 files / 1729 tests passed; it includes config-expression-ledger, config-schemas, decision-overlapping-edge-conditions.pin (still green, so no behaviour moved), decision-branch-routing, logic-nodes and builtin-node-form-zod-ledger. metadata-protocol full: 188 files passed + 3 skipped, 2686 tests passed + 19 skipped; it includes reference-sites.derivation, and reference-sites.ts walks SCHEMALESS_NODE_CONFIG_SCHEMAS. lint: not a consumer, since a repo-wide git grep finds no DecisionConfigSchema or SCHEMALESS_NODE_CONFIG_SCHEMAS import in packages/lint. (4) ablation, after the commit, via scripts/ablation-replace.mjs: the schema-level error was replaced with error: () => undefined, anchor hits went 1->0, and the blob went 67cb1f11->02b7fc37. The test file read 7 failed / 30 passed: the 6 refusal cases plus the objectStackErrorMap-precedence case. After restore, git hash-object equals the HEAD blob 67cb1f11 and git diff HEAD is empty. (5) reverse verification on the REBUILT dist d.ts: a scratch tsc program assigning { mode: 'all' } to DecisionConfig exits 2 with TS2322 "Type '\"all\"' is not assignable to type '\"inclusive\" | \"exclusive\" | undefined'"; the control leg exits 0. Through service-automation's resolution of @objectstack/spec/automation (dist), { mode: 'all' } is refused with invalid_value at ['mode'] and { mode: 'inclusive' } is accepted. (6) spec check:generated after gen:docs: 15/15 artifacts current.",
    "gates": "Re-derived with node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at 5fbb7ed on the 5 changed paths: 107 commands, 36 more than the dispatch lead. Every command was run with its exit code written to disk (/tmp/claude-0/-home-user/d7055c86-c1e6-5545-851c-a50c5d9fd367/scratchpad/issue-19867/gate-results.tsv, per-command logs in /tmp/claude-0/-home-user/d7055c86-c1e6-5545-851c-a50c5d9fd367/scratchpad/issue-19867/gatelogs/). --ran /tmp/claude-0/-home-user/d7055c86-c1e6-5545-851c-a50c5d9fd367/scratchpad/issue-19867/ran.list: 107 derived, 105 run, 2 NOT-MEASURED, 0 UNRUN, exit 0. NOT MEASURED: check:dual-build-cjs-loads (exit 3 PREREQUISITE; it needs every workspace package's dist, i.e. a whole-workspace build; substitute: all 18 CJS require entries of @objectstack/spec load, 0 fail) and check:type-check-debt (exit 3 PREREQUISITE; it needs the whole-workspace .d.ts closure that lint.yml builds first; no DEBT-ledgered package's TypeScript is touched). check:skill-examples first read exit 3 (client-react unbuilt), then exit 0 after building the client-react closure with declarations; the ran list records the final 0. Extra: the roster gates whose rosters sit under my paths (check:authz-resolver, check:error-code-casing, check:filter-alias-parity, spec check:error-code-provenance) all exit 0. check-changeset-no-major level axis driven offline with --event on a pull_request payload carrying the exact PR body: exit 0, 'LEVEL AXIS: this PR declares clause-② yes, and no package whose packages//src/ it moves is graded patch'. check-adr-0087-registration: 1 non-breaking changeset. CI-only, NOT MEASURED locally: the path-scheduled CI jobs and the type-check lanes that dispatch-gates names as outside the list.",
    "line_budget": "n/a: no skills/** or governed-surface file in the diff. Size: 5 files, +190/-20 (210 changed lines, under the 5000 human-merge threshold).",
    "files_changed": [
    ".changeset/19867-decision-config-mode.md (+12/-0; @objectstack/spec minor, Clause-②: yes (widening))",
    "content/docs/references/automation/schemaless-node-config.mdx (+8/-5; generated by gen:docs)",
    "packages/spec/authorable-surface/automation.json (+1/-0; generated by the build: automation/DecisionConfig:mode)",
    "packages/spec/src/automation/schemaless-node-config.test.ts (+83/-4)",
    "packages/spec/src/automation/schemaless-node-config.zod.ts (+86/-11)"
    ],
    "deviations": [
    "Wording choice (Zone 2 item 2, card and triage note 2). The .describe() itself, and not only the docblock, carries the not-yet-read status. Every value is phrased as what the author DECLARES, never as what the engine does. The two pre-existing 'plain BPMN exclusive gateway' sentences were rewritten rather than left in place. This is within the card's 'Is' bullet (describe + docblock rewritten to describe the two modes), not a departure.",
    "Scope sentence: the docblock/describe say mode speaks about the out-edges and 'a conditions list is first-match on its own'. That is grounded in the ruling's item 1 and its conversion, which address only decisions without config.conditions, and the conditions-list executor reads no mode. No refinement forbids mode next to conditions, and no meaning is assigned to that combination. That question is routed to #15429 as a carrier item in out_of_scope_findings rather than open_questions, because a non-empty open_questions on THIS card reads (half-states H52) as a maintainer decision owed here, and it is not owed here.",
    "PR body carries the claim's line verbatim (Clause-②: yes); the changeset adds the widening arm (Clause-②: yes (widening)). Both read as yes to check-changeset-no-major and the level axis is green.",
    "Commit trailers use AGENTS.md's model-free pair (Co-authored-by: Claude + Claude-Session). The harness attribution reminder asked for a model-named Co-Authored-By; it yields to AGENTS.md and the pre-push hook refuses a model identifier.",
    "Local build state only, nothing committed: the consumer closure (20 packages) was built JS-only with OS_SKIP_DTS=1 to run their tests (OS_SKIP_DTS is in turbo globalEnv, so there is no cache cross-talk). The client-react closure and metadata-core were then rebuilt WITH declarations for check:skill-examples / check:type-check-debt. One spec rebuild was needed because my own outer timeout killed a turbo run mid spec:build and left spec/dist partial (22 files, 0 .d.ts); it was detected via dist inspection and fully rebuilt before any dist-reading gate ran.",
    "Main moved 2 commits after the branch point (9401b84, 49144fc; packages/rest and packages/core only, 0 packages/spec paths). No merge was made; there is no overlap with this diff.",
    "Answered one PM status probe via SendMessage (not a GitHub write)."
    ],
    "settled_choices": [
    "No .default('exclusive'). Business need: the ruling's migration writes explicit inclusive and nothing needs a parsed default today. Long-term: the parsed output stays the authored shape, so no consumer's fixture flips. AI-error: a JSON-Schema default would read as enforced while nothing reads the key. Startup focus: smallest surface. Pinned: parse({}) === {} and no JSON-Schema default.",
    "One prescription for every invalid value, with a schema-level error that outranks objectStackErrorMap. AI-error axis: the likely wrong values ('all', 'first', 'parallel', true) are the same idea spelled the way other engines spell it, which a did-you-mean cannot reach. Ablation proves the message is load-bearing."
    ],
    "mcp_calls": "0 (no MCP GitHub tools used; reads went through single-card REST GETs with the ambient token)",
    "api_writes": "3 REST writes, each through the fleet-write relay as objectstack-fleet[bot], each carried by one POST /repos/objectstack-ai/objectstack/dispatches from this container: (1) pr_create -> POST /repos/objectstack-ai/objectstack/pulls (draft), relay run 36293009940, PR #20162; (2) label-write --assign os-zhuang -> POST /repos//issues/20162/assignees, relay run 36293045004, read back MATCHES; (3) this os-dev-report comment -> POST /repos//issues/19867/comments via post-stamped. No label added (the dispatch named none; skip-changeset does not apply because the diff publishes). Not REST: 3 paced git push (branch probe, 7419430, 5fbb7ed).",
    "open_questions": [],
    "out_of_scope_findings": [
    "carrier: #15429 (its engine + os migrate meta PR) · noted in PR #20162 Acceptance notes, not filed. objectui's flow-node-config.spec-reconciliation.test.ts (measured read-only at .objectui-sha f8a9d0fb) reconciles the decision form against DecisionConfigSchema.shape in both directions, and the pinned form offers only conditions. So on objectui's first install of a spec carrying mode, the 'read by the executor but not offered by the designer form' assertion reds for decision until the form offers mode. Best landed with or after #15429's engine change, so the designer never offers a switch the engine ignores. This repo's CI is unaffected, because the Console Pin Gate builds and runs no objectui tests. Dedupe words: decision mode objectui reconciliation flow-node-config.",
    "carrier: #15429 · noted, not filed. Open design point for the engine half: what mode: 'inclusive' means on a decision that ALSO declares config.conditions. That shape takes one branch by label, and the ruling's item 1 and its conversion address only the edge-branched shape. Options: A) refuse the combination at authoring (os validate / registerFlow parse), B) give it a meaning (all matching entries), C) leave it inert. Four axes: business need, since the ruling's own producer (the migration) writes inclusive only onto edge-branched decisions, so B has no measured demand; long-term, A keeps one strict contract; AI-error, since C is a declared-but-inert key, the exact trap to avoid; startup focus, A is the narrowest. Recommendation A, decided with the engine change. Related, same carrier: decision config is export-only, so a stored mode: 'bogus' is not refused at run time today; the parse belongs with the reader. Dedupe words: decision mode inclusive conditions combination refuse."
    ]
    }

  4. objectstack-fleet commented on Sep 27, 2026

    @objectstack-fleet
    ContributorAuthor

    Review: ACCEPT · PR #20162 at head 5fbb7ed86a07287014af17e6a11749a720a7e20f · 2026-09-27T04:05Z

    domain:spec seat 1 (session_01Rjy9MeetSfq34PKn81CRiN), reviewer of record, on claim 5851910762. Checked against GitHub, ⛔ not against the report.

    Checklist

    • Shape: draft, base main, first line Fixes #19867; it is the body's only closing keyword, and the card's whole scope (the spec key only) is delivered.
    • Scope: 5 files, +190/−20, all inside the claimed surface: schemaless-node-config.zod.ts + its test, the generated authorable-surface/automation.json (+automation/DecisionConfig:mode) and reference page, and the changeset. ⛔ Nothing in packages/services/**, no os migrate meta conversion, no flows.mdx, as the ruling's split order requires. No governed path; 210 lines.
    • Diff read by the seat. mode: z.enum(['exclusive','inclusive']), .optional(), no .default(), so the parsed output of existing configs is unchanged. Every other value is refused at mode with one prescription that names what each member means. The two sentences that called the edge-branched shape 「a plain BPMN exclusive gateway」 are rewritten rather than repeated. The .describe(), the docblock and the changeset each say plainly that nothing reads mode until A decision node with no declared config.conditions takes EVERY out-edge whose condition holds, in parallel — nothing enforces or warns that intended-exclusive edges partition #15429's engine change lands, which answers the card's own caution.
    • Changeset: @objectstack/spec minor, Clause-②: yes (widening); consistent with the claim's yes.
    • Tests: absent / 'exclusive' / 'inclusive' parse; the refusal cases assert path + prescription. The ablation (prescription → undefined) went 7 red and was restored clean. Consumers: service-automation 145/145 files, including the status-quo pin decision-overlapping-edge-conditions.pin.test.ts still green, and metadata-protocol 188 files green.
    • CI at this head: 13 success / 2 skipped / 17 in progress / 0 failing (an honest in-progress reading; landing waits for it).

    Contract review: owed on both limbs (claim Clause-②: yes, and the path limb). An isolated at-tier reviewer is running. needs:contract-review is hung on PR #20162 in the same act. The PR is ⛔ not readied and ⛔ not enqueued before a same-shape PASS lands on this head.

    Deviations: none that changes the verdict. The changeset adds the (widening) arm to the claim's yes, and the commit trailers follow AGENTS.md's model-free pair.

    Findings, one line each

  5. objectstack-fleet commented on Sep 27, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed: PR #20162 → 369bcbeda7 · 2026-09-27T04:45Z

    domain:spec seat 1 (session_01Rjy9MeetSfq34PKn81CRiN), claim 5851910762. Landing record.

  6. added 2 commits that reference this issue on Sep 28, 2026
    369bcbe
    733822c
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:workflowApprovals and automation — the work that runs without a person driving itdomain:specenhancementNew feature or requestpm:blockingpriority:p2Medium: important, M3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions