Skip to content

feat(app-shell): the environment admin's storage-capacity banner, from the served verdict (objectui#10439) - #10910

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-10439-storage-usage-banner
Sep 28, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-10439-storage-usage-banner

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #10439

⏸️ Parked on an external gate, not a defect of this PR: the required Spec Main Shape Gate fails every objectui merge group (report-chart-query-spec-parity.test.ts line 747, TS1360, against objectstack df3ba164a588). Anchor card: #10916. This PR is re-queued by the claiming seat when that card closes.
Clause-②: no

What this does

The tenant runtime's GET /api/v1/usage/storage already serves the storage verdict that its upload and bulk-import guardrail refuses with. Nothing in the console rendered it, so an environment reached 80% with nothing shown and found out it was full when an upload failed. This PR renders the environment admin's banner from that verdict. It follows the product adjudication quoted on the card:

提醒:≥ 80% 环境管理员横幅「已用 X / Y」;≥ 100% 横幅升级为「已满,上传与导入已暂停」+ 升级入口。

served verdict what renders
blocked: true "Storage is full: uploads and imports are paused", the used and limit figures in MB, and an upgrade link
warn: true "Storage is filling up" and the used and limit figures in MB, with no link
ok, unknown (the endpoint's miss answer), unlimited nothing
network failure, non-2xx, or an off-contract payload nothing
  • useStorageUsageReading (new, in packages/app-shell/src/hooks/) reads the flat storage half of the response: state, warn, blocked, usedMb and limitMb. Parsing is contract-first:
    • state must be one of the five values the producer declares, and warn and blocked must be booleans.
    • usedMb and limitMb must be finite, non-negative numbers when present.
    • A warn or blocked verdict that arrives without both numbers is off-contract. The producer's evaluateStorageQuota raises either flag only after both numbers are known, and the banner copy is made of those two numbers.
    • Anything off-contract becomes unavailable. It is never coerced and never rendered with a stand-in number.
  • classifyStorageUsage decides the banner from blocked first, then warn, and from nothing else. It never compares usedMb with limitMb, and never compares fraction with warnFraction or a literal line. Neither fraction nor warnFraction is modelled. This is the card's rule: "Read the verdict, do not re-derive the threshold."
  • StorageUsageBanner (new, in packages/app-shell/src/layout/) is mounted in ConsoleShell beside ReadRateBanner, behind the same useWorkspaceAdminStatus gate. A non-admin session renders nothing and issues no request.
  • One request, not two. readStorageUsage (new, in storageUsageEndpoint.ts) shares one in-flight request per URL between the two hooks. It forgets the request when it settles, which is the same shape as the INFLIGHT map in useApproverDirectory. It keeps no answer, so refetch still issues a new request.
  • useReadRateReading now reads through readStorageUsage. resolveRuntimeApiBase moved into the shared module and is re-exported from its old home. The three separate "no banner" answers from classifyReadRate (unmeasured, unavailable, ok) and all existing read-rate tests are unchanged.
  • i18n: the five console.storageUsage.* keys are added to all ten locale packs. Each pack uses its own unit spelling: MB, Mo (French), МБ (Russian) or ميغابايت (Arabic).
  • Docs: the app-shell README's read-rate section gains a storage-capacity subsection.
  • Changeset: .changeset/10439-storage-usage-banner.md bumps @object-ui/app-shell and @object-ui/i18n as minor.

Nothing is added to the package entry (src/index.ts). The new hook and banner are re-exported only from the internal hooks/ and layout/ barrels, the same route the read-rate pair takes. So Clause-②: no holds as the claim stated it.

Premises measured, not taken from the card

The cloud side was read at objectstack-ai/cloud main 96eb092, read-only:

  • Wire keys. The response carries usedMb and limitMb. The card body's storageUsedMb and storageLimitMb are the registry-row fields the handler reads, as the claim's correction says. Every miss answers 200 with state: 'unknown' and both flags false.

  • Nothing read the storage half. On objectui main at the fork point, git grep -c -E "usedMb|warnFraction|STORAGE_LIMIT_REACHED|limitMb" -- packages apps found nothing and exited 1. The control, usage/storage, hit four files.

  • Audience. useWorkspaceAdminStatus confirms admin through any of four sources:

    • an owner or admin role on the active organization's membership (either spelling);
    • an admin user.role;
    • the isPlatformAdmin rung;
    • org_owner or org_admin in positions.

    This is the gate ReadRateBanner already uses for the same audience ("the environment's own admin"). Cloud names the audience 「环境管理员」 in storage-quota.ts and in the read-rate changeset, and defines no narrower tenant-console role. So no second gate was invented.

  • The upgrade entry needs no plan field. The guardrail's refusal envelope for the same verdict, StorageLimitRefusal, declares details.upgrade as the literal type true. An unlimited plan (limit 0) never reaches blocked. The link is cloudConsoleUrl(), the control plane's origin. It is left out when that returns an empty string (no upstream cloud), which is the rule from objectui#7253 that the AI usage indicator's call to action also follows. No cloud-side field gap was found.

Verification

All runs are at HEAD efa30fc, after merging origin/main at 7ea8118, with heavy runs going through os-verify-lock.sh.

command exit result
turbo run build --filter="@object-ui/app-shell^..." --concurrency=2 0 28 of 28 tasks
pnpm --filter @object-ui/app-shell type-check (tsc --noEmit && tsc -p tsconfig.test.json) 0 --listFiles shows both new test files in the test project
pnpm exec vitest run --reporter=verbose over 28 targets: the 4 touched test files, cloudConsoleUrl-7253, the 22 test files that render ConsoleShell, and packages/i18n/ 0 99 files and 1404 tests passed, including new hook 18/18, new banner 9/9, read-rate hook 9/9 and banner 10/10 (unchanged), all-locales-key-parity 32/32, entry-locale-shape-7479 8/8, consoleUserPreferenceBudget 3/3
pnpm exec eslint --format json on the 19 touched .ts / .tsx files 0 0 errors, 10 warnings, 0 ignored
pnpm check:i18n-keys / check:i18n-drift / check:i18n-dead-keys 0 / 0 / 0 drift reports 5 keys added and 0 en values changed; dead-keys lists no storageUsage key
pnpm check:vi-mock-specifiers / check:vi-mock-inherit / check:vi-mock-override-shape 0 / 0 / 0
pnpm check:test-path-roots / check:self-import / check:phantom-deps / check:unreferenced-sources 0 / 0 / 0 / 0
pnpm check:new-line-citations / check:control-bytes 0 / 0 0 new citations
node scripts/check-changeset-presence.mjs / check-changeset-no-major.mjs 0 / 0
pnpm check:changeset-claims / check:pending-changeset-literals 0 / 0 report-only; the one flagged paragraph (6661, about console.nav rows) stays true
node scripts/check-governed-queue-guard.mjs --test on the 21 changed paths 0 NOT GOVERNED

Ablation: one-shot, restored, no permanent test file

Each mutation went through ablation-replace.mjs in WRAP mode. The anchor hit exactly once, the blob changed on disk, and afterwards the blob matched HEAD again with git diff HEAD empty. No build leg was needed: the tests import the source by relative path, so dist/ is not on the resolution path.

  1. Re-derive the banner from the numbers (usedMb / limitMb >= 0.8 in place of the warn / blocked check). useStorageUsageReading.test.tsx went red: 3 failed, 15 passed. The failures were the verdict pin, the classifier table, and the unlimited-plan case, because re-deriving divides by a zero limit and would show a banner on an unlimited plan.
  2. Drop the in-flight sharing (if (pending) return pending; removed). The same file went red: 1 failed, 17 passed. The failure was the one-request pin (toHaveBeenCalledTimes(1)).

Both went red, which is the direction expected before the runs.

Acceptance notes

  • NOT MEASURED: check:eager-locale-catalogues. It reported PREREQUISITE NOT MET because it reads a built console bundle. This diff only adds rows inside the existing catalogue object literals. It touches no import, no locales/index.ts and no registry.ts. The source-shape pin entry-locale-shape-7479 passed 8 of 8.
  • NOT MEASURED: check:readme-exports. It exited 1 because 86 self-imports across packages could not be judged: their dist/index.d.ts is not built. The README diff adds no fenced block and no import binding, so it cannot change this gate's verdict.
  • Lint was narrowed, and the narrowing is declared. It ran on the 19 touched .ts / .tsx files instead of the packages' eslint ., because the shared lock was held by another agent's full app-shell vitest run for about 20 minutes. Why this still covers every file the diff can affect:
    1. The population comes from ESLint's own run: 19 files in the --format json output, none reported as ignored.
    2. The file count is read from that JSON.
    3. The root eslint.config.js sets no parserOptions.project or projectService and loads no import-resolving plugin, and no local rule in eslint-rules/ reads the disk. So this diff cannot change the verdict for any file it does not touch.
  • One new lint warning (not an error): react-hooks/set-state-in-effect on the new hook's synchronous loading set. This is the same pattern, and the same warning, that useReadRateReading already carries at base. lint.yml sets no --max-warnings. The warning counts for ConsoleShell.tsx (8) and useReadRateReading.ts (1) are unchanged from base.
  • Guide docs. No content/docs/guide page covers the console's chrome banners: read-rate, impersonation, or now storage. The app-shell README section is the doc surface. No one is picking this up.
  • Release text in another pending changeset. .changeset/9954-read-rate-banner.md says useReadRateReading and ReadRateBanner "are exported from @object-ui/app-shell". The package entry exports neither; they are re-exported only from the internal barrels. This is reported to the seat and not edited here.
  • Upgrade link destination. The upgrade link lands on the control plane's origin, not on a billing page. That is objectui#7253's standing limit: the control plane does not yet publish an upgrade URL for tenant SPAs.
  • Shared locale files. The locale files are also being edited by other in-flight work, including the parallel objectui#8524 work. origin/main was merged twice before opening this PR.

Session: https://claude.ai/code/session_015AUunPkX7UTkCH9e7AdZo1, by an os-dev agent dispatched by the domain:ui seat 4 PM.


Generated by Claude Code

…er from the served verdict (objectui#10439)

The tenant runtime's GET /api/v1/usage/storage already serves the storage
verdict its upload / bulk-import guardrail refuses with (warn at 80% and
up, blocked at 100%), plus usedMb / limitMb. Nothing rendered it.

- useStorageUsageReading reads the flat storage half, contract-first, and
  classifyStorageUsage resolves it from warn / blocked alone; the two
  numbers never decide the banner.
- StorageUsageBanner renders "X MB of Y MB used" on warn, and the
  "storage is full: uploads and imports are paused" banner with the
  upgrade entry on blocked. Mounted in ConsoleShell beside ReadRateBanner,
  behind the same useWorkspaceAdminStatus gate.
- readStorageUsage shares one in-flight request per URL, so the storage
  banner and the read-rate report cost one request between them.
- console.storageUsage.* in all ten locale packs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015AUunPkX7UTkCH9e7AdZo1
…geset (objectui#10439)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015AUunPkX7UTkCH9e7AdZo1
@github-actions

Copy link
Copy Markdown
Contributor

changeset-claim-re-read

⚠️ 1 pending changeset(s) describe a file this change touches

Their bodies publish verbatim into the CHANGELOG at the next release, so this is a request to re-read them against your diff — addressed here because you are the one seat that can answer it without re-deriving anything.

⛔ Nothing here blocks, and nothing here is a verdict on your change. This gate exits 0, is not a required context, and judges name resolution, never meaning: it asked whether a pending body names a file you touched. "Is this sentence still true?" is the one question it will not answer, and the one you are being asked to answer.

.changeset/6661-app-launcher-nav-menu-renderers.md

  • names en.ts → packages/i18n/src/locales/en.ts — edited by this change

    Three new strings — the launcher's and the menu's accessible names, and the menu's empty state — are declared under console.nav in en.ts and its nine sibling packs. An inline defaultValue alone is not a fix: it renders English at one call site and leaves the string untranslatable everywhere (objectui#3517).

Read the paragraph, not the line: both false halves of the objectui#8617 claim sat in one paragraph, and correcting either alone would have left it asserting the same wrong thing.

If a claim did go false, correct the body. That is precedented and prose-only, frontmatter untouched; check-changeset-overwrite.mjs will report the correction as its own case 2 ("correcting a declaration on purpose … legitimate"), which is the intended shape — one gate asks for the read, the other records the write.

Not covered, stated so nobody reads this as more: a born-false claim that spells no line address at all (objectui#9495 coordinated one by ORDINAL — "a grep finds that member first" — and deciding that means reading what the sentence means), a claim spelled as a symbol or a package rather than a backticked file name, and a file named ambiguously.

Compared the checked-out tree with 8522396c0 (merge-base with origin/main): 20 file(s) changed outside .changeset/, read against 1669 pending declaration(s) that publish a body (2263 pending in total). · run

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 329 chunks) 3097.0 KB 3104.5 KB
Main entry chunk (gzip) 149.3 KB 350 KB
Entry file index-B3rG625C.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.58KB 6.17KB
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) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.22KB 10.61KB
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.70KB 2.23KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.39KB 1.21KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.70KB 10.94KB
auth (createAuthenticatedFetch.js) 8.52KB 3.45KB
auth (index.js) 3.63KB 1.64KB
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) 27.13KB 7.95KB
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) 557.59KB 133.60KB
core (index.js) 9.93KB 3.94KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 226.44KB 63.00KB
fields (index.js) 261.01KB 66.28KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.24KB 2.27KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.40KB 12.91KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 39.32KB 11.09KB
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) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
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.33KB 3.07KB
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) 16.01KB 3.93KB
plugin-calendar (index.js) 51.96KB 14.83KB
plugin-charts (index.js) 83.99KB 22.86KB
plugin-chatbot (index.js) 197.67KB 46.90KB
plugin-dashboard (index.js) 136.93KB 36.48KB
plugin-designer (index.js) 215.03KB 44.21KB
plugin-detail (index.js) 233.48KB 61.79KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 161.21KB 41.41KB
plugin-gantt (index.js) 170.35KB 42.19KB
plugin-grid (index.js) 228.33KB 62.59KB
plugin-kanban (index.js) 48.43KB 15.11KB
plugin-list (index.js) 115.86KB 28.64KB
plugin-map (index.js) 22.90KB 7.62KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 44.17KB 12.20KB
plugin-timeline (index.js) 31.00KB 9.09KB
plugin-tree (index.js) 11.21KB 3.89KB
plugin-view (index.js) 88.55KB 22.21KB
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.81KB 3.58KB
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) 119.16KB 39.05KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.03KB 1.86KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.78KB 2.09KB
sdui-parser (codegen.js) 7.50KB 3.05KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 6.16KB 2.71KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.84KB 1.90KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 18.27KB 6.22KB
types (ai.js) 4.39KB 2.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 3.83KB 1.49KB
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.26KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 2.52KB 1.31KB
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) 17.15KB 6.32KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.27KB 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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: efa30fcfe82b649da88fc80ff59694b265eb67df
Local-runs: none

