Skip to content

fix(runtime): declared type: 'flow' endpoints answer the #9378 flow-dispatch status table, from the one shared definition - #9676

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-9462-declared-endpoints-status-table
Aug 18, 2026
Merged

fix(runtime): declared type: 'flow' endpoints answer the #9378 flow-dispatch status table, from the one shared definition#9676
os-zhuang merged 1 commit into
mainfrom
claude/issue-9462-declared-endpoints-status-table

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes#9462

Door 3 of 3. packages/runtime/src/endpoint-executor.ts's flow branch ended with one
unconditional line:

returnsuccessAnswer(awaitautomation.execute(plan.flow,automationContext));

so a declared type: 'flow' endpoint answered 200 for every outcome, with the raw
engine result in data. A flow that was disabled, had no start node, could not be found,
or ran and was rejected all reached the caller as
{"success":true,"data":{"success":false, ...}} — the double envelope #3962 removed from
/actions, still standing on the surface an app publishes as its own public API. A client
branching on the HTTP status read every one of those failures as a success.

It now answers the #9378 table, by calling the one shared definition#9446 created
(packages/runtime/src/flow-dispatch-status.ts) rather than by writing a third private
copy of the rule:

engine exitrealitythis door answers
flow not foundnever dispatched404
flow disablednever dispatched409FLOW_DISABLED
flow has no start nodenever dispatched422FLOW_NO_START_NODE
ran and was rejectedran, rejected400FLOW_FAILED

Maintainer ruling, 2026-08-18, verbatim 「同意」: the table is a property of the
flow-dispatch CONTRACT rather than of the trigger route, converged in stages. This is the
last stage; all three doors now read one definition.

⚠️ BREAKING — and what a caller sees differently

  • A failed or refused flow is now a 4xx, carrying the platform's declared error
    envelope. There is no inner data.success left to read; the same fact is in
    error.code.
  • A 400 carries the run's own artefacts in error.details (errorMessage,
    summary), byte-identical to POST /api/v1/automation/:name/trigger. The three
    never-dispatched rows carry neither — no run happened to describe them.
  • A successful run is unchanged: 200 with the result in data.
  • outputMapping is no longer applied to a failure body. No change was needed for
    this: api-endpoint-step.ts already keys the projection on answer.status < 400, so
    the refusal rows fall outside it by the rule that was already written. It closes a real
    hole — the projection used to be applied to the 200-wrapped FAILURE body and could
    present a refused dispatch as data.
  • cacheTtl's Cache-Control rides the same status < 400 test, so it no longer rides a
    flow failure either. The rateLimit / authRequired chain is untouched: it runs
    upstream of execution, so a refusal here is only ever reached by a request that already
    passed it.

Consumer enumeration — the real greps, and what they found

The card requires consumers be enumerated before flipping. Every query below is reported
with a control query proving the search works, because a zero-match grep exits 0 and
reads exactly like a pass.

RepoQueryHitsControl
objectstackapi/v1/apps/ (the declared-endpoint mount, ADR-0121 D1) across *.ts,*.tsx,*.mdx,*.json,*.yml,*.mjs267, in 41 files — all of them the runtime's own tests, the spec/metadata matcher tests, the showcase declaration, and docsappEndpointMountPrefix resolves in 4 files
objectstack.data.success / data?.success40 — none of them a declared-endpoint caller (they are /actions, package-install, and automation-API schema tests)same query finds the /actions sites it should
objectuiapi/v1/apps0apps/ (the UI router prefix) 1109 · v1/automation 33
cloudapi/v1/apps/0api/v1/ 1102
packages/client (SDK)api/v1/apps / APP_ENDPOINT0v1/actions 4

There is no objectui half to coordinate. objectui has zero references to the
declared-endpoint mount; all 1109 of its apps/ hits are the console's own router paths
(/apps/{app_id}/{object}), and interpretFlowResponse — its double-envelope reader — is
driven only by FlowRunner, RecordDetailView and useConsoleActionRuntime, i.e. the
trigger route and /actions, both of which already answer the table.

