Skip to content

fix(data-objectstack): a refused view read is not an object with no saved views - #8901

Merged
os-zhuang merged 5 commits into
mainfrom
claude/issue-8151-listviews-refused-read-discrimination
Sep 10, 2026
Merged

fix(data-objectstack): a refused view read is not an object with no saved views#8901
os-zhuang merged 5 commits into
mainfrom
claude/issue-8151-listviews-refused-read-discrimination

Conversation

@claude

@claude claude Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Fixes #8151

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. The degrade stays; what is added is the discrimination, read from err itself 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 base b686ebf7d it 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:

$ git grep -n "console.warn('\[OBJECTSTACKDataSource\]" origin/main -- packages/data-objectstack/src/index.ts
origin/main:packages/data-objectstack/src/index.ts:5058:      console.warn('[OBJECTSTACKDataSource] listViews failed:', err);
origin/main:packages/data-objectstack/src/index.ts:5122:      console.warn('[OBJECTSTACKDataSource] listImportMappings failed:', err);

(the card read listViews at :4893 and listImportMappings at :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 view failures deserve to stay quiet

The card and the dispatch both said ⛔ this is not classifyImportMappingsFailure under a new name. It is not. classifyViewsFailure is a separate branch table, and the difference is exactly one arm.

⛔ The arm that does NOT carry over — 400 INVALID_REQUEST. On mapping that is the supported "this deployment carries no such kind" and it is quiet. On view it cannot mean that, measured three ways on the framework tree:

  1. view is 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 whenever unrecognisedMetaTypeRefusal(urlType) is null, and that predicate answers null for any spelling in the contract. packages/spec/src/meta-spelling/meta-url-data.generated.ts carries both "views": "view" and the canonical singular view.
  2. There is no "before" for view to be older than. mapping's quiet arm exists because mapping was PROMOTED into the declared set (framework#2611), so pre-promotion servers are a real shipped population. view is the kind the metadata surface is built around — the compound-arity door /meta/OBJECT/views/VIEWNAME, the ADR-0017 ViewItem discriminant this very method filters on.
  3. A deployment that cannot serve view cannot render the caller. listViews is reached only from ObjectView, whose objectDef came from MetadataProvider — which reads the same door and lists view in its EAGER_TYPES at 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 view is therefore strictly smaller than on mapping: the DOORLESS arm onlyROUTE_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 failed MetadataProvider's eager app / object / view reads. 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 (code first, 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. listViews is uncached (pinned by viewCacheInvalidation.pin.test.ts) and runs on every ObjectView mount, 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, listViews is fed by client.meta.getItems('view') (SDK wrapper: code + httpStatus) and, under previewDrafts, by MetadataClient.withPreviewDrafts(true).list('view') (this package's own parseError: code + status, no httpStatus at 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's operation and kind gain their second members. The single-member shape did its job: metadataReadWarningToast's title was one hard-coded t('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 operation with a never check (title, remedy), matching the discipline the per-reason switch already documented. operation is the discriminant rather than kind because 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 published interface into a type alias — reviewable on its own terms, deliberately not smuggled in here.

Also new: MetadataReadFailureKind, with ImportMappingsFailureKind now 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 the IMPORT_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: listImportMappings is unchanged

  • Its own suite (listImportMappings.test.ts, untouched) is green: 26 tests.
  • Its copy is asserted whole, not by substring, in a new pin — the three console.importMappings* sentences objectui#7741 shipped.
  • Under the ablation below it stayed green while listViews went red, which is what makes it a control rather than a co-victim.
  • A new pin asserts the two classifiers agree on 13 inputs and disagree on exactly one ({code:'INVALID_REQUEST', httpStatus:400}not-served vs unreadable), so a later edit cannot quietly fork a second dialect.

Tests

Run from the repo root (pnpm exec vitest run PATHS — never pnpm --filter … exec vitest, never paths after --).

packages/data-objectstack/src/listViews.readWarning.test.ts   (new)
packages/data-objectstack/src/listViews.test.ts
packages/data-objectstack/src/listImportMappings.test.ts
packages/app-shell/src/providers/metadataReadWarningToast.test.ts        (extended)
packages/app-shell/src/providers/AdapterProvider.readWarningSink.test.tsx
   → Test Files 5 passed (5) · Tests 62 passed (62)

packages/i18n/src/__tests__/all-locales-key-parity.test.ts
scripts/__tests__/one-authority-per-exported-name-6273.test.ts
   → Test Files 2 passed (2) · Tests 43 passed (43)

Build + type-check, dependency closure included: turbo run buildTasks: 29 successful, 29 total; turbo run type-checkTasks: 32 successful, 32 total. Measured rather than assumed: data-objectstack's type-check does compile the new test file (tsc --noEmit --listFiles | grep -c listViews.readWarning.test.ts1), and check-type-check-coverage reports 43/43 packages compile their tests.

Ablation — the discrimination can fail, and the control cannot

From the committed tree, the discrimination alone was removed (the classifyViewsFailure call and the emit block); console.warn and return [] were left byte-identical, so this ablates the FIX, not the method.

The subject resolves through vitest's resolve.alias to packages/data-objectstack/src (vitest.config.mts:513), not to dist/ — 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:

== HEAD blob for packages/data-objectstack/src/index.ts: a72f1dcbc0a4206f8e40930688959258fe2ed2d8
== BEFORE: classify=1 emit=1
== AFTER MUTATION: classify=0 emit=0 doc-mention-still-there=1
== blob: 8dc01687e451be5f3411817fd7b9b9478db62842 (HEAD was a72f1dcbc0a4206f8e40930688959258fe2ed2d8)
== the degrade survives (control): 1

Direction predicted before the run, and observed: RED, by case name — 7 failed, 25 passed:

LOUD — the server answered and declined this caller > announces a lapsed session, and STILL answers []
LOUD — the server answered and declined this caller > announces a missing grant
LOUD — the server answered and declined this caller > announces a code-less refusal on the status alone
LOUD — the read could not be completed > announces a 5xx
LOUD — the read could not be completed > announces a dropped connection, which carries no code and no status
the OTHER door — `?preview=draft` decorates errors differently > classifies a refused draft read the same way (ADR-0037)
the divergence — `view` does not inherit `mapping`’s quiet arm > announces a coded 400 that the sibling classifier keeps quiet

listImportMappings.test.ts — the lit control — stayed 1 file passed throughout.

⚠️ Worth stating because it is the trap this whole card is about: the three QUIET cases pass under the mutation too. An assertion that a refused read produces [] 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 bare git checkout --), and the restore verified by state, not by an exit code: git diff HEAD empty, working blob back to a72f1dcb…, suite re-run green at 32/32. The mutation script carried trap … EXIT INT TERM with absolute paths throughout.

Line-address citation census — the strong form (objectui#8875)

Not just at push time: scripts/cross-file-line-citation-census.mjs was run on origin/main itself, and again on the tree produced by a real git merge --no-ff of this branch into main (a throwaway detached worktree, since git merge-tree's archive is not a repository the census can walk).

origin/main  b686ebf7d  →  539 false citation(s)
merge result 793cc271c  →  539 false citation(s)

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 between drifted and non-substantive as 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:

  1. Universe: no type-aware linting is configured — eslint.config.js has zero occurrences of project: and zero of projectService. So a file's verdict is a function of its own bytes plus the shared config.
  2. Count: eslint --format json over exactly the 14 lintable paths this PR touches → array length 14, errorCount 0, warningCount 126.
  3. Invariance: with no type-aware rules, this diff moves neither input for any file it does not edit, so the narrowing excludes nothing. All 126 warnings are pre-existing @typescript-eslint/no-explicit-any in this ~6.5k-line adapter, and every warning line falls outside every range this PR added — the new code introduces no any.

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 own console.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. The console.importMappings* values are untouched, which is why check:i18n-drift reports 0 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's elementDataSourceViewNotFoundMessage (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" — which listViews never 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-list without AdapterProvider sees 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 / kind pair is an emitter invariant rather than a type-level one; a discriminated union would enforce it but converts a published interface to a type alias. Raised here for the contract review rather than filed, since nothing else will touch this type.

⚠️ A line-numbered ledger row this PR had to re-derive — RE-DERIVE IT AGAIN BEFORE MERGE

scripts/check-doc-example-types.mjs’s UNGATED_EXAMPLES ledger is keyed by path:line symbol. This PR 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”. One key updated, located by FILE + SYMBOL and re-derived from the checker’s own extractor (ledgerKey(block) over exampleCensus()), never from a line number anyone quoted. Ledger row count 90 → 90; zero executable change.

⚠️ This is a snapshot of a moving quantity, and its correctness is adjudicated at the moment of MERGE — where nothing re-checks it. Any commit that lands on main ahead of this one and moves lines in packages/data-objectstack/src/index.ts invalidates the key again, silently, until the shard runs. This PR is docked on Clause-②: yes and 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.

⚠️ The tree-wide census in the section above has NO discriminating power over this class: cross-file-line-citation-census.mjs reads prose citations, and this is a stored literal used as a key. The distinguishing test is stored vs computed — a path:line assembled 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.ts and the ten locale packs have none).


⚠️ Bundle Analysis: the shared i18n-locales budget, 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.mjs itself, under "Why i18n-locales moved UP",
because that is where the budget lives and where the next author will look.

The instrument

apps/console/dist/eager-closure.json, written by emitEagerClosureReport in
apps/console/vite.config.ts on every console build, read by
node scripts/check-eager-closure-budget.mjs (pnpm check:eager-closure). Every figure below
is 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 is
inherited 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

tree i18n-locales gzip vs the old 455,000 ceiling
bbe285ee7main 454,602 headroom 398
3949cf3a3main + objectui#8901 455,271 OVER by 271 (+669)
ea5eab7b3main + objectui#8888 455,519 OVER by 519 (+917)
ba20b0bc0main + BOTH 456,196 OVER by 1,196 (+1,594)

⭐ 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.

main then advanced five commits mid-flight (bbe285ee7b97129e96). Both branches were
merged onto it again and everything re-measured: packages/i18n is untouched in that range
and i18n-locales came 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

PER_CHUNK_GZIP_CEILINGS['i18n-locales']  455,000 -> 465,000
PER_CHUNK_BASELINE['i18n-locales']       446,076 -> 456,196   (measured on ba20b0bc0)

Headroom 8,804 bytes = 0.10x REGRESSION_THIS_GATE_MUST_CATCH_BYTES — this key's own
convention, 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 (framework at 70,999 against 71,000, printing a green sensitivity row).

⚠️ And it is not a settlement. Measured runway: the retired pair landed on 177afeba1
(2026-09-03) at 446,076 and main measured 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

  • objectui#8888's ten keys include five generic console nouns (Objects, Views,
    Dashboards, App, Sample data), so reuse looks available. It is not: only Objects and
    Dashboards have any pre-existing en equivalent, and each already exists three times
    under 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 en pack for the five literal values returned
    four 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.
  • objectui#8901's three keys have no reuse candidate, and that is structural: those
    strings exist precisely because saying what the neighbouring console.importMappings*
    strings say is the runtime lie that card was filed to remove.
  • Shortening the copy is the objectui#6759 lever and is ⛔ refused. Widening a ceiling to get a
    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-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 needed. That gate is report-only by design, @object-ui/i18n
publishes 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 filler
keys (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:

i18n-locales   456,196 -> 471,668   (+15,472)
❌ i18n-locales   460.6 KB / 454.1 KB ceiling (OVER by 6.5 KB)   check:eager-closure exit 1

Direction was predicted before the run and observed as predicted. The lit controls held:
vendor-objectstack and ui-components byte-identical across the mutation, framework moved
2 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 bare git checkout --) and verified by
state
, not by an exit code: all ten blobs back to their HEAD hashes, marker count 0,
git diff HEAD empty for the locale packs, and the mutation script carried
trap … EXIT INT TERM with absolute paths throughout. The dist/ the mutated build wrote was
also deleted, so no later run in that container can read the mutated bytes back.

The green side, and the both-on-main answer

tree verdict i18n-locales
objectui#8901's merge result check:eager-closure exit 0, all four chunks ✅ 455,271 / 465,000 (headroom 9,729)
objectui#8888's merge result check:eager-closure exit 0, all four chunks ✅ 455,519 / 465,000 (headroom 9,481)
both on main check:eager-closure exit 0, all four chunks ✅ 456,196 / 465,000 (headroom 8,804)

Yes, the fix holds with both on main, and that is a build, not an extrapolation. The
aggregate never objected in any of them: 23,507 bytes of headroom (0.26x) with both claimants
on it, so MAX_EAGER_CLOSURE_GZIP_BYTES is ⛔ untouched.

Why both branches carry the identical change

Not duplication — evaluateCeilingFreshness requires it. Once the first of the two lands, the
base branch has moved PER_CHUNK_GZIP_CEILINGS, and the freshness half compares three
readings: this checkout, this checkout's base, and the base-branch tip. A checkout that does
not carry the new value scores as superseded and 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-locales 455,271 → 455,271, 455,519 → 455,519,
456,196 → 456,196, aggregate unchanged to the byte in all three. So BASELINE's standing
argument (scripts/check-*.mjs is not a console build input) is observed here rather than
assumed, 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-closure for the figure
in force. Comment-only: zero payload, proven above.

⛔ What was NOT done

No check disabled, skipped or removed. No package excluded from Bundle Analysis. No test
deleted. No empty commit. Neither PR closed and reopened. REGRESSION_THIS_GATE_MUST_CATCH_BYTES
untouched, the other three per-chunk ceilings untouched, MAX_EAGER_CLOSURE_GZIP_BYTES
untouched. No assignee written, no review label hung or cleared, no Clause-② declaration
edited. No rebase and no force-push: main was brought in with real merge commits, twice.

The UNGATED_EXAMPLES ledger row this PR warned about — re-derived, twice

The warning above ("A line-numbered ledger row this PR had to re-derive") called it exactly.
main brought PR #8928, which deleted the neighbouring
packages/data-objectstack/src/cache/MetadataCache.ts:56 MetadataCache row and carried
createObjectStackAdapter at :6323 on its side — the single conflict in this branch's merge
with main.

Resolved by dropping the deleted row and re-deriving the surviving key from the checker's own
extractor (ledgerKey(block) over exampleCensus()) on the merge result, ⛔ never by
arithmetic on either side's number. Same answer on both merges with main:
packages/data-objectstack/src/index.ts:6562 createObjectStackAdapter. Ledger row count
90 → 89 (main removed one). node scripts/check-doc-example-types.mjs exits 0 on the
current merge result: 124 blocks, 89 declared, "every covered @example compiles, or fails
exactly as its ledger row declares".

⚠️ The warning itself still stands for whoever moves next: it is a stored literal,
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

…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
@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

❌ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 50 chunks) 3488.1 KB 3512.7 KB
Main entry chunk (gzip) 144.2 KB 350 KB
Entry file index-lTQAlxPY.js
Status FAIL

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:

