Skip to content

docs(spec): document typed approval decision outputs as enforced at the decision, not a widget hint - #29371

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-kmo37m
Nov 2, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-kmo37m

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

domain:spec#1 · os-steve · session_01ADHkmPi25XXKx92Xmg4viS

Requested by Steve · project thread
Fixes #29340

Clause-②: no
Metadata-protocol: none

维护者速读

  • 改了什么:审批节点「决策输出」的 spec 注释(三处)和审批文档一段,改成「type/multiple 在决策时强制校验」,删掉「只影响输入控件」的旧说法。
  • 为什么改:approvals: a typed user decision output is checked for presence only — an id that resolves to no user records the decision, then answers 500 RESUME_FAILED and strands the run #28658 裁决 A 要求运行时按声明校验决策输出;旧文字与 ADR-0104 2026-10-30 修订矛盾,且服务端 PR(席 5)要等这条文字先落地。
  • 风险与回滚:只改注释和文档,无 schema、键、describe、导出变化;回滚即 revert 本 PR。
  • 先看:packages/spec/src/automation/approval.zod.ts(DecisionOutputDefSchema 注释块)、content/docs/automation/approvals.mdx(typed declaration 段落)。
  • 两分钟验证:git grep -n "only shape" packages/spec/src/automation/approval.zod.ts content/docs/automation/approvals.mdx 应无输出。
  • 需要你做的:无,席位落地。

What changes

  • packages/spec/src/automation/approval.zod.ts, three docblocks (no code, no .describe() string):
    • DecisionOutputDefSchema: replaces "type and multiple only shape the INPUT WIDGET, never the accepted value" with the enforced contract: a picker output is judged by record id (user, department, team) or by machine name (position), through the lookup the matching expression approver's resolveAs uses; multiple: true takes a list with no entry twice, absent/false takes one value; a mismatch is refused with 400 VALIDATION_FAILED naming outputs.KEY before any write, the request staying pending; multiple binds every entry, text and bare keys included; text is judged by shape only.
    • required: "Unlike type/multiple — which only shape the input widget" becomes "type/multiple judge the SHAPE of a supplied value; this one judges PRESENCE".
    • decisionOutputs on the approval node config (a third copy the card did not name): "The type describes the INPUT WIDGET only — the runtime accepts whatever value shape arrives … verbatim" is rewritten to the same contract.
  • content/docs/automation/approvals.mdx: the typed-declaration paragraph drops "The type shapes only the input widget" and gains the enforced rule (a short list), and the required paragraph no longer contrasts against an unenforced type/multiple.
  • content/docs/automation/approvals.mdx next_reviewers example now declares { key: 'next_reviewers', multiple: true } (the bare key fed a list, which approvals: a typed user decision output is checked for presence only — an id that resolves to no user records the decision, then answers 500 RESUME_FAILED and strands the run #28658's server PR refuses).
  • .changeset/29340-decision-output-enforced-docs.md: @objectstack/spec patch (the docblocks ship in the published src/**/*.zod.ts).

No .describe() string carried the contradiction (checked: the type, multiple and decisionOutputs describes only say what the approver answers with), so nothing regenerates. Forced test edits: none.

Ordering: the runtime enforcement is #28658's domain:server PR (branch claude/project-thread-rsqu0h), which merges after this one; the text is written as the contract, in the present tense, as ADR-0104's amendment is.

Acceptance notes

  • Error code: VALIDATION_FAILED, not the card's VALIDATION_ERROR. The approvals door answers 400 VALIDATION_FAILED for a malformed decision (the guide's own error-code table; the existing required refusal throws VALIDATION_FAILED:), and the enforcement branch for approvals: a typed user decision output is checked for presence only — an id that resolves to no user records the decision, then answers 500 RESUME_FAILED and strands the run #28658 (head 88c96ad6) throws VALIDATION_FAILED naming each outputs.KEY. VALIDATION_ERROR is the action-params gate's code from ADR-0104 D2. The text names the code the door answers.
  • Approve and reject alike. The guide says the typed check runs on an approve and a reject alike, matching the enforcement branch ("each admitted output is judged … on an approve and on a reject alike"). If that PR changes its reject choice, this sentence moves with it.
  • plugin-approvals/src/approval-service.ts carries the same "only shape the input widget" comment beside the required check; that file is the server half's, so it is left to that PR.

Verification

