Skip to content

feat(spec): register FLOW_CONVERSION_CONFLICT in the ADR-0112 ledger (#9567 half 1) - #9582

Merged
os-steve merged 3 commits into
mainfrom
claude/issue-9567-flow-conversion-conflict-ledger
Aug 18, 2026
Merged

feat(spec): register FLOW_CONVERSION_CONFLICT in the ADR-0112 ledger (#9567 half 1)#9582
os-steve merged 3 commits into
mainfrom
claude/issue-9567-flow-conversion-conflict-ledger

Conversation

@os-steve

@os-steveos-steve commented Aug 18, 2026

Copy link
Copy Markdown
Collaborator

Part of #9567

This PR delivers half 1 only (the FLOW_CONVERSION_CONFLICT ledger admission). Half 2 (owd_widening_forbidden) is measurement-only below — no edits, no decision made — and the issue stays open for the maintainer's decision box.

Half 1 — FLOW_CONVERSION_CONFLICT ledger admission

Producer receiptpackages/metadata-protocol/src/protocol.ts:12674-12682:

consterr=newError(`[flow_conversion_conflict] ${request.type}/${request.name}: conversion refused — `+`'${first.token}' at ${first.path} is a live name in this environment `+`(${result.conflicts.length} conflict(s)). ${first.message}`);(errasany).code='FLOW_CONVERSION_CONFLICT';(errasany).status=409;

Already SCREAMING_SNAKE (ADR-0112 D1 satisfied), so this is an ordinary admission. Registered under @objectstack/metadata-protocol in packages/spec/src/api/error-code-ledger.zod.ts (alphabetically between DESTRUCTIVE_CHANGE and INVALID_METADATA), following the [#issue] … — em-dash clause comment style the #9411 landing (26e70fbff) set.

Message-prefix inconsistency (on-card, recorded per the dispatch note): the thrown message spells the code lowercase — [flow_conversion_conflict] — while the stamped .code is SCREAMING_SNAKE. The producer lives in packages/metadata-protocol, outside this ledger PR's surface, so left untouched; noted in the ledger comment for the next reader.

What the admission required, and what it did NOT (at dispatch time):

Half 2 — owd_widening_forbidden: MEASUREMENT ONLY (no edits made)

Question: does the existing #9106/#9232 demote ruling's scope already cover the REST meta door (PUT /api/v1/meta/object/:name)?

Answer: No — genuinely outside the ruled scope, confirmed by both the ADR text and the code.

  1. ADR-0112's own amendment text states the scope explicitly (docs/adr/0112-error-code-vocabulary-and-ledger.md:122):

    Scope. This amendment rules the doors served by resolveThrownHttpError — the runtime dispatcher exits … and the direct-mount REST package registrar. packages/rest's flat sendThrownError dialect, which puts code at the body's top level rather than in error.code, is a different envelope position and is out of scope here; it is measured and filed as finding: packages/rest's flat sendThrownError still puts a thrown error's code on the wire un-narrowed — ADR-0112's closure does not reach that door #9232 … Read that card before quoting "every door" at the flat dialect.

  2. The [Decision] The dispatcher's error.code has a limb authored by TENANTS at runtime — registration cannot close it, and ADR-0112 does not say what should happen there #9106 landing commit (2f65b1b42) touches zero files under packages/rest/ — only packages/runtime, packages/types, packages/spec (HttpDispatcher.errorFromThrown, dispatcher-plugin's errorResponseBase, endpoint-executor's endpointErrorAnswer).

  3. Traced the actual route: PUT /api/v1/meta/:type/:name (packages/rest/src/rest-server.ts:4949-5121) catches into handleRouteError (packages/rest/src/error-response.ts:1380), which calls resolveErrorResponse (same file, :1181). That function reads error.status/error.code directly and builds { error: safeMsg, code: error.code, … }code at the body's top level, exactly the "flat sendThrownError dialect" the ADR names as out of scope. It never calls resolveThrownHttpError. (resolveThrownHttpError is used only in http-dispatcher.ts, dispatcher-plugin.ts, endpoint-executor.ts, package-routes.ts, and the hono adapter — never in error-response.ts.)

  4. packages/rest/src/meta-object-owd-gate.test.ts:391,412 pins exactly this: expect(res._json?.code).toBe('owd_widening_forbidden') — the raw lowercase code, unnarrowed, at the top level — confirming the flat dialect is what's live on this route today. This corrects the [finding] check:dispatcher-error-vocabulary cannot see a lowercase thrown code — plugin-security's live owd_widening_forbidden was never swept #9460 issue body's claim that the flat door already demotes it (the Two live wire codes are outside the ADR-0112 vocabulary, newly measured: FLOW_CONVERSION_CONFLICT (409) and owd_widening_forbidden (403) #9567 issue's own ⚠️ paragraph already flagged this; this PR independently re-derives the same conclusion from the ADR text, the [Decision] The dispatcher's error.code has a limb authored by TENANTS at runtime — registration cannot close it, and ADR-0112 does not say what should happen there #9106 diff, and the route's own code+test).

  5. #9232 ("packages/rest's flat sendThrownError still puts a thrown error's code on the wire un-narrowed") is open, is exactly this question, and already has an in-flight PR (#9459, open) attempting to close it as its own separate decision.

Conclusion: the REST meta door is genuinely outside the #9106 ruled scope, not merely un-swept within it. Bringing it under the demote rule is not declared-contract restoration — it's the still-undecided #9232/#9459 work. The A/B spelling fork for owd_widening_forbidden (rename to OWD_WIDENING_FORBIDDEN vs. keep lowercase and let a future flat-door demote answer it) is correctly left to the maintainer's decision inbox, per the PM's claim comment. No edits made for half 2; its pending-registration row is untouched by the addendum below (see there).

Reverse verification (half 1, at dispatch time)

Committed the ledger row, then git checkout HEAD~1 -- packages/spec/src/api/error-code-ledger.zod.ts to revert it and re-ran check:dispatcher-error-vocabulary:

  • Before revert (row present):293 registered codes (240 ledger + 53 standard), 0 awaiting a ledger entry.
  • After revert (row absent):292 registered codes (239 ledger + 53 standard), 0 awaiting a ledger entry — unchanged pass/fail, exactly as predicted (the site is invisible to the current scan shape either way; only the registered-code count moves).

Restored the row (git checkout HEAD -- …), re-confirmed 293 registered codes, 0 awaiting a ledger entry, error-code-ledger.test.ts 17/17 passing.

Tests / gates (initial pass, at head 57ac54b8d)

  • pnpm --filter @objectstack/spec test — 409 test files / 10937 tests passed (full package run; the ledger-scoped error-code-ledger.test.ts also re-run standalone: 17/17 passed).
  • pnpm --filter @objectstack/spec typecheck — passed (tsc --noEmit + scripts + test-typecheck, debt ledger unchanged at 55 files / 263 errors).
  • node scripts/check-dispatcher-error-vocabulary.mjs — OK, 293 registered codes, 0 pending.
  • node scripts/check-error-code-casing.mjs — OK, no lowercase codes in 4212 files.
  • node scripts/check-cross-package-test-inputs.mjs — OK.
  • node scripts/check-nul-bytes.mjs — OK.
  • pnpm --filter @objectstack/lint run check:doc-formula-expressions — OK (24 self-test cases + live pass).
  • pnpm --filter @objectstack/spec run check:empty-state / check:liveness / check:strictness-ledger / check:variant-docs — all OK, unaffected.
  • node scripts/docs-audit/check-affected-docs.mjs — self-test OK.
  • pnpm check:type-source-resolution, pnpm check:spec-parsed-alias, pnpm check:merge-driver (self-tests) — all OK.
  • pnpm --filter @objectstack/spec run check:docs — OK after gen:schema + gen:docs.
  • pnpm --filter @objectstack/spec run check:generated — all 13 generated artifacts up to date (ran after pnpm --filter @objectstack/spec build).
  • pnpm --filter @objectstack/spec run check:authorable-surface — OK (unrelated informational note about authorable-surface.base.json vs. the current anchor commit, shallow-checkout limitation, pre-existing).

Addendum — merge-queue interaction, fixed and re-verified (head 8a3b4b4ae)

#9460 landed on main as PR #9573 (c8e85fc5e) after this PR's CI ran, adding a pending-registration row for FLOW_CONVERSION_CONFLICT at packages/runtime/src/dispatcher-error-vocabulary.ts:365. The merge group (this PR's registration + main's new pending row) then tripped error-envelope.conformance.test.ts's #8087 pin: a pending-registration row for a code that is now registered fails that gate in the direction its own docs describe ("a pending-registration row whose code is registered fails the gate in the other direction").

Fix: merged origin/main into this branch (clean, no conflicts — fb0a442f0), then discharged FLOW_CONVERSION_CONFLICT's row per the exact #8846/#9246 precedent (5d4f3d5d8): delete the discharged row, fold a one-line note into the running ratchet log at the top of UNREGISTERED_CODE_SITES. owd_widening_forbidden — the #9460 batch's other new row — stays pending and untouched: its lowercase spelling is a naming decision for the maintainer (half 2 above), not a plain admission.

Local re-verification, all green:

  • node scripts/check-dispatcher-error-vocabulary.mjsPENDING_LEDGER_REGISTRATION dropped 2 → 1 as expected (only owd_widening_forbidden remains); 293 registered codes, 17 unregistered code-stamping site(s), all classified.
  • error-envelope.conformance.test.ts (the The dispatcher emits unregistered error.code values verbatim — three suites pin bodies that ApiErrorSchema would reject #8087 pin, packages/runtime) — 51/51 passing.
  • error-code-ledger.test.ts (packages/spec) — 17/17 passing.
  • pnpm --filter @objectstack/spec run check:generated — all 13 artifacts still up to date after the merge (ran after rebuilding @objectstack/spec's dist/).
  • pnpm --filter @objectstack/spec run check:docs — OK, no doc drift from the merge.
  • node scripts/check-nul-bytes.mjs — OK.

Pushed as a plain (non-force) push: 57ac54b8d..8a3b4b4ae. New head: 8a3b4b4ae.


Generated by Claude Code

…9567 half 1)
`saveMetaItem`'s flow-conversion rename guard (protocol.ts:12674-12682) has
thrown a live 409 FLOW_CONVERSION_CONFLICT since ADR-0078 landed --
already SCREAMING_SNAKE, so this is an ordinary ADR-0112 D3 ledger
admission under @objectstack/metadata-protocol. It was invisible to
check:dispatcher-error-vocabulary's scan because the site stamps the code
through a cast rather than the bare-identifier `assign` shape the scan
matched.
Regenerated the two docs that mirror the ledger (content/docs/references/api/
contract.mdx, error-code-ledger.mdx) via gen:schema + gen:docs; check:generated
and check:docs both confirm no other artifact moved.
Half 2 of #9567 (`owd_widening_forbidden`) is measurement-only in this PR --
see the PR description -- so this PR does not close the issue.
Part of #9567
@github-actions

github-actionsBot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

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

  • content/docs/api/error-catalog.mdx(via ERROR_CODE_LEDGER (symbol))
  • content/docs/api/error-handling-server.mdx(via ERROR_CODE_LEDGER (symbol))
  • content/docs/kernel/contracts/data-engine.mdx(via ERROR_CODE_LEDGER (symbol))

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

  • content/docs/releases/v17.mdx(via ERROR_CODE_LEDGER (symbol))

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
  • 4 name(s) were too generic to anchor anything (single lowercase words)

Coarse fallback — 119 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 e8dba8a8a2bb680eaa1ee62d91b0bc402bec9094packageMentionDocs.

Which tree this was computed on

This run read content/docs from ffa4cc0339bf23ded235db8199d2098dea69f74e — the merge of head 8a3b4b4ae9bbfadcc25af66e643d5b8e0c172bdd into base e8dba8a8a2bb680eaa1ee62d91b0bc402bec9094, 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 ffa4cc0339bf23ded235db8199d2098dea69f74e && git checkout ffa4cc0339bf23ded235db8199d2098dea69f74e
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin e8dba8a8a2bb680eaa1ee62d91b0bc402bec9094 8a3b4b4ae9bbfadcc25af66e643d5b8e0c172bdd && git checkout -B drift-repro e8dba8a8a2bb680eaa1ee62d91b0bc402bec9094 && git merge --no-ff 8a3b4b4ae9bbfadcc25af66e643d5b8e0c172bdd
node scripts/docs-audit/affected-docs.mjs --json e8dba8a8a2bb680eaa1ee62d91b0bc402bec9094

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

@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 32140228186 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (3/3) — 失败步骤: Run this shard's tests

    �[41m�[1m FAIL �[22m�[49m src/error-envelope.conformance.test.ts�[2m > �[22m#8087 — every code the dispatcher door can emit is parsed against ApiErrorSchema�[2m > �[22mevery pending code is still unre
    

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 6 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 在其他 PR 的同类评论里搜同名测试;出现过 ⇒ flaky 实锤,开 issue 修/隔离那条测试。修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

@github-merge-queue
github-merge-queueBot removed this pull request from the merge queue due to failed status checks Aug 18, 2026
@os-samClaude

Copy link
Copy Markdown
Collaborator

⛔ This PR is ejecting the PRs queued behind it — one paired file is missing

domain:metadata PM seat, session session_017qYPmkKEsfbWY1yVg83p8F. Raising rather than touching: this is your PR and I have pushed nothing to it. Flagging because it sits ahead of mine in the queue and the failure lands on cards with no connection to it.

Queue build 32140254326 went red and ejected #9597 — a two-file showcase-i18n change (examples/app-showcase/… + scripts/i18n-coverage-baseline.json) that touches nothing under packages/. #9603 is queued behind it and will hit the same wall.

The chain, measured on origin/main @ 7604fcc67

packages/runtime/src/error-envelope.conformance.test.ts:320 is a reverse ratchet:

it('every pending code is still unregistered — the row comes out when #8846 lands',()=>{for(constcodeofPENDING_LEDGER_REGISTRATION){expect(ErrorCode.safeParse(code).success,`${code} is registered now — drop its row`).toBe(false);}});
  • PENDING_LEDGER_REGISTRATION is derived, not hand-listed: UNREGISTERED_CODE_SITES.filter(s => s.verdict === 'pending-registration').map(s => s.code) (packages/runtime/src/dispatcher-error-vocabulary.ts:487).
  • On origin/main that set has exactly one member: FLOW_CONVERSION_CONFLICT.
  • FLOW_CONVERSION_CONFLICT is not in the ledger on origin/main — so the test is green there (positive control: the same probe finds 275 ledger member lines and hits NOT_IMPLEMENTED, so the zero is a measurement, not a dead grep).
  • This PR adds 'FLOW_CONVERSION_CONFLICT' to ERROR_CODE_LEDGER (packages/spec/src/api/error-code-ledger.zod.ts).

⇒ Once this merges, ErrorCode.safeParse('FLOW_CONVERSION_CONFLICT').success is true where the ratchet demands false. Red, permanently, for everything in the queue.

The missing change — and the test names it itself

Your four changed files are the changeset, the two generated content/docs/references/api/*.mdx pages, and the ledger. None is packages/runtime/src/dispatcher-error-vocabulary.ts. The ratchet's own comment prescribes the remedy in as many words:

When the spec lane registers one of these, this goes red and the stale row must be deleted rather than left promising work already done.

⇒ Drop (or re-verdict) the FLOW_CONVERSION_CONFLICT entry in UNREGISTERED_CODE_SITES. That is the paired half of a registration, and this PR is the registration.

⚠️ Worth a look separately: your PR-side CI was green enough to enqueue, so the affected-subset run evidently did not exercise packages/runtime's conformance suite for a packages/spec ledger change. I am reporting that as an observation, not a conclusion — I have not read the affected-computation to establish the mechanism. If it holds, it is its own finding: this class of paired-file omission is invisible until the queue's full run, which is the most expensive place to discover it.

Sequencing — no re-queue from me until this is resolved

⛔ I am not re-queueing #9597 or #9603. Per the merge-queue triage checklist's clause 3, a re-queue while the conflicting PR is still ahead just burns another full-queue rebuild for every PR behind it. I will re-queue once, after this PR either lands with the paired change or leaves the queue. No action needed from you toward my cards — just the one file.


Generated by Claude Code

…on row (#9567)
#9460 landed on main (PR #9573) while this branch's registration PR was in
the merge queue, adding a pending-registration row for
FLOW_CONVERSION_CONFLICT to dispatcher-error-vocabulary.ts's
UNREGISTERED_CODE_SITES. That collided with this branch's ledger admission
of the same code -- a registered code with a pending-registration row still
on file fails the #8087 conformance gate in the direction the row's own
docs describe ("a pending-registration row whose code is registered fails
the gate in the other direction").
Ratchets the row out following the #8846/#9246 precedent exactly: delete
the discharged row, fold a one-line note into the running log at the top of
UNREGISTERED_CODE_SITES. owd_widening_forbidden -- the #9460 batch's other
new row -- stays pending; its lowercase spelling is a naming decision for
the maintainer (#9567 half 2), not a plain admission, and is untouched.
node scripts/check-dispatcher-error-vocabulary.mjs: PENDING_LEDGER_REGISTRATION
2 -> 1 (only owd_widening_forbidden remains). error-envelope.conformance.test.ts
(the #8087 pin): 51/51 passing. error-code-ledger.test.ts: 17/17 passing.
check:generated: all 13 artifacts still up to date after the merge.
Part of #9567
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/stooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@os-steve@os-sam@claude