Skip to content

fix(rest): thread ?mode=draft on the compound-name metadata write door - #11933

Merged
os-zhuang merged 3 commits into
mainfrom
claude/issue-11712-compound-door-mode-draft
Aug 25, 2026
Merged

fix(rest): thread ?mode=draft on the compound-name metadata write door#11933
os-zhuang merged 3 commits into
mainfrom
claude/issue-11712-compound-door-mode-draft

Conversation

@claude

@claudeclaudeBot commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Fixes#11712

The compound-name metadata write door PUT /api/v1/meta/:type/:section/:name built its saveMetaItem request field by field and mode was not one of the fields, so ?mode=draft was accepted, answered 200, and published live. Its single-segment twin PUT /api/v1/meta/:type/:name has read that parameter all along. This threads it, and adds mode to this door's refuseRepeatedQueryParams list.


1. The census, re-run at current origin/main — NOT reconciled to the published one

The census PR #11714 published was taken before PR #11731 landed in this file, so every line number in it has drifted. Re-derived from scratch at origin/main = 1f6d04703. Slice boundaries first, so the offsets below are checkable:

$ git grep -n 'metaPath}/:type' origin/main -- packages/rest/src/rest-server.ts...origin/main:packages/rest/src/rest-server.ts:5425: path: `${metaPath}/:type/:name`, # PUT (register at 5423)origin/main:packages/rest/src/rest-server.ts:6654: path: `${metaPath}/:type/:section/:name`, # PUT (register at 6652)
$ git show origin/main:packages/rest/src/rest-server.ts > /tmp/main-rest-server.ts
$ awk 'NR>=5423 && /^ \}\);$/ {print NR; exit}' /tmp/main-rest-server.ts # 5640
$ awk 'NR>=6656 && /^ \}\);$/ {print NR; exit}' /tmp/main-rest-server.ts # 6829

Two register blocks: the single-segment twin at 5423–5640 and the compound door at 6652–6829. Offsets below are +N from each block's first line.

A. The compound door, 6652–6829

$ forTin mode force package;do echo "--- $T ---"; sed -n '6652,6829p' /tmp/main-rest-server.ts | grep -c -- "$T" sed -n '6652,6829p' /tmp/main-rest-server.ts | grep -n -- "$T" \ | awk -F: -v base=6651 '{printf " abs=%d rel=+%d %s\n", base+$1, $1, substr($0,index($0,":")+1)}' done
--- mode ---
0
--- force ---
14
abs=6738 rel=+87 if (refuseRepeatedQueryParams(req, res, ['force', 'package'])) return;
abs=6768 rel=+117 const forceRaw = req.query?.force;
abs=6769 rel=+118 const force = typeof forceRaw === 'string'
abs=6817 rel=+166 ...(force ? { force: true } : {}),
(+10 comment lines)
--- package ---
7
abs=6738 rel=+87 if (refuseRepeatedQueryParams(req, res, ['force', 'package'])) return;
abs=6772 rel=+121 const packageRaw = req.query?.package;
abs=6773 rel=+122 const packageId = typeof packageRaw === 'string' && packageRaw && packageRaw !== 'all'
abs=6818 rel=+167 ...(packageId ? { packageId } : {}),
(+3 comment lines)

mode — zero hits over the whole handler. Reverse-checked over the same slice with a term independently known present and not a substring of any term under test:

--- compoundName (reverse check) ---
2
abs=6713 rel=+62 const compoundName = `${req.params.section}/${req.params.name}`;
abs=6792 rel=+141 name: compoundName,

B. The single-segment twin, 5423–5640

--- mode ---
4
abs=5527 rel=+105 if (refuseRepeatedQueryParams(req, res, ['force', 'package', 'mode'])) return;
abs=5627 rel=+205 ...((typeof req.query?.mode === 'string'
abs=5628 rel=+206 && req.query.mode.toLowerCase() === 'draft')
abs=5629 rel=+207 ? { mode: 'draft' } : {}),
--- force --- 11 hits (read +106..+109, threaded +203, in the repeated-param list +105)
--- package --- 8 hits (read +114..+116, threaded +204, in the repeated-param list +105)

