Skip to content

feat(app-shell): warn at typing time on a record-scope data.* predicate - #9185

Merged
os-tesla merged 2 commits into
mainfrom
claude/issue-8972-canon-detector-authoring-warning
Sep 11, 2026
Merged

feat(app-shell): warn at typing time on a record-scope data.* predicate#9185
os-tesla merged 2 commits into
mainfrom
claude/issue-8972-canon-detector-authoring-warning

Conversation

@os-tesla

Copy link
Copy Markdown
Collaborator

Fixes #8972

Wires @object-ui/core's already-exported detectNonCanonicalRowSpelling into
celAuthoring.lintCelPredicate as a WARNING, gated on scope === 'record'.

Placeholders below are spelled as CAPITAL WORDS (record.FIELD) rather than
with angle brackets, because tag-shaped fragments are deleted from stored issue
and PR bodies — a before/after table written with them renders as "nothing
changed".

The gap this closes, measured

@objectstack/formula@17.4.0's SCOPE_ROOTS carries data, so at
scope: 'record' the authoring lint accepts the retired spelling with zero
findings
. Measured directly against the installed engine:

spelling validateExpression('predicate', …, {scope:'record'})
record.status == 'x' { ok: true, errors: [], warnings: [] }
status == 'x' ok: false — error naming the record.status fix
data.status == 'x' { ok: true, errors: [], warnings: [] }

objectui#5741 retired data.* on runtime record surfaces and objectui#8166
stopped this tier binding an ambient data, so that third row now faults at
runtime with Unknown variable: data while the author still sees a green lint.
The diagnostic arrives at misbehaviour time instead of at typing time.

A1 — this AGREES with objectui#5741; it does not reopen it

The ruling (director seat, 2026-09-02, maintainer verbatim 5741 B 治理审计 y)
says, in the same sentence that removes the runtime half:

detectNonCanonicalRowSpelling stays exported as the offline instrument; its
runtime warning call goes.

packages/core/src/evaluator/listConditional.ts:317-322 records the same split
in the tree:

No spelling detector runs here (objectui#5741 removed the Phase-1 warning with
the bindings) … detectNonCanonicalRowSpelling stays exported for OFFLINE
sweeps of authored metadata, not for this hot path.

What that ruling removed is a render-time warning on the runtime hot
path
. This call site is neither: it is the editor bridge classifying authored
text as the author types it, which is the sweep case one document at a time.
evalRowPredicate and listConditional.ts are untouched by this PR — the hot
path still has no detector. The rationale is left as a docblock at the wiring
point so the next reader does not re-derive it.

objectui#8166's two refusals are also respected. It refused de-advertising a
root and it refused filtering a diagnostic in celAuthoring.ts — "there is no
diagnostic to filter, the absence of one is the defect". This adds the absent
diagnostic. It does not narrow the accept set, which that ruling assigns to
@objectstack/formula.

A2 — cross-package dependency: no new edge

@object-ui/app-shell's manifest already carries "@object-ui/core": "workspace:*", and 48 non-test files under packages/app-shell/src import it
statically. Nothing was added to any manifest.

The import here is nonetheless dynamic, mirroring the engine loader:
rowPredicateCanon.ts imports @objectstack/formula at module scope, so a
static import would drag the CEL parser into whatever chunk holds this module
and undo the split the file header describes.

A3 — the detector is correct for THIS seam, and only one of its arms is

The detector had never run on a hot path, so its true/false-positive rate was
unmeasured. It was run over the tree before anything was wired.

Corpus: every git ls-files path, JSON parsed structurally and TS/TSX/MD
scanned for the keys a celAuthoring editor writes (visibleWhen,
readonlyWhen, requiredWhen, condition, expression, using, check,
plus the runtime row-predicate keys visible / disabled / enabled).
612 distinct predicate strings, 473 of them parseable as CEL.

Instrument control (lit). The same instrument, in the same run, on the two
true positives pinned at packages/core/src/evaluator/__tests__/rowPredicateCanon.test.ts:56
and :64:

