test(rest): drive the tenancy axis of GET /ui/view/:object/:type — an anonymous caller can name another environment and receive its UI view - #13258
Conversation
Extends PR #13244's measurement with the axis it explicitly did not assert: its harness had no envRegistry and no kernelManager, so the environment resolution chain was read from source and never driven. Measurement only. rest-server.ts is untouched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TvqBFLRzXdSPcbusDoED9k
…s body The ablation showed three §3 assertions were envelope-shape readings true of either environment, so they survived a leg that removed the crossing. Each now asserts environment B's own label first, making the inventory a statement about what the NAMED environment disclosed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TvqBFLRzXdSPcbusDoED9k
…k creates An unknown X-Environment-Id does not refuse; it falls through to the default environment and answers 200 with that environment's view. Two 200s with different bytes let an anonymous caller distinguish a real environment id from an invented one, which is the difference between "must possess an id" and "can discover one". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TvqBFLRzXdSPcbusDoED9k
📓 Docs Drift CheckNothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs. What this run could not see
Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): |
|
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 33297756999 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 失败的 job(日志抽取,best effort):
跨 PR 相同签名(24h,按失败测试文件聚合):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
|
根因定位:⛔ 不是本 PR 的缺陷 —— 它测到的东西被修好了,而它在队列外停了 2 小时没察觉
在当前 main 上的失败分支更新到当前 main 后(head ⇒ 红的正是本 PR §3 的招牌读数:「 ⭐ 原因:
|
| 时刻 | 事件 |
|---|---|
| 08-29 | 本 PR 测到 hidden 不统一,读数为真 |
| 08-30 03:57Z | #13329 修好它 ⇒ 本 PR 的 §3 变成假的 |
| 08-30 06:33Z | 本席 arm 它 —— 在陈旧 base(74049254)上,CI 全绿 |
| 08-30 08:5x | 发现它 2h20m 未入队,update_pull_request_branch 成功 ⇒ 证明从未入队 |
| 08-30 09:0x | 在当前 main 上重跑 ⇒ 红 |
⇒
⭐ 与今天的 #13392 是同一族:一个语法合法、看起来完整、而对当前世界为假的答案。区别是那条关于 gate 派生,这条关于 CI 绿灯本身。
返工令(已派出,范围严格受限)
- 先重测再改,⛔ 不许因为我说了原因就直接翻转断言;两种字段(
beta_secret非 priority、statuspriority)都要驱动并报读数; - 断言改为钉当前行为,
⚠️ 并判断本例是否与 fix(metadata-protocol): honourhiddenon getUiView's list priority pass #13329 自己的钉重复 —— 若重复,只保留它独有的部分(跨环境那条路径,fix(metadata-protocol): honourhiddenon getUiView's list priority pass #13329 的生产者级钉可能没驱动);⛔ 不因为变红就删掉用例; - 用例名与 PR 正文 §3 同步更正,⭐ 但保留历史:那个不对称是真的、8-29 在此测得、8-30 由 fix(metadata-protocol): honour
hiddenon getUiView's list priority pass #13329 修复。⛔ 不能让读者以为当初测错了; - ⛔ 不碰 §1/§2 的跨环境读数、控制组、消融、
rest-server.ts(必须与 base 逐字节相同)、以及卡 [finding]GET /api/v1/ui/view/:object/:typeresolves a UI view with no identity resolution and no anonymous-deny at the REST seam #13214 的定级(仍是needs-user-decision)。本 PR 保持Part of,⛔ 不改成Fixes。
绿了之后重新 arm。
Generated by Claude Code
Generated by Claude Code
The case asserted that `hidden` is NOT a uniform floor on the crossed path: `beta_secret` (hidden, non-priority) was dropped from the list body while `status` (hidden, priority-named) was served, carrying its authored label. That reading was true when taken on 2026-08-29 and has since been made false by a repair, not by an error in the measurement. `2a75270b1e` (#13329, merged 2026-08-30) put `!fields[k].hidden` on getUiView's list priority pass, where it had sat on the fill pass alone, so the list branch now agrees with the form branch it had disagreed with. Re-driven on the repaired producer through the same crossed path — anonymous request, `X-Environment-Id: env_beta`, unscoped mount, built `dist/` — both hidden field kinds are now withheld, together with their labels and their `searchableFields` entries: columns = ["name", "created_at", "beta_only_field"] colLabels = ["Beta Account Name", "created_at", "Beta Only"] searchable = ["name", "created_at", "beta_only_field"] formFields = ["name", "beta_only_field"] The case is rewritten to pin that, keeps controls so an empty producer cannot satisfy it vacuously, and records the 2026-08-29 -> 2026-08-30 sequence in place so a reader does not conclude the original measurement was wrong. It is kept rather than deleted because #13329's own pin calls `getUiView` directly in-package from source; this one is the blast-radius reading for the CROSSED, cross-environment path through the REST seam and the built artifact. `rest-server.ts` remains byte-identical to base; no production code changes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TvqBFLRzXdSPcbusDoED9k
|
os-dev-report {
"issue": 13214,
"status": "done",
"branch": "claude/issue-13214-ui-view-tenancy-exposure",
"pr": "https://github.com/objectstack-ai/objectstack/pull/13258",
"premise_still_valid": true,
"summary": "Rework of PR 13258, one focused correction. RE-MEASURED FIRST, not flipped on the diagnosis. Merged current origin/main (branch head was 4 then 1 commit behind; final head 5869d27f23), rebuilt the closure, and drove the crossed path again (anonymous request, X-Environment-Id naming env_beta, unscoped mount, built metadata-protocol dist). Readings for BOTH field kinds on current main: status (hidden AND priority-named) is now WITHHELD - statusCol null, label 'Beta Status' absent from the emitted column labels, absent from searchableFields; beta_secret (hidden, non-priority) also withheld - secretCol null. Full crossed list columns = [name, created_at, beta_only_field]; colLabels = ['Beta Account Name', 'created_at', 'Beta Only']; searchableFields = [name, created_at, beta_only_field]; form branch = [name, beta_only_field]. The crossing itself is unaffected (label 'Beta Environment Accounts', acquired ['env_beta'], status 200), so sections 1, 2 and the rest of section 3 stand untouched. Cause confirmed as stated: 2a75270b1e (PR 13329, card 13259) put !fields[k].hidden on getUiView's list PRIORITY pass, where it had sat on the fill pass alone. CHANGED IN THE TEST: the case is kept and rewritten, not deleted - new title 'hidden IS a uniform floor on the crossed path - both field kinds measured, not assumed'; assertions now pin both kinds withheld, both labels withheld, searchableFields carrying no hidden field, a name-agnostic invariant computed from the fixture (so a tenth priority name added without the filter reds it), controls (name, beta_only_field, 'Beta Account Name' must still be served) so an empty producer cannot pass vacuously, and the list/form branches asserted to AGREE. The in-file comment records the sequence: measured 2026-08-29, repaired 2026-08-30, the original reading was true when taken. JUDGED BY MEASUREMENT that the case still earns its place: 13329 does ship its own producer-level pin (packages/metadata-protocol/src/protocol.ui-view-hidden-columns.test.ts - it calls getUiView directly, in-package, from source, sweeps all nine priority names, pins labels, searchableFields, the form branch and the response schema), and that pin is the authority on the producer; what it does NOT drive is the crossed, cross-environment path through the REST seam, environment resolution, kernel acquisition and the BUILT dist artifact, which is this file's whole subject - a regression reachable only through that chain would leave the producer pin green. That is what the retained case now covers, and the comment says so explicitly. CHANGED IN THE BODY: the section 3 hidden bullet is rewritten as a four-part sub-list (measured 2026-08-29 with the old readings preserved verbatim; repaired 2026-08-30 by 13329 with commit sha; re-driven readings; kept-not-deleted with the coverage argument) so a reader cannot conclude the measurement was wrong. rest-server.ts is untouched and byte-identical to base; card 13214's grading untouched; the PR remains 'Part of', never a closing keyword.",
"tests": "All exit codes captured before any pipe; verdicts quoted from each gate's own printed line. Heavy runs serialized through scripts/pm/os-verify-lock.sh (slot dev-13258). AFFECTED FILE: pnpm --filter @objectstack/rest exec vitest run --maxWorkers=2 src/ui-view-route-tenancy.measurement.test.ts -> 'Test Files 1 passed (1)' / 'Tests 27 passed (27)'. WHOLE PACKAGE: pnpm --filter @objectstack/rest test -> 'Test Files 160 passed (160)' / 'Tests 2698 passed (2698)'. TYPECHECK: pnpm --filter @objectstack/rest typecheck exit 0 -> 'check:test-typecheck: OK - @objectstack/rest's test layer compiles under packages/rest/tsconfig.test.json; 3 file(s) / 5 error(s) held in test-typecheck-debt.json'; NOT a not-measured green - tsc -p tsconfig.test.json --listFiles shows this file 1 hit, tsc -p tsconfig.json --listFiles shows 0 hits, so the test-typecheck leg is the one that reads it. GATE UNION DERIVED AFTER THE MERGE, per instruction: node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack, stderr banner read (not just the exit code) - 'gate list derived from the tree of objectstack-ai/objectstack at commit 5869d27f23' and 'change set derived from git - 1 path(s) vs merge base 2be4a6729 of origin/main and HEAD', repo assertion 'it holds'. It paid off: check:doc-authoring IS in this union and was NOT in the previous run's list. VERDICT LINES, all exit 0: check:nul-bytes 'OK (scanned 7433 text file(s) - 7433 tracked, 0 untracked-not-ignored; skipped 7 binary; no raw ASCII control bytes)'; check:cross-package-test-inputs 'OK: 24 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob'; check:dispatcher-error-vocabulary 'OK - 22 unregistered code-stamping site(s), all classified'; check:doc-authoring '393 files clean - no bare metadata literals' + '48 published skill files clean' + '14050 customer-facing string(s) across 691 spec sources clean' + 'sibling-package prose ids hold the baseline - 831 pinned site(s)'; check:logger-receiver-detach 'OK every log channel keeps its receiver: 2306 non-test TS file(s) walked, 0 detach(es)'; check:objectql-double-limit 'OK ObjectQL double limit conformance holds: 293 double(s) graded'; check:page-declaration-shape 'OK - 34 page entries across 2317 sources ... all reach the kernel through a discoverable declaration'; check:published-files '69 publishable package(s) of 78 workspace member(s) declare a files whitelist'; check:slot-lookup 'slot-lookup ratchet holds: 107 unswept site(s) in 25 file(s), none new'; check:test-source-alias 'OK - 72 packages with tests scanned; 61 registered as still resolving a workspace dep through dist/'; check:type-source-resolution 'OK - 95 tsc program(s) across 77 packages scanned'; check:where-matcher 'where-matcher conformance holds: 316 matcher(s) discovered ... 0 silently-wrong'; check:query-options-erasure 'query-options-erasure ratchet holds: 67 unswept non-test site(s) in 17 file(s), none new'; check:engine-double-contract 'OK - 709 pinned, 134 in the DEBT ledger, 3 exempt'; check:type-check-coverage 'OK - 66/78 workspace packages type-checked (plus the root), 12 in the DEBT ledger, 1 exempt'; check:authz-resolver 'single shared authorization resolver intact; both entry points delegate'; check-ci-filter-parity 'OK: all 123 declared cross-package glob(s) (88 unique) are covered by core or crosspkg'; check-comment-mask-adoption 'OK - 14 private comment-stripper(s) ... all 14 recorded'; check-cross-package-test-inputs 'OK: 24 package(s) read outside themselves, all declared'; check-keyed-text-bounds '112 *.object.ts files ... 148 keyed text-family columns judged, 148 bounded'; check-plugin-teardown-shape '64 Plugin implementation(s) across 5036 source(s) ... 0 known-unreached'; check-shard-attestation '2 aggregate gate(s) count 3 declared leg(s) across 3 attesting job(s)'; check-undeclared-dep-imports '78 workspace packages ... 7 ledger row(s), all evidence intact'; docs-audit/check-affected-docs exit 0; docs-audit/check-drift-comment '56 cases pass across 5 fixture diff(s)'. BUILD-DEPENDENT PAIR, run after building the closure (pnpm exec turbo run build --concurrency=2 --filter=./packages/* --filter=./packages/*/* -> '8 cached, 70 total', exit 0, git status --porcelain empty afterwards): check:dual-build-cjs-loads '102 published require entry point(s) across 66 package(s) load; 610 emitted CommonJS file(s) parse; 1 cross-format behaviour probe(s) agree'; check:type-check-debt 'check-type-check-coverage --re-measure: OK - 29 ledger entr(ies) re-measured in 302.4s, 1547 raw tsc error(s) total, none above its recorded number' + 'surplus: none'. REPO-WIDE LINT: pnpm lint (eslint . --no-inline-config) exit 0, run in full, no narrowing claimed. NOT MEASURED, exactly one: node scripts/check-test-completeness.mjs exits 3, PREREQUISITE NOT MET - it grades a saved turbo run test log it cannot produce; recorded NOT MEASURED, never folded into the green list. REVERSE VERIFICATION of the corrected assertion. Direction predicted BEFORE running: red, and red on that one case only, because removing the guard should put status back into the crossed columns. Subject: packages/metadata-protocol/src/protocol.ts (the producer), mutated back to the pre-repair spelling; rest-server.ts never touched. Mutation proved on disk, not by exit code: anchor asserted unique (1 occurrence) before mutating, post-mutation guard-text grep -c = 0, mutated-spelling grep -c = 1, blob 71c528b66a42fb4bfbc83f12b8483844cc9d02b6 (= git rev-parse HEAD:packages/metadata-protocol/src/protocol.ts) moved to ae9f8f56c2ae19c3e0b72c2e614753521573b55b. REBUILT and proved on the artifact, because this dependency resolves through dist/ and an unbuilt ablation stays green: pnpm --filter @objectstack/metadata-protocol build, then node scripts/ablation-dist-preflight.mjs @objectstack/metadata-protocol 'priorityFields.includes(k) and the hidden guard' --absent -> 'dist/: marker absent from all 24 built files'. Result: 'Test Files 1 failed (1)' / 'Tests 1 failed | 26 passed (27)', the single red being 'AssertionError: expected [ Array(4) ] to not include status' - predicted direction, aimed at exactly the rewritten case, every control and every other section-3 reading still green. RESTORE LEG given equal treatment, under trap ... EXIT INT TERM with absolute paths: git checkout HEAD -- REPO_ROOT/packages/metadata-protocol/src/protocol.ts; restored git hash-object = 71c528b66a42fb4bfbc83f12b8483844cc9d02b6 = git rev-parse HEAD:PATH (equal, non-empty), guard-text count back to 1, git diff HEAD empty, git status --porcelain empty over the WHOLE tree; rebuilt and re-proved on the artifact with the same preflight WITHOUT --absent -> 'dist/: marker present in 2 built files ... tree: working tree clean against HEAD'; re-run on the restored tree 'Test Files 1 passed (1)' / 'Tests 27 passed (27)'.",
"mcp_calls": "2 - update_pull_request (body PATCH) and this comment. Everything else went through git and the public-repo zero-quota page payload: the raw PR body was read from the page's clipboard-copy value, not round-tripped through an MCP read, so hazard 13165 (HTML-escaped entities stored back) could not apply. Container REST is 403 for this seat ('GitHub access is not enabled for this session'), gh CLI absent.",
"open_questions": [],
"out_of_scope_findings": [],
"scope_and_conflicts": [
"BODY EDIT CHOSEN OVER A COMMENT, and verified: the body was PATCHed and then read back from the page's raw clipboard-copy value. Stored text is byte-identical to intent except the footer region. Hazard 12886 did NOT fire - the attribution footer survived. What did change: the body had TWO identical footers before; I submitted two bare ones and the platform stored exactly ONE bare footer, collapsing the duplicate. No content was lost.",
"ONE EDIT BEYOND THE NAMED SCOPE, reported rather than made silently: body line 3 read 'Deliberately not `Fixes`: #13214 ...' - a closing keyword immediately adjacent to a card number. GitHub's closing-keyword parser ignores the negation around it, so that spelling risked auto-closing the needs-user-decision card 13214 on merge, which is precisely the outcome the sentence exists to prevent. Rewritten to 'Deliberately a `Part of` link and not a closing keyword - #13214 is a decision awaiting the maintainer ...', keeping the meaning and separating the keyword from the number. The PR remains 'Part of #13214'.",
"ALSO UPDATED, as the standing contract requires and the out-of-scope list did not name: the Verification section, which quoted final head ba06dabd and 'Test Files 159 / Tests 2650' - both stale after the merge and the edit. It now quotes head 5869d27f23, the post-merge derivation banner, and the real counts. A second ablation subsection was ADDED beside the original one; the original rest-server ablation readings are untouched.",
"UNTOUCHED as instructed: sections 1 and 2, the other blast-radius bullets, the controls, the original ablation, rest-server.ts (byte-identical to base - the diff is one file, packages/rest/src/ui-view-route-tenancy.measurement.test.ts), and card 13214's needs-user-decision grading.",
"NO FORCE-PUSH, no draft flip, no label writes. Labels already on the PR (read from the page, data-name spelling): size/l, skip-changeset, tests - skip-changeset is present, so nothing was added.",
"NOT MEASURED, restated so it is not read as green: node scripts/check-test-completeness.mjs (exit 3, PREREQUISITE NOT MET). Everything else in the derived union ran and is quoted above."
]
}Generated by Claude Code Generated by Claude Code |
Part of #13214
⛔ Deliberately a
Part oflink and not a closing keyword — #13214 is a decision awaiting the maintainer, and a measurement must not auto-close it. (The keyword and the card number are kept apart on purpose: GitHub's parser reads a closing keyword next to a number regardless of the prose around it.)⛔ MEASUREMENT ONLY — nothing is repaired
The change set is one new test file.
packages/rest/src/rest-server.tsis byte-identical to the base — blob3eca151d4b118f0a84c10958cd3c5e8b43b79406on both sides, verifiable withgit rev-parse HEAD:packages/rest/src/rest-server.ts. Access control is a human floor in this repo: reporting the measurement is the complete delivery.packages/core/src/security/auth-gate.ts(#7898's on-hold trigger file, H17) was not read or edited on this branch — the previous run already settled that question by driving the predicate.What this extends, and what it does NOT re-measure
PR #13244 settled the single-tenant half: this route resolves no identity at the REST seam, and
getUiViewapplies no authorization downstream because the seam hands it exactly{ object, type }. ⛔ None of that is re-measured here.What #13244 explicitly did not assert is its own
UNMEASURED 1. Its harness had noenvRegistryand nokernelManager, so the environment-resolution chain was read from source and never driven. This PR drives it for the first time, treating the earlier reading as a hypothesis rather than a fact.⭐ The instrument is proved bidirectional before any reading is trusted
Four controls run first (
section 0), because a harness that always answers "environment B" is no evidence of a crossing and one that can only answer "environment A" is no evidence of its absence:/environments/:environmentId/ui/view/...), where naming an environment is declared and URL-visible, answers with environment B's view. So the route CAN deliver B's body on this instrument, and a negative would have been a real negative.acquiredis an observable and not a constant.A third distinct answer (
Control Plane Accounts) is wired at the bootprotocolargument, so "fell through to the control plane" cannot collapse into "resolved to A".① and ② — the answers, per channel
Both channels cross. Each is a separate reading and each carries its own controls.
Channel 1 — the
X-Environment-IdheaderAn anonymous request (no execution context at all) on the unscoped URL, whose hostname resolves to nothing, carrying only
X-Environment-Idnaming environment B, receives environment B's UI view. Three independent observations, not one:Beta Environment Accounts,beta_only_field), not the A baseline C2 measured;acquired === ['env_beta']);resolveExecCtxwas called zero times, on every site the request reached.An entitled caller naming B receives byte-identical bytes, so the answer does not depend on the caller.
Negative control: a header naming an id the registry does not know does not cross — it falls to the default environment — and the registry was consulted (
idLookupscontains the probed id), so that green is a decision rather than a header nobody read.Channel 2 — the request hostname
An anonymous request whose hostname is bound to environment B receives environment B's view, again with
resolveExecCtxat zero and B's kernel acquired.③ Blast radius on the cross-environment path — measured, ⛔ not inherited
Every reading below is pinned to the crossed body (it asserts B's own label first), so none of it is an envelope statement true of any environment.
{ object, list }withlist={ type, label, columns, sort, searchableFields }and each column exactly{ field, label, sortable }.rows,records,data,total,valuesoritems; and the payload names no driver, connection, datasource or credential, and does not echo the environment id.required/readonly/type/ label declarations.hiddenIS a uniform floor on the crossed path — as of 2026-08-30.beta_secret(hidden, not a priority name) was dropped from the list, whilestatus(hidden, and one of the producer's priority names) was served, with its authored label. The producer's list branch applied!fields[k].hiddento the fill pass only and never to the priority pass, while the form branch filtered all hidden fields uniformly — two branches of one producer disagreeing. test(rest): measure identity resolution at GET /ui/view/:object/:type — seam, downstream producer, and the ratchet blind spot behind it #13244 had measured this with one hidden non-priority field and reported "hidden is dropped by declaration": true of the field it drove, not of the class.hiddenon getUiView's list priority pass #13329 (card [finding]getUiViewlist branch serves fields declaredhiddenwhen the field name is one of its nine priority names — declared, not enforced #13259, commit2a75270b1e), which put the same filter on the priority pass. ⛔ The 2026-08-29 reading was not wrong — it was true when taken and has been made false by a fix.columns=["name", "created_at", "beta_only_field"]; neitherBeta StatusnorBeta Secretappears among the emitted column labels;searchableFields(derived fromcolumns) carries neither; and the form branch is unchanged at["name", "beta_only_field"]— so the two branches now agree. The case is rewritten to pin that, with controls (name,beta_only_field,Beta Account Namemust still be served) so an empty producer cannot satisfy it vacuously, and with a name-agnostic invariant computed from the fixture rather than from a copy of the producer's priority list.hiddenon getUiView's list priority pass #13329 ships its own pin (packages/metadata-protocol/src/protocol.ui-view-hidden-columns.test.ts) which callsgetUiViewdirectly, in-package, from source and sweeps all nine priority names — that pin is the authority on the producer, and none of it is re-measured here. What it does not drive is this file's subject: the crossed, cross-environment path — anonymous request,X-Environment-Id, unscoped mount, through the REST seam and environment resolution into the builtmetadata-protocolartifact. This bullet is the blast-radius reading for the crossing; a regression reachable only through that chain would leave the producer-level pin green.④⚠️ Exactly what a caller must know — the severity turns on this
Must be true of the deployment (all measured, each with a control):
envRegistryANDkernelManagerboth wired. With neither, the header is inert and the request falls to the control-plane protocol — which is precisely why test(rest): measure identity resolution at GET /ui/view/:object/:type — seam, downstream producer, and the ratchet blind spot behind it #13244's harness could not see this, and that reproduction is included as a control. WithenvRegistrybut nokernelManager, the legacy chain is skipped entirely (its guard isenvRegistry && kernelManager).requestEnvResolverinjected, or one that throws. When the host injects a resolver its normal return is final (ADR-0076 D11 step ④), so the legacy hostname/header chain is never consulted — measured, with the control that shows the same injected resolver can send the request to B, so this is the resolver deciding and not a dead channel. A throwing resolver degrades to the legacy chain and the header crosses again — also measured.⭐ The generalisation matters more than either channel. The route applies no identity check regardless of which channel named the environment: with a
requestEnvResolverwired that names B, the anonymous caller still receives B's view. So injecting a resolver relocates the decision; it is not by itself a gate. Whether a given production deployment wireskernel-resolveris a deployment fact this PR did not drive —rest-api-plugin.ts's comment says the cloud runtime registers one next toenv-registry, and nokernel-resolverimplementation ships in this repo. Stated as READ, not driven.Must be known by the caller:
accountis a stock name and no credential is needed to try one.envRegistry.resolveById, so an arbitrary string does not win.env_alpha) produces the same "this id resolves" signal, so the discriminator is validity and not the identity of B.Must NOT be known: nothing else. No cookie, no
Authorization, no session, no prior request. Driven with a request carrying onlyhostplus the one header.Contrast — a guarded sibling under the same tenancy wiring
GET /data/:objectnaming environment B anonymously answers401with the anonymous-deny code, and serves an entitled caller — so this harness does express a refusal, and the 200s above are readings about this route rather than a driver that cannot produce a 401.Reverse verification (ablation)
Run on the committed implementation, mutating the legacy hostname/header branch of
resolveRequestEnvironmentIdto be unreachable. Predicted direction: the crossing disappears — every cross-environment reading should fall back to the default environment.grep -c= 1, anchor text remaining = 0, blob moved3eca151dto8a0174ee. The script aborts the run if any of those does not hold.rest-server.ts, imported in-package as./rest-server.jsand resolved by vitest to the TS source, not through a dependency'sexportstodist/. The 12 reds with no rebuild are themselves the proof the mutation reached the code under test. The one dependency that does resolve todist/is@objectstack/metadata-protocol(unaliased here, registered inKNOWN_UNALIASED_TEST_IMPORTSfor@objectstack/rest); the full workspace closure was built before every reading, and the test additionally asserts a post-GET /ui/view/:object/:type发的是{list}/{form},spec 的GetViewResponseSchema声明的是{object, view}—— 同一路由声明与实际两种形状(#5563 同族) #5948 shape (objecton the container, absent fromlist) so a stale dist would fail loudly rather than report an old producer's behaviour as current.git checkout HEAD -- "$REPO_ROOT/packages/rest/src/rest-server.ts"(absolute path,HEAD-pinned to bypass the index) undertrap ... EXIT INT TERM. Restored blob3eca151d...equals the HEAD blob, injected text count 0,git diff HEADempty.⭐ Second ablation — the corrected
hiddenreading (2026-08-30)The rewritten section-3 case gets its own reverse verification, because an assertion that changed direction has to be shown discriminating in its NEW direction. Predicted: red — and red on that one case only. With the guard removed the crossed
columnsshould carrystatusagain.packages/metadata-protocol/src/protocol.ts, mutated back to its pre-repair spelling —!fields[k].hiddendeleted from the priority pass and left on the fill pass. ⛔ Notrest-server.ts, which stays byte-identical to base throughout.grep -c= 0, mutated-spellinggrep -c= 1, blob moved71c528b66a42fb4bfbc83f12b8483844cc9d02b6(git rev-parse HEAD:packages/metadata-protocol/src/protocol.ts) toae9f8f56c2ae19c3e0b72c2e614753521573b55b. The script aborts the run if any of those does not hold.dist/(@objectstack/metadata-protocolis unaliased for@objectstack/rest, registered inKNOWN_UNALIASED_TEST_IMPORTS), so an unbuilt mutation would have stayed green and certified a vacuous assertion.pnpm --filter @objectstack/metadata-protocol build, thennode scripts/ablation-dist-preflight.mjs @objectstack/metadata-protocol '...' --absent—dist/: marker absent from all 24 built files.AssertionError: expected [ Array(4) ] to not include 'status'. Every control and every other section-3 reading stays green, so the ablation is aimed rather than broad.git checkout HEAD -- "$REPO_ROOT/packages/metadata-protocol/src/protocol.ts"(absolute path,HEAD-pinned so the index cannot serve the mutation back) undertrap ... EXIT INT TERM. Restored blob71c528b66a42fb4bfbc83f12b8483844cc9d02b6equals the HEAD blob, guard-text count back to 1,git diff HEADempty andgit status --porcelainempty — whole tree, not a per-path diff. Then rebuilt again and re-proved on the artifact (same preflight, without--absent):dist/: marker present in 2 built files ... tree: working tree clean against HEAD. Re-run on the restored tree:Test Files 1 passed (1) / Tests 27 passed (27).Verification
All exit codes captured before any pipe; every gate quoted from its own printed verdict line. ⭐ The union was re-derived after merging current⚠️ That ordering is the point: on a stale tree the script exits 0 and silently omits families that have since landed. It paid off here —
origin/main, not before —node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack, whose stderr banner readsgate list derived from the tree of 'objectstack-ai/objectstack' at commit 5869d27f23andchange set derived from git — 1 path(s) vs merge base 2be4a6729 of 'origin/main' and HEAD.check:doc-authoringis in this union and was not in the previous run's list. Everything below ran at final head5869d27f23.pnpm --filter @objectstack/rest exec vitest run --maxWorkers=2 src/ui-view-route-tenancy.measurement.test.ts—Test Files 1 passed (1)/Tests 27 passed (27).pnpm --filter @objectstack/rest test(whole package) —Test Files 160 passed (160)/Tests 2698 passed (2698).pnpm --filter @objectstack/rest typecheck— exit 0;check:test-typecheck: OK — @objectstack/rest's test layer compiles under packages/rest/tsconfig.test.json; 3 file(s) / 5 error(s) held in test-typecheck-debt.json. ⭐ Not a NOT-MEASURED green, and measured both ways this time:tsc -p tsconfig.test.json --listFilesputs this file in the program (1 hit) andtsc -p tsconfig.json --listFilesdoes not (0 hits), socheck:test-typecheckis the leg that actually reads it and the package's plaintsc --noEmitis not. That program is also what makes the constructor pin real — the boot builds its 20 arguments as a tuple typed as the constructor's own parameter list (theConstructorParametersutility type applied toRestServer), so both arity and every position are type-checked. test(rest): measure identity resolution at GET /ui/view/:object/:type — seam, downstream producer, and the ratchet blind spot behind it #13244's 27-argument call to the 20-parameter constructor ran fine while shifting three providers onto the wrong parameters, andkernelManager/envRegistryare positions 4 and 5, which is exactly where a tenancy harness would have been silently wrong.pnpm check:authz-resolver—check:authz-resolver: single shared authorization resolver intact; both entry points delegate.check:nul-bytes(OK (scanned 7433 text file(s) — 7433 tracked, 0 untracked-not-ignored; skipped 7 binary; no raw ASCII control bytes)),check:cross-package-test-inputs(OK: 24 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob),check:doc-authoring(393 files clean — no bare metadata literals;48 published skill files clean;14050 customer-facing string(s) across 691 spec sources clean; sibling-package prose ids hold the baseline),check:dispatcher-error-vocabulary,check:logger-receiver-detach,check:objectql-double-limit,check:page-declaration-shape,check:published-files,check:slot-lookup,check:test-source-alias,check:type-source-resolution,check:where-matcher,check:query-options-erasure,check:engine-double-contract,check:type-check-coverage,check-ci-filter-parity,check-comment-mask-adoption,check-cross-package-test-inputs,check-keyed-text-bounds,check-plugin-teardown-shape,check-shard-attestation,check-undeclared-dep-imports,docs-audit/check-affected-docs,docs-audit/check-drift-comment.turbo run build --concurrency=2 --filter=./packages/* --filter=./packages/*/*—8 cached, 70 total, exit 0) because the next two refuse outright unbuilt and a refusal is NOT MEASURED:check:dual-build-cjs-loads—102 published require entry point(s) across 66 package(s) load; 610 emitted CommonJS file(s) parse; 1 cross-format behaviour probe(s) agree;check:type-check-debt—check-type-check-coverage --re-measure: OK — 29 ledger entr(ies) re-measured in 302.4s, 1547 raw tsc error(s) total, none above its recorded number,surplus: none.pnpm lint(repo-wideeslint . --no-inline-config) — exit 0. Run in full; no narrowing claimed.node scripts/check-test-completeness.mjsexits 3PREREQUISITE NOT METwith no argument — it grades a savedturbo run testlog and cannot produce one; CI tees it and passes the path. Recorded NOT MEASURED, never as a pass and never as a red.No changeset
Test-only diff: one file under
packages/rest/src/, publishing nothing from any package (the build config excludes*.test.tsfrom the published artifact, and ci.yml gates that).skip-changesetapplies.Generated by Claude Code