Repository navigation
feat(spec)!: refuse an SLA escalateTo that no approver acts under, at registration - #26154
Conversation
ApprovalEscalationSchema.escalateTo refuses a team:, department:, org_membership_level: or role: address by shape, under every action, with guidance naming what to write. A user id, an email, a position name and position:<name> stay accepted. D3 entry approval-escalation-dead-target-refused and a minor changeset. Claude-Session: https://claude.ai/code/session_01RqGZT3pZz4qUuw2Z5TMGSy Co-authored-by: Claude <noreply@anthropic.com>
…nements ledger, regenerate docs Claude-Session: https://claude.ai/code/session_01RqGZT3pZz4qUuw2Z5TMGSy Co-authored-by: Claude <noreply@anthropic.com>
…new sites Claude-Session: https://claude.ai/code/session_01RqGZT3pZz4qUuw2Z5TMGSy Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift Check4 anchor(s) derived from 1 changed package(s); no hand-written page names any of them. What this run could not see
Coarse fallback — 145 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 52420d612f84575ada576a4aff4200b7a43c7706 && git checkout 52420d612f84575ada576a4aff4200b7a43c7706
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin a335f1defbea57ffe552d2e9efc5ad8d56a2e76a dc70de6236ed9275e90fde37961a7880937c4b0c && git checkout -B drift-repro a335f1defbea57ffe552d2e9efc5ad8d56a2e76a && git merge --no-ff dc70de6236ed9275e90fde37961a7880937c4b0c
node scripts/docs-audit/affected-docs.mjs --json a335f1defbea57ffe552d2e9efc5ad8d56a2e76a |
Contract reviewServed-tier: Inputs: card #25598 body and all 9 comments (claim 6415741797, os-dev-report 6417943862, seat answer 6418683750 included), PR #26154 body, file list and net diff against ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS |
Seat ACCEPT — PR #26154 at
|
Requested by Steedos · project thread
Fixes #25598
Clause-②: no (narrowing)
Metadata-protocol: narrowed
The registration-time leg of the card, and its last. The runtime leg landed in PR #25600 (
2351c8d079): on timeout, areassignwhose target resolves to no one keeps the slate and notifies it. This PR refuses, at registration, the targets whose shape alone proves they name no one.What changes
ApprovalEscalationSchema.escalateTo(packages/spec/src/automation/approval.zod.ts) was a barez.string(). It now refuses a value starting withteam:,department:,org_membership_level:orrole:(after trimming, as the runtime trims it), under every escalation action. The refusal is acustomissue atescalateTothat names the value, says why no one could take it, gives a per-prefix remedy, and lists whatescalateToaccepts.Measured on
main0a694821before the edit:approver-address.ts(plugin-approvals): a person acts on a slot only under their user id, their email, orposition:NAMEfor a position they hold (actingAddresses,POSITION_ADDRESS_PREFIXES = ['position:']). No approver acts under ateam:,department:,org_membership_level:orrole:literal.escalateRequest(approval-service.ts) readsescalateTounderreassign(new slate) and undernotify(notice audience), so a dead literal names no one under either action. Underauto_approve/auto_rejectit is unread. The refusal therefore does not depend onaction.What stays accepted: a user id, an email, a bare position machine name and
position:NAME. Whether they resolve is directory state, and the runtime leg already judges it on timeout.The refusal reaches every door that judges an approval node's config, by #21848's route:
FlowSchema.parse(defineStack,defineFlow,objectstack validate/compile, the metadata save door) and the build judgeflowNodeConfigRefusals, which reportsnode-config-refused-by-contractatescalation.escalateTo.The narrowing record (AGENTS.md Post-Task Checklist step 3)
.changeset/25598-escalation-dead-target-refused.md:@objectstack/specminor (Metadata-protocol: narrowed), BREAKING banner, the Before / Now / Fix migration, and the ADR-0087 markerregistered approval-escalation-dead-target-refused.packages/spec/src/migrations/entries/semantic/18.approval-escalation-dead-target-refused.ts, modelled onflow-approval-node-config-contract-refused. There is no D2 conversion: which position or person a team, department or membership tier stood for is the author's call, and no value the platform could write would keep what the flow meant..superRefine()does not project into JSON Schema, soautomation/ApprovalEscalationgains the siteescalateTo, andautomation/ApprovalNodeConfiggainsescalation.escalateTo. The header totals moved with them (245 schemas, 840 sites). Projecting a prefix ban would need a new arm inrefinement-projection.ts, which is a public-contract decision, so it is not done here.content/docs/references/automation/approval.mdxregenerated for the newescalateTodescription.Tests
New
packages/spec/src/automation/approval-escalation-dead-target.test.ts, 26 cases:escalateTounder each of the 4 actions, and again when padded. The message names the value andposition:NAME.usr_7f3a,vp@example.com,vp_operationsandposition:vp_operationsparse with the whole schema green and the value carried through. An omitted target stays absent.FlowSchema.safeParserefuses each prefix atnodes.1.config.escalation.escalateTo.flowNodeConfigRefusals('approval', …)returns exactly onenode-config-refused-by-contractatescalation.escalateTo. The four accepted shapes register.Ablation at
dc70de62, throughscripts/ablation-replace.mjs:.superRefine(checkEscalationTargetShape)removed (anchor 1 → 0, blob585bcb51→039f25ea). Result: 17 failed and 9 passed. Every refusal pin went red and every acceptance pin stayed green, as expected. Restored blob equals HEAD, andgit diff HEADis empty.Verification (head
dc70de62)pnpm --filter @objectstack/spec test: 724 files. The first run had one red,scripts/dropped-refinements.test.ts, because the ledger header still read 244. The totals were moved (dc70de62). On re-run, that file, the new file andsrc/migrationspassed: 7 files, 284 tests.pnpm --filter @objectstack/spec typecheck: pass.pnpm --filter @objectstack/spec buildandcheck:generated: all 14 artifacts current aftergen:docs.(narrowing)arm), underos-verify-lock, atdc70de62: VERDICT command-exit 0. Test Files 296 passed, 3 skipped (299). Tests 2523 passed, 18 skipped (2541).dispatch-gates --commands: 114 derived families run atdc70de62, each exit 0 (exit codes captured before any pipe).--ranreconciliation: 114 run, 0 NOT-MEASURED (a derived zero). On the first sweep, seven gates refused with exit 3, PREREQUISITE NOT MET (no build output, or the teardown fixture missing from a shallow clone). Their prerequisites were built, or the fixture fetched, and all seven then exited 0. Every family CI runs is read from this PR's check-runs.Acceptance notes
queue:is the one othertype:valuefallback literal the approvals service can write, from the deprecatedqueueapprover type, and nobody acts under it either. It is outside the card's four and is not refused here. It is reported to the seat as an open question.skills/objectstack-automation/references/state-machines-and-approvals.mddescribesescalateToas "a position machine name … or a specific user id", which is still true. It is not edited, becauseskills/**is a governed surface.Clause-② basis: nothing widens. The
(narrowing)arm applies becauseescalateTorefuses values it used to accept. The claim's ownClause-②:line carries no arm, and the report names that difference.Generated by Claude Code