Skip to content

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

Description

@os-zhuang

Measured while implementing #11391 (giving @objectstack/client's meta.saveItem the query-string options its route reads). Separate surface, filed rather than folded — it is a server-side route defect and #11391's ruling is client-side only.

What was measured

packages/rest/src/rest-server.ts registers two PUT doors onto one generic saveMetaItem:

  • single-segment PUT /meta/:type/:name reads three query parameters and threads all three:
    if (refuseRepeatedQueryParams(req, res, ['force', 'package', 'mode'])) return;
    ...
    ...(force ? { force: true } : {}),
    ...(packageId ? { packageId } : {}),
    ...((typeof req.query?.mode === 'string'
        && req.query.mode.toLowerCase() === 'draft')
        ? { mode: 'draft' } : {}),
    
  • compound-name PUT /meta/:type/:section/:name reads two:
    if (refuseRepeatedQueryParams(req, res, ['force', 'package'])) return;
    ...
    ...(force ? { force: true } : {}),
    ...(packageId ? { packageId } : {}),
    
    There is no mode read and no mode in the request object it builds.

So PUT /api/v1/meta/object/views/all_leads?mode=draft is accepted and published live, while the byte-identical intent against a single-segment name stages a draft. The parameter is not refused — it is silently ignored, and the caller gets a 200 saying the save succeeded.

mode is also absent from the compound door's refuseRepeatedQueryParams list, so the #6877 repeated-parameter guard does not cover it there either.

Why this looks like the same class the twin pair keeps closing

The compound route's own comments describe it as "word for word the same operation" as its single-segment twin — one generic saveMetaItem reached by a name spelled in two segments — and record four divergences closed on that reason:

?mode is the same shape, one field along: one door reading a parameter and its literal twin ignoring it. #11095's comment explicitly scopes its own ruling ("⛔ NOT a licence for every door that reaches this gate") to the dispatcher, which has no query string at all — that carve-out does not describe this door, which has a query string and already reads two parameters off it.

Not asserted here

Whether the compound door should gain mode is a contract question, not folded into this report — the draft/publish door may have its own reason to be single-segment only. What is measured is only that the two doors disagree and that the disagreement is silent.

Where it would land

packages/rest/src/rest-server.ts (the compound PUT handler), plus the parity pins in packages/rest/src/meta-compound-save-force-parity.test.ts, which already pins the force half of exactly this parity.

Related: #11095 (threaded ?force on this same door), #11391 (the client-side half — that card is not addressed by this one), #7019 (the twin-parity ruling).

Measured on origin/main at 4ceae8ab0.


Generated by Claude Code


Generated by Claude Code

Activity

  1. added theissue type on Aug 24, 2026
  2. os-zhuang commented on Aug 24, 2026

    @os-zhuang
    ContributorAuthor

    Triage (session session_01LrqnyLhY3kYBBRird4peBT, 2026-08-24): lands in packages/rest/src/rest-server.ts ⇒ domain:cli, pm:queue, type Bug. The twin-parity family ruling is inherited with its reason: four divergences on this exact door pair were each closed on "word for word the same operation" (#6603/#7019 capability gate, #8805 org, #7035 envelope, #11095 ?force), and #11095's dispatcher carve-out is measured as not describing this door (it has a query string and already reads two parameters). A silently-ignored mode=draft that publishes live is the harmful direction of the divergence.

    Inheritance is a mechanism assumption, not a verdict: if implementation surfaces a real reason the draft door must stay single-segment-only, report the fork on this card — do not force it through, and do not silently drop to leaving the door as-is. Also add mode to the compound door's refuseRepeatedQueryParams list (the #6877 guard gap the card measures). Claiming seat evaluates Clause-② at claim time: this changes what an accepted request does on a published REST door.


    Generated by Claude Code

  3. self-assigned this
    on Aug 25, 2026
  4. os-zhuang commented on Aug 25, 2026

    @os-zhuang
    ContributorAuthor

    Claim — domain:cli lane execution seat, session 019siH5jDmk5hrayvfyojUqR, round R35.

    Branch: claude/issue-11712-compound-door-mode-draft.
    Declared file surface: packages/rest/src/rest-server.ts (the compound handler body, PUT ${metaPath}/:type/:section/:name) + its pins + a changeset.

    Serial released. This card was hard-serialised behind PR #11731 under ruling ① (same file, no region exemption). #11731 merged 2026-08-25T00:22:19Z — the merge is what releases a serial, not the arming. Verified with pull_request_read method:get.

    Fold-or-serial with #11637: SERIAL, not fold. #11637 lands in the same file and is therefore hard-serial with this card. I am not folding them, and the reason is the one the #11683/#11684 fold turned on: that pair was folded because they shared one question. These two share only a file. #11712 is about whether a metadata write door threads a query parameter it already accepts; #11637 is about whether the server parses its own config schema at construction. Two independent risks, two contract-review surfaces, one review each. #11637 is now pm:blocked with Blocked-by: #11712 and returns to the queue when this lands.

    Clause ②: yes — evaluated at claim time as triage's comment 5396584183 requires. Two limbs, and the second is the load-bearing one:

    1. Threading ?mode=draft does not widen the accepted set — the parameter is already accepted on this door and answered 200. What changes is what an accepted request does: today it is silently published live, afterwards it drafts. A shipped door giving a different outcome for an unchanged request is the same class as fix(rest): classify record-share and analytics refusals at the shared /data door #11731's status changes, which were graded Clause ② yes.
    2. Adding mode to this door's refuseRepeatedQueryParams list narrows the accepted set — a repeated mode starts being refused where it is accepted today. That alone is contract-review tier.

    The changeset must state both: the outcome change on ?mode=draft, and the newly-refused repeated-mode shape.

    ⛔ The binding clause from triage, carried verbatim

    The twin-parity family ruling is inherited with its reason: four divergences on this exact door pair were each closed on "word for word the same operation" (#6603/#7019 capability gate, #8805 org, #7035 envelope, #11095 ?force), and #11095's dispatcher carve-out is measured as not describing this door (it has a query string and already reads two parameters). A silently-ignored mode=draft that publishes live is the harmful direction of the divergence.

    Inheritance is a mechanism assumption, not a verdict: if implementation surfaces a real reason the draft door must stay single-segment-only, report the fork on this card — do not force it through, and do not silently drop to leaving the door as-is.

    Both failure modes are named there and both are forbidden. If the measurement says the compound door cannot draft, that is a reportable result, not a reason to quietly ship nothing.

    The starting measurement is already on the record, and must be re-verified rather than reused

    PR #11714 (merged) published a census over this exact handler body, then rest-server.ts:6646-6824:

    • force — read at +117, threaded at +166.
    • package — read at +121, normalized at +122 ('all'/empty → undefined), threaded at +167; also in refuseRepeatedQueryParams(['force','package']) at +87.
    • mode — zero hits over the whole handler body, reverse-checked with compoundName (2 hits) over the same slice.
    • The single-segment twin (5470-5636) reads all three, including mode in its repeated-param list at +52 and threading at +152-154.

    ⛔ #11731 landed in this file after that census was taken, so those line numbers have moved. Re-derive the slice boundaries and re-run the census at current origin/main before writing anything, and publish what you actually measure.

    ⚠️ The client-side docstring shipped in #11714 explicitly warns the next reader not to "harden" this by refusing the whole options bag on a compound name — force and packageId both work there. The repair belongs on this door, which is what this card is.


    Generated by Claude Code

  5. claude commented on Aug 25, 2026

    @claude
    Contributor
    {
      "issue": 11712,
      "status": "done",
      "branch": "claude/issue-11712-compound-door-mode-draft",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/11933",
      "premise_still_valid": true,
      "summary": "The compound-name PUT /api/v1/meta/:type/:section/:name built its saveMetaItem request field by field with no `mode` field, so ?mode=draft was accepted, answered 200 and PUBLISHED LIVE while its single-segment twin staged a draft. Threaded `mode: 'draft'` byte-identically to the twin (same case-insensitive spelling test) and added `mode` to this door's refuseRepeatedQueryParams list, closing the #6877 guard gap in the same stroke. The fork triage left open is CLOSED IN THE NEGATIVE and reported on the card: saveMetaItem keys the draft on type/name/org/package and never reads the name's shape, and the ADR-0033 read half GET /:type/:section/:name/published is already mounted in BOTH arities (#7526) citing the SDK's getPublished('lead','views/all_leads') — a compound draft is a shape this surface already serves on the read side, only the write door was missing. Clause 2 = yes on both limbs; the changeset states both. needs:contract-review hung on PR #11933 and left on both carriers; PR is draft, auto-merge NOT enabled.",
      "census_rerun": {
        "note": "Re-derived from scratch at origin/main = 1f6d04703 (PR #11731 had landed in this file after the published census). NOT reconciled to the published numbers. Slice boundaries derived first so the offsets are checkable.",
        "commands": [
          "git grep -n 'metaPath}/:type' origin/main -- packages/rest/src/rest-server.ts",
          "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",
          "for T in mode force package; do 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",
          "(same loop with sed -n '5423,5640p' and base=5422 for the twin)"
        ],
        "slices": {
          "compound_PUT_register_block": "6652-6829 (path literal at 6654, handler at 6655)",
          "single_segment_PUT_register_block": "5423-5640 (path literal at 5425, handler at 5426)"
        },
        "compound_door_raw": {
          "mode": "count=0 — ZERO HITS over the whole handler body",
          "force": "count=14 — refuseRepeatedQueryParams(req,res,['force','package']) abs=6738 rel=+87; const forceRaw = req.query?.force abs=6768 rel=+117; const force = typeof forceRaw === 'string' abs=6769 rel=+118; ...(force ? { force: true } : {}) abs=6817 rel=+166; +10 comment lines",
          "package": "count=7 — same repeated-param list abs=6738 rel=+87; const packageRaw = req.query?.package abs=6772 rel=+121; normalization ('all'/empty -> undefined) abs=6773 rel=+122; ...(packageId ? { packageId } : {}) abs=6818 rel=+167; +3 comment lines",
          "reverse_check": "compoundName count=2 over the SAME slice — abs=6713 rel=+62 `const compoundName = `${req.params.section}/${req.params.name}`;` and abs=6792 rel=+141 `name: compoundName,`. Independently known present and NOT a substring of mode/force/package."
        },
        "single_segment_twin_raw": {
          "mode": "count=4 — refuseRepeatedQueryParams(req,res,['force','package','mode']) abs=5527 rel=+105; the threading spread abs=5627-5629 rel=+205..+207",
          "force": "count=11 — read abs=5528-5531 rel=+106..+109, threaded abs=5625 rel=+203, in the repeated-param list abs=5527 rel=+105",
          "package": "count=8 — read abs=5536-5538 rel=+114..+116, threaded abs=5626 rel=+204, in the repeated-param list abs=5527 rel=+105"
        },
        "what_moved_vs_published": "Relative offsets are UNCHANGED in both slices; only the bases drifted, and the two published slices used different bases than the ones derived here (compound 6646 vs 6652; twin 5470 vs 5423 — a constant 53-line offset that explains the published +52 -> my +105 and +152-154 -> my +205-207). Finding re-measured, not inherited: the compound door reads TWO query parameters, its twin reads THREE."
      },
      "clause_2": {
        "answer": "yes",
        "limb_1_outcome_change": "?mode=draft was already ACCEPTED here and answered 200 — the accepted set does not widen. What changes is what an accepted request DOES: silently published live before, staged after. Same class as #11731's status changes.",
        "limb_2_narrowing": "Adding `mode` to refuseRepeatedQueryParams NARROWS the accepted set: a repeated ?mode=draft&mode=draft is answered 200 today and 400 VALIDATION_ERROR after. Sufficient on its own.",
        "changeset": ".changeset/compound-meta-door-mode-draft.md states BOTH, with a before/after table and a migration note for callers relying on the old live-publish behaviour.",
        "label": "needs:contract-review hung on PR #11933 (additive POST /labels, read back). NOT cleared on either carrier. PR is draft, not marked ready, auto-merge not enabled."
      },
      "fork_report": {
        "verdict": "NO FORK — the compound door CAN draft. Reported rather than assumed, as triage required.",
        "evidence": [
          "saveMetaItem keys the draft on type/name/organizationId/packageId and passes `state` to repo.put; nothing on that path reads the name's SHAPE, so 'crm/task' is a draft key exactly as 'crm_task' is.",
          "The ADR-0033 read half GET /:type/:section/:name/published is ALREADY mounted in both arities (#7526), whose own comment cites the SDK's getPublished('lead','views/all_leads') and calls a compound name 'how every other read on this surface addresses a sub-resource'.",
          "Measured before the change, real registered handlers against one store: COMPOUND ?mode=draft -> 200 {\"state\":\"active\"} and row_compound (LIVE) overwritten with the new body; SINGLE ?mode=draft -> 200, live row untouched, a new state=draft row inserted."
        ]
      },
      "tests": "ALL heavy runs through scripts/pm/os-verify-lock.sh; exit codes captured BEFORE any pipe; each verdict quoted from the gate's own printed line. Gate UNION re-run on the FINAL commit 66ef4d900 (git rev-parse --short HEAD).\n\nNEW PINS — packages/rest/src/meta-compound-save-mode-parity.test.ts, 23 cases, drives the REAL registered handler via RestServer.getRoutes() over the real ObjectStackProtocolImplementation + a sys_metadata-backed engine (no stand-in). Load-bearing assertions read the STORE as [live label, staged label], not the status — a status-only pin stays green against a door that accepts the parameter and ignores it, which is this defect. Door-to-door parity pin (§5) is literal-free on BOTH sides: compound.status === single.status, compound.body.state === single.body.state, and the two store outcomes equal (#11731 §4 precedent). WITH FIX: 'Test Files 1 passed (1) / Tests 23 passed (23)'.\n\nABLATION — reverted ONLY packages/rest/src/rest-server.ts to origin/main, kept the pins. Mutation confirmed ON DISK with anchored greps in BOTH directions before the run (an editing tool's exit code is not evidence): 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. (A and D are 2 with the fix because the TWIN carries one of each; they drop to the twin's single occurrence, and B — the string the fix replaced — reappears.) RESULT: 'Test Files 1 failed (1) / Tests 12 failed | 11 passed (23)'.\nREBUILD: none needed for either leg, and this is stated rather than skipped — the mutated subject is imported RELATIVELY ('./rest-server.js') and resolves from SOURCE, not through exports to dist/. The protocol under it DOES resolve to dist/ (KNOWN_UNALIASED_TEST_IMPORTS) and was built once up front (`pnpm --filter '@objectstack/rest^...' build`, VERDICT command-exit 0), unchanged across both legs.\nRESTORE LEG proved on disk too: re-grep A=2 B=0 C=2 D=2 and `git diff HEAD --stat` empty + `git status --porcelain` empty. The ablation script carries trap '<restore>' EXIT INT TERM, so a foreground cap-kill could not have left the tree mutated.\nGREEN-BOTH-SIDES, reported as regression guards and NOT as red-before evidence: the 11 that stay green under ablation are §2 (publishing is still the default and still what every non-'draft' spelling means), §4's repeated-`force` and repeated-`package` refusals (400 VALIDATION_ERROR, nothing written), and the twin controls.\n\nPACKAGE: `pnpm --filter @objectstack/rest typecheck` -> echoed '> @objectstack/rest@17.2.0 typecheck' + '> tsc --noEmit', VERDICT command-exit 0 (script name echoed, so not a zero-match no-op). `pnpm --filter @objectstack/rest test -- --maxWorkers=2` -> 'Test Files 145 passed (145) / Tests 2340 passed (2340)'.\n\nREPO-WIDE LINT: `pnpm lint` (= eslint . --no-inline-config) ran IN FULL — 'os-verify-lock: VERDICT command-exit 0 · held the lock 98s (1m38s)', no output. NO narrowing was taken and none is declared.\n\nGATE FAMILY re-derived from the ACTUAL change set with `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack`, run TWICE — the second run after the ledger commit added scripts/engine-double-contract.pinned.json, which pulled in FOUR scripts/** families the first derivation could not name. Provenance line checked both times ('--repo objectstack-ai/objectstack checked against this checkout's origin remote — it holds').\n  check:route-envelope — '✓ Express-style response modules — 4 module(s) discovered and audited (walked, not enumerated — #9937), 12 hand-built body/bodies: 2 conformant, 2 ratcheted, 0 exempt, 0 vendor-wire'\n  check:dispatcher-error-vocabulary — 'check-dispatcher-error-vocabulary: OK — 21 unregistered code-stamping site(s), all classified; 1 awaiting a ledger entry (#8846).'\n  check:engine-double-contract — FIRST RUN RED (exit=1): 'RETAINED [delete]/[update]: packages/rest/src/meta-compound-save-mode-parity.test.ts pins 1 engine double(s) that the pinned ledger does not record … Run --write and commit.' Ledger regenerated: 'check-engine-double-contract --write: 387 (file, verb) row(s), 2 added or grown, 0 lost.' RE-RUN TO A REAL OK: 'check-engine-double-contract: OK — 405 pinned, 133 in the DEBT ledger, 2 exempt.' (refuse != pass: not reported as measured until it ran to OK)\n  check:where-matcher — '✓ where-matcher conformance holds: 297 matcher(s) discovered, 297 answer the combinator battery correctly or refuse it loudly (184 refuse). 0 silently-wrong … none new.'\n  check:query-options-erasure — '✓ query-options-erasure ratchet holds: 67 unswept non-test site(s) in 17 file(s), none new'\n  check:slot-lookup — '✓ slot-lookup ratchet holds: 107 unswept site(s) in 25 file(s), none new'\n  check:type-check-coverage — 'OK — 65/78 workspace packages type-checked (plus the root), 13 in the DEBT ledger'\n  check:type-check-debt — needed the BUILT closure, so the closure was built exactly as lint.yml does (`pnpm exec turbo run build --filter=./packages/* --filter=./packages/*/*` -> 'Tasks: 70 successful, 70 total') and then: 'check-type-check-coverage --re-measure: OK — 32 ledger entr(ies) re-measured in 254.8s, 1898 raw tsc error(s) total, none above its recorded number.' @objectstack/rest is IN TEST_DEBT (155), so this gate genuinely applies; it did not drift up. (One pre-existing surplus of -1 on @objectstack/plugin-approvals, unrelated, and the gate itself says 'Not an error'.)\n  check:authz-resolver — '✓ single shared authorization resolver intact; both entry points delegate.'\n  check:cross-package-test-inputs (and scripts/check-cross-package-test-inputs.mjs) — 'OK: 16 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob.'\n  check:published-files — '✓ 69 publishable package(s) of 78 workspace member(s) declare a `files` whitelist …'\n  check:test-source-alias — 'check-test-source-alias OK — 72 packages with tests scanned; 61 registered as still resolving a workspace dep through dist/'\n  check:type-source-resolution — 'check-type-source-resolution OK — 77 packages with a tsconfig.json scanned'\n  check:changeset-gate-self-tests — '✓ check-empty-changeset --self-test: 118 assertions' + '✓ check-adr-0087-registration --self-test: 212 assertions' + '✓ check-changeset-no-major --self-test: 116 assertions'\n  check-empty-changeset — '✓ No empty-frontmatter changeset introduced by this diff (1 declaring changeset(s) added).'\n  check-changeset-no-major — '✓ This diff introduces no `major` bump.'\n  check-adr-0087-registration — '✓ this PR adds no declared-breaking changeset (1 non-breaking changeset(s) seen).'\n  check:agent-test-spelling — '✓ check-agent-test-spelling: 0 violations — 364 file(s) · 3604 bare `--` token(s)'\n  check:entry-guard — '✓ check:entry-guard: 154 scripts/ file(s) — every entry guard goes through invoked-as.mjs'\n  check:parse-guard — '✓ check:parse-guard: 153 scripts/ file(s) — every TypeScript parse goes through ts-parse.mjs.'\n  check:pnpm-filter-targets — '✓ 135/168 `--filter` occurrence(s) across 26 file(s) resolve against 78 workspace package(s)'\n  check:objectui-changeset — '✓ objectui-range --self-test: all checks passed'\n  scripts/check-ci-filter-parity.mjs — 'OK: all 96 declared cross-package glob(s) (81 unique) are covered by core or crosspkg'\n  scripts/check-plugin-teardown-shape.mjs — '✓ 63 Plugin implementation(s) across 4639 source(s); every teardown-shaped method sits beside a real destroy()'\n  scripts/docs-audit/check-affected-docs.mjs — exit=0; scripts/docs-audit/check-drift-comment.mjs — '✓ 56 cases pass across 5 fixture diff(s).'\n  scripts/pm/release-rehearsal-clone.mjs --self-test — '✓ self-test passed'\n  check-nul-bytes — 'OK (scanned 6638 text file(s) … no raw ASCII control bytes)'; plus my own grep -naP over the 4 touched files, no hits.\n\nNOTE ON THE RE-RUN: the changeset-diffing gates were run once before the ledger commit and again on 66ef4d900 — the second run's own counts prove the re-run mattered ('1 declaring changeset(s) added' / '1 non-breaking changeset(s) seen' vs '0' before). CI convergence is the PM's step; nothing was waited on here.",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #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 /:type/:section/:name/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 registered server; publishMetaItem itself promotes fine, so it is the route that is missing, not the capability. Filed UNLABELLED and UNASSIGNED for triage to grade (concrete measured defect, not an observation), with a session-URL footer read back intact."
      ]
    }

    Generated by Claude Code

  6. claude commented on Aug 25, 2026

    @claude
    Contributor
    {
      "issue": 11712,
      "status": "done",
      "round": "R35 follow-up — requested change addressed; head moved 66ef4d900 -> 43ade5ffa",
      "branch": "claude/issue-11712-compound-door-mode-draft",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/11933",
      "premise_still_valid": true,
      "summary": "Requested change made: the changeset now names the missing compound-arity REST promotion door, so a consumer reading CHANGELOG.md learns from the same paragraph that the state ?mode=draft can now reach has no per-item promotion door in that arity. One new commit, 43ade5ffa, touching only .changeset/compound-meta-door-mode-draft.md. The route was NOT added and the PR was NOT widened. Changeset gates re-run on the new commit and quoted below. Docs-drift re-derived on the exact tree the advisory names: 20 rows reproduced exactly, and NO page is falsified by this diff — reported with the command and the reasoning per page. PR remains draft, auto_merge null, needs:contract-review still hung on both carriers.",
      "requested_change": {
        "what": "The changeset must name the #11932 gap.",
        "where": ".changeset/compound-meta-door-mode-draft.md, placed immediately after the before/after table and the existing migration note — i.e. where a consumer learns ?mode=draft now works on this arity.",
        "text_added": "⚠️ **The draft you can now stage has no per-item REST promotion door in this arity.** `POST /meta/:type/:name/publish` is mounted for single-segment names only, while its read twin `GET /meta/:type/:section/:name/published` is mounted for both — so a compound-named draft is writable and readable over REST and not promotable there. Until that route exists, promote through `POST /packages/:id/publish-drafts` (whole-package) or the runtime dispatcher's own `meta.publish` verb. Tracked in #11932; this release does not change it.",
        "scope_held": "No route added, no source file touched, no other file touched. `git show --stat 43ade5ffa` = 1 file changed.",
        "closing_keyword_check": "grep -noiP '\\b(fix|fixes|fixed|close|closes|closed|resolve|resolves|resolved)\\b[:\\s]*#[0-9]+' over the changeset returns NOTHING — '#11932' is reached via 'Tracked in', so the changeset cannot close the card it points at. (The PR body's only match remains the intended first line, `Fixes #11712`.)"
      },
      "changeset_gates_on_43ade5ffa": {
        "note": "Gate family re-derived on the new commit first — the change set is still the SAME 4 paths (only one file's CONTENT moved), so the family is unchanged; derivation asserted the repo and printed 'committed 4, working tree 0, untracked 0'. Exit codes captured before any pipe; verdict lines are the gates' own.",
        "check-empty-changeset": "'Diffing HEAD from 1f6d04703 (merge base with origin/main).' / '✓ No empty-frontmatter changeset introduced by this diff (1 declaring changeset(s) added).' (exit=0)",
        "check-changeset-no-major": "'Diffing HEAD from 1f6d04703 (merge base with origin/main).' / '✓ This diff introduces no `major` bump.' (exit=0)",
        "check-adr-0087-registration": "'✓ check-adr-0087-registration: this PR adds no declared-breaking changeset (1 non-breaking changeset(s) seen).' (exit=0)",
        "check:changeset-gate-self-tests": "'✓ check-empty-changeset --self-test: 118 assertions over real temp git repos' + '✓ check-adr-0087-registration --self-test: 212 assertions' + '✓ check-changeset-no-major --self-test: 116 assertions' (exit=0)",
        "check-nul-bytes": "'OK (scanned 6638 text file(s) -- 6638 tracked, 0 untracked-not-ignored; skipped 5 binary; no raw ASCII control bytes).' (exit=0); plus my own grep -naP over the edited file, no hits."
      },
      "docs_drift": {
        "tree_answered_on": "f9fde1b03416043db489fb6b07b4326c0d02fd45 — the merge of head 66ef4d900 into base 200fc8244, i.e. the tree the advisory names, NOT the PR head and NOT my worktree's older main. Reproduced with the advisory's own recipe in a throwaway comparison worktree (`git fetch origin f9fde1b03… && git worktree add … f9fde1b03…`), verified by `git log --format='%h %p'` -> 'f9fde1b03 200fc8244 66ef4d900'.",
        "command": "node scripts/docs-audit/affected-docs.mjs --json 200fc82446d75da1bd1eed595ba15c72849ccc47",
        "reproduction": "EXACT. 'summary': '20 docs name something this change touched (2 anchor(s) — 0 symbol, 2 route, 0 sdk, 0 literal, 0 command, 0 rule) across 1 changed package(s)'. 20 rows = the advisory's 17 hand-written + 3 release-owned. Anchors: route '/:type/:section/:name/published' and route '/meta/:type/:name'.",
        "verdict": "NONE. No page in content/docs is falsified by this diff.",
        "how_the_class_was_checked": [
          "A. The 17 hand-written rows grepped for the behaviour that moved (`mode=draft` / `?mode` / `:section` / `section/:name` / `compound`) — exactly TWO hits, both read in full context and both unaffected. (1) content/docs/api/declarative-endpoints.mdx:284 'a perfectly valid endpoint body gets the same 403, in ?mode=draft as well as direct writes' — that is the `api` type's allowRuntimeCreate:false refusal firing BEFORE body validation, on a SINGLE-segment name, and it fires regardless of mode; untouched. (2) content/docs/api/error-catalog.mdx:597 'Compound arity. A name containing / exempts the request…' — the INVALID_REQUEST type-name check's arity exemption; says nothing about `mode` and this diff does not touch that refusal.",
          "B. The advisory's own stated blind spot (a page stating a rule by its INPUTS shares no token with the emitter) answered by sweeping the WHOLE content/docs tree, not just the 17 rows: every page naming `?mode` / `mode=draft` / `mode:'draft'`, and every page naming the compound write path. Four further pages surfaced, none falsified. content/docs/references/api/protocol.mdx:1451 documents the response `state` as \"'draft' when the request asked for draft mode (?mode=draft), otherwise 'active'\" — arity-agnostic, and this diff makes it MORE true (the compound door used to answer 'active' when asked for draft); it is also AUTO-GENERATED ('DO NOT EDIT. Run build-docs.ts to regenerate'). content/docs/permissions/authorization.mdx:208 and content/docs/deployment/validating-metadata.mdx:378 both describe draft staging at the `saveMetaItem` / lifecycle level with no arity claim, and neither asserts a compound REST promotion door, so neither is falsified by this diff nor by #11932.",
          "C. Swept specifically for the two shapes that WOULD be falsified: (i) any page claiming a parameter is ignored / not read / has no effect on a meta door — 2 hits, both unrelated (content/docs/ai/agents.mdx source-mode routing; content/docs/deployment/cli.mdx `--org` in user mode); (ii) any page documenting a repeated-query-parameter refusal for these doors — 1 hit, content/docs/api/data-api.mdx:79, which is the `/data` door's `?filter` and is not this door.",
          "D. content/docs/api/metadata-api.mdx and content/docs/concepts/metadata-lifecycle.mdx were checked by name as the two most likely lifecycle pages. metadata-api.mdx has no draft/publish/query-parameter statement for this door at all (its only 'publish' hits are `POST /packages/publish`). metadata-lifecycle.mdx has ZERO hits for draft / /publish / compound / section — REVERSE-CHECKED with a term independently known present and not a substring of any term under test: 'metadata' = 60 hits over 213 lines. It is on the list because it names the route `PUT /api/v1/meta/view/case_grid`, i.e. the `/meta/:type/:name` anchor — the single-segment twin, which this diff does not touch."
        ],
        "release_owned": "content/docs/releases/implementation-status.mdx, v14.mdx, v17.mdx — read-only, NOT edited, and nothing in them was measured as falsified either. Nothing filed against them.",
        "edits_made": "NONE. No doc page was edited, because none was falsified."
      },
      "ci_on_66ef4d900": "Read at follow-up time: 31 check runs, nothing red. Completed = success or skipped (Build Core, Test Core 2/3/4/5/6, Dogfood 2/3 + Verify CLI, Temporal Conformance, Type Check source/consumer/workspace, Check Changeset, PR-size, single-writer-path, Part-of-must-not-close, docs-drift). Still in_progress at that read: Test Core 1/6, Dogfood 1/3 and 3/3, Lint & Repo Gates, Type Check debt ledger. The new commit 43ade5ffa supersedes that run; a red job is mine to fix and none has appeared.",
      "arming": "UNCHANGED and NOT touched. PR 11933: draft=true, auto_merge=null, mergeable_state=blocked. needs:contract-review still on the PR and on card #11712, cleared on neither. The other labels on the PR (documentation, size/l, tests, tooling) are the auto-label/size bot's, not mine — recorded, not corrected.",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #11932 (unchanged, still unlabelled and unassigned): a compound-name draft has no REST promotion door. Now also named in the changeset per this round's requested change, via 'Tracked in #11932' — a pointer, never a closing keyword."
      ]
    }

    Generated by Claude Code

  7. os-zhuang commented on Aug 25, 2026

    @os-zhuang
    ContributorAuthor

    Contract review: PASS — PR #11933 at head 66ef4d900. Reviewed the actual diff against the card and the inherited twin-parity ruling (#7019, applied a fifth time): the mode threading is byte-identical to the single-segment twin (same case-insensitive spelling test), the repeated-mode guard joins the #6877 list in the same stroke, and the fork triage left open is closed in the negative BY MEASUREMENT (the draft store keys on type/name/org/package and never reads the name's shape; the ADR-0033 read half is already mounted in both arities) — exactly the reportable-result path the triage comment demanded. The parity pins read the STORE ([live label, staged label]), not the status — the one instrument this defect (accepted-and-ignored parameter under a 200) cannot fool — and the door-to-door parity case is literal-free on both sides. The clause-② declaration is correct on both limbs (outcome change for an accepted request; repeated-mode 200→400 narrowing), the changeset states both with a migration note, and patch is the right tier — nothing new is promised; the door starts honoring the contract its twin already documents. The missing compound-arity promotion door is honestly filed (#11932) and flagged in the changeset rather than silently widened into this PR. Tier reading this round: get_session.external_metadata.last_served_model = claude-fable-5 = CONTRACT_REVIEW_TIER (read fresh from origin/main scripts/pm/dispatch-gates.mjs:3070). Standing authorization: maintainer 2026-08-23 「要不还是你挂个定时处理审核吧」. Clearing needs:contract-review from both carriers in this stroke; enqueue follows the dispatching seat.


    Generated by Claude Code

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

Metadata

Metadata

Assignees

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions