Skip to content

spec(automation): an approval step's excludeSubmitter option, default off — the protocol half of #25573 (ruling 6377731715, letter B) #25609

Closed

Description

@objectstack-fleet

Carries the protocol half of #25573 (the approval-step key); #25573 keeps the runtime half (plugin-approvals honours the key) and is Blocked-by: this card.

Filing gate ④: the per-layer child of a cross-layer card. The ruling of record fixes the order. The director's state comment 6377859800 on #25446 (the card #25573 re-files), verbatim: "Protocol addition: spec first, then plugin-approvals. Level p1. Workload S–M." The ruling is 6377731715 on #25496 (batch #388 item 4, the maintainer's 「同意」 on the seat's letters). Filed by domain:server seat 2 (seat post #20966) · sunhaolin · session session_01JVTd3PprStGa26aez5omLB. ⛔ Not a claim. Domain and grade are triage's.

Business reason (the parent's): an app with a two-level approval (the category manager on the record, then the procurement director) must never let a submitter approve their own request. Procurement control frameworks treat self-approval as a finding (SOX-style segregation of duties; T/CFLP 0016). Today every app builds this by hand, per flow, as an expression approver or a validation rule. That is easy to forget.

Reader who acts: the domain:spec seat, one PR. Then #25573's domain:server dispatch builds the runtime half on the merged key.

The ruled shape (letter B), verbatim from 6377859800

"a step-level option excludeSubmitter: true that removes the submitter from the resolved approver set of a field or position (or any) approver step. An emptied set takes the node's existing onEmptyApprovers fallback (packages/spec/src/automation/approval.zod.ts:107) and otherwise ADR-0042 escalation — never a silent pass. The approval record shows when the exclusion changed the route. Off (the default), behaviour is unchanged; excludeSubmitter has 0 hits on origin/main today. Not taken: C (default on — it would silently reroute every existing approval)."

Measured on origin/main 8801f2bb3

  • git grep excludeSubmitter: 0 hits in the repository.
  • packages/spec/src/automation/approval.zod.ts: the step's empty-set text names the node-level onEmptyApprovers policy (the describe() strings near :104–:107).

This card

  • The key on the approval step schema, default off, with describe() text that says what it removes and where an emptied set goes.
  • Whatever the spec lane's process owes a new authorable key (liveness ledger row, generated artefacts, docs), by the spec lane's own rules.
  • Expected declarations: Clause-②: yes (widening) and Metadata-protocol: added. The spec seat confirms them.

Not in this card

Dedupe

The open and closed issues created since 2026-10-23T20:00Z, matched on excludeSubmitter|submitter|25573|25446: only #25573 itself and the ruling card #25496. excludeSubmitter has 0 hits in the tree. New.


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 24, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: first grade — enhancement · priority:p1 · domain:spec · area:workflow · pm:queue · triage seat · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-24T09:57Z

    ⛔ Not a claim, ⛔ not a dispatch.


    Generated by Claude Code

  2. objectstack-fleet commented on Oct 24, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1
    Session: session_01PC4NPXPgPmpDRG9DdDnyXT
    Account: sunhaolin (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/issue-25609-approval-exclude-submitter
    Worktree: objectstack-issue-25609
    Domain: domain:spec
    Seat: domain:spec#2 (seat post #18549)
    File surface: packages/spec/src/automation/approval.zod.ts (the approval step gains excludeSubmitter, default off, with describe text naming what it removes and where an emptied set goes: the node's onEmptyApprovers, else ADR-0042 escalation) and its tests; the liveness ledger row for the new key (packages/spec/liveness/flow.json, planned, carrier #25573) and whatever else the spec lane owes a new authorable key; regenerated automation shards and references; one changeset .changeset/25609-approval-exclude-submitter.md. No runtime change: that half is #25573 (domain:server, pm:blocked on this card). (stop on breach; explain in the report)
    Container & model: M, mode:subagent (a packages/spec-only surface; the build stays inside packages/spec), model: default (TIER_DEFAULT; dispatch-gates --tier for this surface: no path-derived mandate), effort: xhigh (p1). Clause-②: yes ⇒ a CONTRACT_REVIEW_TIER contract review is owed before the queue.
    Clause-②: yes (widening)
    Metadata-protocol: added
    Responsibility: n/a — not a defect card
    Ruling-ref: 6377731715 (on #25496, batch #388 item 4, letter B for #25446), which agrees on this item with the later 6377842908 (reconciliation 6377965040); the director's state comment 6377859800 on #25446 quotes the shape
    Thread-read: 6382817354
    Serial constraints cleared: none — no open PR touches automation/approval.zod.ts or packages/spec/liveness/ (all open PRs' file lists read 2026-10-24T10:36Z); no open pm:dispatched claim declares them. PR #25600 (#25598, domain:server) edits plugin-approvals runtime only. This seat's in-flight cards are disjoint (#24814 ui/*, #25580 automation/flow.zod.ts, #25574 the form publication, PR #25617 stack.zod.ts, PR #25620 system/*).
    Premise: holds on origin/main 7265b33e77 — excludeSubmitter has 0 hits in packages/ and examples/; approval.zod.ts names the node-level onEmptyApprovers fallback at about :104–:107.


    Generated by Claude Code

  3. objectstack-fleet commented on Oct 24, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 25609,
      "status": "done",
      "branch": "claude/issue-25609-approval-exclude-submitter",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/25632",
      "session": "session_01PC4NPXPgPmpDRG9DdDnyXT",
      "premise_still_valid": true,
      "summary": "ApprovalNodeConfigSchema gains excludeSubmitter: z.boolean().optional(). It sits on the node config because ADR-0019 made each approval step a node, and it applies to the whole slate `approvers` resolves to, every approver type. It is absent when off, so existing nodes and each request's node_config_json snapshot parse unchanged. The describe opens with [PLANNED — not yet enforced], defines the submitter as the run's trigger user recorded as submitter_id, sends an emptied slate to the node's onEmptyApprovers route ('never passes silently'), and tells the author the engine does not read the key yet; it is tracker-free per check:doc-authoring Rule 3. The PM's ledger-row assumption is DISPROVED by measurement: a planted flow.json row under nodes.config makes check:liveness exit 1 with 'flow/nodes.config (declared children but property is not a container)', because the walk ends at a z.record(z.string(), z.unknown()). So, following the page.json precedent, the planned record is a PLANNED RECORD sentence in flow.json _note naming carrier #25573, plus the describe marker; the checkable record is the authorable-surface row. The runtime (#25573) is not touched.",
      "tests": "At head 4823c57936. `pnpm --filter @objectstack/spec test`: Test Files 715 passed (715), Tests 21703 passed | 1 todo (21704), lock VERDICT command-exit 0. `pnpm --filter @objectstack/spec typecheck`: exit 0 (check:test-typecheck OK, which compiles the new test file). New file packages/spec/src/automation/approval-exclude-submitter-25609.test.ts: 16/16. Ablation 1 via scripts/ablation-replace.mjs: renamed the key to excludeSubmitterAblated (anchor 1->0, blob 27c795830a90->50ecb7c5d102) => Tests 10 failed | 6 passed; every key-dependent test red, the §2 control, the four §3 refusals and 'omitted stays absent' green; restored, blob == HEAD, git diff HEAD empty. The first run of ablation 1 errored the file at collection because §5 read the description at describe time; that read was moved inside it() (c6f43c7960) and the ablation re-run. Ablation 2: .optional() -> .default(false) => Tests 2 failed | 14 passed ('omitted stays absent', 'no default'); restored the same way. No dist leg: the test imports ./approval.zod from src.",
      "gates": "Local subset at head 4823c57936 (after one origin/main merge, 481ac62116, via os-regen-merge.sh, and a spec rebuild). dispatch-gates --commands --repo objectstack-ai/objectstack derived 114 commands; each ran with its exit code captured before any pipe. 108 exited 0 on the first pass. 6 first exited 3, PREREQUISITE NOT MET (no lint/formula/client-react/objectql dist in a fresh worktree): check:doc-formula-expressions, check:doc-security-posture, spec check:skill-examples, check:docs-transcript-drift, check:dual-build-cjs-loads, check:lean-entry-closure. After `turbo run build --filter=@objectstack/lint... --filter=@objectstack/client-react... --filter=@objectstack/objectql...` (37 tasks, all cache hits; spec dist verified to carry the key) all 6 re-ran to exit 0 with measured verdicts. --ran reconciliation: '✓ dispatch-gates --ran: 114 derived famil(ies) accounted for — 114 run, 0 NOT-MEASURED (a DERIVED zero — all 114 recorded an exit code and none of them is 3)'. Also: spec check:generated '✓ All 14 generated artifacts are up to date' (needed gen:docs once); check:liveness exit 0; local-only check:dev-prereqs exit 0. CI check-runs on 4823c57936 at report time: completed success = Check Documentation Links, Check PR Size, No other open PR may claim the same issue, No other open PR may claim the same single-writer path, Part-of PR must not also close its card, The card this PR closes must claim this branch, filter. in_progress = Build Core, Test Core (1/6..6/6), Type Check · workspace/source gates/consumer gates/debt ledger, Lint · ESLint/repo & tooling gates/docs & code guards/CI, release & engine gates/PM dispatch-gates/scoped ×3, Governed Surface Queue Guard, Spec property liveness, Check Changeset, Build Docs, Dogfood Regression Gate, Auto Label, Flag docs affected by code changes. Not waited on.",
      "dogfood_battery": "n/a — no `!` in the PR title and Clause-② is (widening), no (narrowing) arm",
      "mcp_calls": "0 — no MCP GitHub tool was called",
      "api_writes": "2 relay strokes (each one POST /repos/objectstack-ai/objectstack/dispatches executed by fleet-write as objectstack-fleet[bot]): (1) pr_create => POST /repos/objectstack-ai/objectstack/pulls (draft) + POST /repos/objectstack-ai/objectstack/issues/25632/assignees [sunhaolin], read back byte-identical; (2) this os-dev-report comment => POST /repos/objectstack-ai/objectstack/issues/25609/comments. Zero label writes: the dispatch named no label, and skip-changeset does not apply (a changeset is present). git push is not a REST write.",
      "deviations": [
        "The PM's mechanism assumption 'ledger row in flow.json, planned, authorHint' was measured false (check:liveness exit 1 on a planted row; restored and hash-verified). Shipped instead: a flow.json _note PLANNED RECORD plus the describe marker, per the page.json precedent. No authorWarn can reach this key's authors, because the liveness lint reads no deeper than a container's direct children.",
        "My first commit was written with the harness reminder's model-named Co-Authored-By trailer. I amended it to the AGENTS.md model-free pair BEFORE any push. No pushed commit carries a model identifier.",
        "The PR body footer uses the AGENTS.md session-URL footer form, not the harness reminder's emoji 'Generated with' line, per the role file's precedence clause.",
        "I ran gen:docs directly instead of `check:generated --fix`, because the spec dist was fresh from the preceding build and --fix rebuilds first. check:generated was then green.",
        "test:repo was not run locally (CI's Test Core runs it); the diff touches no repo-project test.",
        "No reverse-verification for a cross-package type change: the change only widens an optional key, and no consumer maps ApprovalNodeConfig keys exhaustively (git grep)."
      ],
      "files_changed": [
        ".changeset/25609-approval-exclude-submitter.md",
        "content/docs/references/automation/approval.mdx",
        "packages/spec/authorable-surface/automation.json",
        "packages/spec/liveness/flow.json",
        "packages/spec/src/automation/approval-exclude-submitter-25609.test.ts",
        "packages/spec/src/automation/approval.zod.ts"
      ],
      "open_questions": [
        {
          "question": "excludeSubmitter: true beside onEmptyApprovers: 'auto_approve'. The ruling sends an emptied set to the node's onEmptyApprovers route AND says 'never a silent pass', but auto_approve is the silent pass (its describe: 'silently waves the record through'). A submitter who is the only resolved approver would be auto-approved. Which shape resolves it? (For #25573 or the maintainer; no spec change was made here, and the describe states only the ruled invariant.)",
          "options": [
            "A — the spec refuses excludeSubmitter: true with onEmptyApprovers: 'auto_approve' at parse, a refinement in the checkFallbackApproversPairing style. Loud, contract-first, a small spec PR. It is a narrowing once a release carries the key, so it is cheapest before then.",
            "B — the runtime (#25573) routes an exclusion-emptied slate to escalation (admin_rescue or ADR-0042) instead of auto-approving. No spec change, but a declared policy is then partly overridden at runtime, the 'quietly ignores half its config' shape this module's strictness comments warn against.",
            "C — accept auto_approve as authored: the route note on the approval record counts as 'not silent'. Lowest cost, but it reads against the ruling's segregation-of-duties intent."
          ],
          "recommendation": "A, because it makes the ruled 'never a silent pass' structurally true at the contract. It costs least before the key ships in a release, and it keeps the runtime free of a policy override. Route it to the maintainer as a scoped follow-up to ruling 6377731715 if the seat judges it beyond that ruling's letter."
        },
        {
          "question": "Does the exclusion also screen the fallbackApprovers slate and the admin_rescue administrators when the submitter is among them? The ruling names only 'the resolved approver set'; the describe says 'the slate approvers resolves to'.",
          "options": [
            "A — primary slate only (as the describe says today).",
            "B — every slate the node opens on (primary, fallback, admin rescue), with the record noting each exclusion."
          ],
          "recommendation": "B for segregation of duties. It is a runtime decision for #25573, which widens the describe when it lands."
        }
      ],
      "out_of_scope_findings": [
        "carrier: none · noted, not filed — the liveness walk cannot reach ANY approval node config key (flow/nodes.config is z.record(z.string(), z.unknown())), so onEmptyApprovers, fallbackApprovers and the rest carry no ledger row either. Coverage observation, recorded in PR #25632's Measured section.",
        "carrier: none · noted, not filed — packages/spec/src/ui/component-record-blocks-empty-state-24266.test.ts's header says check-liveness MARKER_RE reads its [PLANNED] marker. By the same walk-boundary measurement (page/regions is a recorded undrilled container) no gate reads it. A comment inaccuracy only."
      ]
    }

    Generated by Claude Code

  4. objectstack-fleet commented on Oct 24, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed · domain:spec seat 2 (seat post #18549) · sunhaolin · session session_01PC4NPXPgPmpDRG9DdDnyXT · 2026-10-24T13:04Z

    reading value
    PR #25632 MERGED through the queue; merge commit 49653f9181, committed 2026-10-24T12:16:40Z. ACCEPT 6383784354; contract review PASS 6383996601 on head 4823c57936
    queue branch gh-readonly-queue/main/pr-25632-* gone (git ls-remote: 0 refs)
    origin/main carries 49653f9181; packages/spec/src/automation/approval.zod.ts holds excludeSubmitter (1 hit)

    State: the card closed completed on the merge; pm:dispatched is removed in this stroke. #25573 (the runtime half) has its spec prerequisite; its Blocked-by: #25635 still holds. The key is unreleased. The next release carries it, and #25635's letter A is a free edit only before that release (contract review 6383996601, ③ item 1).


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:workflowApprovals and automation — the work that runs without a person driving itdomain:specenhancementNew feature or requestpriority:p1High: required for production / M2

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions