Skip to content

Commit 1872e52

Browse files
fix(spec): the edge-condition upgrade entry and its pending changeset name POST /api/v1/automation, not the nonexistent POST /flows (#20031)
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 named `POST /flows` as 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. | File | Where | Before | After | |---|---|---|---| | `packages/spec/src/migrations/entries/semantic/18.flow-edge-condition-evaluated-slot-source-required.ts` | `surface` (`:21`) | `a POST /flows body` | `a POST /api/v1/automation body` | | same | `acceptanceCriteria` (`:63`) | `` `POST /flows` bodies `` | `` `POST /api/v1/automation` bodies `` | | `packages/spec/src/migrations/registry.ts` | generated mirror (`:9068`, `:9110`) | regenerated with `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` `` | | same | `:63`–`:67` (PM patch round) | the whitespace-only `config.condition` ruling "is untouched." | "is untouched by this change, but it does not survive the release that carries it", pointing at the two sibling notes that refuse the value | | same | `:109`–`:115` (PM patch round) | the start node's producer-side gate "is the structural refusal above" | the structural pass at `registerFlow` and `objectstack validate`: the shape refusal, then the blank-source check running `EvaluatedExpressionInputSchema` | Text only. No runtime, schema or export change. ### The pending changeset: a DELIBERATE CORRECTION, not a collision `.changeset/flow-edge-condition-evaluated-slot.md` belongs to another card's PR (#15807). This PR corrects three sentences in it, each of which reads false in the release it ships in: 1. **The route.** In "The paragraph above is the author's remedy, at `objectstack validate` / `POST /flows`; a stored row has no author in front of it.", the route now reads `POST /api/v1/automation`. 2. **The whitespace-only `config.condition` ruling** (PM patch round). The note said the #15662 ruling "is untouched". That is true of #15807's own diff, but two sibling notes in the same pending release refuse the value: #17322 (`@objectstack/service-automation`, `.changeset/blank-node-condition-refused-at-registration.md`) at `registerFlow`, and #17495 (`@objectstack/lint`, `.changeset/validate-refuses-blank-structural-condition.md`) at `objectstack 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 because `changeset version` deletes those files, so a path would dangle in the published CHANGELOG. Measured at `66417af31f`: `service-automation/src/engine.ts:9480` sends a node's `config.condition` to `checkStructuralCondition`, whose second gate (`:9460`) is `evaluatedSourceRefusal` (`:9437`–`:9451`), and that runs `EvaluatedExpressionInputSchema.safeParse` on the source (`:9448`). The engine reaches this from `registerFlow` (`:4107` → `validateFlowExpressions`, `:4134`). The lint pass does the same: `lint/src/validate-expressions.ts:1368` → `:1349` → `evaluatedSourceRefusal`, `:966`–`:972` (`safeParse` at `:969`). All three notes are pending: none of #15807, #17322 or #17495 appears in the spec, service-automation or lint `CHANGELOG.md`. 3. **The start node's producer-side gate** (PM patch round). The note said the gate "is the structural refusal above". It now names the structural pass at both doors: the shape refusal, then the blank-source check running `EvaluatedExpressionInputSchema` on the condition's `source`, added by #17322 at `registerFlow` and by #17495 at `objectstack validate` (the same file:line readings as item 2). "Has no Zod schema to narrow (the start node's `config` is an open record)" is kept because it is still true. `FlowNodeSchema.config` is `z.record(z.string(), z.unknown())` (`spec/src/automation/flow.zod.ts:488`). The only per-type config parse on the node contract is for `end` (`parseEndNodeConfig`, `:439`–`:449`), and no start-node config schema exists in `packages/spec/src`. Nothing else in that note changed. No new changeset is added, and `skip-changeset` is deliberately not applied: the note being corrected is itself the release input. ⚠️ `Check Changeset` is **expected red by design**. `check-empty-changeset --base origin/main` refuses 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 composes `createDispatcherPlugin({})` with the default prefix, exactly like `objectstack serve` (`packages/cli/src/commands/serve.ts`, which passes no `prefix` either; `dispatcher-plugin.ts` defaults 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. | Reading | Result | |---|---| | Route table of the composed app | 194 routes. **0** match `/flow` or `/flows`. 17 live under `/api/v1/automation`, including `POST /api/v1/automation` and `PUT /api/v1/automation/:name` | | `POST /api/v1/flows`, a well-formed flow | **404** `ENDPOINT_NOT_FOUND` | | `POST /api/automation`, a well-formed flow (the spec `AutomationApiContracts` path) | **404** `ENDPOINT_NOT_FOUND` | | `POST /api/v1/automation`, a well-formed flow | **200**. Read-back `GET /api/v1/automation/probe_good` answers 200 with the parsed flow | | `POST /api/v1/automation`, edge `condition: ' '` | **400** `VALIDATION_FAILED`, `edges.0.condition`, leading with the `EVALUATED_EXPRESSION_SOURCE_REQUIRED` sentence. Not registered (read-back 404) | | `POST /api/v1/automation`, edge `{ dialect: 'cel', source: ' ' }` | **400** `VALIDATION_FAILED`, `edges.0.condition.source`, the same sentence | | `POST /api/v1/automation`, `ast`-only edge envelope | **400** `VALIDATION_FAILED`, `edges.0.condition`, the same sentence | | `PUT /api/v1/automation/probe_good`, edge `condition: ' '` | **400** `VALIDATION_FAILED`, the same sentence | So 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`, client `automation.create`; "prepend `/api/v1` for the wire path") agrees with the measurement. Reproduction on `origin/main` `44639665ee` before the edit: `git grep -n 'POST /flows'` found the entry `:21` and `:63`, the registry `:9068` and `:9110` (the card cited `:8994`/`:9036` at an older main), and the changeset `:83`. `git grep -c '/flows' packages/runtime/src/route-ledger.ts` found 0 rows (exit 1). The control `POST /automation` has one row at `:429`. ## Upgrade-guide artefacts `gen:upgrade-guide` and `gen:spec-changes` were re-run and produced **zero byte change**. The guide renders majors up to `PROTOCOL_MAJOR` (17 today, `build-upgrade-guide.ts`), and this entry sits in step 18. So `docs/protocol-upgrade-guide.md` will render the corrected `surface` from the first protocol-18 build, and not before. `check:upgrade-guide` and `check:spec-changes` are green at this head. ## Verification (head `66417af31f`) - Re-derived on the new change set at `66417af31f` (3 paths, 23 changed lines). The set is the same 81 families as at `6820d6ef8c`, and all 81 were re-run at `66417af31f` against the same 33-package closure (turbo, 33 of 33 cached). `--ran` reconciles them: "81 derived famil(ies) accounted for — 80 run, 1 NOT-MEASURED". - 79 answered exit 0, including `check-adr-0087-registration`, `check:migration-registry`, `check:upgrade-guide`, `check:spec-changes`, `check:docs`, `check:api-surface`, `check:authorable-surface` and `check:nul-bytes`. - `check-empty-changeset --base origin/main`: exit 1, the expected #17712 refusal above. - NOT MEASURED: `check:dual-build-cjs-loads`, exit 3 `PREREQUISITE NOT MET`, because the gate needs every package's `dist` and this worktree built only the 33-package closure it needed. It is left to CI. - At `6820d6ef8c`, the last commit touching `packages/spec` (the patch round changes only the changeset): - `pnpm --filter @objectstack/spec check:generated` exit 0, "All 15 generated artifacts are up to date". - `packages/spec` `vitest run --project local src/migrations/ src/conversions/conversions.test.ts` passed 4 files, 354 tests. - `tsc --noEmit` exit 0. - At `66417af31f` the generated-artifact gates were re-run with exit 0: `check:migration-registry`, `check:upgrade-guide`, `check:spec-changes`, `check:docs` and `check:api-surface`. ## Acceptance notes - The corrected entry names the create door only, as the original sentence did. `PUT /api/v1/automation/:name` is a second definition-write door that refuses the same spellings (measured above), and the entry does not name it. - At the REST door, a blank bare-string edge condition's 400 message carries the sentence twice (`edges.0.condition: …; edges.0.condition: …`). The REST mapping flattens both aborted union arms. `FlowSchema.safeParse` itself reports ONE `invalid_union` issue at the slot, as the changeset says (measured on the built spec). - I checked every other sentence of the corrected changeset against source. Only the two sentences corrected in items 2 and 3 above had been overtaken by the same pending release. The PM widened this PR's scope to fold them in, as carrier of this note's #17712 correction. - The evaluator itself still answers a whitespace-only condition `false` for a direct caller (`engine.ts:10777`, `exprStr.trim() === ''`). The corrected sentence names only the two authoring doors that now refuse the value. - Spec `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](https://claude.ai/code/session_019c3Hi6ZMU1p6m6aA6Bz45d)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 6aa3188 commit 1872e52

3 files changed

Lines changed: 15 additions & 8 deletions

File tree

‎.changeset/flow-edge-condition-evaluated-slot.md‎

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,10 @@ edges:
6161
refused by `evaluateCondition` with the same sentence, instead of answering a
6262
silent `false`. An `ast` BESIDE a string `source` is still admitted
6363
everywhere. The whitespace-only STRING ruling on `config.condition` (#15662:
64-
consistent `false` on both sides) is untouched.
64+
consistent `false` on both sides) is untouched by this change, but it does not
65+
survive the release that carries it: two sibling notes in that release refuse
66+
the value, at `registerFlow` (#17322, `@objectstack/service-automation`) and
67+
at `objectstack validate` (#17495, `@objectstack/lint`).
6568
- **Three doors agree, through the spec.** `registerFlow` refuses the flow at
6669
`FlowSchema.parse` (edge) or at its structural pass (`config.condition`);
6770
`objectstack validate` refuses it at its `ObjectStackDefinitionSchema` parse
@@ -80,7 +83,7 @@ the refusal itself carries the prescription.
8083

8184
**A flow ALREADY STORED in `sys_metadata` stops running entirely — the whole
8285
flow, not just the edge.** The paragraph above is the author's remedy, at
83-
`objectstack validate` / `POST /flows`; a stored row has no author in front of
86+
`objectstack validate` / `POST /api/v1/automation`; a stored row has no author in front of
8487
it. Stored flows are deliberately NOT canonicalized by
8588
`applyConversionsToStoredItem` (`spec/src/conversions/stored.ts`, and the same
8689
skip in `metadata/src/loaders/database-loader.ts`'s `rowToData`) — flow-node
@@ -104,5 +107,9 @@ door, `objectstack validate`, which locates the same edge at
104107
judgment for a consumer replaying the chain.
105108

106109
Not touched here: `start.config.condition` has no Zod schema to narrow (the
107-
start node's `config` is an open record); its producer-side gate is the
108-
structural refusal above, which this change tightens but does not type.
110+
start node's `config` is an open record). Its producer-side gate is the
111+
structural pass at `registerFlow` and `objectstack validate`: the shape refusal
112+
above, which this change tightens but does not type, and after it a blank-source
113+
check that runs this change's `EvaluatedExpressionInputSchema` on the
114+
condition's `source` (added by #17322 at `registerFlow` and by #17495 at
115+
`objectstack validate`).

‎packages/spec/src/migrations/entries/semantic/18.flow-edge-condition-evaluated-slot-source-required.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export const entry: SemanticMigration = {
1818
+ 'than deriving a second one; it is the same decision reaching the second slot, which is why '
1919
+ 'it is named here instead of in an entry of its own. Reachable wherever a flow is authored '
2020
+ 'or stored: defineStack({ flows }) sources, an exported stack passed to objectstack validate, '
21-
+ 'a POST /flows body, and a flow row already sitting in sys_metadata',
21+
+ 'a POST /api/v1/automation body, and a flow row already sitting in sys_metadata',
2222
replacement:
2323
'a non-blank `source` — `{ dialect: \'cel\', source: \'record.amount > 10\' }`, or the bare '
2424
+ 'string `\'record.amount > 10\'` — if the edge was meant to branch; or REMOVE the '
@@ -60,7 +60,7 @@ export const entry: SemanticMigration = {
6060
acceptanceCriteria:
6161
'Grep every authored structural condition — BOTH `edges[].condition` and a node\'s '
6262
+ '`config.condition` (a `decision` node\'s predicate, and on a `start` node the trigger '
63-
+ 'gate) — in `defineStack({ flows })` sources, exported stacks and `POST /flows` bodies, and '
63+
+ 'gate) — in `defineStack({ flows })` sources, exported stacks and `POST /api/v1/automation` bodies, and '
6464
+ 'every flow row in `sys_metadata`, for an envelope with no `source` key and for a `source` '
6565
+ '(or bare string) that is empty after trimming. ⚠️ Sweeping only the edge key leaves the '
6666
+ 'node key unswept, and the node key is the one with no schema in front of it. For each '

‎packages/spec/src/migrations/registry.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9187,7 +9187,7 @@ const step18: MigrationStep = {
91879187
+ 'than deriving a second one; it is the same decision reaching the second slot, which is why '
91889188
+ 'it is named here instead of in an entry of its own. Reachable wherever a flow is authored '
91899189
+ 'or stored: defineStack({ flows }) sources, an exported stack passed to objectstack validate, '
9190-
+ 'a POST /flows body, and a flow row already sitting in sys_metadata',
9190+
+ 'a POST /api/v1/automation body, and a flow row already sitting in sys_metadata',
91919191
replacement:
91929192
'a non-blank `source` — `{ dialect: \'cel\', source: \'record.amount > 10\' }`, or the bare '
91939193
+ 'string `\'record.amount > 10\'` — if the edge was meant to branch; or REMOVE the '
@@ -9229,7 +9229,7 @@ const step18: MigrationStep = {
92299229
acceptanceCriteria:
92309230
'Grep every authored structural condition — BOTH `edges[].condition` and a node\'s '
92319231
+ '`config.condition` (a `decision` node\'s predicate, and on a `start` node the trigger '
9232-
+ 'gate) — in `defineStack({ flows })` sources, exported stacks and `POST /flows` bodies, and '
9232+
+ 'gate) — in `defineStack({ flows })` sources, exported stacks and `POST /api/v1/automation` bodies, and '
92339233
+ 'every flow row in `sys_metadata`, for an envelope with no `source` key and for a `source` '
92349234
+ '(or bare string) that is empty after trimming. ⚠️ Sweeping only the edge key leaves the '
92359235
+ 'node key unswept, and the node key is the one with no schema in front of it. For each '

0 commit comments

Comments
 (0)