…draft
showcase_approver_bindings is a designer specimen whose approval nodes
resolve to nobody on a fresh org. It shipped as status 'draft' on the
belief that draft keeps a flow out of the runtime, but the engine binds
and fires draft flows (only obsolete / invalid disable), so every
showcase_field_zoo update started a run that parked an admin_rescue
approval request on the org administrators.
Declare the platform's "loaded, not armed" status, obsolete: the engine
still registers and lists the flow (the designer opens it; Studio's
switch reads it as Off) but hands it to no trigger. The docblock now
states that mechanism, and a real-engine pin with a draft positive
control asserts the specimen is held but never bound.
Claude-Session: https://claude.ai/code/session_018VqJPGA1NDNSCZsnecsChP
Co-authored-by: Claude <noreply@anthropic.com>
Fixes #23965
Clause-②: no
What changed
showcase_approver_bindingsis the designer specimen for the record-backed approver kinds. Its approval nodes resolve to nobody on a fresh org. It shipped asstatus: 'draft'on the belief that draft "keeps it out of the runtime". That belief is false: the engine disables a flow only onobsolete/invalid, and it binds a draft flow exactly like anactiveone. So everyshowcase_field_zooupdate started a run, and each run parked a pendingadmin_rescueapproval request on the org administrators.examples/app-showcase/src/automation/flows/approver-bindings.flow.ts:status: 'obsolete', which is the platform's declared "loaded, not armed" status. The docblock's## Why draftsection is now## Why obsoleteand states the real mechanism: draft fires, and obsolete loads, lists and opens but is never handed to a trigger. The author-visibledescriptiondrops "Draft on purpose" in the same way.examples/app-showcase/src/automation/flows/index.ts:2012-2014: the registration comment said "Draft on purpose — see the module docstring". It now says "Obsolete on purpose (loaded, never armed)". This is a bounded in-place fix outside the claim's declared file surface; see Acceptance notes.examples/app-showcase/test/approver-bindings-specimen-unarmed.test.ts(new): a real-engine pin with adraftpositive control.No
draftsemantics changed: nothing underpackages/**is touched, and neither is the checklist ledger.The deciding read: does the designer still open an
obsoleteflow? Yes.Measured at objectstack
074b33be1band objectuif8267df93a(the.objectui-shapin):packages/services/service-automation/src/engine.ts):registerFlowstores the flow (:4695this.flows.set(name, parsed)) before it reads status.:4718then disables onlyobsolete/invalid.listFlows/getFlow(:5040-5046) answer from that map whatever the status. The one arming gate isactivateFlowTrigger:3932(if (!this.isFlowEnabled(flowName)) return;), andexecuterefuses a disabled flow withFLOW_DISABLED. The boot line is atplugin.ts:1627, and the lint rule names the same semantics atvalidate-flow-trigger-readiness.ts:129and:830.packages/metadata/src,packages/metadata-protocol/src,packages/rest/srcandpackages/objectql/srccarry noobsoletefilter (git grepreturns zero hits). The only runtime hits are prose inpackages/runtime/src/domains/automation.tsandflow-clone.ts.StudioDesignSurface.tsx:7008-7024creates every new flow withstatus: 'obsolete'("born switched OFF", objectui#11779). Opening an obsolete flow is therefore the designer's own default path.:7146-7165renders the on/off switch with obsolete as Off. The rail listsclient.list(type, { packageId })(packageSurfaces.ts:71) with no status filter.Evidence
Pin + ablation.
pnpm --filter @objectstack/example-showcase exec vitest run --maxWorkers=2 test/approver-bindings-specimen-unarmed.test.tsgives 4/4 passed. The pin registers the shipped definition on a realAutomationEnginewith a recordingrecord_changetrigger, and asserts four things:getFlowRuntimeStates()readsenabled: false, bound: false, status: 'obsolete';executeanswerssuccess: false, code: 'FLOW_DISABLED'and records no run.The positive control registers the same definition under
draftand must see it bound toshowcase_field_zoo/record-after-update.Ablation, one run:
node scripts/ablation-replace.mjsputstatus: 'draft'back (anchor x1 to x0, blobe4ef4d4723to20712cc37a) and the result was 2 failed / 2 passed. The specimen showed up in the trigger's bindings, andexecutedid not answerFLOW_DISABLED. The control and the membership cases stayed green. Restore: blob after restoree4ef4d4723== HEAD, andgit diff HEADwas empty. The specimen module is imported by relative path (source), so nodist/sits between the mutation and the run.Live, public HTTP door. A fresh boot:
pnpm dev -- --fresh -p 47391 --log-level info, at this branch after a fullpnpm build.[Automation] 8 flow(s) have status 'draft' (...). The card measured 9, and the specimen is no longer in that list.Flow registered: showcase_approver_bindings (version 1)is still printed. No[record-change] bound flowline namesshowcase_field_zoo, while the same log carries that line for other flows.GET /api/v1/meta/flow/showcase_approver_bindingsreturns 200 withstatus: obsoleteand 9 nodes.GET /api/v1/meta/flowreturns 32 items, and the specimen is listed.GET /api/v1/automation/_statusreturns the rowenabled: false, bound: false, status: obsolete, triggerType: record_change, object: showcase_field_zoo.PATCH /api/v1/data/showcase_field_zoo/IDwith{"f_email":"qa23965@example.com"}on "Specimen — Full" returns 200. More than 3 s later:GET /api/v1/automation/showcase_approver_bindings/runsreturnsruns: [];sys_automation_runfiltered on the flow returnstotal: 0;sys_approval_requestfiltered onprocess_name: flow:showcase_approver_bindingsreturnstotal: 0;admin_rescuelines.priority: urgentgivessys_automation_runtotal 1,showcase_urgent_task_alertcompleted.Tests and gates (head
074b33be1b)turbo run build --filter='@objectstack/example-showcase^...' --concurrency=2passed 62/62. A fullpnpm buildpassed 75/75; the dev boot's prereq check needed it.pnpm --filter @objectstack/example-showcase exec vitest run --maxWorkers=2: 33 files / 415 tests passed.pnpm --filter @objectstack/example-showcase typecheck(tsc --noEmit, which includestest/**/*) exited 0.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 42 commands. All 42 ran and each exited 0 (exit captured before any pipe).--ranreports: 42 derived, 42 run, 0 NOT-MEASURED, 0 UNRUN.test:repois not touched and is left to CI (Test Core).Acceptance notes
Surface supplement, a bounded in-place fix. The
index.ts:2012-2014comment repeated the "Draft on purpose" premise and would have become false with this change. Why the in-place fix applies:examples/app-showcase/src/automation/(per the claim);The claim's file surface named only the flow file, the pin and a changeset.
No changeset.
examples/app-showcaseis"private": trueand publishes nothing, so this PR carriesskip-changeset.Checklist premise, the ledger owner's, not in this diff. Three lines still describe the specimen as draft:
docs/qa/platform-checklist/areas/automation.json:985("showcase_approver_bindings is draft on purpose");docs/qa/platform-checklist/areas/approvals.json:1224("approver-bindings.flow.ts is status:'draft' with EMPTY values, deliberately");docs/qa/platform-checklist/areas/approvals.json:1290("the 审批节点「处理人 Value」应改为记录 lookup(现查 metadata 端点→只能手填);附 approver value 语义核实 + queue 未实现 #3508 DESIGNER specimen — draft, empty values").The conclusions those notes draw still hold, now for the declared reason: no armed record-change flow on Field Zoo, and runtime legs need scratch flows. Only the status word goes stale. The card routes these to the ledger owner. Once this PR closes the card, there is no other carrier.
The specimen's
labelstill readsApprover Binding Specimens (#3508), an author-visible string carrying a tracker number. That predates this PR and is left as it is (naming).Generated by Claude Code