Eager-closure half Verdict
Aggregate closure ceiling ✅ pass
Per-chunk ceilings ❌ over its ceiling
Ceiling sensitivity (headroom) ✅ pass
Ceiling freshness (checkout vs. base branch) ✅ pass

📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.69KB 6.21KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 500.23KB 114.67KB
core (index.js) 7.48KB 2.96KB
create-plugin (index.js) 26.68KB 8.94KB
data-objectstack (index.js) 203.50KB 56.34KB
fields (index.js) 246.94KB 62.28KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 6.57KB 2.76KB
i18n (index.js) 3.65KB 1.47KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 26.89KB 9.04KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.84KB 10.94KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 4.39KB 1.66KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.39KB 3.10KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 14.81KB 3.63KB
plugin-calendar (index.js) 49.03KB 13.93KB
plugin-charts (index.js) 71.39KB 19.92KB
plugin-chatbot (index.js) 194.54KB 46.34KB
plugin-dashboard (index.js) 131.71KB 34.50KB
plugin-designer (index.js) 215.51KB 44.29KB
plugin-detail (index.js) 252.45KB 65.33KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 136.26KB 34.13KB
plugin-gantt (index.js) 166.96KB 40.93KB
plugin-grid (index.js) 210.86KB 57.28KB
plugin-kanban (index.js) 57.53KB 16.46KB
plugin-list (index.js) 112.54KB 27.65KB
plugin-map (index.js) 20.49KB 6.83KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.42KB 11.92KB
plugin-timeline (index.js) 30.10KB 8.74KB
plugin-tree (index.js) 9.55KB 3.32KB
plugin-view (index.js) 84.42KB 20.80KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 81.07KB 26.86KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 2.32KB 1.24KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.55KB 2.45KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 20.57KB 5.88KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 13.64KB 4.59KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 4.73KB 2.28KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 14.27KB 5.47KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

