docs(automation): say when the fallbackApprovers pairing is actually enforced - #18868
Merged
Merged
Conversation
`approvals.mdx` said `fallback` without a `fallbackApprovers` list is "refused at authoring time". The pairing is real and enforced in both directions, but not at authoring time: the only non-test parser of `ApprovalNodeConfigSchema` is the executor (`plugin-approvals/src/approval-node.ts`), which `safeParse`s the node config at node entry, so both arms of `checkFallbackApproversPairing` fire at flow execution. `os lint` has no pairing arm for this. The page uses "authoring time" three other times (`:25`, `:27`, `:102`) where it correctly means `os lint`, so a reader arriving at this sentence expects `os lint` to catch a mis-paired fallback. It does not. Reword to state the real timing while keeping what is true: the refusal is loud, names both keys, and lands before the node opens any approval request. Precision fix, not a hole. Claude-Session: https://claude.ai/code/session_017ef78bLdybu3AffehKkhfk Co-authored-by: Claude <noreply@anthropic.com>
os-try-charles
marked this pull request as ready for review
September 18, 2026 03:11
os-try-charles
deleted the
claude/issue-18551-approvals-refusal-timing
branch
September 18, 2026 03:35
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #18551
The correction
content/docs/automation/approvals.mdxsaid theonEmptyApprovers: 'fallback'/fallbackApproverspairing is "refused at authoring time". The pairing is enforced, in both directions — but not at authoring time.Re-measured on this branch's base,
objectstack-ai/objectstack@a09d79708(the card and the dispatch measured18cc3b1df;mainmoved, and every leg still holds):ApprovalNodeConfigSchemaacrosspackages,appsandexamplesispackages/plugins/plugin-approvals/src/approval-node.ts:133—ApprovalNodeConfigSchema.safeParse(node.config ?? {}), the first statement of the executor'sexecute(). Every other hit in those trees is the schema's own definition, a test, a doc comment or a generated artifact.checkFallbackApproversPairing(packages/spec/src/automation/approval.zod.ts:728), attached with.superRefine(...)atapproval.zod.ts:947.os linthas no pairing arm for it.packages/lint/src/validate-approval-approvers.tsdeclares ten rule ids and none of them pairs the two keys; itsfallbackApproversmentions are remedy prose inside other rules' messages, and its own header records that the rule "reads SHAPE".So the refusal fires at flow execution, when the approval node parses its config at node entry.
Why the old wording misled rather than merely being imprecise
The same page uses "authoring time" three other times, and every one of them correctly means
os lint, or its correctly-stated absence::25— "os lintrejects the shapes it can prove at authoring time":27— "Nothing can flag that at authoring time":102— "os lintreports the shape at authoring time"A reader arriving at the fallback paragraph from those expects
os lintto catch a mis-paired fallback. It does not.What the replacement keeps
This is a precision fix, not a hole. All three properties of the enforcement are real and survive the rewrite:
success: falsewithApproval node '...' has invalid config: ...built from the issue paths and messages.checkFallbackApproversPairingnameonEmptyApproversandfallbackApprovers.safeParseguard returns ahead of theservice.openNodeRequest(...)call in the same function.The card also warned against inheriting PR #18525's body framing ("degrades to
admin_rescue"). The replacement sentence does not use it: through the executor a mis-paired node fails at the parse, and theadmin_rescuedegrade the next paragraph describes is the separate, correct case of a declared fallback list resolving empty at run time.Scope
Arm (a) of the card only — reword the sentence. Arm (b) (a lint pairing arm) is not in this PR and is not dropped: the dispatching seat files it as its own card, which is why the card's own scope is fully delivered here. (The
Fixesline at the top is the one and only closing binding in this body; this sentence deliberately carries none — seecheck:partof-closing-keywordRULE 3, which red-flagged an earlier wording that bound a second keyword to that number inside a sentence about what is not here.)Nothing here touches
packages/plugins/plugin-approvals/**, any schema, oros lint.:25,:27and:102are deliberately untouched — they are the card's evidence, not its target.Verification
Gate families derived mechanically from the change set, not from a hand-written list:
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack(exit 0), which reported the tree and commit it derived against and resolved 1 changed path to 39 runnable commands.All 39 were run, each exit code captured before any pipe (
cmd > log 2>&1; EXIT=$?):check:doc-formula-expressions,check:doc-security-posture,check:docs-transcript-driftat exit 3;check:docsandcheck:skill-examplesat exit 1 with their own "nothing was measured" text). Each names an unbuilt@objectstack/lint/@objectstack/spec. Re-run after building those closures — results in a comment below.Local scope readings, per the change set:
content/docs/automation/approvals.mdx, which sits under no package directory — the nearestpackage.jsonabove it is the private repo root.pnpm lintis not narrowed, it is inapplicable..mdxis outside eslint's covered population:eslint.config.mjsnames the extension zero times, and everyfiles:entry is TS/JS only. Measured directly —pnpm exec eslint --no-inline-config --format json content/docs/automation/approvals.mdxexits 0 over 1 reported file with 0 errors and the single warningFile ignored because no matching configuration was supplied.grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]'over the edited file returns no hits, beside a greencheck:nul-bytes.Firing controls, read mechanically from the probed file itself rather than from a diff or any report's prose (a single-line
grepwas not used to establish absence — the scan is whole-file and wrap-tolerant):refused at authoring timenow occurs 0 times in the page.authoring timestill occurs at:25,:27and:102, plus once inside the new sentence.Changeset
skip-changeset. The criterion is whether anything published moves, and it was measured rather than assumed: no non-private package declares anycontent/docspath in itsfiles[], and the edited file lies under no package directory at all. Positive control on the same scan — 70 of 70 non-private packages declare adistpath infiles[]— so the scan does readfiles[].apps/docs, which renders this tree, isprivate: true.Acceptance notes
Noted, not filed — neither is a defect, and neither has a carrier:
:27("Nothing can flag that at authoring time") is a third correct usage the card did not count. This strengthens the card's argument rather than weakening it, and:27needs no change.dispatch-gatesreports 14 families that "apply once this card's changeset exists". Withskip-changesetmeasured above, no changeset path is created, so those 14 stay out of scope here rather than being skipped.Generated by Claude Code
Generated by Claude Code