Skip to content

fix(spec): the edge-condition upgrade entry and its pending changeset name POST /api/v1/automation, not the nonexistent POST /flows - #20031

Merged
os-litant merged 2 commits into
mainfrom
claude/issue-19966-flows-route-in-upgrade-entry
Sep 25, 2026
Merged

os-litant merged 2 commits into
mainfrom
claude/issue-19966-flows-route-in-upgrade-entry

Conversation

@objectstack-fleet

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

Copy link
Copy Markdown
Contributor

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 service-automation: evaluateCondition answers a silent false for a non-string predicate, and a non-string config.condition registers clean #15662 ruling "is untouched". That is true of spec/automation: FlowEdgeSchema.condition still accepts an envelope the engine cannot evaluate (ast-only, whitespace-only source) — 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-only config.condition string is a silent false at 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) at registerFlow, and lint: objectstack validate reports nothing for exactly the blank config.condition that registerFlow now refuses — and a test pins that silence #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 spec/automation: FlowEdgeSchema.condition still accepts an envelope the engine cannot evaluate (ast-only, whitespace-only source) — the evaluated-slot rule of #15430 has not reached the edge condition #15807, A whitespace-only config.condition string is a silent false at the node door, while #15807 made the edge door refuse the same value at parse #17322 or lint: objectstack validate reports nothing for exactly the blank config.condition that registerFlow now refuses — and a test pins that silence #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 A whitespace-only config.condition string is a silent false at the node door, while #15807 made the edge door refuse the same value at parse #17322 at registerFlow and by lint: objectstack validate reports nothing for exactly the blank config.condition that registerFlow now refuses — and a test pins that silence #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".
  • 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 finding: random changeset filenames collide silently across parallel agents — a round overwrote a sibling PR's minor changeset and every gate stayed green #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

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

github-actions Bot commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 2 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 acceptanceCriteria; a path literal in semantic; a path literal in surface))
  • content/docs/api/declarative-endpoints.mdx (via /api/v1/automation (route, a path literal in acceptanceCriteria; a path literal in semantic; a path literal in surface))
  • content/docs/automation/approvals.mdx (via /api/v1/automation (route, a path literal in acceptanceCriteria; a path literal in semantic; a path literal in surface))
  • content/docs/automation/connectors.mdx (via /api/v1/automation (route, a path literal in acceptanceCriteria; a path literal in semantic; a path literal in surface))
  • content/docs/automation/flows.mdx (via /api/v1/automation (route, a path literal in acceptanceCriteria; a path literal in semantic; a path literal in surface))
  • content/docs/protocol/kernel/http-protocol.mdx (via /api/v1/automation (route, a path literal in acceptanceCriteria; a path literal in semantic; a path literal in surface))
  • content/docs/ui/actions.mdx (via /api/v1/automation (route, a path literal in acceptanceCriteria; a path literal in semantic; a path literal in surface))

⛔ 3 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 acceptanceCriteria; a path literal in semantic; a path literal in surface))
  • content/docs/releases/v17/17-1.mdx (via /api/v1/automation (route, a path literal in acceptanceCriteria; a path literal in semantic; a path literal in surface))
  • content/docs/releases/v17/17-3.mdx (via /api/v1/automation (route, a path literal in acceptanceCriteria; a path literal in semantic; a path literal in surface))

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
  • 2 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 — 136 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 9d81af714f4938f368909322ac8eb2c24276598a → packageMentionDocs.

Which tree this was computed on

This run read content/docs from b5b97a1b17dcd1d31672501eef5134e452597e45 — the merge of head 66417af31f21671b927f69d9feb1d5af488e24ec into base 9d81af714f4938f368909322ac8eb2c24276598a, 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 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

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

… 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>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Maintainer confirmation — the DELIBERATE CORRECTION of .changeset/flow-edge-condition-evaluated-slot.md, 2026-09-24T23:06Z

domain:spec seat 4 (session_019c3Hi6ZMU1p6m6aA6Bz45d), holder of #19966. Carrying the maintainer's words to the PR.

Provenance.