Falsified: the SDK is not a consumer. The dispatch's starting measurement named
packages/client/src/index.ts plus two test files as matching the double-envelope shape.
They do — but for POST /api/v1/actions/... (normalizeActionResult) and for the generic
{success, data, meta} unwrap. The SDK exposes no method that calls a declared
endpoint at all
, so nothing there changes.

The only in-repo consumers of a declared flow endpoint's response are:

  • packages/qa/dogfood/test/showcase-declarative-endpoints.dogfood.test.ts — asserts the
    success path (200, body.success === true). Unchanged by this PR.
  • docs/qa/platform-checklist/areas/api-backend.json — its clauses cover the
    object_operation feed, authRequired, the 501 and the transport 404. It pins nothing
    about the flow failure envelope, so it needs no edit.

Docs

  • content/docs/protocol/kernel/http-protocol.mdx — the boundary wording spec: add FLOW_DISABLED / FLOW_NO_START_NODE to AutomationResult.code and deliver the trigger ruling's remaining two rows (409/422) #9415 corrected
    to describe the divergence flips back to "the same response contract", as the card's
    own constraints require.
  • content/docs/automation/flows.mdxa second page carrying the same sentence,
    found by the enumeration above and not named in the card. It read "Declared endpoints
    (type: 'flow') are the remaining exception: they still answer 200 for every outcome,
    so read data.success there rather than the status."
    Leaving it would ship a documented
    falsehood created by this very diff, so it is corrected here, in the same defect class
    and with the correct form already pinned by the card's constraint. Flagged rather than
    folded in silently; the claim's file surface was amended on the issue in the same round.

Reverse verification — predicted, then measured

The ablation restores the unconditional successAnswer (dropping both the flowIsUnknown
probe and the classifyFlowRefusal mapping). Predictions were written to a file before
the run
; direction predicted: RED.

Predicted 11 fail / 4 pass. Actual 11 fail / 4 pass — and the same four survivors.

The four that stay green are deliberate CONTROLS, not cases that fail to pin the defect:
a successful run is UNCHANGED, an UNCLASSIFIED refusal keeps today's 200, and their two
cross-door twins. Each pins behaviour this change must NOT alter, so green under the
ablation is their correct result. endpoint-executor.test.ts (49 cases, the pre-change
suite) also stays fully green under the ablation — the control proving the ablation touched
only what this card changes.

Excerpt, ablated tree:

 ❯ src/endpoint-flow-dispatch-status.test.ts (15 tests | 11 failed) 47ms
× row 1: a flow the service does not hold is 404, and is never dispatched
× row 2: a disabled flow is 409 FLOW_DISABLED, not a 200 carrying a false success flag
× row 3: a flow with no start node is 422 FLOW_NO_START_NODE
× row 4: a run that dispatched and was rejected is 400 FLOW_FAILED, with the run's own artefacts
× the four rows are DISTINGUISHABLE - the half a per-row assertion cannot see
...
Test Files 1 failed | 1 passed (2)
Tests 11 failed | 53 passed (64)

The fix was restored from the branch (git checkout {branch} -- {path}), verified
byte-identical to the commit by an empty git diff HEAD.

Verification — all at 45c2720d2, the final commit

whatresult
full workspace pnpm buildEXIT=0
@objectstack/runtime test171 files, 2543 tests, all passed
@objectstack/runtime typecheck (tsc --noEmit)EXIT=0
consumer packages (client 310 · cli 1452 · client-react 34 · hono 73 · http-conformance 72 · verify 32 · plugin-dev 54 · cloud-connection 191 · app-showcase 334 · app-crm 42 · app-todo 106)all passed
dogfood, this seam over a real socket (showcase-declarative-endpoints + declarative-endpoint-policy)2 files, 25 tests passed
check:type-check-coverage · check:type-check-debt · check:nul-bytes · check:route-envelope · check:error-status-conformance · check:error-code-casing · check:dispatcher-error-vocabularyall EXIT=0
check:cross-package-test-inputs · check:engine-double-contract · check:where-matcher · check:query-options-erasure · check:docs-audit-scope · check:docs-redirects · check:role-word · affected-docs drift · check-adr-0087-registration · check-changeset-no-major · check-empty-changesetall EXIT=0

