Skip to content

docs(spec): describe innerRefusal as a node's read, write or script function-resolution refusal - #33519

Draft
objectstack-fleet[bot] wants to merge 1 commit into
mainfrom
claude/project-thread-qecamw
Draft

objectstack-fleet[bot] wants to merge 1 commit into
mainfrom
claude/project-thread-qecamw

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Leehom · project thread

Fixes #33402

Clause-②: no
Metadata-protocol: none

维护者速读

  • 改了什么:innerRefusal 的说明文字(AutomationResult.innerRefusal 与 FlowFailureDetailsSchema.innerRefusal 的 describe、FlowInnerRefusalSchema 与 FlowFailureDetailsSchema 的 JSDoc、contracts/automation-service.ts 的 JSDoc)从「节点写入时遇到的拒绝」改为「节点在运行中遇到的拒绝(数据节点的读或写,或 script 节点解析函数名)」;生成的 API 参考随之更新。
  • 为什么改:运行时四个数据节点(含读取的 get_record)和 script 节点函数名解析失败(RESOURCE_CONFLICT,ADR-0150 D3)都会填这个字段;旧说法让写流程错误处理的客户端和 AI 以为读取或函数名拒绝不会出现在这里。
  • 风险与回滚:只改说明文字,schema 形状、枚举、可选性、导出和类型都不变,同一输入解析结果不变;spec patch changeset。回滚即 revert。
  • 先看:packages/spec/src/api/automation-api.zod.ts 的两处 innerRefusal describe;其次 content/docs/references/api/automation-api.mdx 的两行生成结果。
  • 两分钟验证:git grep -n -i "write met inside" -- packages/spec/src content/docs/references,预期零命中(main 上 7 处)。
  • 需要你做的:无,席位落地。

What changes