Run on head fc024e80 (round 2: two text-only commits over 821642ea).

  • pnpm --filter @objectstack/spec build && pnpm --filter @objectstack/spec check:generated: ✓ All 14 generated artifacts are up to date, lock VERDICT command-exit 0.
  • Offline changeset check of this body: node scripts/check-changeset-no-major.mjs --base origin/main --event FILE exit 0, ✓ LEVEL AXIS (clause-② no, metadata-protocol none: a non-protocol change at patch). The card's Clause-②: no (narrowing) was also measured: the gate accepts it too (exit 0), but reads the arm as BREAKING, which a text-only diff is not, so this body declares bare no as the seat's claim does.
  • Derived gate list (dispatch-gates --commands, 96 commands): 96/96 exit 0 after a full turbo run build --filter=./packages/** (lock VERDICT command-exit 0); dispatch-gates --ran: 96 derived famil(ies) accounted for — 96 run, 0 NOT-MEASURED.
  • pnpm --filter @objectstack/spec typecheck && pnpm --filter @objectstack/spec test: Test Files 791 passed (791), Tests 23391 passed | 1 todo, lock VERDICT command-exit 0.

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic.
  • 2. Every name I added uses the casing for its kind. (No name added.)
  • 3. No formatter ran; pnpm lint passes (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 (none stale).
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English (the 维护者速读 block is the maintainer's Chinese summary).

Generated by Claude Code

The DecisionOutputDefSchema docblock, its `required` docblock and the
approval node's `decisionOutputs` docblock said `type`/`multiple` only shape
the input widget and the runtime accepts any value shape. Rewrite them, and
the approvals guide, to the enforced contract recorded by ADR-0104's
2026-10-30 amendment: picker outputs judged by record id or position machine
name, `multiple` judged as list-vs-single with no duplicates, a mismatch
refused 400 naming `outputs.KEY` before any write.

Claude-Session: https://claude.ai/code/session_01ADHkmP8ELNFNfBHmk387wA
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added the size/s label Nov 2, 2026
@github-actions

github-actions Bot commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

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

  • content/docs/releases/v17/17-7.mdx (via ApprovalNodeConfigSchema (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
  • the SDK route bridge reached 60 of 216 client-bound route-ledger rows — the other 156 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 156: 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; 101 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 1b6709b8ebc37d6ce1f73470fbebe7569d4dd650 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 1b6709b8ebc37d6ce1f73470fbebe7569d4dd650

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

@github-actions github-actions Bot added documentation Improvements or additions to documentation tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 2, 2026
…held

Seat 5's correction on the card, read against the server half: `multiple`
judges the shape of every typed output, `text` included (judged by shape
only), and a `position` value is judged by its holders, so a machine name
nobody holds is refused. Drop the "text is not judged" sentences.

Claude-Session: https://claude.ai/code/session_01ADHkmP8ELNFNfBHmk387wA
Co-authored-by: Claude <noreply@anthropic.com>
…rs example

The server half shape-checks every decision output, bare keys included:
absent `multiple` takes one value. Say so in the three docblocks and the
guide, and give the guide's "previous approver picks the next step's
approvers" example `{ key: 'next_reviewers', multiple: true }`, since the
list it sends would be refused under a bare key.

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

Copy link
Copy Markdown
Contributor Author

domain:spec#1 · os-steve · session_01ADHkmPi25XXKx92Xmg4viS

Head-sha: fc024e80 · read 2026-11-02T13:56Z

维护者速读

席位意见:可落地,等契约复审与 CI。

席位核对 — ACCEPT · domain:spec#1 · head fc024e80

  1. Shape: draft, base main, Fixes #29340, Clause-②: no / Metadata-protocol: none at line start in the body and the changeset (spec patch). The card wrote no (narrowing); the narrowing ships in the server PR fix(plugin-approvals)!: judge typed decision outputs against their declared type and multiple #29372, which carries its own BREAKING banner, and the dev measured that this text-only pair passes the level gate. Within claim 6577348141. Not governed.
  2. Diff, read on the head: comment lines only in approval.zod.ts (the DecisionOutputDefSchema, required and node decisionOutputs docblocks; the third copy was found by the dev's grep), plus approvals.mdx (the typed-declaration and required paragraphs, and the example that fed a bare key a list, now { key, multiple: true }). No .describe() moves, nothing regenerates.
  3. Deviation accepted: the refusal code is VALIDATION_FAILED, not the card's VALIDATION_ERROR. It is what the approvals route maps to 400, what main's required refusal throws, and what fix(plugin-approvals)!: judge typed decision outputs against their declared type and multiple #29372 throws.
  4. Sibling alignment: server seat 5 answered its Q4 = B, so every typed output, text and bare keys included, is shape-checked. Head fc024e80 says so in all three docblocks and the page.
  5. Ordering: this PR lands first and fix(plugin-approvals)!: judge typed decision outputs against their declared type and multiple #29372 follows, per the ruling. No release should cut between the two.
  6. Landing: one CONTRACT_REVIEW_TIER review (comment changes under packages/spec/src/**), run by the seat (claim line follows). Then the seat readies and queues once CI is green, and tells server seat 5.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#1 · os-steve · session_01ADHkmPi25XXKx92Xmg4viS

Contract-review-claim: session_01ADHkmPi25XXKx92Xmg4viS · head fc024e8 · 2026-11-02T13:57Z

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 2, 2026 14:38
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 2, 2026
Merged via the queue into main with commit 8e6ab05 Nov 2, 2026
70 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-kmo37m branch November 2, 2026 15:01
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/s tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

1 participant