⚠️ A first consumer run was scoped with --filter '...@objectstack/runtime' alone and
reported four red suites. Every one was Failed to resolve entry for package @objectstack/service-settings / -automation / -storage / -realtime — packages
outside that filter closure and therefore unbuilt. That is NOT MEASURED, not red; the
table above is the re-run on a fully built tree.

Not addressed here

The unclassified-refusal residual — an AutomationResult with success: false and no
code and no status — keeps today's 200 at this door, and is pinned as such. #5040 §4
(this module's opening rule) makes a declared flow endpoint a stable URL plus a policy
layer over POST /automation/:name/trigger: same context builder, same execute call, so
the same answer. /actions refuses that residual under its own #3962 ruling about its
route; adopting that reading here would promote an exit the producer never classified —
the one thing the shared table's note says a door must not do — and would do it by
borrowing a ruling about a different door. If the residual should speak HTTP everywhere,
that is one change to the shared table for all three doors, not a fourth reading invented
at this one. Raised in the report for the maintainer.

Generated by Claude Code


Generated by Claude Code

…-dispatch status table (#9462)
Door 3 of the #9446 ruling. `executeFlow` ended with an unconditional
`successAnswer(await automation.execute(...))`, so every refusal left the
seam as `200 {success:true,data:{success:false,...}}` — the double envelope
#3962 removed from /actions, on the surface an app publishes as its own API.
It now reads `flow-dispatch-status.ts`, the one shared definition the trigger
door and /actions already read: 404 / 409 FLOW_DISABLED / 422
FLOW_NO_START_NODE / 400 FLOW_FAILED.
Co-Authored-By: Claude <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/runtime, touching 3 documentable anchor(s).

5 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/api/client-sdk.mdx(via automation.execute (sdk))
  • content/docs/api/plugin-endpoints.mdx(via /automation/:name/trigger (route))
  • content/docs/automation/flows.mdx(via automation.execute (sdk), /automation/:name/trigger (route))
  • content/docs/protocol/kernel/http-protocol.mdx(via /automation/:name/trigger (route))
  • content/docs/ui/actions.mdx(via /automation/:name/trigger (route))
What this run could not see

Coarse fallback — 23 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json c07d6e8b9382d6e6b08603e4335d5a60747081depackageMentionDocs.

Which tree this was computed on

This run read content/docs from e03df8ab1fb5e7faafbfb5839a773552bdf83ea2 — the merge of head 45c2720d286222c30b42eeb9327e72c9dd7054f2 into base c07d6e8b9382d6e6b08603e4335d5a60747081de, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin e03df8ab1fb5e7faafbfb5839a773552bdf83ea2 && git checkout e03df8ab1fb5e7faafbfb5839a773552bdf83ea2
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin c07d6e8b9382d6e6b08603e4335d5a60747081de 45c2720d286222c30b42eeb9327e72c9dd7054f2 && git checkout -B drift-repro c07d6e8b9382d6e6b08603e4335d5a60747081de && git merge --no-ff 45c2720d286222c30b42eeb9327e72c9dd7054f2
node scripts/docs-audit/affected-docs.mjs --json c07d6e8b9382d6e6b08603e4335d5a60747081de

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs c07d6e8b9382d6e6b08603e4335d5a60747081de → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Aug 18, 2026
@os-zhuang
os-zhuang marked this pull request as ready for review August 18, 2026 15:14
@os-zhuang
os-zhuang added this pull request to the merge queueAug 18, 2026
@os-zhuangClaude

Copy link
Copy Markdown
ContributorAuthor

PM review — accepted, ready + auto-merge armed. ✅ A confirmed (it is the status quo). ⛔ B and C are not mine to rule.

domain:cli seat, session session_012WKSnqAaoqtW3QX7SSf1Vk. Six required checks all completed: success.

The card's central claim, verified structurally rather than from the report

The whole point of this card is call the shared definition, do not write a third copy. I re-derived it:

readingresult
flowIsUnknown / classifyFlowRefusal in endpoint-executor.ts on origin/main0 — clean probe baseline
control: files referencing flow-dispatch-status on origin/main2 — the two doors already converged
on the branch: import at :58-62 from ./flow-dispatch-status.js, called at :529 and :545genuine call
FLOW_DISABLED / FLOW_NO_START_NODE still in the file2 — both inside the JSDoc table at :459-464, directly above "Read from ../flow-dispatch-status.js"

⭐ That last row is the one worth checking rather than assuming: two status-code literals surviving in the file is exactly what a third private copy would look like. They are documentation pointing at the shared source, and no branching code duplicates the table. 2 → 3 doors on one definition.

The staged convergence is therefore observable in the repo, not merely asserted in prose — which is the evidence I relied on for a BREAKING change, over and above the cited ruling.

Landing probe registered:flowIsUnknown|classifyFlowRefusal in packages/runtime/src/endpoint-executor.ts — baseline 0 pre-merge. A symbol this diff introduces at that location, measured against its own baseline. ⛔ Not an absence, ⛔ not a gate's count.

✅ Q — A confirmed, and the reason is narrow

Keeping the unclassified success:false residual at 200 is what the code already did. Confirming it changes nothing, which is precisely why it is inside a PM's authority.

B is not. Refusing that residual 400 would change contract reject behaviour on a published surface — Clause ② on its own terms, and an unruled widening decided at one call site. The dev named the hazard exactly right: that is the shape that produced three divergent doors in the first place. ⛔ I am not ruling it, and it must not be smuggled in as a follow-up "consistency" edit.

C is not either. "Make the residual a fifth row, or have the producer classify it" is a change to a shared contract governing all three doors. That is a design decision for the maintainer, not a defect.

⭐ The dev's judgement to take A now and not file C as a backlog issue was correct: "it is a decision, not a defect." Filing it as a card would have put an unruled design question into a queue that dispatches things. It belongs in the maintainer's decision channel, and I am raising it there rather than closing over it.

What I am NOT treating as settled

The residual leaves a real asymmetry on a public surface: for that one outcome the double envelope survives, and outputMapping can still project a 200-wrapped failure as data. The dev said so plainly instead of letting A read as "all clean". That asymmetry is now the only place this seam disagrees with itself, and it is on the maintainer's desk, not closed.

Also verified

  • ⚠️A retracted measurement, correctly retracted. A first consumer run scoped --filter '...@objectstack/runtime' alone reported 4 red suites — every one Failed to resolve entry for package …, i.e. packages outside the filter closure and therefore unbuilt. Not measured, not red. Reporting that as a red result would have been wrong; suppressing it silently would also have been wrong.
  • ⚠️One consumer not run@objectstack/driver-turso, whose lock turn never came inside the ~20 min budget. Declared, with the reason it is low-risk (a database driver with no path to the HTTP flow envelope) and left to CI. That is the fourth measurement this shared lock has cost today; mechanism filed as [finding] The shared verify lock starves convention-COMPLIANT agents: -w budgets differ 6x, and flock grants to whoever is currently waiting #9661, ⛔ not this PR's problem.
  • Ablation: predicted 11 fail / 4 pass in writing before running, observed 11/4 with the same four survivors — and the four are deliberate controls (success path, the residual, and their cross-door twins) pinning what this change must not alter. Green under ablation is their correct result, not a gap.
  • Docs: I independently searched all of content/docs for the claim this diff falsifies. It appears in exactly one place on mainautomation/flows.mdx:1318-1319 — which is the page the dev found on its own and which the card did not name. There is no third page. The other pages the drift bot listed are anchored via /automation/:name/trigger and automation.execute, routes this PR does not change.

Generated by Claude Code

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

Labels

documentationImprovements or additions to documentationsize/lteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Declared endpoints (type: 'flow'): converge onto the #9378 status table — the ruled follow-up to #9446's staged execution

2 participants

@os-zhuang@claude