fix(spec): AutomationApiContracts names the served /api/v1/automation paths - #20056
Conversation
… paths The nine flow endpoints were declared under /api/automation while the dispatcher mounts the automation door only at its prefix (default /api/v1) plus /automation, so every declared path answered 404 on the default composition. The nine path values, the Base path docblock, the endpoint list and every in-file path quote (including the retired-cursor tombstone text) move to /api/v1/automation; the spec pin moves with them. A runtime test boots the dispatcher plugin with its default prefix and requires every contract route to be mounted and to be a route-ledger row under the documented /api/v1 wire prefix. The pending runs-cursor changeset quoted the tombstone text and is corrected to the new path. Claude-Session: https://claude.ai/code/session_019c3Hi6ZMU1p6m6aA6Bz45d Co-authored-by: Claude <noreply@anthropic.com>
Generated by `pnpm --filter @objectstack/spec gen:docs` after the contract moved to /api/v1/automation. Claude-Session: https://claude.ai/code/session_019c3Hi6ZMU1p6m6aA6Bz45d Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 2 package(s): 7 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 4 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 141 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 244dd0322131a56c20485c489615199af0f72c1d && git checkout 244dd0322131a56c20485c489615199af0f72c1d
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin b76aad5f6fac71cbbcd4ea6bfdf21a59e2fd4d56 10b117632855d4ccf9bebd2cdde3f0dfb02c53f2 && git checkout -B drift-repro b76aad5f6fac71cbbcd4ea6bfdf21a59e2fd4d56 && git merge --no-ff 10b117632855d4ccf9bebd2cdde3f0dfb02c53f2
node scripts/docs-audit/affected-docs.mjs --json b76aad5f6fac71cbbcd4ea6bfdf21a59e2fd4d56
|
Contract reviewServed-tier: 77/77 Isolated at-tier reviewer subagent, run by the Inputs read: card #20034 body + comments 5823177456 (triage), 5823821835 (claim), 5824409816 (os-dev-report); origin comment 5822432366 on #19966 ( ① Derived judgments(a) The fix — correct and complete inside the file.
(b) Consumer search — the dev is right; remedy 1 is correct.
(c) The drift pin guards the real thing.
(d) Changeset
(e) Deliberate correction of
(f) Things left false — verified; acceptable as a follow-up, but it needs a carrier.
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
…v1 runs path Patch round 1 on the AutomationApiContracts path fix: - The ADR-0087 D3 entry automation-runs-cursor-retired named GET /api/automation/:name/runs three times; it now names GET /api/v1/automation/:name/runs, the path the contract publishes and the dispatcher mounts. registry.ts regenerated with gen:migration-registry; gen:upgrade-guide and gen:spec-changes re-run with no byte change (the entry is in step 18). - Two comments (runtime query-param.ts, service-automation run-list-truncation.test.ts) quote the same path in the v1 form. - The changeset declares Clause-② no: no key added, no accept set widened, no export grown. The level stays minor. Claude-Session: https://claude.ai/code/session_019c3Hi6ZMU1p6m6aA6Bz45d Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: 127/127 Isolated at-tier reviewer subagent, run by the Inputs read: card #20034 body + comments 5823177456, 5823821835, 5824409816, 5824987999 (round-1 os-dev-report); ① Derived judgments(a) Round-1 diff is exactly the list — nothing missing, nothing beyond. (b) D3 entry — the three sentences are true at the head; nothing else falsified; mirror byte-identical to its generator.
(c) (d)
(e) PR body — one factual sentence is false; the rest checks.
(f) Nothing round 1 should have fixed and did not. Tree-wide residue at head is the Acceptance list; a second sweep ② Semver level
③ Boundary flags
Implemented-by: VERDICT: FAIL — one blocking item: the PR-body sentence "None of this PR's 10 paths is touched by those commits" is false ( Generated by Claude Code |
Contract reviewServed-tier: 162/162 Isolated at-tier reviewer subagent, run by the Re-issue after the seat's body edit. Inputs read: everything the first record ( Body change is exactly one line. ① Derived judgments(a) Round-1 diff is exactly the list — nothing missing, nothing beyond. Unchanged from (b) D3 entry true at head; mirror byte-identical to its generator. Unchanged: (c) (d) (e) PR body — the false sentence is gone; the replacement bullet's claims measure true, with two precision notes.
(f) Nothing round 1 should have fixed and did not. Unchanged: residue sweeps exit 1 beyond the Acceptance list; both ② Semver levelUnchanged: ③ Boundary flags
Implemented-by: VERDICT: PASS — same head Generated by Claude Code |
…iConversationsResponse declares hasMore (objectstack-ai#19543) (objectstack-ai#20192) Fixes objectstack-ai#19543 Clause-②: yes This finishes the card: door ③'s spec half and door ④. Door ① landed in objectstack-ai#19493 and door ② is absorbed by objectstack-ai#17158, so nothing of the card's ruled work is left after this PR. Door ③'s server half is objectstack-ai/cloud#2426 (open) and is ⛔ not touched here. Rulings executed (card comment 5825819437, maintainer's words verbatim): > door ④: 「退役,统一走 /meta/flow」 > > door ③: 「**Ruled**: the list is **newest first**.」 · 「**This card owns the spec half.** `ListAiConversationsResponseSchema` gains `hasMore` (and `nextCursor`, if declared, meaning "the id of the last conversation on the page"), `cursor` is described, and the SDK doc stays "newest first".」 · 「Land them together, or land cloud after this card.」 ## Door ④ — `GET /api/v1/automation` is retired; the flow list is `GET /api/v1/meta/flow` The route's contract described a capability no build delivered: the request declared `status` / `type` / `limit` (default 50) / `cursor` and the handler read none of them; the response declared `FlowSummary[]` + `total` + `nextCursor` + `hasMore` and the handler answered bare names beside a literal `hasMore: false`. | surface | before | after | |:--|:--|:--| | `packages/runtime/src/dispatcher-plugin.ts` | `server.get(base + '/automation')` mounted (and its environment-scoped twin) | not mounted for GET; `POST` at the same path (createFlow) unchanged | | `packages/runtime/src/domains/automation.ts` | `GET /` branch → `listFlows()` | no branch; the domain declines (`handled: false`); the `objectstack-ai#7900` audit note kept for the surviving reads | | `packages/runtime/src/route-ledger.ts` | row `GET /automation` · `automation.list` | row removed; census sentence 82 → 81 (`check:route-ledger-census --fix`) | | `@objectstack/client` | `automation.list()` | removed (compile error on use) | | `@objectstack/spec/api` | `ListFlowsRequestSchema`, `ListFlowsResponseSchema`, `FlowSummarySchema` + 5 types | removed — `FlowSummarySchema` had no reader but `ListFlowsResponseSchema` (grep of the tree: its own test and the ADR-0122 pin only) | | `AutomationApiContracts` | 9 entries incl. `listFlows` | 8 entries; none is a GET at the bare path | | ADR-0087 | — | D3 semantic entry `automation-flow-list-route-retired` + `RETIRED_DEFS_BY_MAJOR[18]`: `api/ListFlowsRequest`, `api/ListFlowsResponse`, `api/FlowSummary` | Every other `/automation` route is unchanged (runs, `/_status`, actions and connectors catalogs, create / update / delete / trigger / toggle / clone / resume / cancel / restore-suspension / screen). objectstack-ai#20056's table stays true: `automation-api-contract-mounts.test.ts` (every contract route is mounted at the default prefix AND is a ledger row) is green with the entry and the row gone together. **What the wire answers now — measured, not assumed.** On a real socket (`HonoServerPlugin` + `createDispatcherPlugin`, `dispatcher-plugin.anonymous-gate.integration.test.ts`): `GET /api/v1/automation` answers **`405 METHOD_NOT_ALLOWED` with `Allow: POST`**, anonymous and signed in alike, byte-identical (once the echoed path is factored out) to a GET on the POST-only control path `/api/v1/automation/:name/toggle`, and `listFlows` is never called. It is a 405 and not a 404 because `POST` still lives at the path; the host's own unmatched answer says exactly that, and the retired route leaves no text of its own. A transport that forwards every automation path to the dispatcher (the `@objectstack/hono` catch-all) gets `handled: false` and renders its own 404; the domain's anonymous floor still answers 401 first there (pinned in `anonymous-gate-actions-automation.test.ts`). ## Door ③ — spec half: `ListAiConversationsResponseSchema` gains `hasMore` Describe texts, quoted exactly (they ship in the JSON Schema and the references page): - `cursor`: "The `id` of the last conversation on the previous page. The next page starts with the conversation created immediately before it, continuing newest first. Omit it to read the first page. An id that names no conversation of the caller is refused rather than read as the start of the list." - `conversations`: "The caller's conversations, newest first — ordered by creation time, then `id`, both descending" - `hasMore` (new, **required**): "Whether at least one more conversation follows this page. When `true`, send the `id` of the last conversation in `conversations` as `cursor` to read the next page." `nextCursor` is **not** declared. The SDK is unchanged: `client.ai.conversations.list()` still resolves to the array and its doc still reads "newest first"; `content/docs/api/client-sdk.mdx` shows the next-page call (`cursor` = last id held). ### The open choice this PR settles: `hasMore` required, `nextCursor` absent — four axes | axis | `hasMore` required (taken) | `hasMore` optional | |:--|:--|:--| | 实际业务需求 (measured) | Readers today: none parse it — the SDK returns the array, objectui's `useConversationList` reads `?limit=50` page one only (`packages/app-shell/src/hooks/useConversationList.ts` at main `5c61e524` and pin `f8a9d0fb`). The need it serves is the truncated sidebar: a caller with more than `limit` conversations cannot tell a full page from the last one. The only producer is cloud, which cloud#2426 makes compute it. | Same readers; the flag could be absent forever on a conforming server, so the need is served only by convention. | | 项目长远合理性 | The declaration states what every server must do; the window until cloud#2426 lands is ruled ("land cloud after this card") and named, not baked in. | Bakes the transition window into the permanent contract. | | 防 AI 写错 | A server or mock written against `ListAiConversationsResponse` without `hasMore` is a tsc error and a parse refusal (pinned). | Omission is spec-valid; every reader needs an absent-means-unknown fallback — the consumer-side tolerance the frame rejects. | | 创业阶段不扩散 | No staged window, no new gate, no new field beyond the ruled one. | — | `nextCursor`: zero readers anywhere, and by the ruling's own definition it equals the last conversation's `id`, already on the page — a second field is a second place for one value to disagree (startup axis: no pull, no surface). What the SDK does when `hasMore` is absent: nothing — it never reads it, so the window between this PR and cloud#2426 changes nothing any in-repo caller sees. ## Zone-2 measurements (PM mechanism assumptions) 1. At `8d1f7ab7`: `ListAiConversationsResponseSchema` was `{ conversations }` only (`protocol.zod.ts:2946`); `ListFlows*` / `FlowSummarySchema` at `automation-api.zod.ts:59-109`; `listFlows` at `:661-666`; all three exported in `api-surface`, `declaration-map`, `export-origins` — **confirmed**. 2. `client.automation.list` / `ListFlows*` / `FlowSummary` / a bare GET of the list path: **zero callers** outside their own tests and the ledger row in objectstack (branch base `8d1f7ab7`), objectui pin `f8a9d0fb` and main `5c61e524`, cloud main `48d70663`. Positive controls on the same instruments: objectui `apps/console/src/pages/developer/FlowRunsPage.tsx:152` `client.meta.getItems('flow')` and `packages/app-shell/src/views/setup/PackagedAutomationPage.tsx:144` `GET /meta/flow` hit at both objectui refs; cloud `packages/service-ai/src/routes/ai-routes.ts` hit for the conversation route. objectui's `useApiDiscovery.ts` names `/api/v1/automation` only as a route prefix with a `POST /trigger` endpoint — not the list. **Confirmed.** 3. objectstack-ai#20056 keeps `AutomationApiContracts` equal to the served paths; the removal takes the entry and the mount together, and its pin is green — **confirmed**. 4. cloud main `48d70663`: `ai-routes.ts` answers `{ conversations }` with no `hasMore`; `objectql-conversation-service.ts` orders ascending and keyset-pages on `(created_at, id)` — **confirmed**; cloud ⛔ not edited. 5. Generated surfaces, regenerated with the tooling, never by hand except the two deletions the gates prescribe by name: `json-schema.manifest/api.json` (−3 keys) and `authorable-surface/api.json` (−16 keys, reported by the build as "def no longer emitted by this build" — path 3); then `gen:migration-registry`, `check:generated --fix` (api-surface, export-origins, declaration-map, references docs, strictness-ledger counts), `gen:test-typecheck-debt` (runtime ledger −1 signature, the `listFlows` TS2339 it recorded vanished), `check:route-ledger-census --fix`. `authorable-surface.base.json` untouched. Three merges of `origin/main` went through `scripts/pm/os-regen-merge.sh` (the third brought objectstack-ai#20194, see Patch round 1); after each, `check:generated` reported all 15 artifacts current and main's sibling entries (`ui-report-joined-container-selection-refused`, `export-job-family-retired`) are present in `registry.ts`. ## Pins (accept and refuse) - `packages/runtime/src/dispatcher-plugin.anonymous-gate.integration.test.ts` — real socket: GET → 405 + `Allow: POST` + `METHOD_NOT_ALLOWED` + `details` (anonymous and with a session), byte-equal to the control, `listFlows` never called; POST at the same path still mounted (anonymous → 401); the inventory-privacy probe moved to `GET /api/v1/automation/_status` → 401. - `packages/runtime/src/domain-handler-registry.test.ts` — real `dispatch()`: `GET /automation` → exactly `{ handled: false }`, identical to a never-served sub-path, `listFlows` never called; `GET /automation/_status` on the same dispatcher → 200 (anti-vacuity). - `packages/runtime/src/domains/anonymous-gate-actions-automation.test.ts` — anonymous `GET /` still 401 (floor precedes routing), authenticated `GET /` unhandled; inventory reads moved to `/_status`. - `packages/runtime/src/http-dispatcher.test.ts`, `automation-write-capability-gate.test.ts`, `automation-run-read-permission-gate.test.ts`, `http-dispatcher.tenancy-posture-outage.test.ts` — the cases that used the list as a convenient probe now read `/_status` (their subjects — service resolution, stub/degraded slots, tenancy verdicts, the objectstack-ai#7900 audit — are route-independent); the retired row leaves the audit table with a note. - `packages/client/src/client.test.ts` — `'list' in client.automation` is false, with a `@ts-expect-error` on the access (the client test layer compiles with 0 debt, so the directive is live); `meta.getItems('flow')` targets `GET /api/v1/meta/flow`. - `packages/spec/src/api/automation-api.zod.test.ts` — the three names are not exported (with a surviving-export control); the contract map has 8 entries, no `listFlows`, no `GET /api/v1/automation`, and still `POST /api/v1/automation`. - `packages/spec/src/api/protocol.test.ts` — accepts `hasMore` true/false and keeps it; refuses a page without it: issue `code` `invalid_type`, `path` `["hasMore"]`, message `Invalid input: expected boolean, received undefined`; refuses `hasMore: 'false'`; the response shape is exactly `conversations` + `hasMore`; the request keeps `agentId` / `limit` / `cursor`. - `packages/spec/src/type-alias-convention.pin.test.ts` — the `FlowSummarySchema` pin leaves with the schema. At the merged head the count is **786**: objectstack-ai#17158 (landed first) took 790 → 787 and this PR's receipt reads 787 → 786. Re-derived from the merged file (`grep -c '^export type Iso_'` = 786; the test's own recompute agrees), not by arithmetic. - `packages/qa/dogfood/test/authz-probe-blind-spot.census.ts` — `route-ledger.ts` population 82 → 81 (controls re-measured by grep: 82 at base, 81 now), and its prose reading "82 rows over 21 domains" → 81; `authz-conformance.matrix.ts`'s objectstack-ai#17111-pinned docblock figure (82 rows / 21 domains) → 81 / 21, and the dated note in `authz-ledger-population.baseline.ts` records the 82 → 81 move — rows and domains derived from the `ROUTE_LEDGER` table (81 rows, 21 distinct domains; domains unchanged); `showcase-anonymous-deny-surfaces.dogfood.test.ts` — the automation probes read `/automation/_status`. **Ablation (one-shot, not a standing test).** With the fix committed, `scripts/ablation-replace.mjs` re-planted the `GET base + '/automation'` mount in `dispatcher-plugin.ts` (anchor 1 → 0, blob `acbf6f93` → `01d21238`, marker count 1): the socket pin went red — `expected 401 to be 405` — and the restore leg proved blob == HEAD and an empty `git diff HEAD`. The first attempt was a no-op the tool refused (the replacement contained its own anchor); the second is the one reported. ## Tests and gates (read at the final head, quoted from real output) Head **`f3ed706f`** (after Patch round 1). The test matrix ran at `3b8a66d6`; the only change from `3b8a66d6` to `f3ed706f` is the revert of a comment in `.github/workflows/lint.yml` (`git diff --stat`: 1 file, +2 / −3), which no test suite reads. The gate union and the citation check ran at `f3ed706f`. Heavy runs went through `scripts/pm/os-verify-lock.sh`. | run | reading | |:--|:--| | `@objectstack/spec` `vitest run --project local` | 540 files, 15872 passed, 2 todo | | `@objectstack/runtime` `vitest run --project local` | 279 files, 3909 passed, 1 skipped | | `@objectstack/client` `vitest run` | 50 files, 636 passed | | `@objectstack/dogfood`: the whole `authz-conformance.test.ts` (the objectstack-ai#17111 pins that were red in CI), `showcase-anonymous-deny-surfaces.dogfood.test.ts` (real showcase boot), `authz-probe-blind-spot.test.ts` | 3 files, 130 passed | | `typecheck` for spec, runtime, client and dogfood | exit 0 each; the test layers are OK (runtime ledger: 190 errors / 68 signatures, one fewer than base; client: 0) | | `pnpm --filter @objectstack/spec check:generated` | all 15 artifacts current | | `node scripts/check-issue-citations.mjs` (live, diff-scoped) | "✅ check-issue-citations: every citation this change adds resolves (or is a declared cross-repo reference)." — 22 judged: 18 resolve, 2 resolve as pull requests, 2 cross-repo | | `dispatch-gates.mjs --commands` union: 116 families derived for this diff at `f3ed706f` | 116 run, all exit 0. `--ran`: "116 derived famil(ies) accounted for — 116 run, 0 NOT-MEASURED (a DERIVED zero — all 116 recorded an exit code and none of them is 3)" | | roster gates whose roster sits in this diff's directories: `check:authz-resolver`, `check:error-code-casing`, `check:filter-alias-parity`, `check:route-ledger-census` | exit 0 each | | eslint, narrowed to the added or modified `.ts`/`.mjs` files (round 0) | `--format json`: 24 files, 0 errors, 0 warnings. Population: the `files: ['**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}']` block in `eslint.config.mjs`. Invariance: that config enables no type-aware linting (`eslint.config.mjs:328`), so this diff cannot change the verdict on an untouched file. The repo-wide `pnpm lint` runs in CI. | Consumer direction: the removed spec exports have zero importers outside `packages/spec`, so the downstream check is the client and runtime typechecks above. The removed SDK method has zero callers in all three repos. NOT MEASURED locally and left to CI: the path-scheduled CI jobs (Test Core shards, Temporal Conformance, the full Dogfood gate, Build Core) and the workspace type-check lanes. ## Patch round 1 — the two CI reds at `eb08fb39`, fixed 1. **Lint & Repo Gates → `check-issue-citations`**: "2 citation(s) THIS CHANGE ADDS do not resolve". The citation was `objectstack-ai#8715`, which was allocated but never resolved (REST 404). It appeared in `retired-defs/18.api__ListFlowsRequest.ts` and in its generated copy in `registry.ts`. The file now names the precedent by its ADR-0087 entry id, `package-rollback-response-retired`, and its `api/PackageRollbackResponse` row, with no guessed number. The registry was regenerated. 2. **Dogfood Regression Gate (1/3) → `authz-conformance.test.ts` objectstack-ai#17111 pins**: "packages/runtime/src/route-ledger.ts: docblock says 82 rows, the table holds 81". The fix is the matrix docblock → "81 rows / 21 domains". I derived both numbers from the `ROUTE_LEDGER` table: 81 rows, 21 distinct domains, so the domain count did not move. The same sweep fixed the census reading in `authz-probe-blind-spot.census.ts` and the dated note in `authz-ledger-population.baseline.ts`. Two statements remain that are dated and historically true: the reading in `scripts/check-route-ledger-census.mjs`'s header ("at the commit that added this gate") and the `lint.yml` comment (see Acceptance notes). 3. **Merge**: after objectstack-ai#20194 (objectstack-ai#17158) merged (`4db1bf17`, an ancestor of this head), `origin/main` came in through `os-regen-merge.sh` as merge `fd76315a`: - `registry.ts` took main's side and was then regenerated from both sides' entries (+95 lines, no deletions). - The type-alias pin test was resolved by hand, keeping both receipts. - Main's generated shards were taken and regenerated in `3b8a66d6`, with this PR's two hand deletions (manifest −3, authorable-surface −16) re-applied on top of main's bytes. - The generated delta against `origin/main` is exactly this PR's: the three retired defs are out, `ListAiConversationsResponse:hasMore` is in, and strictness-ledger `api/` goes 435 → 432. ## Acceptance notes - `packages/adapters/hono/src/hono.test.ts:454` "GET /api/automation delegates to dispatch()" is an adapter-delegation test against a mock dispatcher and stays true (the catch-all forwards any path); not edited. carrier: none. - `packages/qa/dogfood/test/authz-conformance.matrix.ts:251` names `GET /automation` in prose describing the pre-objectstack-ai#5519 ungated state; historical, not edited. - With no automation service registered, a catch-all transport answers the retired path with the domain's 501 (the capability probe precedes routing domain-wide, by design, so a 501-vs-404 does not fingerprint deployments); pre-existing ordering, unchanged. - The SDK's `ai.conversations.list()` does not surface `hasMore` (out of this card's ruled scope; see the report's open question). - `.github/workflows/lint.yml`'s census-gate comment still says 82 (historical prose, left as is). - `authz-probe-blind-spot.census.ts`'s census paragraph also says "Nine more ledgers exist repo-wide (290 rows in total)". The nine other `*-route-ledger.ts` files hold 117 rows today, and all eleven hold 282 at the base and 281 here, so the 290 was already stale before this PR. It is left as is; carrier: none. - `packages/services/service-automation/README.md` drops the list line and points at `GET /api/v1/meta/flow`; `content/docs/api/plugin-endpoints.mdx` teaches both doors; `docs/qa/platform-checklist/areas/access-security.json` re-points its automation probes to `/_status` and to a single-flow read. Changeset: `.changeset/19543-list-doors-3-4.md` — `minor` for spec / client / runtime, a BREAKING banner with FROM → TO per surface, the Clause-② line with its `(narrowing)` arm, and the ADR-0087 disposition marker `registered automation-flow-list-route-retired`. No `content/docs/releases/` edit. Written by the `domain:spec` seat-1 dispatch (session `session_01Rjy9MeetSfq34PKn81CRiN`), branch `claude/issue-19543-list-doors-3-4`. --------- Co-authored-by: Claude <noreply@anthropic.com>
Fixes #20034
Clause-②: no
Patch round 1 (head
10b1176328)Added on top of the reviewed head
4c216561c6(at-tier review PASS, comment 5824559177). It carries the implementer's own two out-of-scope findings and the reviewer's Clause ② reading:automation-runs-cursor-retired:packages/spec/src/migrations/entries/semantic/18.automation-runs-cursor-retired.ts:11,:43and:72now nameGET /api/v1/automation/:name/runs, the path this PR's contract publishes and the dispatcher mounts.packages/spec/src/migrations/registry.tswas regenerated withpnpm --filter @objectstack/spec gen:migration-registry(not hand-edited; the diff is the same three lines at:5943,:5975and:6004).gen:upgrade-guideandgen:spec-changeswere re-run and changed no bytes, because the entry is in step 18, beyondPROTOCOL_MAJOR17. The text still ships today as data inMIGRATIONS_BY_MAJOR[18], which is why it is corrected now. Open PR fix(spec): the edge-condition upgrade entry and its pending changeset namePOST /api/v1/automation, not the nonexistentPOST /flows#20031 regenerates a different region ofregistry.ts; whichever of the two lands second regenerates.packages/runtime/src/query-param.ts:179andpackages/services/service-automation/src/run-list-truncation.test.ts:6now quote the/api/v1path. Both are comments only..changeset/20034-automation-contract-api-v1-paths.md:9and this body's line 2 now readClause-②: no, with no arm. This diff adds no key, widens no accepted input and adds no export (scripts/pm/clause2-line.mjs:70). The level staysminor..changeset/19365-automation-runs-cursor-hasmore.md:117carries only the registration marker naming the entry's id, and the id is unchanged. So this round needs no further deliberate correction.What this changes
AutomationApiContracts(@objectstack/spec/api) declared its nine flow endpoints under/api/automation. The dispatcher mounts the automation door atconfig.prefix || '/api/v1'plus/automation, andobjectstack servepasses no prefix, so every declared path answered404 ENDPOINT_NOT_FOUNDon the default composition (measured on a composed runtime by the #19966 dev). This PR takes remedy 1: the contract moves to the served paths. The runtime and dispatcher are unchanged.packages/spec/src/api/automation-api.zod.ts: the ninepathvalues, the module'sBase pathline and endpoint list, and every other in-file path quote (section headers,@examples, the resume docblock, and thecursortombstone textListRunsRequestSchemaraises) move from/api/automation…to/api/v1/automation…. After the edit the file holds 0 occurrences of/api/automation(28 moved, plus the 10 docblock lines rewritten).packages/spec/src/api/automation-api.zod.test.ts: the nine path pins move with the values.content/docs/references/api/automation-api.mdx: regenerated withpnpm --filter @objectstack/spec gen:docs(not hand-edited).packages/runtime/src/automation-api-contract-mounts.test.ts(new): the drift pin, below..changeset/20034-automation-contract-api-v1-paths.md(new):@objectstack/specminor..changeset/19365-automation-runs-cursor-hasmore.md: a deliberate correction of a pending note, below.migrations/entries/semantic/18.automation-runs-cursor-retired.tsand the regeneratedmigrations/registry.ts, plus comments inpackages/runtime/src/query-param.tsandpackages/services/service-automation/src/run-list-truncation.test.ts.Reproduction, at base
adbbc5d01eautomation-api.zod.ts:14Base path: /api/automation;:658–:706ninepathvalues under/api/automation; the test pinned all nine to themselves (automation-api.zod.test.ts:803–:811).dispatcher-plugin.ts:909const prefix = config.prefix || '/api/v1';;registerAutomationRoutes(base)mounts${base}/automation…(:1465onwards), called withprefixat:1746, and with${prefix}/environments/:environmentIdat:1742/:1750when project scoping is on.route-ledger.ts:429POST /automation(clientautomation.create) and siblings; the header (:17) says to prepend/api/v1for the wire path.packages/cli/src/commands/serve.ts:4412callscreateDispatcherPlugin({ scoping, enforceProjectMembership, observability, rateLimit }), noprefix; scoping defaults to off (:4340).Consumer search: nothing depends on the unversioned form
path?packages/adapters/hono/src/hono.test.ts:453(GET /api/automation delegates to dispatch())createHonoAppwith the adapter's own defaultprefix(options.prefix || '/api',hono/src/index.ts:303) against a mocked dispatcher and asserts the dispatcher-internal/automation. It never imports the contract.packages/client/api/v1/automationconvention (getRoute('automation')); it never namesAutomationApiContracts. Two comments name the spec test fileautomation-api.zod.test.ts, not the constant.AutomationApiContractsoccurs only in its declaring file, its spec test,api-surface/api.json(name only) andexport-origins/api.json. No generator reads the path values..objectui-sha62597c588git grep -Fat that commit:AutomationApiContracts0 files,/api/automation0 files; positive control/api/v1/automation33 files.objectstack-ai/cloudand npm consumersOne served surface does use the unversioned form: a host built with
createHonoApp({ kernel })and noprefixserves the whole dispatcher, automation included, under/api. That is a documented adapter default, and it applies to every contract family: under that host every other*ApiContractsrow (/api/v1/…) is off by the same segment. The old automation paths matched it by coincidence, not by design, and no code reads the contract under that host, so remedy 2 does not apply. The changeset says how such a host maps the contract paths.Deliberate correction of a pending release note
.changeset/19365-automation-runs-cursor-hasmore.md(pending, not yet released) quotes thecursortombstone text in its FROM/TO block. That text is one of the path quotes this PR moves, so the note became false. Its line 32 changes fromto
Nothing else in that note changes. This is the DELIBERATE CORRECTION class that
check-empty-changeset.mjsnames.skip-changesetis not applied, andCheck Changesetstays red by design. The same-head at-tier review (comment 5824559177) names the note and judges the changed sentence. No other pending changeset quotes an unversioned automation path. At the base,git grep -n "/api/automation" -- '.changeset/*.md'showed only that line. Patch round 1 corrected no further note: no pending changeset quotes the D3 entry's sentences.Changeset level
minor, not declared breaking. Thepathtype staysstring, no accepted input narrows, no method changes, and the old values named paths that no route served on the default composition, so a caller that read the constant gets a working URL now without changing code. This follows the precedent of thePackageApiContracts.installPackage.pathrebind (.changeset/18058-install-door-contract-rebind.md,minor, not breaking). The declaration isClause-②: nowith no arm, in both this body and the changeset. It answers the reader's question "does this widen an accepted input or grow the public surface?" and the answer here is no: no key added, no accepted input widened, no export grown. It is not(narrowing)either, because nothing an author writes is removed.minoris valid underno: a published constant's value moves, andpatchis a floor, not a ceiling. The first head declaredyes, copied from the claim, and the reviewer judged that over-declared.The drift pin, and proof that it can fail
packages/runtime/src/automation-api-contract-mounts.test.tshas two legs:createDispatcherPluginwith noprefix(the compositionobjectstack servebuilds) on a server that records registrations, and requires every contractMETHOD pathto be one of them. The prefix comes from the plugin's own default, not from a constant in the test.route-ledger.tsrow under the documented/api/v1wire prefix. The live-mount parity gate probes those rows through the real router.The runtime vitest config aliases
@objectstack/spec/*to spec source, so the spec side of the pin readssrc/, not a build. The ablation was run on the committed tree (4c216561c6) withscripts/ablation-replace.mjs, one leg at a time, with restores anchored onHEAD:getRun.pathback to/api/automation/:name/runs/:runIdgetRunnamed), 1 passed'/api/v1'→'/api/v2'HEAD,git diff HEADempty for both paths)Environment-scoped mount
The contract does not carry
/api/v1/environments/:environmentId/automation…, and this PR does not add it. No*ApiContractsmap declares the scoped variants. Scoping is one mount-time transformation the dispatcher applies to automation, actions, AI and packages alike, and the client derives scoped URLs from discovery. If the variants are ever declared, that belongs once in a contract shared by all the families, not copied into each map. Note that underprojectResolution: 'required'none of the nine unscoped paths is mounted (dispatcher-plugin.required-scoping-mounts.integration.test.tspins that).Verification (head
10b1176328, patch round 1)batch-last-exit 0: the last part of the batch requires all three suites to exit 0, and the batch printedSUITES spec=0 service-automation=0 runtime=0.@objectstack/spec: 532 files, 15648 passed, 2 todo.@objectstack/service-automation: 144 files, 1725 passed.@objectstack/runtime(--project local): 277 files, 3896 passed, 1 skipped, including the drift pin's 3 cases.turbo run build --filter='./packages/*' --filter='./packages/*/*'at this head: 72 of 72 tasks succeeded, including the tsup and declaration builds of spec, runtime and service-automation.check:generatedreports "All 15 generated artifacts are up to date".check:migration-registryreports "src/migrations/registry.ts is current (242 semantic, 210 retired-key, 183 retired-def)".check:spec-changesandcheck:upgrade-guideboth report up to date.check:api-surfacereports "public API surface + factory signatures unchanged", andcheck:docsreports "225 generated files in sync".git fetch origin main(dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, merge baseadbbc5d01, 10 paths): 113 families, 6 more than round 0 (check:migration-registry,check:spec-changes,check:upgrade-guide,check:future-spec-major, andcheck-tenant-audit-censuswith its self-test). All 113 ran, and--ranreports "113 run, 0 NOT-MEASURED" with 0 UNRUN. 112 exited 0.node scripts/check-empty-changeset.mjs --base origin/mainexits 1 on the deliberate correction, as designed. No family answered PREREQUISITE NOT MET this round.origin/main(15 commits at the seat's re-read). Three of those commits regenerated one of this PR's 10 paths,packages/spec/src/migrations/registry.ts: chore(objectui): bump the console pin to f8a9d0fb0596 (carries objectui#10221 and the objectui#9910 re-cut) with re-measured pin citations and the lockstep re-record #20036 (0bf85eaae6), fix(spec,objectql)!: retirescalefrom the currency field type — refused at parse, no longer enforced on writes #19909 (5b9402d89b) and fix(lint): walk page filterBy and lookup-field lookupFilters as authored filters #19818 (66960564d9).git merge-tree --write-treeoforigin/mainand this head is clean, and the at-tier re-review measured that chore(objectui): bump the console pin to f8a9d0fb0596 (carries objectui#10221 and the objectui#9910 re-cut) with re-measured pin citations and the lockstep re-record #20036's and fix(spec,objectql)!: retirescalefrom the currency field type — refused at parse, no longer enforced on writes #19909's hunks do not touch this PR's region (:5940–:6004). The merge queue's rebuilt generation regenerates the file. The derivation's one changed family input across that range isscripts/sdui-manifest.record.json, from chore(objectui): bump the console pin to f8a9d0fb0596 (carries objectui#10221 and the objectui#9910 re-cut) with re-measured pin citations and the lockstep re-record #20036. (Corrected by the seat after the at-tier review5825376693found the earlier sentence, "None of this PR's 10 paths is touched by those commits", false.)pull_requestevent:check-changeset-no-major --eventprints "✓ This diff introduces nomajorbump." and "✓ LEVEL AXIS: this PR declares clause-②no, so no package here is declared to have grown a published surface." (declaration lineClause-②: no, no arm).check-adr-0087-registrationprints "✓ … this PR adds no declared-breaking changeset (2 non-breaking changeset(s) seen)". This gate reads the Clause ② arm from the changeset body (readClause2Line(parsed.body)), not from the PR event. Its verdict is the same with and without--event.--print-configresolves each).--no-inline-config --format jsonreports 7 files, 0 errors, 0 warnings.eslint.config.mjsenables no type-aware linting: all sevenparserOptionsblocks are{ ecmaVersion, sourceType }only, with noprojectorprojectService. So this diff cannot change the verdict on any untouched file. The repo-widepnpm lintis left to CI.4c216561c6, and the spec and runtimetypecheckruns, both exit 0. Round 1 changes only string literals (the D3 entry and its registry mirror), comments and one changeset line. The type-check-debt gate re-measured at this head: "4 ledger entr(ies) … none above its recorded number".Acceptance notes (observations, not filed)
RouterConfigSchema(spec/src/api/router.zod.ts) defaultsbasePathto/apiwithmounts.automation: '/automation'. That spec-only declaration has no runtime reader in this repo.registerAutomationRoutesmounts. The ones not listed are resume, cancel, restore-suspension, screen, actions, connectors,_statusand the legacy trigger form./api/automationpath at head10b1176328, and why it stays (git grep -n "/api/automation\b", with theautomation-apifile-name hits filtered out):.changeset/20034-automation-contract-api-v1-paths.md:5and:15-:20: the FROM column of this PR's own FROM/TO table.packages/adapters/hono/src/hono.test.ts:453-:454: the Hono adapter's own defaultprefix(/api), driven against a mocked dispatcher. It does not read the contract.packages/runtime/src/automation-api-contract-mounts.test.ts:9: the pin's docblock, describing the drift it guards.packages/spec/scripts/file-description.test.ts:878-:937: synthetic fixtures for the docblock-description extractor. They do not quote the contract.packages/spec/src/api/router.test.ts:343: a custom-mounts fixture forRouterConfigSchema.CHANGELOG.mdentries:packages/client/CHANGELOG.md:3230,packages/runtime/CHANGELOG.md:11589,packages/services/service-automation/CHANGELOG.md:5802andpackages/spec/CHANGELOG.md:32723quoteGET /api/automation/:name/runsin the released ExecutionStatus-filter entry. These are release-owned and never edited in a code PR; an amendment would be a dedicated docs-only PR.packages/spec/CHANGELOG.md:14982names the fileautomation-api.mdx, not a path.Implemented in session
session_019c3Hi6ZMU1p6m6aA6Bz45d(claim 5823821835; patch round 1 dispatched by thedomain:specseat 4).