Skip to content

feat(spec)!: retire the export-job API family, IExportService and ScheduleState (ADR-0049) - #20194

Merged
objectstack-fleet[bot] merged 9 commits into
mainfrom
claude/issue-17158-export-family-retire
Sep 27, 2026
Merged

objectstack-fleet[bot] merged 9 commits into
mainfrom
claude/issue-17158-export-family-retire

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #17158
Clause-②: no

Retires the export-job API contract family, the IExportService contract (with ScheduleExportInput) and automation/ScheduleState from @objectstack/spec: ADR-0049 enforce-or-remove, route 3 (whole-def removal). Nothing ever served, bound or read any of them. The served export door GET /api/v1/data/:object/export and 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 in api/export.zod.ts, IExportService and ScheduleExportInput leave the public surface through RETIRED_DEFS_BY_MAJOR[18] rows, one D3 semantic entry and refusal pins, with the api-surface/ and json-schema.manifest/ ratchets moving and the reference pages regenerated. ScheduleState goes too, "retired with the family unless a live consumer is measured". BREAKING at minor, with the ADR-0087 disposition and Clause-②: 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 plus ScheduleState and carries the pin.
  • 5825830323, scope note, 「同意」: the export-job LIST pair (ListExportJobsRequestSchema with limit default 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)

reading tree family names (43) control
objectstack, code outside packages/spec base 49144fcc 0 code files: only prose (2 unreleased changesets, generated reference pages, docs/qa FOLLOW-UPS, 3 comment lines in the dogfood D7 ledger) ImportJobProgress(Schema): 7 files
objectui at the pinned .objectui-sha f8a9d0fb0596 pin 0 non-markdown files; the only hits are CHANGELOG prose recording objectui#10247 ImportJobProgress 7, GetMetaItemLayeredResponseSchema 9, @objectstack/spec/api 34
cloud main 48d70663 0 files (git grep exit 1) @objectstack/spec: 537 files
served routes base @objectstack/rest mounts no /api/v1/data/export route and no POST on /api/v1/data/:object/export; the route ledger lists only GET /api/v1/data/:object/export the import/jobs routes are served and ledgered
ADRs naming the family base 0 (git grep exit 1) "enforce-or-remove": 43 ADRs

Pin: not moved. objectui#10247 landed as objectui PR #10264, merge 8b1f066192a4. git merge-base --is-ancestor 8b1f066192a4 f8a9d0fb0596 exits 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-sha is 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); and ExportApiContracts.
  • @objectstack/spec/contracts: IExportService, CreateExportJobInput, CreateExportJobResult, ExportJobDownload, ListExportJobsOptions, ExportJobListResult, ScheduleExportInput. The module contracts/export-service.ts and its test are deleted, and the barrel line is replaced by a note.
  • @objectstack/spec/automation: ScheduleStateSchema, ScheduleState, ScheduleStateParsed.
  • Stays: ExportFormat, ExportImportTemplateSchema, the import validation shapes, the whole import-job family including ImportJobApiContracts, and GET /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:

  • 13 RETIRED_DEFS_BY_MAJOR[18] entry files under migrations/entries/retired-defs/: 12 api/… plus automation/ScheduleState.
  • The D3 semantic entry export-job-family-retired, whose reason states why it is not a D2 conversion.
  • registry.ts regenerated by gen:migration-registry. The step-18 rationale is not touched.
  • The route's own evidence, kept as proof. The first gen:schema refused 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 as json-schema/api/….json — RETIRED_DEFS_BY_MAJOR, major 18., and the authorable-surface gate records 12 baseline deletion(s) … carry their own proof (#4650), each def no longer emitted by this build. ExportJobStatus is 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):

artifact lines
json-schema.manifest/{api,automation} −12 / −1
authorable-surface/{api,automation} −67 / −16
authorable-defaults/{api,automation} −7 / −4
api-surface/{api,automation,contracts} −34 / −3 / −7
export-origins/{api,automation,contracts} −33 / −3 / −7
declaration-map/{api,automation} −23 / −2

