Uh oh!
There was an error while loading. Please reload this page.
docs(spec): state minApprovals' real per-behavior default in the schema prose - #14543
Conversation
`ApprovalNodeConfigSchema.minApprovals` described itself as "Default 1", but an omitted threshold has never meant 1 under `quorum`: the runtime falls back to the resolvable approver count, so a quorum node authored without the key requires EVERY approver, not one. Under `per_group` the fallback really is 1 per group. Converge the declared text onto the enforced behaviour — the prose is the half that drifted, so only the prose moves; no schema default is added and no runtime threshold changes. A pin test asserts the description names both behaviours' defaults and that the schema still injects no default, so the two readings cannot drift apart silently again. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GDA48PuRFrHyRfdkBz8m21
…eset Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GDA48PuRFrHyRfdkBz8m21
…n-approvals-default-describe
…ate registry
Making the omitted-threshold contract explicit put a permissive-shaped
sentence in front of the empty-state scanner ("Omitted ⇒ all resolvable
approvers …"). The gate is right to demand a decision, and the decision is
`closed`: an omitted threshold lands on the STRICTEST reading — every
resolvable approver under `quorum` — so careless authoring lands on least
privilege, not on the widest grant. Registered with the runtime enforcement
site as evidence rather than reworded around the scanner.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GDA48PuRFrHyRfdkBz8m21📓 Docs Drift Check1 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 128 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 12dd5daba633b922291ec48a35871548a02b3e54 && git checkout 12dd5daba633b922291ec48a35871548a02b3e54
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 9acddde94c5242db560913aa2d2c2e8f7b843511 f1aceed03d2fb789fcb7d753f3ce18913571cbbc && git checkout -B drift-repro 9acddde94c5242db560913aa2d2c2e8f7b843511 && git merge --no-ff f1aceed03d2fb789fcb7d753f3ce18913571cbbc
node scripts/docs-audit/affected-docs.mjs --json 9acddde94c5242db560913aa2d2c2e8f7b843511 |
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 33624604584 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 失败的 job(日志抽取,best effort):
跨 PR 相同签名(24h,按失败测试文件聚合):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
Queue build 33624604584 red — not this PR's; no change needed here. ( That build ran on the old speculative stack ( #14536 was ejected; the queue has rebuilt this PR on the post-ejection stack (queue head Generated by Claude Code |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#13809
ApprovalNodeConfigSchema.minApprovalsdescribed itself as "Default 1". Theapproval runtime has never read it that way: an omitted threshold falls back to
the resolvable approver count under
quorum, so a quorum node authored withoutthe key requires every approver, not one. Under
per_groupthe fallbackreally is 1 per group. This converges the declared text onto the enforced
behaviour — the prose is the half that drifted, and only the prose moves.
Premise, re-checked on
origin/mainpackages/spec/src/automation/approval.zod.ts:714(JSDoc) and:718(.describe(...)), at merge baseecbb6fd9bpackages/plugins/plugin-approvals/src/approval-service.ts:2395Math.min(Math.max(1, config.minApprovals ?? n), n)underquorum—nis the resolved approver count, so omitted means ALLpackages/plugins/plugin-approvals/src/approval-service.ts:2400Math.max(1, config.minApprovals ?? 1)underper_group— omitted means 1 per groupThat runtime file's own doc comment already said it, at
:2378: "quorum— atleast
minApprovalsdistinct approvals (default = all)". The?? nisdeliberate and is not touched here — relaxing it (a schema
.default(1), ora runtime change) would lower the approval threshold of every stored quorum flow
and is a maintainer decision, not this PR's.
The new text
The JSDoc above the property carries the same statement in long form. No tracker
ids in either (doc-authoring rule 3).
Every copy of the old string, regenerated
packages/spec/src/automation/approval.zod.ts— the.describe()and theJSDoc above it (hand-edited; this is the source).
content/docs/references/automation/approval.mdx:72— theApprovalNodeConfigproperties table, regenerated with
pnpm --filter @objectstack/spec gen:docs,never by hand. One cell changed.
authorable-surface/,authorable-surface.base.jsonand
json-schema.manifest/are key-level and hold no describe prose;check:generatedreports all 15 artifacts current on the final head.skills/objectstack-automation/SKILL.mdalready states the per-behaviourdefault in its own words and is hand-written, so it needed no change — this PR
touches no governed surface.
Pin test
packages/spec/src/automation/approval.test.tsgains one case asserting (a) theschema still injects no default — omitting the key leaves
minApprovalsundefinedunder both behaviours — and (b) the description names bothbehaviours' defaults and no longer claims "Default 1". Reverse-verified: with the
old string restored on disk (mutation confirmed by marker counts and a changed
blob hash), the case fails on exactly that claim, 1 failed / 44 passed; restoring
reproduces the HEAD blob byte-for-byte.
One extra file, named on purpose
Stating the omitted-threshold contract out loud put a permissive-shaped sentence
in front of
check:empty-state, which reds on any unclassified "omitted = all".The gate is right to demand a decision, so the decision is recorded rather than
worded around:
packages/spec/scripts/liveness/empty-state-registry.mtsgains aminApprovalsentry classifiedclosed— the omitted threshold lands on thestrictest reading (every resolvable approver), so careless authoring lands on
least privilege — citing the runtime enforcement site as evidence. No behaviour,
no accept set and no runtime file changes with it.
Clause-②: no — describe-only, accept set and behaviour unchanged.
Sibling work: the
timeoutHoursdescribe on this same file (issue #13801, phase2) is untouched, and
origin/mainhad moved neitherapproval.zod.tsnor anyregenerated product this branch writes at the last pre-push fetch.
Verification union — run on head
f1aceed03Derived mechanically with
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack(no paths; it reads the change set itself), plus thedispatch's must-haves. Exit codes captured before any pipe.
69 of 75 green · 5 NOT MEASURED · 1 declared narrowing. The 75 are the 72
commands the deriver printed for this change set plus three must-haves it does
not carry (
check:nul-bytes,pnpm --filter @objectstack/spec typecheck, thespec suite).
Named results:
check:generated— all 15 artifacts current, includingcheck:docs,check:authorable-surface,check:api-surface,check:strictness-ledger,check:livenessandcheck:test-typecheck.check:doc-authoring·check:nul-bytes·check:empty-state·check:role-word·check:type-check-coverage·check:cross-package-test-inputs·check:pm-governed-merges— green.pnpm --filter @objectstack/spec typecheck(tsc + scripts tsconfig + testtsconfig) — green.
approval.test.ts45 passed, and with the liveness gatesuites that cover the registry module this PR edits
(
empty-state.test.ts,evidence.test.ts,check-liveness.test.ts)163 passed across 4 files.
Declared narrowing: the whole
@objectstack/specvitest suite is not inthis union. Two attempts acquired the shared verify lock and were killed by the
container's ~10-minute foreground ceiling mid-run, under heavy contention from
sibling agents; the run boots a dev server and does not fit the remaining window.
Narrowed to the four files above — the one this PR changes plus every test that
loads the module it adds a row to. CI runs the full suite on this head.
NOT MEASURED (never read as green, never as red) — every one is a build
precondition of this worktree, which builds only the
@objectstack/specclosurewhile CI checks out fresh and builds the workspace:
check:dev-prereqs,check:dual-build-cjs-loads(exit 3),check:type-check-debt(exit 3 — re-measure needs 51 built dependency closures),check:skill-examples(needsclient-react.d.ts, a 34-package closure) andcheck:test-completeness(exit 3 — it grades a savedturbo run testlog onlyCI produces).
Which tree each command measured. All 75, on the final head
f1aceed03,with the working tree clean. Commands first run on an earlier commit — the
descriptive scans, and the spec audits that had been blinded by a stale
packages/spec/dist— were re-run there after the last commit, so no line aboveis a reading about a tree nobody is on.
🤖 Generated with Claude Code
https://claude.ai/code/session_01GDA48PuRFrHyRfdkBz8m21
Generated by Claude Code