Skip to content

feat(automation)!: edge-branched decision is exclusive; mode: 'inclusive' takes every branch (#15429) - #20344

Draft
objectstack-fleet[bot] wants to merge 7 commits into
mainfrom
claude/issue-15429-decision-first-match
Draft

objectstack-fleet[bot] wants to merge 7 commits into
mainfrom
claude/issue-15429-decision-first-match

Conversation

@objectstack-fleet

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

Copy link
Copy Markdown
Contributor

Fixes #15429
Clause-②: yes

Carries the maintainer's ruling 5793803317 (「跟主流对齐」, 2026-09-23) as ONE change, routed to domain:spec by 5860007474 and dispatched by the domain:spec seat 4 PM (session_01CiCTczDo7tGhafXjf61dUJ, claim 5860277199). Branch base 10ea9eb2e; origin/main (a78f731ad) merged through os-regen-merge.sh; every reading below is at head e92edee5b unless it says otherwise; the patch commit 27a1a4598 (the seat's answer A in 5861311629) carries its own readings where stated. (Body revised by the seat at 2026-09-28T01:34Z from the patch-round report 5861745226.)

Coordination card: objectstack-ai/objectui#10750 (the designer offers mode). Nothing is written in objectui here.

What changes

  1. Exclusive by default (ruling item 1). AutomationEngine.traverseNext: on a decision node the conditioned out-edges are evaluated in the order the flow's edges array declares them and the FIRST one whose condition holds is the branch; its later siblings are not evaluated and record the same skipped step a closed gate does (skippedBy names the gate and the edge). isDefault is unchanged: it runs when no conditioned sibling did. Scoped to decision: conditioned out-edges of any other node type keep the every-true-edge traversal (the census below found none).
  2. Explicit inclusive (item 2). config.mode: 'inclusive' takes every out-edge whose condition holds, one successor at a time (never Promise.all; the pin's positive control proves the instrument can see interleaving).
  3. Registration door (acceptance 5857171841). registerFlow parses every decision's config through the spec's DecisionConfigSchema and refuses the flow on any issue rooted at mode — the refinement (mode beside a non-empty conditions list, either member) and the value refusal — with the schema's own sentence at node 'x' (decision) at config.mode, inside ADR-0031 regions too. Judged on mode alone, deliberately: the same parse also refuses an undeclared key, but that strictness binds at authoring by the standing decision in the module header of schemaless-node-config.zod.ts, and refusing an inert extra key at boot would be a second behaviour change riding a ruling that ordered one. Measured reach of the alternative: zero decision nodes in either corpus carry a key other than conditions, so promoting it later is cheap.
  4. os validate door. @objectstack/lint gains flow-decision-mode-invalid (gating; the finding IS the schema's issue message, so both doors say one sentence) and flow-decision-inclusive-overlap (advisory, ruling item 4: mode: 'inclusive' with two or more conditioned out-edges; beside flow-decision-unconditional-branch, which is about an out-edge nothing gates).
  5. Migration (item 3). ADR-0087 D2 conversion flow-decision-mode-inclusive-explicit (protocol 18): a decision with no conditions list, no mode, and two or more conditioned out-edges (a fault edge is error routing; a blank condition is none; regions walked through the shared slot table) gets mode: 'inclusive' written, with a notice naming the count. One conditioned edge plus a default is left alone; an authored mode is left alone (idempotent by construction). No inference over the conditions, per the ruling. Paired D3 entry flow-decision-edge-branching-first-match names the D2 id as a whole word and carries the three-way judgment the diff asks for. MIGRATIONS_BY_MAJOR[18] wires the id and its rationale grows a sentence.
  6. Rewrites (item 5). decision-overlapping-edge-conditions.pin.test.ts is the contract pin now, per its own header. flows.mdx, the DecisionConfigSchema docblock and mode describe, the module header (the declared-ahead sites of 5852576993 item 2), logic-nodes.ts, and engine.ts's traversal comment all describe both modes; the reference mdx is regenerated.

PM mechanism assumptions, measured

1. Where the traversal lives — held. Both sites relocated by content: the conditional loop under the old 「evaluate sequentially (mutually exclusive)」 comment in engine.ts (traverseNext), and the config.conditions first-match in builtin/logic-nodes.ts (untouched, still label-narrowing). Declaration order is flow.edges array order: traverseNext filters that array in place; FlowSchema.parse (region transform included) and every conversion walker are copy-on-write maps that never reorder; normalizeStackInput normalizes map-form collections at the stack level and never touches a flow's edges; canonicalizeStoredFlow runs those same two. Grep for any edge re-sort across packages/*/src and packages/*/*/src (edges.sort, sortEdges, .edges.slice().sort, localeCompare on edges): 0 hits. NOT MEASURED: the Studio designer's own serialization order at save time — objectui is not checked out in this container; server side, saveMetaItem canonicalizes without reordering.

2. The migration predicate — census. Corpus: this repository's examples at 10ea9eb2e and objectstack-ai/hotcrm at 2f7b2326 (read-only), every flow module loaded and every graph walked including regions; platform packages ship no decision node (grep over packages/platform-objects, plugins, services: only the executor and the README).

corpus flows decisions rewritten (no list, ≥ 2 conditioned, no mode) left alone non-decision nodes with a conditioned out-edge
examples (app-crm, app-todo, app-showcase) 35 5 4 1 (crm_convert_lead_wizard.check_converted: 1 conditioned + isDefault) 0
hotcrm 13 25 13 (incl. lead_conversion.decision_duplicate, the #1555 node, now three conditioned edges) 12 (single-conditioned guards, no default) 0

Every one of the 17 positives is a hand-written partition by inspection (a predicate beside its negation, > beside <=, has() beside !has(), hotcrm's CASE_HAS_OWNER beside its exact complement, memberSource != "contacts" beside == "contacts"), so first-match changes none of their runs; the conversion still writes the key onto all 17, as ruled, and the D3 entry tells the author to delete it there. No decision in either corpus carries a config key other than conditions. examples/** is outside this claim's surface and is not edited: the four in-tree positives partition, so nothing changes at boot.

3. Stored flows — FAILED, and adapted. The assumption was that the conversion replays at rehydration like the other step-18 entries. It cannot: this is a DEFAULT FLIP (the old shape still parses and now means exclusive), and the flow rehydration seam serves post-flip authored bodies too — canonicalizeStoredFlow is reached by the boot pull for code-shipped flows, by POST /automation, by saveMetaItem (every Studio save) and by duplicatePackage, all through one two-argument signature, none dated. Replaying there would rewrite every NEW exclusive decision into an inclusive one at registration and persist it at save, and the ruled default would be unobservable. The registry's own doctrine for this class (excludeConversionIds, the artifact door's DEFAULT_FLIPS_NOT_REPLAYED_HERE for app-hidden-to-unpublished on #17885, the WITHDRAWN field-required-notnull-explicit note) says a seam that cannot state 「this body predates the flip」 refuses the entry by id. So:

  • the entry is retiredFromLoadPath: true (no authoring window — 「不留过渡窗口」) and replays where the operator asserts the source's age: os migrate meta --from 17 (the D3 chain), pinned both ways;
  • canonicalizeStoredFlow refuses it by id (CONVERSIONS_NOT_REPLAYED_AT_REHYDRATION, reason at the call site), pinned on parsed, storable and notices, with the chain as the firing control;
  • ⚠️ stored sys_metadata flows are therefore rewritten by nothing today: os migrate meta --stored canonicalizes through that same seam. A decision saved from the Studio before this release with two or more conditioned out-edges and no mode now runs first-match. The D3 entry states this as the judgment owed (list those rows, declare mode in the designer); an operator-asserted opt-in on the stored pass is the follow-up plumbing (migrateStoredMetadata in metadata-protocol → the engine seam), outside this claim's surface.
  • The artifact-ingestion door refuses it too (patch commit 27a1a4598, the seat's answer A in 5861311629). packages/metadata-core/src/artifact-forward-conversion.ts lists flow-decision-mode-inclusive-explicit in DEFAULT_FLIPS_NOT_REPLAYED_HERE beside the app-hidden-to-unpublished precedent, with its reason: the door's trigger is the artifact's declared engines.protocol floor, ^17.0.0 is what create-objectstack stamps, so an app scaffolded today against the exclusive contract lands inside the window and would otherwise be handed an inclusive gateway it never asked for. The door pin has four legs (subject, the strict parse the door feeds, negative, firing control), and the engine seam's and the entry's docblocks now cite the door precisely.

4. Serial state — moved, merged. origin/main gained #20286 (view-overlay-owner-hidden-removed) on the same registry lines; os-regen-merge.sh merged it (both entries kept in landing order in conversions/registry.ts and in MIGRATIONS_BY_MAJOR[18], rationale concatenated), gen:migration-registry regenerated to an identical file, check:generated found every artifact current, and every sibling symbol was asserted present on both sides by exact-name grep (viewOverlayOwnerHiddenRemoved 3/3, view-overlay-owner-hidden-removed 9/9, view.zod.ts retiredKey 21/21).

Surface

17 files, +1624 / −203 (1827 changed lines against origin/main, under the 5000 human-merge threshold). Two files entered by the claim's surface amendment (5861311629): packages/metadata-core/src/artifact-forward-conversion.ts (only the DEFAULT_FLIPS_NOT_REPLAYED_HERE array and its reason docblock) and packages/metadata-core/src/artifact-forward-conversion.test.ts (the door pin). Two files the claim did not spell are recorded there as covered: packages/spec/src/conversions/registry.ts (where every D2 conversion lives) and packages/lint/src/index.ts (the two rule-id exports, required by rule-id-barrel-exports.test.ts). ⛔ Not touched: flow-node-expression-paths.ts, examples/**, packages/metadata-protocol/**, packages/cli/**, packages/runtime/**, packages/rest/**, packages/spec/src/contracts/**, objectui.

Pins that carry weight, and the ablations

decision-overlapping-edge-conditions.pin.test.ts (21 tests): two overlapping true edges → exactly one runs, the first declared, the sibling records skipped; declaration order decides (the same predicates reversed take the other branch); mode: 'inclusive' → both run nested, no skipped step; none true → isDefault runs in both modes; a true edge beside a default passes the default over in both modes; a conditions list still narrows by label; registration refuses the pair (either member) and a bad value with the spec sentence, inside a loop body too, and the flow is never armed; the four controls register; the rehydration seam leaves the two-branch shape unrewritten while applyMetaMigrations(stack, 17, 18) rewrites it; a non-decision node keeps every-true-edge. conversions.test.ts: the fixture pair (2 notices) plus the predicate's edges, region reach, idempotence, the authoring funnel's silence, and the seam refusal with its firing control. lint-flow-patterns.test.ts: both rules, gating vs advisory, controls, regions, no double report through rule (2). migrations.test.ts's census pin sees the D3 entry naming the D2 id. artifact-forward-conversion.test.ts (27a1a4598): a ^17.0.0-floor artifact carrying a two-branch decision passes the door with no mode written, no notice for the id and the same reference back; the strict parse the door feeds receives no mode; ^99.0.0 shuts the window; and the same fixture through applyConversions with includeRetired: true and no refusal comes back { mode: inclusive } with the entry's notice (firing control).

Both ablations ran from committed state through scripts/ablation-replace.mjs (anchor hit 1→0, marker 0→1, blob hashes printed), the reading was taken, and restore was git checkout HEAD -- ABS_PATH under a trap, proven by git diff HEAD clean and git hash-object equal to the HEAD blob. No dist leg was owed: both suites resolve their subject through src (../engine.js inside service-automation; ./registry.js inside spec).

  • traversal: if (exclusive && anyConditionMet) → if (false && …). Blob a60861d3985717a743cb32c16d9e3ba925dee3c7 → f0e25d39262ae22b38ef67b5affbba494c0023bf. Ablated run: 6 failed (exactly the exclusivity, skipped-step, declaration-order, written-exclusive, default-passed-over and seam-runs-exclusive pins), 15 passed (the controls, inclusive, default and registration pins). Restored: a60861d3… on disk and at HEAD.
  • predicate: MIN_CONDITIONED_EDGES = 2 → 3. Blob fd1a7902d480b791e7f53116eb38c97ad268fb78 → a03f5cbdf9f742aabf42e8400a1fc5df50b50d1b. Ablated run: 5 failed (the fixture pair, the wiring pin, the two-edge rewrite, the left-alone pin, the seam-refusal firing control), 216 passed. Restored: fd1a7902… on disk and at HEAD.
  • artifact door (27a1a4598): the id removed from DEFAULT_FLIPS_NOT_REPLAYED_HERE (anchor 1→0, marker 0→1). Blob 16742f49e72eaa98214eca097b9b14cef03e8809 → 26220cf59c92d7b4daf75a74b17e5a076156503c. Ablated run: 2 failed (the subject leg — mode written — and the strict-parse leg), 27 passed (the firing control and the negative stayed green). Restored: 16742f49… on disk and at HEAD.

Tests, at e92edee5b, every exit captured after a redirect

  • pnpm --filter @objectstack/spec test → exit 0: Test Files 554 passed (554) · Tests 16366 passed | 1 todo.
  • pnpm --filter @objectstack/service-automation exec vitest run --maxWorkers=2 → exit 0: Test Files 147 passed (147) · Tests 1782 passed (1782).
  • pnpm --filter @objectstack/lint exec vitest run --maxWorkers=2 → exit 0: Test Files 111 passed (111) · Tests 4313 passed (4313).
  • typecheck for the same three packages → exit 0 each (check:test-typecheck OK on each test layer).
  • Importers of DecisionConfigSchema outside these packages: metadata-protocol's JSON-projection walk (a refinement projects byte-identically) and config-expression-ledger.test.ts (in the service-automation run above); no other importer of the traversal exists (registerFlow callers in runtime and plugin.ts are unchanged call sites).
  • Gates: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 114 commands on this branch; all 114 ran on e92edee5b with exit 0 (check:dual-build-cjs-loads and check:type-check-debt first answered exit 3, PREREQUISITE NOT MET, until the whole packages/* closure was built — 71/71 — then 0); --ran reconciliation: 114 derived, 114 run, 0 NOT-MEASURED, 0 UNRUN. check:generated on the merged tree: every artifact current after gen:docs. spec-changes.json and the upgrade guide render no protocol-18 id yet (control: report-joined-chart-removed 0 hits too), so their green is genuine, not a missed regeneration.
  • Patch commit 27a1a4598, readings at that head: pnpm --filter @objectstack/metadata-core exec vitest run --maxWorkers=2 → exit 0: Test Files 16 passed (16) · Tests 289 passed (289); metadata-core typecheck → exit 0. Re-run because the diff reaches them (docblock edits in engine.ts and conversions/registry.ts): service-automation Test Files 147 passed (147) · Tests 1782 passed (1782), spec Test Files 554 passed (554) · Tests 16366 passed | 1 todo, both typechecks exit 0; lint is not reached and was not re-run. Gates: the same 114 derived commands (0 added, 0 dropped), all exit 0 on 27a1a4598; --ran: 114 derived, 114 run, 0 NOT-MEASURED, 0 UNRUN. check:type-check-debt first refused (exit 3) because metadata-core's cache-restored dist/ was older than its source after the ablation's restore rewrote the file; a direct pnpm --filter @objectstack/metadata-core build, as the gate prescribes, and a re-run gave 0.

Changeset grade, measured at landing

npm latest @objectstack/spec is 17.4.0 (npm view, 2026-09-27), whose published DecisionConfig.json declares conditions only; .changeset/19867-decision-config-mode.md and .changeset/20168-…md are still unconsumed, so mode is unreleased and reaches its first release with the traversal that reads it and the conversion that writes it. Clause-②: yes per the ruling's item 2 (the D2/D3 entries and the two lint rules widen the published surface; nothing published narrows). minor for @objectstack/spec, @objectstack/service-automation and @objectstack/lint, with the BREAKING banner, the FROM → TO block and the disposition marker registered flow-decision-mode-inclusive-explicit (the changeset file carries it in the gate's own form).

Acceptance notes

  • Landed on this PR (27a1a4598): the artifact door's refusal of flow-decision-mode-inclusive-explicit, per the seat's answer A (5861311629).
  • The stored-row half is in the maintainer's decision box on A decision node with no declared config.conditions takes EVERY out-edge whose condition holds, in parallel — nothing enforces or warns that intended-exclusive edges partition #15429 (the seat's 5861311629): os migrate meta --stored canonicalizes flow rows through the engine seam that refuses the id, so a Studio decision saved before this release with two or more conditioned out-edges and no mode runs first-match and is rewritten by nothing; the D3 entry and the changeset say so. This PR does not land before that ruling.
  • skills/objectstack-automation/SKILL.md line 65 (「routed by edge condition predicates」) stays true and does not mention mode; governed surface, not touched.
  • The REST door answers a registration refusal as VALIDATION_ERROR 400 through flowDefinitionRefusal (unchanged code path); not pinned here, the runtime package is outside this surface.

Generated by Claude Code

…ive'` takes every branch

A `decision` with no `config.conditions` now takes the FIRST conditioned
out-edge whose condition holds, in declaration order (BPMN exclusive
gateway); the passed-over siblings record a `skipped` step. `mode:
'inclusive'` takes every one, sequentially. `isDefault` is unchanged.

- registration parses `DecisionConfigSchema` and refuses an invalid `mode`
  (value outside the pair, or beside a non-empty `conditions` list) with the
  schema's sentence; `os validate` reports the same as
  `flow-decision-mode-invalid`, plus the advisory
  `flow-decision-inclusive-overlap`.
- ADR-0087 D2 `flow-decision-mode-inclusive-explicit` (retired from the load
  path, refused by the flow rehydration seam by id) writes `mode: 'inclusive'`
  onto decisions with >= 2 conditioned out-edges for `os migrate meta --from
  17`; D3 entry `flow-decision-edge-branching-first-match` carries the
  judgment.
- the status-quo pin is rewritten as the contract pin; docs describe both
  modes.

Claude-Session: https://claude.ai/code/session_01CiCTczDo7tGhafXjf61dUJ
Co-authored-by: Claude <noreply@anthropic.com>
Both registries gained an entry on each side on the same lines
(`view-overlay-owner-hidden-removed` from #20286, this branch's
`flow-decision-mode-inclusive-explicit`); both kept, landing order.

Claude-Session: https://claude.ai/code/session_01CiCTczDo7tGhafXjf61dUJ
Co-authored-by: Claude <noreply@anthropic.com>
…anchor the retirement-jurisdiction citation to the docblock that decided it

- packages/lint/src/index.ts: FLOW_DECISION_MODE_INVALID and
  FLOW_DECISION_INCLUSIVE_OVERLAP join the flow-pattern export block
  (rule-id-barrel-exports pin).
- conversions/registry.ts: the retiredFromLoadPath jurisdiction is cited
  from MetadataConversion's docblock and ADR-0087's 2026-07-31 addendum,
  not from a tracker number that no longer resolves.
- schemaless-node-config.zod.ts: the mode JSDoc no longer spells an
  omitted-means-every sentence the empty-state gate has to classify.

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

github-actions Bot commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 4 package(s): @objectstack/lint, @objectstack/metadata-core, @objectstack/service-automation, @objectstack/spec, touching 24 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/lint/src/index.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

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

  • content/docs/automation/flows.mdx (via registerFlow (symbol, a method of class AutomationEngine))
  • content/docs/data-modeling/formulas.mdx (via registerFlow (symbol, a method of class AutomationEngine))

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

  • content/docs/releases/v16.mdx (via AutomationEngine (symbol, a top-level class))
  • content/docs/releases/v17/17-0.mdx (via AutomationEngine (symbol, a top-level class), registerFlow (symbol, a method of class AutomationEngine))
  • content/docs/releases/v17/17-4.mdx (via registerFlow (symbol, a method of class AutomationEngine))

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 changed file(s) yielded no anchor (packages/lint/src/index.ts) — pages documenting those are invisible to this run
  • 8 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 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; 97 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 8af914a30d1dab62fd7b73d1d847076b639cb2ab → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 8af914a30d1dab62fd7b73d1d847076b639cb2ab

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

…lusive-explicit` by id (#15429 patch round)

The entry is a DEFAULT FLIP: an omitted `mode` IS the exclusive gateway by
the contract on `DecisionConfigSchema`, so writing `mode: 'inclusive'` is a
reinterpretation that is sound only where the source's age is a fact —
`os migrate meta --from 17`. The artifact-ingestion door's trigger is the
declared `engines.protocol` floor, and `^17.0.0` is what `create-objectstack`
stamps, so an app scaffolded today against the exclusive contract lands
inside the window and would be handed an inclusive gateway it never asked
for. The id joins `DEFAULT_FLIPS_NOT_REPLAYED_HERE` beside the
`app-hidden-to-unpublished` precedent, with its reason; the door pin has four
legs (subject, strict parse, negative, firing control through the primitive).

The engine seam's `CONVERSIONS_NOT_REPLAYED_AT_REHYDRATION` docblock and the
conversion entry's docblock now cite the door precisely (「must」 became
「does」).

Claude-Session: https://claude.ai/code/session_01CiCTczDo7tGhafXjf61dUJ
Co-authored-by: Claude <noreply@anthropic.com>
… sentence the repo pin requires (#15429 CI fix)

CI `Test Core (1/6)` ran `packages/spec` `test:repo` and the repo-project pin
`src/shared/retired-key-migrate-sentence.test.ts` refused two sites in
`migrations/entries/semantic/18.flow-decision-edge-branching-first-match.ts`:
the `reason` prose quoted the command mid-sentence ("the diff `os migrate meta
--from 17` prints is where…") and `acceptanceCriteria` opened with a bespoke
"Run `os migrate meta --from 17` over each authored stack…" — neither is the
house sentence the pin requires as the LAST sentence of any literal that names
the command, and the pin's anti-vacuity case turned red with it.

- `reason` no longer names the command (the chain replay's edit list is where
  the judgment is made); `acceptanceCriteria` now ends with the house sentence
  "Run `os migrate meta --from 17` to list the mechanical edits for existing
  sources; apply them by hand." and opens with the review list instead.
- `migrations/registry.ts` regenerated from the entry (`gen:migration-registry`;
  298 semantic, 217 retired-key, 199 retired-def — counts unchanged).
- `content/docs/automation/flows.mdx` upgrade callout reworded to the same
  house sentence so the docs and the entry read identically.

Stored-row sentences in the entry are untouched.

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

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/xl tests tooling

Projects

None yet

2 participants