Repository navigation
GET /api/v1/meta/<unknown-type> answers 200 with an empty collection while the write door refuses the same type #9488
Description
Activity
os-support-ai commented
on Aug 18, 2026 CollaboratorMore actionsTriage: lands in
packages/rest's/metaread door ⇒domain:cli; queued as Bug — the read and write doors disagree about which type names exist, and 200-with-empty-collection is the trap shape this wave already paid for once (?id=on app name). Clause-② flag for the dispatching seat: this changes accept/reject behaviour on a public route (unknown type goes from 200 to a refusal) — claim must declareClause-②: yes, dispatch atCONTRACT_REVIEW_TIER. Fix picks the refusal the write door already teaches (400naming the unknown type, or404) and pins door agreement — the test asserts read and write answer consistently for the same invented name, withcode+statuson the refusal per house discipline. (Triage seat, sessionsession_019gCKd9EZfQ6MbGTnMHHJvW.)
Generated by Claude Code
Claim: PM loop round 20
Session:session_012WKSnqAaoqtW3QX7SSf1Vk
Branch:claude/issue-9488-meta-unknown-type-refusal
Worktree:objectstack-issue-9488
Domain:domain:cli
File surface (region-level, ⛔ not line numbers — this file took three landings today and every line number has moved):packages/rest/src/rest-server.ts— the/metaunknown-type read path only. ⛔ NotenforceAuth(that is #9487's region, now in the decision box); ⛔ not the org-scope read threading #9454 just landed; ⛔ not the audit limb #9426 changed. Plus its pin and.changeset/**.Clause-②: YES — declared as triage instructed, verbatim from its 09:07:04Z comment:
"Clause-② flag for the dispatching seat: this changes accept/reject behaviour on a public route (unknown type goes from 200 to a refusal) — claim must declare
Clause-②: yes, dispatch atCONTRACT_REVIEW_TIER."Container & model:
M,mode:subagent,model: fable— the Clause-② mandate, ⛔ not the floor fromdispatch-gates.mjs --tier(which reports no path-derived mandate; Clause ② is judged from card CONTENT and that line is explicitly "a FLOOR, never a clearance").⚠️ Why fable and not the authorised opus. This card is one of the five the maintainer named in the one-time downgrade (#9446, #9416, #9462, #9488, #9487). That authorisation carries its own expiry — 「Fable 恢复后回默认」. It was a concession for Fable being unavailable, not a standing preference, so the mandated tier is attempted first and the downgrade used only if it fails. ⛔ If this dispatch falls back toopus, that is the downgrade being exercised, and it still does not spread beyond the five.Serial constraints cleared
- Same file (
packages/rest/src/rest-server.ts) — clear as of now. Org-overridable metadata (view,dashboard) is accepted with a 200state:'active'receipt but served by no read door #9454 merged (b6c769019, probeorganizationIdForMetaRead0 → 9 across 3 files). Every 401 answers{error, message}with nocodekey, while every other error family carries{error, code}#9487 lands nothing — it returnedneeds_decisionwith zero commits and is nowneeds-user-decisionpending an ADR-0112 D5 ruling — so it releases the file rather than holding it. No other card or in-flight claim touches this file. - Same package — parallel authoring permitted under the maintainer's 2026-08-18 exemption 「放开同包,同文件仍硬串行」; four replacement rules carried into the dispatch (region surface · merge
mainbefore the PR · merge again if a sibling lands · conflicts to the merge queue, ⛔ not hand-ordered). - In flight in this lane:
os init -t appscaffold does not compile on CLI 17.0.0 — the template's own object trips thesecurity-owd-unsetauthor-time rule #9666 (packages/cli/src/commands/init.ts) — different package, different file, no read coupling.
Carried into the dispatch, from a sibling card's measured findings
⚠️ Enumerate with an optional-call-tolerant shape. On #9454 a scan for.getMetaItems(missed four.getMetaItems?.(sites — my own re-derivation was short by 4 for exactly that reason. #9487's dev then used\.method\s*\??\s*\(and found what a plain scan would have missed. Any enumeration on this file must catch both forms and state the shape used.⚠️ A zero-match grep exits 0 and reads as a pass. #9487's consumer sweep returned zero for subject and control because the regex excluded a.it needed; only the control caught it. Every search reported must come with a control.
Generated by Claude Code
- Same file (
Tier correction:
fableattempted, unavailable, falling back to the authorisedopusdomain:cliseat, sessionsession_012WKSnqAaoqtW3QX7SSf1Vk, round 20. Amends theContainer & modelline of the claim above.The
fabledispatch terminated immediately on:"You've reached your Fable 5 limit. Switch to another model, or manage usage credits…"
⇒ The condition the maintainer's one-time downgrade was granted for still holds. I attempted the Clause-② mandated tier first rather than assuming the concession was still needed — the attempt is what established it, and it cost nothing: the agent died before creating a worktree, and there is no branch and no commit to recover.
Re-dispatched at
model: opus, under the maintainer's 2026-08-18 authorisation naming exactly five cards (#9446, #9416, #9462, #9488, #9487). This is that authorisation being exercised, not reinterpreted:- ⛔ It does not spread beyond those five.
- ⛔ It expires on its own terms — 「Fable 恢复后回默认」 — and the next Clause-② card outside the five gets
fableattempted the same way. - Clause-② stays
yeson this card regardless of which model runs it. The tier is a resourcing fact; the contract flag is a property of the change, and every ⛔ in the dispatch brief is unchanged.
⚠️ Recorded rather than silently swapped, because a claim that saysfableand a run that usedopusare otherwise indistinguishable from a seat quietly downgrading a mandatory clause.
Generated by Claude Code
- added 4 commits that reference this issue
on Aug 18, 2026 os-dev-report
{ "issue": 9488, "status": "done", "branch": "claude/issue-9488-meta-unknown-type-refusal", "pr": "https://github.com/objectstack-ai/objectstack/pull/9743", "premise_still_valid": true, "summary": "The /meta LIST door now refuses a :type segment that names no metadata type, with the same status and code the write door already emits (400 / INVALID_REQUEST, naming the type), instead of serving it as a real-but-empty collection. The rule is the UNION of the two authorities the platform already has -- the static spelling contract unrecognisedMetaTypeRefusal (what the write door consults) and the live listing getMetaTypes (what GET /meta/types serves) -- and refuses only a name in neither; no list of type names is hand-written. The live half is required, not decorative: #8421 rejected raising the static verdict on the read entries because data/kind/package/policy and plugin types are advertised by GET /meta/types while sitting outside the static contract, and that objection is still true. Static verdict runs first (silent for all 68 accepted spellings, so an ordinary request pays nothing); the live probe is reached only by a request already headed for a refusal, and fails OPEN when the listing cannot be read. Nothing in packages/spec moved; the write door, the compound arity and the single-item doors are untouched.", "dynamic_type_read_doors_enumerated": { "grep_shape_for_route_paths": "grep -nE '\\$\\{metaPath\\}/:type' packages/rest/src/rest-server.ts, then each path paired with its nearest preceding method: literal via awk", "grep_shape_for_optional_calls": "\\.(getMetaTypes|getMetaItems|getMetaItem|getMetaItemCached)\\s*(\\?\\.)?\\s*\\( -- note the tolerance is (\\?\\.) not \\?? : my FIRST attempt used '\\.name\\s*\\??\\s*\\(' and found ZERO optional sites, because the real text is '.name?.(' -- the ? is followed by a DOT, not by the paren. The brief's own worked example has the same shape and the same trap.", "control_for_that_shape": "on packages/rest/src/rest-server.ts the corrected shape finds 19 lines, the intolerant '\\.name\\s*\\(' finds 15 -- an 8-vs-1 gap on the protocol.ts control and a 4-site gap here, and the 4 are real: lines 1304, 3276, 3317, 8422, all '.getMetaItems?.('. This is the same class as the #9454 'short by 4'.", "doors_found": [ "GET /meta/:type -- THE DEFECT. Covered.", "GET /meta/:type/:name -- measured 404 RESOURCE_NOT_FOUND already. Not covered (no defect).", "GET /meta/:type/:name/references -- measured 501 NOT_IMPLEMENTED. Not covered.", "GET /meta/:type/:name/layers -- measured 501. Not covered.", "GET /meta/:type/:name/history -- measured 501. Not covered.", "GET /meta/:type/:name/audit -- measured 501. Not covered.", "GET /meta/:type/:name/diff -- measured 501. Not covered.", "GET /meta/:type/:name/published -- measured 501. Not covered.", "GET /meta/:type/:section/:name -- COMPOUND arity: :type carries an OBJECT name (the write door's own exemption 1). Deliberately NOT covered.", "GET /meta/_drafts -- literal route, no dynamic :type segment.", "write doors PUT/DELETE/POST on :type -- out of scope by instruction; untouched." ] }, "refusal_chosen": { "answer": "400 / INVALID_REQUEST, message opening with \"'<type>' is not a metadata type\"", "measured_write_door_reference": "PUT /api/v1/meta/totally_invented_type/x through the REST layer answers 400 {\"error\":\"[invalid_request] '...' is not a metadata type ...\",\"code\":\"INVALID_REQUEST\"} -- measured on this branch, not read off a comment", "why_not_404": "404 already means 'no such ITEM' on this family (GET /meta/:type/:name). Reusing it for 'no such TYPE' re-creates the very indistinguishability this card is about, and would be a THIRD answer for one condition.", "envelope_decision": "THROWN, not hand-built. The handler's existing catch shapes it through handleRouteError, so the wire body is byte-identical to the write door's. This is also why neither ratchet moves: no body literal is added to the file. Re #9487's caution -- I did not default to the nested shape. This file speaks two dialects (refuseRepeatedQueryParams writes the ADR-0112 NESTED body, documented in query-multiplicity.ts as 'right for the /meta family it guards'; the data routes speak the FLAT mapDataError envelope). The sibling that governs DOOR AGREEMENT is the same condition on the write door of the same transport, and that arrives FLAT." }, "error_code": { "code": "INVALID_REQUEST", "declared_in": "packages/spec/src/api/error-code-ledger.zod.ts, ERROR_CODE_LEDGER under '@objectstack/rest' (line ~191)", "minted": false, "spec_changed": false, "note": "Also already listed in isExpectedQueryRejection (packages/rest/src/error-response.ts), so the refusal produces no '[REST] Unhandled error' log noise." }, "ratchets": { "gate": "check:route-envelope, packages/rest/src/rest-server.ts (dialectOnly)", "before": { "stringError": 44, "siblingCode": 69 }, "after": { "stringError": 44, "siblingCode": 69 }, "after_merge_of_main_at_6b894e9f": { "stringError": 44, "siblingCode": 69 }, "verdict": "unmoved in both directions; neither ticked up" }, "consumer_sweep": { "subject": "repo-wide scan of every literal '/meta/<segment>' across packages, apps, examples, scripts, docs, content (ts/tsx/mts/mjs/js/json/mdx/md/yml): 73 distinct spellings, 1898 occurrences, each classified against the 68-name accepted contract (35 URL spellings + 33 canonical singulars, read from META_URL_TO_SINGULAR at runtime)", "control": "positive control -- the sweep returns object(397), view(236), api(111), external_catalogs(2) etc., all known-present; negative control -- 'meta/zzz_no_such_marker_9488' returns 0 while 'api/v1' returns 20 files in the same tree, so a zero is a real zero and not a broken regex", "result": "every off-contract segment is a literal route registered BEFORE /meta/:type (types, _drafts, diagnostics, _migrate), a DIFFERENT route family (POST /api/meta/query|export|import|validate|bulk), a file path (src/commands/meta/list), prose/ADR text, or a refusal fixture (fieldz, zzz_not_a_type, capabilitys, viewes, my_widget). packages/client, packages/qa/dogfood and the console consumers are inside the swept tree.", "gap_the_literal_sweep_could_not_see": "type names passed as helper ARGUMENTS rather than URL literals. Caught by running the suites, not by grep -- packages/rest/src/meta-plural-i18n.test.ts calls listOf(rest,'widget'). Recorded because it is the sweep's real limitation.", "one_real_consumer_of_the_200": "packages/qa/dogfood/test/meta-types-create-seed.dogfood.test.ts pinned the defect literally (expect(bogus.status).toBe(200); expect(body).toEqual({type:'zzz_not_a_type',items:[]})). Falsifies PM assumption 3." }, "reverse_verification": { "prediction_written_before_running": true, "ablation_A_drop_the_refusal_call": { "predicted": "3 red (unknown->400, door agreement, ?preview=draft), all 'must stay 200' cases green", "actual": "4 red / 15 green", "reasons_checked": "all three predicted reds failed with 'AssertionError: expected 200 to be 400' -- the predicted direction. The door-agreement case failed on read.statusCode === write.statusCode, i.e. GET 200 vs PUT 400, also as predicted. The 4th red is a case I added AFTER writing the prediction ('does not ask the protocol to list a type it has refused'), which failed on its own claim ('expected vi.fn() to not be called at all, but actually been called 1 times').", "honest_verdict": "MISS on count (4, not 3), match on direction and reasons. Recorded rather than smoothed over: the extra red is my own un-predicted case, not an unexplained failure." }, "ablation_B_drop_the_live_set_exemption": { "predicted": "7 red -- data/kind/package/policy (4), the plugin type (1), both fail-open cases (2); every declared type green", "actual": "7 red / 12 green", "reasons_checked": "all seven failed with 'AssertionError: expected 400 to be 200' -- the predicted direction. The eight declared-type cases stayed GREEN, which is what makes B discriminating: it isolates the live half rather than disabling the whole refusal.", "honest_verdict": "EXACT on count, direction and reasons." }, "restored_and_reconfirmed": "19/19 green after restoring; fix was committed BEFORE ablating, so each restore was a git checkout of a committed state, never a discard of the only copy." }, "gate_union_re_derived_from_actual_changed_paths": { "command": "node scripts/pm/dispatch-gates.mjs .changeset/meta-unknown-type-read-refusal.md packages/qa/dogfood/test/meta-types-create-seed.dogfood.test.ts packages/rest/src/meta-plural-i18n.test.ts packages/rest/src/meta-unknown-type-read-refusal.test.ts packages/rest/src/rest-server.ts", "brief_named_only": "check:route-envelope (+ the standing check:nul-bytes). The re-derivation added 15 more, and 6 of those appeared only AFTER the dogfood fixture entered the diff -- the brief's list was a clue, not a spec, exactly as the standing clause says.", "results_echoed": [ "check:route-envelope -- 'ratchet #9559 ... stringError 44, siblingCode 69' (unmoved)", "check:nul-bytes -- 'OK (scanned 6236 text file(s) ... no raw ASCII control bytes)'", "check:error-code-casing -- 'no lowercase error codes in 4244 scanned file(s) (ADR-0112)'", "check:authz-resolver -- 'single shared authorization resolver intact; both entry points delegate'", "check:cross-package-test-inputs -- 'OK: 12 package(s) read outside themselves, all declared'", "check:dispatcher-error-vocabulary -- 'OK -- 17 unregistered code-stamping site(s), all classified'", "check:changeset-gate-self-tests -- 'OK' (118 + 212 + 116 assertions)", "check:objectui-changeset -- 'objectui-range --self-test: all checks passed'", "check:engine-double-contract -- 'OK -- 321 pinned, 133 in the DEBT ledger, 2 exempt'", "check:where-matcher -- '255 matcher(s) discovered ... 0 silently-wrong ... none new'", "check:query-options-erasure -- 'ratchet holds: 67 unswept non-test site(s) ... none new'", "check:type-check-coverage -- 'OK -- 64/77 workspace packages type-checked'", "check:test-source-alias -- 'OK -- 72 packages with tests scanned'", "check:type-source-resolution -- 'OK -- 76 packages with a tsconfig.json scanned'", "check-adr-0087-registration -- 'this PR adds no declared-breaking changeset'", "check-affected-docs -- 'affected-docs self-test: 242 cases pass'", "spec check:empty-state -- 'all classified (1 closed, 2 open, 4 output, 9 scope)'", "spec check:liveness -- 'every governed-type property ... is classified'", "spec check:strictness-ledger -- '61 file(s) across 5 triaged director(ies)'", "spec check:variant-docs -- '18 discriminated union(s) -- 8 governed, 10 exempt'", "check:type-check-debt -- WENT RED, then fixed: see below" ], "the_one_that_went_red": "check:type-check-debt --re-measure: '@objectstack/rest: TEST_DEBT records 155 raw tsc error(s), tsc --noEmit now reports 156 (+1)'. Cause: my new pin imported './rest-server' without the .js extension -- TS2835 under moduleResolution nodenext, the exact AGENTS.md lesson. FIXED BY FIXING THE ERROR (92 local imports in this package's tests already carry the extension). Re-run: 'OK -- 33 ledger entr(ies) re-measured in 403.7s, 1926 raw tsc error(s) total, none above its recorded number'. NO ledger entry raised, no threshold moved, no baseline edited, no test skipped (ruling 3 held). Note it refuses outright on an unbuilt closure -- I built the workspace as lint.yml does (70 tasks, 8m9s) so the number is MEASURED, not 'not applicable'." }, "tests": "All at 6b894e9f (main merged in, then re-verified there). @objectstack/rest full suite: 'Test Files 128 passed (128)' / 'Tests 2096 passed (2096)'. @objectstack/rest typecheck: script name echoed ('> tsc --noEmit'), exit 0 -- not a zero-match pnpm filter. New pin alone: 'Tests 19 passed (19)'. Downstream consumers, direction stated: pnpm --filter '...@objectstack/rest' is the DEPENDENT set (17 packages); I ran the HTTP-behaviour ones -- @objectstack/runtime 'Test Files 173 passed (173)' / 'Tests 2590 passed (2590)', @objectstack/client 'Test Files 23 passed (23)' / 'Tests 310 passed (310)', @objectstack/http-conformance 'Test Files 4 passed (4)' / 'Tests 72 passed (72)', @objectstack/hono 'Test Files 2 passed (2)' / 'Tests 73 passed (73)'. DOGFOOD, ON A REAL BOOTED STACK: meta-types-create-seed + route-ledger-live-mount-parity, 'Test Files 2 passed (2)' / 'Tests 14 passed (14)' -- and because dogfood resolves from dist/, I REBUILT @objectstack/rest first and PROVED the change reached the artifact with 'node scripts/ablation-dist-preflight.mjs @objectstack/rest \"this deployment has registered no items under it\"' -> 'marker present in 2 built files ... live in the artifact the suite consumes'. Two pre-existing failures were diagnosed as unbuilt-closure, not my change ('Failed to resolve entry for package @objectstack/runtime' / '@objectstack/connector-mcp'), and both went green once built. Every verdict read from reported counts, never from a piped exit code. All build/test runs wrapped in flock -E 99 -w <=540 /tmp/os-heavy-verify.lock; one acquisition returned 99, I named the holder with fuser -v (PIDs 21268 flock / 21409 node), spent the gap on lock-free work (the PR body) and re-acquired in-turn rather than ending the turn.", "zone2_assumptions": [ "ASSUMPTION 1 -- 'a single authoritative list the read door can consult, and the write door already consults it': PARTIALLY FALSIFIED, and this was the load-bearing finding. The single static authority exists (unrecognisedMetaTypeRefusal in packages/spec/src/shared/metadata-url-spelling.ts) and the write door does consult it -- but it is NOT sufficient for a READ door, and #8421 says so in its own words: 'Refusing unrecognised names on the READ entries would answer 400 for types this same service advertises, trading one declared-not-served gap for another.' Measured: unrecognisedMetaTypeRefusal returns a verdict for data, kind, package and policy, all of which GET /meta/types enumerates, and plugin types enter the live set by registering items (adding-a-metadata-type.mdx). So the read door needs the UNION of TWO authorities. Using the static one alone would have shipped the regression the brief's own STOP condition names.", "ASSUMPTION 2 -- 'the list route and any single-item route need the same treatment': FALSIFIED. Measured every dynamic-type GET door: the single-item address already answers 404 RESOURCE_NOT_FOUND and the six sub-resource limbs answer 501 NOT_IMPLEMENTED. All are distinguishable from a served answer, so none carries this defect. Only the LIST door served an unknown type as a real-but-empty collection. Treating them all would have been widening with no defect to justify it.", "ASSUMPTION 3 -- 'no consumer depends on the current 200-empty': FALSIFIED, by exactly one: packages/qa/dogfood/test/meta-types-create-seed.dogfood.test.ts asserted status 200 and the body {type:'zzz_not_a_type',items:[]}. Its CLAIM (that /meta/types is not the /meta/:type catch-all wearing a 200) is unaffected and in fact sharper now, so only the catch-all's half was restated, with code AND status. Its sibling route-ledger-live-mount-parity.dogfood.test.ts asserts ROUTING for the same name and is untouched -- the request still reaches /meta/:type.", "ASSUMPTION 4 -- 'this needs no packages/spec change': HELD. Nothing in packages/spec moved. The predicate and the error code were both already there." ], "additional_behaviour_change_worth_the_PM_seat_reading": "Beyond obviously-invented names, three SNAKE-PLURAL spellings go 200 -> 400: sharing_rules, analytics_cubes, rag_pipelines. Measured cause: the six plugin kinds with no registry entry are carried by limb 1 of META_URL_TO_SINGULAR in their MANIFEST spelling, which is camelCase for the multi-word ones (sharingRules, analyticsCubes, ragPipelines); limb 2 only pluralises REGISTRY entries, and these have none. So every registry type and every single-word plugin kind has its snake plural accepted, and exactly these three do not. The write door ALREADY refuses all three today, so this is the read door catching up rather than a new rule. Nothing in the repo addresses any of the three (grep across packages/apps/examples/content/docs returned zero, with external_catalogs as the positive control returning hits). I did NOT file a card asking for them to be added: #9180 is a standing maintainer ruling -- 'The /meta type segment is singular. Always. There is no exception and no tolerated plural alias' -- and adding plurals would push against it. My change composes with that ruling's step 3 automatically, because it consults the very predicate step 3 will narrow rather than restating any spelling itself.", "open_questions": [], "out_of_scope_findings": [ "filed as #9744 (label: finding, unassigned): GET /meta/<unknown-type>/<name> answers 404 'Metadata item x/y not found' -- the single-item 404 cannot tell an unknown TYPE from an unknown ITEM, so a caller who typo'd the type is pointed at the item name. Distinguishable from success (so NOT this card's defect, which is why it was left alone here), but it makes three doors give three accounts of one segment. Searched first: no existing card covers it (#9180 is the plural axis, #9243/#9193 are catalog/docs, #8297 is the 501). Any fix needs the same static-union-live composition #9488 built, plus a decision on whether a type-level refusal outranks the item-level 404." ], "notes": [ "In-scope bounded fix, declared rather than smuggled: packages/rest/src/meta-plural-i18n.test.ts's double advertised an EMPTY type set (getMetaTypes -> []) while its getMetaItems served a document for 'widget' -- a name chosen precisely because it is outside every map. Same defect class (a listing disagreeing with what is served), mechanical, no other claim on the file, same gate family. Its listing is now derived from RAW_BY_NAME, the same source the item double reads, in both spellings the item double answers. NO assertion in that file changed. Named in the PR body as the clause requires.", "No model identifier appears in any pushed artifact (commit messages, PR title/body, code comments); Co-Authored-By: Claude retained. No conflict with the harness default arose.", "PR body was read back after storing: the session-URL footer survived, draft state confirmed true, and the only closing keyword in the body is 'Fixes #9488' on line 1 -- #9487 and #8421 are referenced with no closing keyword in range (audited with a proximity grep).", "Worktree removed without --force after the PR was opened; the remote branch is intact at 6b894e9f." ] }
Generated by Claude Code
- added a commit that references this issue
on Aug 23, 2026 - added a commit that references this issue
on Sep 29, 2026
QA-source: #9417 · platform-core.metadata-registry-serving · — (round observation; no clause of the Tier-2A lane covers it)
Extracted at close-out of the #9296 QA wave (#9480). Designated for extraction by the Tier-2A reviewer of record — "two API-shape observations worth their own cards rather than burial in a run record". Subject sha
e4e5c6e3c608b1b807c83a0d5b734f213eb1a1dd, stock showcase.Observed
An unknown metadata type is served as a real-but-empty collection. The write door for the same name refuses precisely —
400, "'' is not a metadata type" — so the two doors disagree about which type names exist.Why it matters
A 200-with-an-empty-collection is indistinguishable from "this type exists and has no items". That is the same shape as the
?id=trap this wave already paid for once:GET /api/v1/meta/app?id=<package-id>answers{"items":[]}because the parameter keys on the app name, and it read to a runner exactly like "the app metadata is gone" (#9296 standing fact 3; #9334 checklist finding 2). A typo'd or renamed type name silently reads as an empty surface rather than as an error.Related
#8421 (closed) fixed the write side of the unknown-type namespace —
PUT /meta/<unknown>/xno longer answers 200. The read door still does.Fix direction — not prescribed
Either a
404, or a400naming the unknown type as the write door already does, makes the two answers distinguishable. Whatever lands should be pinned by a test asserting the read and write doors agree on which type names exist.No priority and no
domain:*label — normal triage first-touch.