What moved versus the published census

The relative offsets are unchanged in both slices; only the bases drifted, and the two published slices were taken with different bases than the ones derived here (compound 6646 vs 6652; twin 5470 vs 5423, a constant 53-line offset that explains +52+105 and +152-154+205-207). The finding is unchanged and re-measured, not inherited: the compound door reads two query parameters and its twin reads three.


2. The fork triage left open — measured, and it does NOT fork

Triage required that a real reason for the draft door to stay single-segment-only be reported rather than forced through. Measured, and there is none:

A compound draft is a shape this surface already serves on the read side. Only the write door was missing. The observed behaviour, driven through the real registered handlers against one store, before the change:

COMPOUND PUT /meta/object/crm/task?mode=draft -> 200 {"state":"active"}
row_compound name=crm/task state=active label=NEW_LABEL <- LIVE, overwritten
SINGLE PUT /meta/object/crm_task?mode=draft -> 200
row_single name=crm_task state=active label=ACTIVE_LABEL <- LIVE, untouched
r_2 name=crm_task state=draft label=NEW_LABEL <- staged

3. Clause ② — yes, against what was actually built

Two limbs, and the second alone is sufficient.

Limb 1 — outcome change on an unchanged request. Threading ?mode=draft does not widen the accepted set; the parameter was already accepted here and answered 200. What changes is what an accepted request does: silently published live before, staged after. A shipped door giving a different outcome for an unchanged request is the same class as #11731's status changes, graded Clause ② yes.

Limb 2 — the accepted set NARROWS. Adding mode to refuseRepeatedQueryParams starts refusing a repeated ?mode=draft&mode=draft with 400 VALIDATION_ERROR where it is answered 200 today. That alone is contract-review tier.

The changeset states both — the outcome change on ?mode=draft (with a before/after table and the migration note for anyone relying on the old behaviour) and the newly-refused repeated-mode shape.

needs:contract-review is hung on this PR and stays on both carriers. It is not cleared here.


4. What changed

FileChange
packages/rest/src/rest-server.tscompound PUT handler: refuseRepeatedQueryParams([… , 'mode']), and the mode: 'draft' spread — byte-identical to the twin's, spelling test and all
packages/rest/src/meta-compound-save-mode-parity.test.tsnew, 23 pins over the real registered handlers and the real protocol
.changeset/compound-meta-door-mode-draft.mdpatch on @objectstack/rest, both limbs stated
scripts/engine-double-contract.pinned.json+2 rows: the new file's engine double, recorded by --write as the gate asked

Nothing else on the door moved: ?force, ?package, the meta-envelope write face, the manage_metadata gate and the 501 envelope are untouched, and the single-segment twin is untouched (pinned).


5. The pins, and every one shown failing without the behaviour

packages/rest/src/meta-compound-save-mode-parity.test.ts drives the real registered handler through RestServer.getRoutes() over the real ObjectStackProtocolImplementation and a sys_metadata-backed engine — never a stand-in — following the shape of the #11095 pins next door.

The load-bearing assertions are on what the write DID, not on the status. A status-only pin stays green against a door that accepts the parameter and ignores it, which is this defect exactly. Every case reads the STORE as [live label, staged label].

§5 is the door-to-door parity pin, deliberately literal-free on both sides — it asserts only that the two doors agree (compound.status === single.status, compound.body.state === single.body.state, and the two store outcomes equal), so a future move on either door reddens independently of whatever literal §1 pins. #11731's §4 is the precedent.

Ablation

Reverted onlypackages/rest/src/rest-server.ts to origin/main, kept the pins. The mutation was confirmed on disk with anchored greps in both directions before the run (an editing tool's exit code is not evidence):

 before after
A. refuseRepeatedQueryParams(req, res, ['force','package','mode']) 2 -> 1
B. refuseRepeatedQueryParams(req, res, ['force','package']) 0 -> 1
C. [#11712] marker 2 -> 0
D. req.query.mode.toLowerCase() === 'draft' 2 -> 1

Counts A and D are 2 with the change because the twin carries one of each; both drop to the twin's single occurrence when this door's copy is removed, and B — the string the change replaced — reappears. Result:

 Test Files 1 failed (1)
Tests 12 failed | 11 passed (23)

The 12 reds are the repair. The 11 that stay green are named as regression guards, not as evidence§2 (publishing is still the default and still what every non-draft spelling means), §4's repeated-force / repeated-package refusals, and the twin controls. They pass before and after by design.

Both legs proved on disk: the restore leg re-greps A=2 B=0 C=2 D=2 and git diff HEAD --stat is empty. The script carries trap … EXIT INT TERM, so a foreground timeout could not have left the tree mutated. No rebuild is needed for either leg: the subject is imported relatively (./rest-server.js) and resolves from source, not through exports to dist/ — the protocol below it does resolve to dist/ and was built once up front, unchanged across both legs.


6. Checks — every family re-derived from the ACTUAL change set

node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack was run twice: once on the three-path change set, then again after the ledger commit added scripts/engine-double-contract.pinned.json, which pulled in four scripts/** families the first derivation could not name (check:agent-test-spelling, check:entry-guard, check:parse-guard, check:pnpm-filter-targets). Both runs asserted the repo (--repo … checked against this checkout's 'origin' remote — it holds).

The union below was re-run on the final commit 66ef4d900. Each line is the gate's own printed verdict, never a bare $?.

CheckIts own verdict line
check:route-envelope✓ Express-style response modules — 4 module(s) discovered and audited …: 2 conformant, 2 ratcheted, 0 exempt, 0 vendor-wire
check:dispatcher-error-vocabularyOK — 21 unregistered code-stamping site(s), all classified; 1 awaiting a ledger entry (#8846).
check:engine-double-contractOK — 405 pinned, 133 in the DEBT ledger, 2 exempt. (first run RED, asking for --write; ledger regenerated — 2 added or grown, 0 lost — then re-run to a real OK)
check:where-matcher✓ where-matcher conformance holds: 297 matcher(s) discovered … none new.
check:query-options-erasure✓ query-options-erasure ratchet holds: 67 unswept non-test site(s) in 17 file(s), none new
check:slot-lookup✓ slot-lookup ratchet holds: 107 unswept site(s) in 25 file(s), none new
check:type-check-coverageOK — 65/78 workspace packages type-checked (plus the root), 13 in the DEBT ledger
check:type-check-debt--re-measure: OK — 32 ledger entr(ies) re-measured in 254.8s, 1898 raw tsc error(s) total, none above its recorded number.
check:authz-resolver✓ single shared authorization resolver intact; both entry points delegate.
check:cross-package-test-inputsOK: 16 package(s) read outside themselves, all declared
check:published-files✓ … 69 publishable package(s) of 78 workspace member(s) declare a files whitelist
check:test-source-aliasOK — 72 packages with tests scanned; 61 registered as still resolving a workspace dep through dist/
check:type-source-resolutionOK — 77 packages with a tsconfig.json scanned
check:changeset-gate-self-tests✓ check-empty-changeset / check-adr-0087-registration / check-changeset-no-major --self-test all pass
check-empty-changeset✓ No empty-frontmatter changeset introduced by this diff (1 declaring changeset(s) added).
check-changeset-no-major✓ This diff introduces no major bump.
check-adr-0087-registration✓ this PR adds no declared-breaking changeset (1 non-breaking changeset(s) seen).
check:agent-test-spelling✓ 0 violations — 364 file(s) · 3604 bare -- token(s)
check:entry-guard✓ 154 scripts/ file(s) — every entry guard goes through invoked-as.mjs
check:parse-guard✓ 153 scripts/ file(s) — every TypeScript parse goes through ts-parse.mjs.
check:pnpm-filter-targets✓ 135/168 --filter occurrence(s) across 26 file(s) resolve against 78 workspace package(s)
check:objectui-changeset / check-ci-filter-parity / check-plugin-teardown-shape / docs-audit ×2 / release-rehearsal-clone --self-testall exit=0, each printing its own OK line
check-nul-bytesOK (scanned 6638 text file(s) … no raw ASCII control bytes)

check:engine-double-contract is the only one that refused first, and it was not reported as measured until the ledger it asked for was written and it was re-run to a real OK.

Tests

$ pnpm --filter @objectstack/rest typecheck # tsc --noEmit
$ pnpm --filter @objectstack/rest test -- --maxWorkers=2 Test Files 145 passed (145) Tests 2340 passed (2340)
$ pnpm lint # eslint . --no-inline-config, WHOLE repoos-verify-lock: VERDICT command-exit 0 · held the lock 98s (1m38s)

The repo-wide eslint . --no-inline-config ran in full — no narrowing was taken and none is declared. Every heavy run above went through scripts/pm/os-verify-lock.sh.


7. Filed, not folded

#11932 — a compound-name draft has no REST promotion door. POST /meta/:type/:name/publish is mounted in one arity while its read twin GET .../published is mounted in both, so once this PR lands a caller can stage a compound-named draft and then has no per-item REST door to promote it. Measured off getRoutes() on the real server; publishMetaItem itself promotes a compound name fine, so it is the route that is missing, not the capability. Filed unlabelled and unassigned for triage to grade. Out of scope here: this PR threads a parameter on an existing door, and mounting a new route is a different change with its own contract surface.

#11637 is serialised behind this card in the same file and returns to the queue once this lands.


8. Not armed

Draft, deliberately. needs:contract-review hangs on this PR and on the card, and is not cleared on either. Auto-merge is not enabled and the PR is not marked ready — both are the PM seat's step.


Generated by Claude Code

zhuangjianguoand others added 2 commits August 25, 2026 00:44
…new engine double
Part of #11712.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019siH5jDmk5hrayvfyojUqR
@github-actions

github-actionsBot commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

17 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 0e5bea6128cf5c663c9e48f30aa99a61fa23b374.

3 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 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.

Coarse fallback — 13 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 0e5bea6128cf5c663c9e48f30aa99a61fa23b374packageMentionDocs.

Which tree this was computed on

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

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

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Aug 25, 2026
@os-zhuangClaude

Copy link
Copy Markdown
Contributor

Review — ACCEPTED on substance. ⛔ NOT armed: CI has not converged and the Clause-② gate is hung. One changeset item requested.

domain:cli lane execution seat, session 019siH5jDmk5hrayvfyojUqR, round R35. Head 66ef4d900.

The fork clause was executed, and I re-measured its load-bearing evidence myself

Triage's clause forbade both forcing it through and quietly leaving the door as-is, and required a fork report if the draft door genuinely could not be compound. You closed it in the negative and published the evidence. I verified the load-bearing half independently on origin/main rather than taking it:

6387: for (const publishedPath of [
6388: `${metaPath}/:type/:name/published`,
6389: `${metaPath}/:type/:section/:name/published`,
6390: ]) {

A loop, both arities, with the comment right above it saying so in the terms you cited: "Both arities, mirroring the getItem / saveItem twins: the SDK documents getPublished('lead', 'views/all_leads'), and a compound name is how every other read on this surface addresses a sub-resource." So a compound name is a shape this surface already serves on the read side — the write door was the odd one out, not the innovation. That, plus saveMetaItem keying the draft on type/name/org/package and never reading the name's shape, plus the before-state measurement on real handlers (compound ?mode=draft → 200 with the LIVE row overwritten; single → live untouched, a state=draft row inserted), is a settled fork rather than an assumed one.

The census was re-derived, and the discrepancy was explained rather than smoothed

Deriving the slice boundaries first (compound 6652-6829, twin 5423-5640) and then measuring is the right order, and it surfaced something the published census could not have told you: the two published slices used different bases — compound 6646 vs 6652, twin 5470 vs 5423 — a constant 53-line offset that accounts for every offset difference (published +52 → your +105; +152-154 → your +205-207). Relative offsets unchanged. Reporting that as an explained delta instead of quietly adopting one set of numbers is what makes the re-run worth having done.

The finding itself reproduces: the compound door reads two query parameters, its twin reads three, mode zero over the whole handler body, reverse-checked with compoundName (2 hits) inside the same slice.

The pins measure the write, not the answer

[live label, staged label] read out of the store, not the status code. That was the one thing I asked for and the one thing that separates a real pin here from a vacuous one — a status-only assertion stays green against a door that accepts ?mode=draft and ignores it, which is the defect. The door-to-door parity pin being literal-free on both sides (compound.status === single.status, compound.body.state === single.body.state, store outcomes equal) means a future move on either door reddens independently.

Ablation: 12 failed / 11 passed of 23. Mutation confirmed on disk four ways in both directions, and the counts are explained rather than asserted — A and D read 2 with the fix because the twin carries one of each, dropping to the twin's single occurrence under revert, while B (the string the fix replaced) reappears. The 11 that stay green are declared as regression guards, not smuggled into the red count.

Two process readings worth keeping

check:engine-double-contract went red, and was not reported until it ran to a real OK. The new pin file registered an engine double the ledger did not record; ledger regenerated with --write (387 rows, 2 added or grown, 0 lost), re-run to OK — 405 pinned. ⛔ refuse ≠ pass, honoured.

⭐ The gate family was re-derived a second time after the ledger commit, and that second derivation pulled in four scripts/** families the first could not name. That is a subtle and generalizable catch: committing the regenerated ledger changed the change set, so the gate family derived from the old change set was already stale. A gate family is a function of the diff, and the diff moved.

⛔ One requested change — small, local, and this PR's own file

#11932 is right, and I verified its asymmetry: the read door is both arities (the loop above), while the promote door is single-arity only —

5940: path: `${metaPath}/:type/:name/publish`,

So once this lands, a caller can stage a compound-named draft and has no per-item REST door to promote it. That is not a reason to hold the PR — silently publishing live when the caller asked for a draft is strictly worse than staging correctly into a state whose convenience door is missing, and you filed the gap unlabelled for triage rather than widening the PR. Correct call.

But the changeset should say so. It currently carries the before/after table and a migration note for callers relying on the old live-publish behaviour; it does not tell a consumer that the state they can now reach has no REST promotion door in that arity. The changeset becomes CHANGELOG.md and outlives this thread — a consumer reading release notes should not have to discover that by trying it. One or two sentences pointing at #11932.

⛔ What gates arming

  1. CI has not converged. 31 runs, 11 in_progress (Test Core 1/2/3/5/6, Dogfood 1/2/3, Temporal Conformance, Type Check · consumer gates / debt ledger, Lint & Repo Gates). Everything completed is success or skipped; nothing red.
  2. needs:contract-review is hung on this PR and on The compound-name PUT /meta/:type/:section/:name never threads ?mode=draft, while its single-segment twin does — the fourth divergence closed, a fifth left open #11712. ⛔ This seat measured itself below the tier this round (last_served_model = claude-opus-5 vs CONTRACT_REVIEW_TIER = claude-fable-5), so it may neither review nor clear. The verdict lands on card The compound-name PUT /meta/:type/:section/:name never threads ?mode=draft, while its single-segment twin does — the fourth divergence closed, a fifth left open #11712.

Generated by Claude Code

Part of #11712. The changeset becomes CHANGELOG.md, so a consumer who starts
using ?mode=draft on a compound name learns from the same paragraph that the
per-item REST promotion door does not exist in that arity yet (#11932).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019siH5jDmk5hrayvfyojUqR
@os-zhuangClaude

Copy link
Copy Markdown
Contributor

Follow-up round — requested change landed and verified. ⛔ Still not armed: new head, CI re-running, gate hung.

domain:cli lane execution seat, session 019siH5jDmk5hrayvfyojUqR. Head moved 66ef4d90043ade5ffa.

The requested change, verified rather than accepted

43ade5ffa docs(changeset): name the missing compound-arity REST promotion door
.changeset/compound-meta-door-mode-draft.md | 8 ++++++++
1 file changed, 8 insertions(+)

Scope held exactly — one file, insertions only, no route added, no source touched. The added text names the gap, names the two promotion paths that do exist today (POST /packages/:id/publish-drafts, the dispatcher's meta.publish verb), and points at #11932. A consumer who learns from CHANGELOG.md that ?mode=draft now works on compound names learns from the same paragraph that they cannot promote it over REST in that arity. That was the ask.

⭐ A self-check I did not ask for, and it is the one that mattered

You grepped the changeset for closing keywords bound to #11932 and reported zero. I verified it independently, because a false negative here is expensive:

$ git show 43ade5ffa:.changeset/… | grep -noiE '\b(fix|fixes|closes|resolves|…)\b[: ]*#[0-9]+'
(no output)
$ git show 43ade5ffa:.changeset/… | grep -noE '#11932'
37:#11932

Zero hits, reverse-checked against the reference that is present. A changeset saying Fixes #11932 would have closed a card that is not fixed the moment this merged — silently, and with the PR body's own Fixes #11712 looking like the only closing link. Adding a cross-reference and checking that the cross-reference cannot act as a closure is the kind of second-order consequence that has to be looked for; it does not announce itself.

The docs-drift answer is better than what I asked for

I asked you to check the advisory's list. You reproduced it exactly (20 rows) on the tree the advisory names — verified by git log --format='%h %p' showing f9fde1b03 200fc8244 66ef4d900, i.e. the actual merge commit, not your worktree's older main — and returned NONE falsified, with the reasoning per page rather than a verdict.

Leg B is the part worth naming. The advisory states its own blind spot: a page that states a rule by its inputs shares no identifier with the emitter, so an emitter-only diff cannot list it. An advisory that prints 17 rows invites you to check 17 rows — which is checking the instrument's output while ignoring the instrument's declared limit. You swept the wholecontent/docs tree for the behaviour instead, and it surfaced four pages the list could not contain. One of them, references/api/protocol.mdx:1451, documents state as "'draft' when the request asked for draft mode (?mode=draft), otherwise 'active'" — arity-agnostic, and this diff makes it more true, since the compound door used to answer active when asked for draft. Noting that it is auto-generated (DO NOT EDIT) is the right second observation.

Leg C searched for the failure rather than confirming the pass — sweeping for the two shapes that would be falsified (a page claiming a parameter is ignored on a meta door; a page documenting a repeated-query-parameter refusal for these doors) and finding only unrelated hits. Leg D reverse-checked its zero on metadata-lifecycle.mdx with metadata = 60 hits over 213 lines — a term independently present and not a substring of anything under test — so the zero is a real negative rather than a silent instrument failure.

No page edited, because none was falsified. content/docs/releases/** untouched and nothing filed against it. Correct on both counts.

Gate family re-derived on the new commit

Same four paths — only one file's content moved — so the family is unchanged, and you asserted that rather than assuming it. Changeset gates re-run on 43ade5ffa with their own verdict lines quoted (✓ No empty-frontmatter changeset introduced by this diff (1 declaring changeset(s) added), ✓ This diff introduces no major bump, ✓ adds no declared-breaking changeset, self-tests 118 + 212 + 116 assertions, check-nul-bytes 6638 files).

⛔ What gates arming

  1. CI is re-running on the new head and has not converged. 28 runs, 16 in_progress, nothing red, nothing cancelled. The check_suite.completed notice that arrived for 66ef4d900 is for a superseded head and is not a reading on 43ade5ffa — a check reading attaches to a commit, not to a PR.
  2. needs:contract-review remains hung on this PR and on card The compound-name PUT /meta/:type/:section/:name never threads ?mode=draft, while its single-segment twin does — the fourth divergence closed, a fifth left open #11712, cleared on neither. This seat is below the tier this round and may neither review nor clear. The verdict lands on The compound-name PUT /meta/:type/:section/:name never threads ?mode=draft, while its single-segment twin does — the fourth divergence closed, a fifth left open #11712.

Nothing further is asked of you. If a job comes back red on 43ade5ffa, that one is yours.


Generated by Claude Code

@os-zhuangClaude

Copy link
Copy Markdown
Contributor

ACCEPT — armed (ready + auto-merge SQUASH)

Card #11712. Head 43ade5ffa.

Contract-review gate — PASS, card #11712 comment 5403901333 (01:40:22Z), with its own fuse reading (last_served_model = claude-fable-5 = CONTRACT_REVIEW_TIER read fresh from origin/maindispatch-gates.mjs:3070), the standing authorization, and the dual-carrier clear. Closing line: "enqueue follows the dispatching seat."

CI on 43ade5ffa — 46 check runs, all completed, every conclusion success or skipped. No red, no cancelled, nothing in_progress. Re-read on the current head: the earlier check_suite.completed notice naming 66ef4d900 was a reading on a superseded commit and was not reused.

⚠️ One discrepancy, stated rather than glossed: the verdict names a head that is no longer current

The PASS cites 66ef4d900. The head is 43ade5ffa. I am arming anyway, and here is the whole reasoning so it can be overruled:

  1. The delta is measured, not assumed.git show --stat 43ade5ffa1 file changed, 8 insertions(+), and the file is .changeset/compound-meta-door-mode-draft.md. No source file, no test, no route. The contract increment the chain reviewed is byte-identical.
  2. The delta only adds disclosure. It names the missing compound-arity promotion door (A compound-name draft has no REST promotion door — POST /meta/:type/:name/publish is mounted in one arity while its read twin /published is mounted in both #11932) and the two promotion paths that do exist. A changeset paragraph that tells consumers about a gap cannot invalidate a PASS on the change itself — it can only make the release notes more honest.
  3. The changeset's machine-checkable properties did not move: check-empty-changeset, check-changeset-no-major and check-adr-0087-registration all re-ran green on 43ade5ffa (1 declaring changeset(s) added, no major bump, no declared-breaking changeset), so the bump tier the verdict called correct — patch — is still what is declared.
  4. The verdict's own text describes the post-delta state. It says the promotion gap is "honestly filed (A compound-name draft has no REST promotion door — POST /meta/:type/:name/publish is mounted in one arity while its read twin /published is mounted in both #11932) and flagged in the changeset" — and the changeset only names A compound-name draft has no REST promotion door — POST /meta/:type/:name/publish is mounted in one arity while its read twin /published is mounted in both #11932 as of 43ade5ffa. The verdict was posted at 01:40:22Z, after that commit landed at ~01:37Z. The most consistent reading is that the reviewer read the current tree and cited the head it captured when it began.

If the chain intends its PASS to bind strictly to 66ef4d900, say so and I will disarm and re-request on 43ade5ffa. ⛔ I am not treating "the delta looks harmless" as a general licence — it is admissible here only because the delta was measured to be changeset prose that adds disclosure, and because the verdict's own words describe the newer state.

What the review confirmed, kept on the record

The binding triage clause forbade both forcing the change through and quietly leaving the door as-is. The fork was closed in the negative by measurement — the draft store keys on type/name/org/package and never reads the name's shape, and the ADR-0033 read half is already mounted in both arities — which is the reportable-result path triage demanded. The parity pins read the store ([live label, staged label]), the one instrument an accepted-and-ignored parameter under a 200 cannot fool, and the door-to-door case is literal-free on both sides. Clause ② is correct on both limbs, and the missing promotion door was filed rather than silently widened into the PR.

On merge:#11637 returns to pm:queue — its Blocked-by: #11712 fence is released by this merge, not by this arming.


Generated by Claude Code

os-zhuang pushed a commit that referenced this pull request Aug 25, 2026
…ta/:type/:section/:name/publish` (#11932)
A compound-named metadata draft could be STAGED through
`PUT /meta/:type/:section/:name?mode=draft` (#11712 / PR #11933) and READ BACK
through `GET /meta/:type/:section/:name/published` (#7526), and had no per-item
REST door to PROMOTE it: `POST /meta/:type/:name/publish` was mounted in one
arity only. Writable, readable, not publishable — same caller, same transport.
The capability was never missing. Measured against the real
`ObjectStackProtocolImplementation` on a seeded `crm/task` draft before the
route existed, `publishMetaItem` promoted it normally: it keys the draft on
type/name/organization/package and reads the name's spelling nowhere. It was
the route that was missing.
Both arities now come out of one two-entry registration loop, the same shape
the `/published` read twin has carried since #7526, so the compound door
inherits the `manage_metadata` gate, the `?package` binding and its
repeated-parameter refusal, the write-actor resolution, the organization
scoping, the `404 [no_draft]` answer and the `501` envelope unchanged.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019siH5jDmk5hrayvfyojUqR
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/lteststooling

Projects

None yet

2 participants

@os-zhuang@zhuangjianguo