Wording only. The text now says what the producers on main do: refusalCarriedBy (packages/services/service-automation/src/inner-refusal.ts) is spread into all four data-node catch arms of builtin/crud-nodes.ts (get_record's arm included, so a READ refusal lands in innerRefusal), and into the script node's function-resolution refusal in builtin/screen-nodes.ts (a config.function that names no single function for the flow's package, code: 'RESOURCE_CONFLICT', ADR-0150 D3, PR #32257).

  • packages/spec/src/api/automation-api.zod.ts
    • FlowInnerRefusalSchema JSDoc, first sentence: FROM "The refusal a flow node's write met INSIDE the run — the structured answer the data layer gave the node, …" TO "The refusal a flow node met INSIDE the run (a data node's read or write, or a script node's function resolution) — the structured answer the data layer (or the function resolver) gave the node, …". The "(or the function resolver)" clause is added because "the data layer" alone would be false for the script node's refusal; the rest of the sentence is unchanged.
    • AutomationResult.innerRefusal (TriggerFlowResponseSchema) .describe: FROM "The refusal a node's write met inside the run, structured: …" TO "The refusal a node met inside the run (a data node's read or write, or a script node's function resolution), structured: …". The rest is byte-identical.
    • FlowFailureDetailsSchema JSDoc: FROM "the refusal a node's write met inside the run, as innerRefusal" TO "the refusal a node met inside the run (a data node's read or write, or a script node's function resolution), as innerRefusal".
    • FlowFailureDetailsSchema.innerRefusal .describe: FROM "The refusal a node's write met inside the run (AutomationResult.innerRefusal): …" TO "The refusal a node met inside the run (a data node's read or write, or a script node's function resolution; AutomationResult.innerRefusal): …".
    • userMessage's describe already says "the refusal a node met inside the run": left as it is.
  • packages/spec/src/contracts/automation-service.ts, AutomationResult.innerRefusal JSDoc: same change as above (the card did not list this site; the order added it).
  • content/docs/references/api/automation-api.mdx: regenerated with cd packages/spec && pnpm gen:schema && pnpm gen:docs (not hand-edited). Only the two innerRefusal rows move (the FlowFailureDetails and AutomationResult tables). No other generated file changed.
  • .changeset/33402-inner-refusal-describe.md: '@objectstack/spec': patch. The describe text ships in spec dist and the JSDoc in its declarations.

No schema shape, enum, optional/required, export or type changes.

Tests

  • No behaviour changes, so no test is added and nothing is ablated. No test or snapshot pins the old sentence: on origin/main f068cde8, git grep -n "write met" over packages/spec, content/docs and skills hit only the five edited source sites and the two generated rows.
  • After the edit, git grep -n "write met" -- packages/spec content/docs skills shows zero hits that describe innerRefusal (the remaining hits are "write metadata …" sentences and released CHANGELOG.md entries about other subjects).
  • On 4da1fb68: pnpm --filter @objectstack/spec typecheck exit 0; pnpm --filter @objectstack/spec exec vitest run --maxWorkers=2 src/api/automation-api src/contracts/automation-service: Test Files 2 passed (2), Tests 93 passed (93) (lock VERDICT command-exit 0).

Acceptance notes

Comments in packages/services/** are outside this card's surface. Two still describe the inner refusal as the data nodes' alone; reported, not touched:

  • packages/services/service-automation/src/engine.ts:416-420 (NodeExecutionResult.innerRefusal doc): "The refusal this node's data call met … Set by the CRUD executors' catch arms". The script node's function-resolution refusal sets it too.
  • packages/services/service-automation/src/inner-refusal.ts:54-56 (refusalCarriedBy doc): "the one call each data node's catch arm makes (builtin/crud-nodes.ts), so the four arms cannot drift …", and :64 "the caller asked for a write the platform will not do". builtin/screen-nodes.ts also calls it.
  • FlowInnerRefusalSchema.code's describe ("the ADR-0112 member a record save of the same write answers in error.code") and the contract JSDoc's "the shape a record save of the same write answers with" keep their wording: they name the comparison for the code spelling, not which node met the refusal, and the order keeps each sentence's other clauses as they are.

Verification

On 4da1fb68 (after building the spec, @objectstack/lint, @objectstack/objectql, client and client-react closures): node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 103 commands, run one by one with timeout 900, each exit code captured before any pipe.

  • 101 exited 0 in the loop, among them pnpm --filter @objectstack/spec run check:generated (the regenerated reference is current), check:docs, check:skill-examples and pnpm check:nul-bytes.
  • node scripts/check-plugin-teardown-shape.mjs --self-test exited 3 in the loop (shallow clone: its pinned fixture commit was absent). After git fetch --depth=1 origin 621a4876… it exited 0 ("48 cases pass").
  • pnpm check:dual-build-cjs-loads exited 3 (PREREQUISITE NOT MET: it needs every package's dist; only the closures above were built), so it is NOT MEASURED here and left to CI.
  • --ran reconciliation: "103 derived famil(ies) accounted for — 102 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3)".
  • Local-only node scripts/check-dev-prereqs.mjs exited 1 because 91 of 107 workspace packages have no dist (only the closures above were built); it judges this checkout's build state, not the diff, so it is NOT MEASURED for this change.
  • Spec drift gate (order hypothesis): yes. pnpm --filter @objectstack/spec run check:docs (also run inside check:generated, whose roster maps it to gen:docs and content/docs/references/**) reds a reference that no longer matches the .describe text; cd packages/spec && pnpm gen:schema && pnpm gen:docs regenerates it. Both exited 0 on this head.

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. (No formatter ran; pnpm lint is CI's run, not run locally.)
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: its generated artifacts are regenerated and committed.
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English.
  • The Before Submitting list in CONTRIBUTING.md is done. (Left to the seat's review.)

🤖 Generated with Claude Code

https://claude.ai/code/session_01FEkQ5mkZ32rnBNh7FK8TxM

The inner refusal of a flow run is filled by every data node's catch arm,
get_record's read included, and by a script node's function-resolution
refusal. The describe and JSDoc text on AutomationResult.innerRefusal,
FlowFailureDetailsSchema.innerRefusal and FlowInnerRefusalSchema said
"a node's write"; they now name the read, the write and the script
node's function resolution. The generated API reference follows.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FEkQ5mkZ32rnBNh7FK8TxM
@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 14, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

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

  • content/docs/api/client-sdk.mdx (via AutomationResult (symbol, a top-level interface))
  • content/docs/api/declarative-endpoints.mdx (via /api/v1/automation/:name/trigger (route, bridged from symbol TriggerFlowResponseSchema — its route source's handler names it))
  • content/docs/automation/flows.mdx (via AutomationResult (symbol, a top-level interface), /api/v1/automation/:name/trigger (route, bridged from symbol TriggerFlowResponseSchema — its route source's handler names it))
  • content/docs/protocol/kernel/http-protocol.mdx (via /api/v1/automation/:name/trigger (route, bridged from symbol TriggerFlowResponseSchema — its route source's handler names it))

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

  • content/docs/releases/v17/17-1.mdx (via AutomationResult (symbol, a top-level interface))
  • content/docs/releases/v17/17-3.mdx (via AutomationResult (symbol, a top-level interface))
  • content/docs/releases/v17/17-6.mdx (via AutomationResult (symbol, a top-level interface))

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 65 of 247 client-bound route-ledger rows — the other 182 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 182: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 59 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 123 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 — 172 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 e40cd1e28dd79e60e7f4be0db06f071f3f962574 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json e40cd1e28dd79e60e7f4be0db06f071f3f962574

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

@objectstack-fleet objectstack-fleet Bot added the needs:contract-review Marker only — PR awaits its at-tier Contract review PASS; no check reads it, the record decides label Nov 14, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#4 · os-project-manager · session_01FEkQ5mecqk8M5q9vjKj6aS

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 4da1fb68df06244e029b1644db5846664572116c
Local-runs: none
Scope: packages/spec/src/api/automation-api.zod.ts, packages/spec/src/contracts/automation-service.ts + none

Inputs read: card #33402 (body and its three comments: triage 6884924903, claim 6885523313, os-dev-report 6885846785; no earlier review record), PR #33519 (body, 4 files, net diff against main, one commit on merge-base f068cde8), and the check-runs on this head. The producers were read at the head with git show; main has not moved any of them, nor the two hits files, since the merge-base.

Check-runs on the head as read (50): 14 success, 13 skipped, 23 in_progress, 0 failed. Completed green: Check PR Size, Check Documentation Links, Spec property liveness, Governed Surface Queue Guard, PR-body guards, Lint · PM dispatch-gates, Lint · scoped · tooling self-tests, Flag docs affected by code changes, Dogfood Regression Gate, Plan Test UI shards, Auto Label, filter. Still running: Check Changeset (read success on the first read of the check-runs, in_progress on the second, a newer run), Build Core, Build Docs, Build UI Deps, Test Core 1/8 to 8/8, Type Check (workspace, source gates, consumer gates, debt ledger), Lint (ESLint, repo & tooling gates, docs & code guards, CI release & engine gates, scoped slot lookup & corpus, scoped query-options erasure), Live E2E UI. The generated-reference drift gate (check:generated / check:docs) sits in the running lint families, so it is NOT yet answered by CI on this head. This verdict is the design judgment; the merge still waits on those runs.

① Derived judgments

  • Accept set: unchanged. RIGHT. Every hunk in the two hits files is JSDoc or a .describe() string. No z. call, key, enum member, .optional(), export or type is touched; the same input parses to the same output on both sides of the diff.
  • Public surface: three description strings move, nothing else. RIGHT. TriggerFlowResponseSchema result innerRefusal describe, FlowFailureDetailsSchema.innerRefusal describe, and the JSDoc of FlowInnerRefusalSchema, FlowFailureDetailsSchema and AutomationResult.innerRefusal. The describe text ships in spec dist and the JSDoc in its declarations.
  • Generated reference: content/docs/references/api/automation-api.mdx moves exactly the two innerRefusal rows (FlowFailureDetails, AutomationResult), and each row's new text is byte for byte the concatenated describe string of its source. RIGHT. No other generated file is in the diff, and git grep -i "write met" at the head finds no remaining sentence about innerRefusal in packages/spec, content/docs or skills.
  • No behaviour change, so no test and no ablation are owed. RIGHT.

② Semver level

.changeset/33402-inner-refusal-describe.md is '@objectstack/spec': patch. That matches what the diff publishes: description text only, in one package. No other package's published output changes (the .mdx is docs, not a package).

Clause-②: no. Present on the PR body and in the changeset, with Metadata-protocol: none on both. Correct: nothing a consumer could parse, author or call yesterday is refused, renamed or re-typed today.

③ Boundary flags

  • open_questions: empty. Nothing to answer.
  • Dev flag, surface: the card listed only automation-api.zod.ts; the claim (6885523313) added contracts/automation-service.ts:771 to the file surface before the work started. The diff stays inside the claimed surface (two sources, the generated reference, one changeset). Answered.
  • Dev flag, the one clause beyond "write met": "the data layer gave the node" became "the data layer (or the function resolver) gave the node". Accepted: without it the sentence is false for the script producer (see ④).
  • out_of_scope_findings (two, both carrier: none): the comments at packages/services/service-automation/src/engine.ts:416-420 and inner-refusal.ts:54-64 still describe the member as the data nodes' alone. Confirmed by reading both at the head. They are comments outside this card's surface and outside the diff, so not reviewed here and no bar to this PR. ESCALATED to the seat: they have no carrier, so the seat files one p3 card for them or records them as accepted. This record does not file it.

④ Design judgment

The new sentence, in all five sites: "the refusal a node met inside the run (a data node's read or write, or a script node's function resolution)".

Producers, read at the head. innerRefusal is minted in one place only, refusalCarriedBy (inner-refusal.ts:86), and it has five call sites:

Call site What threw Fits the sentence
builtin/crud-nodes.ts:669 get_record catch data.find / data.findOne a data node's read
builtin/crud-nodes.ts:831 create_record catch the insert a data node's write
builtin/crud-nodes.ts:947 update_record catch the update a data node's write
builtin/crud-nodes.ts:1026 delete_record catch the delete a data node's write
builtin/screen-nodes.ts:659 script engine.resolveFunction throwing PackageScopedNameRefusedError (status 409, code RESOURCE_CONFLICT) a script node's function resolution

Every other path that puts the member on a result only CARRIES one of those five: dag-traversal.ts:519 brands the node failure and :1595, engine.ts:8097 and run-resume.ts:2264 read the brand at the terminal exits; subflow-node.ts:178, map-node.ts:288 and run-resume.ts:1550 copy a child run's; try-catch-node.ts:244/356 and engine.ts:11194 carry the last attempt's or the catch region's. In each, the refusal was still met by a data node or a script node inside the run (or its child run), so the sentence holds. The runtime doors (flow-dispatch-status.ts:268) and the MCP reader (mcp-http-tools.ts:805) pass it on verbatim.

  • No producer is left out. The four crud-nodes.ts executors are the only category: 'data' executors; connector_action, http, notify, wait, screen, decision, assignment, loop, parallel and save_notice never set the member.
  • Nothing is over-claimed. The script clause says "function resolution", not the function's own run: the handler's catch (screen-nodes.ts:706-713) sets no member, and the unchanged "Absent on every other failure (a script that threw, …)" in the same JSDoc still says so. RESOURCE_CONFLICT is a standard-catalog member (api/errors.zod.ts:95) that ErrorCode (api/error-code-ledger.zod.ts:1968) includes, so the produced value parses against FlowInnerRefusalSchema; flow-function-package-scope.test.ts:134 pins it.
  • "(or the function resolver)" is correct: the script refusal comes from the package-scoped resolver, not the data layer.
  • First consumers (the flow doors' FLOW_FAILED details, the MCP tool's message ladder) read code and message and never the description, so no consumer changes.
  • Attempt to break it: two failures a reader might expect under the new wording do not fill the member. An unregistered function name (screen-nodes.ts:662-668, the resolver's not-found, not a refusal) and get_record's pre-engine storedMetadataFilterRefusal return. Both are consistent with the text, which every site still closes with "only when a refusal was met" / "Absent when the failure met no refusal": the sentence states which nodes can fill it, not that every failure of theirs does. No finding.

Findings:

  • N1 (nit). FlowInnerRefusalSchema.code's describe ("the ADR-0112 member a record save of the same write answers in error.code"), the same phrase in that schema's Semantics JSDoc, and "the shape a record save of the same write answers with" in the contract JSDoc keep a write-only comparison. For a read refusal or a resolver refusal there is no "same write". It names the code's spelling, not which node fills the member, and the sentence above it now says the latter, so it has no effect on correctness or the contract. The PR body names it as left on purpose. A fix would be in a hits file and would move the head: leave it as an accepted deviation, or carry it on a follow-up card.
  • N2 (nit). The unchanged "The problem it solves" paragraph still says "met by a flow's write node". It is history of the defect that B′ closed, accurate as history. No change asked.

No must-fix, no should-fix.

Implemented-by: claude/project-thread-qecamw
Reviewed-by: session_01FEkQ5mecqk8M5q9vjKj6aS

VERDICT: PASS

@objectstack-fleet objectstack-fleet Bot removed the needs:contract-review Marker only — PR awaits its at-tier Contract review PASS; no check reads it, the record decides label Nov 14, 2026

This branch has not been deployed

No deployments
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

Development

Successfully merging this pull request may close these issues.

spec(automation-api): innerRefusal is still described as "the refusal a node's write met"; a read or a script node's function-name refusal fills it too

1 participant