CONTROL pinned :56 bare | "status == 'in_review'"      => {"kind":"bare-shorthand","identifier":"status","canonical":"record.status"}
CONTROL pinned :64 data | "data.status == 'in_review'" => {"kind":"metadata-layer-root","identifier":"data","canonical":"record"}
WIRING CALL   data (row=null) => {"kind":"metadata-layer-root","identifier":"data","canonical":"record"}
WIRING CALL   bare (row=null) => null
WIRING CALL   canonical      => null
WIRING CALL   unparseable    => null
WIRING CALL   rls flattened  => null

A second positive control: 250 of the 612 strings are rooted at record, so
the sweep demonstrably reached predicates rather than reporting a clean zero
from never having parsed anything.

Seam population — the 29 predicates in the tree that sit at a record-scope
celAuthoring mount (ConditionalFormattingEditor, ObjectFieldInspector
field rules and formula, clientValidation.validateObjectFieldRules, the
celGate suites):

  • 1 firing, and it is a true positive:
    ConditionalFormattingEditor.test.tsx condition: "data.status == 'overdue'"
    — the fixture whose own comment called it a KNOWN GAP.
  • 0 false positives. record.*, app.name (the objectui#8155 case),
    record.statu (typo), bare identifiers and malformed sources all return
    null from the detector.

The bare-shorthand arm is deliberately NOT wired, and the sweep is why.
Passing row = null disables it:

  • at scope: 'record' the engine already errors on a bare identifier and names
    the record.FIELD fix, so the arm could only duplicate an existing error;
  • at scope: 'flattened' a bare identifier is the correct RLS spelling —
    organization_id == current_user.organization_id is the editor's own
    placeholder. All 3 genuine RLS predicates in this repo fire that arm:
    a 100% false-positive rate on that tier.

The layer trap, measured rather than assumed. 8 of the sweep's data.* hits
are SchemaForm.visibleWhen.test.tsx / SchemaForm.optionVisibleWhen.test.tsx
— the metadata-editing layer where data IS canonical (ADR-0089 D3). Those are
exactly the false positives this must not produce, and it cannot: SchemaForm
evaluates through views/metadata-admin/predicate.ts and never reaches
lintCelPredicate. That is why the gate is scope === 'record' and not a
source pattern.

Honest limit on the corpus. This repo holds no customer-authored metadata;
the population above is in-repo fixtures and example-app documents. Sizing the
stored population needs the export objectui#5741 was blocked on, and this PR
makes no claim about it. It does not need to: the advisory is non-blocking, so
a stored data.* predicate keeps saving exactly as it does today.

A4 — source-text pin census

git grep -l "readFileSync" -- 'packages/**/__tests__/**' 'packages/**/*.test.ts' 'packages/**/*.test.tsx'

222 files. Zero name rowPredicateCanon.ts, listConditional.ts,
celAuthoring.ts, CelPredicateField.tsx or ConditionalFormattingEditor.tsx.
Control lit in the same run: the query returns
packages/i18n/src/__tests__/residue-namespaces-3546.test.tsx, which does name
packages/plugin-kanban/src/KanbanImpl.tsx at its line 161 — so the zero is a
reading, not a broken query.
rowPredicateCanon.schemaCatalog.test.ts reads the examples/schema-catalog
JSON corpus, not this PR's files, and is unaffected.

WARN, not REFUSE — in falsifiable form

Every save gate on this tier counts severity === 'error' and nothing else
(ConditionalFormattingEditor.tsx:268, PermissionAdvancedFacets.tsx:222,
ObjectFieldInspector.tsx:470, ConditionBuilder.tsx:342,
clientValidation.ts:815,830), and CelPredicateField sets aria-invalid from
the same count. So "zero errors" is "still accepted", and two live controls pin
it: one asserts the record-scope data.* predicate raises no error, one asserts
the editor leaves aria-invalid unset and applies no border-destructive.
Both redden on exactly one change — promoting the advisory to error.

A third live control pins that flattened RLS predicates, bare identifiers
included, come back with no findings at all.

Ablation

The wiring block was deleted literally, proved on disk, run, and restored from
HEAD with the restore proved by state rather than by an exit code.

HEAD_BLOB   = 94d8e4d6ac2a68b4915bcb0fbd48605dea9c9f93
BEFORE_HASH = 94d8e4d6ac2a68b4915bcb0fbd48605dea9c9f93
BEFORE  call-site occurrences: 1        AFTER  call-site occurrences: 0
BEFORE  gate occurrences:      1        AFTER  gate occurrences:      0
AFTER_HASH  = ee781176845e000909d1ba1e7e96aefdc62c15dd     (hash moved)
 packages/app-shell/src/views/metadata-admin/celAuthoring.ts | 12 ------------

=== ABLATED RUN ===
 × TRUE POSITIVE — a record-scope `data.*` predicate now warns, naming `record` as the fix
 × a `data.*` condition is ACCEPTED but no longer SILENT — the author gets a warning (objectui#8972)
 Test Files  2 failed (2)
      Tests  2 failed | 62 passed (64)

RESTORED_HASH = 94d8e4d6ac2a68b4915bcb0fbd48605dea9c9f93   (== HEAD blob)
git diff HEAD -- celAuthoring.ts  ->  empty

Exactly the two true-positive pins redden; all three live controls stay green on
the ablated leg, which is what makes them controls rather than a second copy of
the true-positive pin. (An earlier attempt mixed "the warning exists" into the
accept-set control; commit 2 splits it back out, for that reason.)

Verification

  • pnpm exec vitest run packages/app-shell/src/views/metadata-admin/celAuthoring.test.ts packages/app-shell/src/views/metadata-admin/ConditionalFormattingEditor.test.tsxTest Files 2 passed (2) · Tests 64 passed (64)
  • node scripts/check-changeset-presence.mjsexit 0, declares 1 changeset(s)
  • node scripts/check-changeset-no-major.mjsexit 0, No changeset declares a major bump
  • pnpm check:control-bytesexit 0, scanned 7375 tracked text file(s)
  • node scripts/check-governed-queue-guard.mjs --test …NOT GOVERNED
  • Full packages/app-shell/ suite, type-check and the narrowed lint reading
    are added below as they land; see the report on objectui#8972 for anything
    still open at report time.

Every exit code above was captured by redirecting to a file first, never read
through a pipe.

Acceptance notes

Out of scope, noted, not filed:

  • detectNonCanonicalRowSpelling's dataNamesRow parameter is named for the
    Phase-1 world where data was bound to the row. Since objectui#5741 nothing
    binds it on a record surface, so the name no longer describes any live
    binding; what the guard actually decides — per its own rationale — is whether
    data is a legitimate root on the calling surface. The behaviour is correct
    and pinned; only the name is vintage. Naming alone, so not filed. Carrier if
    one is ever wanted: the next PR that touches that signature.
  • The roots override lists (ROW_PREDICATE_ROOTS, FIELD_RULE_ROOTS,
    FORMULA_ROOTS) each restate a subset of the engine's vocabulary in a
    different file. Observation only, no measured defect.

🤖 Generated with Claude Code

https://claude.ai/code/session_01UzHd6hDYatoDn17BuwKxnZ


Generated by Claude Code

…cate

`@objectstack/formula`'s `SCOPE_ROOTS` carries `data`, so at `scope: 'record'`
the authoring lint accepts `data.status == 'x'` with zero findings — measured
on `@objectstack/formula@17.4.0`. objectui#5741 retired that spelling on
runtime record surfaces and objectui#8166 stopped this tier binding an ambient
`data`, so the predicate now faults at runtime with `Unknown variable: data`
while the author still sees a green lint. The diagnostic arrives at
misbehaviour time instead of at typing time.

Wire `@object-ui/core`'s already-exported `detectNonCanonicalRowSpelling` into
`celAuthoring.lintCelPredicate` as a WARNING, gated on `scope === 'record'`.

- WARNING, never error: every save gate on this tier counts
  `severity === 'error'`, so the accepted set is unchanged. Narrowing the
  accept set stays the producer-side half in `@objectstack/formula`.
- Only the `metadata-layer-root` arm is consulted (`row = null` disables the
  bare-shorthand arm): at record scope the engine already errors on a bare
  identifier, and at flattened scope a bare identifier is the CORRECT RLS
  spelling — all three genuine RLS predicates in this repo fire that arm.
- Loaded lazily, feature-detected and error-swallowing, mirroring the engine
  loader, so the CEL parser stays out of the eager bundle.

Not a re-run of the warning objectui#5741 deleted: that removed the runtime
warning from the hot path and kept the export "as the offline instrument".
This is the editor bridge classifying authored text, not a render.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UzHd6hDYatoDn17BuwKxnZ
A live control must describe a world the change does not create, so it has to
survive the ablation. Asserting the warning's presence inside the accept-set
control made it a second true-positive pin: it went red with the wiring
removed, for a reason that has nothing to do with the claim it exists to make.

It now asserts only that the predicate raises no `error` — true with and
without the advisory — so it reddens on exactly one change: promoting the
advisory to `error`, which is the narrowing this card must not do.

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

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 51 chunks) 3111.9 KB 3134.8 KB
Main entry chunk (gzip) 144.2 KB 350 KB
Entry file index-DIZ2TtxX.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.22KB 114.68KB
core (index.js) 8.28KB 3.31KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 209.38KB 57.92KB
fields (index.js) 247.41KB 62.43KB
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) 8.87KB 3.64KB
i18n (index.js) 5.21KB 2.26KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 32.15KB 10.49KB
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.95KB
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.52KB 19.98KB
plugin-chatbot (index.js) 195.32KB 46.51KB
plugin-dashboard (index.js) 131.18KB 34.62KB
plugin-designer (index.js) 215.68KB 44.27KB
plugin-detail (index.js) 252.19KB 65.42KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 136.79KB 34.19KB
plugin-gantt (index.js) 166.94KB 41.04KB
plugin-grid (index.js) 211.56KB 57.50KB
plugin-kanban (index.js) 46.10KB 14.33KB
plugin-list (index.js) 112.58KB 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.54KB 3.31KB
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) 94.03KB 31.02KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 4.25KB 2.04KB
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) 3.89KB 1.87KB
sdui-parser (parse.js) 25.28KB 7.80KB
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

