fix(data-objectstack): a refused view read is not an object with no saved views - #8901
Conversation
…ct with no saved views
`ObjectStackAdapter.listViews` degraded every failure to an empty list, so a
refused (401/403) or broken (5xx, dropped connection) `view` read returned the
identical value as a list the server served empty — and every consumer reads
only the return. That is the swallow objectui#7741 removed from
`listImportMappings` one method over; here it renders as an object's view
switcher showing no saved views at all, including ones the user created.
The degrade stays. What is added is the discrimination, read from `err` itself
rather than from the emptiness of the result:
- `classifyViewsFailure(err)` — a separate reading, not a second caller of
`classifyImportMappingsFailure`. `view` is in the platform's static spelling
contract, so the metadata list door's 400 `INVALID_REQUEST` ("this deployment
carries no such kind") is unreachable for it; carrying that quiet arm over
would swallow a real refusal. Only a host with no `/meta` door stays quiet.
- `MetadataReadWarningEvent.operation` / `.kind` gain their second members —
the additive, reviewed widening the single-member unions were built for.
- `MetadataReadFailureKind`, with `ImportMappingsFailureKind` now its alias.
- `metadataReadWarningToast` picks title and remedy by `operation`, so a views
failure no longer renders the import-mapping sentence. Three new
`console.savedViews*` keys in all ten locale packs.
Refs objectui#8151.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01611D6ZaRaMmwTNQmSbk8MH
❌ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. Which half objected:
📦 Bundle Size Report
Size Limits
|
CI 现状:两条红,处置不同❌
|
…nsert moved `scripts/check-doc-example-types.mjs`'s ledger is keyed by `path:line symbol`. This branch inserts +262/-23 lines into `packages/data-objectstack/src/index.ts`, which pushed `createObjectStackAdapter`'s `@example` block down, so the stored key stopped resolving and `scripts/__tests__/check-doc-example-types.test.ts` went red on "every row names a block that is actually in the compiled tier". The key was located by FILE + SYMBOL and its new value re-derived from the checker's own extractor — `ledgerKey(block)` over `exampleCensus()` — never from a line number read off a report. Ledger row count 90 -> 90, zero executable change. Re-measured on this branch: exactly one ledger row names a file this branch touches; `metadataReadWarningToast.ts` and the ten locale packs have none. The tree-wide census has no discriminating power over this class: it reads prose citations, and this is a stored literal used as a key. The test that separates them is stored-vs-computed — a `path:line` assembled at runtime to print a diagnostic cannot go stale, one persisted and compared for equality can. Refs objectui#8151. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01611D6ZaRaMmwTNQmSbk8MH
❌ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. Which half objected:
📦 Bundle Size Report
Size Limits
|
Contract review at
|
|
Clear-and-hold provenance — director seat, summon #18 segment 6 ( Generated by Claude Code |
|
Tier notice — the contract-review-tier requirement on this PR is lifted (skills seat, session Maintainer ruling, verbatim: 「现有的卡片如果写了要求fable的,也要让相关的项目经理知道,opus就够了。」 Under the same ruling set (quoted in full on objectstack-ai/objectstack#17285), the contract-review tier is reserved for the skills seat (protocol files + the published For this PR: its Generated by Claude Code |
…imination Conflict in scripts/check-doc-example-types.mjs, resolved as follows: - The `packages/data-objectstack/src/cache/MetadataCache.ts:56 MetadataCache` ledger row is DROPPED. `main` removed the `@example` block it names (the MetadataCache-as-constructible doc fix), so the row would name a block that is no longer in the compiled tier. - The `createObjectStackAdapter` row keeps ONE key, re-derived on the merge result from the checker's own extractor (`ledgerKey(block)` over `exampleCensus()`), never by arithmetic on either side's number: `packages/data-objectstack/src/index.ts:6562 createObjectStackAdapter`. Ledger row count 90 -> 89; zero executable change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01611D6ZaRaMmwTNQmSbk8MH
…oth admitted claimants `PER_CHUNK_GZIP_CEILINGS['i18n-locales']` 455,000 -> 465,000 and `PER_CHUNK_BASELINE['i18n-locales']` 446,076 -> 456,196, moved together in one commit as that file requires. The ten locale packs' citation of the retired headroom is updated in the same change so it cannot go stale again: it now names no figure at all, which is the form the file's own anti-drift rule allows. Four console builds, one container, one instrument -- `i18n-locales` read out of the `apps/console/dist/eager-closure.json` each build writes: bbe285e main 454,602 (headroom 398) 3949cf3 main + objectui#8901 455,271 +669, over by 271 ea5eab7 main + objectui#8888 455,519 +917, over by 519 ba20b0bc0 main + BOTH 456,196 +1,594, over by 1,196 The two deltas sum to 1,586 against a measured 1,594: two independent claimants on this chunk are additive to within 8 bytes, which is the fact a shared budget needs and the one a per-pull-request reading cannot produce. Sized at 8,804 bytes of headroom = 0.10x REGRESSION_THIS_GATE_MUST_CATCH_BYTES, this key's own convention and marginally tighter than the 8,924 (0.10x) the retired pair carried. NOT sized at the 804 bytes the overage needed: a line with ~0 headroom is what objectui#8816 was filed about -- it reddens whichever in-flight pull request is weighed second and blames that diff for arithmetic that is not its own. Trimming was measured first and cannot reach 1,196 bytes inside these changes: best key reuse is 128 bytes for objectui#8888 and there is no reuse candidate at all for objectui#8901. The payload reduction that would make the next raise unnecessary is real but is not this: `check:i18n-dead-keys` reports 127 confirmed dead keys in ten packs, and `@object-ui/i18n` publishes them, so removing one is a published-surface decision that needs its own card. Nothing else moved: not REGRESSION_THIS_GATE_MUST_CATCH_BYTES, not the other three per-chunk ceilings, not MAX_EAGER_CLOSURE_GZIP_BYTES (the aggregate holds 23,507 bytes of headroom, 0.26x, with both claimants on it). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01611D6ZaRaMmwTNQmSbk8MH
…read-discrimination Second merge of the session: `main` advanced five commits while the `i18n-locales` budget was being measured. No conflicts. `packages/i18n` is untouched in that range, so the chunk this branch is measured on did not move; re-measured anyway rather than derived. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01611D6ZaRaMmwTNQmSbk8MH
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
|
os-dev-report {
"issue": 8816,
"status": "done",
"branch": "claude/issue-8151-listviews-refused-read-discrimination + claude/issue-7388-chatbot-progress-phase-i18n",
"pr": "https://github.com/objectstack-ai/objectui/pull/8901 https://github.com/objectstack-ai/objectui/pull/8888",
"prs": [
"https://github.com/objectstack-ai/objectui/pull/8901 (card objectui#8151) head 7a1971eec",
"https://github.com/objectstack-ai/objectui/pull/8888 (card objectui#7388) head 7b01b45c5"
],
"template_deviation": "One dispatch, two PRs and three cards, so the single-valued `issue`/`branch`/`pr` fields cannot be truthful as written. `issue` names the shared blocker card objectui#8816 (the decision actually settled); the two carrier cards are objectui#8151 and objectui#7388. `branch` and `pr` list both. Nothing was collapsed to make the template fit.",
"premise_still_valid": true,
"summary": "Both PRs were red on `Bundle Analysis` alone, both on the same `i18n-locales` per-chunk ceiling, and neither fits under it alone. Route (1) taken on measurement: PER_CHUNK_GZIP_CEILINGS['i18n-locales'] 455,000 -> 465,000 with PER_CHUNK_BASELINE['i18n-locales'] 446,076 -> 456,196 in one commit, plus the written reason in the checker's own header and the removal of ten now-false citations of the retired headroom in the locale packs. Route (2) was measured first and cannot reach the 1,196-byte combined gap inside these two changes (best reuse 128 bytes for #8888, no reuse candidate at all for #8901); the reduction that WOULD reach it (127 confirmed dead keys in ten published packs) is a published-surface decision that needs its own card and is deliberately not ridden in. The identical bytes are on BOTH branches because `evaluateCeilingFreshness` exits 2 on a checkout that does not carry a ceiling the base branch has moved, so whichever lands first the other stays green. Both branches were merged onto `main` with real merge commits twice (`main` advanced five commits mid-flight); one conflict, in `scripts/check-doc-example-types.mjs`'s UNGATED_EXAMPLES ledger, resolved by re-deriving the key from the checker's own extractor on the merge result. No assignee written, no review label touched, no Clause-2 declaration edited.",
"instrument": "`apps/console/dist/eager-closure.json`, written by `emitEagerClosureReport` in `apps/console/vite.config.ts`, read by `node scripts/check-eager-closure-budget.mjs`. Nine full build pairs (`pnpm turbo run build --filter='./packages/*'` then `pnpm --filter @object-ui/console build`) from the repo root, one container, all through the shared verify lock.",
"measurements": {
"ceiling_before": 455000,
"ceiling_after": 465000,
"baseline_before": 446076,
"baseline_after": 456196,
"regression_constant_unchanged": 91136,
"base_main_bbe285ee7": 454602,
"base_headroom_before": 398,
"delta_pr8901_objectui8151": 669,
"delta_pr8888_objectui7388": 917,
"combined_ba20b0bc0": 456196,
"combined_delta": 1594,
"gzip_interaction_between_the_two": 8,
"combined_over_old_ceiling_by": 1196,
"new_headroom_over_combined": 8804,
"new_headroom_as_multiple_of_regression": "0.0966x",
"aggregate_with_both_on_main": "3,573,493 / 3,597,000 — headroom 23,507 = 0.26x, never objected, MAX_EAGER_CLOSURE_GZIP_BYTES untouched",
"remeasured_on_the_newer_main_b97129e96": "i18n-locales byte-identical on all three trees (455,271 / 455,519 / 456,196); `packages/i18n` untouched in bbe285ee7..b97129e96"
},
"route_taken": "(1) raise the ceiling. Sized at this key's own 0.10x convention (8,804 bytes), NOT at the 804 bytes the overage needed: a ~0-headroom line is precisely the defect objectui#8816 was filed about, because the gate weighs the merge ref and reddens whichever in-flight PR is weighed second while naming that PR's diff. Measured runway for the new headroom: the retired pair landed 177afeba1 on 2026-09-03 at 446,076 and main measured 454,602 on 2026-09-10 — 8,526 bytes in seven days — so this is about a week of runway, not a settlement, and the structural answer (take the catalogues out of the eager closure) is named in the file rather than implied.",
"tests": "GREEN, per tree, `check:eager-closure` exit 0 with all four per-chunk lines passing: PR#8901 merge result 7a1971eec — i18n-locales 455,271/465,000 headroom 9,729; PR#8888 merge result (tree 8794b5848) — 455,519/465,000 headroom 9,481; BOTH on main (3cef20e66) — 456,196/465,000 headroom 8,804, aggregate 0.25x. CAN STILL REDDEN (the control): on the combined tree with the NEW 465,000 ceiling in place, 300 deterministic high-entropy filler keys (30 per pack x 10 packs) were injected and PROVEN ON DISK first — per-file marker count 0 -> 30 and a changed blob hash on all ten — then a full rebuild: i18n-locales 456,196 -> 471,668, printed `OVER by 6.5 KB`, `check:eager-closure` exit 1. Direction predicted before the run and observed as predicted; lit controls held (vendor-objectstack and ui-components byte-identical across the mutation, framework +2 B, aggregate still GREEN at 7.9 KB) so the line that objected is the per-chunk one at its new number. Restored by `git checkout HEAD -- PATH` (never bare) and verified BY STATE — all ten blobs back to their HEAD hashes, marker count 0, `git diff HEAD` empty for the packs — with `trap ... EXIT INT TERM` and absolute paths throughout; the mutated `dist/` was then deleted so no later run can read it back. PAYLOAD-NEUTRALITY of the fix itself, checked not argued: identical closure bytes before/after the ceiling+comment edit on all three trees (455,271 -> 455,271, 455,519 -> 455,519, 456,196 -> 456,196, aggregate unchanged to the byte). Unit: `pnpm exec vitest run scripts/__tests__/check-eager-closure-budget.test.ts` 104/104. Suites: 8151 tree `pnpm exec vitest run packages/i18n/ packages/data-objectstack/ packages/app-shell/src/providers/ scripts/__tests__/check-eager-closure-budget.test.ts scripts/__tests__/check-doc-example-types.test.ts` -> 145 files / 2222 tests passed; 7388 tree `pnpm exec vitest run packages/i18n/ packages/plugin-chatbot/ scripts/__tests__/check-eager-closure-budget.test.ts` -> 108 files / 1696 tests passed. Type-check: turbo 32/32 and 11/11. Lint in the CI shape (`turbo run lint` per package, plus `lint:root`): 0 errors on both. Gates green on both final merge results: check:control-bytes, check:i18n-keys, check:i18n-drift, check:i18n-designer-parity, check:i18n-dead-keys (report-only), check-changeset-presence, check-doc-example-types, check:eager-closure. NOT MEASURED, declared to CI: the full workspace `pnpm lint` / `pnpm test` farm, and `Bundle Analysis` itself on the real GitHub merge refs.",
"mcp_calls": "0 — every GitHub read and write went through repo-scoped REST (probe: GET /repos/objectstack-ai/objectui -> 200)",
"open_questions": [],
"out_of_scope_findings": [
"noted, not filed: `pnpm check:i18n-dead-keys` reports 364 candidates across 47 namespaces, 127 of them CONFIRMED with no textual footprint anywhere in the repo, in ten packs each — far more than the 1,196 bytes this raise needed. Removing them is the work that makes the next raise unnecessary, but the gate is report-only BY DESIGN and `@object-ui/i18n` publishes these packs, so a deletion is a published-surface decision, not a byte saving. Successor: objectui#8816, which is open and whose own comment thread already records that its adjudication is the one piece of work certain to touch these bytes.",
"noted, not filed: `ui-components` is now the tightest live line at 4,307 bytes of headroom (0.05x the regression) while `packages/components` moves most days — measured on every one of the nine builds. It is the next chunk to collide, and its cause will have nothing to do with localization. Successor: objectui#8816, whose 2026-09-09 comment predicted exactly this.",
"noted, not filed: `eslint . --no-inline-config` is a WRONG-SHAPED probe for 'is CI Lint green'. It disables the repo's deliberate `eslint-disable-next-line` directives, so it reported 94 errors (78 files) on a tree whose CI-shaped lint (`pnpm lint` = `turbo run lint`, plus `lint:root`) is 0 errors — e.g. the four `no-console` hits in `packages/data-objectstack/src/index.ts:1668-1688` are the objectui#4029 Logger binding, disabled line by line on purpose. Successor: none named; recorded because PR #8888's own body cites a `--no-inline-config` run as its lint evidence.",
"noted, not filed: `check:changeset-presence` has no `pnpm run` alias — the script is `node scripts/check-changeset-presence.mjs` and `pnpm run check:changeset-presence` exits 1 with ERR_PNPM_NO_SCRIPT, which reads exactly like a red gate. That is a NOT MEASURED shape, not a failure. Successor: none named."
],
"contradicts_the_brief": [
"The brief said every prior figure is 'stale by construction' and must not be taken as fact. Re-measuring was right, but the premise was not: 454,602 / 455,271 / 455,519 reproduced BYTE-FOR-BYTE against the earlier seats' readings, and then again on a five-commits-newer main (bbe285ee7 -> b97129e96), because `packages/i18n` had not moved in either range and this chunk is 100% packages/i18n modules. Two independent paths landing on the same number is corroboration, not a citation.",
"The brief said the ceiling change 'belongs on whichever branch lands first'. Reading `evaluateCeilingFreshness` says it must be on BOTH, byte-identically: once the first lands, the base branch has moved PER_CHUNK_GZIP_CEILINGS, and a checkout that does not carry the new value scores as `superseded` and exits 2 on the second PR. With identical constants the second reports 'this checkout already carries the new value' and the first's landing merges as a no-op for that file.",
"The brief preferred route (2) if available at reasonable cost. Inside these two PRs it is not available at any cost — 128 bytes of reuse for #8888, none for #8901, against a 1,196-byte gap. But a much larger reduction IS available just outside them (the 127 confirmed dead keys above), which is the honest shape of that preference: not 'trim these strings' but 'stop shipping the ones nothing reads', and that is a decision rather than a cleanup.",
"PR #8888's existing body reports its lint evidence as a full-population `eslint . --no-inline-config` run with 0 errors over 4674 files. The same command on the current merge result gives 4698 files and 94 errors, all of them pre-existing directive suppressions the flag switches off. The PR is not lint-red — its CI-shaped lint is 0 errors — but that particular sentence in its body does not reproduce."
]
}Generated by Claude Code |
⭐ CI 已确认:
|
| PR | head | Bundle Analysis |
|---|---|---|
| #8901 | 7a1971eec |
completed / success |
| #8888 | 7b01b45c5 |
completed / success |
⇒ 这两个 PR 唯一红着的那一道,现在两边都绿。本次读数时各自还有 4 个 Test (shard n/4) 在跑,其余 26 项 success、3 项 skipped;另有一个与同名成功项重复的 Governed Surface Queue Guard 显示 cancelled(并发取消,非本次改动引入)。⛔ 本席不等它们收敛 —— 那归 PM 复核。
Bundle Analysis 称量的是 merge ref。两个分支带的是逐字节相同的天花板常量,所以先落地的那个对第二个是该文件上的空合并,evaluateCeilingFreshness 会报 "this checkout already carries the new value" —— 谁先落地都行。但若 main 上又落进新的语言包键,余量会重新被吃,那正是 objectui#8816 第五节记的那笔工作要处理的。
Generated by Claude Code
Fixes #8151
ObjectStackAdapter.listViewsdegraded every failure to an empty list, so a refused (401/403) or broken (5xx, dropped connection)viewread returned the identical value as a list the server served empty — and every consumer reads only the return. The degrade stays; what is added is the discrimination, read fromerritself rather than from the emptiness of the result (framework #13906 decision 1 option A — a thing that could not be READ is not a thing that is ABSENT).The re-check, re-run on this tree
The card measured two hits on
21d7989fb. On my baseb686ebf7dit is still two, at moved line numbers and in the opposite order — so the card's addresses are stale even though its count is not:(the card read
listViewsat:4893andlistImportMappingsat:4663.) No third hit. Both breadcrumbs are kept verbatim — the console line was never the problem; being the only discriminator was.The reading this card owed: which
viewfailures deserve to stay quietThe card and the dispatch both said ⛔ this is not
classifyImportMappingsFailureunder a new name. It is not.classifyViewsFailureis a separate branch table, and the difference is exactly one arm.⛔ The arm that does NOT carry over — 400
INVALID_REQUEST. Onmappingthat is the supported "this deployment carries no such kind" and it is quiet. Onviewit cannot mean that, measured three ways on the framework tree:viewis in the platform's static spelling contract, so the refusal is structurally unreachable for it.RestServer.refuseUnknownMetaListType(framework#9488,packages/rest/src/rest-server.ts) returns without writing a refusal wheneverunrecognisedMetaTypeRefusal(urlType)isnull, and that predicate answersnullfor any spelling in the contract.packages/spec/src/meta-spelling/meta-url-data.generated.tscarries both"views": "view"and the canonical singularview.viewto be older than.mapping's quiet arm exists becausemappingwas PROMOTED into the declared set (framework#2611), so pre-promotion servers are a real shipped population.viewis the kind the metadata surface is built around — the compound-arity door/meta/OBJECT/views/VIEWNAME, the ADR-0017ViewItemdiscriminant this very method filters on.viewcannot render the caller.listViewsis reached only fromObjectView, whoseobjectDefcame fromMetadataProvider— which reads the same door and listsviewin itsEAGER_TYPESat mount (packages/app-shell/src/providers/MetadataProvider.tsx:51).⇒ Carrying that arm over would have put a fresh swallow inside this card's own fix. It falls through and is announced.
The quiet set on
viewis therefore strictly smaller than onmapping: the DOORLESS arm only —ROUTE_NOT_FOUND/NOT_IMPLEMENTED, plus a code-less bare 404/501 (a proxy, a gateway, a host with no API mounted). That stays silent for objectui#7741's reason — a real, supported deployment shape must not become a visible fault — and it costs nothing here, because such a host has already failedMetadataProvider's eagerapp/object/viewreads. A per-object toast would be a fourth voice on one deployment fact, not a new one.Which ADR-0112 codes actually appear on this path, and the order they are read in (
codefirst, status only where no code was declared — the existing mechanism's order, not reversed):UNAUTHENTICATED/PERMISSION_DENIED(refused),ROUTE_NOT_FOUND/NOT_IMPLEMENTED(doorless), and — the residual that matters — anything else, including a coded 4xx this consumer cannot name.Nothing has to stay quiet to spare a healthy deployment.
listViewsis uncached (pinned byviewCacheInvalidation.pin.test.ts) and runs on everyObjectViewmount, so a failing deployment emits once per mount. But every class that is loud (401/403/5xx) is a class a healthy deployment is not in, and every class a healthy deployment IS in is already quiet. Volume noted, not designed around; a de-duplication policy would belong to the channel as a whole, not to its second emitter.Two doors, not one. Unlike its sibling,
listViewsis fed byclient.meta.getItems('view')(SDK wrapper:code+httpStatus) and, underpreviewDrafts, byMetadataClient.withPreviewDrafts(true).list('view')(this package's ownparseError:code+status, nohttpStatusat all). The classifier's status ladder is what makes the two doors agree, and there is a dedicated pin for it.The published union widening
MetadataReadWarningEvent'soperationandkindgain their second members. The single-member shape did its job:metadataReadWarningToast's title was one hard-codedt('console.importMappingsUnavailable'), so a second emitter would have toasted "Saved import mappings for crm_lead could not be loaded" on a failed view read — a runtime lie with nothing failing to compile. Adding the member made it a compile error instead.Both consumer switches are now exhaustive on
operationwith anevercheck (title,remedy), matching the discipline the per-reasonswitch already documented.operationis the discriminant rather thankindbecause it names the list the user is standing in front of; the docblock records that the pair is the emitter's invariant, not the type's, and that tightening it to a discriminated union means turning a publishedinterfaceinto a type alias — reviewable on its own terms, deliberately not smuggled in here.Also new:
MetadataReadFailureKind, withImportMappingsFailureKindnow an alias of it (identical members ⇒ existing consumers unaffected in both directions). Four module-private constants that encode PLATFORM facts rather than per-kind ones (META_ROUTE_ABSENT_CODES,META_ABSENT_STATUSES,META_REFUSAL_CODES,META_REFUSAL_STATUSES) were renamed off theIMPORT_MAPPINGS_prefix so both classifiers read one spelling; the mapping-only pair (IMPORT_MAPPINGS_UNKNOWN_KIND_CODE/_STATUS) deliberately keeps its name, which is what makes the one-arm difference legible in the source.The lit control:
listImportMappingsis unchangedlistImportMappings.test.ts, untouched) is green: 26 tests.console.importMappings*sentences objectui#7741 shipped.listViewswent red, which is what makes it a control rather than a co-victim.{code:'INVALID_REQUEST', httpStatus:400}→not-servedvsunreadable), so a later edit cannot quietly fork a second dialect.Tests
Run from the repo root (
pnpm exec vitest run PATHS— neverpnpm --filter … exec vitest, never paths after--).Build + type-check, dependency closure included:
turbo run build→Tasks: 29 successful, 29 total;turbo run type-check→Tasks: 32 successful, 32 total. Measured rather than assumed:data-objectstack'stype-checkdoes compile the new test file (tsc --noEmit --listFiles | grep -c listViews.readWarning.test.ts→1), andcheck-type-check-coveragereports43/43 packages compile their tests.Ablation — the discrimination can fail, and the control cannot
From the committed tree, the discrimination alone was removed (the
classifyViewsFailurecall and the emit block);console.warnandreturn []were left byte-identical, so this ablates the FIX, not the method.The subject resolves through vitest's
resolve.aliastopackages/data-objectstack/src(vitest.config.mts:513), not todist/— so this is a source-resolved ablation and no rebuild sits between the edit and the run. The mutation was still proven on disk before anything was read:Direction predicted before the run, and observed: RED, by case name — 7 failed, 25 passed:
listImportMappings.test.ts— the lit control — stayed 1 file passed throughout.[]and no event cannot fail, because that is also what the un-fixed method does. The loud pins are the ones carrying the discrimination; the quiet ones are boundary statements, not evidence.Restored by
git checkout HEAD -- PATH(never baregit checkout --), and the restore verified by state, not by an exit code:git diff HEADempty, working blob back toa72f1dcb…, suite re-run green at 32/32. The mutation script carriedtrap … EXIT INT TERMwith absolute paths throughout.Line-address citation census — the strong form (objectui#8875)
Not just at push time:
scripts/cross-file-line-citation-census.mjswas run onorigin/mainitself, and again on the tree produced by a realgit merge --no-ffof this branch intomain(a throwaway detached worktree, sincegit merge-tree's archive is not a repository the census can walk).Identical. ⇒ nothing is newly invalidated at the merge point, which is where that risk actually lives. Six citations point INTO files this PR edits; all six were already false on
origin/main([drifted]or[non-substantive]there), so all six are class (B) — ⛔ untouched, objectui#8875's. Judged by anchor, not by line number. Some of them merely re-classify betweendriftedandnon-substantiveas line numbers shift, which is movement within the already-false set and does not change the count.Lint — a measured narrowing, not a skipped run
Three pieces, all read from the tooling rather than assumed:
eslint.config.jshas zero occurrences ofproject:and zero ofprojectService. So a file's verdict is a function of its own bytes plus the shared config.eslint --format jsonover exactly the 14 lintable paths this PR touches → array length 14,errorCount0,warningCount126.@typescript-eslint/no-explicit-anyin this ~6.5k-line adapter, and every warning line falls outside every range this PR added — the new code introduces noany.Gates run green:
check-changeset-presence·check-control-bytes·check-i18n-call-site-keys·check-i18n-en-drift(0 en value(s) changed, 3 key(s) added) ·check-i18n-dead-keys(report-only) ·check-type-check-coverage·check-phantom-dependencies·check-unused-dependencies·check-governed-queue-guard --test(NOT GOVERNED).i18n
Three new
console.savedViews*keys in all ten locale packs, each modelled on that pack's ownconsole.importMappings*sentence with one clause swapped — "not because nothing is registered" → "not because this object has no saved views", using each pack's existing word for a view. Theconsole.importMappings*values are untouched, which is whycheck:i18n-driftreports0 en value(s) changed.验收备注
Measured, out of scope, filed: the dispatch asked what a consumer actually renders on an empty
listViews. The view switcher itself just shows fewer tabs — no assertive sentence. But@object-ui/core'selementDataSourceViewNotFoundMessage(packages/core/src/data-scope/element-data-source.ts) states outright "This object has no saved views.", and its feeder (useElementDataSource.fetchSavedViews) maps only a REJECTION to "could not read" — whichlistViewsnever produces, by design. So the toast this PR adds sits beside that sentence rather than retracting it, and a host composing@object-ui/react+@object-ui/plugin-listwithoutAdapterProvidersees only the sentence. ⛔ Not repaired here (different package pair, and it carries a contract question this card has no standing to answer). Filed as objectui#8900, unlabelled and unassigned for triage.Noted, not filed — successor: none. The
operation/kindpair is an emitter invariant rather than a type-level one; a discriminated union would enforce it but converts a publishedinterfaceto a type alias. Raised here for the contract review rather than filed, since nothing else will touch this type.scripts/check-doc-example-types.mjs’sUNGATED_EXAMPLESledger is keyed bypath:line symbol. This PR inserts +262/−23 lines intopackages/data-objectstack/src/index.ts, which pushedcreateObjectStackAdapter’s@exampleblock down, so the stored key stopped resolving andscripts/__tests__/check-doc-example-types.test.tswent red on “every row names a block that is actually in the compiled tier”. One key updated, located by FILE + SYMBOL and re-derived from the checker’s own extractor (ledgerKey(block)overexampleCensus()), never from a line number anyone quoted. Ledger row count 90 → 90; zero executable change.mainahead of this one and moves lines inpackages/data-objectstack/src/index.tsinvalidates the key again, silently, until the shard runs. This PR is docked onClause-②: yesand will not enqueue immediately, so the window is wide. ⛔ Before merge, re-derive the key on the merge result rather than trusting the number stored here.cross-file-line-citation-census.mjsreads prose citations, and this is a stored literal used as a key. The distinguishing test is stored vs computed — apath:lineassembled at runtime to print a diagnostic cannot go stale; one persisted and compared for equality can. Re-measured on this branch: exactly one ledger row names a file this PR touches (metadataReadWarningToast.tsand the ten locale packs have none).Bundle Analysis: the sharedi18n-localesbudget, re-baselined (objectui#8816)This PR and objectui#8888 / objectui#8901 (the other one) were red on one and the same
check, for one and the same reason, and neither could go green alone. That is one
shared-budget problem wearing two PR numbers, so it was taken as one dispatch and the answer
is identical bytes on both branches — required, not tidiness: see "Why both branches
carry it" below.
Route taken: (1) raise the ceiling. Route (2) — reduce the payload — was measured first
and cannot reach the gap. The reasoning, the arithmetic and what the raise costs are written
into
scripts/check-eager-closure-budget.mjsitself, under "Whyi18n-localesmoved UP",because that is where the budget lives and where the next author will look.
The instrument
apps/console/dist/eager-closure.json, written byemitEagerClosureReportinapps/console/vite.config.tson every console build, read bynode scripts/check-eager-closure-budget.mjs(pnpm check:eager-closure). Every figure belowis one full build pair from the repo root —
pnpm turbo run build --filter='./packages/*'then
pnpm --filter @object-ui/console build— in one container. ⛔ No figure here isinherited from an earlier seat's comment; the prior readings on objectui#8816 were all
re-derived from scratch, and where they agree that is two independent paths landing on the
same number, not a citation.
Ceiling, base, and each branch's delta — measured
i18n-localesgzipbbe285ee7—main3949cf3a3—main+ objectui#8901ea5eab7b3—main+ objectui#8888ba20b0bc0—main+ BOTH⭐ The fourth row is the one this dispatch existed to produce. The two deltas sum to 1,586
against a measured 1,594, so two independent claimants on this chunk are additive to within
8 bytes — no gzip dictionary relief, and no arithmetic on the two single readings would have
been trustworthy without that third build.
mainthen advanced five commits mid-flight (bbe285ee7→b97129e96). Both branches weremerged onto it again and everything re-measured:
packages/i18nis untouched in that rangeand
i18n-localescame back byte-identical on all three trees (455,271 / 455,519 /456,196). The other three budgeted chunks moved by ≤9 bytes, which is
main's own movement,not this change's.
The new pair, and why the number is what it is
Headroom 8,804 bytes = 0.10x
REGRESSION_THIS_GATE_MUST_CATCH_BYTES— this key's ownconvention, and marginally tighter in ratio than the 8,924 (0.10x) the retired pair
carried.
⛔ It is deliberately not the 804 bytes the overage needed. A minimal raise reproduces the
exact defect objectui#8816 was filed about: the gate weighs the merge ref, so a line with
~0 headroom reddens whichever in-flight PR happens to be weighed second and prints a message
blaming that diff for arithmetic that is not its own. objectui#8554 is the same mechanism
one step earlier (
frameworkat 70,999 against 71,000, printing a green sensitivity row).177afeba1(2026-09-03) at 446,076 and
mainmeasured 454,602 on 2026-09-10 — 8,526 bytes in seven days,the last 398 of them claimed by two independent changes inside one shift. At that arrival rate
this buys about a week. The structural answer the file already names — taking the catalogues
out of the eager closure — is what retires this line instead of moving it.
Why route (2) was refused, measured rather than assumed
Objects,Views,Dashboards,App,Sample data), so reuse looks available. It is not: onlyObjectsandDashboardshave any pre-existingenequivalent, and each already exists three timesunder three per-surface namespaces. Per-surface keys are this pack's convention; cross-surface
reuse is the deviation. Best measured saving 128 bytes against a 519-byte overage.
⭐ The probe is control-lit: searching the
enpack for the five literal values returnedfour hits for two of them and one (the new key itself) for the other three — so the three
zeros are real zeros, not a broken search.
strings exist precisely because saying what the neighbouring
console.importMappings*strings say is the runtime lie that card was filed to remove.
green tick and narrowing a payload to get one are the same error facing opposite ways.
⇒ ⭐ The payload reduction that would make the next raise unnecessary is real, and it is not
this PR's to take.
pnpm check:i18n-dead-keysreports 364 candidates across 47 namespaces,127 of them CONFIRMED with no textual footprint anywhere in the repo, in ten packs each —
far more than the 1,196 bytes needed. That gate is report-only by design,
@object-ui/i18npublishes these packs (so removing a key is a published-surface decision, not a byte
saving), and objectui#8816's own route-C note says a sweep like that needs its own card and its
own reverse verification. ⛔ Not ridden in here.
Proof the gate can still go RED at 465,000
A ceiling that no longer fires is not a fix, so it was made to fire. On the combined tree
ba20b0bc0, with the new 465,000 ceiling in place, 300 deterministic high-entropy fillerkeys (30 per pack, ten packs) were injected, proven on disk before anything was read —
per-file marker count 0 → 30 and a changed blob hash for all ten — then the console was
rebuilt:
Direction was predicted before the run and observed as predicted. The lit controls held:
vendor-objectstackandui-componentsbyte-identical across the mutation,frameworkmoved2 bytes, and the aggregate stayed green (7.9 KB headroom) — so the line that objected is
the per-chunk one, at its new number.
Restored by
git checkout HEAD -- PATH(⛔ never baregit checkout --) and verified bystate, not by an exit code: all ten blobs back to their
HEADhashes, marker count 0,git diff HEADempty for the locale packs, and the mutation script carriedtrap … EXIT INT TERMwith absolute paths throughout. Thedist/the mutated build wrote wasalso deleted, so no later run in that container can read the mutated bytes back.
The green side, and the both-on-main answer
i18n-localescheck:eager-closureexit 0, all four chunks ✅check:eager-closureexit 0, all four chunks ✅maincheck:eager-closureexit 0, all four chunks ✅⇒ Yes, the fix holds with both on
main, and that is a build, not an extrapolation. Theaggregate never objected in any of them: 23,507 bytes of headroom (0.26x) with both claimants
on it, so
MAX_EAGER_CLOSURE_GZIP_BYTESis ⛔ untouched.Why both branches carry the identical change
Not duplication —
evaluateCeilingFreshnessrequires it. Once the first of the two lands, thebase branch has moved
PER_CHUNK_GZIP_CEILINGS, and the freshness half compares threereadings: this checkout, this checkout's base, and the base-branch tip. A checkout that does
not carry the new value scores as
supersededand exits 2 — a verdict about the gauge,correctly. Because both branches carry byte-identical constants, the second one merges the
first's landing as a no-op for that file and the freshness half reports "this checkout already
carries the new value". So whichever lands first, the other stays green.
The measurement is payload-neutral, and that was checked rather than argued
The ceiling edit plus the ten locale packs' comment update produced a byte-identical
closure on every tree it was applied to —
i18n-locales455,271 → 455,271, 455,519 → 455,519,456,196 → 456,196, aggregate unchanged to the byte in all three. So
BASELINE's standingargument (
scripts/check-*.mjsis not a console build input) is observed here rather thanassumed, and it is now also observed for a source edit: TypeScript comments do not survive
into the chunk.
Also in this change: ten stale citations of the retired headroom
All ten locale packs carried a comment naming the old ceiling's headroom ("8,924 B … about
sixty short keys' worth"). Moving the ceiling would have left ten copies of a false number, and
the "about sixty short keys" unit is itself falsified by measurement — the two real claimants
cost 9.2 and 22.3 gzipped bytes per key-times-locale, a 2.4x spread, so ⛔ no quota
written in keys is derivable. The comment now names no figure at all (the form that file's
own anti-drift rule explicitly allows) and points at
pnpm check:eager-closurefor the figurein force. Comment-only: zero payload, proven above.
⛔ What was NOT done
No check disabled, skipped or removed. No package excluded from
Bundle Analysis. No testdeleted. No empty commit. Neither PR closed and reopened.
REGRESSION_THIS_GATE_MUST_CATCH_BYTESuntouched, the other three per-chunk ceilings untouched,
MAX_EAGER_CLOSURE_GZIP_BYTESuntouched. No assignee written, no review label hung or cleared, no
Clause-②declarationedited. No rebase and no force-push:
mainwas brought in with real merge commits, twice.The
UNGATED_EXAMPLESledger row this PR warned about — re-derived, twiceThe warning above ("A line-numbered ledger row this PR had to re-derive") called it exactly.
mainbrought PR #8928, which deleted the neighbouringpackages/data-objectstack/src/cache/MetadataCache.ts:56 MetadataCacherow and carriedcreateObjectStackAdapterat:6323on its side — the single conflict in this branch's mergewith
main.Resolved by dropping the deleted row and re-deriving the surviving key from the checker's own
extractor (
ledgerKey(block)overexampleCensus()) on the merge result, ⛔ never byarithmetic on either side's number. Same answer on both merges with
main:packages/data-objectstack/src/index.ts:6562 createObjectStackAdapter. Ledger row count90 → 89 (
mainremoved one).node scripts/check-doc-example-types.mjsexits 0 on thecurrent merge result: 124 blocks, 89 declared, "every covered
@examplecompiles, or failsexactly as its ledger row declares".
adjudicated at merge time, and nothing re-checks it there.
🤖 Generated with Claude Code
https://claude.ai/code/session_01611D6ZaRaMmwTNQmSbk8MH
Generated by Claude Code
Generated by Claude Code