Inputs read: card objectui#10439 (body and all 4 comments: 5824958765, 5824965929, claim 5864158793, os-dev-report 5864901900); PR #10910 (body, the 21-file list, the net diff against main); the 43 check-runs on the head (40 success, 3 skipped: coverage x2 and dependabot, 0 failed); the producer at cloud origin/main 72857a1: storage-usage-endpoint-plugin.ts, storage-quota.ts, storage-limit-guardrail-plugin.ts. One extra read-only read, disclosed: packages/app-shell/src/index.ts and packages/app-shell/package.json at the reviewed head itself (refs/review/pr-10910), because ② turns on them and the dev's name-grep on the entry would not see a wildcard re-export. Nothing was built, run or re-run.

① Derived judgments

Accept-set — what the client admits from GET /api/v1/usage/storage, each against cloud's StorageUsageResponse and evaluateStorageQuota:

  • state must be one of ok, warning, exhausted, unlimited, unknown — RIGHT: exactly cloud's StorageQuotaState. An undeclared state resolves to unavailable and renders nothing (fail-soft, never a wrong banner).
  • warn and blocked must be booleans — RIGHT: always present and boolean on the wire, in buildStorageUsageResponse and in the miss answer unknownStorageUsage.
  • usedMb / limitMb optional; when present, finite and non-negative numbers — RIGHT: cloud's finiteNonNegative emits exactly that or leaves the key absent; null is never emitted, so refusing it is contract-true.
  • a warn or blocked verdict without both numbers is off-contract — RIGHT: evaluateStorageQuota reaches warning or exhausted only after both numbers are defined and the limit is not 0, so the invariant holds on the producer, not only in the client's head.
  • fraction and warnFraction not modelled; readRate left to useReadRateReading — RIGHT: the card's ⭐ rule; the read-rate parser is untouched by the diff.
  • non-2xx, network failure or non-JSON body resolves to unavailable — RIGHT: cloud answers every miss with 200 unknown, so a non-2xx only ever means no route or no auth; nothing renders.
  • unknown extra keys ignored — RIGHT (forward-compatible; the producer may add keys).

Classifier classifyStorageUsage: blocked first, then warn, else no banner. Read in full: it never compares usedMb with limitMb, never reads fraction or warnFraction, and holds no literal line; the parser's only numeric test is a below-zero check. RIGHT — this is the card's ⭐ rule, and the flags are the same verdict the guardrail refuses with (both sides call evaluateStorageQuota). state is carried for diagnostics and not cross-checked against the flags; on the producer all three come out of one function, so they cannot disagree on the wire. Right.

Rendering against the adjudication the card quotes (at or over 80%: 「已用 X / Y」; at or over 100%: 「已满,上传与导入已暂停」 plus 升级入口):

  • warn renders "Storage is filling up" with the used and limit MB and no link — RIGHT: the informational tier; the upgrade entry belongs to the full banner only.
  • blocked renders "Storage is full: uploads and imports are paused", the used and limit MB, and an upgrade link to cloudConsoleUrl(), omitted when that is empty — RIGHT. The body copy mirrors the guardrail's own messageEn and zh message (data untouched; reading, exporting and single-record editing still work).
  • ok, unknown, unlimited, unavailable, pending all render null — RIGHT. unlimited (limitMb: 0) can never reach a banner, matching cloud, where unlimited is decided by the limit alone before anything is compared.
  • Audience: useWorkspaceAdminStatus().isAdmin gates both the render and the request (enabled: isAdmin; the test pins that a non-admin issues no request) — RIGHT: the endpoint handler does no role check (read), so the 环境管理员 gate is the client's, and it is the same gate ReadRateBanner uses for the same producer's other half.
  • Mount: ConsoleShell, beside ReadRateBanner — RIGHT (diff).
  • One request: readStorageUsage keeps an in-flight map keyed by URL, deletes the entry in finally, caches no answer — RIGHT; holds under a StrictMode double mount too, and refetch still re-requests. useReadRateReading now goes through it; its parser, its three no-banner values and its tests are unchanged, and the head's Test shards are green.

Public-surface changes in the diff:

  • hooks/index.ts gains useStorageUsageReading, classifyStorageUsage and seven types; layout/index.ts gains StorageUsageBanner and StorageUsageBannerProps — both are internal barrels (see ②).
  • resolveRuntimeApiBase moved into hooks/storageUsageEndpoint.ts and is re-exported from useReadRateReading.ts, its old module — no name removed anywhere. RIGHT.
  • ConsoleShell (an entry export) renders one more child — additive behavior.
  • @object-ui/i18n gains 5 keys under console.storageUsage in 10 packs — additive; the dev's check:i18n-keys, -drift, -dead-keys exited 0 and the head's Test check-runs (which carry the locale parity pin) are green.

Shipped prose, sentence by sentence:

  • .changeset/10439-storage-usage-banner.md: wire keys usedMb / limitMb — TRUE. warn from 80%, blocked at 100% — TRUE (STORAGE_WARN_FRACTION = 0.8; blocked when used reaches the limit). Same verdict the upload and bulk-import guardrail refuses with — TRUE (one evaluateStorageQuota; the guardrail intercepts POST .../import and the /storage/upload/* family). "850 MB of 1,024 MB used" — TRUE for the en locale, pinned by the banner test. The blocked banner's copy, its link, and the link's omission on a runtime with no upstream cloud — TRUE. Anything else renders nothing — TRUE. Never compares the figures with each other or a threshold — TRUE. Mounted beside the read-rate report, admin-only render and admin-only request — TRUE. One shared request — TRUE. "Neither banner is exported from the package entry" — TRUE, verified in ②. Five keys in all ten packs — TRUE (ar, de, en, es, fr, ja, ko, pt, ru, zh; parity pin green).
  • README "Storage capacity (environment admin)": every sentence TRUE against the diff and the producer. The table's four rows are the classifier's four outcomes; the cloudConsoleUrl destination and its omission, the one shared reader, and "same audience and the same gate as the read-rate report" all hold.
  • en copy (warningTitle, warning, blockedTitle, blocked, upgrade): consistent with the adjudication and with the guardrail's messageEn. "Uploads and imports" is the guardrail's own phrasing for attachment upload plus bulk import, and the blocked body says single-record editing still works, so the short form misleads nobody. TRUE.
  • zh copy: blockedTitle 「存储已满,上传与导入已暂停」 is the adjudication's wording; warning and blocked open with 「已用 X MB / Y MB」, the adjudication's 「已用 X / Y」; the blocked tail 「现有数据不受影响,读取、导出与单条记录编辑照常」 is the guardrail's own zh message; upgrade 「升级以继续」 matches its 「升级可继续」. TRUE.
  • The card body's storageUsedMb / storageLimitMb are the registry-row names the handler reads, not wire keys; the diff followed the producer, not the card. RIGHT.

② Semver level

  • Changeset: @object-ui/app-shell: minor, @object-ui/i18n: minor — RIGHT on both. app-shell adds a component, a hook, barrel names and new ConsoleShell behavior, with no removal and no signature change (resolveRuntimeApiBase keeps its old module); i18n adds keys only. On the head: Changeset Bump Policy, Changeset Declaration, Changeset Fixed Group Check, Changeset Claim Re-read all success.
  • Clause-②: no (claim 5864158793 and the PR body) — HOLDS, verified at the head rather than taken from the claim: packages/app-shell/src/index.ts contains no wildcard re-export; its ./hooks/index.js block names 14 hooks and its ./layout/index.js block 11 chrome names, and none of them is useStorageUsageReading, classifyStorageUsage, StorageUsageBanner, useReadRateReading or ReadRateBanner; package.json exports is . and ./styles.css only, so the two barrels are not reachable as subpaths either. The diff touches neither file. No published symbol widens; the semver level matches what the diff publishes.

③ Boundary flags

open_questions: []. Every dev deviation and PR Acceptance note, answered or escalated:

  1. Lint narrowed to the 19 touched files — ANSWERED by the head's Lint check-run: success.
  2. check:eager-locale-catalogues NOT MEASURED — ANSWERED: the diff adds rows inside existing catalogue literals and changes no import, locales/index.ts or registry.ts; Build & E2E and Bundle Analysis are success on the head.
  3. check:readme-exports NOT MEASURED — ANSWERED by the head's README Export Check: success.
  4. One new react-hooks/set-state-in-effect warning — ACCEPTED: the same shape the sibling hook carries at base; Lint success; a warning, not an error.
  5. No content/docs/guide page covers the console chrome banners (read-rate, impersonation, storage); the README is the only doc surface; no carrier — ESCALATED to the seat: file a docs card for the three banners together. Not a fail here: the PR ships the README subsection and follows both sibling banners' precedent.
  6. .changeset/9954-read-rate-banner.md says the read-rate pair "are exported from @object-ui/app-shell" — CONFIRMED FALSE at this head (② above; the entry names neither) — ESCALATED to the seat: correct that pending changeset before the next release. Outside this PR's file surface, correctly left unedited.
  7. Upgrade link lands on the control plane origin, not a billing page — ACCEPTED: the producer publishes no upgrade URL (the refusal envelope's details.upgrade is the literal true; the usage endpoint carries nothing), so there is no cloud field to consume; this is objectui#7253's standing limit and stays tracked there.
  8. Locale files shared with in-flight PRs — ANSWERED: mergeable: true, mergeable_state: clean at review time.
  9. File surface also includes hooks/storageUsageEndpoint.ts and layout/index.ts — ANSWERED: within the claim's "a sibling hook in hooks/ if the endpoint read is shared" and "a new banner under layout/".
  10. resolveRuntimeApiBase moved and re-exported — ANSWERED: no name removed from any module or barrel; the read-rate tests import it unchanged.
  11. PR footer in the os-dev contract form — not a contract matter.

Implemented-by: claude/issue-10439-storage-usage-banner
Reviewed-by: session_015AUunPkX7UTkCH9e7AdZo1

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 28, 2026 07:18
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 28, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Sep 28, 2026
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 28, 2026
Merged via the queue into main with commit 4357a27 Sep 28, 2026
45 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-10439-storage-usage-banner branch September 28, 2026 09:02
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Sep 28, 2026
…ts name 23 objectui issues that answer 404, and re-qualify 19 bare objectstack numbers (objectui#10803, batch 6) (objectstack-ai#10914)

Part of objectstack-ai#10803
Clause-②: no

Dispatched implementation of the `domain:ui` seat objectstack-ai#1 claim (comment
`5864334310`) on objectui#10803, batch 6, session
`https://claude.ai/code/session_01DuWo5bdP9SdVebamn99GGk`. Citations
only: no sentence's claim moves, and every edited pending changeset's
frontmatter is byte-identical. The one runtime string that moves is
`InputSchema.wrapperClass`'s zod `.describe()` text in
`@object-ui/types`, which loses its dead pointer and nothing else
(amendment `5860244997`, Q1 = A; `patch` changeset). No test pins any
changed text: the literal-anchor sweep below finds no specific anchor,
so no test file is edited.

This batch carries the release note `5863776648`'s three lists:
- the last **23 family numbers**;
- the **18 bare objectstack numbers above 10900** from PR
objectui#10892's C0 census, plus **#13086**, a 404 the C0 instrument
cannot see because it shares a token with #13337 (**Premise**);
- the live-but-wrong bare `objectstack-ai#3391` at the two `rowCrudAffordances.ts`
sites beside `objectstack#3720`.

## Why `Part of`, not a closing line

The order says `Part of`. With this PR the family list (all 115 numbers
of amendment `5860244997` Q2) and the C0 above-10900 list both read 0.
Whether the card now closes, or carries the dead `objectstack#N` class
measured in **Acceptance notes** 1, is the seat's call.

## Premise, re-measured on `origin/main` `9f0c84a44` (the branch point)

- **The 23 family numbers.** REST `GET
/repos/objectstack-ai/objectui/issues/N`: 23 of 23 answer 404, and a
second read of each answers 404 again. `GET .../pulls/N` answers 404 for
all 23. Lit controls: objectui#10533 and objectui#7714 answer 200.
- **No new family member.** The distinct `objectui#N` citations in the
two in-scope classes at the branch point, less those at batch 1's head
`6c3ad7c80`, are 24 numbers. Each was read once: 24 of 24 answer 200. So
the family list is still batch 5's 23.
- **#14026 was a sister-repo card written as objectui's.**
objectui#14026 answers 404 as an issue and as a pull. objectstack#14026
answers 301 to objectui#10102, the card it was transferred to. That
card's measurement-round claim (branch
`claude/issue-14026-import-mapping-selector-probe`) is the session of
`ecf14190e`, the commit that added `14026-list-import-mappings-pin.md`,
and six sibling in-scope lines already write `objectstack#14026` for the
same card.
- **C0, the bare-number census** (the instrument of PRs objectui#10875
and objectstack-ai#10892, at the branch point):

```
git grep -hoP '(?:^|(?<=[^\w#&/.\-]))#\d+(?![0-9A-Za-z_])' 9f0c84a -- '.changeset/*.md' 'packages/*/src/**' ':!**/__tests__/**' ':!**/*.test.*' ':!**/*.spec.*' ':!**/test/**' ':!**/tests/**' | sort -u | wc -l
```

- 984 distinct at `9f0c84a44`, against 998 at batch 5's `b2683a2c0`. The
14 that batch 5 re-pointed are gone, and none is new.
- Above 10900 there are 23: the same 5 CSS colours and the same 18
numbers. Each of the 18 answers 404 as an objectui issue and as an
objectui pull on two reads.
- In objectstack, 16 of the 18 answer 200 with the sentence's own
subject (the **C0 mapping** below). #13033 and #13413 answer 404 there
too, as issues and as pulls. Their squash commits exist in objectstack's
history (a full, not shallow, clone): `c459da6bc` "narrow the per-option
`default` key out of the form-view options vocabulary … (#13033)" and
`89448a52b` "remove the inert AUTH_SSO_PROVIDER_SCHEMA export (#13413)".
Both are ancestors of objectstack `main` (`git merge-base
--is-ancestor`, exit 0).
- **The C0 instrument's blind spot.** Its lookbehind refuses a `#`
preceded by `/`, `-`, `.` or `&`. That form (`#A/#B`, `-#N`) carries 109
distinct numbers at the branch point. The 17 that no earlier batch read
were each read once. 16 answer 200. **#13086 answers 404**, as an issue
and as a pull, in objectui and in objectstack. It sits in
`6985-wizard-card-r-alignment.md` as "the #13337/#13086 fence", in the
same token as #13337, so it rides here. objectstack's
`check-yaml-examples.ts` header, at its landing `2ebfe7e9f` (PR
objectstack#13267, which answers 200), reads "Check YAML Examples
(anti-drift for the AUTHORING format, #13086)" and "Ruled 2026-08-29
(#13086)".
- **objectstack-ai#3391.** objectui#3391 answers 200 with an unrelated subject (a
record-header api action placeholder). objectstack#3391 answers 200 as
the apiMethods whitelist contract card ("跟踪:UI 操作按钮与 apiMethods
白名单一致性契约落地"), and objectstack#3720's own title calls itself "objectstack-ai#3391
遗漏的第四个面".
- Every edited changeset is pending: it is present in `.changeset/` on
`main`. The checkout is not shallow.

## Census (the enumeration pin for this batch)

The instrument PR objectui#10854 printed and PRs objectui#10869 / objectstack-ai#10875
/ objectstack-ai#10892 reused, with this batch's 23 numbers substituted (REF = a
commit or tree):

```
git grep -nE '(objectui#|#|issues/)(8072|8127|8137|8204|8229|8248|8307|8408|9231|9241|9244|9365|9373|9375|9542|9553|9585|10117|10119|10120|10129|10132|14026)([^0-9]|$)' REF -- '.changeset/*.md' 'packages/*/src/**' ':!**/__tests__/**' ':!**/*.test.*' ':!**/*.spec.*' ':!**/test/**' ':!**/tests/**' | grep -v 'objectstack#' | wc -l
```

- REF = `9f0c84a44` (branch point): **104** lines.
- One more line is hidden by the `grep -v 'objectstack#'` filter:
`9943-viewtype-totals-page-row-retired.md` names objectui#8127 beside an
`objectstack#` citation.
- So the true population is **105** lines: 28 changeset lines in 26
files, and 77 src lines in 42 files.
- Unfiltered, REF reads 111. The other 6 lines are live
`objectstack#14026` lines.
- REF = `2b3d2061a` (this head): **0**. Unfiltered it reads 7, all live:
the six `objectstack#14026` lines and the re-qualified one.
- This head merged with a fresh `main` (`8522396c0`, `git merge-tree
--write-tree`, clean, tree `f19d17884`): **0**. REF = `8522396c0` alone:
104.
- **C0**, the 19 numbers, bare and not `objectstack#`-qualified: 28
lines in 23 files at the branch point; 0 at this head. At this head C0
reads 966 distinct numbers, and above 10900 only the five CSS colours
are left.
- Lit control, the printed instrument over live objectui#7714 at this
head: 17 lines. Hex-colour false positives (a number followed by a hex
letter) at the branch point: 0.
- **Out of scope, as it stands** (the 23 numbers, filtered, whole tree
at this head):
  - 94 test lines in 36 files;
  - 5 `.github` lines in 1 file (`ci.yml`, objectui#9241);
  - 2 `apps/console` lines in 1 file (`FormPage.tsx`, objectui#8408);
  - 1 line of the root `vitest.config.mts`;
  - 0 scripts, 0 governed, 0 `CHANGELOG.md`.
- For the 19 C0 numbers: 13 test lines in 10 files, and 1 line of
`packages/plugin-tree/README.md`.

## Citation form

- **Landing shas.** The 9-character backticked sha of the commit on
`main` that landed the change the sentence rests on, as in the earlier
batches. All 15 distinct objectui shas below are ancestors of `main`:
`git merge-base --is-ancestor`, exit 0 each. Control legs in the same
checkout: the head of PR objectui#10902 (`496c63c06`) answers exit 1,
and the known ancestor `5f789538d` answers exit 0. `git rev-parse
--short=9` returns the same 9 characters for each.
- **Own-card pointers** in a changeset are dropped, not replaced. The
configured changelog generator (`@changesets/cli/changelog`, per
`.changeset/config.json`) prefixes each released entry with the hash of
the commit that added the file, which is that landing.
- **The claim lived only on the card.** Where the sentence rests on
something that lived only on the dead card, the sha only locates it:
"the card behind SHA".
- **Nothing answers.** The pointer is dropped and the sentence names the
card by role (objectstack-ai#9553 only).
- **Sister-repo numbers.**
- A bare sister-repo number becomes `objectstack#N` after reading it
there.
- "objectstack PR #N" becomes "PR objectstack#N", since the qualifier
now carries the repository. "framework #N", "upstream #N" and "spec #N"
keep their word, as batch 4 kept "framework PR objectstack#6942".
- Where the objectstack pull request itself answers 404, the sentence
cites its commit in that repository as objectstack `SHA`, the spelling
the tree already uses for an objectstack commit ("Measured on
objectstack `9bd4344e4`").
- **Runtime text** carries no sha: see **Special cases** 6.

## Mapping, the 23 family numbers

Lines / files are the branch-point census for that number. "Method" is
how the landing was found; for every source site, `git log -S
'objectui#N' -- FILE` names the commit that wrote the citation, and it
is the landing below unless the row says otherwise.

| dead number | resolution | method | lines / files | why the commit
carries it |
|:--|:--|:--|:--|:--|
| objectstack-ai#8072 | `c974edf14`; own-card pointer dropped; runtime pointer dropped
| changeset's adding commit, subject "(objectui#8072)" | 5 / 3 | it
mirrors `wrapperClass` on `InputSchema`, which is what both comments and
the `7722` changeset say happened |
| objectstack-ai#8127 | `ca3942729`; "the card behind `ca3942729`" twice (**Special
cases** 2) | `git log -S` names `ca3942729` for the first citation in
all 10 source files; `05a49f2ee` (objectui#9880) and `8a7e09fa1` (the
`9943` changeset's landing) wrote three later sentences that cite the
same fix | 17 / 11 | it derives `ViewType` from `@objectstack/spec`
instead of re-declaring it, and its message records the
`page`-degrades-like-a-typo measurement the sentences cite |
| objectstack-ai#8137 | own-card pointer dropped (landing `0fa7a9c83`) | changeset's
adding commit | 1 / 1 | the only site is its own changeset's pointer |
| objectstack-ai#8204 | own-card pointer dropped (landing `580b0fdf4`) | changeset's
adding commit | 1 / 1 | the pointer opened a paragraph as its own
sentence |
| objectstack-ai#8229 | "a separate finding" (changeset); "the finding `8b7ea3945`
reconciled" (`flex.tsx`) | `8b7ea3945`'s message: "Reconciles the third
face objectui#8229 found" | 2 / 2 | the finding lived on the dead card;
`8b7ea3945` is the commit that reconciled it, and it is the `7735`
changeset's own landing |
| objectstack-ai#8248 | own-card label dropped from the second list item (landing
`d02942b0e`) | changeset's adding commit; its message covers
objectui#8458 and objectui#8248 | 1 / 1 | the changeset lists the two
cards its one landing closed |
| objectstack-ai#8307 | `5591f03bd`; own-card pointer dropped | changeset's adding
commit | 11 / 4 | it makes a lane header over a windowed fetch say
`77+`, and it wrote every comment that cites the card |
| objectstack-ai#8408 | own-card pointer dropped; "seam 2 of the card behind
`8241a4400`" | changeset's adding commit; `8241a4400`'s message names no
seams | 2 / 2 | the seam list lived on the dead card |
| objectstack-ai#9231 | `383502b23`; own-card pointer dropped | changeset's adding
commit | 3 / 2 | it gives the create dialog's confirm control its own
accessible name, the rule both `i18n.ts` comments state |
| objectstack-ai#9241 | own-card pointer dropped (landing `250429c8f`) | changeset's
adding commit | 1 / 1 | the only site is its own changeset's heading |
| objectstack-ai#9244 | `bd0995738` | `git log --grep`: its message names the card; it
wrote all three comments | 3 / 2 | it emits one col-span class per
breakpoint tier, not one for the widest |
| objectstack-ai#9365 | own-card pointer dropped (landing `0970a0e00`) | changeset's
adding commit | 1 / 1 | the only site is its own changeset's pointer |
| objectstack-ai#9373 | `c1006ed8e`; own-card pointer dropped | changeset's adding
commit | 2 / 2 | it resolves the inline locale map before the
interpolation options, the `ListView` comment's subject |
| objectstack-ai#9375 | own-card pointer dropped (landing `e427e9c00`) | changeset's
adding commit | 1 / 1 | the only site is its own changeset's pointer |
| objectstack-ai#9542 | `43c0d1710`; own-card pointer dropped; "filed as a separate
card" (**Special cases** 3) | changeset's adding commit; `git log -S`
names it for the `action-button` / `action-icon` comments too | 9 / 6 |
it accepts and resolves an inline `I18nLabel` on `resultDialog` and
makes `ResultDialogSpec` derive its label members |
| objectstack-ai#9553 | nothing answers: "a separate card carried that census" | `git
log --grep` finds only `40f34b4ba`, which points at the card; no commit
lands it | 1 / 1 | see **Special cases** 4 |
| objectstack-ai#9585 | "`ee70287e4`'s pin measures it NOT GATED"; in its own
changeset, "a pin in this change measures it" | no commit names it;
`ee70287e4` adds the test "NOT GATED: the renderer paints the very node
the mirror refuses, without parsing it" | 2 / 2 | see **Special cases**
5 |
| objectstack-ai#10117 | `4c6f549ef`; own-card pointer dropped | changeset's adding
commit | 3 / 2 | it resolves a lookup title candidate in `page:header`
and adds the record-key safety net both comments describe |
| objectstack-ai#10119 | `73a3c89af`; own-card pointer dropped | changeset's adding
commit | 4 / 2 | it makes a nav ancestor's gates reach its subtree and
stops a group outliving its children |
| objectstack-ai#10120 | `80c54122e`; own-card pointer dropped | changeset's adding
commit; for each file `git log -S` names it, or a later commit that
cites its gate: `e0f820246` (objectui#10563) or `b809375ac`
(objectui#10163) | 16 / 13 | it makes a form neither submit nor offer a
field the caller may read but not edit, the FLS half every site names |
| objectstack-ai#10129 | `6cc910b6d`; own-card pointer dropped | changeset's adding
commit | 10 / 5 | it routes a field-backed action param to its record
picker and refuses an unreadable one |
| objectstack-ai#10132 | `061f5e829`; own-card pointer dropped | changeset's adding
commit | 8 / 4 | it makes the two declared-translatable dashboard
surfaces resolve, including the axis `title` forward |
| #14026 | re-qualified `objectstack#14026` | see **Premise** | 1 / 1 |
it is the card the pin's measurement answered |

## Mapping, the 19 bare sister-repo numbers

| number | resolution | what objectstack says | lines / files |
|:--|:--|:--|:--|
| objectstack-ai#11289 | `objectstack#11289` ("upstream") | "record:details sections
cannot survive an empty record: `hideEmpty` / `collapsible` /
`showBorder` are honoured by the renderer but undeclared" | 2 / 2 |
| #11662 | PR `objectstack#11662` | "feat(spec): declare hideEmpty /
collapsible / showBorder on record:details sections" | 1 / 1 |
| #12616 | PR `objectstack#12616` | "feat(spec): declare record:details
section headerColor as a closed six-token enum" | 1 / 1 |
| #12718 | PR `objectstack#12718` | "feat(spec): retire preview mode —
the RuntimeMode 'preview' value and the whole PreviewModeConfig block" |
1 / 1 |
| #13033 | objectstack `c459da6bc` | the pull request answers 404; its
squash commit narrows the per-option `default` key out of the form-view
options vocabulary, the ruling `form-spec.ts` says was executed upstream
| 1 / 1 |
| #13337 | `objectstack#13337` | "docs(objectui): layout-dsl teaches
only shapes the live schemas accept" | 1 / 1 |
| #13086 | `objectstack#13267` | the card answers 404; PR
objectstack#13267 landed its ruled YAML-examples gate (see **Premise**)
| the same line |
| #13413 | objectstack `89448a52b` | the pull request answers 404; its
squash commit removes an inert schema export and leaves a note recording
the absence as a choice, the shape the `base.zod.ts` note cites as
precedent | 1 / 1 |
| #13632 | `objectstack#13632` | "[spec] `FieldSchema` accepts a
`lookup`/`master_detail` with no `reference` target …", the card
17.3.0's refinement answered | 3 / 3 |
| #13733 | PR `objectstack#13733` | "feat(spec): wizard view v1 —
declaration-and-refusal tightening of FormViewSchema type:'wizard' (Card
S)" | 1 / 1 |
| #13855 | `objectstack#13855` | the field-grouping decision card whose
option B is a section `group` reference, landed by `39404f3d9` (#13897)
"a layout section can reference a declared field group" | 1 / 1 |
| #13906 | `objectstack#13906` ("framework") | "two more
`computeExecCtx` seams read "failed" and "not wired" as one value …" | 6
/ 3 |
| #14274 | PR `objectstack#14274` | "fix(sdui-parser): refuse an
authored `type` attribute on the html tier …" | 1 / 1 |
| #14945 | `objectstack#14945` | "A flow cannot REFUSE with per-record
text …", honoured by `cca699149` "the flow `end` node honours `outcome:
'refused'`" | 1 / 1 |
| #15469 | `objectstack#15469` ("spec") | "`GanttConfigSchema` is
`strictObject(...).passthrough()` …", landed as `9c270bba0` "close the
gantt/tree config .passthrough() windows … (#15469)" | 2 / 2 |
| #15948 | `objectstack#15948` | "fix(plugin-auth)!: session payload
`positions[]` is the security axis, not the better-auth role scalar" | 1
/ 1 |
| #17493 | `objectstack#17493` | "three residues of #17322's node-door
refusal …", landed as `2c1011b01` "refuse a blank string in a flow
node's predicate slot" | 1 / 1 |
| #20051 | `objectstack#20051` | "judge a flattened view overlay's
top-level `options.KIND` …", whose door half is `6a4aec71d` | 1 / 1 |
| #20160 | PR `objectstack#20160` | "fix(spec): a joined report refuses
a top-level dataset / rows / columns / values, pointing each onto
blocks[]" | 2 / 2 |

The live-but-wrong number: "(`/me/permissions` `apiOperations`, objectstack-ai#3391)"
becomes "… `apiOperations`, objectstack#3391)" at the two
`rowCrudAffordances.ts` sites.

## Special cases (the judgement calls)

1. **#14026 is re-qualified, not dropped as an own-card pointer.** The
card behind `14026-list-import-mappings-pin.md` was never an objectui
card: it was objectstack#14026, now objectui#10102. Dropping the pointer
would lose which hypothesis the pin refuted. Re-qualifying it matches
the six sibling lines.
2. **objectstack-ai#8127, the card by role twice.**
- `CreateViewDialog.tsx`: "one of the sites the card records as
"drifted, …"" becomes "one of the sites the card behind `ca3942729`
records as …". That record lived on the card.
- `plugin-list` `ViewSwitcher.tsx`: "objectui#8127 was filed against the
two in `@object-ui/types`, and the maps below were described there as
total …" becomes "the card behind `ca3942729` was filed against …".
- `normalize-list-view.ts`: "The bug objectui#8127 records" becomes "The
bug `ca3942729` records". That commit's message carries the measurement:
`page` resolved "to exactly what it resolved a typo to".
3. **objectstack-ai#9542 in `8648-ui-action-four-undeclared-keys.md`.**
- The superseded paragraph keeps its pre-landing voice: "so it is filed
as a separate card and ⛔ not guessed at here".
- The superseding paragraph reads "`43c0d1710` landed the derivation".
- "(Noted here by the objectui#9542 seat, …)" becomes "(Noted here by
the seat that landed `43c0d1710`, …)".
4. **objectstack-ai#9553, nothing answers.** `base.zod.ts` said "No count of authored
`events` keys is stated here — objectui#9553 carries that census." No
commit landed that card. `40f34b4ba` only points at it, and the
`AGENTS.md` fix `7550728a6` names a different card. So the sentence now
reads "— a separate card carried that census". The census is not
restated, per AGENTS.md objectstack-ai#9.
5. **objectstack-ai#9585, the measurement's instrument.** The card measured the render
path NOT GATED, and no commit names it. But the commit that wrote both
citing sentences, `ee70287e4`, adds a pin that re-measures exactly that:
"NOT GATED: the renderer paints the very node the mirror refuses,
without parsing it".
- `disclosure.ts` now reads "(`ee70287e4`'s pin measures it NOT GATED)".
- Its own changeset, `8236-collapsible-open-intercept-retire.md`, whose
landing is `ee70287e4`, reads "(a pin in this change measures it NOT
GATED)".
6. **The runtime string.** Only the listed text moves.

| file | member | before | after |
|:--|:--|:--|:--|
| `types/src/zod/form.zod.ts` | `InputSchema.wrapperClass` `.describe()`
| "Classes on the wrapper div around the input and its label
(objectui#8072)" | "Classes on the wrapper div around the input and its
label" |

No test, doc or changeset quotes it: a whole-tree search for the old
string returns only the source line.
7. **objectstack-ai#8229 in `flex.tsx`.** "(objectui#8229, folded into objectui#7735's
ruling)" becomes "(the finding `8b7ea3945` reconciled, folded into
objectui#7735's ruling)". objectui#7735 answers 200 and stays.
8. **#13413's sentence.** "Precedent of the same shape:
objectstack#12009 / PR #13413." becomes "… objectstack#12009 /
objectstack `89448a52b`." objectstack#12009 answers 404 too, but it is a
qualified sister-repo number outside this batch's lists, so it is left
and listed (**Acceptance notes** 1).
9. **#20160 across a line break.** In
`10746-joined-report-clears-binding.md`, "(objectstack PR" ends one line
and "#20160: …" opens the next. They become "(PR" and
"objectstack#20160: …".

## The literal-anchor sweep (both test-pin classes, ruling `5861900779`)

- **Instrument.** Every string, template, numeric and regex literal in
all 4068 tracked test and script files (119016 distinct literals),
parsed with TypeScript.
- **Candidate filter.** A literal is a candidate only if it matches the
diff's removed lines with two lines of context, raw or
comment-flattened. That leaves 2839.
- **Test applied to each candidate.** Does its occurrence count DROP
between `9f0c84a44` and `2b3d2061a` in any of the 92 files this PR
changes, in raw text or in a comment-flattened form? 229 distinct
literals drop.
- **Every one is a generic token.** Digits and short numbers,
punctuation, single letters, `objectui#`, `objectui#1`, and whole-tree
scanners such as `/#\d+/` and `/objectui#\d+/`. The three `objectui#\d+`
matchers (`ActionRunner.disabledGate`, `registry-inputs-spec-parity`,
`catalog-gallery-render`) assert over their own test data and read no
changed file.
- **No specific anchor.** None is a batch number, a changed phrase, or
the changed `.describe()` string. So no test pins changed runtime text
(class one), and no source-reading test pins a changed comment or
docblock citation (class two).
- Test titles and comments that name these numbers are out of the card's
classes and stay (**Census**, out of scope).

## Held

**By the serial rule: nothing.** Re-mapped before the push, on 9 open
PRs: objectui#10910, objectstack-ai#10908, objectstack-ai#10907, objectstack-ai#10906, objectstack-ai#10901, objectstack-ai#10891, objectstack-ai#10777,
objectstack-ai#10278 and the release PR objectstack-ai#5400 (objectstack-ai#10902 and objectstack-ai#10904 had merged since the
claim).

Two of them share files with this PR. Their hunks were read against
their merge-bases; each file is identical at its merge-base and at
`9f0c84a44`, so the lines map directly.
- **objectui#10907, `types/src/zod/form.zod.ts`.** It inserts a refusal
constant before `InputSchema`, two tombstone members further down
`InputSchema`, and a comment in the `InputShorthandSchema` arm. This
PR's three edited lines (the `wrapperClass` comment and description, and
the arm's "shrank that row" comment) sit outside every one of its hunks
and their 3-line context.
- **objectui#10906, `metadata-admin/i18n.ts` and
`previews/ViewPreview.tsx`.** Its `i18n.ts` insertions are far from the
two `createDraft` comments, and its `ViewPreview.tsx` hunks are far from
the `options` fold docblock.

Trial merges with this head (`git merge-tree --write-tree`):
- clean for objectstack-ai#10910, objectstack-ai#10908, objectstack-ai#10907, objectstack-ai#10906, objectstack-ai#10901, objectstack-ai#10891 and objectstack-ai#10777;
- objectstack-ai#10278 (`eab4c8e52`) conflicts in `ObjectGrid.tsx`,
`plugin-grid/README.md` and `content/docs/plugins/plugin-grid.mdx`, and
conflicts identically against `9f0c84a44` alone;
- objectstack-ai#5400 (Version Packages) regenerates and is not a hold.

objectui#10891 shares no file with this PR.

## Changesets

- `.changeset/10803-dead-citation-sweep-sixth-batch.md`, EMPTY
frontmatter. It covers the comment-only edits in 15 released packages;
no published behaviour changes through them. It points at the second
file for `@object-ui/types`' runtime text.
- `.changeset/10803-sixth-batch-runtime-strings.md`,
`'@object-ui/types': patch`: the `wrapperClass` description loses its
pointer. No key, path, issue code, accept set, refusal or severity
moves.

## Proof of prose-only (C4), against `9f0c84a44`, on this head
`2b3d2061a`

- **Source.** Each of the 53 touched `.ts` / `.tsx` files was parsed at
`9f0c84a44` and at this head with TypeScript 6.0.3's `createSourceFile`,
and re-printed by `createPrinter({ removeComments: true })`.
  - 52 of 53 prints are identical.
- `form.zod.ts` is equal once the one listed substitution (**Special
cases** 6) is applied to the base print, matched once.
  - 0 parse diagnostics.
- Lit controls on the same instrument: editing a string literal moves
the print; re-spacing a comment does not.
- **Changesets.** The frontmatter block of every one of the 37 edited
changesets is byte-identical at `9f0c84a44` and this head (37 of 37, by
md5). The overwrite gate below agrees.
- **Scope of the diff:** 92 files, +175 / −140. That is 37 edited and 2
new changesets, and 53 non-test source files in 15 released packages. No
test file.

## Gates, on this head `2b3d2061a`

Each line is the gate's own verdict and exit code, captured by
redirect-then-`$?`.

- `node scripts/check-changeset-presence.mjs`, exit 0: "53 source
file(s) of 15 released package(s) changed, and this change declares 2
changeset(s): .changeset/10803-dead-citation-sweep-sixth-batch.md,
.changeset/10803-sixth-batch-runtime-strings.md."
- `pnpm changeset:check`, exit 0: "All workspace packages are in the
changeset fixed group." / "No changeset declares a `major` bump."
- `node scripts/check-changeset-overwrite.mjs` (report-only), exit 0: "2
changeset(s) added, 37 modified, 0 deleted". `declared at base` equals
`declares now` for 37 of 37.
- `pnpm check:changeset-claims` (report-only), exit 0:
- "Every one of those 1 address(es) either names the tree it was read
from, or points at a line this change does not move";
- "Every package declared across those 31 body(ies) is either not
negated …";
- the standing notice "75 pending changeset(s) describe a file this
change touches".
- Read against the diff: a pending changeset quoting a replaced pointer
would itself carry the dead number and sit in the census, which reads 0.
No pending changeset quotes the changed description.
- `pnpm check:control-bytes`, exit 0: "check-control-bytes: OK (scanned
9178 tracked text file(s); skipped 85 binary)."
- `pnpm check:new-line-citations`, exit 0: "VERDICT
new-cross-file-line-citations: 0 new citation(s), enforcement
report-only -> exit 0".
- `pnpm check:pending-changeset-literals`, exit 0: "No test source names
a pending changeset."
- Also run over the touched comments:
  - `pnpm check:installed-pin-claims`, exit 0 ("OK");
- `pnpm check:comment-mask-corpus`, exit 0 (1 disagreeing file, within
the ceiling objectui#7882 holds open);
- `pnpm check:handler-key-reads`, exit 0 ("every judged read is a
declared member of it").
- The governed-surface predicate over the 92 paths, exit 0: "NOT
GOVERNED — 92 path(s) checked against 5 governed surface(s); none
matched." Lit control `AGENTS.md`: exit 3.

**Tests**, through the shared verify lock, on `2b3d2061a`. Each is
`VERDICT command-exit 0`.
- `scripts/__tests__/`, the whole directory, whose whole-tree scanners
read the touched files: `Test Files 177 passed | 2 skipped (179)`,
`Tests 5322 passed | 2 skipped (5324)`. The two skipped files are the
network-escape fixtures that run only as a child.
- `packages/types/`, the whole package, whose runtime text moved: `Test
Files 264 passed (264)`, `Tests 5854 passed (5854)`. `pnpm --filter
@object-ui/types type-check`, exit 0.
- **The pins nearest the re-pointed text:**
- in `types`: `input-wrapper-class-mirrored-8072`, `zod-mirror-parity`,
`wrapper-class-declared-7722`, `zod-mirror-authors-no-defaults-7735`,
`collapsible-open-refusal-8236`, `tree-view-config-readers-8253` and
`layout-default-jsdoc-7361`;
- in `components`: `collapsible-open-intercept-8236`,
`registration-defaults-match-renderer-8229` and
`action-undeclared-keys-8648`;
- in `plugin-view`: `ViewSwitcher` and
`ViewSwitcher.viewTypeTotalsBothLegs-9943`;
  - in `plugin-kanban`: `laneCountHonesty-8307`;
  - in `plugin-grid`: `rowCrudAffordances` and `rowCrudEffectiveOps`.
  - Result: `Test Files  15 passed (15)`, `Tests  381 passed (381)`.
- No red leg: the sweep found no anchor to move, so there is no pin
whose old copy should fail.

**Declared narrowing.** NOT MEASURED locally:
- the full suites and type-check of the 14 other touched packages, and
eslint.
- Reason: the comment-stripped syntax tree of 52 of 53 touched source
files is identical to `main`, and the 53rd differs only by the listed
literal.
- CI runs the full farm.

## Acceptance notes

1. **A sister-repo class of the same defect: 30 `objectstack#N`
citations answer 404 in objectstack.**
- **Measurement.** Every distinct `objectstack#N` in the two in-scope
classes at the branch point is 359 numbers. Each was read once: 328
answer 200, 1 answers 301 (objectstack#14026 to objectui#10102), and 30
answer 404, each confirmed by a second read.
- **The 30:** objectstack-ai#5970 objectstack-ai#5976 objectstack-ai#6038 objectstack-ai#6124 objectstack-ai#6281 objectstack-ai#6331 objectstack-ai#6450 objectstack-ai#6483 objectstack-ai#6515
objectstack-ai#9933 objectstack-ai#9934 objectstack-ai#10354 objectstack-ai#10485 objectstack-ai#10695 objectstack-ai#11330 objectstack-ai#11507 objectstack-ai#11513 #11658 #11703
#11753 #11846 #12009 #12868 #13117 #13670 #16126 #17147 #17762 #17987
#18012.
- **Sites at this head:** 82 lines in 63 files, 26 of them changeset
lines.
- **Three of them sit in sentences this PR edits, and are left as they
are:**
- objectstack#11846, "(objectstack#11846, landed as PR
objectstack#12718)" in `6748-preview-mode-provenance-ratchet.md`;
     - objectstack#12009, in `base.zod.ts`'s precedent line;
- objectstack#12868, in `form-spec.ts`'s "RULED 2026-08-28
(objectui#6263 / objectstack#12868, …)".
- **Why left.** They were not in this batch's lists, and the class is
the seat's to scope. objectstack's own card for dead tracker citations
in its tree is objectstack#19123, whose landing `66e266c93` counts
#12868 among the dead numbers it measured. Triage item 3 would put a
dead number in these classes on this card. The fix shape measured here
for such a number is the objectstack commit, as with `c459da6bc`.
2. **The rest of the bare `objectstack-ai#3391` population.** Only the two sites the
claim names were re-qualified. 28 more in-scope lines in 10 files write
the apiMethods whitelist card as a bare `objectstack-ai#3391`, which resolves to the
unrelated objectui#3391:
   - `ObjectDataPage.tsx`, `ObjectView.tsx` (app-shell);
   - `managedBy.ts`;
- `MePermissionsProvider.tsx`, `PermissionContext.ts`,
`PermissionProvider.tsx`;
   - `fieldWriteGate.ts`;
   - `ImportWizard.tsx`, `ObjectGrid.tsx`;
   - `ListView.tsx`.

It is live, not a 404, so it is outside the family pin. Two of those
lines pair it with a bare 3546 that means objectstack#3546 ("detail/form
面的 edit/delete 按钮接入服务端 effective 操作集"), while objectui#3546 is an
unrelated i18n card. Carrier: none.
3. **A stale claim beside a re-pointed sentence.** The same
`base.zod.ts` paragraph says "AGENTS.md's abridged protocol sketch shows
`events?: Record…` and its action-system commandment authors one".
`7550728a6` removed both from `AGENTS.md`, so that sentence is now
false. It is not a citation, and this PR does not touch it. Carrier:
none.
4. **The C0 blind spot, for any later census.** The bare-number
instrument's lookbehind hides a number written after `/`, `-`, `.` or
`&` (`#13337/#13086`, `-objectstack-ai#2231`). This batch read the 17 such numbers no
batch had read, and #13086 was the only 404. A later census can narrow
the lookbehind to refuse only a word character, `#` or `&` before the
`#`, and drop URL fragments by hand.
5. **Filenames are not citations.** Pending changeset and test FILENAMES
carry several of these numbers. They stay, as in PRs objectui#10707,
objectstack-ai#10797, objectstack-ai#10854, objectstack-ai#10869, objectstack-ai#10875 and objectstack-ai#10892.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01DuWo5bdP9SdVebamn99GGk)_

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation package: app-shell package: i18n tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[seam -> repo:objectui] the 80% / 100% storage banner has no renderer — cloud serves the numbers and the verdict, nothing shows them

2 participants