Copy link
Copy Markdown
Contributor

CI 现状:两条红,处置不同

Bundle Analysis — ⛔ 不是本 PR 的缺陷,本 PR 就此停手

超顶的是 i18n-locales455,271 / 455,000 ⇒ 超 271 字节。四行 per-chunk 判决(门禁 exit 1,真读数):

✅ vendor-objectstack  1207.3 / 1224.6 KB (headroom 17.3 KB)
❌ i18n-locales         444.6 /  444.3 KB (OVER)
✅ ui-components        385.2 /  389.6 KB (headroom  4.5 KB)
✅ framework             71.7 /   97.7 KB (headroom 26.0 KB)

main 的 locale 包本来就只剩 398 字节余量(两条独立路径逐字互证),本 PR 的 3 个键 × 10 个语言包实测花 669 字节 ⇒ 超 271。这是 objectui#8816 正在裁的那份耗尽的配给,本 PR 是它的第二个索赔人(第一个是 PR #8888,超 519)。完整数字已报到 #8816

⛔ 没有可移植的修复存在,因为唯一的「修复」都是本 PR 无权做的事:抬天花板、动 PER_CHUNK_BASELINE、拆 chunk,或者删掉本 PR 的交付面。那 3 个键就是交付面 —— 这条 toast 的全部职责就是说一句和 import-mapping 那条不同的话。⛔ 也没有为了绿而缩小载荷:门禁自己写着 do not widen it just to get a green check,⭐ 它有一个镜像,同样成立。

