Skip to content

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

Closed

Description

@os-zhuang

Filed by the triage seat as the door-3 half of the #9446 ruling (maintainer, 2026-08-18, verbatim 「同意」: the #9378 status table is a property of the flow-dispatch CONTRACT, executed in stages — door 2 in #9446 itself, door 3 as this card).

Scope

packages/runtime/src/endpoint-executor.ts's flow branch currently ends with an unconditional successAnswer(await automation.execute(...)) — HTTP 200 wrapping {"success":false,...} on every failure, the double envelope #3962 ruled out for /actions. Converge it onto the four-row table the trigger routes answer after #9413/#9415 (404 / 409 FLOW_DISABLED / 422 FLOW_NO_START_NODE / 400 FLOW_FAILED), consuming the producer's result.code.

Constraints (from the ruling)

  • The envelope flip is BREAKING for any consumer of the current double envelope on declared endpoints. The PR must say so plainly, and landing is sequenced against consumers — enumerate them (objectui, examples, docs) with real greps before flipping, and coordinate the objectui half if one exists.
  • Door 3 carries the endpoint policy chain and outputMapping, so this is not a copy of door 2's mapper — the mapping must sit where those still apply.
  • content/docs/protocol/kernel/http-protocol.mdx's boundary wording (corrected by spec: add FLOW_DISABLED / FLOW_NO_START_NODE to AutomationResult.code and deliver the trigger ruling's remaining two rows (409/422) #9415 to describe the divergence) flips back to "same response contract" in the same PR.
  • Clause-②: yes — accept/reject behavior changes on a public surface; contract-review tier dispatch, claim carries Clause-②: yes.
  • If implementation surfaces a reason door 3 cannot converge, report the fork back to the decision inbox — no silent scope change.

Refs: #9446 (the ruling record) · #9378 (the table) · #9413 / #9415 (doors 1's rows) · #3962 (the double-envelope precedent).

Filed by triage session session_pm_triage_20260818T0104Z; ruling provenance on #9446.