authorable-surface.base.json is untouched (written only by gen: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.ts module docblock, which renders into content/docs/references/api/export.mdx: "The export the platform serves is the synchronous streaming door GET /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 and ExportApiContracts) was never served by any route and was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove); a recurring export is a Job whose handler you write."
  • ImportJobStatus docblock, which used to link the retired enum: "Import Job Status — the states the import worker actually moves a job through (succeeded, not completed, is the success terminal)."
  • RunListResult docblock (contracts/automation-service.ts), which cited ExportJobListResult: "Unlike a cursor-paged list shape, and deliberately: there is ⛔ NO nextCursor here."
  • The retirement notes at the old section 2 of api/export.zod.ts and section 6 of automation/execution.zod.ts, plus the contracts/index.ts barrel note (code comments, not rendered).

The prose says "@objectstack/spec 17" and not 18, because check:future-spec-major requires naming the release line (ADR-0087 amended): a pre-GA retirement ships as a minor of 17. The registry rows stay under major 18.

Pins (new and flipped)

  • New packages/spec/src/api/export-job-family-retirement.test.ts (7 tests; added to vitest.repo-tests.json, repo project). It follows the system/compliance-families-retirement.test.ts form:
    • zero holders on every public entry for all 43 names via export-origins/, with survivors on ./api, ./automation and ./contracts;
    • the runtime namespaces agree;
    • contracts/export-service.ts is gone, with no in-package importer;
    • the 13 defs are absent from json-schema.manifest/, and the 43 names from the api-surface/, declaration-map/ and export-origins/ shards, each with a lit control;
    • no authorable rows remain under the defs;
    • registration: all 13 defs in RETIRED_DEFS_BY_MAJOR[18], the D3 entry wired with its "not a D2 conversion" reason, a backtick-free surface, a replacement naming the import-job family as NOT retired, and no conversion id for the family;
    • a tree-scoped absence leg over packages / examples / skills / content / scripts. That radius is already declared for @objectstack/spec in scripts/cross-package-test-inputs.mjs and turbo.json, and check:cross-package-test-inputs is green.
  • Flipped cron-typed-positions-retirement.test.ts: the three sites whose defs retire whole (ScheduledExport / ScheduleExportRequest schedule.cronExpression, ScheduleState.cronExpression) move to LEFT_WITH_THEIR_DEFS. Two facts stay asserted: no key-level registration ever, and each enclosing def is now a RETIRED_DEFS_BY_MAJOR[18] entry (dark control integration/DataSyncConfig). The four live positions are pinned unchanged, and the envelope case now uses CacheWarmup.schedule.
  • Flipped type-alias-convention.pin.test.ts: the three isomorphic pins leave with their schemas, 790 → 787, receipt added.
  • Removed with their subjects: the family's blocks in api/export.test.ts, the ScheduleStateSchema block in automation/execution.test.ts, and contracts/export-service.test.ts.

Ablations (one-shot; the fix was committed first; scripts/ablation-replace.mjs wrap mode under trap … EXIT INT TERM with absolute paths)

The subject resolves from src/ and committed JSON (no package specifier, no dist/), so no rebuild is needed per leg. These ran at 6c35939e.

leg mutation (on disk: anchor count → 0, blob moved) result
A1b re-plant ExportJobStatus const + type in api/export.zod.ts (blob 501ee4a5 → 2d84be39) RED 2: "runtime namespaces agree…" (api must not export ExportJobStatus) and the tree-scoped leg (packages/spec/src/api/export.zod.ts references typeof ExportJobStatus)
A2 delete 'automation/ScheduleState', from the generated RETIRED_DEFS_BY_MAJOR[18] region (blob 6a3b0b58 → 58ab1619) RED 2: "declares all thirteen defs…" and the cron file's "[#17158] … covered at DEF grain"
A3b re-add "api/ExportJobStatus" to json-schema.manifest/api.json (blob 8c11f3d0 → 5ea54e58) RED 2: "the generated shards…" (must have left json-schema.manifest/) and the tree-scoped leg
  • Every restore was proven: blob equals HEAD, and git diff HEAD is empty.
  • ⚠️ The first A1/A3 attempts were no-ops: each replacement contained its own anchor, so the tool refused, restored and never ran the tests. Those readings are void; A1b/A3b are the re-runs with consuming anchors.
  • Under A1b the export-origins "zero holders" leg stayed green, as designed: it reads the committed artifact. A re-planted export is caught by check:api-surface / check:export-origins in CI.

Tests and gates at a050a8a7 (final head)

  • @objectstack/spec: build + check:generated --fix (15 of 15 current; only check:docs was regenerated); test (local) 539 files, 15777 passed, 2 todo; test:repo 32 files, 586 passed; typecheck (tsc + scripts + test-typecheck) exit 0 (at d08bc4e2, before a comment-only commit).
  • Consumer suites, direction stated: these are the non-spec suites that read what this diff edits.
    • @objectstack/vitest-filter-preflight test/config-wiring-sweep.test.ts reads spec's vitest.repo-tests.json: 65/65.
    • @objectstack/dogfood test/expression-conformance.test.ts, the D7 ledger in the retirement radius: 7/7.
    • Both ran at d08bc4e2. No package outside spec imports a removed name (measured above), so no downstream closure was rebuilt for an absence check.
  • Gates: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, derived at a050a8a7, gives 111. All were run, and --ran reconciles: "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 "no major bump";
    • check:api-surface green;
    • check:spec-parsed-alias "787 pinned isomorphic";
    • check:nul-bytes OK;
    • check:cross-package-test-inputs OK;
    • the live node scripts/check-issue-citations.mjs "every citation this change adds resolves".
  • NOT MEASURED: check:dual-build-cjs-loads and check: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.
  • Lint, narrowed and proven:
    1. population: eslint --print-config resolves the config for the 24 changed JS/TS files, and none is ignored;
    2. eslint --no-inline-config --format json gives 24 files, 0 errors, 0 warnings;
    3. invariance: the resolved parserOptions carry only ecmaVersion / sourceType (no type-aware project), so no untouched file's verdict can move.
  • Merge: origin/main was merged at 369bcbed through scripts/pm/os-regen-merge.sh. The one deferred shard (authorable-surface/automation.json) was regenerated in its own commit, keeping main's automation/DecisionConfig:mode and dropping the 16 retired ScheduleState rows again. registry.ts against origin/main is +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)

  • Served door vs ExportFormat (read-only inference): GET /api/v1/data/:object/export reads format as csv, json or xlsx, and any other value falls back to csv with a 200. ExportFormat also declares jsonl and parquet, and its only in-repo reader is ExportImportTemplateSchema, which has no reader outside spec either. Neither is in the ruling. A first draft of this PR's prose tied the served door to ExportFormat, and it was corrected before opening. carrier: 承接者:无
  • .changeset/19365-automation-runs-cursor-hasmore.md (another PR's unreleased changeset) cites "the shape IExportService.listExportJobs already 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 export schedule blocks and ScheduleState keep 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 and ScheduleStateSchema.status as enforce-or-remove candidates, which are now resolved. §9d is append-only and outside this card's surface. carrier: 承接者:无
  • Ordinary concurrency: [finding] three sibling list doors declare limit/cursor and never read them, one reporting hasMore: false as a literal — REBUILD of #19365, which stopped resolving on 2026-09-21 #19543's branch also moves the type-alias-convention.pin.test.ts count (790 → 789). Whichever lands second rebases, and the counts compose to 786.

Generated by Claude Code

…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
…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
…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
@github-actions github-actions Bot added size/xl documentation Improvements or additions to documentation tests tooling labels Sep 27, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 78 documentable anchor(s). ⚠️ 21 changed file(s) yielded no anchor (packages/spec/api-surface/api.json, packages/spec/api-surface/automation.json, packages/spec/api-surface/contracts.json, …), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

20 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 98f722a742cc8510d6f84fc9428d3c48d3b78369.

⛔ 1 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 21 changed file(s) yielded no anchor (packages/spec/api-surface/api.json, packages/spec/api-surface/automation.json, packages/spec/api-surface/contracts.json, …) — pages documenting those are invisible to this run
  • 1 cross-cutting symbol(s) contributed no route anchor: jobId (10 routes)
  • 15 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 207 client-bound route-ledger rows — the other 153 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 153: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 98 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 136 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 98f722a742cc8510d6f84fc9428d3c48d3b78369 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 299ddf32a41bb2de515924d08465ba85f9073753 — the merge of head a050a8a7f3fd3d9cbd95d68f8c230b99b21355d8 into base 98f722a742cc8510d6f84fc9428d3c48d3b78369, 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 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

⚠️ 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 98f722a742cc8510d6f84fc9428d3c48d3b78369 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: a050a8a7f3fd3d9cbd95d68f8c230b99b21355d8

① Derived judgments

Inputs used: card #17158 body and all 13 comments (ruling A 5644350616, landing route 5815151991, scope note 5825830323 read verbatim); PR #20194 body, files and the head's check-runs; objectstack at merge-base 369bcbed (= git merge-base head origin/main) and at the head; objectui at the pin f8a9d0fb0596; cloud main at 48d70663 (read-only depth-1 clone under my scratch dir). No gate family was rerun locally; every conclusion below comes from the head's check-runs (39 runs at a050a8a7: 34 success, 5 skipped, 0 failure, including Lint & Repo Gates, Test Core 1-6, Check Changeset x2, Type Check x5, Spec property liveness, Build Core, Build Docs).

  • Removed set vs the ruling's words: RIGHT. Diff of export lines in packages/spec/src/api/export.zod.ts merge-base vs head: 12 defs leave (ExportJobStatus, CreateExportJobRequest, CreateExportJobResponse, ExportJobProgress, ScheduledExport, GetExportJobDownloadRequest, GetExportJobDownloadResponse, ListExportJobsRequest, ExportJobSummary, ListExportJobsResponse, ScheduleExportRequest, ScheduleExportResponse) = 32 exported names (Schema consts, input aliases, 9 Parsed aliases; GetExportJobDownloadRequest and ExportJobSummary never had a Parsed alias) plus ExportApiContracts = 33; contracts/export-service.ts (deleted) carried IExportService + 6 interfaces = 7; automation/execution.zod.ts loses ScheduleStateSchema / ScheduleState / ScheduleStateParsed = 3. Total 43, as the PR states. Ruling A item 1 names ExportJob…, ScheduledExport, ScheduleExportRequest, the POST /api/v1/data/export/… bodies, IExportService and ScheduleExportInput; item 2 names ScheduleState "unless a live consumer is measured" (none measured, below); the scope note adds the list pair (ListExportJobsRequest with limit default 20 / cursor, and its response) and rules the import-job family OUT. The other five IExportService types are that module's own input/output shapes and two of them (CreateExportJobInput / CreateExportJobResult) are named by the landing route as what objectui retired first. Nothing ruled out is left behind; nothing kept by the ruling is removed.
  • ExportFormat, ExportImportTemplateSchema and ImportJob* stay: RIGHT by the ruling's words. ExportFormat is not an ExportJob… name, not a scheduled-export def and not a request/response body of the unserved family, and it is still consumed at the head by the kept template shape (packages/spec/src/api/export.zod.ts:200 format: ExportFormat.optional()); the scope note's last bullet puts ListImportJobs* / ImportJob* explicitly out of scope, and the head keeps all of them (export.zod.ts:361-494, ImportJobApiContracts at :494). The kept import family's describe strings are unchanged: 0 added lines in the export.zod.ts / execution.zod.ts diff are code (every + line is comment text) and 0 contain .describe(.
  • Module docblock truthful: RIGHT. export.zod.ts:15-21 says the served export is the synchronous GET /api/v1/data/:object/export answering CSV, JSON or XLSX and the async family "was never served by any route and was removed in @objectstack/spec 17". Measured at the head: packages/rest/src/rest-server.ts:10280-10282 registers method: 'GET', path: ${dataPath}/:object/export; :10326-10327 reads format as 'csv' | 'json' | 'xlsx'; git grep -nE "export/(schedules|:jobId)|/data/export" -- packages/ at the head hits only the retirement comment (export.zod.ts:55), i.e. no /api/v1/data/export route and no POST export route exists. The rendered page content/docs/references/api/export.mdx:15-21 carries the same paragraph; references/index.mdx moves 1538 to 1525 schemas (API 444 to 432, Automation 75 to 74 = the 12 + 1).
  • 13 retired-def entry files: RIGHT, complete. json-schema.manifest/api.json loses exactly 12 keys and automation.json loses automation/ScheduleState (13 - lines, 0 + lines). The 13 files packages/spec/src/migrations/entries/retired-defs/18.api__{CreateExportJobRequest,CreateExportJobResponse,ExportJobProgress,ExportJobStatus,ExportJobSummary,GetExportJobDownloadRequest,GetExportJobDownloadResponse,ListExportJobsRequest,ListExportJobsResponse,ScheduleExportRequest,ScheduleExportResponse,ScheduledExport}.ts and 18.automation__ScheduleState.ts each export entry = '{namespace}/{Def}' spelled exactly as the removed manifest key; the set difference in both directions is empty. registry.ts is +206/-0 and is generated from that directory (packages/spec/scripts/build-migration-registry.ts:114-116 reads semantic/ and retired-defs/; the file's own header says the three tables are generated). spec-changes.json is untouched and rightly so: it projects protocol 16 to 17 only (0 records with toMajor: 18; the step-18 saved-report retirement in 8d1f7ab7 did not move it either).
  • Semantic entry export-job-family-retired: ACCURATE. entries/semantic/18.export-job-family-retired.ts: surface names all 13 defs, the 43-name breakdown and the three sub-paths, with no backtick; replacement points at the served GET door, the SDK data.export (exists: packages/client/src/index.ts:7181, exercised in client.test.ts:547), Job.schedule.expression (exists: packages/spec/src/system/job.zod.ts:56) and the flow start node's config.schedule (packages/spec/src/automation/flow.zod.ts:902-903), and names the import-job family as NOT retired; reason quotes the three rulings and states why D3 not D2 (no stack collection / metadata type; matches the retired-defs precedent in the same directory, e.g. 18.api__CrudEndpointPattern.ts); acceptanceCriteria states runtime is unchanged and the out-of-repo population is not measured. The "cancel" route it mentions did exist (cancelExportJob in the old ExportApiContracts, merge-base export.zod.ts:804-809).
  • Walking pin packages/spec/src/api/export-job-family-retirement.test.ts: MEANINGFUL, not tautological. Seven cases: (1) holdersOf(name) is [] for all 43 names over every public entry with survivors asserted on ./api, ./automation, ./contracts; (2) runtime import('./index') / import('../automation/index') have none of the runtime names and all survivors; (3) contracts/export-service.ts and its test are gone from disk with an in-package importer walk over src/ (anti-vacuity: kept files exist); (4) the 13 def keys are absent from json-schema.manifest/ and the 43 names word-bounded absent from api-surface/, declaration-map/, export-origins/ and the two authorable shards, each shard with a lit control (ImportJobStatus, IAutomationService, ExecutionLogSchema, api/ImportRequest:); (5) registration: all 13 in RETIRED_DEFS_BY_MAJOR[18], the D3 entry wired into step 18 with the "not a D2 conversion" reason, no conversion id matching export-job|schedule-state|scheduled-export, lit control on the conversion table; (6) a matcher self-test that a reference matches and a backticked prose mention does not; (7) a tree-scoped walk over packages / examples / skills / content / scripts (more than 1000 visited files asserted) excluding only the retirement kit, .changeset/, releases and authorable-surface.base.json. The three reported ablations (A1b re-plant ExportJobStatus RED on legs 2 and 7; A2 delete 'automation/ScheduleState' from the registry RED on leg 5 and the cron file's def-grain case; A3b re-add the manifest key RED on legs 4 and 7) each target a distinct leg and the stated reds are the legs that read that surface, so they are consistent with the code. Stated bound, accepted: leg 1 reads the committed export-origins/ artifact and stays green under a bare re-plant (the PR says so; check:api-surface / check:export-origins in Lint & Repo Gates are the CI catch, green at the head). Registered in vitest.repo-tests.json (+1), so it runs in test:repo (Test Core, green).
  • 83 authorable-surface row deletions: the gate's prescribed route, not a hiding hand edit. All 83 - lines in authorable-surface/{api,automation}.json (67 + 16) are keyed "api/{one of the 12}: or "automation/ScheduleState: (0 lines fall outside; 0 + lines). The route is the one packages/spec/scripts/build-schemas.ts prescribes: the manifest disappearance ratchet (:779-795) demands the manifest key be deleted AND declared in RETIRED_DEFS_BY_MAJOR; the merge-base-anchored manifest deletion gate (:2605-2640) prints each declared removal as RETIRED_DEFS_BY_MAJOR, major 18; check (c) proof 3 (:1271-1280) waives per-key rows under a def the build no longer emits. authorable-surface.base.json (the pinned anchor, written only by gen:authorable-surface-base, baseRev 85c6d76e) is untouched, as it must be. authorable-defaults (-11), api-surface (-44 lines = 43 names, ExportJobStatus const+type), export-origins (-43), declaration-map (-25) contain only lines matching the 43 names and no additions. check:authorable-surface runs in lint.yml:5544 under Lint & Repo Gates: success at the head.
  • Type-alias pin 790 to 787: DERIVED from the tree. grep -c '^export type Iso_' on type-alias-convention.pin.test.ts gives 790 at the merge base and 787 at the head; the three removed pins are Iso_api_export__ExportJobStatus, __ExportJobSummarySchema, __GetExportJobDownloadRequestSchema, exactly the three family schemas that had no XParsed alias (the other nine defs and ScheduleState carried one and were never on the list). The test's runtime companion recomputes the count from source and asserts the title and header against it, so a wrong hand count is red, never silent; check:spec-parsed-alias reports 787 per the PR and Lint & Repo Gates is green.
  • cron-typed-positions-retirement.test.ts still pins what it pinned. The four positions on live schemas (DataSyncConfig.schedule, Connector.syncConfig.schedule, CacheWarmup.schedule, BackupConfig / DisasterRecoveryPlan.testing) keep every case (strip, nesting carriers, envelope, tsc channel, materialization control). The three sites whose defs left move to LEFT_WITH_THEIR_DEFS and keep both facts that still hold: no RETIRED_KEYS_BY_MAJOR entry at any major (lit control integration/Connector:errorMapping) and each enclosing def present in RETIRED_DEFS_BY_MAJOR[18] (dark control integration/DataSyncConfig). The envelope case swaps the departed ScheduledExport site for CacheWarmup.schedule, whose premise holds: .changeset/cron-typed-positions-retired.md records that all seven positions normalized into the { dialect: 'cron', source } envelope. Nothing was weakened that still has a subject.
  • Consumers, measured with lit controls. objectstack head, outside packages/spec, all file types: 2 files, both prose (content/docs/references/api/export.mdx:20, the regenerated retirement sentence; packages/qa/dogfood/test/expression-conformance.ledger.ts:449-451, comment lines recording spec: retire the seven cron-typed positions nothing reads — export schedules, ScheduleState.cronExpression, DataSyncConfig.schedule, CacheWarmup.schedule, backup/DR schedules — under ADR-0049 (#15954 ruling, option A per family) #16320); code files: 0; control ImportJobProgress(Schema)?: 7 files. Inside packages/spec outside the kit: only CHANGELOG.md, the anchor file, and the three retirement comments. objectui at the pin f8a9d0fb0596: git merge-base --is-ancestor 8b1f066192a4 (objectui#10247, PR dogfood: simulate a brand-new developer's first-run journey — README → create an app → skills-driven AI build → validate & test #10264) exits 0; non-markdown files naming any of the 43: 0 (exit 1 on code); markdown hits are the objectui#10247 changeset, six CHANGELOGs and ROADMAP.md; controls @objectstack/spec/api 34 files, ImportJobProgress 1 non-md file. cloud main 48d70663: 0 files (exit 1); control @objectstack/spec 537 files. .objectui-sha is unchanged at merge base and head (f8a9d0fb0596), so the landing route's pin condition is met without a bump.

② Semver level

  • .changeset/17158-export-job-family-retired.md: '@objectstack/spec': minor, a **BREAKING** banner in the first body line, the ADR-0087 HTML-comment disposition adr-0087: registered export-job-family-retired, and Clause-②: no. minor is what scripts/check-changeset-no-major.mjs requires during the launch window (a major on any fixed-group member is refused; the BREAKING banner and the ADR-0087 disposition are the mandatory breaking-ness carriers); check-adr-0087-registration.mjs:1931-1935 parses the registered disposition followed by one or more ids, and the id resolves to the semantic entry this PR adds, so registered (not already-registered) is the honest disposition. Clause-②: no is as ruled and true: no published payload gains a key. Both Check Changeset runs are green at the head. Form matches the precedent retirement changesets (15110-…, 15932-…, 20102-retire-saved-report-stack.md).
  • FROM to TO table is accurate and complete: rows 1-4 account for the 33 api names (10 + 13 + 9 + 1), row 5 the 7 contracts names, row 6 the 3 automation names = 43; each "instead" is verified above (GET door, data.export, Job.schedule.expression, flow config.schedule). One harmless over-generalisation: rows 1-3 say "with their types and Parsed aliases" though GetExportJobDownloadRequest and ExportJobSummary had no Parsed alias; nothing a consumer could mis-act on.
  • "Supersedes the unreleased cron-positions rows": the claim is that .changeset/cron-typed-positions-retired.md says the export schedule blocks and ScheduleState keep their surviving keys, and this entry says those defs are now gone whole. Both files exist side by side in .changeset/ at the head; the cron entry is a merge-base changeset (not editable by a code PR, check-empty-changeset green) and its statements were true when written. Release compilation concatenates both entries; the new entry's "Read together with" paragraph resolves the reading order. Harmless.
  • Tombstones name release line 17: RIGHT. packages/spec/package.json is 17.4.0; scripts/check-future-spec-major.mjs (header, quoting ADR-0087 amended 2026-09-13: "a tombstone names the npm release it ships in, never the protocol major […] a retirement shipping minor lands in 17.x.y") refuses prose dating a change above the published major, and pnpm check:future-spec-major runs in lint.yml:5750 (green). Registry rows stay under major 18, matching every other step-18 retired def in the directory.

③ Boundary flags

None blocking. Each is outside this card's ruled surface or is prose in another repo; carriers are named where one exists.

  • ?format= silent fallback to csv (dev Acceptance note 1): OUT of this card. packages/rest/src/rest-server.ts:10326-10327 maps any unrecognised format to 'csv' with a 200, and ExportFormat (export.zod.ts:33-39) declares jsonl / parquet that no route serves. Pre-existing on main; ruling A rules on the unserved async family and item 3 / the D3 acceptance criteria say runtime is deliberately unchanged. Candidate for its own card (served-door accept-set vs declared enum); not filed here by the reviewer.
  • ExportFormat / ExportImportTemplateSchema unconsumed outside spec (dev note 2): out of scope by the ruling's words (see ① first bullet); noted, not a defect of this PR.
  • .changeset/19365-automation-runs-cursor-hasmore.md:97-98 cites "the shape IExportService.listExportJobs already uses" as precedent (dev note 3). A merge-base changeset; once this lands the precedent it names is a removed contract. Release prose only; no code reads it; not editable by this PR (check-empty-changeset). Carrier: the release compiler or the maintainer at release time.
  • docs/qa/platform-checklist/FOLLOW-UPS.md §9c (dev note 4) lists the async export-job surface and ScheduleStateSchema.status as enforce-or-remove candidates now resolved; append-only section, outside this card's surface. Noted.
  • objectui ROADMAP.md at the pin still advertises the retired family (ROADMAP.md:63-64, :83, :204: "Integrate spec v3.0.9 CreateExportJobRequest / ExportJobStatus for async streaming exports", unchecked). Markdown prose in the sibling repo, not code; objectui#10247 retired the code path. Candidate objectui prose follow-up; not this card's surface.
  • Orphaned runtime or REST route: none. Nothing served the family (only GET /api/v1/data/:object/export is registered, untouched); IExportService had no provider binding (old contracts/export-service.ts:14-16); packages/rest and packages/core name none of the 43 (measured above). The Dogfood Regression Gate x4, Dogfood Verify CLI and Temporal Conformance runs are green at the head.
  • Forward merge with PR feat!: retire GET /api/v1/automation for GET /api/v1/meta/flow; ListAiConversationsResponse declares hasMore (#19543) #20192 (claude/issue-19543-list-doors-3-4, head eb08fb39): nothing in THIS head makes it unsafe. Overlap: both add step-18 entries under migrations/entries/ (registry.ts is generated from the entries dir, so a regen after the text merge is the resolution and any stale text is caught by check:generated); both move the type-alias-convention.pin.test.ts count (787 here, 789 there; the count is machine-recomputed and asserted against the prose, so a wrong composed count goes red rather than silent; 786 is the arithmetic); both delete disjoint rows from json-schema.manifest/api.json, api-surface/api.json, export-origins/api.json, authorable-surface/api.json, authorable-defaults/api.json. This head hard-codes nothing about feat!: retire GET /api/v1/automation for GET /api/v1/meta/flow; ListAiConversationsResponse declares hasMore (#19543) #20192's defs (FlowSummary, ListFlows*), and the new pin's matchers are bounded to the 43 names and 13 def keys, so feat!: retire GET /api/v1/automation for GET /api/v1/meta/flow; ListAiConversationsResponse declares hasMore (#19543) #20192's files cannot trip it. The scope note's absorption of [finding] three sibling list doors declare limit/cursor and never read them, one reporting hasMore: false as a literal — REBUILD of #19365, which stopped resolving on 2026-09-21 #19543 door ② here and feat!: retire GET /api/v1/automation for GET /api/v1/meta/flow; ListAiConversationsResponse declares hasMore (#19543) #20192 carrying doors 3-4 are consistent.
  • Not measured, stated as such by the PR and the changeset: consumers of the published @objectstack/spec outside the three repos.

Implemented-by: claude/issue-17158-export-family-retire
Reviewed-by: session_01Rjy9MeetSfq34PKn81CRiN

VERDICT: PASS

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 27, 2026 07:41
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 27, 2026
Merged via the queue into main with commit 4db1bf1 Sep 27, 2026
44 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-17158-export-family-retire branch September 27, 2026 08:13
os-zhuang pushed a commit that referenced this pull request Sep 27, 2026
…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
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/xl tests tooling

Projects

None yet

2 participants