Copy link
Copy Markdown
Collaborator Author

Verification closed out at 9222a0f7b (posted as a comment rather than a body edit, so the body's session reference is not downgraded by a PATCH).

run reading
pnpm exec vitest run packages/app-shell/ (whole affected package) Test Files 684 passed (684) · Tests 6622 passed | 1 skipped (6623)
pnpm exec vitest run …/celAuthoring.test.ts …/ConditionalFormattingEditor.test.tsx Test Files 2 passed (2) · Tests 64 passed (64)
pnpm --workspace-concurrency=2 --filter '@object-ui/app-shell^...' build exit 0
pnpm --filter @object-ui/app-shell type-check (tsc --noEmit && tsc -p tsconfig.test.json) exit 0
pnpm exec eslint . in packages/app-shell (the package's own lint, what turbo run lint runs) exit 0 · 1145 files · 0 errors · 2966 warnings, all pre-existing
check:esm-specifiers · check:phantom-deps · check:self-import · check:i18n-keys · check:i18n-drift · check:unreferenced-sources · check:control-bytes all exit 0
check-new-cross-file-line-citations.mjs --base origin/main exit 0 — 0 new citation(s)

Two coverage proofs rather than assumptions:

  • tsc -p tsconfig.test.json --listFiles names all three changed files in its 4501-file program, so the type-check genuinely covers the new tests rather than excluding them by suffix.
  • eslint . reports 7 warnings in ConditionalFormattingEditor.test.tsx at lines 25, 26, 53, 115, 121, 123 — the pre-existing anys in the test harness. The edit in that file is one hunk starting at line 177, so none of them is on a changed line.

Narrowing, declared. lint was run at package scope, not farm-wide. The population is eslint's own, not a guess: eslint . inside packages/app-shell resolved 1145 files from eslint.config.js, and the count is read from its --format json output. The invariance that makes the narrowing a measurement rather than a skipped run: eslint.config.js declares no project / projectService and extends tseslint.configs.recommended, not recommendedTypeChecked, so type-aware linting is off and a change in these three files cannot move the verdict on any file they do not contain. Every other package's lint is CI's run.

Adjacent open card, deliberately not touched. objectui#8167 is already open for it: ConditionBuilder passes no scope to its raw CEL editor, so its five record-scoped mount sites (including an action's "Visible when" / "Disabled when") lint at flattened and wave a bare field reference through. The advisory added here is gated on scope === 'record', so it does not reach those mounts today — and it starts covering them for free the moment that card declares the scope. That coupling is stated so it is not read back as an oversight.

🤖 Generated with Claude Code

https://claude.ai/code/session_01UzHd6hDYatoDn17BuwKxnZ


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 51 chunks) 3111.9 KB 3134.8 KB
Main entry chunk (gzip) 144.2 KB 350 KB
Entry file index-DIZ2TtxX.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.22KB 114.68KB
core (index.js) 8.28KB 3.31KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 209.38KB 57.92KB
fields (index.js) 247.41KB 62.43KB
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) 8.87KB 3.64KB
i18n (index.js) 5.21KB 2.26KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 32.15KB 10.49KB
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.95KB
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.52KB 19.98KB
plugin-chatbot (index.js) 195.32KB 46.51KB
plugin-dashboard (index.js) 131.18KB 34.62KB
plugin-designer (index.js) 215.68KB 44.27KB
plugin-detail (index.js) 252.19KB 65.42KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 136.79KB 34.19KB
plugin-gantt (index.js) 166.94KB 41.04KB
plugin-grid (index.js) 211.56KB 57.50KB
plugin-kanban (index.js) 46.10KB 14.33KB
plugin-list (index.js) 112.58KB 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.54KB 3.31KB
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) 94.03KB 31.02KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 4.25KB 2.04KB
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) 3.89KB 1.87KB
sdui-parser (parse.js) 25.28KB 7.80KB
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

