feat(spec)!: retire the export-job API family, IExportService and ScheduleState (ADR-0049) - #20194
Conversation
…eduleState (ADR-0049) The export-job family in api/export.zod.ts (ExportJobStatus, CreateExportJob*, ExportJobProgress, ScheduledExport, GetExportJobDownload*, ListExportJobs* and ExportJobSummary, ScheduleExport*, ExportApiContracts), the IExportService contract with its six types (ScheduleExportInput included) and automation/ScheduleState leave the public surface. Nothing served, bound or read any of them; the served GET /api/v1/data/:object/export door and the import-job family are unchanged. Thirteen RETIRED_DEFS_BY_MAJOR[18] entries, the D3 semantic entry export-job-family-retired, the retirement pin with its tree-scoped absence leg, and the test triage (export, execution, cron-positions, type-alias pins). Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Rjy9MeetSfq34PKn81CRiN
…t moves json-schema.manifest (13 defs deliberately deleted; the build then reads each as RETIRED_DEFS_BY_MAJOR, major 18), authorable-surface (83 rows, proof 3: def no longer emitted by this build), authorable-defaults, api-surface / export-origins / declaration-map (43 names), the reference pages and the strictness-ledger counts. The changeset carries the BREAKING banner, the FROM -> TO table and the ADR-0087 registered marker. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Rjy9MeetSfq34PKn81CRiN
…not a code comment The module docblock renders into the generated reference page, where a pointer at a code comment dangles. It now states the served synchronous door and the removal in reader terms; check:generated --fix regenerated check:docs only. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Rjy9MeetSfq34PKn81CRiN
…port-family-retire
…ged tree The os-regen driver deferred this shard at the origin/main merge. The merged tree keeps main's automation/DecisionConfig:mode row and drops the sixteen automation/ScheduleState rows this branch retires (proof 3: def no longer emitted by this build). check:generated: all 15 artifacts up to date. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Rjy9MeetSfq34PKn81CRiN
… protocol major check:future-spec-major: a pre-GA retirement ships as a minor of the current published major, so the tombstone prose says @objectstack/spec 17 (ADR-0087, amended). The registry rows stay under RETIRED_DEFS_BY_MAJOR[18]. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Rjy9MeetSfq34PKn81CRiN
…ne wording fix Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Rjy9MeetSfq34PKn81CRiN
…ExportFormat set GET /api/v1/data/:object/export reads its own format (csv | json | xlsx, anything else falling back to csv); ExportFormat also declares jsonl and parquet. The retirement prose (module docblock, section note, D3 entry, changeset) no longer ties the served door to ExportFormat. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Rjy9MeetSfq34PKn81CRiN
…mat wording fix Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Rjy9MeetSfq34PKn81CRiN
📓 Docs Drift CheckThis PR changes 1 package(s): 20 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 1 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 136 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 299ddf32a41bb2de515924d08465ba85f9073753 && git checkout 299ddf32a41bb2de515924d08465ba85f9073753
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 98f722a742cc8510d6f84fc9428d3c48d3b78369 a050a8a7f3fd3d9cbd95d68f8c230b99b21355d8 && git checkout -B drift-repro 98f722a742cc8510d6f84fc9428d3c48d3b78369 && git merge --no-ff a050a8a7f3fd3d9cbd95d68f8c230b99b21355d8
node scripts/docs-audit/affected-docs.mjs --json 98f722a742cc8510d6f84fc9428d3c48d3b78369
|
Contract reviewServed-tier: ① Derived judgmentsInputs used: card #17158 body and all 13 comments (ruling A
② Semver level
③ Boundary flagsNone blocking. Each is outside this card's ruled surface or is prose in another repo; carriers are named where one exists.
Implemented-by: VERDICT: PASS |
…20194) os-regen-merge.sh took main's side of every generated path both sides moved; this commit re-derives them from the merged sources. The two hand deletions a generator cannot reproduce (json-schema.manifest/api.json -3, authorable-surface/api.json -16, the whole-def removals of api/FlowSummary, api/ListFlowsRequest, api/ListFlowsResponse) are re-applied on top of main's bytes; registry.ts is regenerated from both sides' entries (+95 lines, no deletions); check:generated --fix rebuilt spec and rewrote the five it proved stale. Delta vs origin/main is exactly this PR's: the three retired defs out, ListAiConversationsResponse:hasMore in, strictness-ledger api/ 435 -> 432. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Rjy9MeetSfq34PKn81CRiN
…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 #17158
Clause-②: no
Retires the export-job API contract family, the
IExportServicecontract (withScheduleExportInput) andautomation/ScheduleStatefrom@objectstack/spec: ADR-0049 enforce-or-remove, route 3 (whole-def removal). Nothing ever served, bound or read any of them. The served export doorGET /api/v1/data/:object/exportand the import-job family in the same module are untouched.Rulings executed (quoted, not re-ruled)
5644350616, decision batch 🔗 Broken links detected in documentation #122 item 3, maintainer 「同意」: the family inapi/export.zod.ts,IExportServiceandScheduleExportInputleave the public surface throughRETIRED_DEFS_BY_MAJOR[18]rows, one D3 semantic entry and refusal pins, with theapi-surface/andjson-schema.manifest/ratchets moving and the reference pages regenerated.ScheduleStategoes too, "retired with the family unless a live consumer is measured". BREAKING atminor, with the ADR-0087 disposition andClause-②: no.5815151991, decision batch 🔗 Broken links detected in documentation #221 item 2, letter A, 「同意」: objectui retires its side first (objectui#10247), then this card retires the whole family plusScheduleStateand carries the pin.5825830323, scope note, 「同意」: the export-job LIST pair (ListExportJobsRequestSchemawithlimitdefault 20 /cursor, and its response) is in. The import-job family (ListImportJobs*,ImportJob*) is NOT, because it is served.Premise re-measured at this change's base (every zero beside a lit control)
packages/spec49144fccdocs/qaFOLLOW-UPS, 3 comment lines in the dogfood D7 ledger)ImportJobProgress(Schema): 7 files.objectui-shaf8a9d0fb0596ImportJobProgress7,GetMetaItemLayeredResponseSchema9,@objectstack/spec/api34main48d70663git grepexit 1)@objectstack/spec: 537 files@objectstack/restmounts no/api/v1/data/exportroute and noPOSTon/api/v1/data/:object/export; the route ledger lists onlyGET /api/v1/data/:object/exportimport/jobsroutes are served and ledgeredgit grepexit 1)Pin: not moved. objectui#10247 landed as objectui PR #10264, merge
8b1f066192a4.git merge-base --is-ancestor 8b1f066192a4 f8a9d0fb0596exits 0, which is self-proving on any checkout, and the pin is 58 commits ahead of it and 0 behind. The ruling's pin condition is already met, so.objectui-shais untouched.ScheduleState: no live consumer in any of the three repos (the rows above include its three names), so it retires per ruling item 2.What leaves the surface
@objectstack/spec/api:ExportJobStatus;CreateExportJobRequest*/CreateExportJobResponse*,ExportJobProgress*,ScheduledExport*,GetExportJobDownloadRequest*/GetExportJobDownloadResponse*,ListExportJobsRequest*/ExportJobSummary*/ListExportJobsResponse*,ScheduleExportRequest*/ScheduleExportResponse*(Schema consts, input aliases, Parsed aliases); andExportApiContracts.@objectstack/spec/contracts:IExportService,CreateExportJobInput,CreateExportJobResult,ExportJobDownload,ListExportJobsOptions,ExportJobListResult,ScheduleExportInput. The modulecontracts/export-service.tsand its test are deleted, and the barrel line is replaced by a note.@objectstack/spec/automation:ScheduleStateSchema,ScheduleState,ScheduleStateParsed.ExportFormat,ExportImportTemplateSchema, the import validation shapes, the whole import-job family includingImportJobApiContracts, andGET /api/v1/data/:object/export.Route and registration (ADR-0087)
Route 3: none of these shapes is a stack collection or a metadata type, so there is no carrier key for a
retiredKey()tombstone and no authored document for a D2 conversion. The declaration is:RETIRED_DEFS_BY_MAJOR[18]entry files undermigrations/entries/retired-defs/: 12api/…plusautomation/ScheduleState.export-job-family-retired, whosereasonstates why it is not a D2 conversion.registry.tsregenerated bygen:migration-registry. The step-18rationaleis not touched.gen:schemarefused with❌ 13 previously published schema(s) disappeared from this build, naming exactly the 13 defs. After the deliberate manifest deletion, the build reads each one asjson-schema/api/….json — RETIRED_DEFS_BY_MAJOR, major 18., and the authorable-surface gate records12 baseline deletion(s) … carry their own proof (#4650), eachdef no longer emitted by this build.ExportJobStatusis an enum with no key rows.Ratchet movement is all removals, as a whole-def removal must show (an enum-value narrowing would show none):
json-schema.manifest/{api,automation}authorable-surface/{api,automation}authorable-defaults/{api,automation}api-surface/{api,automation,contracts}export-origins/{api,automation,contracts}declaration-map/{api,automation}authorable-surface.base.jsonis untouched (written only bygen:authorable-surface-base).Changed prose, quoted for the contract review
No
.describe()or refusal text changed; the removed schemas' describe strings left with them. Four docblocks changed:api/export.zod.tsmodule docblock, which renders intocontent/docs/references/api/export.mdx: "The export the platform serves is the synchronous streaming doorGET /api/v1/data/:object/export, which answers the file itself as CSV, JSON or XLSX. The asynchronous export-job API that used to be declared here (export jobs, their progress / download / list shapes, scheduled exports andExportApiContracts) was never served by any route and was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove); a recurring export is aJobwhose handler you write."ImportJobStatusdocblock, which used to link the retired enum: "Import Job Status — the states the import worker actually moves a job through (succeeded, notcompleted, is the success terminal)."RunListResultdocblock (contracts/automation-service.ts), which citedExportJobListResult: "Unlike a cursor-paged list shape, and deliberately: there is ⛔ NOnextCursorhere."api/export.zod.tsand section 6 ofautomation/execution.zod.ts, plus thecontracts/index.tsbarrel note (code comments, not rendered).The prose says "@objectstack/spec 17" and not 18, because
check:future-spec-majorrequires naming the release line (ADR-0087 amended): a pre-GA retirement ships as aminorof 17. The registry rows stay under major 18.Pins (new and flipped)
packages/spec/src/api/export-job-family-retirement.test.ts(7 tests; added tovitest.repo-tests.json,repoproject). It follows thesystem/compliance-families-retirement.test.tsform:export-origins/, with survivors on./api,./automationand./contracts;contracts/export-service.tsis gone, with no in-package importer;json-schema.manifest/, and the 43 names from theapi-surface/,declaration-map/andexport-origins/shards, each with a lit control;RETIRED_DEFS_BY_MAJOR[18], the D3 entry wired with its "not a D2 conversion" reason, a backtick-freesurface, areplacementnaming the import-job family as NOT retired, and no conversion id for the family;packages/examples/skills/content/scripts. That radius is already declared for@objectstack/specinscripts/cross-package-test-inputs.mjsandturbo.json, andcheck:cross-package-test-inputsis green.cron-typed-positions-retirement.test.ts: the three sites whose defs retire whole (ScheduledExport/ScheduleExportRequestschedule.cronExpression,ScheduleState.cronExpression) move toLEFT_WITH_THEIR_DEFS. Two facts stay asserted: no key-level registration ever, and each enclosing def is now aRETIRED_DEFS_BY_MAJOR[18]entry (dark controlintegration/DataSyncConfig). The four live positions are pinned unchanged, and the envelope case now usesCacheWarmup.schedule.type-alias-convention.pin.test.ts: the three isomorphic pins leave with their schemas, 790 → 787, receipt added.api/export.test.ts, theScheduleStateSchemablock inautomation/execution.test.ts, andcontracts/export-service.test.ts.Ablations (one-shot; the fix was committed first;
scripts/ablation-replace.mjswrap mode undertrap … EXIT INT TERMwith absolute paths)The subject resolves from
src/and committed JSON (no package specifier, nodist/), so no rebuild is needed per leg. These ran at6c35939e.ExportJobStatusconst + type inapi/export.zod.ts(blob501ee4a5→2d84be39)api must not export ExportJobStatus) and the tree-scoped leg (packages/spec/src/api/export.zod.ts references typeof ExportJobStatus)'automation/ScheduleState',from the generatedRETIRED_DEFS_BY_MAJOR[18]region (blob6a3b0b58→58ab1619)"api/ExportJobStatus"tojson-schema.manifest/api.json(blob8c11f3d0→5ea54e58)must have left json-schema.manifest/) and the tree-scoped leggit diff HEADis empty.export-origins"zero holders" leg stayed green, as designed: it reads the committed artifact. A re-planted export is caught bycheck:api-surface/check:export-originsin CI.Tests and gates at
a050a8a7(final head)@objectstack/spec: build +check:generated --fix(15 of 15 current; onlycheck:docswas regenerated);test(local) 539 files, 15777 passed, 2 todo;test:repo32 files, 586 passed;typecheck(tsc + scripts + test-typecheck) exit 0 (atd08bc4e2, before a comment-only commit).@objectstack/vitest-filter-preflighttest/config-wiring-sweep.test.tsreads spec'svitest.repo-tests.json: 65/65.@objectstack/dogfoodtest/expression-conformance.test.ts, the D7 ledger in the retirement radius: 7/7.d08bc4e2. No package outside spec imports a removed name (measured above), so no downstream closure was rebuilt for an absence check.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, derived ata050a8a7, gives 111. All were run, and--ranreconciles: "111 derived famil(ies) accounted for — 109 run, 2 NOT-MEASURED". Highlights:check:adr-0087-registration"1 declared-breaking changeset(s), each carrying an ADR-0087 disposition";check:changeset-no-major"nomajorbump";check:api-surfacegreen;check:spec-parsed-alias"787 pinned isomorphic";check:nul-bytesOK;check:cross-package-test-inputsOK;node scripts/check-issue-citations.mjs"every citation this change adds resolves".check:dual-build-cjs-loadsandcheck:type-check-debt, reason: PREREQUISITE NOT MET (exit 3). Each needs a whole-workspace build that does not fit the foreground cap on this shared box; CI builds that closure first in lint.yml.eslint --print-configresolves the config for the 24 changed JS/TS files, and none is ignored;eslint --no-inline-config --format jsongives 24 files, 0 errors, 0 warnings;parserOptionscarry onlyecmaVersion/sourceType(no type-aware project), so no untouched file's verdict can move.origin/mainwas merged at369bcbedthroughscripts/pm/os-regen-merge.sh. The one deferred shard (authorable-surface/automation.json) was regenerated in its own commit, keeping main'sautomation/DecisionConfig:modeand dropping the 16 retiredScheduleStaterows again.registry.tsagainstorigin/mainis +206 / −0. Main has since moved one commit (e2c4e125, core security only, no overlapping path), which is not merged here.Acceptance notes (noted, not filed)
ExportFormat(read-only inference):GET /api/v1/data/:object/exportreadsformatas csv, json or xlsx, and any other value falls back to csv with a 200.ExportFormatalso declares jsonl and parquet, and its only in-repo reader isExportImportTemplateSchema, which has no reader outside spec either. Neither is in the ruling. A first draft of this PR's prose tied the served door toExportFormat, and it was corrected before opening. carrier: 承接者:无.changeset/19365-automation-runs-cursor-hasmore.md(another PR's unreleased changeset) cites "the shapeIExportService.listExportJobsalready uses" as precedent. Once this lands it names a removed contract. It is not edited here, because a changeset from the merge base is not modified by a code PR (check:empty-changeset). carrier: 承接者:无.changeset/cron-typed-positions-retired.md(unreleased, same release) says the exportscheduleblocks andScheduleStatekeep their surviving keys. This PR's changeset says so explicitly and supersedes it ("Read together with…").docs/qa/platform-checklist/FOLLOW-UPS.md§9c lists the async export-job surface andScheduleStateSchema.statusas enforce-or-remove candidates, which are now resolved. §9d is append-only and outside this card's surface. carrier: 承接者:无limit/cursorand never read them, one reportinghasMore: falseas a literal — REBUILD of #19365, which stopped resolving on 2026-09-21 #19543's branch also moves thetype-alias-convention.pin.test.tscount (790 → 789). Whichever lands second rebases, and the counts compose to 786.Generated by Claude Code