Skip to content

fix(spec): AutomationApiContracts names the served /api/v1/automation paths - #20056

Merged
os-litant merged 3 commits into
mainfrom
claude/issue-20034-automation-contract-api-v1-paths
Sep 25, 2026
Merged

os-litant merged 3 commits into
mainfrom
claude/issue-20034-automation-contract-api-v1-paths

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #20034
Clause-②: no

Patch round 1 (head 10b1176328)

Added on top of the reviewed head 4c216561c6 (at-tier review PASS, comment 5824559177). It carries the implementer's own two out-of-scope findings and the reviewer's Clause ② reading:

  • ADR-0087 D3 entry automation-runs-cursor-retired: packages/spec/src/migrations/entries/semantic/18.automation-runs-cursor-retired.ts :11, :43 and :72 now name GET /api/v1/automation/:name/runs, the path this PR's contract publishes and the dispatcher mounts. packages/spec/src/migrations/registry.ts was regenerated with pnpm --filter @objectstack/spec gen:migration-registry (not hand-edited; the diff is the same three lines at :5943, :5975 and :6004). gen:upgrade-guide and gen:spec-changes were re-run and changed no bytes, because the entry is in step 18, beyond PROTOCOL_MAJOR 17. The text still ships today as data in MIGRATIONS_BY_MAJOR[18], which is why it is corrected now. Open PR fix(spec): the edge-condition upgrade entry and its pending changeset name POST /api/v1/automation, not the nonexistent POST /flows #20031 regenerates a different region of registry.ts; whichever of the two lands second regenerates.
  • Two comments: packages/runtime/src/query-param.ts:179 and packages/services/service-automation/src/run-list-truncation.test.ts:6 now quote the /api/v1 path. Both are comments only.
  • Clause ②: .changeset/20034-automation-contract-api-v1-paths.md:9 and this body's line 2 now read Clause-②: no, with no arm. This diff adds no key, widens no accepted input and adds no export (scripts/pm/clause2-line.mjs:70). The level stays minor.
  • No pending changeset quotes a sentence of the D3 entry. .changeset/19365-automation-runs-cursor-hasmore.md:117 carries only the registration marker naming the entry's id, and the id is unchanged. So this round needs no further deliberate correction.

What this changes

AutomationApiContracts (@objectstack/spec/api) declared its nine flow endpoints under /api/automation. The dispatcher mounts the automation door at config.prefix || '/api/v1' plus /automation, and objectstack serve passes no prefix, so every declared path answered 404 ENDPOINT_NOT_FOUND on the default composition (measured on a composed runtime by the #19966 dev). This PR takes remedy 1: the contract moves to the served paths. The runtime and dispatcher are unchanged.

  • packages/spec/src/api/automation-api.zod.ts: the nine path values, the module's Base path line and endpoint list, and every other in-file path quote (section headers, @examples, the resume docblock, and the cursor tombstone text ListRunsRequestSchema raises) move from /api/automation… to /api/v1/automation…. After the edit the file holds 0 occurrences of /api/automation (28 moved, plus the 10 docblock lines rewritten).
  • packages/spec/src/api/automation-api.zod.test.ts: the nine path pins move with the values.
  • content/docs/references/api/automation-api.mdx: regenerated with pnpm --filter @objectstack/spec gen:docs (not hand-edited).
  • packages/runtime/src/automation-api-contract-mounts.test.ts (new): the drift pin, below.
  • .changeset/20034-automation-contract-api-v1-paths.md (new): @objectstack/spec minor.
  • .changeset/19365-automation-runs-cursor-hasmore.md: a deliberate correction of a pending note, below.
  • Patch round 1: migrations/entries/semantic/18.automation-runs-cursor-retired.ts and the regenerated migrations/registry.ts, plus comments in packages/runtime/src/query-param.ts and packages/services/service-automation/src/run-list-truncation.test.ts.

Reproduction, at base adbbc5d01e

  • Spec: automation-api.zod.ts:14 Base path: /api/automation; :658–:706 nine path values under /api/automation; the test pinned all nine to themselves (automation-api.zod.test.ts:803–:811).
  • Runtime: dispatcher-plugin.ts:909 const prefix = config.prefix || '/api/v1';; registerAutomationRoutes(base) mounts ${base}/automation… (:1465 onwards), called with prefix at :1746, and with ${prefix}/environments/:environmentId at :1742 / :1750 when project scoping is on.
  • Route ledger: route-ledger.ts:429 POST /automation (client automation.create) and siblings; the header (:17) says to prepend /api/v1 for the wire path.
  • CLI: packages/cli/src/commands/serve.ts:4412 calls createDispatcherPlugin({ scoping, enforceProjectMembership, observability, rateLimit }), no prefix; scoping defaults to off (:4340).

Consumer search: nothing depends on the unversioned form

candidate reads the contract's path? verdict
packages/adapters/hono/src/hono.test.ts:453 (GET /api/automation delegates to dispatch()) no Not a consumer. It drives createHonoApp with the adapter's own default prefix (options.prefix || '/api', hono/src/index.ts:303) against a mocked dispatcher and asserts the dispatcher-internal /automation. It never imports the contract.
packages/client no Builds automation URLs from discovery or its /api/v1/automation convention (getRoute('automation')); it never names AutomationApiContracts. Two comments name the spec test file automation-api.zod.test.ts, not the constant.
everything else in this repo no AutomationApiContracts occurs only in its declaring file, its spec test, api-surface/api.json (name only) and export-origins/api.json. No generator reads the path values.
objectui at the pinned .objectui-sha 62597c588 no git grep -F at that commit: AutomationApiContracts 0 files, /api/automation 0 files; positive control /api/v1/automation 33 files.
objectstack-ai/cloud and npm consumers not measured not checked out here

One served surface does use the unversioned form: a host built with createHonoApp({ kernel }) and no prefix serves the whole dispatcher, automation included, under /api. That is a documented adapter default, and it applies to every contract family: under that host every other *ApiContracts row (/api/v1/…) is off by the same segment. The old automation paths matched it by coincidence, not by design, and no code reads the contract under that host, so remedy 2 does not apply. The changeset says how such a host maps the contract paths.

Deliberate correction of a pending release note

.changeset/19365-automation-runs-cursor-hasmore.md (pending, not yet released) quotes the cursor tombstone text in its FROM/TO block. That text is one of the path quotes this PR moves, so the note became false. Its line 32 changes from

-> throws: '`cursor` was removed from GET /api/automation/:name/runs in

to

-> throws: '`cursor` was removed from GET /api/v1/automation/:name/runs in

Nothing else in that note changes. This is the DELIBERATE CORRECTION class that check-empty-changeset.mjs names. skip-changeset is not applied, and Check Changeset stays red by design. The same-head at-tier review (comment 5824559177) names the note and judges the changed sentence. No other pending changeset quotes an unversioned automation path. At the base, git grep -n "/api/automation" -- '.changeset/*.md' showed only that line. Patch round 1 corrected no further note: no pending changeset quotes the D3 entry's sentences.

Changeset level

minor, not declared breaking. The path type stays string, no accepted input narrows, no method changes, and the old values named paths that no route served on the default composition, so a caller that read the constant gets a working URL now without changing code. This follows the precedent of the PackageApiContracts.installPackage.path rebind (.changeset/18058-install-door-contract-rebind.md, minor, not breaking). The declaration is Clause-②: no with no arm, in both this body and the changeset. It answers the reader's question "does this widen an accepted input or grow the public surface?" and the answer here is no: no key added, no accepted input widened, no export grown. It is not (narrowing) either, because nothing an author writes is removed. minor is valid under no: a published constant's value moves, and patch is a floor, not a ceiling. The first head declared yes, copied from the claim, and the reviewer judged that over-declared.

The drift pin, and proof that it can fail

packages/runtime/src/automation-api-contract-mounts.test.ts has two legs:

  1. mount: it starts createDispatcherPlugin with no prefix (the composition objectstack serve builds) on a server that records registrations, and requires every contract METHOD path to be one of them. The prefix comes from the plugin's own default, not from a constant in the test.
  2. ledger: every contract route must be a route-ledger.ts row under the documented /api/v1 wire prefix. The live-mount parity gate probes those rows through the real router.

The runtime vitest config aliases @objectstack/spec/* to spec source, so the spec side of the pin reads src/, not a build. The ablation was run on the committed tree (4c216561c6) with scripts/ablation-replace.mjs, one leg at a time, with restores anchored on HEAD:

leg mutation (landed on disk: anchor 1 → 0, blob moved) pin result
contract getRun.path back to /api/automation/:name/runs/:runId mount red, ledger red (getRun named), 1 passed
dispatcher default prefix '/api/v1' → '/api/v2' mount red (all nine named), 2 passed
restored none (blobs equal HEAD, git diff HEAD empty for both paths) 3 passed

Environment-scoped mount

The contract does not carry /api/v1/environments/:environmentId/automation…, and this PR does not add it. No *ApiContracts map declares the scoped variants. Scoping is one mount-time transformation the dispatcher applies to automation, actions, AI and packages alike, and the client derives scoped URLs from discovery. If the variants are ever declared, that belongs once in a contract shared by all the families, not copied into each map. Note that under projectResolution: 'required' none of the nine unscoped paths is mounted (dispatcher-plugin.required-scoping-mounts.integration.test.ts pins that).

Verification (head 10b1176328, patch round 1)

  • Tests, each package's full local project, under the verify lock at this head. The lock's verdict is batch-last-exit 0: the last part of the batch requires all three suites to exit 0, and the batch printed SUITES spec=0 service-automation=0 runtime=0.
    • @objectstack/spec: 532 files, 15648 passed, 2 todo.
    • @objectstack/service-automation: 144 files, 1725 passed.
    • @objectstack/runtime (--project local): 277 files, 3896 passed, 1 skipped, including the drift pin's 3 cases.
  • Build: turbo run build --filter='./packages/*' --filter='./packages/*/*' at this head: 72 of 72 tasks succeeded, including the tsup and declaration builds of spec, runtime and service-automation.
  • Spec generated artifacts: check:generated reports "All 15 generated artifacts are up to date". check:migration-registry reports "src/migrations/registry.ts is current (242 semantic, 210 retired-key, 183 retired-def)". check:spec-changes and check:upgrade-guide both report up to date. check:api-surface reports "public API surface + factory signatures unchanged", and check:docs reports "225 generated files in sync".
  • Derived gate set, taken after git fetch origin main (dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, merge base adbbc5d01, 10 paths): 113 families, 6 more than round 0 (check:migration-registry, check:spec-changes, check:upgrade-guide, check:future-spec-major, and check-tenant-audit-census with its self-test). All 113 ran, and --ran reports "113 run, 0 NOT-MEASURED" with 0 UNRUN. 112 exited 0. node scripts/check-empty-changeset.mjs --base origin/main exits 1 on the deliberate correction, as designed. No family answered PREREQUISITE NOT MET this round.
  • Clause ② and ADR-0087, run offline with this body as the pull_request event:
    • check-changeset-no-major --event prints "✓ This diff introduces no major bump." and "✓ LEVEL AXIS: this PR declares clause-② no, so no package here is declared to have grown a published surface." (declaration line Clause-②: no, no arm).
    • check-adr-0087-registration prints "✓ … this PR adds no declared-breaking changeset (2 non-breaking changeset(s) seen)". This gate reads the Clause ② arm from the changeset body (readClause2Line(parsed.body)), not from the PR event. Its verdict is the same with and without --event.
  • Lint, narrowed and proven: all 7 touched TS files are in the eslint population (--print-config resolves each). --no-inline-config --format json reports 7 files, 0 errors, 0 warnings. eslint.config.mjs enables no type-aware linting: all seven parserOptions blocks are { ecmaVersion, sourceType } only, with no project or projectService. So this diff cannot change the verdict on any untouched file. The repo-wide pnpm lint is left to CI.
  • Round-0 evidence still stands, and its sources are unchanged in round 1: the ablation above on 4c216561c6, and the spec and runtime typecheck runs, both exit 0. Round 1 changes only string literals (the D3 entry and its registry mirror), comments and one changeset line. The type-check-debt gate re-measured at this head: "4 ledger entr(ies) … none above its recorded number".

Acceptance notes (observations, not filed)

  • RouterConfigSchema (spec/src/api/router.zod.ts) defaults basePath to /api with mounts.automation: '/automation'. That spec-only declaration has no runtime reader in this repo.
  • The contract lists nine of the 17 routes registerAutomationRoutes mounts. The ones not listed are resume, cancel, restore-suspension, screen, actions, connectors, _status and the legacy trigger form.
  • Every remaining /api/automation path at head 10b1176328, and why it stays (git grep -n "/api/automation\b", with the automation-api file-name hits filtered out):
    • .changeset/20034-automation-contract-api-v1-paths.md :5 and :15-:20: the FROM column of this PR's own FROM/TO table.
    • packages/adapters/hono/src/hono.test.ts:453-:454: the Hono adapter's own default prefix (/api), driven against a mocked dispatcher. It does not read the contract.
    • packages/runtime/src/automation-api-contract-mounts.test.ts:9: the pin's docblock, describing the drift it guards.
    • packages/spec/scripts/file-description.test.ts:878-:937: synthetic fixtures for the docblock-description extractor. They do not quote the contract.
    • packages/spec/src/api/router.test.ts:343: a custom-mounts fixture for RouterConfigSchema.
    • Released CHANGELOG.md entries: packages/client/CHANGELOG.md:3230, packages/runtime/CHANGELOG.md:11589, packages/services/service-automation/CHANGELOG.md:5802 and packages/spec/CHANGELOG.md:32723 quote GET /api/automation/:name/runs in the released ExecutionStatus-filter entry. These are release-owned and never edited in a code PR; an amendment would be a dedicated docs-only PR. packages/spec/CHANGELOG.md:14982 names the file automation-api.mdx, not a path.

Implemented in session session_019c3Hi6ZMU1p6m6aA6Bz45d (claim 5823821835; patch round 1 dispatched by the domain:spec seat 4).

… paths

The nine flow endpoints were declared under /api/automation while the
dispatcher mounts the automation door only at its prefix (default /api/v1)
plus /automation, so every declared path answered 404 on the default
composition. The nine path values, the Base path docblock, the endpoint
list and every in-file path quote (including the retired-cursor tombstone
text) move to /api/v1/automation; the spec pin moves with them.

A runtime test boots the dispatcher plugin with its default prefix and
requires every contract route to be mounted and to be a route-ledger row
under the documented /api/v1 wire prefix.

The pending runs-cursor changeset quoted the tombstone text and is
corrected to the new path.

Claude-Session: https://claude.ai/code/session_019c3Hi6ZMU1p6m6aA6Bz45d
Co-authored-by: Claude <noreply@anthropic.com>
Generated by `pnpm --filter @objectstack/spec gen:docs` after the
contract moved to /api/v1/automation.

Claude-Session: https://claude.ai/code/session_019c3Hi6ZMU1p6m6aA6Bz45d
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Sep 25, 2026
@github-actions

github-actions Bot commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/runtime, @objectstack/spec, touching 35 documentable anchor(s).

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

  • content/docs/ai/actions-as-tools.mdx (via /api/v1/automation (route, a path literal in a comment on a changed line; a path literal in createFlow; a path literal in listFlows; bridged from symbol getFlow — its route source's handler names it; bridged from symbol updateFlow — its route source's handler names it), /api/v1/automation/:name (route, a path literal in a comment on a changed line; a path literal in deleteFlow; a path literal in getFlow; a path literal in updateFlow; bridged from symbol createFlow — its route source's handler names it; bridged from symbol deleteFlow — its route source's handler names it; bridged from symbol triggerFlow — its route source's handler names it), /api/v1/automation/:name/runs (route, a path literal in RUNS_LIST_CURSOR_REMOVED; a path literal in a comment on a changed line; a path literal in acceptanceCriteria; a path literal in listRuns; a path literal in reason; a path literal in semantic; a path literal in surface; bridged from symbol getRun — its route source's handler names it), /api/v1/automation/:name/runs/:runId (route, a path literal in a comment on a changed line; a path literal in getRun), /api/v1/automation/:name/runs/:runId/resume (route, a path literal in a comment on a changed line), /automation/:name/runs (route, the route ledger binds it to client method automation.listRuns), /automation/:name/runs/:runId (route, the route ledger binds it to client method automation.getRun))
  • content/docs/api/declarative-endpoints.mdx (via /api/v1/automation (route, a path literal in a comment on a changed line; a path literal in createFlow; a path literal in listFlows; bridged from symbol getFlow — its route source's handler names it; bridged from symbol updateFlow — its route source's handler names it), /api/v1/automation/:name (route, a path literal in a comment on a changed line; a path literal in deleteFlow; a path literal in getFlow; a path literal in updateFlow; bridged from symbol createFlow — its route source's handler names it; bridged from symbol deleteFlow — its route source's handler names it; bridged from symbol triggerFlow — its route source's handler names it), /api/v1/automation/:name/trigger (route, a path literal in a comment on a changed line; a path literal in triggerFlow; bridged from symbol toggleFlow — its route source's handler names it))
  • content/docs/automation/approvals.mdx (via /api/v1/automation (route, a path literal in a comment on a changed line; a path literal in createFlow; a path literal in listFlows; bridged from symbol getFlow — its route source's handler names it; bridged from symbol updateFlow — its route source's handler names it), /api/v1/automation/:name (route, a path literal in a comment on a changed line; a path literal in deleteFlow; a path literal in getFlow; a path literal in updateFlow; bridged from symbol createFlow — its route source's handler names it; bridged from symbol deleteFlow — its route source's handler names it; bridged from symbol triggerFlow — its route source's handler names it), /api/v1/automation/:name/runs (route, a path literal in RUNS_LIST_CURSOR_REMOVED; a path literal in a comment on a changed line; a path literal in acceptanceCriteria; a path literal in listRuns; a path literal in reason; a path literal in semantic; a path literal in surface; bridged from symbol getRun — its route source's handler names it), /api/v1/automation/:name/runs/:runId (route, a path literal in a comment on a changed line; a path literal in getRun), /api/v1/automation/:name/runs/:runId/resume (route, a path literal in a comment on a changed line), /automation/:name/runs (route, the route ledger binds it to client method automation.listRuns), /automation/:name/runs/:runId (route, the route ledger binds it to client method automation.getRun))
  • content/docs/automation/connectors.mdx (via /api/v1/automation (route, a path literal in a comment on a changed line; a path literal in createFlow; a path literal in listFlows; bridged from symbol getFlow — its route source's handler names it; bridged from symbol updateFlow — its route source's handler names it), /api/v1/automation/:name (route, a path literal in a comment on a changed line; a path literal in deleteFlow; a path literal in getFlow; a path literal in updateFlow; bridged from symbol createFlow — its route source's handler names it; bridged from symbol deleteFlow — its route source's handler names it; bridged from symbol triggerFlow — its route source's handler names it))
  • content/docs/automation/flows.mdx (via getRun (symbol, a field of const object AutomationApiContracts), listRuns (symbol, a field of const object AutomationApiContracts), /api/v1/automation (route, a path literal in a comment on a changed line; a path literal in createFlow; a path literal in listFlows; bridged from symbol getFlow — its route source's handler names it; bridged from symbol updateFlow — its route source's handler names it), /api/v1/automation/:name (route, a path literal in a comment on a changed line; a path literal in deleteFlow; a path literal in getFlow; a path literal in updateFlow; bridged from symbol createFlow — its route source's handler names it; bridged from symbol deleteFlow — its route source's handler names it; bridged from symbol triggerFlow — its route source's handler names it), /api/v1/automation/:name/runs (route, a path literal in RUNS_LIST_CURSOR_REMOVED; a path literal in a comment on a changed line; a path literal in acceptanceCriteria; a path literal in listRuns; a path literal in reason; a path literal in semantic; a path literal in surface; bridged from symbol getRun — its route source's handler names it), /api/v1/automation/:name/runs/:runId (route, a path literal in a comment on a changed line; a path literal in getRun), /api/v1/automation/:name/runs/:runId/resume (route, a path literal in a comment on a changed line), /api/v1/automation/:name/trigger (route, a path literal in a comment on a changed line; a path literal in triggerFlow; bridged from symbol toggleFlow — its route source's handler names it), /automation/:name/runs (route, the route ledger binds it to client method automation.listRuns), /automation/:name/runs/:runId (route, the route ledger binds it to client method automation.getRun))
  • content/docs/protocol/kernel/http-protocol.mdx (via /api/v1/automation (route, a path literal in a comment on a changed line; a path literal in createFlow; a path literal in listFlows; bridged from symbol getFlow — its route source's handler names it; bridged from symbol updateFlow — its route source's handler names it), /api/v1/automation/:name (route, a path literal in a comment on a changed line; a path literal in deleteFlow; a path literal in getFlow; a path literal in updateFlow; bridged from symbol createFlow — its route source's handler names it; bridged from symbol deleteFlow — its route source's handler names it; bridged from symbol triggerFlow — its route source's handler names it), /api/v1/automation/:name/trigger (route, a path literal in a comment on a changed line; a path literal in triggerFlow; bridged from symbol toggleFlow — its route source's handler names it))
  • content/docs/ui/actions.mdx (via /api/v1/automation (route, a path literal in a comment on a changed line; a path literal in createFlow; a path literal in listFlows; bridged from symbol getFlow — its route source's handler names it; bridged from symbol updateFlow — its route source's handler names it))

⛔ 4 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v15.mdx (via /api/v1/automation (route, a path literal in a comment on a changed line; a path literal in createFlow; a path literal in listFlows; bridged from symbol getFlow — its route source's handler names it; bridged from symbol updateFlow — its route source's handler names it), /api/v1/automation/:name (route, a path literal in a comment on a changed line; a path literal in deleteFlow; a path literal in getFlow; a path literal in updateFlow; bridged from symbol createFlow — its route source's handler names it; bridged from symbol deleteFlow — its route source's handler names it; bridged from symbol triggerFlow — its route source's handler names it))
  • content/docs/releases/v17/17-0.mdx (via getRun (symbol, a field of const object AutomationApiContracts), listRuns (symbol, a field of const object AutomationApiContracts))
  • content/docs/releases/v17/17-1.mdx (via /api/v1/automation (route, a path literal in a comment on a changed line; a path literal in createFlow; a path literal in listFlows; bridged from symbol getFlow — its route source's handler names it; bridged from symbol updateFlow — its route source's handler names it), /api/v1/automation/:name (route, a path literal in a comment on a changed line; a path literal in deleteFlow; a path literal in getFlow; a path literal in updateFlow; bridged from symbol createFlow — its route source's handler names it; bridged from symbol deleteFlow — its route source's handler names it; bridged from symbol triggerFlow — its route source's handler names it), /api/v1/automation/:name/runs (route, a path literal in RUNS_LIST_CURSOR_REMOVED; a path literal in a comment on a changed line; a path literal in acceptanceCriteria; a path literal in listRuns; a path literal in reason; a path literal in semantic; a path literal in surface; bridged from symbol getRun — its route source's handler names it), /api/v1/automation/:name/runs/:runId (route, a path literal in a comment on a changed line; a path literal in getRun), /api/v1/automation/:name/runs/:runId/resume (route, a path literal in a comment on a changed line), /automation/:name/runs (route, the route ledger binds it to client method automation.listRuns), /automation/:name/runs/:runId (route, the route ledger binds it to client method automation.getRun))
  • content/docs/releases/v17/17-3.mdx (via /api/v1/automation (route, a path literal in a comment on a changed line; a path literal in createFlow; a path literal in listFlows; bridged from symbol getFlow — its route source's handler names it; bridged from symbol updateFlow — its route source's handler names it), /api/v1/automation/:name (route, a path literal in a comment on a changed line; a path literal in deleteFlow; a path literal in getFlow; a path literal in updateFlow; bridged from symbol createFlow — its route source's handler names it; bridged from symbol deleteFlow — its route source's handler names it; bridged from symbol triggerFlow — its route source's handler names it), /api/v1/automation/:name/toggle (route, a path literal in a comment on a changed line; a path literal in toggleFlow; bridged from symbol listRuns — its route source's handler names it))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 anchor(s) matched too much of the corpus to be a work list: /api/v1 (route, 88 pages)
  • 3 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 60 of 215 client-bound route-ledger rows — the other 155 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 155: 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; 100 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 — 141 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 b76aad5f6fac71cbbcd4ea6bfdf21a59e2fd4d56 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 244dd0322131a56c20485c489615199af0f72c1d — the merge of head 10b117632855d4ccf9bebd2cdde3f0dfb02c53f2 into base b76aad5f6fac71cbbcd4ea6bfdf21a59e2fd4d56, 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 244dd0322131a56c20485c489615199af0f72c1d && git checkout 244dd0322131a56c20485c489615199af0f72c1d
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin b76aad5f6fac71cbbcd4ea6bfdf21a59e2fd4d56 10b117632855d4ccf9bebd2cdde3f0dfb02c53f2 && git checkout -B drift-repro b76aad5f6fac71cbbcd4ea6bfdf21a59e2fd4d56 && git merge --no-ff 10b117632855d4ccf9bebd2cdde3f0dfb02c53f2

node scripts/docs-audit/affected-docs.mjs --json b76aad5f6fac71cbbcd4ea6bfdf21a59e2fd4d56

⚠️ 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 b76aad5f6fac71cbbcd4ea6bfdf21a59e2fd4d56 → 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: 77/77 CONTRACT_REVIEW_TIER
Head-sha: 4c216561c6b2cd90511c35d9a058f4f664ad6ba2

Isolated at-tier reviewer subagent, run by the domain:spec seat-4 session; every one of its 77 transcript turns served at the tier the constant names. First review of this head. For the DELIBERATE CORRECTION of .changeset/19365-automation-runs-cursor-hasmore.md this is the same-head at-tier PASS that names the note and judges the changed sentence; the maintainer's standing words in this seat's chat, 「changeset 你看着更新就行」, cover the correction of pending notes. Seat's note on (f): packages/spec/src/migrations/registry.ts is not a single-writer path (scripts/check-single-claim-paths.mjs declares only .objectui-sha, and its header names a shared registry as ordinary concurrent work), so the seat folds the automation-runs-cursor-retired entry, its mirror, the two comments and the reviewer's Clause-②: no reading into a patch round on this PR; the new head gets its own record. Adopted by the seat 2026-09-25T00:25Z. The record below is the reviewer's, unedited except the two header lines.

Inputs read: card #20034 body + comments 5823177456 (triage), 5823821835 (claim), 5824409816 (os-dev-report); origin comment 5822432366 on #19966 (route_measurement: 194 routes, 17 under /api/v1/automation, 0 under /api/automation; POST /api/automation 404, POST /api/v1/automation 200); pulls/20056 body, 6 files, diff (213+/59-), 1 bot comment (docs-drift advisory), 0 review comments; check-runs for the head (3 snapshots); refs origin/main = 7e6ca1787aa97a98dd88b1d6fe32ec1962655edc, refs/review/pr-20056 = head, merge-base adbbc5d01e76ac7ed561dec1c95592320acb8565; objectui read-only at pin 62597c588072636e9c30ea35b3d89b1e46fd765d. Nothing re-run; no repo file edited; nothing written to GitHub.

① Derived judgments

(a) The fix — correct and complete inside the file.

  • git grep -F /api/automation refs/review/pr-20056 -- packages/spec/src/api/automation-api.zod.ts exits 1 (0 hits); base had 38 hits (:14, :18-26, 19 body quotes, :658-706). Head has 38 /api/v1/automation quotes: Base path :14, endpoint list :24-32, section headers and @examples (:59, :65, :112, :132, :138, :153, :163, :175, :200, :223, :229, :486, :492, :514, :563, :614), the 7b resume docblock :407, :414, the RUNS_LIST_CURSOR_REMOVED tombstone :528, the nine path rows :664-712.
  • Method-by-method against origin/main dispatcher-plugin.ts (:909 const prefix = config.prefix || '/api/v1'; registerAutomationRoutes(prefix) :1746; base = prefix): listFlows GET ${base}/automation :1466; createFlow POST :1475; getFlow GET ${base}/automation/:name :1525; updateFlow PUT :1534; deleteFlow DELETE :1543; triggerFlow POST ${base}/automation/:name/trigger :1561; toggleFlow POST .../toggle :1570; listRuns GET .../runs :1579; getRun GET .../runs/:runId :1588. All nine mounted with the declared method. serve.ts:4412-4417 passes scoping, enforceProjectMembership, observability, rateLimit and no prefix.
  • Route ledger (origin/main route-ledger.ts): header :16-17 "prepend /api/v1 for the wire path"; rows GET /automation :428, POST /automation :429, POST /automation/:name/trigger :434, POST /automation/:name/toggle :435, GET /automation/:name/runs/:runId :457, GET /automation/:name/runs :458, GET /automation/:name :459, PUT :460, DELETE :462. Nine of nine.
  • Consumer-read text: automation-api.mdx regenerated (diff shows only the docblock and the cursor [REMOVED] description move). git grep -l -F "was removed from GET /api/v1/automation" refs/review/pr-20056 hits exactly 3 files (zod.ts, the mdx, the corrected 19365 note): no JSON Schema / api-surface artifact carries the describe text, so nothing else needed regenerating (api-surface/api.json:106 and export-origins/api.json:100 record the name only).
  • Tree-wide residue of /api/automation at head (CHANGELOGs excluded): hono.test.ts:453-454 (adapter's own /api default, see b), the pin's history docblock :9, comments query-param.ts:179 and run-list-truncation.test.ts:6, synthetic generator fixtures spec/scripts/file-description.test.ts:878-937, RouterConfigSchema custom-mounts fixture router.test.ts:343, and the ADR-0087 entry (see f). None is a published describe text, generated doc or JSON Schema description.

(b) Consumer search — the dev is right; remedy 1 is correct.

  • git grep -F AutomationApiContracts refs/review/pr-20056 outside the declaring file, its test and the new pin: only the new changeset :5, the mdx :19, api-surface/api.json:106, export-origins/api.json:100. No code reads the nine values.
  • packages/client/src/index.ts:7540 automation: '/api/v1/automation' (convention) and 18 getRoute('automation') calls; the two "comment mentions" the dev cites (index.ts:5395-5405, analytics-automation-json-erasure.test.ts:69) name automation-api.zod.test.ts, not the constant — immaterial.
  • Hono: packages/adapters/hono/src/index.ts:303 const prefix = options.prefix || '/api'; :725 app.all(${prefix}/*) delegates :739 dispatcher.dispatch(method, subPath, ..., prefix). hono.test.ts:440 asserts /api/analytics the same way as :453 /api/automation: the default is adapter-wide and moves every dispatcher family one segment. The 23 sibling /api/v1 rows (git grep -E "^\s*path: '/api/v1" origin/main -- packages/spec/src/api/ non-test = 23, in ExportApiContracts, ImportJobApiContracts, PackageApiContracts, StorageApiContracts; BatchApiContracts and StandardApiContracts declare no path rows) are equally off under that host, so the old automation values matched it by coincidence. Aligning this map with the other rows is the right answer; remedy 2 would record a property no other map has.
  • objectui at pin 62597c588072636e9c30ea35b3d89b1e46fd765d (git -C /home/user/objectui grep -c -F ... 62597c58...): AutomationApiContracts exit 1, /api/automation exit 1, positive control /api/v1/automation 33 files.

(c) The drift pin guards the real thing.

  • packages/runtime/src/automation-api-contract-mounts.test.ts:86-98: createDispatcherPlugin({ securityHeaders: false }) then plugin.start(ctx) (dispatcher-plugin.ts:836), which takes the recorder from ctx.getService('http.server') :839 and registers routes through it; the only non-verb call, server.use :870, sits under if (rateLimitMiddleware) :868 (null with no budget), so the recorder's get/post/put/delete/patch cover every call the plugin makes (grep -o "server!?\.[a-zA-Z]*(" on the file: get/post/put/delete/patch/use only). The mount leg compares METHOD path from the contract to the recorded registrations, with the prefix supplied by the plugin's own default, not the test.
  • Ledger leg :100-113 prefixes non-absolute rows with the literal /api/v1. The ledger exports no wire-prefix constant (header prose only, :17); the live-mount parity gate packages/qa/dogfood/test/route-ledger-live-mount-parity.dogfood.test.ts:89 uses the same literal DISPATCHER_PREFIX = '/api/v1'. So the ledger leg pins "each contract route is a ledger row", not the prefix.
  • Directions, from source: a contract-side drift reddens both legs; a dispatcher default-prefix drift reddens the mount leg only (ledger leg keeps comparing v1 to v1); a route removed from registerAutomationRoutes reddens the mount leg; a ledger row removed reddens the ledger leg; :82-84 refuses a vacuous map. The two ablation legs the dev reports (contract leg: 2 failed / 1 passed; dispatcher /api/v2 leg: 1 failed / 2 passed) are exactly what this reading predicts. Ablation not re-run here.
  • It is a test, not a gate: matched by vitest project local (packages/runtime/vitest.config.ts include: ['src/**/*.test.ts'], not in vitest.repo-tests.json), in the tsconfig.test.json program (include: ["src/**/*"]); @objectstack/spec/api resolves to spec src/api/index.ts through the anchored alias; the diff adds no package.json script, no workflow step (6 files only).

(d) Changeset 20034-automation-contract-api-v1-paths.md — grade right, declaration over-stated but not refused.

  • '@objectstack/spec': minor :2; no **BREAKING banner, Clause-②: yes bare :9. check-adr-0087-registration.mjs:629-641 breakingDeclaration reads BREAKING banner / major / ! summary / (narrowing) arm — none present, so non-breaking, no ADR-0087 marker owed. check-changeset-no-major.mjs level axis (:1851 verdict line): yes needs a moved-src package graded at least minor; spec is minor. Green on both.
  • Not breaking is right under AGENTS.md :1069-1076: "breaking" there is removing or renaming something an author can write; path is read, not authored; its type stays string (only method is as const, zod.ts :663-664); no accept set narrows. Precedent .changeset/18058-install-door-contract-rebind.md:15 rebinds PackageApiContracts.installPackage.path at minor with the same "a caller reading the constant was building a URL nothing served" reasoning.
  • Clause-② reading: the reader's question is 「本卡放宽接受集或扩大公开面吗」 (scripts/pm/clause2-line.mjs:70), glossed "a new key on a published payload" (check-changeset-no-major.mjs:7-8). This diff adds no key, widens no accept set and grows no export; the 18058 precedent's yes (widening) covered a real overwrite? key and new exports, which this PR has no analogue of. The dev's report concedes yes "holds only in the sense that the served surface grows from 0 to 9 reachable routes". Truthful reading: Clause-②: no (bare; not (narrowing), nothing authorable is removed). minor remains valid under no (a published constant's value moves; the bug-fix rule sets patch as the floor, not the ceiling, and the launch-window guard says a commit type may raise a bump). Nothing refuses yes + minor: non-blocking, recorded under ②.
  • Sentences: "prefix defaults to /api/v1, objectstack serve passes none" true (:909, serve.ts:4412-4417); "all nine ... 404 / same under /api/v1/automation 200" true by the measured route table (0 routes under /api/automation) plus the POST/GET probes; "generated API reference printed the nine unserved paths" true (base mdx :17-27); "every other *ApiContracts map already carried /api/v1" true (7 maps; the 24 prefix-relative and 3 empty path rows are in plugin-rest-api.zod.ts, not a *ApiContracts map); table true; tombstone sentence true (:528); "path type unchanged (string)" true; Hono sentence true (:303, :725-739); "the only one served under projectResolution: 'required'" holds under enableProjectScoping: true (dispatcher-plugin.ts:1741 if (enableProjectScoping && projectResolution === 'required')), which is the only scope the config docblock gives projectResolution (:45) and how the cited test configures it (dispatcher-plugin.required-scoping-mounts.integration.test.ts:66) — precise enough, noted; "Kept from drifting again" true (c).

(e) Deliberate correction of 19365-automation-runs-cursor-hasmore.md:32 — byte-exact, sole, nothing else falsified.

  • Head :32-33: throws-quote of "cursor was removed from GET /api/v1/automation/:name/runs in" then "@objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) …". Tombstone at head zod.ts :528-529: "cursor was removed from GET /api/v1/automation/:name/runs in @objectstack/spec 17.5.0 " + "(ADR-0049 enforce-or-remove) — ...". The quoted span is identical modulo the note's line wrap and closing ellipsis. Diff is the single line.
  • git grep -F /api/automation refs/review/pr-20056 -- '.changeset/*.md' hits only the new 20034 note's from/to table: no other pending note quoted the old path.
  • Rest of 19365 unaffected: :8 names the ledger form GET /automation/:name/runs; :56, :59 GET …/automation/f/runs; :115 Clause-②: yes; :117 adr-0087 marker.
  • Check Changeset red as designed: annotation on .changeset/19365-...md names the finding: random changeset filenames collide silently across parallel agents — a round overwrote a sibling PR's minor changeset and every gate stayed green #17712 foreign-changeset rule and the DELIBERATE CORRECTION remedy "do NOT restore it -- say so on the PR and get it confirmed" (check-empty-changeset.mjs:551, :563); the PR body says so. This record is that confirmation.

(f) Things left false — verified; acceptable as a follow-up, but it needs a carrier.

② Semver level

  • @objectstack/spec minor, not declared breaking: correct under AGENTS.md :1069-1076 and the 18058 precedent; check-adr-0087-registration reads non-breaking (no marker owed); check-changeset-no-major level axis is satisfied. No runtime changeset owed: the runtime change is a test file, excluded from the published artifact.
  • Clause-② line (PR body line 2 and changeset :9): yes bare is over-declared — no new key, no accept-set widening, no new export; by the reader's own question the truthful line is Clause-②: no (bare). No gate and no rule text refuses yes with minor, and minor stays valid under no. Non-blocking; recommend correcting both carriers (PR body, changeset :9) when the branch is next touched.

③ Boundary flags

  • Non-blocking — file outside the claimed surface: .changeset/19365-automation-runs-cursor-hasmore.md (one-line deliberate correction), named and justified in the PR body and report, as the claim's "stop on breach; explain in the report" requires.
  • Non-blocking — CI state at review (check-runs for 4c216561c6b2cd90511c35d9a058f4f664ad6ba2, total 33): 24 success (incl. Build Core, Build Docs, Type Check · source gates, Type Check · debt ledger, Dogfood Regression Gate 1/3 and 2/3, Dogfood Verify CLI, Spec property liveness, Governed Surface Queue Guard, Check Documentation Links, the four claim/single-writer guards), 2 skipped (Console Pin Gate, Packed-tarball smoke), 1 failure (Check Changeset — by design, e), 6 in progress (Test Core 1/6, 4/6, 5/6, 6/6, Type Check · workspace, Lint & Repo Gates). The pin's own pass and check:generated/check:docs sync rest on the dev's local runs until those complete; nothing was re-run here. Governed surface (PD feat: Comprehensive CRM example demonstrating all ObjectStack protocol features #14): landing waits for full green plus this record; PR is a draft, mergeable_state blocked.
  • Non-blocking — branch not merged with origin/main before the PR (AGENTS.md multi-agent §10 :568-571): merge-base adbbc5d01e, main 5 commits ahead; git diff --stat adbbc5d01e origin/main -- (the 6 files, dispatcher-plugin.ts, route-ledger.ts) is empty. The queue's rebuilt generation covers it.
  • Non-blocking — commit trailers on f4312f8eeb and 4c216561c6 are Claude-Session: plus a model-free Co-authored-by: Claude line, not the harness's model-bearing form; dev-declared deviation.
  • Non-blocking — follow-up carrier owed for the ADR-0087 entry automation-runs-cursor-retired (f) and, at whoever's convenience, the two comments query-param.ts:179, run-list-truncation.test.ts:6.
  • Non-blocking — report accuracy: the dev's "two client comments mention the contract" refer to automation-api.zod.test.ts, not AutomationApiContracts; the consumer conclusion is unaffected.
  • Non-blocking — docs-drift bot (comment 5824393761) lists 7 hand-written pages naming /api/v1/automation; they already carry the v1 form (triage 5823177456 checked declarative-endpoints.mdx, approvals.mdx, connectors.mdx, actions-as-tools.mdx), so no edit is owed; release pages are read-only.

Implemented-by: claude/issue-20034-automation-contract-api-v1-paths
Reviewed-by: session_019c3Hi6ZMU1p6m6aA6Bz45d

VERDICT: PASS


Generated by Claude Code

…v1 runs path

Patch round 1 on the AutomationApiContracts path fix:

- The ADR-0087 D3 entry automation-runs-cursor-retired named
  GET /api/automation/:name/runs three times; it now names
  GET /api/v1/automation/:name/runs, the path the contract publishes
  and the dispatcher mounts. registry.ts regenerated with
  gen:migration-registry; gen:upgrade-guide and gen:spec-changes
  re-run with no byte change (the entry is in step 18).
- Two comments (runtime query-param.ts, service-automation
  run-list-truncation.test.ts) quote the same path in the v1 form.
- The changeset declares Clause-② no: no key added, no accept set
  widened, no export grown. The level stays minor.

Claude-Session: https://claude.ai/code/session_019c3Hi6ZMU1p6m6aA6Bz45d
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: 127/127 CONTRACT_REVIEW_TIER
Head-sha: 10b117632855d4ccf9bebd2cdde3f0dfb02c53f2

Isolated at-tier reviewer subagent, run by the domain:spec seat-4 session; every one of its 127 transcript turns served at the tier the constant names. Second review, of the head after the PM patch round. Its one blocking item is a false sentence in the seat-written PR body (the stale-tree bullet), not in the code; the seat corrects the body in the next act and asks the same reviewer to re-check it on this unchanged head. Adopted by the seat 2026-09-25T01:51Z. The record below is the reviewer's, unedited except the two header lines.

Inputs read: card #20034 body + comments 5823177456, 5823821835, 5824409816, 5824987999 (round-1 os-dev-report); pulls/20056 (draft, mergeable_state blocked, head = the sha above, base main at 7e6ca1787a), its 10 files, 3 commits, 2 issue comments incl. the at-tier PASS 5824559177 in full; commits/10b1176328…/check-runs read 2026-09-25T01:17:25Z (39 runs) plus both Check Changeset jobs' step lists and annotations; pulls/20031 + files; refs origin/main = b76aad5f6fac71cbbcd4ea6bfdf21a59e2fd4d56, refs/review/pr-20056 = head, merge-base adbbc5d01e76ac7ed561dec1c95592320acb8565; at origin/main: build-migration-registry.ts, check-changeset-no-major.mjs, check-adr-0087-registration.mjs, pm/clause2-line.mjs, check-single-claim-paths.mjs, AGENTS.md, pr-automation.yml, lint.yml, eslint.config.mjs. Every git read via git show/git grep/git diff on refs; nothing re-run; no repo file edited; nothing written to GitHub. Scratch only under …/scratchpad/pr-20056/review2/.

① Derived judgments

(a) Round-1 diff is exactly the list — nothing missing, nothing beyond. git diff --stat 4c216561c6 10b1176328: 5 files, 9 insertions, 9 deletions: .changeset/20034-automation-contract-api-v1-paths.md :9 (Clause-②: yes to Clause-②: no), packages/runtime/src/query-param.ts :179 (comment), packages/services/service-automation/src/run-list-truncation.test.ts :6 (comment), packages/spec/src/migrations/entries/semantic/18.automation-runs-cursor-retired.ts :11/:43/:72, packages/spec/src/migrations/registry.ts :5943/:5975/:6004. Each hunk moves one GET /api/automation/:name/runs to GET /api/v1/automation/:name/runs and nothing else. One commit, 10b1176328^ = 4c216561c6. PR files list = 10 (the 6 from round 0 + these; the two spec files were not in round 0).

(b) D3 entry — the three sentences are true at the head; nothing else falsified; mirror byte-identical to its generator.

  • :11 "GET /api/v1/automation/:name/runs declared by ListRunsRequestSchema": automation-api.zod.ts:565 declares ListRunsRequestSchema; the listRuns row :704-706 is method: 'GET' as const, path: '/api/v1/automation/:name/runs'; dispatcher-plugin.ts:1579 server!.get(${base}/automation/:name/runs, …) with base = prefix (:1746) = config.prefix || '/api/v1' (:909); dispatcher-plugin.ts and route-ledger.ts identical between head and origin/main (git diff --stat empty). :43 and :72 name the same door; only the path segment moved. git grep -c -E "/api/automation\b" head -- automation-api.zod.ts exits 1 (0 hits); tombstone :528 names the v1 path.
  • Entry id :6 'automation-runs-cursor-retired' unchanged; filename = shardNameFor(18, id); .changeset/19365-automation-runs-cursor-hasmore.md:117 carries the adr-0087 registered automation-runs-cursor-retired marker, unchanged (file is 117 lines; round-0's :32 correction stands). Every tree reference to the id: 19365 :117, the retired-key sibling 18.api__ListRunsRequest__cursor.ts:29 (a comment naming the entry, no path), the entry :6, registry.ts:5938 and :14518 (same comment mirrored).
  • Re-derivation with the generator's own rules (parseEntry :158-223: leading // run above export const entry is empty because :4 is blank; literal = :5 after = through :107 with the trailing ; stripped, 103 lines; renderRegion :258-267: 4-space indent, trailing comma; loadEntries sort by major then id :253): region = registry.ts:5937-6039, 103 lines, byte-identical (sha256 303d8a29… on both), sorted between audience-posture-default-invite-only (:5915) and autonumber-default-unique-organization (:6041). Whole-file derivation over the head's 242 semantic / 210 retired-key / 183 retired-def entry files spliced into the head's marked regions is byte-identical to the head's registry.ts (1,544,532 bytes), the counts matching the dev's check:migration-registry line. git diff on registry.ts is exactly the three lines.
  • Rendered artifacts: protocol-version.ts:18 PROTOCOL_VERSION = '17.0.0', :21 PROTOCOL_MAJOR parsed from it; build-upgrade-guide.ts:78 and build-spec-changes.ts:254 both loop major <= PROTOCOL_MAJOR, so a step-18 entry cannot reach either; at head git grep -c automation-runs-cursor-retired -- spec-changes.json docs/protocol-upgrade-guide.md exits 1, the guide's last heading is "Protocol 16 → 17", spec-changes.json contains no "18". The "no byte change" claim derives from source. MIGRATIONS_BY_MAJOR :13901-13903 still carries 18: step18, so the corrected text is what ships as data.

(c) registry.ts is not a single-writer path; #20031 does not overlap. check-single-claim-paths.mjs:324-333 SINGLE_CLAIM_PATHS = .objectui-sha only; :319-322 says "a shared registry does NOT belong here". The previous record's (f)(ii) premise ("generated single-writer file held by #20031") was wrong on the single-writer half — the seat's header note on 5824559177 already said so; the "outside the claim surface" half stands, and round 1 was the seat's answer. Overlap: #20031 (open, head 66417af31f, files: .changeset/flow-edge-condition-evaluated-slot.md, 18.flow-edge-condition-evaluated-slot-source-required.ts, registry.ts) has registry.ts hunks @@ -9065 and @@ -9107; this PR's are @@ -5940, -5972, -6001; different entry files, different regions. The No other open PR may claim the same single-writer path check-run is success on this head.

(d) Clause-②: no (bare) — consistent with all three readers; minor valid.

  • clause2-line.mjs:70 question 「本卡放宽接受集或扩大公开面吗」; :91-99: yes at least minor, no (narrowing) breaking, no (widening) malformed, bare no declares no direction. A port of readClause2Line (CLAUSE2_KEY_LINE :124, matchValueToken :222-232, readArmToken :280-293, clause2LineDescribes :359-373) over the head carriers: PR body line 2 reads declared / no / arm null (the three later mentions at body lines 10, 58, 89 are inline prose and never key-initial); changeset :9 reads declared / no / arm null; 19365 :115 reads yes (inherited, untouched).
  • check-changeset-no-major.mjs: judgeLevel :1547-1647 returns not-declared for a bare no (control :2957); renderLevel :1845-1847 prints "✓ LEVEL AXIS: this PR declares clause-② no, so no package here is declared to have grown a published surface." and exits 0 — the line the PR body quotes.
  • check-adr-0087-registration.mjs breakingDeclaration :629-641: signals are a major bump, a **BREAKING banner, a !: summary, or a narrowing arm read by the imported readClause2Line(parsed.body) :639. Changeset 20034: minor, no banner, summary opens with a backtick, arm null — non-breaking, pushed to skipped (:3831). 19365 carries **BREAKING** at :15 but was already breaking at the branch point, so :3848-3852 inherits it into skipped. skipped.length = 2 — :7224 prints "adds no declared-breaking changeset (2 non-breaking changeset(s) seen)", the line the PR body quotes. No marker owed by 20034.
  • AGENTS.md :1071-1073: a bug fix takes at least patch; yes takes at least minor; (narrowing) is BREAKING; no (widening) malformed. no bare with minor is refused by nothing. Truthfulness: the PR adds no key, no export (no api-surface file in the 10; check:api-surface "unchanged"), narrows no accept set (path stays string); a published const's value moves, which is the 18058 precedent's minor. The previous record's ② recommendation is implemented on both carriers.

(e) PR body — one factual sentence is false; the rest checks.

  • True: the "Patch round 1" bullets (lines/positions :11/:43/:72, :5943/:5975/:6004, regeneration command = package.json:276, gen:upgrade-guide/gen:spec-changes no-byte-change by (b), MIGRATIONS_BY_MAJOR[18], fix(spec): the edge-condition upgrade entry and its pending changeset name POST /api/v1/automation, not the nonexistent POST /flows #20031 "different region" by (c), the two comment lines, clause2-line.mjs:70, 19365 :117 marker); the changeset-level paragraph; "7 touched TS files" (count from the 10 files = 7); "all seven parserOptions blocks are { ecmaVersion, sourceType }" (eslint.config.mjs :984-:1242, seven blocks plus one comment at :328, no project/projectService); check:migration-registry "242/210/183" by (b); the two gate quotations by (d); "this gate reads the arm from the changeset body" (:639); Acceptance list of remaining hits = the tree-wide git grep -n -E "/api/automation\b" at head exactly, once file-name hits (automation-api.zod, automation-api.mdx, api/automation-api) are set aside; "nine of the 17 routes" (17 ${base}/automation… registrations at :1466-:1641; the eight unlisted are the ones named). Suite/build counts are the dev's and were not re-run.
  • False: Verification, stale-tree bullet: "None of this PR's 10 paths is touched by those commits." git diff --stat adbbc5d01e 3557f85fa5 -- (the 10 paths) → packages/spec/src/migrations/registry.ts | 49 (26+/23-), from 0bf85eaae621dd3329629a8caba18447cb7fd3d7 (chore(objectui): bump the console pin to f8a9d0fb0596 (carries objectui#10221 and the objectui#9910 re-cut) with re-measured pin citations and the lockstep re-record #20036, committed 2026-09-24T23:52:44Z, inside the range), which regenerated six unit-in-key entries at hunks @@ -9764, -9955, -10099, -12593, -12788, -12888. The dev's report carried the same sentence; the seat copied it. Also "at least 12 commits behind": git log --oneline adbbc5d01e..3557f85fa5 | wc -l = 11 (quoted tool phrase; imprecision only). Consequence measured: none of those hunks touches :5940-:6004; git merge-tree --write-tree origin/main head yields a clean tree aa6f101a44… (main has since also landed 5b9402d89b fix(spec,objectql)!: retire scale from the currency field type — refused at parse, no longer enforced on writes #19909, a new entry at @@ -8330); the merged registry.ts (243 semantic, 1,548,740 bytes) re-derives byte-identical from the merged entries with this PR's three lines at merged :5943/:5975/:6004. So the landing is safe, but the sentence a landing reader would use to skip the AGENTS.md §10 overlap re-check is wrong.

(f) Nothing round 1 should have fixed and did not. Tree-wide residue at head is the Acceptance list; a second sweep git grep -n -E "[^/a-zA-Z]api/automation\b" (excluding CHANGELOGs, export-origins, file names) exits 1; both Base path lines (zod.ts:14, automation-api.mdx:13) are v1; 19365 :8, :56, :59 use the ledger/elided forms and were never affected; the retired-key sibling names no path; spec-changes.json and the upgrade guide carry nothing. Released CHANGELOG lines (client :3230, runtime :11589, service-automation :5802, spec :32723) are release-owned. The previous record's owed "follow-up carrier" for the D3 entry and the two comments is discharged by this round.

② Semver level

@objectstack/spec minor, not declared breaking — correct under AGENTS.md :1071-1076 (nothing an author can write is removed or renamed; path type stays string) and the 18058 precedent; check-adr-0087-registration reads it non-breaking (no marker owed; 19365's breaking-ness is inherited, not introduced); check-changeset-no-major level axis stands down on the bare no and the launch-window guard sees no major. Clause-②: no (bare) on both carriers is the truthful reading of clause2-line.mjs:70 — no key, no accept-set widening, no export; not (narrowing) since nothing authorable is removed. No runtime or service-automation changeset owed (test file and comments only).

③ Boundary flags

  • Blocking — a false factual sentence in the PR body (e): "None of this PR's 10 paths is touched by those commits." Remedy is a body edit only, no push: e.g. "One of the 10, packages/spec/src/migrations/registry.ts, was regenerated on main by chore(objectui): bump the console pin to f8a9d0fb0596 (carries objectui#10221 and the objectui#9910 re-cut) with re-measured pin citations and the lockstep re-record #20036 (0bf85eaae6, six unit-in-key entries, hunks :9764–:12891) and since by fix(spec,objectql)!: retire scale from the currency field type — refused at parse, no longer enforced on writes #19909 (5b9402d89b, :8330); none overlaps this PR's :5940–:6004, git merge-tree of origin/main and the head is clean, and the merged registry.ts regenerates byte-identical." Optionally correct "at least 12" to 11 for that range.
  • Non-blocking — CI at read time 2026-09-25T01:17:25Z, 39 check-runs on the head: 20 success (Build Docs, Check Documentation Links, Dogfood Verify CLI, Governed Surface Queue Guard, Spec property liveness, Temporal Conformance, Type Check · debt ledger, Type Check · source gates, the four claim/single-writer guards twice, Auto Label, Check PR Size, Flag docs, filter), 4 skipped (Console Pin Gate, Packed-tarball smoke, and the body-edit re-run's Auto Label / Check PR Size), 2 failure (both Check Changeset: push run 36080792955 at 01:09:45Z and the body-edit run 36081204859 at 01:15:20Z, each failing at step 12 "Reject an empty-frontmatter changeset" with the single annotation on .changeset/19365-…md naming the DELIBERATE CORRECTION remedy — by design, round 0's (e)), 13 in progress (Build Core, Test Core 1–6, Dogfood Regression Gate 1–3, Lint & Repo Gates, Type Check · consumer gates, Type Check · workspace). Draft, mergeable_state blocked; governed-surface landing waits for green plus this record.
  • Non-blocking, structural — the two Clause-② readers never run in CI on this PR: steps 13 "Require an ADR-0087 disposition" and 15 "Guard against accidental major bumps" are skipped in both jobs because step 12 fails and neither carries always() (pr-automation.yml:893-897, :933-937, :1069-1075); lint.yml:3528-3540 runs only their self-tests. The evidence for those two gates on this head is the dev's offline --event runs plus the source derivation in (d). A landing reader should not wait for a CI tick that this changeset class cannot produce.
  • Non-blocking — branch 13 commits behind origin/main, main touched registry.ts twice (above); not merged before the round (AGENTS.md §10 :568-571, dev-declared deviation). Merge derived clean and regeneration-current; the queue's rebuilt generation covers the rest.
  • Non-blocking — the round-1 commit's trailers are Claude-Session: plus a model-free Co-authored-by: Claude line, as in round 0 (dev-declared deviation).
  • Non-blocking — previous record's (f)(ii) "single-writer" premise corrected by (c); its judgment that the entry fix belonged in a follow-up is superseded by the seat's fold-in, which this record reviewed.
  • Non-blocking — docs-drift bot comment 5824393761 unchanged since round 0; the seven hand-written pages already carry the v1 form.

Implemented-by: claude/issue-20034-automation-contract-api-v1-paths
Reviewed-by: session_019c3Hi6ZMU1p6m6aA6Bz45d

VERDICT: FAIL — one blocking item: the PR-body sentence "None of this PR's 10 paths is touched by those commits" is false (registry.ts was regenerated on main by #20036 inside the cited range; evidence in (e)). Every other judgment passes: the round-1 diff is exactly the list; the D3 entry and its regenerated mirror are true and byte-identical to the generator; registry.ts is not a single-writer path and #20031's region is disjoint; Clause-②: no bare with minor is the truthful declaration and is refused by no gate or rule; nothing else naming the unversioned path ships as data or docs. A body edit correcting that sentence (text offered under ③) is the whole remedy; no push is needed, and this record can be re-issued as PASS on the same head once the body is corrected.


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: 162/162 CONTRACT_REVIEW_TIER
Head-sha: 10b117632855d4ccf9bebd2cdde3f0dfb02c53f2

Isolated at-tier reviewer subagent, run by the domain:spec seat-4 session; every one of its 162 transcript turns served at the tier the constant names. Re-issue on the unchanged head, after the seat corrected the one sentence the first record of this head (5825376693) found false. The turn count covers both passes of the same reviewer transcript. This record governs head 10b1176328. Adopted by the seat 2026-09-25T01:59Z. The record below is the reviewer's, unedited except the two header lines.

Re-issue after the seat's body edit. Inputs read: everything the first record (5825376693, 01:52:07Z) read, plus: pulls/20056 re-read 2026-09-25T01:53:34Z (head still 10b117632855d4ccf9bebd2cdde3f0dfb02c53f2, draft, mergeable_state unstable, body 16072 bytes, updated_at 01:52:59Z); a diff of the saved body against the new one; origin/main re-fetched to 66960564d9b628c446be3c8f8a8f0217c367a17f; refs/review/pr-20056 re-fetched (unchanged); commits/10b1176328…/check-runs re-read 01:55:35Z (49 runs) plus the third Check Changeset job's steps and annotations; the three PR comments; at origin/main: scripts/pm/dispatch-gates.mjs (DERIVATION_SURFACE :15722, driftLines :15942-15968, rev-list/three-dot at :15804-15809) and lint.yml:368-386. Nothing re-run; no repo file edited; nothing written to GitHub.

Body change is exactly one line. diff of the previous body against the current one: line 87 only (the stale-tree bullet under Verification). Line 2 still Clause-②: no; every other sentence byte-identical. Round-1 diff, D3 entry, registry mirror, changesets and comments are unchanged (same head).

① Derived judgments

(a) Round-1 diff is exactly the list — nothing missing, nothing beyond. Unchanged from 5825376693: git diff --stat 4c216561c6 10b1176328 = 5 files, 9+/9-: .changeset/20034-…md:9, query-param.ts:179, run-list-truncation.test.ts:6, 18.automation-runs-cursor-retired.ts:11/:43/:72, registry.ts:5943/:5975/:6004; one commit, parent 4c216561c6.

(b) D3 entry true at head; mirror byte-identical to its generator. Unchanged: :11 names ListRunsRequestSchema (automation-api.zod.ts:565) and the listRuns row :704-706 (GET, /api/v1/automation/:name/runs), mounted by dispatcher-plugin.ts:1579 server!.get(${base}/automation/:name/runs) under prefix = config.prefix || '/api/v1' (:909, :1746); id :6 unchanged; 19365 :117 marker names it; region registry.ts:5937-6039 re-derived with parseEntry/renderRegion/sort-by-id byte-identical (sha256 303d8a29…), whole file byte-identical over 242/210/183 entries; guide and spec-changes bounded by major <= PROTOCOL_MAJOR (17) so step 18 reaches neither; MIGRATIONS_BY_MAJOR[18] ships the corrected text as data.

(c) registry.ts is not a single-writer path; #20031 disjoint. Unchanged: SINGLE_CLAIM_PATHS = .objectui-sha only (:324-333); #20031's hunks @@ -9065/-9107 vs this PR's -5940/-5972/-6001.

(d) Clause-②: no (bare) consistent with all three readers; minor valid. Unchanged: reader port gives declared / no / arm null on body line 2 and changeset :9; judgeLevel → not-declared, exit 0 (:1647, :1846); breakingDeclaration finds no signal on 20034 and inherits 19365's **BREAKING** (:3848-3852), so skipped.length = 2; AGENTS.md :1071-1073 refuses nothing here.

(e) PR body — the false sentence is gone; the replacement bullet's claims measure true, with two precision notes.

(f) Nothing round 1 should have fixed and did not. Unchanged: residue sweeps exit 1 beyond the Acceptance list; both Base path lines v1; rendered artifacts carry nothing.

② Semver level

Unchanged: @objectstack/spec minor, not declared breaking — correct under AGENTS.md :1071-1076 and the 18058 precedent; check-adr-0087-registration non-breaking (no marker owed; 19365 inherited); check-changeset-no-major level axis stands down on the bare no, no major. Clause-②: no (bare) on both carriers is the truthful reading of clause2-line.mjs:70. No runtime or service-automation changeset owed.

③ Boundary flags

  • Non-blocking — the first record's blocking item is discharged: the false sentence "None of this PR's 10 paths is touched by those commits" no longer exists in the body; its replacement names the file, the three commits, the count, the clean merge and the measurement, all verified above.
  • Non-blocking — two precision notes on the new bullet, (e) notes 1 and 2: mechanism ("regenerates" vs re-derives-and-refuses) and scope ("that range" for a derivation taken at 11 commits). Neither is a false claim about the PR's content; the material facts are independently derived here. Wording offered; a body edit, no push.
  • Non-blocking — CI at read time 2026-09-25T01:55:35Z, 49 check-runs on the head: 40 success, now including Build Core, Test Core 1–6 and its aggregate, Lint & Repo Gates, Type Check · workspace, Type Check · consumer gates, Type Check · source gates, Type Check · debt ledger, TypeScript Type Check, Dogfood Regression Gate 1–3 and aggregate, Dogfood Verify CLI, Build Docs, Temporal Conformance, Spec property liveness, Governed Surface Queue Guard, the four claim/single-writer guards; 6 skipped (Console Pin Gate, Packed-tarball smoke, and Auto Label / Check PR Size on the two body-edit re-runs); 3 failure, all Check Changeset (push run 36080792955, body-edit runs 36081204859 and 36083927256), each failing at step 12 with the single 19365 DELIBERATE CORRECTION annotation — by design. Nothing else in progress. Draft; mergeable_state unstable (that one context). Governed-surface landing waits for this record.
  • Non-blocking, structural — steps 13 (ADR-0087) and 15 (launch-window / level axis) skipped in all three Check Changeset jobs (no always(); pr-automation.yml:893-897, :933-937, :1069-1075); lint.yml runs only their self-tests. The Clause-② readers' evidence on this head is the dev's offline --event runs plus the source derivation in (d); no CI tick for them will appear while 19365's correction stands.
  • Non-blocking — branch 15 commits behind origin/main, main regenerated registry.ts three times; not merged before the round (AGENTS.md §10 :568-571, dev-declared). Merge derived clean and the merged registry current; check:migration-registry in the queue would refuse otherwise.
  • Non-blocking — the round-1 commit's trailers are Claude-Session: plus a model-free Co-authored-by: Claude line, as in round 0 (dev-declared).
  • Non-blocking — previous record's (f)(ii) "single-writer" premise corrected by (c); the seat's fold-in of the entry fix is what this record reviewed.
  • Non-blocking — docs-drift bot comment 5824393761 unchanged; the seven hand-written pages already carry the v1 form.

Implemented-by: claude/issue-20034-automation-contract-api-v1-paths
Reviewed-by: session_019c3Hi6ZMU1p6m6aA6Bz45d

VERDICT: PASS — same head 10b117632855d4ccf9bebd2cdde3f0dfb02c53f2; the body's only change is the corrected stale-tree bullet, whose count (15), three registry.ts commits (0bf85eaae6, 5b9402d89b, 66960564d9), clean merge-tree (8bb2dc6ba4…, merged registry byte-identical to its regeneration, this PR's region untouched) and attribution to 5825376693 all measure true. Two precision notes on that bullet (queue re-derives rather than regenerates; the derivation's one-file finding is at its 11-commit range, a second surface file changed by the 15th) are non-blocking, with wording offered. Round-1 diff, D3 entry and mirror, Clause-② line and minor level stand as in the first record; CI is complete and green except the by-design Check Changeset red on the 19365 deliberate correction.


Generated by Claude Code

@os-litant
os-litant marked this pull request as ready for review September 25, 2026 02:00
@os-litant
os-litant enabled auto-merge September 25, 2026 02:00
@os-litant
os-litant added this pull request to the merge queue Sep 25, 2026
Merged via the queue into main with commit 1df29df Sep 25, 2026
47 of 50 checks passed
@os-litant
os-litant deleted the claude/issue-20034-automation-contract-api-v1-paths branch September 25, 2026 02:41
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/m tests tooling

Projects

None yet

2 participants