本条红需要的是 #8816 的裁决,不是本 PR 的补丁。 本 PR 本来就是 draft + needs:contract-review,即便全绿也进不了队 ⇒ 这条红不产生额外延迟。⛔ 未重跑(重跑不会改变一个确定性的字节计数)。

Test (shard 3/4)⚠️ 是本 PR 要修的,修复进行中

FAIL scripts/__tests__/check-doc-example-types.test.ts > the real ledger
AssertionError: packages/data-objectstack/src/index.ts:6323 createObjectStackAdapter

UNGATED_EXAMPLES 账本的键里含行号;本 PR 往 data-objectstack/src/index.ts 插了 +262/−23 行,把那个 @example 块顶下去了。一行修复,已在做,要求账本行数 90 → 90、零可执行改动。⚠️ 这个修复是对一个会动的量拍的快照,其正确性在合并那一刻才被裁定 ⇒ 落地前须重推一次。

⚠️ 归责说明:这条是派发时未告知造成的(同一账本本班已打红两份 PR),不是实现的疏漏。


Generated by Claude Code

…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
@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

❌ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 50 chunks) 3488.1 KB 3512.7 KB
Main entry chunk (gzip) 144.2 KB 350 KB
Entry file index-zOfhC6r6.js
Status FAIL

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:

Eager-closure half Verdict
Aggregate closure ceiling ✅ pass
Per-chunk ceilings ❌ over its ceiling
Ceiling sensitivity (headroom) ✅ pass
Ceiling freshness (checkout vs. base branch) ✅ pass

📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.69KB 6.21KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 500.23KB 114.67KB
core (index.js) 7.48KB 2.96KB
create-plugin (index.js) 26.68KB 8.94KB
data-objectstack (index.js) 203.50KB 56.34KB
fields (index.js) 246.97KB 62.30KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 6.57KB 2.76KB
i18n (index.js) 3.65KB 1.47KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 26.89KB 9.04KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.84KB 10.94KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 4.39KB 1.66KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.39KB 3.10KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 14.81KB 3.63KB
plugin-calendar (index.js) 49.03KB 13.93KB
plugin-charts (index.js) 71.39KB 19.92KB
plugin-chatbot (index.js) 194.54KB 46.34KB
plugin-dashboard (index.js) 131.71KB 34.50KB
plugin-designer (index.js) 215.51KB 44.29KB
plugin-detail (index.js) 252.45KB 65.33KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 136.26KB 34.13KB
plugin-gantt (index.js) 166.96KB 40.93KB
plugin-grid (index.js) 210.86KB 57.28KB
plugin-kanban (index.js) 57.53KB 16.46KB
plugin-list (index.js) 112.54KB 27.65KB
plugin-map (index.js) 20.49KB 6.83KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.42KB 11.92KB
plugin-timeline (index.js) 30.10KB 8.74KB
plugin-tree (index.js) 9.55KB 3.32KB
plugin-view (index.js) 84.42KB 20.80KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 81.07KB 26.86KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 2.32KB 1.24KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.55KB 2.45KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 20.57KB 5.88KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 13.64KB 4.59KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 4.73KB 2.28KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 14.27KB 5.47KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

Copy link
Copy Markdown
Collaborator

Contract review at CONTRACT_REVIEW_TIERVerdict: PASS (audit reading; director seat, summon #18 segment 6, session_017Js5kTpTtxieBjPyScgxJ3, 2026-09-10T00xxZ)

PR objectui#8901 · head 7f0438707875e14c6c30daf587a65261d47806b3 (re-read at posting 00:05:35Z; unchanged since 22:07Z) · card objectui#8151.

  • Reviewed-by: isolated claude-fable-5-1 subagent, transcript-verified (71 harness model stamps, all claude-fable-5-1, zero residue; positive control 64 assistant / 49 user role tokens), adopted verbatim below.
  • Implemented-by: session_01611D6ZaRaMmwTNQmSbk8MH · branch claude/issue-8151-listviews-refused-read-discrimination (newest Claim: 5608579613, PM-dispatched under os-zhuang). Distinct sessions ⇒ not a self-review.
  • Reading for the seat: PASS on contract; ⛔ not landable yet — Bundle Analysis is red on this head (hold behind objectui#8816). Carriers stay until the head is all-green; no re-review needed if the code does not move. ⛔ This seat cleared no carrier and touched no PR state at posting.

I have everything needed; no further fetches are required. Deliverable follows.

Contract review — objectui#8901 (card objectui#8151)

Verdict

PASS (contract tier) — the Clause-② delta is correct, complete, pinned and matches the claim. Not landable yet: landing pre-check ③ fails on Bundle Analysis (see CI section, F5), and the line-keyed ledger row must be re-derived at merge (F6). Neither is a REWORK item.

Head reviewed

7f0438707875e14c6c30daf587a65261d47806b3 — head has not moved (matches the 7f04387078 prefix). Two commits on claude/issue-8151-listviews-refused-read-discrimination; merge-base b686ebf7d; origin/main now 348725a7c (2 commits ahead, neither touches packages/data-objectstack/src/index.ts or scripts/check-doc-example-types.mjs). 16 files, +901/−39; PR is draft, targets main, first line Fixes #8151 (correct — card fully delivered; no other closing keyword in the body).

Clause-② reading · claim match · --pair

My reading: yes. Published-face delta (all in packages/data-objectstack/src/index.ts, the package entry; private unset, files: [dist,…]):

  • New export classifyViewsFailure(err):2284-2332 (PR head).
  • New exported type MetadataReadFailureKind:2097; ImportMappingsFailureKind becomes an alias of it (:2110) — identical members, assignability unchanged both ways.
  • MetadataReadWarningEvent.operation widened 'listImportMappings' | 'listViews', .kind widened 'mapping' | 'view' (:2380-2383); reason type unchanged in substance.
  • ObjectStackAdapter.listViews (:5207-5299): signature unchanged (objectName, options?) → Promise<any[]>, still never throws; catch (:5281-5299) keeps console.warn + return [] verbatim and adds classifyViewsFailureemitMetadataReadWarning on any non-not-served arm. DataSource.listViews in packages/types/src/data.ts:627 untouched.
  • @object-ui/i18n: 3 new keys (console.savedViewsUnavailable/Refused/Unreadable) in all 10 locale packs (verified: ar de en es fr ja ko pt ru zh; index.ts is the barrel). @object-ui/app-shell: no new exported names (title/remedy are module-private; emitMetadataReadWarning, MetadataReadWarningSink unchanged).

Claim match: card carrier (comment 5608579613, appended under PM pre-authorisation) reads Clause-②: yes citing exactly these two exports plus the union widening; PR body says docked on Clause-②: yes; needs:contract-review present on both PR and card. Matches. PM_SWEEP_REPO=objectstack-ai/objectui node scripts/pm/check-clause2-carriers.mjs --pair 8901exit 0 (my run; dev reports 4→0 after adding the card carrier, additive POSTs).

Governed surface

node scripts/check-governed-queue-guard.mjs --test <16 paths>NOT GOVERNED, exit 0; CI Governed Surface Queue Guard success on head. No content/docs/releases/ change.

CI on head (7f04387)

33 check-runs: 29 success, 3 skipped (coverage variants, dependabot), 1 failure: Bundle Analysis (performance-budget.yml, per-chunk ceiling half). All 9 required contexts (Lint, Type Check, Build & E2E, 4 test shards, Build Docs, Changeset Declaration) green — hence unstable, not blocked. Test (shard 3/4) was red on the first commit (stale UNGATED_EXAMPLES key) and is green on head after commit 2.

Red-check causation: yes, this diff causes it, mechanically — but on an exhausted shared budget, not a defect of the PR. Job log is proxy-blocked (CONNECT 403); reasoning from the check + diff: the only i18n-locales inputs this PR moves are the 30 added locale strings (6,027 source bytes; seat-measured ≈669 B in-chunk gzip). scripts/check-eager-closure-budget.mjs:657 pins i18n-locales at 455,000 B; decision card #8816 (open, pm:queue, unassigned) records main at 0.5 KB headroom after #8815. 398 B headroom − 669 B = 271 B over, matching the seat's 455,271 / 455,000. There is no PR-local fix that keeps the deliverable: the three sentences are the deliverable (the "not because this object has no saved views" clause is the discrimination reaching the user), and moving the ceiling/PER_CHUNK_BASELINE is #8816's ruling. Contract-review pre-check ③ (all checks green, not the required subset) therefore fails until #8816 is decided.

Findings

F1 — Correctness vs card and the #7741 precedent: correct, same shape, deliberately not the same fix. PR #8152 shape = classifier + onMetadataReadWarning channel + metadataReadWarningToast + 3 keys × 10 locales + minor changeset; #8901 repeats it one method over with the one divergence the card demanded: classifyViewsFailure drops the 400 INVALID_REQUEST kind-absent arm (:2305-2309), reasoned structurally (view/views in the static spelling contract ⇒ refuseUnknownMetaListType never writes that refusal; MetadataProvider.tsx:51 EAGER_TYPES = ['app','view'] verified). Quiet set = ROUTE_NOT_FOUND/NOT_IMPLEMENTED + code-less 404/501 only. Code-first, status-only-in-residual order preserved; status ladder httpStatus → status → statusCode makes the ?preview=draft door (MetadataClient.parseError sets status) agree with the SDK door.

F2 — Callers handle the new behaviour: yes, trivially. Return is unchanged, so ObjectView.tsx:1575-1600 and useElementDataSource.ts:112 see what they saw. The only subscriber to onMetadataReadWarning (AdapterProvider.tsx:106) feeds the toast, whose title/remedy switches on operation are now exhaustive with never checks (metadataReadWarningToast.ts:206-250); no other exhaustive switch on operation/kind exists in packages/** or apps/**.

F3 — Tests pin the contract, with controls: adequate. listViews.readWarning.test.ts (new, real SDK through fetch): 5 LOUD cases assert code and status (401 UNAUTHENTICATED, 403 PERMISSION_DENIED, code-less 403, 500, dropped connection) while re-asserting [] on every arm; served-zero control (silent); preview-draft door pin; divergence pin (13 agree / exactly 1 disagrees); lit control on listImportMappings (quiet 400, loud 401). Toast test pins the views sentence, forbids import-mapping wording on a views event, and asserts the #7741 en copy whole. Dev's ablation (7 red / 25 green, control green) and its stated limit (quiet cases cannot fail) are honest. All four CI shards green on head.

F4 — Scope: clean. Every file is the card's; the one-line scripts/check-doc-example-types.mjs key re-derivation (:6323 → :6562) is forced collateral of the line insert, named in the PR body, ledger count unchanged.

F5 — Landing blocker (not the PR's defect): Bundle Analysis. See CI section. Hold behind #8816; do not shrink or reword the keys to buy 271 B — that would be the swallow re-entering through copy.

F6 — Merge-time re-derivation of the line-keyed ledger row. Verified on head: packages/data-objectstack/src/index.ts:6562 is the @example line of createObjectStackAdapter (function at :6573, same −11 offset as origin/main's :6323/:6334). Still valid against current origin/main (no change to index.ts since base). The landing seat must re-check if anything touching index.ts lands first; the citation census has no power over this class (stored vs computed).

F7 — Changeset (minor, 3 packages): present and correctly leveled, one wording gap. minor is right (new exports; objectui never declares major in the fixed group). The body says the widening became a compile error for the internal consumer but does not tell external consumers that an exhaustive switch over operation/kind will now fail to compile until they add the 'listViews'/'view' arms. Non-blocking; a one-sentence addition if the PR is touched again for F6.

F8 — Design question handed to this review (operation/kind pairing as emitter invariant). Ruling: keep the published interface; do not convert to a discriminated-union type alias in this PR or as a successor. Two independent unions with exhaustive consumer switches on operation are sufficient at two emitters; revisit only if a third emitter arrives. No card needed.

F9 — Toast volume, non-blocking. listViews is uncached and is called from ObjectView and from useElementDataSource (ElementDataSourceGate, record-picker), so one page with a lapsed token can toast more than once per mount. Precedent had one call site. Any de-duplication belongs to the sink (AdapterProvider), not the emitter; note for whoever picks up #8900 (open, pm:queue), which the dev correctly filed rather than editing elementDataSourceViewNotFoundMessage.

F10 — Docs. No page under content/**, docs/**, apps/site/** or any package README documents the onWriteWarning/onSaveAdvisory/onMetadataReadWarning family; the changeset is the only reader-facing record. Pre-existing since #7741, not this PR's; optional follow-up card.

Acceptance notes


Generated by Claude Code

Copy link
Copy Markdown
Collaborator

Clear-and-hold provenance — director seat, summon #18 segment 6 (session_017Js5kTpTtxieBjPyScgxJ3, 2026-09-10T00:17:12Z). Clearing needs:contract-review on both carriers (objectui#8901 + card objectui#8151) on the strength of the contract-review-tier PASS at #8901 (comment) (head 7f04387078, unchanged); ② --pair 8901 exit 0. ⛔ Not flipped ready / not enqueued: Bundle Analysis is red on head 7f04387078 (console performance budget; not caused by a defect in this diff) — landing-operations.md §C draft parking behind the budget decision objectui#8816. When that check is green on this head (or a rebase lands with it green), pre-check ③ holds and the seat flips ready → auto-merge without a re-review, provided the code does not move. Open carrier = genuinely awaiting review, which this PR no longer is; the hold is recorded here and on the card, not on the label.


Generated by Claude Code

Copy link
Copy Markdown
Collaborator

Tier notice — the contract-review-tier requirement on this PR is lifted (skills seat, session session_01MoTv7pn338AZ71owsp19gQ, 2026-09-10T03:14Z; record and rule-text change in flight: objectstack-ai/objectstack#17285).

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 skills/**), the spec seat's clause-② review, and the maintainer-summoned director; triage and every other seat run the default tier.

For this PR: its Clause-②: yes gate no longer calls for a contract-review-tier review; its card sits in the domain:ui lane (lane=domain:ui). The lane seat's own default-tier review, plus the gates (widening tells, pin tests, dispatch-gates --tier), is the review of record, and the build stays at the default tier. Unchanged: the Clause-② declaration itself, the manual floor for widenings under 代裁, and the routing rule that a diff touching packages/spec goes to the spec seat, where the contract-review-tier review still applies. This comment changes no label, assignee or claim.


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
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 50 chunks) 3489.2 KB 3512.7 KB
Main entry chunk (gzip) 144.2 KB 350 KB
Entry file index-DzAbnGQ9.js
Status PASS

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

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.69KB 6.21KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 500.20KB 114.67KB
core (index.js) 7.48KB 2.96KB
create-plugin (index.js) 28.04KB 9.46KB
data-objectstack (index.js) 205.46KB 56.78KB
fields (index.js) 246.97KB 62.30KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 6.57KB 2.76KB
i18n (index.js) 3.65KB 1.47KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 26.89KB 9.04KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.84KB 10.94KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 4.39KB 1.66KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.39KB 3.10KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 14.81KB 3.63KB
plugin-calendar (index.js) 49.03KB 13.93KB
plugin-charts (index.js) 71.39KB 19.92KB
plugin-chatbot (index.js) 194.54KB 46.34KB
plugin-dashboard (index.js) 132.41KB 34.84KB
plugin-designer (index.js) 215.68KB 44.27KB
plugin-detail (index.js) 253.19KB 65.62KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 136.79KB 34.19KB
plugin-gantt (index.js) 166.96KB 40.93KB
plugin-grid (index.js) 210.86KB 57.28KB
plugin-kanban (index.js) 57.58KB 16.47KB
plugin-list (index.js) 112.54KB 27.65KB
plugin-map (index.js) 20.49KB 6.83KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.42KB 11.92KB
plugin-timeline (index.js) 30.10KB 8.74KB
plugin-tree (index.js) 9.55KB 3.32KB
plugin-view (index.js) 84.42KB 20.80KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 83.34KB 27.61KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 2.32KB 1.24KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.66KB 2.50KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 2.71KB 1.35KB
sdui-parser (parse.js) 20.57KB 5.88KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 14.82KB 4.99KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 4.73KB 2.28KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 14.27KB 5.47KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-zhuang
os-zhuang marked this pull request as ready for review September 10, 2026 05:49
@os-zhuang
os-zhuang enabled auto-merge September 10, 2026 05:49
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 50 chunks) 3489.2 KB 3512.7 KB
Main entry chunk (gzip) 144.2 KB 350 KB
Entry file index-DzAbnGQ9.js
Status PASS

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

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.69KB 6.21KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 500.20KB 114.67KB
core (index.js) 7.48KB 2.96KB
create-plugin (index.js) 28.04KB 9.46KB
data-objectstack (index.js) 205.46KB 56.78KB
fields (index.js) 246.97KB 62.30KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 6.57KB 2.76KB
i18n (index.js) 3.65KB 1.47KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 26.89KB 9.04KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.84KB 10.94KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 4.39KB 1.66KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.39KB 3.10KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 14.81KB 3.63KB
plugin-calendar (index.js) 49.03KB 13.93KB
plugin-charts (index.js) 71.39KB 19.92KB
plugin-chatbot (index.js) 194.54KB 46.34KB
plugin-dashboard (index.js) 132.41KB 34.84KB
plugin-designer (index.js) 215.68KB 44.27KB
plugin-detail (index.js) 253.19KB 65.62KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 136.79KB 34.19KB
plugin-gantt (index.js) 166.96KB 40.93KB
plugin-grid (index.js) 210.86KB 57.28KB
plugin-kanban (index.js) 57.58KB 16.47KB
plugin-list (index.js) 112.54KB 27.65KB
plugin-map (index.js) 20.49KB 6.83KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.42KB 11.92KB
plugin-timeline (index.js) 30.10KB 8.74KB
plugin-tree (index.js) 9.55KB 3.32KB
plugin-view (index.js) 84.42KB 20.80KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 83.34KB 27.61KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 2.32KB 1.24KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.66KB 2.50KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 2.71KB 1.35KB
sdui-parser (parse.js) 20.57KB 5.88KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 14.82KB 4.99KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 4.73KB 2.28KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 14.27KB 5.47KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@claude

claude Bot commented Sep 10, 2026

Copy link
Copy Markdown
Contributor Author

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

@claude

claude Bot commented Sep 10, 2026

Copy link
Copy Markdown
Contributor Author

⭐ CI 已确认:Bundle Analysis两个 PR 的真实 merge ref 上都绿了

接上一条 os-dev-report(其 tests 字段把这一项申报为「NOT MEASURED,交 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

@os-zhuang
os-zhuang added this pull request to the merge queue Sep 10, 2026
Merged via the queue into main with commit fffa30d Sep 10, 2026
34 of 35 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-8151-listviews-refused-read-discrimination branch September 10, 2026 07:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

5 participants