What it confirms, at head 66417af31f. Three edits to the one pending note, and no other note is touched:

  1. objectstack validate / POST /flows → POST /api/v1/automation. The route was measured on a composed runtime (5822432366).
  2. "The whitespace-only STRING ruling on config.condition (service-automation: evaluateCondition answers a silent false for a non-string predicate, and a non-string config.condition registers clean #15662 …) is untouched." now says it is untouched by this change, and that two sibling notes in the same release refuse the value: at registerFlow (A whitespace-only config.condition string is a silent false at the node door, while #15807 made the edge door refuse the same value at parse #17322) and at objectstack validate (lint: objectstack validate reports nothing for exactly the blank config.condition that registerFlow now refuses — and a test pins that silence #17495).
  3. "its producer-side gate is the structural refusal above …" now names the structural pass at registerFlow and objectstack validate: the shape refusal, plus the blank-source check that A whitespace-only config.condition string is a silent false at the node door, while #15807 made the edge door refuse the same value at parse #17322 / lint: objectstack validate reports nothing for exactly the blank config.condition that registerFlow now refuses — and a test pins that silence #17495 added.

Edits 2 and 3 are the seat's widening of this correction (patch round). Without them, the note would contradict its siblings in the same release.

The red. Check Changeset is red by design (#17712 / #18375). It is not a required context, and there is ⛔ no skip-changeset. The TypeScript Type Check red on the old head 6820d6ef8c was the cancelled run of a superseded push. On this head every other check-run is green.

Still owed before landing: the dev's patch-round report and PR-body update, then an at-tier review of this head. A push after this comment voids the head binding above. The ccr landing denial (5819399538) is also still open.


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: 94/94 CONTRACT_REVIEW_TIER
Head-sha: 66417af31f21671b927f69d9feb1d5af488e24ec

Isolated at-tier reviewer subagent, run by the domain:spec seat-4 session; every one of its 94 transcript turns served at the tier the constant names. Review of the head after the PM patch round (the route fix plus the two sentences the seat widened in). With the maintainer confirmation 5823714381 on the same head, this is the DELIBERATE CORRECTION's same-head at-tier PASS: it names the one note and judges each changed sentence. Adopted by the seat 2026-09-24T23:26Z. The record below is the reviewer's, unedited except the two header lines.

① Derived judgments

Inputs read. Card #19966 body + all 4 comments (triage 5813144721, claim 5821602143, os-dev-reports 5822432366, 5823746214); PR #20031 body (10718 chars, 0 angle-bracket chars), pulls/20031/files (3 files), the application/vnd.github.diff diff, PR comments (5822421406 docs-drift, 5823714381 maintainer confirmation), 0 review comments; commits/66417af.../check-runs; git fetch origin main (adbbc5d) and refs/review/pr-20031 = 66417af31f21671b927f69d9feb1d5af488e24ec; merge base 44639665eea4adc6de1eaa963ccd0e5a3e875f98; 2 commits (6820d6ef8c, 66417af31f); report-r1.json / report-r2.json as claims.

(a) The route — holds, from source at the PR ref.

  • packages/cli/src/commands/serve.ts:4340-4341 defaults enableProjectScoping ?? false, projectResolution ?? 'auto'; :4412-4417 calls createDispatcherPlugin({ scoping, enforceProjectMembership, observability, rateLimit }) with no prefix.
  • packages/runtime/src/dispatcher-plugin.ts:909 const prefix = config.prefix || '/api/v1'; :1741-1746 the required arm is not taken, so the else arm runs registerAutomationRoutes(prefix); :1475-1477 server!.post(base + '/automation') dispatches POST /automation; packages/runtime/src/domains/automation.ts:1984-2017 POST / → createFlow → automationService.registerFlow(body.name, body).
  • packages/runtime/src/route-ledger.ts:429 { route: 'POST /automation', client: 'automation.create' }; header :17-18 "prepend /api/v1 for the wire path". git grep -c '/flows' refs/review/pr-20031 -- packages/runtime/src/route-ledger.ts → exit 1 (0 rows); same at merge base.
  • packages/verify/src/harness.ts:683 kernel.use(createDispatcherPlugin({})) — the probe harness composes the same default prefix as serve. Source count of server!.<verb>(base + '/automation…') mounts in dispatcher-plugin.ts = 17, matching the report's "17 under /api/v1/automation".
  • Leftovers: git grep -n 'POST /flows' refs/review/pr-20031 -- . → exit 1 (none in tree). git grep -n '/flows' refs/review/pr-20031 -- .changeset hits only content/docs/automation/flows.mdx doc paths in three unrelated notes (16314, 18235, 18975), not route claims. Entry :21 and :63, note :86 all read POST /api/v1/automation.
  • Mirror vs generator, byte for byte: I replicated parseEntry + renderRegion of packages/spec/scripts/build-migration-registry.ts (:173-192, :258-267: leading // run, literal after = , strip trailing ;, 4-space indent, trailing ,) on the git show of the entry file. The 83-line render is found verbatim in registry.ts at :9052-9134, inside the os-generated semantic:18 markers (:5323 / :13896); md5 of expected render and of the registry slice are both ca29da768e63fa5f1da9955ad28bef78; the id appears once. Registry diff vs merge base is exactly the two mirrored lines (:9068, :9110).
  • Upgrade guide: build-upgrade-guide.ts:78 loops major <= PROTOCOL_MAJOR; protocol-version.ts:18 PROTOCOL_VERSION = '17.0.0', :21 derives 17. Step 18 is not rendered; git grep for the entry id / either route in docs/protocol-upgrade-guide.md and packages/spec/spec-changes.json → exit 1. The report's "zero byte change" and "future-true" reading is correct.

(b) The DELIBERATE CORRECTION — exactly one note, all three sentences true.

  • git diff --name-status origin/main...refs/review/pr-20031: M .changeset/flow-edge-condition-evaluated-slot.md is the only changeset row (no A). Three hunks: :63-67, :86, :109-115. Frontmatter "@objectstack/spec": minor, title (#15807), and the adr-0087: registered flow-edge-condition-evaluated-slot-source-required marker at :7 are untouched.
  • (1) Route sentence :86 — true per (a).
  • (2) :63-67 — Siblings exist at the PR ref (and on origin/main): .changeset/blank-node-condition-refused-at-registration.md ("@objectstack/service-automation": minor, title "…refused at registerFlow… (A whitespace-only config.condition string is a silent false at the node door, while #15807 made the edge door refuse the same value at parse #17322)") and .changeset/validate-refuses-blank-structural-condition.md ("@objectstack/lint": minor, title "objectstack validate refuses a blank structural condition… (lint: objectstack validate reports nothing for exactly the blank config.condition that registerFlow now refuses — and a test pins that silence #17495)"). grep -c of #15807, #17322, #17495 in packages/spec/CHANGELOG.md, packages/services/service-automation/CHANGELOG.md, packages/lint/CHANGELOG.md = 0 for all nine; the entry id is in no CHANGELOG. Engine readings confirmed: engine.ts:4107 registerFlow → :4134 validateFlowExpressions → :9480 checkStructuralCondition(... cfg.condition) → :9453-9466 (shape refusal :9455, then :9460 evaluatedSourceRefusal) → :9437-9451, EvaluatedExpressionInputSchema.safeParse(source) at :9448. Lint: validate-expressions.ts:1368 → :1342-1354 (same order) → :966-972, safeParse at :969, severity 'error'. "Untouched by this change": flow-node-expression-paths.ts:578-582 still admits every string. The evaluator half stands (engine.ts:10777 exprStr.trim() === '' → false); the sentence names only the two producer doors, so it is not false (see ③ wording note).
  • (3) :109-115 — flow.zod.ts:488 config: z.record(z.string(), z.unknown()).optional(); the only per-config type parse is parseEndNodeConfig :439-449 (requireTypeScopedConfig :366+ checks the sibling waitEventConfig/boundaryConfig blocks, not config); git grep -i 'Start(Node)?ConfigSchema' under packages/spec/src → exit 1. The named pass (shape refusal, then blank-source EvaluatedExpressionInputSchema on the source) is what both doors do, at the lines above.
  • Whole note re-read at the PR ref, every other sentence against source: expression.zod.ts:93-107 (ExpressionSchema source-or-ast, not narrowed), :117 the sentence, :166-172 EvaluatedExpressionSchema, :184-187 ExpressionInputSchema, :263-268 EvaluatedExpressionInputSchema (issue-shape docblock :160-164, :195-205); git log -S shows EvaluatedExpressionInputSchema introduced by 53ec0b1ca8 (spec/automation: FlowEdgeSchema.condition still accepts an envelope the engine cannot evaluate (ast-only, whitespace-only source) — the evaluated-slot rule of #15430 has not reached the edge condition #15807, 2026-09-10) in expression.zod.ts and flow.zod.ts, with field/hook/object slots only at ce5785790c (feat(spec)!: every engine-evaluated expression slot requires a non-blank source #18638, 2026-09-18, pending note 15811-…), so "new export" and "first slot to compose it" (:35, :40-41) stay true; EVALUATED_EXPRESSION_SOURCE_REQUIRED introduced by ef3a1388dd (spec/formula: ExpressionSchema accepts an ast-only envelope that no engine can evaluate — it validates, it registers, it faults at run time #15430) matches :19-20; STRUCTURAL_CONDITION_SHAPE_REFUSAL text at flow-node-expression-paths.ts:458-461; evaluateCondition shape refusal engine.ts:10754; stored.ts:82-85 flow skip; database-loader.ts:824 if (singular === 'flow') return payload; canonicalizeStoredFlow :4037 → FlowSchema.parse :4089; the three warn strings plugin.ts:995, :2015, :2061; validate.ts:283 ObjectStackDefinitionSchema.safeParse. No other false sentence found — the dev's "none remain" holds.

(c) Check Changeset red and its handling — as prescribed.

  • 42 check-runs on this head: 35 success, 5 skipped (Build Docs, Console Pin Gate, Packed-tarball smoke opt-in, and Auto Label + Check PR Size on the second pr-automation run), 2 failure — both Check Changeset (runs 107840371893 21:24Z and 107873151368 23:11Z, the latter after the body edit). Rulesets rules/branches/main required contexts: TypeScript Type Check, Test Core, Dogfood Regression Gate, Build Core, Temporal Conformance (live PG + MySQL), Lint & Repo Gates, Governed Surface Queue Guard — all success on this head. Check Changeset is not required (pr-automation.yml:727-729 says so in words).
  • The red's annotation (both runs): "This PR adds no changeset. FIRST: if its only .changeset rows are CHANGED, not added, this PR corrects somebody else's pending release note -- do NOT apply 'skip-changeset'; write the confirmation on the PR, naming the note and what changed under it, and leave this check red ([finding] the skip-changeset label suppresses the DELIBERATE-CORRECTION refusal whose own text says 「no label and no diff shape makes that safe」 — declared contract against enforced behaviour #18375)". That is the Require a changeset step, pr-automation.yml:691-842, route 0 (:715-737), which the workflow says fails FIRST on this class so the check-empty-changeset.mjs step (:892-900, 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 two-class refusal, FOREIGN_CORRECTION_REMEDY at script :551, body :605-611) is skipped in CI. Locally the dev's check-empty-changeset.mjs --base origin/main exit 1 is the same class. Prescription met: no skip-changeset label (labels: documentation, size/s, tooling); the PR body's "DELIBERATE CORRECTION" section names the note and each changed sentence; maintainer confirmation 5823714381 quotes the verbatim ruling and binds to head 66417af31f. (Job log fetch was blocked by the egress proxy on the blob-storage redirect; annotations used instead.)

(d) Scope — nothing beyond, nothing missing. git diff --stat 44639665ee..refs/review/pr-20031: 3 files, +15/-8 (= the body's "23 changed lines"); entry 2 lines, registry the 2 mirrored lines, note 3 hunks. No new changeset is needed: check-adr-0087-registration.mjs:3833-3853 inherits an M changeset already breaking at the branch point (→ :7224 "adds no declared-breaking changeset"); the entry has never been released and the corrected pending note is its own release input (@objectstack/spec: minor, adr-0087 marker at :7), so route 0's "AND releases something of its own" (:738-741) does not apply and AGENTS.md :1069-1072 ("bug fix in a released package") does not either. Riding the corrected note is correct.

(e) PR body — factual sentences hold. Every file:line in items 2–3 and the acceptance notes verified above (:9480, :9460, :9437-9451, :9448, :4107, :4134, :1368, :1349, :966-972, :969, :488, :439-449, :10777). Measurement table corroborated by source (17 mounts, 0 /flows, ledger :429, spec automation-api.zod.ts:14 "Base path: /api/automation"). Reproduction lines: 3b5607019f registry :8994/:9036; merge base 44639665ee registry :9068/:9110, note :83, entry :21/:63, ledger 0 rows — all reproduced. Table rows :86, :63-67, :109-115 match the PR ref. Line 1 Fixes #19966, line 2 Clause-②: no — consistent with a text-only change (no key on any payload); the three card/branch checks are green.

② Semver level

No changeset added and none owed. The single corrected note keeps "@objectstack/spec": minor with its BREAKING banner and adr-0087: registered marker unchanged — the launch-window form AGENTS.md :1069-1084 and check-changeset-no-major.mjs:186-191 (row "M, same level at branch point → exempt") prescribe. Clause-②: no is consistent with a text-only diff. skip-changeset correctly absent (route 0). Level, marker and declaration are mutually consistent.

③ Boundary flags

  • Non-blocking, by design: the PR body was PATCHed by the seat, not the dev — .claude/agents/os-dev.md:57 ("PR 正文 dev 只写一次…⛔ 不 PATCH;事后要改的报告点名改法,席位代写"); the stored body carries the v2 content the report described (route row at :86, rows :63-67/:109-115, Verification at 66417af31f), and the second pr-automation run at 23:11Z is the edited event of that write.
  • Non-blocking: the PR body attributes the CI red to check-empty-changeset --base origin/main (finding: random changeset filenames collide silently across parallel agents — a round overwrote a sibling PR's minor changeset and every gate stayed green #17712); the CI job actually reds one step earlier at route 0 ([finding] the skip-changeset label suppresses the DELIBERATE-CORRECTION refusal whose own text says 「no label and no diff shape makes that safe」 — declared contract against enforced behaviour #18375), which cites that class. Same remedy, wording only.
  • Non-blocking: the "How the route was measured" section says "Built from this head (6820d6ef8c)" — that is the previous head; the route text is identical at both heads and the Verification section says the patch round changed only the changeset.
  • Non-blocking wording in the corrected note :64-65: "does not survive the release" refers to service-automation: evaluateCondition answers a silent false for a non-string predicate, and a non-string config.condition registers clean #15662's two-sided consistency; the evaluator half stands (engine.ts:10777, and sibling note :48-50 says so). The sentence names only the two producer doors, so it is not false.
  • Non-blocking: "first slot to compose it" (:40-41) stays true chronologically while sibling note 15811-evaluated-expression-slots-source-required.md (same release, @objectstack/spec: minor) widens the rule to 36 more positions.
  • Commit trailers use the model-free pair AGENTS.md :451-452 prescribes (Claude-Session: + Co-authored-by: Claude with the noreply address); the dev's self-reported "deviation" is not one against AGENTS.md.
  • Files outside the claim: none (claim's file surface = exactly these 3 paths). Governed surfaces (AGENTS.md :259-260): none touched. PR is a draft; landing not reviewed here.
  • Out-of-scope carrier: spec AutomationApiContracts base path /api/automation vs the runtime's /api/v1/automation — reported by the dev as class b, not filed; the PM seat should file it. Not shipped text of this PR.
  • CI: ci_at_report in report-r2 read 35 runs; head now shows 42 because of the second pr-automation run; no new red.

Implemented-by: claude/issue-19966-flows-route-in-upgrade-entry
Reviewed-by: session_019c3Hi6ZMU1p6m6aA6Bz45d

VERDICT: PASS


Generated by Claude Code

@os-litant
os-litant marked this pull request as ready for review September 25, 2026 01:12
@os-litant
os-litant enabled auto-merge September 25, 2026 01:12
@os-litant
os-litant added this pull request to the merge queue Sep 25, 2026
Merged via the queue into main with commit 1872e52 Sep 25, 2026
42 of 44 checks passed
@os-litant
os-litant deleted the claude/issue-19966-flows-route-in-upgrade-entry branch September 25, 2026 02:13
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/s tooling

Projects

None yet

2 participants