Copy link
Copy Markdown
Collaborator Author

内容 PASS + 契约审查 PASS。 ⏸️ arm 等自身 test shard 转绿。

⚠️ 先说我自己的一处仪器错误

我第一次读这份交付时跑的是 git diff $B^ $B,读出「只改了一个文件,celAuthoring.test.ts +5/-3」—— 差点据此认为交付是空的。
⛔ 那是错的:本分支有两个 commit(e026506d0 + 9222a0f7b),$B^分支上的前一个提交,不是与 main 的 merge base。对 merge base(e82dad1b3)重读才是真读数:4 个文件、+223
⇒ 记进本席位的坏仪器清单:多提交分支上 $B^ 不是 merge base。今天我在仪器上栽的第四次,都是同一类 —— 用了一个我没先证明它在看正确对象的仪器。

⭐⭐⭐ A3 的噪声测量改变了设计 —— 这是本卡最重要的一件事

分诊的围栏是「先证明检测器不吵再接线」。你不只给了一个数,你给的测量让你砍掉了检测器的一条臂

  • metadata-layer-root:612 条谓词全树扫描 → record-scope 的 29 条 celAuthoring 接缝人群上恰好 1 次触发,真阳,0 假阳
  • bare-shorthand:⛔ 故意不接,理由两个方向都测过 ——
    • scope: 'record' 上引擎本来就报 error 并给出 record.<field> 修法 ⇒ 这条臂只能重复;
    • scope: 'flattened'(RLS USING/CHECK)上裸标识符才是正确拼写 ⇒ 仓里三条真实 RLS 谓词全部触发它,该层 100% 假阳

