diff --git a/.changeset/6487-visibility-advice-per-tier.md b/.changeset/6487-visibility-advice-per-tier.md index 7750f26b92..3c10f6d887 100644 --- a/.changeset/6487-visibility-advice-per-tier.md +++ b/.changeset/6487-visibility-advice-per-tier.md @@ -29,11 +29,13 @@ prose that described it: byte-for-byte what it was. - **`'app-shell'`** — the chrome gate `ExpressionProvider.evaluateVisibility` runs, wired onto this reporter by objectui#6443. Its evaluator is built from - `{ current_user, user, ctx: { user }, os: { user }, app, data, features }`, so - the line now names `current_user` with its three ADR-0068 alias spellings, - `app`, and `features` — the deployment-flag root that provider documents for - exactly this kind of predicate — and states outright that `record` and - `page.` do not exist there. + `{ current_user, user, ctx: { user }, os: { user }, data, features }`, so + the line now names `current_user` with its three ADR-0068 alias spellings and + `features` — the deployment-flag root that provider documents for exactly + this kind of predicate — and states outright that `record` and `page.` + do not exist there. (That bag and that paragraph also carried an `app` root + when this entry was first written; objectui#8155 removed it from both before + either shipped, so the released message names five roots, not six.) **Why not generalise the copy instead.** Dropping the concrete root names would have made one paragraph true everywhere at the cost of making it useful nowhere: diff --git a/.changeset/7727-conditional-formatting-record-scope.md b/.changeset/7727-conditional-formatting-record-scope.md index c0ad022264..a137516d79 100644 --- a/.changeset/7727-conditional-formatting-record-scope.md +++ b/.changeset/7727-conditional-formatting-record-scope.md @@ -60,13 +60,34 @@ consumer aligns to it, rather than the engine growing a root to match this consu population cannot be measured from this repository.** In-tree usage is zero — swept across `packages/`, `apps/`, `examples/` and `content/` with a firing control — but metadata authored in real deployments lives outside this tree and no sweep here can -see it. Any predicate that reads `app.*` — a conditional-formatting condition, an -action `visible` / `disabled`, a field `visibleWhen` — stops resolving and, because -unresolvable visibility predicates **fail open**, will start reading as "yes" rather -than erroring. That is the accepted cost of the ruling, not an oversight. There is no -replacement root: `app` was never in the protocol. If you need a "current app" value in -a predicate, that is a spec/engine vocabulary widening to be filed (the producer-side -card, objectstack#16420, stays open as the record to reopen). +see it. Any predicate that reads `app.*` stops resolving. There is no replacement +root: `app` was never in the protocol. If you need a "current app" value in a +predicate, that is a spec/engine vocabulary widening to be filed fresh — the +producer-side card objectstack#16420 was closed `not_planned` by the same ruling +(2026-09-07), so there is no open record waiting for it. + +**What a stale `app.*` predicate does now depends on the surface — the direction is +NOT uniform, and two of them fail the safe way.** Measured per surface on the merged +head, each with a resolvable control predicate firing in the same run: + +| Surface | Entry point | A stale `app.*` predicate now | +|---|---|---| +| Conditional-formatting `condition` | `resolveConditionalFormatting` → `evalRowPredicate` (`fallback: false`) | **fails CLOSED** — the rule silently stops matching, no style is applied | +| Row/header action `visible` / `disabled` | `evalRowPredicate` (`fallback: false`) | **fails CLOSED** — the action is hidden / left enabled | +| Action `visible` on `action-button` / `action-menu` / `action-bar` | `useCondition(…, { throwOnError: true })` | **fails CLOSED** — hidden, with a one-time console warning | +| Action `visible` on `action-icon` / `action-group` | `useCondition` (default) | **fails OPEN** — the action is shown | +| Field `visibleWhen` (form field rules) | `resolveFieldRuleState` → `evalFieldPredicate` (fallback `true`) | **fails OPEN** — the field is shown | +| Field `visibleWhen` (app-shell object field) and nav / area `visible` | `isObjectFieldVisible` / `evaluateVisibility` | **fails OPEN** — shown, with a console diagnostic | +| Field `readonlyWhen` / `requiredWhen` | `resolveFieldRuleState` (fallback `false`) | **fails CLOSED** — not readonly, not required | + +So the cost is not one shape: on the fail-OPEN surfaces a gate that used to hide +something starts showing it, and on the fail-CLOSED surfaces a rule that used to fire +silently stops. Both are accepted costs of the ruling, not oversights — but they need +opposite checks after upgrading, which is why they are listed apart rather than +summarised. Every faulting predicate warns on the console; the app-shell diagnostic +names the roots this tier really binds, and objectui#8155's follow-up removed `app` +from that list so it no longer sends an author back to the root that is the reason +(`packages/react/src/utils/visibilityDiagnostic.ts`). `ExpressionProvider` still accepts an `app` prop and still publishes `app` on its React **context value**, which components read as a plain value (`DashboardView` does). Only diff --git a/.changeset/8155-app-root-residue-swept.md b/.changeset/8155-app-root-residue-swept.md new file mode 100644 index 0000000000..003ad90a02 --- /dev/null +++ b/.changeset/8155-app-root-residue-swept.md @@ -0,0 +1,36 @@ +--- +'@object-ui/react': patch +--- + +The published visibility diagnostic no longer tells an author that `app` is a bound +expression-scope root (objectui#8155 follow-up). + +`@object-ui/react`'s `SCOPE_TIER_ADVICE['app-shell']` — the paragraph +`reportUnresolvableVisibilityPredicate` prints in production when a predicate cannot be +evaluated — read "App-shell predicates bind `current_user` … plus `app` and `features`". +objectui#8155 removed `app` from `buildExpressionScope`, so that sentence was printed at +exactly the moment a saved `app.*` predicate faulted, and it answered "why did my +predicate not resolve?" by naming the root that is the reason. The line now names the +five roots the provider really binds: `current_user`, its `user` / `ctx.user` / `os.user` +aliases, and `features`. + +**Swept as a class, not as two coordinates.** Every other place in this tree that stated +`app` was a bound expression-scope root is corrected in the same change — the diagnostic +copy and its byte-pin, the ambient-scope docblocks in `@object-ui/react` +(`SchemaRenderer`, `useExpression`), `@object-ui/core` (`ActionRunner.ParamDef.visible`, +`RowPredicateOptions.scope`), `@object-ui/components` (`form.tsx`, `containers.tsx`), +`@object-ui/plugin-detail`, `@object-ui/plugin-form` (docblock and README), +`@object-ui/app-shell` and the console app, plus fourteen test fixtures that transcribed +the old bag with an `app` key. The fixtures in `@object-ui/app-shell` now call +`buildExpressionScope` instead of transcribing it, so that pair cannot drift again. + +Nothing that was true before objectui#8155 changed: the `app` prop on +`ExpressionProvider` and the `app` field on its React context value are untouched (they +were never CEL roots), and no root other than `app` was added to or removed from any +message, bag or fixture. + +Two release-bound corrections travel with it. The producer-side card objectstack#16420 +was closed `not_planned` on 2026-09-07 by the same ruling that removed the root, not left +open; and the consequence of a stale `app.*` predicate is **not** uniformly "fails open" — +it is per surface, and the changeset that carries the removal now states the seven +measured directions instead of one. diff --git a/apps/console/src/components/FormPage.predicateScope.test.tsx b/apps/console/src/components/FormPage.predicateScope.test.tsx index d2c79ae527..17664d205d 100644 --- a/apps/console/src/components/FormPage.predicateScope.test.tsx +++ b/apps/console/src/components/FormPage.predicateScope.test.tsx @@ -111,7 +111,7 @@ vi.mock('sonner', () => ({ toast: { success: vi.fn(), error: vi.fn() } })); */ function hostScope(positions: string[]) { const user = { id: 'u1', name: 'Kim', role: 'user', isPlatformAdmin: false, positions }; - return { current_user: user, user, ctx: { user }, os: { user }, app: {}, data: {}, features: {} }; + return { current_user: user, user, ctx: { user }, os: { user }, data: {}, features: {} }; } /** The SAME principal shape, one admitted by `GATE` and one refused by it. */ diff --git a/apps/console/src/components/FormPage.tsx b/apps/console/src/components/FormPage.tsx index 27c0ac100d..02e5a17373 100644 --- a/apps/console/src/components/FormPage.tsx +++ b/apps/console/src/components/FormPage.tsx @@ -1677,7 +1677,8 @@ export function FormPage({ mode, recordPath }: FormPageProps) { const isCreateForm = target.kind !== 'edit'; /** * The host shell's global predicate scope — `current_user` plus the ADR-0068 - * `user` / `ctx.user` / `os.user` aliases, `app`, `data`, `features` — read + * `user` / `ctx.user` / `os.user` aliases, `data`, `features` (⛔ no `app`: + * objectui#8155 unbound that root) — read * from the `PredicateScopeProvider` an `ExpressionProvider` mounts, and * threaded into all three evaluators below (objectui#6110). Same binding the * sibling renderer took in #6010, so one authored `visibleWhen` means one diff --git a/packages/app-shell/src/providers/ExpressionProvider.evaluateVisibility.test.ts b/packages/app-shell/src/providers/ExpressionProvider.evaluateVisibility.test.ts index 994f667e9d..5fae5a24ea 100644 --- a/packages/app-shell/src/providers/ExpressionProvider.evaluateVisibility.test.ts +++ b/packages/app-shell/src/providers/ExpressionProvider.evaluateVisibility.test.ts @@ -8,7 +8,7 @@ import { describe, it, expect } from 'vitest'; import { ExpressionEvaluator } from '@object-ui/core'; -import { evaluateVisibility } from './ExpressionProvider'; +import { buildExpressionScope, evaluateVisibility } from './ExpressionProvider'; /** * Regression: nav/area `visible` predicates arrive from the server as @@ -20,9 +20,14 @@ import { evaluateVisibility } from './ExpressionProvider'; * unimplementable from app metadata. */ +/** + * The provider's own bag, from the provider's own builder. It used to be a + * hand-written transcription carrying an `app` key; `buildExpressionScope` has + * not bound one since objectui#8155 (ruled 2026-09-07), and a copy is how a + * fixture keeps asserting a root the shipped builder no longer has. + */ function makeEvaluator(user: Record) { - const context = { current_user: user, user, ctx: { user }, os: { user }, app: {}, data: {}, features: {} }; - return new ExpressionEvaluator(context as any); + return new ExpressionEvaluator(buildExpressionScope({ user }) as any); } describe('evaluateVisibility', () => { diff --git a/packages/app-shell/src/providers/ExpressionProvider.tsx b/packages/app-shell/src/providers/ExpressionProvider.tsx index 3bed17d338..d48cc34b68 100644 --- a/packages/app-shell/src/providers/ExpressionProvider.tsx +++ b/packages/app-shell/src/providers/ExpressionProvider.tsx @@ -99,11 +99,13 @@ export interface ExpressionScopeInput { * * The ruling is that the engine's `SCOPE_ROOTS` is the contract and this * consumer aligns to it, NOT that the engine grows a root to match this - * consumer (option A, objectstack#16420, is explicitly not taken and stays - * open as the record to reopen should a real need for a "current app" root - * ever be measured). ⛔ The other refused route was suppressing the diagnostic - * in `celAuthoring.ts`: that is the lenient-fallback shape AGENTS.md #0.1 - * bans. + * consumer. Option A — widen the engine vocabulary — was the producer-side + * card objectstack#16420, and the same ruling CLOSED it `not_planned` + * (2026-09-07T04:16:22Z). Should a real need for a "current app" root ever be + * measured, it is a fresh spec/engine vocabulary widening; there is no open + * record waiting for it. ⛔ The other refused route was suppressing the + * diagnostic in `celAuthoring.ts`: that is the lenient-fallback shape + * AGENTS.md #0.1 bans. * * Every root below is one the engine accepts, so the three surfaces — what * this binds, what the editor advertises, what the linter admits — now agree. @@ -310,7 +312,9 @@ export function evaluateVisibility( // NODE tier's advice, telling an author whose nav predicate faulted to // check `record` and `page.` — two roots the bag built in // `ExpressionProvider` above does not contain at all — while the identity - // aliases, `app` and `features` that it DOES contain went unnamed. + // aliases and `features` that it DOES contain went unnamed. (`app` was in + // that list until objectui#8155 removed the root; the advice no longer + // names it either.) 'app-shell', ); diff --git a/packages/app-shell/src/providers/ExpressionProvider.visibleFaultDiagnostic.test.ts b/packages/app-shell/src/providers/ExpressionProvider.visibleFaultDiagnostic.test.ts index 4cb4d26f5e..853ae5ddbc 100644 --- a/packages/app-shell/src/providers/ExpressionProvider.visibleFaultDiagnostic.test.ts +++ b/packages/app-shell/src/providers/ExpressionProvider.visibleFaultDiagnostic.test.ts @@ -72,14 +72,24 @@ import { __resetVisibilityPredicateWarnings, } from '@object-ui/react'; import { hasVisibleNavigationItems } from '@object-ui/layout'; -import { evaluateVisibility } from './ExpressionProvider'; +import { buildExpressionScope, evaluateVisibility } from './ExpressionProvider'; /** The label this site puts in the reporter's `type` slot — the dedupe-key decision. */ const SURFACE = 'app-shell:visible'; +/** + * The provider's bag, taken from the provider's own builder rather than + * transcribed. It was a hand-written copy carrying an `app` key, and a copy is + * how a fixture goes on asserting a root the builder has stopped binding + * (objectui#8155) — `buildExpressionScope` is the single declaration, so the + * cells below measure the shipped bag instead of a snapshot of it. + */ +function makeScope(user: Record = { id: 'u1', positions: ['worker'] }) { + return buildExpressionScope({ user }); +} + function makeEvaluator(user: Record = { id: 'u1', positions: ['worker'] }) { - const context = { current_user: user, user, ctx: { user }, os: { user }, app: {}, data: {}, features: {} }; - return new ExpressionEvaluator(context as any); + return new ExpressionEvaluator(makeScope(user) as any); } type WarnSpy = { mock: { calls: unknown[][] } }; @@ -321,8 +331,8 @@ describe('objectui#6443 — the rate limit, measured in both directions', () => * The card #6443 made visible: once this site started printing, it printed the * NODE gate's closing paragraph, telling a nav author to check `record` and * `page.`. The bag `ExpressionProvider` builds is - * `{ current_user, user, ctx: { user }, os: { user }, app, data, features }` — - * it contains neither. + * `{ current_user, user, ctx: { user }, os: { user }, data, features }` — it + * contains neither. * * These cells are the END-TO-END half of the pin: the unit matrix in * `@object-ui/react` proves the two paragraphs differ, and these prove the @@ -347,19 +357,73 @@ describe('objectui#6487 — the line carries the APP-SHELL tier`s roots', () => it('it names the roots this provider really binds — including `features`', () => { // `features` is the deployment-flag root this provider's own docblock // documents for exactly this kind of predicate, and it was unnamed. - // Asserted against the bag `makeEvaluator` builds, which is a copy of the - // provider's: every root named below is a key of it. + // Asserted against the bag `makeEvaluator` builds, which IS the provider's + // (`buildExpressionScope`): every root named below is a key of it. const warn = spyWarn(); const evaluator = makeEvaluator(); evaluateVisibility('nosuchroot6487shellroots.x > 1', evaluator); const [line] = reports(warn); expect(line).toBeDefined(); - for (const root of ['`current_user`', '`user`', '`ctx.user`', '`os.user`', '`app`', '`features`']) { + for (const root of ['`current_user`', '`user`', '`ctx.user`', '`os.user`', '`features`']) { expect(line).toContain(root); } }); + /* ------------------------------------------------------------------------ * + * objectui#8155 — the COUPLING pin. The advice copy lives in + * `@object-ui/react` and the bag lives here, so neither package can pin the + * pair alone; this is the only seat that reaches both. Modelled on the + * three-sided pin PR #8164 left on `ConditionalFormattingEditor.test.tsx`. + * ------------------------------------------------------------------------ */ + + it('objectui#8155 — the printed advice does not name `app`, and the bag does not bind it', () => { + // Face 1 (copy, `@object-ui/react`) and face 2 (bag, this package) asserted + // in one run against the REAL line this surface prints. Re-adding `app` to + // the diagnostic string reddens the first expect; re-adding it to + // `buildExpressionScope` reddens the second. + const warn = spyWarn(); + + evaluateVisibility('nosuchroot8155copy.x > 1', makeEvaluator()); + const [line] = reports(warn); + expect(line).toBeDefined(); + expect(line).not.toContain('`app`'); + expect(Object.prototype.hasOwnProperty.call(makeScope(), 'app')).toBe(false); + }); + + it('objectui#8155 — every root the advice names is a root the bag really binds', () => { + // Face 3, and the one that makes the pair a fence rather than two lists. + // The defect this card cleans up was precisely an advertised root the bag + // did not bind, so the invariant — not the spelling — is what is pinned: + // re-adding `app` to the COPY alone (leaving the bag aligned to the + // engine) reddens here even though the census cell above is the one that + // names it. The reverse case, a root bound but not advertised, is legal + // and deliberate (`data`), so this direction is asserted and not the other. + const warn = spyWarn(); + const scope = makeScope(); + + evaluateVisibility('nosuchroot8155coupling.x > 1', makeEvaluator()); + const [line] = reports(warn); + expect(line).toBeDefined(); + + // Root names as the advice spells them, mapped to the key an author would + // have to be able to name for the advice to be true. `ctx.user` / `os.user` + // are member paths, so the root is the segment before the dot. + const advertised = ['current_user', 'user', 'ctx.user', 'os.user', 'app', 'features', 'record', 'data'] + .filter((root) => (line as string).includes(`\`${root}\``)) + .map((root) => root.split('.')[0]); + expect(advertised.length).toBeGreaterThan(0); // the line named SOMETHING + for (const root of advertised) { + // `record` is named only by the sentence declaring it ABSENT at this + // tier, which is the one advertised name that must NOT be a key. + if (root === 'record') { + expect(Object.prototype.hasOwnProperty.call(scope, root)).toBe(false); + continue; + } + expect(Object.prototype.hasOwnProperty.call(scope, root)).toBe(true); + } + }); + it('CONTROL: the first paragraph is UNCHANGED — it is true on this fail-open surface too', () => { // Green both ways, deliberately. Only the LAST paragraph is per-tier; the // "gate did NOT bite" sentence is true on every surface wired to this diff --git a/packages/app-shell/src/views/metadata-admin/inspectors/ConditionBuilder.tsx b/packages/app-shell/src/views/metadata-admin/inspectors/ConditionBuilder.tsx index c7261ca8d4..94761de738 100644 --- a/packages/app-shell/src/views/metadata-admin/inspectors/ConditionBuilder.tsx +++ b/packages/app-shell/src/views/metadata-admin/inspectors/ConditionBuilder.tsx @@ -121,8 +121,11 @@ export interface ConditionSubjectVocabulary { * bound through `evalFieldPredicate`'s `scope` extra. * * NOT included, on purpose: `data`, `os`, `app`, `features`, `input`, `vars`, - * `page`. Those are real roots at some surfaces, but this builder never offers - * them, and over-capturing there fails in the WRONG direction — `data.csv` is + * `page`. All but `app` are real roots at some surface — `app` is a root at + * none since objectui#8155 unbound it, and it stays on this list because the + * list is what this builder does not capture, not what exists. This builder + * never offers any of them, and over-capturing there fails in the WRONG + * direction — `data.csv` is * a plausible literal, and `data` IS bound, so reading it as a reference would * produce another silently-false predicate instead of a loud one. Which roots * a mounting surface actually binds is caller-supplied vocabulary diff --git a/packages/app-shell/src/views/metadata-admin/predicate.ts b/packages/app-shell/src/views/metadata-admin/predicate.ts index 22f014afb5..3a25139d18 100644 --- a/packages/app-shell/src/views/metadata-admin/predicate.ts +++ b/packages/app-shell/src/views/metadata-admin/predicate.ts @@ -259,9 +259,10 @@ export interface PredicateCtx { * RECORD; metadata-admin edits source metadata and has no record. Leaving it * unbound is what keeps `record.status` a loud diagnostic here instead of a * silent false — `predicate.test.ts` pins that. - * - ⛔ `app` / `features`. Renderer-tier, not ADR-0068 identity, and not named - * by the ruling. Binding them as empty objects would turn `app.x` from a loud - * unresolved-root warning into a silent `undefined`. + * - ⛔ `app` / `features`. `features` is renderer-tier, not ADR-0068 identity, + * and not named by the ruling; `app` is not a root at ANY tier since + * objectui#8155 unbound it. Binding either as an empty object would turn + * `app.x` from a loud unresolved-root warning into a silent `undefined`. */ export const IDENTITY_ROOTS = ['current_user', 'user', 'ctx', 'os'] as const; diff --git a/packages/components/src/renderers/form/__tests__/fieldtab-visiblewhen-6237.test.tsx b/packages/components/src/renderers/form/__tests__/fieldtab-visiblewhen-6237.test.tsx index 8e51abede8..cb334fa0c1 100644 --- a/packages/components/src/renderers/form/__tests__/fieldtab-visiblewhen-6237.test.tsx +++ b/packages/components/src/renderers/form/__tests__/fieldtab-visiblewhen-6237.test.tsx @@ -77,7 +77,7 @@ const UNBOUND_ROOT = cel("'sales_manager' in no_such_root.positions"); /** The host scope `ExpressionProvider` mounts, transcribed (see #6010's pin). */ function hostScope(positions: string[]) { const user = { id: 'u1', name: 'Kim', positions }; - return { current_user: user, user, ctx: { user }, os: { user }, app: {}, data: {}, features: {} }; + return { current_user: user, user, ctx: { user }, os: { user }, data: {}, features: {} }; } const DENIED = hostScope(['sales']); diff --git a/packages/components/src/renderers/form/__tests__/predicate-scope-parity-6010.test.tsx b/packages/components/src/renderers/form/__tests__/predicate-scope-parity-6010.test.tsx index 364d1d6993..f0ec2b13fe 100644 --- a/packages/components/src/renderers/form/__tests__/predicate-scope-parity-6010.test.tsx +++ b/packages/components/src/renderers/form/__tests__/predicate-scope-parity-6010.test.tsx @@ -113,7 +113,7 @@ const UNBOUND_ROOT = cel("'sales_manager' in no_such_root.positions"); */ function hostScope(positions: string[]) { const user = { id: 'u1', name: 'Kim', positions }; - return { current_user: user, user, ctx: { user }, os: { user }, app: {}, data: {}, features: {} }; + return { current_user: user, user, ctx: { user }, os: { user }, data: {}, features: {} }; } const DENIED = hostScope(['sales']); diff --git a/packages/components/src/renderers/form/__tests__/section-grouping-6236.test.tsx b/packages/components/src/renderers/form/__tests__/section-grouping-6236.test.tsx index 2524a78c40..5c4c2e304a 100644 --- a/packages/components/src/renderers/form/__tests__/section-grouping-6236.test.tsx +++ b/packages/components/src/renderers/form/__tests__/section-grouping-6236.test.tsx @@ -69,7 +69,7 @@ const UNBOUND_ROOT = cel("'sales_manager' in no_such_root.positions"); /** The host scope `ExpressionProvider` mounts, transcribed (see #6010's pin). */ function hostScope(positions: string[]) { const user = { id: 'u1', name: 'Kim', positions }; - return { current_user: user, user, ctx: { user }, os: { user }, app: {}, data: {}, features: {} }; + return { current_user: user, user, ctx: { user }, os: { user }, data: {}, features: {} }; } const DENIED = hostScope(['sales']); diff --git a/packages/components/src/renderers/form/form.tsx b/packages/components/src/renderers/form/form.tsx index 00b1869b41..7e3923618a 100644 --- a/packages/components/src/renderers/form/form.tsx +++ b/packages/components/src/renderers/form/form.tsx @@ -1265,8 +1265,10 @@ ComponentRegistry.register('form', // Global predicate scope (from the host shell's ExpressionProvider) — carries // `current_user` (plus the ADR-0068 D1 `user` / `ctx.user` / `os.user` aliases, - // `app`, `data`, `features`) so a `visibleWhen` can gate on role/context in + // `data`, `features`) so a `visibleWhen` can gate on role/context in // addition to sibling field values. Empty object when no provider is mounted. + // ⛔ No `app` root: objectui#8155 unbound it (the engine's `SCOPE_ROOTS` + // never declared one). // // ⛔ Declared HERE, above `readonlyFieldNames`, and not at its historical spot // ~75 lines down (#6010). It used to sit below, which is exactly why the diff --git a/packages/components/src/renderers/layout/containers.tsx b/packages/components/src/renderers/layout/containers.tsx index 1508dfc63e..0ba09c7583 100644 --- a/packages/components/src/renderers/layout/containers.tsx +++ b/packages/components/src/renderers/layout/containers.tsx @@ -1464,9 +1464,11 @@ const PageHeaderRenderer: React.FC = ({ schema, className, ...props }) => { // renders the related record's NAME. const predicateRecord = toPredicateRecord(ctx?.data, headerPredicateFields); return { - // The ambient host scope (`features` / `app` / `current_user` / …) binds + // The ambient host scope (`features` / `current_user` / …) binds // top-level, the way it does for a row predicate — the header used to - // expose it only under `ctx.*`. + // expose it only under `ctx.*`. (⛔ No `app` among them since + // objectui#8155 unbound that root; the `ctx.app` mirror below therefore + // reads whatever a host still passes, which in this repo is nothing.) ...(predicateScope && typeof predicateScope === 'object' ? predicateScope : {}), user: scopeUser, // Server-CEL-parity identity alias (#2358 trap 1): the spec's canonical diff --git a/packages/core/src/actions/ActionRunner.ts b/packages/core/src/actions/ActionRunner.ts index 6075fb7e7e..65f7481677 100644 --- a/packages/core/src/actions/ActionRunner.ts +++ b/packages/core/src/actions/ActionRunner.ts @@ -631,7 +631,8 @@ export interface ActionParamDef { placeholder?: string; /** * Visibility predicate (CEL) evaluated against the same scope as action - * `visible` (`current_user` / `app` / `data` / `features`). When it evaluates + * `visible` (`current_user` / `data` / `features` — ⛔ not `app`, unbound by + * objectui#8155). When it evaluates * false the param dialog omits this param — used to hide a param the backend * only accepts under an opt-in capability (e.g. `create_user.phoneNumber` * gated on `features.phoneNumber`). Absent = always visible. diff --git a/packages/core/src/evaluator/__tests__/listConditional.test.ts b/packages/core/src/evaluator/__tests__/listConditional.test.ts index 46f08b8fb9..60186c5503 100644 --- a/packages/core/src/evaluator/__tests__/listConditional.test.ts +++ b/packages/core/src/evaluator/__tests__/listConditional.test.ts @@ -246,7 +246,8 @@ describe('evalRowPredicate', () => { * the marker that routes dialects — something no author selects deliberately. * * No host injects a `record` key today (`ExpressionProvider` binds - * `current_user`/`user`/`ctx`/`os`/`app`/`data`/`features`), so this is a pin + * `current_user`/`user`/`ctx`/`os`/`data`/`features` — `app` was unbound by + * objectui#8155), so this is a pin * against a plausible future addition — `ctx.record` already exists — not a * live bug repro. It is one test on purpose: the two paths' precedence is a * single contract, and pinning them apart is what let them drift. diff --git a/packages/core/src/evaluator/listConditional.ts b/packages/core/src/evaluator/listConditional.ts index 62f0181b09..1d494e6e15 100644 --- a/packages/core/src/evaluator/listConditional.ts +++ b/packages/core/src/evaluator/listConditional.ts @@ -161,7 +161,7 @@ export interface RowPredicateOptions { * row-action `visible`/`disabled`; `false` (no style) for formatting. */ fallback?: boolean; /** Extra top-level scope merged alongside the row — e.g. the global predicate - * scope (`features` / `user` / `app`) a host shell provides. The row wins on + * scope (`features` / `user`) a host shell provides. The row wins on * collision: a `record` key here never shadows the row. Every OTHER key — * a host's own `data` included — reaches the predicate as the host's own * (objectui#5741: `data` no longer names the row on a record surface). */ diff --git a/packages/plugin-detail/src/renderers/__tests__/record-alert.rowBinding.test.tsx b/packages/plugin-detail/src/renderers/__tests__/record-alert.rowBinding.test.tsx index 1ccc62af62..4314993266 100644 --- a/packages/plugin-detail/src/renderers/__tests__/record-alert.rowBinding.test.tsx +++ b/packages/plugin-detail/src/renderers/__tests__/record-alert.rowBinding.test.tsx @@ -90,7 +90,6 @@ const APP_SCOPE = { user: { id: 'u1', email_verified: true }, ctx: { user: { id: 'u1' } }, os: { user: { id: 'u1' } }, - app: {}, data: {}, features: { multiOrgEnabled: true }, }; diff --git a/packages/plugin-detail/src/renderers/__tests__/record-alert.test.tsx b/packages/plugin-detail/src/renderers/__tests__/record-alert.test.tsx index 2c593b6a2d..620c7017f6 100644 --- a/packages/plugin-detail/src/renderers/__tests__/record-alert.test.tsx +++ b/packages/plugin-detail/src/renderers/__tests__/record-alert.test.tsx @@ -464,7 +464,7 @@ describe('record:alert — the `visible` verdict, real predicate pipeline (objec it('the ambient predicate scope reaches the banner — a `features.*` gate resolves both ways', () => { // The renderer passes only `{ record }`; everything else the header - // docblock promises (`features`, `user`, `app`, …) arrives through the + // docblock promises (`features`, `user`, …) arrives through the // ambient `PredicateScopeProvider` that host shells mount, merged UNDER the // local context by `useCondition`. Stubbing the predicate entry hid this // path entirely — a deployment-level gate could stop resolving with the diff --git a/packages/plugin-detail/src/renderers/__tests__/record-alert.visibleWhen.evidence.test.tsx b/packages/plugin-detail/src/renderers/__tests__/record-alert.visibleWhen.evidence.test.tsx index 4b8bd0a8ca..27baa4c83a 100644 --- a/packages/plugin-detail/src/renderers/__tests__/record-alert.visibleWhen.evidence.test.tsx +++ b/packages/plugin-detail/src/renderers/__tests__/record-alert.visibleWhen.evidence.test.tsx @@ -76,7 +76,6 @@ const APP_SCOPE = { user: { id: 'u1', email_verified: true }, ctx: { user: { id: 'u1' } }, os: { user: { id: 'u1' } }, - app: {}, data: {}, features: { multiOrgEnabled: true }, }; diff --git a/packages/plugin-detail/src/renderers/record-alert.tsx b/packages/plugin-detail/src/renderers/record-alert.tsx index dd5e2179ef..a11b10f9c9 100644 --- a/packages/plugin-detail/src/renderers/record-alert.tsx +++ b/packages/plugin-detail/src/renderers/record-alert.tsx @@ -43,7 +43,7 @@ * * Merged UNDER the row is whatever the host put in the ambient predicate * scope (`PredicateScopeProvider`; app-shell's `ExpressionProvider` supplies - * `current_user` / `user` / `ctx.user` / `os.user` / `app` / `data` / + * `current_user` / `user` / `ctx.user` / `os.user` / `data` / * `features`). The row wins, so a host-supplied `record` / `data` cannot * shadow it. `objectName` is NOT in the predicate scope — it is read from * `useRecordContext()` for the metadata lookup and the dismiss key only. diff --git a/packages/plugin-form/README.md b/packages/plugin-form/README.md index 8d0e7dc091..c13dd7f6a1 100644 --- a/packages/plugin-form/README.md +++ b/packages/plugin-form/README.md @@ -522,8 +522,9 @@ support table below. A tab may carry a `visibleWhen` predicate — the same slot, vocabulary and engine as the field-level rule (`string | { dialect?, source }`, a CEL predicate over the live record, evaluated by `@objectstack/formula` with the -host predicate scope bound, so it can read `current_user` / `app` / `data` / -`features` exactly as a field rule can). Like every conditional rule in this +host predicate scope bound, so it can read `current_user` / `data` / +`features` exactly as a field rule can — there is no `app` root, objectui#8155). +Like every conditional rule in this system it **fails open**: a predicate that cannot be evaluated leaves the tab visible rather than hiding data behind a broken expression. diff --git a/packages/plugin-form/src/WizardForm.tsx b/packages/plugin-form/src/WizardForm.tsx index 596aa97266..41f6813ee3 100644 --- a/packages/plugin-form/src/WizardForm.tsx +++ b/packages/plugin-form/src/WizardForm.tsx @@ -534,7 +534,7 @@ export const WizardForm: React.FC = ({ /** * The host shell's global predicate scope (`ExpressionProvider` → * `PredicateScopeProvider`) — `current_user` plus the ADR-0068 `user` / - * `ctx.user` / `os.user` aliases, `app`, `data`, `features`. Empty `{}` when + * `ctx.user` / `os.user` aliases, `data`, `features`. Empty `{}` when * no provider is mounted, which is the same "nothing to bind" the renderer * sees (objectui#6110). * diff --git a/packages/plugin-form/src/__tests__/sectionPredicateLayoutDiagnostic-6237.test.tsx b/packages/plugin-form/src/__tests__/sectionPredicateLayoutDiagnostic-6237.test.tsx index 8c24a3c667..09f2783a20 100644 --- a/packages/plugin-form/src/__tests__/sectionPredicateLayoutDiagnostic-6237.test.tsx +++ b/packages/plugin-form/src/__tests__/sectionPredicateLayoutDiagnostic-6237.test.tsx @@ -71,7 +71,7 @@ const GATE = cel("'sales_manager' in current_user.positions"); function hostScope(positions: string[]) { const user = { id: 'u1', name: 'Kim', positions }; - return { current_user: user, user, ctx: { user }, os: { user }, app: {}, data: {}, features: {} }; + return { current_user: user, user, ctx: { user }, os: { user }, data: {}, features: {} }; } /** * DENIED on purpose: the predicate resolves FALSE, which is the only state in diff --git a/packages/plugin-form/src/__tests__/sectionVisibleWhen-6111.test.tsx b/packages/plugin-form/src/__tests__/sectionVisibleWhen-6111.test.tsx index c2888e61d6..eb2bf373dd 100644 --- a/packages/plugin-form/src/__tests__/sectionVisibleWhen-6111.test.tsx +++ b/packages/plugin-form/src/__tests__/sectionVisibleWhen-6111.test.tsx @@ -146,7 +146,7 @@ const GATE = cel("'sales_manager' in current_user.positions"); /** The host scope `ExpressionProvider` mounts, transcribed (see #6010's pin). */ function hostScope(positions: string[]) { const user = { id: 'u1', name: 'Kim', positions }; - return { current_user: user, user, ctx: { user }, os: { user }, app: {}, data: {}, features: {} }; + return { current_user: user, user, ctx: { user }, os: { user }, data: {}, features: {} }; } const DENIED = hostScope(['sales']); diff --git a/packages/plugin-form/src/__tests__/tabbedFormSectionPredicate-6237.test.tsx b/packages/plugin-form/src/__tests__/tabbedFormSectionPredicate-6237.test.tsx index f1c923e395..0acffe03af 100644 --- a/packages/plugin-form/src/__tests__/tabbedFormSectionPredicate-6237.test.tsx +++ b/packages/plugin-form/src/__tests__/tabbedFormSectionPredicate-6237.test.tsx @@ -79,7 +79,7 @@ const GATE = cel("'sales_manager' in current_user.positions"); function hostScope(positions: string[]) { const user = { id: 'u1', name: 'Kim', positions }; - return { current_user: user, user, ctx: { user }, os: { user }, app: {}, data: {}, features: {} }; + return { current_user: user, user, ctx: { user }, os: { user }, data: {}, features: {} }; } const DENIED = hostScope(['sales']); const ALLOWED = hostScope(['sales_manager']); diff --git a/packages/plugin-form/src/__tests__/wizardRuledSemantics-6985.test.tsx b/packages/plugin-form/src/__tests__/wizardRuledSemantics-6985.test.tsx index 43b3b999b3..c3017ab798 100644 --- a/packages/plugin-form/src/__tests__/wizardRuledSemantics-6985.test.tsx +++ b/packages/plugin-form/src/__tests__/wizardRuledSemantics-6985.test.tsx @@ -56,7 +56,7 @@ const GATE = cel("'sales_manager' in current_user.positions"); * would REMOVE the step, i.e. where the drop is observable. */ function deniedScope() { const user = { id: 'u1', name: 'Kim', positions: ['sales'] }; - return { current_user: user, user, ctx: { user }, os: { user }, app: {}, data: {}, features: {} }; + return { current_user: user, user, ctx: { user }, os: { user }, data: {}, features: {} }; } const objectSchema = { diff --git a/packages/plugin-form/src/wizardPredicateScope.test.tsx b/packages/plugin-form/src/wizardPredicateScope.test.tsx index 083b1e66e1..7a62f29fa5 100644 --- a/packages/plugin-form/src/wizardPredicateScope.test.tsx +++ b/packages/plugin-form/src/wizardPredicateScope.test.tsx @@ -66,7 +66,7 @@ afterEach(() => { */ function hostScope(positions: string[]) { const user = { id: 'u1', name: 'Kim', positions }; - return { current_user: user, user, ctx: { user }, os: { user }, app: {}, data: {}, features: {} }; + return { current_user: user, user, ctx: { user }, os: { user }, data: {}, features: {} }; } const DENIED = hostScope(['sales']); diff --git a/packages/react/src/SchemaRenderer.tsx b/packages/react/src/SchemaRenderer.tsx index 7f9427de8f..4d187a16a2 100644 --- a/packages/react/src/SchemaRenderer.tsx +++ b/packages/react/src/SchemaRenderer.tsx @@ -559,7 +559,7 @@ export const SchemaRenderer: ForwardRefExoticComponent< > = forwardRef(({ schema, ...props }: SchemaRendererProps & ForwardedProps, _ref) => { const context = useContext(SchemaRendererContext); const dataSource = context?.dataSource || NO_DATA_SOURCE; - // Ambient host scope (user / app / features), fed by app-shell's + // Ambient host scope (user / features), fed by app-shell's // ExpressionProvider. Threaded into `visible`/expression evaluation so // component predicates can gate on the signed-in user & deployment flags. const predicateScope = usePredicateScope(); diff --git a/packages/react/src/__tests__/SchemaRenderer.concealmentGateFaultDiagnostic.test.tsx b/packages/react/src/__tests__/SchemaRenderer.concealmentGateFaultDiagnostic.test.tsx index 5c9ba8c49d..a92ff3b891 100644 --- a/packages/react/src/__tests__/SchemaRenderer.concealmentGateFaultDiagnostic.test.tsx +++ b/packages/react/src/__tests__/SchemaRenderer.concealmentGateFaultDiagnostic.test.tsx @@ -135,7 +135,6 @@ const Probe = () =>
; const APP_SCOPE = { current_user: { id: 'u1' }, user: { id: 'u1' }, - app: {}, data: {}, features: {}, }; diff --git a/packages/react/src/__tests__/SchemaRenderer.disabledGateFaultDiagnostic.test.tsx b/packages/react/src/__tests__/SchemaRenderer.disabledGateFaultDiagnostic.test.tsx index 204c76eb5e..bb9df6cda9 100644 --- a/packages/react/src/__tests__/SchemaRenderer.disabledGateFaultDiagnostic.test.tsx +++ b/packages/react/src/__tests__/SchemaRenderer.disabledGateFaultDiagnostic.test.tsx @@ -89,7 +89,6 @@ const Probe = (props: { disabled?: unknown }) => ( const APP_SCOPE = { current_user: { id: 'u1' }, user: { id: 'u1' }, - app: {}, data: {}, features: {}, }; diff --git a/packages/react/src/__tests__/SchemaRenderer.nodeGateDataPredicate.test.tsx b/packages/react/src/__tests__/SchemaRenderer.nodeGateDataPredicate.test.tsx index 5bf006c48a..8011c3dc2d 100644 --- a/packages/react/src/__tests__/SchemaRenderer.nodeGateDataPredicate.test.tsx +++ b/packages/react/src/__tests__/SchemaRenderer.nodeGateDataPredicate.test.tsx @@ -104,7 +104,6 @@ const cel = (source: string) => ({ dialect: 'cel', source }); const APP_SCOPE = { current_user: { id: 'u1', email_verified: true }, user: { id: 'u1', email_verified: true }, - app: {}, data: {}, features: {}, }; diff --git a/packages/react/src/__tests__/SchemaRenderer.productionPredicateFaultWarning.test.tsx b/packages/react/src/__tests__/SchemaRenderer.productionPredicateFaultWarning.test.tsx index 97460f05b8..7ab4a47b53 100644 --- a/packages/react/src/__tests__/SchemaRenderer.productionPredicateFaultWarning.test.tsx +++ b/packages/react/src/__tests__/SchemaRenderer.productionPredicateFaultWarning.test.tsx @@ -80,7 +80,6 @@ const Probe = () =>
; const APP_SCOPE = { current_user: { id: 'u1' }, user: { id: 'u1' }, - app: {}, data: {}, features: {}, }; diff --git a/packages/react/src/__tests__/SchemaRenderer.visibleWhenRecordBinding.test.tsx b/packages/react/src/__tests__/SchemaRenderer.visibleWhenRecordBinding.test.tsx index d00cbd64f9..84e6371497 100644 --- a/packages/react/src/__tests__/SchemaRenderer.visibleWhenRecordBinding.test.tsx +++ b/packages/react/src/__tests__/SchemaRenderer.visibleWhenRecordBinding.test.tsx @@ -83,7 +83,6 @@ const cel = (source: string) => ({ dialect: 'cel', source }); const APP_SCOPE = { current_user: { id: 'u1', email_verified: true }, user: { id: 'u1', email_verified: true }, - app: {}, data: {}, features: {}, }; diff --git a/packages/react/src/hooks/useExpression.ts b/packages/react/src/hooks/useExpression.ts index 320a8da7c4..458533199f 100644 --- a/packages/react/src/hooks/useExpression.ts +++ b/packages/react/src/hooks/useExpression.ts @@ -12,8 +12,10 @@ import { ExpressionEvaluator, evalRowPredicate } from '@object-ui/core'; /** * Global predicate scope — populated by host shells (e.g. app-shell's * `ExpressionProvider`) so renderers like action buttons can resolve - * deployment-level flags (`features`, `user`, `app`, …) without needing - * to import the host shell directly. + * deployment-level flags (`features`, `user`, …) without needing + * to import the host shell directly. (⛔ Not `app`: objectui#8155 removed that + * root from `buildExpressionScope`, so no host shell in this repo publishes + * one.) * * `useCondition` / `useExpression` merge this scope under the * locally-passed `context`, so per-row `record` overrides still win. diff --git a/packages/react/src/utils/visibilityDiagnostic.adviceTier.test.ts b/packages/react/src/utils/visibilityDiagnostic.adviceTier.test.ts index 31d3e41add..10f1503283 100644 --- a/packages/react/src/utils/visibilityDiagnostic.adviceTier.test.ts +++ b/packages/react/src/utils/visibilityDiagnostic.adviceTier.test.ts @@ -16,11 +16,19 @@ * `current_user`, `page.`. Since objectui#6443 the same reporter also * prints for the app-shell chrome gate, whose evaluator * (`ExpressionProvider.tsx`) is built from - * `{ current_user, user, ctx: { user }, os: { user }, app, data, features }`. + * `{ current_user, user, ctx: { user }, os: { user }, data, features }`. * There is no `record` and no `page` in that bag at all, so an author whose nav * or area `visible` faulted was sent to check two roots that cannot exist at - * their tier, while the identity aliases, `app` and `features` — which do — - * went unnamed. + * their tier, while the identity aliases and `features` — which do — went + * unnamed. + * + * ⛔ That bag quote carried an `app` root until objectui#8155 (ruled + * 2026-09-07) removed it from `buildExpressionScope`. This file's app-shell + * cells asserted `app` was NAMED in the advice, which is what held the false + * sentence in place after the root was gone; they now assert the opposite, and + * the sibling coupling pin in + * `packages/app-shell/src/providers/ExpressionProvider.visibleFaultDiagnostic.test.ts` + * reddens if the root returns to the BAG while the copy stays silent. * * ## Why the assertions sit where the tiers DISAGREE * @@ -173,25 +181,68 @@ describe('#6487 group 2 — app-shell tier', () => { it('names every root the provider actually binds and the node tier left out', () => { // Derived from `ExpressionProvider.tsx`'s own bag: - // `{ current_user, user, ctx: { user }, os: { user }, app, data, features }`. + // `{ current_user, user, ctx: { user }, os: { user }, data, features }`. const msg = appShellTier(); expect(msg).toContain('`current_user`'); expect(msg).toContain('`user`'); expect(msg).toContain('`ctx.user`'); expect(msg).toContain('`os.user`'); - expect(msg).toContain('`app`'); expect(msg).toContain('`features`'); }); it('still names CONCRETE roots — it is not the generalisation the card refused', () => { // The fence the triage put on this card, pinned rather than trusted: the // fix is not allowed to buy correctness at every tier by naming roots at - // none. Six concrete roots is measurably not "check whatever this surface + // none. Five concrete roots is measurably not "check whatever this surface // binds". const msg = appShellTier(); - const named = ['`current_user`', '`user`', '`ctx.user`', '`os.user`', '`app`', '`features`']; + const named = ['`current_user`', '`user`', '`ctx.user`', '`os.user`', '`features`']; expect(named.filter((root) => msg.includes(root))).toHaveLength(named.length); }); + + /* ------------------------------------------------------------------------ * + * objectui#8155 — `app` is not a bound root, so this paragraph must not name + * it. Three cells, each red for a different way of putting the sentence back, + * modelled on the three-sided pin PR #8164 left on + * `ConditionalFormattingEditor.test.tsx`. + * ------------------------------------------------------------------------ */ + + it('objectui#8155 — `app` is named NOWHERE in this tier`s advice', () => { + // The face that was false on `main` after PR #8164: this paragraph is + // printed at exactly the moment a saved `app.*` predicate faults, so + // naming `app` answered "why did this not resolve?" with the reason. + // Asserted on the whole message, not on one line, so re-adding the root to + // ANY sentence of this tier`s copy reddens here. + expect(appShellTier()).not.toContain('`app`'); + }); + + it('objectui#8155 — the paragraph is pinned by its TEXT, not by a root census', () => { + // A census (`not.toContain`) alone cannot tell "the root was removed" from + // "the paragraph was deleted", and the message is the deliverable here — + // it is published `@object-ui/react` output an author reads in production. + // Byte-exact, so a re-worded re-introduction ("plus the app metadata") that + // slips past the census above still reddens. + expect(appShellTier()).toContain( + 'App-shell predicates bind `current_user` - also spelled `user`, `ctx.user`\n' + + 'and `os.user` - plus `features` (the deployment flags).\n' + + 'Neither `record` nor `page.` exists at this tier.\n' + + 'Check those roots and the CEL syntax.', + ); + }); + + it('objectui#8155 CONTROL: the node tier`s paragraph never named `app` and is untouched', () => { + // The other half of the acceptance predicate — "nothing that was true + // before #8164 and unrelated to `app` was changed". This cell was green + // before the removal and stays green, so on its own it is evidence about + // nothing; it is here because a fix that reached the node tier's copy + // (which was already correct) would take it red. + expect(nodeTier()).not.toContain('`app`'); + expect(nodeTier()).toContain( + 'Page-component predicates bind `record` (the row on a record page),\n' + + '`current_user`, and page state as `page.`. Check those roots and the\n' + + 'CEL syntax.', + ); + }); }); /* -------------------------------------------------------------------------- * diff --git a/packages/react/src/utils/visibilityDiagnostic.ts b/packages/react/src/utils/visibilityDiagnostic.ts index 40e466e7d5..57b5bd6291 100644 --- a/packages/react/src/utils/visibilityDiagnostic.ts +++ b/packages/react/src/utils/visibilityDiagnostic.ts @@ -117,12 +117,20 @@ function predicateSourceText(raw: unknown): string { * does not promise and this paragraph therefore does not advertise). * * `'app-shell'` — the chrome gate. `ExpressionProvider.tsx` builds - * `{ current_user, user, ctx: { user }, os: { user }, app, data, features }`. + * `{ current_user, user, ctx: { user }, os: { user }, data, features }`. * There is no `record` and no `page` in it at all, which is the defect this * type exists to fix. The four identity spellings are the ADR-0068 aliases and * all four resolve; `features` is the deployment-flag root that provider's own * docblock documents for exactly this kind of predicate. * + * ⛔ No `app`. It was in that bag and in the paragraph below until + * objectui#8155 (ruled 2026-09-07): neither ADR-0068 nor + * `@objectstack/formula`'s `SCOPE_ROOTS` declares such a root, so + * `buildExpressionScope` stopped binding it. Naming it here would be worse + * than stale — this paragraph is printed at the exact moment a saved `app.*` + * predicate faults, so it would answer "why did my predicate not resolve?" + * with the root that is the reason. + * * `data` is bound at the app-shell tier but is deliberately NOT advertised * there: every mount of `ExpressionProvider` in this repo passes `data={{}}` * or omits it, so naming it would point an author at a root that answers @@ -153,7 +161,7 @@ const SCOPE_TIER_ADVICE: Record = { 'CEL syntax.', 'app-shell': 'App-shell predicates bind `current_user` - also spelled `user`, `ctx.user`\n' + - 'and `os.user` - plus `app` and `features` (the deployment flags).\n' + + 'and `os.user` - plus `features` (the deployment flags).\n' + 'Neither `record` nor `page.` exists at this tier.\n' + 'Check those roots and the CEL syntax.', }; diff --git a/packages/types/src/crud.ts b/packages/types/src/crud.ts index 92658bbc9c..e8a9665fa1 100644 --- a/packages/types/src/crud.ts +++ b/packages/types/src/crud.ts @@ -54,7 +54,7 @@ export interface ActionSchema extends BaseSchema { * neighbouring `ActionSchema` keys live read this one: in * `packages/core/src/actions/ActionRunner.ts` the spellings `action.level` * and `schema.level` have zero hits repo-wide, exit 1, with `action.method` - * as the firing control (`ActionRunner.ts:1787` and `:1793`). Until + * as the firing control (`ActionRunner.ts:1788` and `:1794`). Until * objectui#7735 the zod mirror's `.default('default')` substituted the value * into a parsed document; with that gone the tag described nothing that runs. * Whether the key should exist at all is the ADR-0049 liveness worklist's diff --git a/scripts/check-action-forward-parity.mjs b/scripts/check-action-forward-parity.mjs index f704bc1c6a..fe098a5396 100644 --- a/scripts/check-action-forward-parity.mjs +++ b/scripts/check-action-forward-parity.mjs @@ -269,7 +269,7 @@ export const JUSTIFIED = { "Consumed HERE, not owed to the runner. Every one of these renderers evaluates " + "`disabled` itself (through `hasDeclaredVisibilityGate` + `useCondition`) and greys " + "its own control, so the click the runner's own gate would refuse " + - "(ActionRunner.ts:773) cannot be made in the first place. Forwarding it would put " + + "(ActionRunner.ts:774) cannot be made in the first place. Forwarding it would put " + "the same predicate through a second evaluator on a path that is already closed " + "(objectui#3842 ruling on the declared-gate definition, applied by #3849).", issue: 4050, diff --git a/scripts/check-doc-example-types.mjs b/scripts/check-doc-example-types.mjs index f5bd567fb7..41e22dbc7b 100644 --- a/scripts/check-doc-example-types.mjs +++ b/scripts/check-doc-example-types.mjs @@ -923,7 +923,7 @@ export const UNGATED_EXAMPLES = { reason: 'usage fragment: references `User`, `setUser`, `useEffect`, which the example never declares', }, - 'packages/react/src/hooks/useExpression.ts:163 useExpression': { + 'packages/react/src/hooks/useExpression.ts:165 useExpression': { card: null, codes: [18004], reason: