Uh oh!
There was an error while loading. Please reload this page.
fix(rest,runtime,metadata-protocol): make the destructive-409 remedy true on both remaining /meta doors — compound-name PUT threads ?force, dispatcher states its own face - #11392
Conversation
…true on both remaining /meta doors (#11095) The Phase 3a-destructive `409 DESTRUCTIVE_CHANGE` prescribed `?force=true` on two doors that never read it. Per the maintainer's split ruling: the compound-name REST `PUT /meta/:type/:a/:b` now threads `?force` (inheriting #7019's twin-parity ruling with its reason, plus #6877's repeated-param guard in the same stroke); the runtime dispatcher `PUT /meta` does NOT gain force and instead states its own `meta-dispatch` write face, whose clause names what a caller can actually do at that door. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019siH5jDmk5hrayvfyojUqR
…act ledger (#11095) Both new suites need real `update`/`delete` verbs — their acceptance halves are asserted against the STORE — and both route through `assertEngineUpdateDispatch` / `assertEngineDeleteDispatch`. Regenerated with `--write` as the gate itself prescribes: 4 rows added, 0 lost. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019siH5jDmk5hrayvfyojUqR
📓 Docs Drift CheckThis PR changes 3 package(s): 19 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 3 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 32 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 944ce56889d9406e0e2e73f1103a3a6ea2cd4524 && git checkout 944ce56889d9406e0e2e73f1103a3a6ea2cd4524
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 122ef38603123afdf030eac3644dd82317de5227 a063a6e0ec97f1470178098a7d07a5f326703e94 && git checkout -B drift-repro 122ef38603123afdf030eac3644dd82317de5227 && git merge --no-ff a063a6e0ec97f1470178098a7d07a5f326703e94
node scripts/docs-audit/affected-docs.mjs --json 122ef38603123afdf030eac3644dd82317de5227
|
os-zhuang
commented
Aug 23, 2026
PM review — the dispatch's fork-back condition is DISCHARGED. Ready for contract review. ⛔ Still draft, ⛔ not enqueued, ⛔ not armed, The fork-back condition, and why it does not fireThe dispatch made the #7019 precedent a stop-and-report condition, because this seat had not re-verified it ( ⭐ The best thing in this report is the part that narrows the precedent rather than the part that confirms it. The dev records that #7019 covered three doors and that its own file header insists the arguments stay separate, so #7019 is not a general "all doors onto
That is a verification that could have over-reached in the direction the dev wanted and instead bounded itself — and the bound independently re-derives the maintainer's split from the precedent, rather than the split being an instruction it merely obeyed. Three things I checked rather than accepted
Two results the reviewer should not skim pastA dangerous bug this PR avoided creating. Threading A live false green, reproduced on purpose. Ablation C mutated A self-correction worth creditingThe dev reports that its own For the reviewer's attention
Generated by Claude Code |
os-zhuang
commented
Aug 23, 2026
PM:
Both files are new in this PR, so this is not a pre-existing red and not an infrastructure flake — the engine doubles read a combinator (
Nothing else changes: Generated by Claude Code |
…ine doubles (#11095) `check:where-matcher` was red at 1103852 on both fixtures added by this PR: "NEW silently-wrong WHERE matcher ... no combinator branch (combinator read as a field name)". Both doubles handled `$or` correctly but had no `$and` arm, so `{ $and: [...] }` fell through to `r['$and']`, compared `undefined` against an array, excluded the row and returned an empty result set with nothing erroring — the gate's shape (b). A suite can go green on that while asserting about a DIFFERENT query than the one the protocol sent. MEASURED before choosing between the gate's two acceptable outcomes. Instrumenting `match` to log every `where` it receives recorded 35 calls in the rest suite and 9 in the runtime suite, every one a flat scalar object over `type` / `name` / `state` / `organization_id` / `package_id` — zero `$or` and zero `$and` driven through either. The `$or` arm was dead code, so refusal costs no live coverage and implementing `$and` would have pinned nothing. So both matchers now REFUSE every `$`-prefixed key, the shape 173 of the repo's conforming matchers already use and the one the gate's header names as the cheap correct answer for a double that only ever sees scalar equality (house style copied from `engine-autonumber-default-format.test.ts`: `if (key.startsWith('$')) throw new Error(...)`). The guard is a PRELUDE, not an arm inside the loop, so a preceding scalar miss cannot short-circuit `.every` past an operator the double cannot answer. Nothing was added to any baseline — the gate's own words, "The baseline never grows". Baseline key set verified unchanged against d395692; the refusing count moved 173 -> 175 and silently-wrong 2 -> 0. Suite counts unchanged: 17/17 and 8/8, and the full packages 2218/2218 and 2719/2719. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019siH5jDmk5hrayvfyojUqR
os-zhuang
commented
Aug 23, 2026
PM: the hold in That comment said "do not review until it is green." It is green, so the hold no longer applies and I am retracting it explicitly rather than leaving a stale instruction where a reviewer will read it. Verified from CI's own job results, not from a dev report — that distinction matters on this PR specifically, see below:
What the patch round changed, mechanically checked by this seat: exactly the two test files, ⭐ One finding from the patch round that is worth the reviewer's attention, because it is a defect the original diff carried rather than merely a gate complaint: both doubles handled
Generated by Claude Code |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#11095
The Phase 3a-destructive
409 DESTRUCTIVE_CHANGEends with a remedy clause, and on two/metawrite doors that clause named a?force=truethe door never read. A caller refused there, doing exactly what the sentence told them to do, got the identical refusal back — with nothing in the second answer saying the parameter had been ignored. #11015 repaired the duplicate-package face and deliberately left these two, because the honest repair for each was a contract question rather than a wording one.The maintainer ruled a split (
5383571818, recommendation C, adopted verbatim as 「其他同意你的意见」). Both halves are implemented here, and they are not the same fix.PRE-WORK: the #7019 precedent, verified before it was leaned on
The dispatch made this a fork-back condition — the PM had explicitly not re-verified the precedent that carries the compound-name half (
5381544552, last paragraph). Verified on currentmainbefore writing any code.Positive control for the zero-hit discipline. The corpus search term is
7019, and it is abundantly present: 40+ hits acrosspackages/rest,packages/runtime,packages/qa/dogfoodand two CHANGELOGs. Nothing about this verification rests on a zero — the finding is what the hits say.It holds, and it is stronger than the card claimed. The compound-name
PUThandler carries a live[#7019]comment at its capability gate (packages/rest/src/rest-server.ts):The precedent is not merely alive — it has been applied twice more since, each time citing #7019 by name as the reason:
rest-server.ts里三个相邻/metahandler 的错误信封是三种不同形状,其中两种不符合 ADR-0112 #7035 (the501envelope): the two refusals were converged because they were "BYTE-IDENTICAL (see the comment block on the gate: 'WORD FOR WORD the same mechanism')."Its executable form is alive too —
packages/rest/src/meta-compound-save-and-reset-capability-gate.test.ts§#7019 — compound-name PUT: the ADR-0106 round trip, one route over, green onmain.One honest qualification, recorded because it narrows the precedent rather than the conclusion.#7019 covered three doors, and that file's own header insists the arguments stay separate — "Why these two are one file but not one argument… a reader who takes the masking story as the reason for the DELETE gate would conclude, wrongly, that a caller with unmasked reads needs no gate." So #7019 is not a general "all doors onto
saveMetaItemmust agree" rule. What it rules, specifically and repeatedly, is that the compound-name route and its single-segment twin are one operation spelled two ways and must not diverge — which is exactly and only the claim the compound-name half needs. It notably does not extend to the dispatcher, which is why the split is right rather than merely ordered.The two halves
1. Compound-name
PUT /api/v1/meta/:type/:section/:name— threads?forcepackages/rest/src/rest-server.ts. Byte-identical to the single-segment twin: the same truthy table (true/1/yes/on, case-insensitive), the same...(force ? { force: true } : {})in the same position. The clause became true rather than being reworded.forcealso joins this door'srefuseRepeatedQueryParamslist in the same stroke, and that is not tidying. #6877's sharpest measured case is on this very parameter one route over:?force=false&force=falsearrives as an array, thetypeofternary falls to!!forceRaw, a non-empty array is truthy — so a caller repeating an explicit opt-out turns the destructive guard on, on a destructive verb, answered 200. Threading the parameter without naming it in the guard would have opened that inversion on a door that never had it. Ablation D below reproduces exactly that.2. Runtime dispatcher
PUT /meta— does not gain forcepackages/runtime/src/domains/meta.tsstates a new write face,'meta-dispatch', through the mechanism #11015 landed. Its clause names the door, denies the mechanism and prescribes what a caller can actually do there — the same grammar the duplicate face composes:This door is reached with a path, a method and a body:
?force=truenames a channel the transport does not have, not a parameter someone forgot to read. ⛔ Threading one would be a new public surface no ruling has opened, and the comment at the call site says so, so a later reader does not "harmonise" the pair.The coupling this needed to get right.
writeFacefeeds two switches —destructiveChangeRemedy(409) andspecValidationFindings(422) — and this door's answers differ: noforce, but it does carryissues[](errorFromThrown→details.issues), which is why it declared'meta-envelope'in the first place. So'meta-dispatch'is listed onspecValidationFindings' existing case, leaving the 422 exactly where #10888 left it. The 422's polarity is "declare to trim" — silence renders the full prose — so a face that fell through would have re-introduced #10888's duplication on this one door, silently, with every 409 assertion still green. Ablation C is the pin for that.Pins — both doors, both directions,
code+statusNew, driven end-to-end through the real
ObjectStackProtocolImplementationover asys_metadata-backed engine (a double that refused on a hand-written message would be pinning this PR's own idea of the producer):packages/rest/src/meta-compound-save-force-parity.test.ts— 17 cases. Refusal (409 +DESTRUCTIVE_CHANGE+ store unchanged), acceptance (200 + store actually changed), the five truthy spellings,?force=falsestill refusing, the threepackages/rest的其它req.query.*读取点同样把string | string[]当字符串用(#6307 的未扩大部分) #6877 multiplicity cases, and the single-segment twin as a live control in both directions.packages/runtime/src/domains/meta-save-destructive-remedy.test.ts— 8 cases. Refusal envelope, the clause (noforce=true, and what it does say), the#10886non-effect, a non-destructive save still succeeding (without it the card could be "satisfied" by a door that refuses everything),forcein the body not lifting the refusal,writeFacein the body not being smuggled, and the 422 coupling.packages/metadata-protocol/src/protocol.destructive-409-face-inventory.test.ts— §5[#11095], 6 cases, including the three-face switch and the 422 coupling asserted asdispatch.message === envelope.message(an equality, not a loose headline pattern — a fall-through matches the pattern).Both acceptance halves assert the STORE, not just a 200. "Refused, then accepted but wrote nothing" is a different outcome and only one of them is the fix.
The engine doubles route through
assertEngineUpdateDispatch/assertEngineDeleteDispatch;check:engine-double-contractwas regenerated with--writeas the gate itself prescribes (4 rows added, 0 lost).The face inventory now matches reality
protocol.destructive-409-face-inventory.test.ts's[#11015]section knowingly recorded these two rows as left-open and pointed here. Rows 2 and 3 of the table are updated, and a new[#11095]section records that they went different ways and that the split is the ruling — with an explicit ⛔ against a later reader "harmonising" them. The⚠️paragraph ondestructiveChangeRemedy's docblock is updated the same way, andrest-route-ledger.ts's compound-PUTrow now records?forceas the fourth divergence closed on #7019's reason.⛔ No findings prose was trimmed — #10886's sole-carrier verdict is untouched, and both new suites assert that positively.
Anti-vacuity — four ablations, direction predicted before running
Every leg carries on-disk proof of the mutation (matched counts of the deleted text, never a bare
git diff --stat), and the script armstrap … EXIT INT TERMso a foreground-cap SIGTERM cannot leave a mutated tree.force⭐ ACCEPTED identically at both doors; all refusals green'force'from the #6877 guardAssertionError: expected 200 to be 400— a doubled explicit opt-out answered 200 with the store shrunk. #6877's defect, reproduced'meta-envelope''meta-dispatch'falls out of the 422 case pairAblation C is also the
distmeasurement, and it is worth reading as a result. Both new HTTP suites import@objectstack/metadata-protocol, which resolves throughexportstodist/(a registeredKNOWN_UNALIASED_TEST_IMPORTSpair for both packages). At step (2) the runtime suite reported 8/8 passing against mutated source, becausedist/was stale — the exact false green the discipline exists for, reproduced live. Afterpnpm --filter @objectstack/metadata-protocol build, the dist occurrence count moved 2 → 1 and the coupling case went red. Both new suites carry a⚠️telling the next reader to rebuild.One correction, recorded rather than smoothed over: my
ablation-dist-preflight.mjsinvocation in that run was mis-specified — wrong polarity, and a marker (case "meta-dispatch") that also matches the 409's case, which survives the mutation. It printed an affirmative that did not mean what I asked it to mean. What actually established the false green was the directdist/index.jsoccurrence count (2 → 1 → 2 after restore). The preflight was not the instrument here; the count was.Verification
Union re-run at final commit
11038529(git rev-parse --short HEAD).pnpm --filter @objectstack/metadata-protocol testTest Files 138 passed | 2 skipped (140)·Tests 1889 passed | 10 skippedpnpm --filter @objectstack/rest testTest Files 139 passed (139)·Tests 2218 passed (2218)pnpm --filter @objectstack/runtime testTest Files 185 passed (185)·Tests 2719 passed (2719)pnpm --filter @objectstack/rest --filter @objectstack/runtime typechecktsc --noEmit→Done(script names echoed, so no zero-match silent pass)pnpm lint(full repo,eslint . --no-inline-config)Gate set derived with
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack(no hand-supplied paths) and re-derived after the ledger commit, which pulled in three further gates via thescripts/path —check:entry-guard,check:parse-guard,check:pnpm-filter-targets. All three run and pass.Not run locally, and owned by CI:
check:type-check-debt --re-measure, which refuses on anything but a fully built workspace closure.Out-of-scope finding
#11391 — the first-party SDK's
meta.saveItem(both the unscoped and environment-scoped declarations) builds no query string at all, so the?force=truethis PR made true on both REST doors is still unreachable through@objectstack/client. Equally absent on both doors, so not a twin divergence and not a regression from this card; filed unassigned as a contract question of its own.Scope
⛔ The single-segment
PUT /meta/:type/:nameis untouched — it appears here only as a live control in the parity cases. ⛔ Nocontent/docs/releases/**. Changeset included (@objectstack/restminor — the widened door;@objectstack/runtime+@objectstack/metadata-protocolpatch).Clause-② — this PR widens the compound door's accepted query surface. Staying draft;
needs:contract-reviewuntouched; no ready-flip, no auto-merge, no enqueue. Review routing is the PM's.Generated by Claude Code