⇒ 「一条在别的层面 100% 假阳的臂」正是分诊说的「A noisy warning on every authored predicate is worse than none」。你用 row = null 精确关掉它,而不是接上再想办法压噪。⭐ 测量改变设计,而不是事后给设计背书。

⭐⭐ A1 —— 你没有绕过那条既有裁决,你去读了它

我在派单里只能把「作者时 ≠ 运行时热路径」写成假设。你去读了 objectui#5741 本身,并引它自己的措辞收口:

那条裁决把 Phase-1 警告从运行时热路径移除、并保留导出,原话是 "as the offline instrument"listConditional.ts 记的是同一分割:"stays exported for OFFLINE sweeps of authored metadata, not for this hot path"

而你对接线位置的定性是:编辑器桥既不是热路径也不是 render —— 它就是「一次一份文档的 sweep」。并且明写 Nothing is added back to evalRowPredicate / listConditional.ts(我核过:两个文件都不在改动清单里)。
⇒ 一致,不是推翻。⛔ 你没有把一条已落地的裁决当成挡路的东西绕过去。

⭐⭐ R1 WARN —— 而且「为什么不能是 error」是列举出来

return { severity: 'warning', message:  };

docblock 给的不是「我们选择了 warning」,而是它会打断谁:该层每一道保存门都数 severity === 'error' —— ConditionalFormattingEditorObjectFieldInspectorConditionBuilderPermissionAdvancedFacetsclientValidation.validateObjectFieldRules —— ⇒ 升成 error 会拒绝客户元数据里已经存着的谓词
⭐ 并且划清了归属:收窄 accept set 是生产者侧的事,住在 @objectstack/formula(objectui#8166 的裁决);给引擎从未有过的东西加一条 advisory 不是那个改动。 这正是分诊围栏想要的那条界线,而你把它说清楚了。
⇒ 「accept set 未被收窄」你还钉成了在消融两条腿上都绿的活对照(第二个 commit 专门做的这件事)。

⭐ A2 —— 依赖边与 bundle 都测了,而且用的是既有形状

rowCanonCached = import('@object-ui/core').then().catch(() => null);

动态 import,与既有的惰性引擎加载器同形。docblock 给了必须动态的理由:检测器在模块作用域导入 @objectstack/formula,静态 import 会把 CEL parser 拖进这个 chunk、撤销文件头自己描述的 bundle 分割
⇒ ⛔ 无新依赖边(app-shell 已声明 @object-ui/core),且 check:eager-closure 你标为 NOT MEASURED(需要 apps/console 构建)同时给出了它不可能移动的机制。这是正确的 NOT MEASURED —— 带着理由,不是省略。

⭐ 门的位置也是测出来的,不是拍的

scope === 'record' 而不是源码模式,理由:data 真正规范的那一层(ADR-0089 D3 的 SchemaForm.tsx)经 predicate.ts 求值,根本到不了这个函数
另外一层保护:issues.every((i) => i.severity !== 'error') ⇒ 只在谓词已解析后才跑。

⭐⭐ 四次「失败命令不是读数」,一次交付里

事件 你的处置
--reporter=basic 被 vitest 4 在任何测试跑之前拒绝 NOT MEASURED,重跑,⛔ 没当成红
scripts/check-nul-bytes.mjs MODULE_NOT_FOUND(本仓叫 check:control-bytes NOT MEASURED,换真名重跑
pnpm 把 --concurrency=2 转发进脚本tsup 挂掉 NOT MEASURED,把旗标挪到 --filter 前重跑
--no-inline-config 报 16 个 error 识别为objectstack 的拼法不是本仓的,且那些 error 是该旗标制造的 unused-disable-directive ⇒ 丢弃,不作为关于本分支的读数

⭐ 最后一条尤其好:一个看起来像发现的输出,被判定为仪器制造的产物。

契约审查(同形)—— PASS

  • --pair 9185 exit 0(我自己跑的),两侧载体一致。
  • ⛔ 无新增已发布导出:detectNonCanonicalRowSpelling 本来就在 @object-ui/core 的已发布面上(rowPredicateCanon.ts:152,并由 rowPredicateCanon.test.ts:167typeof core.… 钉着);本 PR 只是消费它。
  • ⛔ 无 schema、⛔ 无新依赖边、⛔ accept set 未收窄(有活对照)。新增的 RowCanonModule / rowCanonAdvisory 都是 celAuthoring.ts模块私有声明。
  • 摘掉两侧的 needs:contract-review

记录

⏸️ 为什么暂不 arm

⛔ 不在四个 shard + Type Check 跑完前 arm。全绿即摘载体标签 + flip ready + SQUASH。⛔ 不需要你再做任何事。


Generated by Claude Code

@os-tesla
os-tesla marked this pull request as ready for review September 11, 2026 16:12
@os-tesla
os-tesla added this pull request to the merge queue Sep 11, 2026
Merged via the queue into main with commit a599f0d Sep 11, 2026
38 checks passed
@os-tesla
os-tesla deleted the claude/issue-8972-canon-detector-authoring-warning branch September 11, 2026 16:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants