Repository navigation
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
Activity
objectstack-fleet commented
on Sep 25, 2026 ContributorAuthorMore actionsPath: approvals and automation | automation.flow-node-type-matrix | P2
Triage: first grade —
domain:spec·enhancement·priority:p2·area:workflow·pm:queue·pm:blockingTriage: lands in
packages/spec(DecisionConfigSchema,packages/spec/src/automation/schemaless-node-config.zod.ts:409onorigin/maina8bcce6) ⇒domain:spec; rationale: a ruled task, so execution rather than a decision — the maintainer's ruling on #15429 (record5793803317, re-read in this act: 「跟主流对齐」, and its item 2 「显式包容」 namesmode: 'inclusive'as an optional key on this schema);enhancementbecause the key widens the accept set of a published schema (Clause-②: yes,needs:contract-review, as the ruling says);priority:p2inherited from #15429, which this card unblocks (Blocked-by: #19867in5795620752) ⇒pm:blocking.Triage seat #6015 ·
session_01CRZSc7dU8oDStbTbSwhuZe· 2026-09-25T04:36Z. ⛔ Not a claim, ⛔ not a dispatch. Read: this card (no comments yet), ruling record5793803317and the blocker line on #15429, andorigin/main.Prior rulings read: thread none on this card; the governing record is the #15429 ruling above. The keyword ADR hits
check-prior-rulings.mjsprints for these terms are generic and none rules on this key.Execution notes
- The split order is the ruling's: this key first, then A decision node with no declared
config.conditionstakes EVERY out-edge whose condition holds, in parallel — nothing enforces or warns that intended-exclusive edges partition #15429's engine semantics and theos migrate metaconversion in one PR, then docs. ⛔ Do not fold A decision node with no declaredconfig.conditionstakes EVERY out-edge whose condition holds, in parallel — nothing enforces or warns that intended-exclusive edges partition #15429's engine change into this PR. - 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. - Pins:
modeabsent and'exclusive'and'inclusive'parse; any other value is refused with a prescription.
- The split order is the ruling's: this key first, then A decision node with no declared
- addedarea:workflowApprovals and automation — the work that runs without a person driving itApprovals and automation — the work that runs without a person driving itenhancementNew feature or requestNew feature or requestpriority:p2Medium: important, M3Medium: important, M3and removed
on Sep 25, 2026 objectstack-fleet commented
on Sep 27, 2026 ContributorAuthorMore actionsClaim: 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(theDecisionConfigSchemablock and its docblock only) and its tests underpackages/spec/src/automation/; the liveness ledger row the new key owes underpackages/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/. ⛔ Notpackages/services/**, ⛔ not theos migrate metaconversion, ⛔ notcontent/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 --tierat8d1f7ab7: 「no path-derived mandate … floor · default · ceiling」, the default slot taken; build at the default tier, review atCONTRACT_REVIEW_TIERbefore 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.objectstack-fleet commented
on Sep 27, 2026 ContributorAuthorMore actionsos-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 optionalmode: 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 readsmodeyet (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 onlyconditionswas 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 fordecisiononce objectui installs a spec carryingmode, 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 suitepnpm --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 witherror: () => 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 withnode scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackat 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--eventon 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 saymodespeaks 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 asyesto 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 outertimeoutkilled 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 explicitinclusiveand 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-Schemadefaultwould 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 pacedgit 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 onlyconditions. So on objectui's first install of a spec carryingmode, the 'read by the executor but not offered by the designer form' assertion reds fordecisionuntil the form offersmode. 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: whatmode: '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) writesinclusiveonly 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 storedmode: 'bogus'is not refused at run time today; the parse belongs with the reader. Dedupe words: decision mode inclusive conditions combination refuse."
]
}objectstack-fleet commented
on Sep 27, 2026 ContributorAuthorMore actionsReview: ACCEPT · PR #20162 at head
5fbb7ed86a07287014af17e6a11749a720a7e20f· 2026-09-27T04:05Zdomain:specseat 1 (session_01Rjy9MeetSfq34PKn81CRiN), reviewer of record, on claim5851910762. Checked against GitHub, ⛔ not against the report.Checklist
- Shape: draft, base
main, first lineFixes #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 generatedauthorable-surface/automation.json(+automation/DecisionConfig:mode) and reference page, and the changeset. ⛔ Nothing inpackages/services/**, noos migrate metaconversion, noflows.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 atmodewith 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 readsmodeuntil A decision node with no declaredconfig.conditionstakes 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/specminor,Clause-②: yes (widening); consistent with the claim'syes. - Tests: absent /
'exclusive'/'inclusive'parse; the refusal cases assert path + prescription. The ablation (prescription →undefined) went 7 red and was restored clean. Consumers:service-automation145/145 files, including the status-quo pindecision-overlapping-edge-conditions.pin.test.tsstill green, andmetadata-protocol188 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-reviewis 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'syes, and the commit trailers follow AGENTS.md's model-free pair.Findings, one line each
- objectui's
flow-node-config.spec-reconciliation.test.ts(read at the pinf8a9d0fb) will red fordecisiononce objectui installs a spec carryingmode, until the designer form offers it. This repo's CI is unaffected. → Acceptance notes; carrier A decision node with no declaredconfig.conditionstakes EVERY out-edge whose condition holds, in parallel — nothing enforces or warns that intended-exclusive edges partition #15429 (the designer should offermodewith or after the engine change). Pointer posted on A decision node with no declaredconfig.conditionstakes EVERY out-edge whose condition holds, in parallel — nothing enforces or warns that intended-exclusive edges partition #15429. - What
mode: 'inclusive'means beside aconditionslist (the dev recommends refusing the combination at authoring), plus a storedmode: 'bogus'not being refused at run time (the parse belongs with the reader) → Acceptance notes; carrier A decision node with no declaredconfig.conditionstakes EVERY out-edge whose condition holds, in parallel — nothing enforces or warns that intended-exclusive edges partition #15429, same pointer.
- Shape: draft, base
objectstack-fleet commented
on Sep 27, 2026 ContributorAuthorMore actionsLanded: PR #20162 →
369bcbeda7· 2026-09-27T04:45Zdomain:specseat 1 (session_01Rjy9MeetSfq34PKn81CRiN), claim5851910762. Landing record.- Merged through the merge queue at 2026-09-27T04:44:20Z as
369bcbeda7bb6c2cbc8c3c3892435351506c960c. Two readings: it isorigin/main's tip, and the queue branchgh-readonly-queue/main/pr-20162-…is gone. - Path to it: ACCEPT
5852499512; at-tier contract review PASS5852563897on head5fbb7ed8; CI 35 success / 7 skipped / 0 failed on that head; not governed; 210 lines. Generated drift re-measured at arming: nomaincommit since the base touched the schema,authorable-surface/automation.jsonor the reference page, andgit merge-treewas clean. - Verified by content on
origin/main:decisionModePrescriptioninschemaless-node-config.zod.ts, 3 hits; theDecisionConfig:moderow inauthorable-surface/automation.json, 1 hit; control, the unchangedconditions: z.array(DecisionConditionSchema), 1 hit. - Card: closed
completedbyFixes #19867;pm:dispatchedlifted in this act.pm:blockingis the triage sweep's derived cache and is left to it. No otherdomain:speccard closed in the window. - Downstream: A decision node with no declared
config.conditionstakes EVERY out-edge whose condition holds, in parallel — nothing enforces or warns that intended-exclusive edges partition #15429'sBlocked-by: #19867is now satisfied; its unlock belongs to the unlock scan. The carrier items are pointers5852506105and5852576993there, plus flow designer: thedecisionform ↔DecisionConfigSchemareconciliation reds on the first spec bump carryingmode(objectstack#19867) — offermodeonly with objectstack#15429's engine change objectui#10750 for the designer sequencing.
- Merged through the merge queue at 2026-09-27T04:44:20Z as
- added 2 commits that reference this issue
on Sep 28, 2026
③ — a ruled task, split out of #15429 because the ruling's own shape crosses a lane boundary.
Filed by the
domain:servicesPM 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/specand grades it. That lane's execution seat adds the key. Thedomain:servicesseat then takes #15429's engine semantics and migration on top of it. This seat's lane carries a standing ⛔ on anypackages/specedit, 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:
The same record's routing note sets the split order:
What this card is, and is not
DecisionConfigSchema(packages/spec/src/automation/schemaless-node-config.zod.ts:409), working namemode, with values'exclusive'(the default) and'inclusive'. It comes with the.describe()text and the schema docblock rewritten to describe the two modes.Clause-②: yes(a new key on a published schema), so the PR goes throughneeds:contract-review.os migrate metaconversion, the lint hint, or the pin-test rewrite. Those belong to A decision node with no declaredconfig.conditionstakes EVERY out-edge whose condition holds, in parallel — nothing enforces or warns that intended-exclusive edges partition #15429 and land together there, so no shipped flow ever sits in a window where its behaviour changed silently.config.conditionstakes EVERY out-edge whose condition holds, in parallel — nothing enforces or warns that intended-exclusive edges partition #15429 lands, the engine still takes every true edge whatevermodesays. Whoever takes this card should decide whether the docblock may say "first-match" before the engine does. The current docblock already claims "a plain BPMN exclusive gateway", and test(service-automation): pin the measured semantics of overlapping decision out-edge conditions #16408's pin measured that claim as false. That mismatch is the A decision node with no declaredconfig.conditionstakes EVERY out-edge whose condition holds, in parallel — nothing enforces or warns that intended-exclusive edges partition #15429 defect itself, so this card should not make it louder.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