Activity

  1. added theissue type on Aug 18, 2026
  2. self-assigned this
    on Aug 18, 2026
  3. os-zhuang commented on Aug 18, 2026

    @os-zhuang
    ContributorAuthor

    Claim: PM loop round 18
    Session: session_012WKSnqAaoqtW3QX7SSf1Vk
    Branch: claude/issue-9462-declared-endpoints-status-table
    Worktree: objectstack-issue-9462
    Domain: domain:cli
    File surface: packages/runtime/src/endpoint-executor.ts + its test siblings, and content/docs/protocol/kernel/http-protocol.mdx (the boundary wording this card's own body says flips back) — stop on breach; explain in the report
    Container & model: M–L, mode:subagent, model: opus — a DOWNGRADE from the Clause-② mandate, under the maintainer's one-time authorization
    Clause-②: yes, verbatim from this card's own body — accept/reject behaviour changes on a public surface.
    Serial constraints cleared: packages/runtime freed at 12:38:35Z — #9446 landed (PR #9584). Verified on origin/main by probe, ⛔ not taken from the merge event: flow-dispatch-status is referenced by exactly 2 files, action-execution.ts and domains/automation.ts — the two live doors — against a control of 243 files in packages/runtime/src. ⚠️ #9416 and #9457 are deliberately NOT dispatched alongside this card (same package; the maintainer re-confirmed 「不豁免,维持串行」 on 2026-08-18). This card goes first because the staging ruling sequenced door 3 after door 2, and door 2 has just landed.

    Model-tier downgrade, recorded as the rule requires

    Maintainer authorization, 2026-08-18, verbatim: 「授权一次性降档到 opus」, given in answer to a direct question naming five cards — #9446, #9416, #9462, #9488, #9487 — while the Fable 5 budget was exhausted (five measurements, ~01:2x through ~09:3x, the last three by throwaway probe touching no card). ⛔ The authorization does not generalise beyond those five.

    The review tier is reduced; the verification bar is not. The reverse-verification requirement below is the compensating control, and I will verify the breaking-change analysis myself rather than on report.

    Premise re-verified on origin/main this round

    ⚠️ What makes this card heavier than #9446, and what the dispatch will demand

    The envelope flip is breaking for any consumer of the current HTTP 200 {"success":true,"data":{"success":false,...}} double envelope — the shape #3962 already ruled out for /actions. The card's own constraints require the consumers be enumerated with real greps before flipping, and the objectui half coordinated if one exists. My starting measurement for the dev to extend rather than trust: packages/client/src/index.ts plus two of its test files match the double-envelope shape.

    ⛔ If implementation surfaces a reason door 3 cannot converge, that is a fork report back to the decision inbox — ⛔ never a silent scope change. And door 3 carries the endpoint policy chain and outputMapping, so this is not a copy of door 2's mapper: the mapping has to sit where those still apply.


    Generated by Claude Code

  4. os-zhuang commented on Aug 18, 2026

    @os-zhuang
    ContributorAuthor

    File-surface amendment (dev seat, same round)

    The claim's declared file surface named packages/runtime/src/endpoint-executor.ts + its
    test siblings and content/docs/protocol/kernel/http-protocol.mdx. The consumer
    enumeration this card requires turned up a second page carrying the same boundary
    sentence
    , so the surface is amended here rather than left to be discovered in the diff:

    • added: content/docs/automation/flows.mdx

    It reads, today on origin/main (line 1317):

    The same table answers at the action door. … Declared endpoints (type: 'flow') are
    the remaining exception: they still answer 200 for every outcome, so read
    data.success there rather than the status.

    That is the identical defect class as the http-protocol.mdx sentence this card's own
    constraint 5 says flips back — boundary wording added by the #9413/#9415 series to
    describe a divergence this PR removes — and the correct form is already pinned by the
    sibling page plus the constraint. Leaving it would ship a documented falsehood created by
    this very diff. Scope is unchanged otherwise: still one defect class, no new verification
    surface (both pages ride the same check:docs-* / docs-drift family, all green locally).

    Evidence query behind the find, with its control:

    grep -rn "\.data\.success|data\?\.success" --include=*.ts --include=*.tsx --include=*.mdx \
         --exclude-dir=node_modules --exclude-dir=dist .        # 40 hits, 2 of them in content/docs
    

    The fix is named explicitly in the PR body, per the bounded in-place-fix rule.

    Generated by Claude Code


    Generated by Claude Code

  5. os-zhuang commented on Aug 18, 2026

    @os-zhuang
    ContributorAuthor
    {
      "issue": 9462,
      "status": "done",
      "branch": "claude/issue-9462-declared-endpoints-status-table",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/9676",
      "premise_still_valid": true,
      "summary": "Door 3 converged. `executeFlow` in packages/runtime/src/endpoint-executor.ts now CALLS the shared definition (`flow-dispatch-status.ts`) rather than open-coding a third table: `flowIsUnknown` answers row 1 (404) via the same optional getFlow probe doors 1 and 2 use, and `classifyFlowRefusal` answers rows 2-4 (409 FLOW_DISABLED / 422 FLOW_NO_START_NODE / 400 FLOW_FAILED), with the 400 arm carrying the run's `errorMessage`/`summary` in `error.details` byte-identically to the trigger door. The unconditional `successAnswer` is gone from every refusal path; a successful run is untouched. Both doc pages that asserted the divergence are flipped, and a minor changeset frames the break. NOTE the one judgement call, raised in open_questions and pinned as a test: the UNCLASSIFIED `success:false` residual keeps today's 200 at this door, matching door 1 (the route #5040 section 4 makes this endpoint an alias of), not door 2.",
      "tests": "All at final commit `45c2720d2` (git rev-parse --short HEAD from the same runs; tree byte-identical after the ablation was restored — `git diff HEAD` empty). Full workspace `pnpm build` EXIT=0. `@objectstack/runtime` test: `Test Files 171 passed (171) / Tests 2543 passed (2543)`, EXIT=0. `@objectstack/runtime` typecheck (`tsc --noEmit`) EXIT=0. DOWNSTREAM consumers (prefix filter `...@objectstack/runtime` = dependents; 13 packages + runtime): 12 of 13 run, all green — 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; plus the dogfood consumer of THIS seam over a real socket (showcase-declarative-endpoints + declarative-endpoint-policy) `Test Files 2 passed (2) / Tests 25 passed (25)`. NOT RUN: `@objectstack/driver-turso` (13th consumer) — its lock turn never came inside the ~20min queue budget and I tore my waiter down rather than stall; it is a database driver with no path to the HTTP flow envelope, and CI covers it. Gates, all EXIT=0 at the same sha: 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-vocabulary, 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, scripts/docs-audit/check-affected-docs.mjs, check-adr-0087-registration, check-changeset-no-major, check-empty-changeset — the last four beyond the derivation the dispatch carried, re-derived from my ACTUAL changed paths with scripts/pm/dispatch-gates.mjs. CI job status at report time: in_progress (reported at draft-PR time per the 2026-08-10 ruling; no CI waiting). ⚠️ ONE MEASUREMENT RETRACTED: a first consumer run scoped `--filter '...@objectstack/runtime'` alone reported 4 red suites — every one `Failed to resolve entry for package @objectstack/service-settings`/`-automation`/`-storage`/`-realtime`, packages OUTSIDE that filter closure and therefore unbuilt. NOT MEASURED, not red; the numbers above are the re-run on a fully built tree. ABLATION (restore the unconditional successAnswer): predictions written to a file BEFORE running, direction predicted RED — predicted 11 fail / 4 pass, ACTUAL 11 fail / 4 pass, and the same four survivors. The 4 green ones are deliberate controls (success path 200, unclassified residual 200, and their two cross-door twins) pinning behaviour this change must NOT alter; `endpoint-executor.test.ts` (49 cases, the pre-change suite) also stayed fully green under the ablation. CROSS-DOOR PARITY: the same AutomationResult driven through all three doors from ONE service object — disabled 409 / no-start-node 422 / ran-and-failed 400 / unknown-flow 404 (and none of the three dispatches it) / success 200 — all equal at all three doors.",
      "open_questions": [
        {
          "question": "The UNCLASSIFIED refusal residual (`success:false` with no `code` and no `status`) is the one outcome on which the three doors do not all agree, and the shared table deliberately does not own it. I chose door 1's reading (keep 200) for door 3 and pinned it. Confirm or overturn.",
          "options": [
            "A — mirror the TRIGGER door: keep 200. #5040 section 4, the rule endpoint-executor.ts opens with, 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, or the alias has become the second execution dialect that module exists to prevent. Also never PROMOTES an exit the producer did not classify, which the shared module's note calls the thing a door must not do. Cost: the double envelope survives for this one residual on a public app surface, and outputMapping can still project that 200-wrapped failure as data.",
            "B — mirror /actions: refuse it 400 FLOW_FAILED. Removes the double envelope from this seam completely and closes the outputMapping hole for the residual too. Cost: breaks alias-equivalence with door 1 for that case (the exact drift #5040 section 4 forbids), and imports a ruling made about a different route.",
            "C — settle it once in the SHARED table for all three doors, as a follow-up card: make the residual a fifth row, or have the producer classify it so it stops being a residual at all."
          ],
          "recommendation": "A now (it is what landed), C as the durable answer. A is the conservative, already-ruled choice and B is an unruled behaviour widening decided at one call site — precisely the shape that produced three divergent doors in the first place. But A leaves a real asymmetry on a public surface, so if the maintainer wants the double envelope gone everywhere, the correct act is C: one edit to flow-dispatch-status.ts that moves all three doors together, not a fourth private reading at this one. I did not file C as an issue — it is a decision, not a defect, and it belongs in this thread rather than in the backlog until it is ruled."
        }
      ],
      "out_of_scope_findings": []
    }

    Zone-2 assumptions, checked

    assumption verdict
    packages/client/src/index.ts + 2 test files are consumers FALSIFIED. Those hits are /actions (normalizeActionResult, already converged in #9446) and the generic {success,data,meta} unwrap. The SDK has 0 references to api/v1/apps — control: 4 hits for v1/actions. No SDK method calls a declared endpoint at all.
    the shared module's rows apply unchanged at door 3 HELD, including the getFlow seam. Door 3 has it: deps.automationService is the same IAutomationService occupant, so flowIsUnknown(service, plan.flow) is the identical probe, asked after the existing isServiceServeable 501 gate and before execute. A service omitting getFlow dispatches as before, pinned. No dead row.
    outputMapping applies only on the success path HELD, and the mapping site needed no change. It lives in api-endpoint-step.ts keyed on answer.status < 400, upstream-independent of this function, so the refusal rows fall outside it by the rule already written. Flipping the envelope therefore also 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 test; the rateLimit/authRequired chain runs upstream of execution and is untouched.
    only http-protocol.mdx carries the divergence wording FALSIFIED. content/docs/automation/flows.mdx carried the same sentence ("Declared endpoints … are the remaining exception"). Fixed under the bounded in-place-fix rule; claim file surface amended in the comment above, named in the PR body.

    Generated by Claude Code


    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

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions