fix(spec): the edge-condition upgrade entry and its pending changeset name POST /api/v1/automation, not the nonexistent POST /flows - #20031
Conversation
…ition upgrade entry
The ADR-0087 D3 entry flow-edge-condition-evaluated-slot-source-required
named `POST /flows` in its `surface` and `acceptanceCriteria`, and the
pending changeset for the same change repeated it. No such route is
mounted. The door a flow definition is created through on a composed
runtime is `POST /api/v1/automation` (dispatcher-plugin mounts
`${prefix}/automation` with the default `/api/v1` prefix; route ledger
row `POST /automation`, client `automation.create`).
Entry text only; `migrations/registry.ts` regenerated with
`gen:migration-registry`. No runtime or schema change.
Claude-Session: https://claude.ai/code/session_019c3Hi6ZMU1p6m6aA6Bz45d
Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 7 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 3 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 136 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin b5b97a1b17dcd1d31672501eef5134e452597e45 && git checkout b5b97a1b17dcd1d31672501eef5134e452597e45
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 9d81af714f4938f368909322ac8eb2c24276598a 66417af31f21671b927f69d9feb1d5af488e24ec && git checkout -B drift-repro 9d81af714f4938f368909322ac8eb2c24276598a && git merge --no-ff 66417af31f21671b927f69d9feb1d5af488e24ec
node scripts/docs-audit/affected-docs.mjs --json 9d81af714f4938f368909322ac8eb2c24276598a
|
… ships in Two sentences of the pending flow-edge-condition-evaluated-slot note were true of their own change but are overtaken by sibling notes in the same pending release. - The whitespace-only `config.condition` ruling is untouched by this change, but the service-automation and lint notes in that release refuse the value at `registerFlow` and at `objectstack validate`. The sentence now says so and points at them. - The start node's `config.condition` producer-side gate is no longer the shape refusal alone: the structural pass at both doors follows it with a blank-source check running `EvaluatedExpressionInputSchema`. Changeset text only. Claude-Session: https://claude.ai/code/session_019c3Hi6ZMU1p6m6aA6Bz45d Co-authored-by: Claude <noreply@anthropic.com>
Maintainer confirmation — the DELIBERATE CORRECTION of
|
Contract reviewServed-tier: 94/94 Isolated at-tier reviewer subagent, run by the ① Derived judgmentsInputs read. Card #19966 body + all 4 comments (triage (a) The route — holds, from source at the PR ref.
(b) The DELIBERATE CORRECTION — exactly one note, all three sentences true.
(c) Check Changeset red and its handling — as prescribed.
(d) Scope — nothing beyond, nothing missing. (e) PR body — factual sentences hold. Every file:line in items 2–3 and the acceptance notes verified above ( ② Semver levelNo changeset added and none owed. The single corrected note keeps ③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
… paths (objectstack-ai#20056) Fixes objectstack-ai#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 objectstack-ai#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 objectstack-ai#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, `@example`s, 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. - The branch is behind `origin/main` (15 commits at the seat's re-read). Three of those commits regenerated one of this PR's 10 paths, `packages/spec/src/migrations/registry.ts`: objectstack-ai#20036 (`0bf85eaae6`), objectstack-ai#19909 (`5b9402d89b`) and objectstack-ai#19818 (`66960564d9`). `git merge-tree --write-tree` of `origin/main` and this head is clean, and the at-tier re-review measured that objectstack-ai#20036's and objectstack-ai#19909's hunks do not touch this PR's region (`:5940`–`:6004`). The merge queue's rebuilt generation regenerates the file. The derivation's one changed family input across that range is `scripts/sdui-manifest.record.json`, from objectstack-ai#20036. (Corrected by the seat after the at-tier review `5825376693` found the earlier sentence, "None of this PR's 10 paths is touched by those commits", false.) - 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). --------- Co-authored-by: Claude <noreply@anthropic.com>
Fixes #19966
Clause-②: no
What this changes
The ADR-0087 D3 entry
flow-edge-condition-evaluated-slot-source-required(landed, unreleased, protocol step 18) and the pending changeset for the same change namedPOST /flowsas a door a flow definition is authored through. No such route is mounted. This PR replaces it with the door a flow definition is actually created through on a composed runtime,POST /api/v1/automation, measured below.packages/spec/src/migrations/entries/semantic/18.flow-edge-condition-evaluated-slot-source-required.tssurface(:21)a POST /flows bodya POST /api/v1/automation bodyacceptanceCriteria(:63)`POST /flows` bodies`POST /api/v1/automation` bodiespackages/spec/src/migrations/registry.ts:9068,:9110)pnpm --filter @objectstack/spec gen:migration-registry.changeset/flow-edge-condition-evaluated-slot.md:86`objectstack validate` / `POST /flows``objectstack validate` / `POST /api/v1/automation`:63–:67(PM patch round)config.conditionruling "is untouched.":109–:115(PM patch round)registerFlowandobjectstack validate: the shape refusal, then the blank-source check runningEvaluatedExpressionInputSchemaText only. No runtime, schema or export change.
The pending changeset: a DELIBERATE CORRECTION, not a collision
.changeset/flow-edge-condition-evaluated-slot.mdbelongs to another card's PR (#15807). This PR corrects three sentences in it, each of which reads false in the release it ships in:objectstack validate/POST /flows; a stored row has no author in front of it.", the route now readsPOST /api/v1/automation.config.conditionruling (PM patch round). The note said the service-automation:evaluateConditionanswers a silentfalsefor a non-string predicate, and a non-stringconfig.conditionregisters clean #15662 ruling "is untouched". That is true of spec/automation:FlowEdgeSchema.conditionstill accepts an envelope the engine cannot evaluate (ast-only, whitespace-onlysource) — the evaluated-slot rule of #15430 has not reached the edge condition #15807's own diff, but two sibling notes in the same pending release refuse the value: A whitespace-onlyconfig.conditionstring is a silentfalseat the node door, while #15807 made the edge door refuse the same value at parse #17322 (@objectstack/service-automation,.changeset/blank-node-condition-refused-at-registration.md) atregisterFlow, and lint:objectstack validatereports nothing for exactly the blankconfig.conditionthatregisterFlownow refuses — and a test pins that silence #17495 (@objectstack/lint,.changeset/validate-refuses-blank-structural-condition.md) atobjectstack validate. The sentence now keeps the first half and points at those two notes by card and package, without restating their rule. It names packages rather than.changeset/paths becausechangeset versiondeletes those files, so a path would dangle in the published CHANGELOG. Measured at66417af31f:service-automation/src/engine.ts:9480sends a node'sconfig.conditiontocheckStructuralCondition, whose second gate (:9460) isevaluatedSourceRefusal(:9437–:9451), and that runsEvaluatedExpressionInputSchema.safeParseon the source (:9448). The engine reaches this fromregisterFlow(:4107→validateFlowExpressions,:4134). The lint pass does the same:lint/src/validate-expressions.ts:1368→:1349→evaluatedSourceRefusal,:966–:972(safeParseat:969). All three notes are pending: none of spec/automation:FlowEdgeSchema.conditionstill accepts an envelope the engine cannot evaluate (ast-only, whitespace-onlysource) — the evaluated-slot rule of #15430 has not reached the edge condition #15807, A whitespace-onlyconfig.conditionstring is a silentfalseat the node door, while #15807 made the edge door refuse the same value at parse #17322 or lint:objectstack validatereports nothing for exactly the blankconfig.conditionthatregisterFlownow refuses — and a test pins that silence #17495 appears in the spec, service-automation or lintCHANGELOG.md.EvaluatedExpressionInputSchemaon the condition'ssource, added by A whitespace-onlyconfig.conditionstring is a silentfalseat the node door, while #15807 made the edge door refuse the same value at parse #17322 atregisterFlowand by lint:objectstack validatereports nothing for exactly the blankconfig.conditionthatregisterFlownow refuses — and a test pins that silence #17495 atobjectstack validate(the same file:line readings as item 2). "Has no Zod schema to narrow (the start node'sconfigis an open record)" is kept because it is still true.FlowNodeSchema.configisz.record(z.string(), z.unknown())(spec/src/automation/flow.zod.ts:488). The only per-type config parse on the node contract is forend(parseEndNodeConfig,:439–:449), and no start-node config schema exists inpackages/spec/src.Nothing else in that note changed. No new changeset is added, and
skip-changesetis deliberately not applied: the note being corrected is itself the release input.Check Changesetis expected red by design.check-empty-changeset --base origin/mainrefuses any edit to a changeset present on the merge base (the #17712 guard) and stays red for a deliberate correction; its own text says the remedy is to say so on the PR and get it confirmed, never to restore the base copy. This paragraph is that statement. The maintainer's word covering corrections of pending notes is quoted on the card's claim (「changeset 你看着更新就行」).How the route was measured
On a composed runtime:
bootStack(@objectstack/example-crm, { automation: true })from@objectstack/verify, the same harness the dogfood suite boots. It composescreateDispatcherPlugin({})with the default prefix, exactly likeobjectstack serve(packages/cli/src/commands/serve.ts, which passes noprefixeither;dispatcher-plugin.tsdefaults it to/api/v1). A throwaway probe (not committed) read the live Hono route table and sent real requests as the seeded platform admin. Built from this head (6820d6ef8c), 33 turbo build tasks, exit 0./flowor/flows. 17 live under/api/v1/automation, includingPOST /api/v1/automationandPUT /api/v1/automation/:namePOST /api/v1/flows, a well-formed flowENDPOINT_NOT_FOUNDPOST /api/automation, a well-formed flow (the specAutomationApiContractspath)ENDPOINT_NOT_FOUNDPOST /api/v1/automation, a well-formed flowGET /api/v1/automation/probe_goodanswers 200 with the parsed flowPOST /api/v1/automation, edgecondition: ' 'VALIDATION_FAILED,edges.0.condition, leading with theEVALUATED_EXPRESSION_SOURCE_REQUIREDsentence. Not registered (read-back 404)POST /api/v1/automation, edge{ dialect: 'cel', source: ' ' }VALIDATION_FAILED,edges.0.condition.source, the same sentencePOST /api/v1/automation,ast-only edge envelopeVALIDATION_FAILED,edges.0.condition, the same sentencePUT /api/v1/automation/probe_good, edgecondition: ' 'VALIDATION_FAILED, the same sentenceSo the entry's claim that the refused spellings are "reachable" at the REST authoring door holds, and that door is
POST /api/v1/automation. The route ledger (packages/runtime/src/route-ledger.ts:429,POST /automation, clientautomation.create; "prepend/api/v1for the wire path") agrees with the measurement.Reproduction on
origin/main44639665eebefore the edit:git grep -n 'POST /flows'found the entry:21and:63, the registry:9068and:9110(the card cited:8994/:9036at an older main), and the changeset:83.git grep -c '/flows' packages/runtime/src/route-ledger.tsfound 0 rows (exit 1). The controlPOST /automationhas one row at:429.Upgrade-guide artefacts
gen:upgrade-guideandgen:spec-changeswere re-run and produced zero byte change. The guide renders majors up toPROTOCOL_MAJOR(17 today,build-upgrade-guide.ts), and this entry sits in step 18. Sodocs/protocol-upgrade-guide.mdwill render the correctedsurfacefrom the first protocol-18 build, and not before.check:upgrade-guideandcheck:spec-changesare green at this head.Verification (head
66417af31f)66417af31f(3 paths, 23 changed lines). The set is the same 81 families as at6820d6ef8c, and all 81 were re-run at66417af31fagainst the same 33-package closure (turbo, 33 of 33 cached).--ranreconciles them: "81 derived famil(ies) accounted for — 80 run, 1 NOT-MEASURED".check-adr-0087-registration,check:migration-registry,check:upgrade-guide,check:spec-changes,check:docs,check:api-surface,check:authorable-surfaceandcheck:nul-bytes.check-empty-changeset --base origin/main: exit 1, the expected finding: random changeset filenames collide silently across parallel agents — a round overwrote a sibling PR'sminorchangeset and every gate stayed green #17712 refusal above.check:dual-build-cjs-loads, exit 3PREREQUISITE NOT MET, because the gate needs every package'sdistand this worktree built only the 33-package closure it needed. It is left to CI.6820d6ef8c, the last commit touchingpackages/spec(the patch round changes only the changeset):pnpm --filter @objectstack/spec check:generatedexit 0, "All 15 generated artifacts are up to date".packages/specvitest run --project local src/migrations/ src/conversions/conversions.test.tspassed 4 files, 354 tests.tsc --noEmitexit 0.66417af31fthe generated-artifact gates were re-run with exit 0:check:migration-registry,check:upgrade-guide,check:spec-changes,check:docsandcheck:api-surface.Acceptance notes
PUT /api/v1/automation/:nameis a second definition-write door that refuses the same spellings (measured above), and the entry does not name it.edges.0.condition: …; edges.0.condition: …). The REST mapping flattens both aborted union arms.FlowSchema.safeParseitself reports ONEinvalid_unionissue at the slot, as the changeset says (measured on the built spec).minorchangeset and every gate stayed green #17712 correction.falsefor a direct caller (engine.ts:10777,exprStr.trim() === ''). The corrected sentence names only the two authoring doors that now refuse the value.AutomationApiContracts(packages/spec/src/api/automation-api.zod.ts) declares its nine paths under/api/automation. Every other spec API contract uses/api/v1, and the composed runtime answers 404 at/api/automation. This is out of scope here and was reported to the PM seat with evidence.Generated by Claude Code