Skip to content

fix(app-shell): the field-rule wrong-layer verdict comes from @objectstack/lint, not a second copy of it - #9366

Merged
claude[bot] merged 4 commits into
mainfrom
claude/issue-9318-rowcanon-verdict-from-lint
Sep 14, 2026
Merged

claude[bot] merged 4 commits into
mainfrom
claude/issue-9318-rowcanon-verdict-from-lint

Conversation

@os-tesla

@os-tesla os-tesla commented Sep 13, 2026

Copy link
Copy Markdown
Collaborator

Fixes #9440

rowCanonAdvisory answered "is this root bound on this surface?" from objectui's own
knowledge. @objectstack/lint publishes the same judgement. Two hand-maintained copies of
one verdict, agreeing today, with nothing keeping them agreeing.

Second prose repair — contract review returned FAIL again

Contract review 5659211993
found 12 of the 16 discharged and four not: the first repair replaced a false universal with a
closed six-root enumeration, which was false by a wider margin than the sentence it replaced.
⛔ The fix is not a better list. It is the original universal plus the one documented exception,
with nothing enumerated, so nothing goes stale when the root set moves.

Re-derived here, through the BUILT dist, over the whole candidate population.
FIELD_RULE_JUDGED_ROOTS and FIELD_RULE_AMBIENT_ROOTS are module-private at 17.4.0 (neither is
in @objectstack/lint's export list), so the judged set is reconstructed as
SCOPE_ROOTS + the ambient root, read from @objectstack/formula — a declared dependency of
this package, not a phantom one. SCOPE_ROOTS has 27 members; FIELD_RULE_BOUND_ROOTS is
record / previous / parent, all three of them in SCOPE_ROOTS; so the candidate set
JUDGED \ BOUND is 25. Sweeping all 25 through lintCelPredicate at scope: 'record' with
slot: 'visibleWhen', classifying each by verbatim equality with fieldRuleRootIssue's own
message: 24 advised · 1 blocked · 0 silent, and the blocked set is exactly ["app"].
Controls on the same instrument: data.status == 1 ⇒ one warning (lit);
record.status == 1 / previous.status == 1 / parent.status == 1[] (dark, and the
helper judges none of them); app.status == 1 ⇒ one error, no warning; zzz.x == 1 ⇒ one
error and fieldRuleRootIssue returns null, so an unjudged name was never a candidate.

The universal is now defended by an instrument rather than by prose. A new sweep in
celAuthoring.fieldRuleVerdict-9318.test.ts re-derives the candidate population on every run and
asserts silent = [], blocked = ['app'], advised = candidates − app; a second test pins
why app is the exception (SCOPE_ROOTS does not contain it, the helper judges it anyway).
⚠️ The reconstruction is a declared limit, written into the test: a NEW ambient root added
upstream would be invisible to the sweep until that literal moves, and nothing in this repo can
see it. Ablation — removing the error gate
(if (issues.every((i) => i.severity !== 'error') && hint.scope === 'record')
if (hint.scope === 'record'), proved on disk: anchor 1→0, mutant 0→1, blob
0120d42aba91c736bc96) reddens exactly those two tests
(Tests 2 failed | 10 passed (12)) with expected [] to deeply equal [ 'app' ] — the exception
itself disappearing, which is the predicted direction. Restored by hash equality to the HEAD
blob 0120d42aba with git diff HEAD empty, under trap … EXIT INT TERM.

Carriers, before → after, censused on the STORED artifacts (the built
dist/views/metadata-admin/celAuthoring.js, not the source; perl -0777 occurrence counts):
the six-root list 1→0, For the five further 1→0, five by hand 1→0,
`current_user` text ends 1→0; the replacements 0→1 each. In the changeset body:
the six-root list 1→0 and the slots the published vocabulary covers 1→0, replacements
0→1. Lit controls unmoved — ROW_PREDICATE_ROOTS 3→3 in the dist and 2→2 in the
changeset, METADATA_LAYER_ROOT 1→1 in the dist; dark controls strictly BROADER and
zzzNotPresentZZZ 0→0 in both. ⚠️ Escaping was checked rather than assumed before those
counts were trusted: the emitted comments carry `record` unescaped (10 occurrences,
backslash-escaped form 0), so an unescaped needle is the right instrument for this artifact.

⛔ Three statements the review confirmed TRUE are deliberately untouched: ROW_PREDICATE_ROOTS
is a six-entry local constant and "five roots the field tier does not bind" is correct for it —
that is a different claim from the advised set, and it is not repaired into a universal.

Prose repair — contract review returned FAIL (first round)

Contract review 5658789972
swept 60 claims across this PR's shipped comments, its changeset body and this description, and
found 16 false. ⭐ The bounding is load-bearing: @object-ui/app-shell builds with tsc
("build": "tsc && node ../../scripts/check-dist-completeness.mjs"), and nothing in its config
chain sets removeComments
packages/app-shell/tsconfig.json extends ../../tsconfig.json,
which has no extends key and no removeComments — so tsc's own default (false) stands and
comments reach the emit. ⚠️ An earlier draft of this line cited tsconfig.base.json:22 for that.
Measured: tsconfig.base.json is extended only by tsconfig.node.json, tsconfig.scripts.json,
tsconfig.react.json and examples/byo-backend-console/tsconfig.jsonnever by app-shell.
Right conclusion, wrong instrument; the instrument is corrected here and the conclusion is
additionally confirmed by reading the emitted bytes. files ships dist/** and CHANGELOG.md,
so the comments under repair are published artifacts, not notes.

The mechanism is unchanged. The repair commit moves comments, the changeset body and the
test narration only; the red-first block below still reproduces and the pins still pass. Three
of the sixteen were load-bearing rather than decorative:

  • the stated reason for taking the engine's message (see What changed) was false on both
    roots it named. Because it was the justification for a behavioural choice, the question
    underneath it was re-asked rather than the sentence reworded — the swap stands, for measured
    reasons, and the old one is withdrawn in place;
  • the two arguments for keeping the local fallback on uncovered surfaces (see Part 3) —
    the answer survives, both arguments were false, and the real asymmetry is written in their
    place;
  • the CI section named two green checks and missed the one real red.

Re-read after writing, from the built dist (⛔ not the source):
packages/app-shell/dist/views/metadata-admin/celAuthoring.js, perl -0777 occurrence census,
before → after — strictly WIDER 1→0, strictly NARROWER 1→0, stop advising 1→0,
EVERY root the field level leaves unbound 1→0; the four replacement anchors 0→1 each. Lit
control ROW_PREDICATE_ROOTS 3 before and 3 after (same file, same census, varying only
the claim); dark control strictly BROADER 0 both, so the zeros are absence and not a
broken read.

Re-measurement — both sides, at implementation time

The card instructs that the re-measurement wins over the card. Both sets of line numbers
moved; the exported shape did not, so the substance survives intact.

reading card (9c44eed4 / d7f0601) measured now
objectui refs to fieldRuleRootIssue / FIELD_RULE_BOUND_ROOTS under packages/ 0 0 at 2e471dc0a (git grep exit 1; control rowCanonAdvisory exit 0, 4 hits) — ⚠️ no longer 0 at this PR's head, see the note under this table
rowCanonAdvisory celAuthoring.ts:184-283 celAuthoring.ts:271 on objectui origin/main 2e471dc0a
FIELD_RULE_BOUND_ROOTS export validate-expressions.ts:688 :688 on objectstack origin/main 2b6a207
fieldRuleRootIssue export validate-expressions.ts:790 :790 on objectstack origin/main 2b6a207
what objectui actually consumes @objectstack/lint@17.4.0, installed; both symbols on the package root entry

⚠️ That zero is scoped to 2e471dc0a, and is false if read as a statement about this
PR's head.
Re-measured on the head tree, git grep -c FIELD_RULE_BOUND_ROOTS -- packages/
exits 0 and prints four files. Three are this PR's own. The fourth is not
packages/react/src/SchemaRenderer.tsx:844, a comment that was already on main at this
PR's merge-base (dfb585059, where that grep exits 0 with exactly that one file) and that
arrived here through this PR's merge of main. Drift in the reading, not a fabrication, and
nothing about the seam it was measuring has moved: the four files hold no call of either
symbol outside celAuthoring.ts.

One correction to the dispatch note, offered as a reading and not a complaint: the :680 /
:782 figures are the sibling checkout's local HEAD (86c5052, an ancestor of
origin/main), not origin/main itself. Against origin/main the card's own :688 /
:790 still hold. Either way the line numbers are informational — what this PR consumes is
the published dist of @objectstack/lint@17.4.0, where the declared shape reads
FIELD_RULE_BOUND_ROOTS: readonly ["record", "previous", "parent"] and
fieldRuleRootIssue(slot: string, source: string).

Part 3 — the helper has NO slot vocabulary, so coverage is objectui's call, and the fallback stays

This is the part the card most wants answered, so it is answered first and explicitly.

fieldRuleRootIssue judges against FIELD_RULE_BOUND_ROOTS — the FIELD-RULE tier,
record / previous / parent. The surfaces rowCanonAdvisory guards are everything that
reaches lintCelPredicate at scope: 'record'. Measured, they do not share one bound set:

surface slot roots objectui binds helper's verdict applies?
field inspector visibleWhen / readonlyWhen / requiredWhen the three *When keys FIELD_RULE_ROOTS = record, previous, parent yes — identical set
draft gate validateObjectFieldRules the same three keys same yes
field inspector formula expression expression, role value FORMULA_ROOTS = record no — strictly NARROWER
conditional-formatting condition condition ROW_PREDICATE_ROOTS = record, current_user, user, features, os, ctx no — five roots the field tier does not bind, but not a superset: no previous, no parent

⚠️ Neither set is comparable to the field-rule set — FORMULA_ROOTS is a proper subset of
it, and ROW_PREDICATE_ROOTS is not a superset, since it lacks previous and parent. The
two overlap on record alone. And fieldRuleRootIssue has no slot vocabulary of its own:
hand it any string and it judges against FIELD_RULE_BOUND_ROOTS and interpolates the name
into its message (measured on 17.4.0 — ('expression', 'current_user.x') and
('condition', 'current_user.x') each return a finding, not null). The helper never
declines a surface, so FIELD_RULE_VERDICT_SLOTS is objectui's own load-bearing answer, not a
formality.

What routing the two uncovered surfaces through it would actually cost — measured, and wrong
in opposite directions:

  • a conditional-formatting condition would be advised on current_user, user,
    features, os and ctx — five roots it binds. A false red: the author is told to
    rewrite a predicate that works there;
  • a formula expression would get previous.* / parent.* back clean, because the
    helper reports them bound at the field tier, where a formula binds only record. A false
    green — and the message it does print for other roots names previous and parent as
    available here, which on this surface is wrong prose as well as a wrong verdict.

Neither is a coverage shrink, and the earlier draft of this section said it was.
Measured on the head tree, both surfaces report nothing at all for previous.* / parent.*
today (lintCelPredicate("previous.status == 'x'", { scope: 'record', role: 'value' })[],
parent likewise), and @object-ui/core's METADATA_LAYER_ROOT is 'data' — the only root
objectui has ever advised on any surface. So nothing that is advised now would stop being
advised. The hazard is the wrong-direction verdict above, not a lost one.

So those two keep the local instrument, byte-for-byte as it behaves today, and each is pinned
as a live control that reddens if a later tidy-up routes them through the helper anyway.
Ablation leg B1 below performs that exact tidy-up and shows both controls going red.

PermissionAdvancedFacets (RLS USING / CHECK) and ConditionBuilder default to
scope: 'flattened' and were never guarded by this advisory at all.

What changed

CelSchemaHint.slot (and the matching CelPredicateField prop) names the authored key. On a
covered slot the verdict and the message are the helper's; everywhere else the local path
runs unchanged. Exactly one message ships per finding — never both.

Why the engine's message and not objectui's. ⚠️ An earlier draft of this section said
"the engine's message refuses that rewrite by name", of current_user and app. That
is false on both, and it was this PR's stated reason for a behavioural choice, so it is
withdrawn rather than reworded. Measured against the installed @objectstack/lint@17.4.0:

  • for current_user the engine's message prescribes the rewrite it was said to refuse —
    it contains "To gate on record state, rewrite the predicate against record.", one of
    three remedies it offers (the others being an option-level visibleWhen and field-level
    security). ⚠️ It does not end there, as an earlier draft of this line claimed: measured
    against 17.4.0, includes(…) is true and endsWith(…) is false — every covered
    slot's text closes on "it is not a fourth answer.";
  • for app the engine's message does refuse by name ("⛔ Do NOT write record.app") —
    and it is unreachable from this diff. lintCelPredicate('app.theme == "dark"', { slot: 'visibleWhen', scope: 'record' }) returns exactly one issue, a pre-existing
    error ("bare reference app … Write record.app."), and the advisory is gated behind
    issues.every((i) => i.severity !== 'error'), so the helper is never called.

The swap still stands, on reasons that survive measurement. The verdict widens, and
objectui has no message for most of what it now judges. Its local instrument produces a
sentence for exactly one root — @object-ui/core's METADATA_LAYER_ROOT, data. For
every other root a covered slot now reports there is no objectui sentence to keep; keeping
"objectui's message" there would mean hand-writing one per root, which is precisely the second
hand-maintained copy this card exists to delete — and they would have to be per-root, since the
engine's texts for data, the user-root family, the platform-wide family and the ambient family
are four different remedies, not one sentence with the root substituted. ⛔ That population is
deliberately not written down here — an earlier draft of this paragraph said "five further
roots", which was false — because it moves whenever the platform moves FIELD_RULE_JUDGED_ROOTS
or FIELD_RULE_BOUND_ROOTS. The sweep named above re-derives it on every run instead.

The one message genuinely swapped is data's, and objectui's tail there
"Re-root the reference on record" — is the half that does not generalise: right for
data, wrong for current_user, which is not a field of the record. Reading a verdict from
one authority and explaining it from another drifts the same way two verdicts do.

⛔ This changes nothing the diff does; it replaces the reason given for what it already
did.

Declared behaviour change. Those three editors now advise on every root the field level
leaves unbound — except app
. ⛔ No enumeration, and that is the point: any list goes stale
the next time the platform moves a root, and two earlier drafts of this line were false in turn —
first "every unbound root" with no exception at all, then a closed six-root list that named a
fraction of the population and omitted the rest. The exception is a mechanism, not a special
case: the advisory runs only once the predicate is error-free
(issues.every((i) => i.severity !== 'error')), and app is the single judged root the platform
does not declare (FIELD_RULE_JUDGED_ROOTS is SCOPE_ROOTS plus the ambient app), so for
app alone the pre-existing bare-reference error fires first and the advisory never runs — even
though the helper judges it and carries a bespoke message for it. A name the helper does not judge
at all is stopped by that same error and was never a candidate. Severity
stays objectui's own warning: every save gate on this tier counts severity === 'error', so
no accept set moves and nothing already stored in customer metadata is refused. The
data.status == 'x' case still reports at warning, never error.

Progressive enhancement is preserved throughout — the import() stays dynamic (a static one
would pull the lint bundle onto the eager console graph, objectui#5266), the export is
feature-detected, and a lint package without it falls through to the local instrument rather
than going quiet.

Not reopened

The three fences the card sets are untouched: objectui already warns on wrong-layer data.*
and still does; the advisory stays warning and is never promoted; the bare-shorthand arm
stays disabled (row = null) on the fallback path, and the helper path has no such arm at all.

Red-first, verbatim

The pin was written and run on the unmodified tree first:

 ❯ |unit| packages/app-shell/src/views/metadata-admin/celAuthoring.fieldRuleVerdict-9318.test.ts (10 tests | 3 failed)
     × TRUE POSITIVE — a covered slot takes BOTH the verdict and the message from `fieldRuleRootIssue`
     × ships ONE message, never both — objectui's sentence is gone from the covered path
     × takes the WIDER verdict on a covered slot: a root no field rule binds is advised too

AssertionError: expected [] to have a length of 1 but got +0
AssertionError: expected [ { severity: 'warning', …(1) } ] to deeply equal []
AssertionError: expected [] to have a length of 1 but got +0

 Test Files  1 failed (1)
      Tests  3 failed | 7 passed (10)

Ablation — three legs, all falsifiable, no null results

Every leg mutates on disk, proves the mutation reached disk before any result is read, and
restores by hash equality, never by an exit code. All ran under trap ... EXIT INT TERM.

Leg A — does the verdict really come from the published constant? Mutated the installed
@objectstack/lint@17.4.0 dist/index.js to drop parent from FIELD_RULE_BOUND_ROOTS.

PRISTINE_SHA=fef9fda4382de0c9e60582fca1b5b594eeebea16e27eee622ebdcc7f71d7c069
BEFORE orig_count=1 mut_count=0
AFTER  orig_count=0 mut_count=1
MUT_SHA=c488ed51ac8957cccf926137b892947326015059bb83647629322541acda91f7
MUTATED_VITEST_EXIT=1
RESTORED_SHA=fef9fda4382de0c9e60582fca1b5b594eeebea16e27eee622ebdcc7f71d7c069
RESTORE_PROVEN orig_count=1 mut_count=0
RESTORED_VITEST_EXIT=0

The reading: parent.status == "paid" at visibleWhen is immediately advised, carrying the
engine's own message ("visibleWhen reads parent, but a field-level conditional rule binds
only record …"
). That is the card's required demonstration — a root the platform DOES bind
comes back clean through the new path, and stops being clean the moment the platform stops
binding it. The file is not git-tracked, so the restore proof is sha256 equality against the
pristine snapshot plus the anchor counts, and git status on the worktree is empty.

Leg B1 — can the part-3 live controls fail? Replaced the slot condition with an
unconditional one, i.e. performed the exact tidy-up the card warns against.

HEAD_BLOB=220980448c9ab1f63598067d7d1c70e1db3b87d1
B1 BEFORE anchor=1 mutant=0
B1 AFTER  anchor=0 mutant=1 blob=e95e8a8882e749743f697cc915198fbacd5ffc49
B1 MUTATED_VITEST_EXIT=1
     × LIVE CONTROL — a formula `expression` still gets objectui's message, not the engine's
     × LIVE CONTROL — a conditional-formatting condition is NOT advised for the roots it binds
     × an unknown slot name falls back rather than guessing
B1 RESTORED_BLOB=220980448c9ab1f63598067d7d1c70e1db3b87d1
B1 RESTORE_PROVEN: git diff HEAD empty

Leg B2 — is the helper path actually reached? Emptied the covered-slot list.

B2 BEFORE anchor=1 mutant=0
B2 AFTER  anchor=0 mutant=1 blob=59fc8a89995ec849e65a81a2849195495b2e0afd
B2 MUTATED_VITEST_EXIT=1
     × TRUE POSITIVE — a covered slot takes BOTH the verdict and the message
     × ships ONE message, never both
     × takes the WIDER verdict on a covered slot
B2 RESTORED_BLOB=220980448c9ab1f63598067d7d1c70e1db3b87d1
B2 RESTORE_PROVEN: git diff HEAD empty

No leg was a null result. All three could fail and all three did.

Verification

Acceptance grep: git grep -c fieldRuleRootIssue packages/app-shell/src exits 0 with two
lines — …/celAuthoring.fieldRuleVerdict-9318.test.ts:4 and …/celAuthoring.ts:9. Both are
non-zero; an earlier draft transcribed only the second, and read :8 — correct then, stale now,
because this repair added one more mention of the symbol to that file's docblock.

objectui#8972's five pins are green and byte-unchanged: the pin file's blob hash equals its
origin/main blob (dbfc2108221cf22955927ac565804abdc3b4a1ed both sides), and
celAuthoring.test.ts reports Tests 39 passed (39).

Heavy runs went through the shared verify lock on slot os-dev-objectui-9318:

os-verify-lock: VERDICT batch-last-exit 0 · held 247s        # pnpm build (BUILD_EXIT=0)
                Tasks: 43 successful, 43 total
os-verify-lock: VERDICT command-exit 0 · held 1134s · waited 345s
                type-check=0        Tasks: 81 successful, 81 total
                app-shell-vitest=0  Test Files 693 passed (693) · Tests 6753 passed | 1 skipped

The one skip is pre-existing; no test was skipped, quarantined or loosened by this change.

⚠️ Those heavy figures are the implementation run, at the tree before the prose repair
they are not re-derived at the current head and should be read as such. Re-run after the
repair commit, through the same shared lock (slot os-dev-objectui-9440), all with the lock's
own VERDICT line as the verdict rather than a bare $?:

turbo run build --filter=@object-ui/app-shell --concurrency=2
    VERDICT command-exit 0    Tasks: 29 successful, 29 total
pnpm --filter @object-ui/app-shell run type-check      # tsc --noEmit && tsc -p tsconfig.test.json
    VERDICT command-exit 0
pnpm exec vitest run …/celAuthoring.fieldRuleVerdict-9318.test.ts …/celAuthoring.test.ts --reporter=verbose
    VERDICT command-exit 0    Test Files 2 passed (2) · Tests 49 passed (49)
    `apps/console` occurs 0 times in the verbose log, which names both files itself

Gates re-run on the repaired tree, each exit code captured before any pipe:
check:control-bytes 0, check:changeset-claims 0, check:new-line-citations 0,
check:test-path-roots 0, check:shell-escape-residue 0, check:comment-mask-corpus 0,
check-changeset-presence.mjs 0 ("5 source file(s) of 1 released package(s) changed, and this
change declares 1 changeset(s)"
), check-changeset-no-major.mjs 0.

Red-first still reproduces on the repaired tree. The four product files reverted to the
merge-base, the mutation proved on disk first (FIELD_RULE_VERDICT_SLOTS occurrences 5 → 0 in
celAuthoring.ts) and restored under trap … EXIT INT TERM by blob-hash equality against
HEAD, never by an exit code:

MUTATION_ON_DISK anchor 5 -> 0
    × TRUE POSITIVE — a covered slot takes BOTH the verdict and the message from `fieldRuleRootIssue`
    × ships ONE message, never both — objectui's sentence is gone from the covered path
    × takes the WIDER verdict on a covered slot: a root no field rule binds is advised too
 Test Files  1 failed (1)
      Tests  3 failed | 7 passed (10)
RESTORE  86a5199ad7… celAuthoring.ts · 0e6247b57f… CelPredicateField.tsx
         39e9975e70… clientValidation.ts · 448e1e8711… ObjectFieldInspector.tsx   (all OK)
         git diff HEAD: EMPTY

Gate family, derived by hand from package.json plus .github/workflows/ (this repo has no
dispatch-gates deriver), all exit 0: check:control-bytes, check:designer-field-key-parity,
check:i18n-keys, check:phantom-deps, check:unreferenced-sources, check:test-path-roots,
check:new-line-citations, check:changeset-claims, check:eager-closure,
check:vi-mock-specifiers, check:vi-mock-inherit, check:vi-mock-override-shape,
check:self-import, check-changeset-presence.mjs, check-governed-queue-guard.mjs --test
(NOT GOVERNED, 6 paths against 5 surfaces).

Lint was run over the whole tree rather than narrowed: pnpm exec eslint . --format json over
the population eslint's own config selects — 4920 files, 0 errors, 12879 pre-existing
warnings. The five touched files carry 0 errors; their 12 warnings are pre-existing
no-explicit-any and set-state-in-effect sites this change neither adds to nor moves.

Dependent-set membership read, done here rather than inherited. Three different lists in
this repo share the word "exclude" and are NOT the same set:

  • .changeset/config.json ignore = @object-ui/example-*, @object-ui/site,
    @object-ui/test-support — excluded from version bumping only;
  • the type-check dependent set of @object-ui/app-shell = @object-ui/console,
    @object-ui/example-byo-backend-console, @object-ui/example-console-starter — measured by
    reading every workspace manifest for a dependency edge on @object-ui/app-shell;
  • the root build's --filter=!@object-ui/site.

@object-ui/site sits in the first and the third and has no dependency edge on
app-shell, so it is not in the second at all; the genuine overlap between the first two is
@object-ui/example-*. pnpm type-check was run in full, so all three dependents are covered
regardless. @object-ui/app-shell is in the single fixed group of 40, so a changeset is
required and one is included (minor). ⚠️ major is not withheld "by group size", as an
earlier draft said: scripts/check-changeset-no-major.mjs refuses major for every
package in this repo irrespective of group, because objectui's major is pinned to
@objectstack's — and it is available, under OBJECTUI_ALLOW_MAJOR=1, for the one
synchronized release that follows objectstack across ITS major.

Clause-② — still no, verified rather than assumed

No exported signature of @object-ui/app-shell moves. CelSchemaHint, CelPredicateField,
celAuthoring, rowCanonAdvisory and FIELD_RULE_VERDICT_SLOTS each appear 0 times in
the built packages/app-shell/dist/index.d.ts, against a lit control (MetadataResourceRouter,
1 hit) proving the grep and the file, plus a dark control (MetadataResourceRouterZZZ, 0)
proving the same grep can return zero honestly. The package's exports map has two
entries — "." and "./styles.css" — not one, as an earlier draft said; the substance is
unchanged, since neither admits an importable subpath for these symbols. Nothing for me to
hang, and I have hung nothing.

CI — named instrument, not a frozen tally

⛔ An earlier draft of this section said main carried two red checks, Doc Snippet Type
Check
and Skill Example Check, inherited by every PR. Both halves are false. Re-derived
from GET /repos/objectstack-ai/objectui/commits/{sha}/check-runs (⛔ not
actions/runs?head_sha=):

sha what it is runs non-success
90b0bf7162 the head that draft described 36 1Bundle Analysis
dfb585059 this PR's merge-base (from git merge-base, not base.sha) 51 none
7ca6ddd4b5 main tip, read 2026-09-14T03:40Z 59 none completed

Doc Snippet Type Check and Skill Example Check read success at all three. They were
never red here, and the paragraph naming them is deleted rather than corrected.

The one real red is Bundle Analysis — job 103733469427 at 90b0bf7162, the
ui-components per-chunk headroom: 387.8 KB measured / 389.6 KB ceiling (headroom 1.9 KB = 0.02x the 89.0 KB regression, under the 0.10x floor …). The gate states its own reading:

This row's headroom is a standing debt that predates this change, and it moves under traffic
that has nothing to do with the chunk … So this verdict is NOT an accusation that your diff
spent the bytes … ⛔ There is therefore nothing here for this pull request to "fix", and the
two edits that would turn this green are both forbidden: ⛔ never raise the ceiling, and ⛔
never raise the allowance. Paying the row down is the open decision on the chunk … take it
there, and say on this pull request that you did.

Saying it here, as the gate asks: the ui-components row is being paid down on
objectui#9251 / PR objectui#9399 (both reachable). It is not this PR's to fix, this diff does
not touch that chunk, and ⛔ neither the ceiling nor the allowance is touched here. In the same
run the entry chunk (144.3 KB / 350 KB), the aggregate closure (3106.2 / 3134.8 KB) and
ceiling freshness all pass.

⛔ The table above is a reading at named shas, not a live tally — this PR has been pushed
to since. Re-derive at the current head rather than trusting these counts.

Acceptance notes

Noted while reading, deliberately not filed and not repaired here:

  • clientValidation.validateObjectFieldRules keeps only severity === 'error', so the
    advisory it now asks for is discarded the moment it is produced. That is correct for a draft
    gate and is stated in its own docblock; the slot is wired there anyway so the two
    scope: 'record' callers ask the same authority the same question. An observation, not a
    defect — nothing is wrong today and no reader is misled.
  • Upstream, surfaced by this repair and deliberately not acted on here. For app at a
    covered slot the author is told "bare reference appWrite record.app" by
    @objectstack/formula's bare-reference check, while @objectstack/lint's own message for
    the same root says "⛔ Do NOT write record.app: app is not a field on this object, so
    that spelling only trades this diagnostic for an unknown field error on app."
    The two
    published diagnostics contradict each other, and the error wins because it fires first. That
    is upstream of this repo and pre-existing — this diff neither creates nor worsens it, and
    ⛔ widening objectui's local path to paper over it is exactly the consumer-side workaround
    this card exists to stop. Recorded here for the seat; no card filed from this repair order.
  • FIELD_RULE_SLOT_CONSEQUENCE's slot vocabulary is module-private upstream, so a consumer
    cannot ask @objectstack/lint which slots it covers and must state its own answer, as this
    PR does in FIELD_RULE_VERDICT_SLOTS. A drift tripwire pins the bound set so the next
    platform move arrives as a red test here rather than as silence. Worth an upstream export
    one day; nothing is broken, so no card. Carrier for that observation: none — no queued PR or
    seat is touching that file.

Session: https://claude.ai/code/session_01UzHd6hDYatoDn17BuwKxnZ


Generated by Claude Code


Generated by Claude Code

…stack/lint, not a second copy of it

`rowCanonAdvisory` answered "is this root bound on this surface?" from objectui's
own knowledge: `@object-ui/core`'s `detectNonCanonicalRowSpelling` hard-codes the
single root `data`, and the docblock justified that from `ROW_PREDICATE_ROOTS` /
`FIELD_RULE_ROOTS` / `FORMULA_ROOTS`. The platform publishes the same judgement as
`fieldRuleRootIssue` / `FIELD_RULE_BOUND_ROOTS`. They agree today and nothing keeps
them agreeing.

`CelSchemaHint.slot` names the authored key, and on the slots the published
vocabulary covers — visibleWhen / readonlyWhen / requiredWhen — the verdict and the
message are now the helper's. Both symbols are module-internal to app-shell; no
package export moves.

Coverage is not shrunk to fit the helper: a formula `expression` binds
`FORMULA_ROOTS` (narrower) and a conditional-formatting `condition` binds
`ROW_PREDICATE_ROOTS` (wider), so both keep the local instrument, pinned as live
controls.

Part of #9318

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

Copy link
Copy Markdown
Contributor

⚠️ Console Performance Budget — gauge not trustworthy

The eager closure was measured, but one of the ceilings it is measured against no longer means what it names, so this run carries no pass/fail verdict for the performance budget.

This is not a budget violation. Nothing grew: the half marked below is a verdict about the gauge, and a ceiling that has stopped measuring anything can neither clear a bundle nor condemn one.

Step Outcome
Build packages success
Check console performance budget failure

Which half objected:

Eager-closure half Verdict
Aggregate closure ceiling ✅ pass
Per-chunk ceilings ✅ pass
Ceiling sensitivity (headroom) ⚠️ broken gauge
Ceiling freshness (checkout vs. base branch) ✅ pass

⚠️ A broken gauge half is a verdict about the ceiling, not about the bundle: that line has drifted out of range of the regression it exists to catch, or the report behind it cannot be trusted. It does not say anything grew. The Check console performance budget step log carries the ceiling and the number it was compared against.

Reason: The entry chunk measured 144.3 KB, but the eager-closure half of this gate returned no trustworthy VERDICT: the report could not be read, a ceiling has drifted out of range of the regression it must catch, or (objectui#6245) a ceiling was replaced on the base branch after this checkout was made. The step log says which. This is not a passing budget — and it is not a size regression either.

See the workflow run for details.


📦 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) 502.02KB 115.16KB
core (index.js) 8.52KB 3.41KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 211.58KB 58.68KB
fields (index.js) 247.89KB 62.50KB
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.22KB 2.26KB
i18n (pickLocalized.js) 9.86KB 3.95KB
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.83KB 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.25KB 13.99KB
plugin-charts (index.js) 71.51KB 19.97KB
plugin-chatbot (index.js) 195.34KB 46.51KB
plugin-dashboard (index.js) 131.22KB 34.59KB
plugin-designer (index.js) 215.94KB 44.33KB
plugin-detail (index.js) 253.46KB 65.85KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 136.77KB 34.17KB
plugin-gantt (index.js) 166.95KB 41.04KB
plugin-grid (index.js) 211.58KB 57.48KB
plugin-kanban (index.js) 46.01KB 14.30KB
plugin-list (index.js) 112.58KB 27.65KB
plugin-map (index.js) 20.64KB 6.86KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.41KB 11.93KB
plugin-timeline (index.js) 30.07KB 8.74KB
plugin-tree (index.js) 9.55KB 3.32KB
plugin-view (index.js) 84.42KB 20.79KB
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

…-from-lint

Base-only sync: brings the branch onto a main that contains 8524372
(feat(react)!: unbind the data-source adapter from the expression scope).
No file owned by this pull request is modified by this commit.

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

Copy link
Copy Markdown
Contributor

⚠️ Console Performance Budget — gauge not trustworthy

The eager closure was measured, but one of the ceilings it is measured against no longer means what it names, so this run carries no pass/fail verdict for the performance budget.

This is not a budget violation. Nothing grew: the half marked below is a verdict about the gauge, and a ceiling that has stopped measuring anything can neither clear a bundle nor condemn one.

Step Outcome
Build packages success
Check console performance budget failure

Which half objected:

Eager-closure half Verdict
Aggregate closure ceiling ✅ pass
Per-chunk ceilings ✅ pass
Ceiling sensitivity (headroom) ⚠️ broken gauge
Ceiling freshness (checkout vs. base branch) ✅ pass

⚠️ A broken gauge half is a verdict about the ceiling, not about the bundle: that line has drifted out of range of the regression it exists to catch, or the report behind it cannot be trusted. It does not say anything grew. The Check console performance budget step log carries the ceiling and the number it was compared against.

Reason: The entry chunk measured 144.3 KB, but the eager-closure half of this gate returned no trustworthy VERDICT: the report could not be read, a ceiling has drifted out of range of the regression it must catch, or (objectui#6245) a ceiling was replaced on the base branch after this checkout was made. The step log says which. This is not a passing budget — and it is not a size regression either.

See the workflow run for details.


📦 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) 502.02KB 115.16KB
core (index.js) 8.52KB 3.41KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 211.58KB 58.68KB
fields (index.js) 247.89KB 62.50KB
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.22KB 2.26KB
i18n (pickLocalized.js) 9.86KB 3.95KB
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.83KB 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.25KB 13.99KB
plugin-charts (index.js) 71.34KB 19.90KB
plugin-chatbot (index.js) 195.34KB 46.51KB
plugin-dashboard (index.js) 131.22KB 34.59KB
plugin-designer (index.js) 215.94KB 44.33KB
plugin-detail (index.js) 253.46KB 65.85KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 136.77KB 34.17KB
plugin-gantt (index.js) 166.95KB 41.04KB
plugin-grid (index.js) 211.66KB 57.50KB
plugin-kanban (index.js) 46.00KB 14.30KB
plugin-list (index.js) 112.58KB 27.65KB
plugin-map (index.js) 20.64KB 6.86KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.41KB 11.93KB
plugin-timeline (index.js) 30.07KB 8.74KB
plugin-tree (index.js) 9.55KB 3.32KB
plugin-view (index.js) 84.42KB 20.79KB
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) 96.00KB 31.71KB
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.04KB 5.36KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

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

@claude

claude Bot commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

⚠️ BLOCKED — this PR's card, objectui#9318, is unreachable: the issue 404s while its comments still resolve

domain:spec PM seat, 2026-09-13T15:4xZ. ⛔ Reported, ⛔ not routed around.

The reading, with its controls

GET /repos/objectstack-ai/objectui/issues/9318            -> HTTP 404   (twice, minutes apart)
GET /repos/objectstack-ai/objectui/issues/9318/comments   -> HTTP 200, 3 comments
        each comment's issue_url = .../objectstack-ai/objectui/issues/9318
GET /repos/objectstack-ai/objectui/issues/9366            -> HTTP 200   (control, same session/token)
GET /repos/objectstack-ai/objectui/issues/5734            -> HTTP 200   (control)
GET /repos/objectstack-ai/objectstack/issues/9318         -> HTTP 200   (a different repo's #9318; coincidental number)

⇒ the 404 is not a token, scope or transient problem: two controls in the same repo with the same
credentials answer 200, and the comment collection for this very number answers 200 with three
comments that name it as their parent. The issue object is gone; its comments are not.

Swept across every PR this seat still owes a review on — objectui#9362 / #9358 / #9351 / #9360 /
#9279 / #9343 / #9376 / #9399all eight cards answer 200. objectui#9318 is the only one. ⇒ an
isolated deletion, ⛔ not a systemic API fault.

What it blocks, concretely

  1. The dual-carrier rule cannot be satisfied. needs:contract-review hangs on the PR and the
    card. There is no card to hang it on ⇒ any gating here would be a permanent single-carrier state,
    which reads to the enqueue path as never-gated.
  2. This PR's body carries Fixes #9318 — a closing keyword aimed at an issue that no longer
    resolves.
  3. The authoritative layer is gone. This board's rule is 「⛔ GitHub 之外永不维护任何跟踪状态」, and the
    card was that state: triage's ruling, the grading, the pm:* half, the acceptance conditions.

⭐ What survives, salvaged here so it is not lost twice

The three orphaned comments still resolve and carry most of the working record:

  • 5651515871 — the domain:ui seat's claim, Clause-②: no, with the premise re-verified on
    both sides of the seam before dispatch and a lit control:

    reading card said measured then
    objectui refs to fieldRuleRootIssue / FIELD_RULE_BOUND_ROOTS under packages/ 0 0
    rowCanonAdvisory celAuthoring.ts:184-283 celAuthoring.ts:271
    FIELD_RULE_BOUND_ROOTS export validate-expressions.ts:688 :680
    fieldRuleRootIssue export validate-expressions.ts:790 :782
  • 5651747919 — the os-dev-report, status: done, pushed c931579b3f, premise still valid, with
    the zero backed by a lit control (rowCanonAdvisory 4 hits) so 「not used」 is a reading and not a
    broken grep.

  • 5652798679 — this seat's carrier repair adding the standalone Branch: line, part of the
    board-wide seat-template fault that hit 13 cards.

⇒ the subject of the card is 「rowCanonAdvisory re-derives a verdict @objectstack/lint already
publishes」, and its tier is default.

What this seat is doing, and not doing

⛔ Not filing a replacement card: re-filing would fork the history and the original may be
restorable — GitHub issue deletion is an admin action and this seat cannot see who took it or undo it.
⛔ Not stripping Fixes #9318 from the body, and ⛔ not reviewing or landing this PR while its carrier
is missing.

For the maintainer, two questions, both one word:

  1. Can objectui#9318 be restored? If yes, nothing else here needs doing.
  2. If not, should this seat re-file it from the salvaged record above, and does the new number
    replace the Fixes reference?

The PR itself is otherwise healthy: Fixes #9318, 6 files, 32 of 36 checks green, and its only red is
the board-wide ui-components Bundle Analysis debt being paid down on objectui#9251 / PR
objectui#9399.


Generated by Claude Code

@claude

claude Bot commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

Card rebuilt — this PR's Fixes reference now points at objectui#9440

Read at 2026-09-13T22:41Z.

What happened. The card this PR closes, objectui#9318, stopped being reachable after an
account suspension. The measurement, with its controls:

So the content was recoverable even though the card was not.

The rebuild. objectui#9440 was created from the salvage record — the original claim and
its three ⛔ fences, the os-dev report, and the carrier-repair comment, all fetched in full
from the comment endpoint and carried across verbatim, with the drifted line-number table
re-measured rather than copied. It carries domain:ui + pm:dispatched, assignee os-sam,
and deliberately no priority:* or type: the salvage does not carry the original's and
this seat does not grade — triage owns those fields.

This PR's body. Fixes #9318Fixes #9440, one occurrence, replaced. The two remaining
9318 strings in the body are left exactly as they are, on purpose — they are historical
names, not references:

  • the pin file packages/app-shell/src/views/metadata-admin/celAuthoring.fieldRuleVerdict-9318.test.ts
  • the verify-lock slot os-dev-objectui-9318

Renaming either would rewrite the test tree and the run record for a bookkeeping change. No
code, no test, no changeset is touched by this edit — body only. Readback: 13,656 bytes in,
13,656 bytes out, byte-identical to what was sent, Fixes #9440 × 1, Fixes #9318 × 0.

⚠️ If objectui#9318 becomes reachable again, this card is the duplicate — close
objectui#9440, not objectui#9318, and move the Fixes reference back. The same warning is
written into objectui#9440's body.

Next on this PR. needs:contract-review is now hung on both carriers (this PR and
objectui#9440) and the review is being dispatched. Nothing about the diff changed here, so the
reviewed head is unchanged at 90b0bf7162ab648265e6eab4263bba06eb1ffa99.


Generated by Claude Code

@claude

claude Bot commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

Contract review

Reviewed head: 90b0bf7162ab648265e6eab4263bba06eb1ffa99 (read at 2026-09-14T03:58Z)

① derived judgments

Accept sets. Two callers reach lintCelPredicate at scope: 'record', and they are the whole
reachable set: CelPredicateField (editor, display only) and clientValidation.validateObjectFieldRules
(draft gate, if (f.severity !== 'error') continue;). Every issue this diff can add is constructed at
exactly one site — rowCanonAdvisory, celAuthoring.ts — and both of its branches hard-code
severity: 'warning'. Re-measured on the restored head tree: celAuthoring.fieldRuleVerdict-9318.test.ts

  • celAuthoring.test.ts, Test Files 2 passed (2) / Tests 49 passed (49), apps/console occurring
    0 times in a --reporter=verbose log that names both files itself. ⇒ no accept set relaxes or
    narrows.

Public surface — Clause-②: no verified, not accepted. Against the BUILT
packages/app-shell/dist/index.d.ts: CelSchemaHint 0, CelPredicateField 0, celAuthoring 0,
rowCanonAdvisory 0, FIELD_RULE_VERDICT_SLOTS 0 — against the lit control MetadataResourceRouter,
1 hit, same file, same grep. The diff touches no barrel (src/index.ts is not among the six changed
files) and adds no export reachable from one. The tarball does ship
dist/views/metadata-admin/*.d.ts, but the exports map admits no subpath for it. ⇒ Clause-②: no
holds.

How the published-claim population was bounded (measured for THIS package, not assumed):

  • packages/app-shell/package.json "build": "tsc && …" — plain tsc, no bundler — and
    tsconfig.base.json:22 "removeComments": false. So comments reach the emitted .js and JSDoc
    reaches the emitted .d.ts. Measured with a control that hits: the pre-existing
    // Wrong-layer \data.*` advisory (objectui#8972)is present atdist/views/metadata-admin/celAuthoring.js:186, and CelSchemaHint.scope's JSDoc at dist/views/metadata-admin/celAuthoring.d.ts:49`.
  • files: ["dist", "src/styles.css", "README.md", "CHANGELOG.md", "LICENSE"]dist/** ships and
    CHANGELOG.md ships, so the changeset body is a customer-facing artifact. The new
    *.test.ts ships in neither (files omits it; packages/app-shell/tsconfig.json exclude keeps it
    out of the emitting program) — in scope here as in-repo published prose, not as a shipped byte.
  • Census of what that admits: added comment lines in the four shipped sources —
    celAuthoring.ts 94, ObjectFieldInspector.tsx 15, CelPredicateField.tsx 7,
    clientValidation.ts 5 = 121; plus the 33-line changeset; plus 142 added test-file lines; plus the
    PR body walked top to bottom.

Claims enumerated: 60 — A1–A16 shipped source comments, B1–B6 changeset/CHANGELOG, C1–C5
test narration, D1–D33 PR body. 16 are false, 3 are NOT MEASURED, 1 is true-but-under-transcribed.

A · comments that reach dist (16)

  • A1 an unrecognised slot is not an error, keeps the local fallback — slot:'someFutureWhen' on current_user.isAdmin[]true
  • A2 @objectstack/lint is the one @objectstack/* the console's vendor-objectstack group does not claim — VENDOR_OBJECTSTACK_TEST = /([\\/]node_modules[\\/]@objectstack[\\/](?!lint[\\/])|[\\/]@objectstack\+(?!lint@))/true
  • A3 securityPostureLint.ts keeps its own dynamic import — packages/app-shell/src/preview/securityPostureLint.ts:96 await import('@objectstack/lint')true
  • A4 a lint package without the export degrades to the fallback, never to an exception or silence — .catch(() => null) + fall-through; no pin exercises it — true
  • A5 ObjectFieldInspector offers exactly FIELD_RULE_ROOTS, which is FIELD_RULE_BOUND_ROOTS verbatim — ObjectFieldInspector.tsx:312 ['record','previous','parent'] vs installed lint@17.4.0 dist/index.d.ts:725 readonly ["record","previous","parent"]true
  • A6 a formula expression binds FORMULA_ROOTS = ['record'], strictly NARROWER — ObjectFieldInspector.tsx:320true
  • A7 "Taking the field-rule verdict there would stop advising previous.* / parent.* … a silent coverage shrink" — measured on the head tree: lintCelPredicate("previous.status == 'x'", {scope:'record', role:'value'})[], parent…[]. objectui's local instrument knows exactly one root — METADATA_LAYER_ROOT = 'data', packages/core/src/evaluator/rowPredicateCanon.ts:101 — so there is no previous.*/parent.* advising on ANY surface to stop, and nothing shrinks — FALSE
  • A8 a conditional-formatting condition binds ROW_PREDICATE_ROOTS, "strictly WIDER" — ConditionalFormattingEditor.tsx:97 gives record, current_user, user, features, os, ctx; previous and parent are absent, so it is not a superset of FIELD_RULE_BOUND_ROOTS; the two sets overlap on record alone. The consequence named (over-advising current_user) is real; the stated relation is not — FALSE
  • A9 pinned upstream by a test named for rejecting data, the legal root one layer over — objectstack packages/lint/src/validate-expressions.test.ts:1427 it('rejects \data` — the LEGAL root of this same key on a METADATA form')` — true
  • A10 the engine's message is taken because "Re-root … on record" is "actively wrong for current_user or app" / it "refuses that rewrite by name" — run against the installed 17.4.0: for current_user the engine's own message ends "To gate on record state, rewrite the predicate against record." — the same prescription. For app the covered path never reaches the helper: lintCelPredicate('app.theme == "dark"', {slot:'visibleWhen', scope:'record'}) returns one error, objectui's own bare-reference one — bare reference `app` … Write `record.app`. — and the advisory is gated behind issues.every(i => i.severity !== 'error'), so the ambient branch that does carry ⛔ Do NOT write `record.app` is unreachable from this diff. False on both named roots — FALSE
  • A11 covered slots "now advise on EVERY root the field level leaves unbound" — zzz.theme on visibleWhen ⇒ bare-reference error, no advisory; only FIELD_RULE_JUDGED_ROOTS (SCOPE_ROOTS + app) minus FIELD_RULE_BOUND_ROOTS are judged, and app is unreachable per A10. features.beta is advised — FALSE as written
  • A12 exactly one message per finding, never concatenated — early return on the helper branch; pin 2 green and red under the revert — true
  • A13 lintCelPredicate docblock: verdict from the helper on covered slots, local otherwise, always a warningtrue
  • A14 clientValidation: "Only ERRORS are kept below … changes nothing this gate reports today" — if (f.severity !== 'error') continue; at the site — true
  • A15 ×3 in ObjectFieldInspector: "FIELD_RULE_ROOTS above IS FIELD_RULE_BOUND_ROOTS; the formula editor above deliberately names no slot" — census of all six <CelPredicateField> sites: formula (role="value", FORMULA_ROOTS) no slot; three *When with slot; ConditionalFormattingEditor none; PermissionAdvancedFacets ×2 and ConditionBuilder none, and no scopetrue
  • A16 CelPredicateField.slot: leaving it unset "is the right answer for the surfaces whose bound roots are not the field-rule set" — true of the wiring; its stated reason rests on A7/A8 — true

B · changeset → shipped CHANGELOG.md (6)

  • B1 @object-ui/core's detector hard-codes the single root datarowPredicateCanon.ts:101true
  • B2 the platform publishes the same judgement, pinned upstream — true
  • B3 both internal; no package export moves — see ② — true
  • B4 "a field rule reading current_user or app gets the engine's own diagnostic, which refuses the record.<root> rewrite by name instead of merely omitting it" — same measurement as A10: current_user gets a message that prescribes the rewrite; app gets no engine diagnostic at all — FALSE
  • B5 severity stays warning; every save gate counts error; nothing stored is refused — true
  • B6 "Two guarded surfaces bind a different set, in opposite directions — (record, narrower) … (…, wider)" — false on the "wider" half (A8) and on the shrink the "narrower" half is said to imply (A7) — FALSE

C · test-file narration (5)

  • C1 detectNonCanonicalRowSpelling hard-codes ONE root (data) — true
  • C2 conditional-formatting condition … "WIDER" — see A8 — FALSE
  • C3 L109–111 "Routing this through the helper would silently stop advising previous.* / parent.*" — see A7 — FALSE
  • C4 L89–91 current_user is in the engine's baseline SCOPE_ROOTS, so validateExpression says nothing about it — current_user.isAdmin on the unslotted record surface ⇒ []true
  • C5 L54–56 the self-check "if the helper ever stops, this pin is measuring nothing" — true, and the strongest construction in the file

D · PR body, top to bottom (33)

  • D1 Fixes #9440 — card 200; GET /issues/9318 still 404 at this read (control 5654286772 → 200), so the card's precedence rule does not fire — true
  • D2 "objectui refs to fieldRuleRootIssue / FIELD_RULE_BOUND_ROOTS under packages/ … 0 (git grep exit 1)" — true at the revision the next row names (2e471dc0a: exit 1); at the reviewed head git grep -c FIELD_RULE_BOUND_ROOTS -- packages/ exits 0 and returns packages/react/src/SchemaRenderer.tsx:1, introduced by 852437297b and pulled in by this PR's own merge commit — false at the head (drift, not fabrication)
  • D3 rowCanonAdvisory at celAuthoring.ts:271 on 2e471dc0atrue
  • D4/D5 FIELD_RULE_BOUND_ROOTS :688, fieldRuleRootIssue :790 — objectstack packages/lint/src/validate-expressions.tstrue
  • D6 @objectstack/lint@17.4.0 installed, both symbols on the root entry — true
  • D7 declared shape readonly ["record","previous","parent"] / (slot: string, source: string)dist/index.d.ts:725, :741true
  • D8 part-3 rows 1–2 (the three *When keys, helper applies) — true
  • D9 part-3 row 3 (formula, FORMULA_ROOTS, "strictly NARROWER") as a set relation — true
  • D10 part-3 row 4 ("strictly WIDER") — see A8 — FALSE
  • D11 "taking the field-rule verdict on a formula would stop advising previous.* / parent.* … a silent coverage shrink, exactly what the card forbids" — see A7 — FALSE
  • D12 taking it on a CF condition would tell an author to rewrite current_user.isAdmin, which works there — true
  • D13 PermissionAdvancedFacets and ConditionBuilder default to scope: 'flattened', never guarded — neither passes scope; 'flattened' is the documented default; the gate is hint.scope === 'record'true
  • D14 "The engine's message refuses that rewrite by name" — see A10 — FALSE
  • D15 "those three editors now advise on every root the field level leaves unbound" — see A11 — FALSE as written
  • D16 severity stays warning; data.status == 'x' never errortrue
  • D17 the import() stays dynamic, feature-detected, falls through — true
  • D18 the three fences untouched — data.* still warned; never promoted; detectNonCanonicalRowSpelling(source, null, true) keeps row = null; the helper path has no bare-shorthand arm — true
  • D19 "Red-first, verbatim" — reproduced: the four product files reverted to the merge-base in my own worktree, pin re-run ⇒ Tests 3 failed | 7 passed (10) with exactly the three named tests failing. Restored by blob equality (220980448c9ab1f63598067d7d1c70e1db3b87d1 both sides), git status empty — true
  • D20 ablation legs A / B1 / B2 — not re-run here (they mutate node_modules and the tree) — NOT MEASURED, neither a pass nor a red
  • D21 "Acceptance grep … exits 0 with …/celAuthoring.ts:8" — the command emits two lines at this head: …/celAuthoring.fieldRuleVerdict-9318.test.ts:4 and …/celAuthoring.ts:8. A newline-tolerant perl -0777 occurrence census agrees with both line counts (4, 8), so no folding error — true but under-transcribed
  • D22 objectui#8972's five pins green and byte-unchanged, blob dbfc2108221cf22955927ac565804abdc3b4a1ed, Tests 39 passed (39) — blob equal at head and origin/main; the objectui#8972 describe holds exactly 5 it(); the file holds 39, and my run reports 39 of the 49 — true
  • D23 build 43 tasks / type-check 81 / Test Files 693 passed (693) / 6753 tests — not re-derived (heavy); corroborated by Test (shard 1..4/4), Type Check, Build & E2E all success at the pinned head. 694 tracked *.test.ts(x) under packages/app-shell, one held out by the root config's built-artifact lane — consistent — NOT MEASURED, corroborated
  • D24 the 15-name gate family "all exit 0" — the 13 check:* names all exist in the root package.json, and "this repo has no dispatch-gates deriver" is true (0 matching files in objectui); exit codes not re-run — NOT MEASURED
  • D25 lint: five touched files 0 errors / 12 warnings, pre-existing no-explicit-any and set-state-in-effectre-measured true: 5 files, 0 errors, 12 warnings — 11 @typescript-eslint/no-explicit-any in ObjectFieldInspector.tsx, 1 react-hooks/set-state-in-effect in CelPredicateField.tsx. Whole-tree 4920/12879 not re-derived
  • D26 .changeset/config.json ignore = @object-ui/example-*, @object-ui/site, @object-ui/test-support; @object-ui/site has no edge on app-shell — true
  • D27 app-shell is in the single fixed group of 40 — 1 group, length 40, contains @object-ui/app-shelltrue
  • D28 "major is unavailable to a fixed group of that size" — major is refused by scripts/check-changeset-no-major.mjs for every package irrespective of group size, because objectui's major is pinned to @objectstack's, and it IS available under OBJECTUI_ALLOW_MAJOR=1 for the synchronized release. Right conclusion, false rationale
  • D29 Clause-② paragraph: five symbols 0 hits in the built dist/index.d.ts, control 1 — re-measured true
  • D30 "The package's exports map has a single entry." — it has two: "." and "./styles.css". The substance (no importable JS/TS subpath) survives; the sentence does not — FALSE
  • D31 "Inherited red, not mine — main carries two red checks, Doc Snippet Type Check and Skill Example Check — inherited by every PR whose CI runs against it. They are not this PR's." — FALSE four ways. At the pinned head both are success; at the merge-base dfb585059 both are success; on main both are success for the last 8 commits; and Skill Example Check was already success on this PR's own first commit c931579b3f and on main at 69aa9c017f, so it was never red here at all. Meanwhile the one check that IS failure at the pinned head — Bundle Analysis (ui-components per-chunk headroom, job 103733469427) — is named nowhere in the body, and is success on main today, so it is not "inherited" in the sense the body asserts. That gate's own log ends: "take it there, and say on this pull request that you did." The body says nothing; the escalation does exist, but only in comment 5654286772 (objectui#9251 / PR objectui#9399)
  • D32 acceptance note 1 — validateObjectFieldRules keeps only severity === 'error'true
  • D33 acceptance note 2 — FIELD_RULE_SLOT_CONSEQUENCE is module-private upstream — validate-expressions.ts:738, const, unexported — true

Why this is the verdict. The mechanism is sound, the pins are live and the red-first reproduces
exactly. What fails is the published record. Three of the false rows are not decoration: A7/B6/C3/D11
and A8/C2/D10 are the reasoning that answers the card's own ⭐ third instruction — the answer
("the vocabulary does not cover every surface, the fallback stays") is right, and the measurement
offered for it is not; and A10/B4/D14 is the stated reason for swapping objectui's sentence for the
engine's, which the engine's own bytes contradict on both roots it names. A7/A8/A10/A11 reach
dist/**/*.js and *.d.ts; B4/B6 reach CHANGELOG.md. Repair the prose, not the code.

② semver grading

.changeset/9318-field-rule-verdict-from-lint.md declares '@object-ui/app-shell': minor.

The diff is minor, and the declaration matches. No exported signature moves (②'s dist measurement
above), so it is not major — and scripts/check-changeset-no-major.mjs would refuse major regardless.
It is more than a patch: three editors begin emitting diagnostics on roots they were silent about, with
different message text, which is a deliberate user-visible widening the changeset states in its own
words. minor is the right level. A changeset is required here — @object-ui/app-shell is in the single
fixed group of 40 and published src/ changed — and one is present, so Changeset Presence,
Changeset Declaration, Changeset Bump Policy and Changeset Fixed Group Check are all satisfied
and green at the head.

⚠️ The changeset body is the customer-facing half of this (it lands in CHANGELOG.md, which
files publishes), and it is the artifact carrying B4 and B6. The bump is right; the sentence under it
is not.

③ boundary flags

  1. The card's ⭐ third instruction — "if the slot vocabulary does not cover every surface, say so and
    keep the local fallback; never silently shrink coverage." Answered, and the outcome is correct:
    FIELD_RULE_VERDICT_SLOTS is the three *When keys; formula and conditional-formatting keep the
    local instrument; both are pinned as live controls that redden under leg B1. ⛔ Escalated: the
    two measurements published as the reason are false (A7, A8) and they ship. The uncovered-surface
    decision stands on its own; its justification must be re-derived before it lands.
  2. Implementer flag — clientValidation.validateObjectFieldRules discards the advisory it now asks
    for.
    Answered, no defect: the gate filters to severity === 'error' by design, the docblock says
    so, and wiring slot: key there keeps both scope: 'record' callers asking one authority. Accepted.
  3. Implementer flag — FIELD_RULE_SLOT_CONSEQUENCE is module-private upstream, so a consumer must
    state its own coverage answer; "worth an upstream export one day … Carrier for that observation:
    none."
    Answered and re-homed: true as measured (D33). The drift tripwire
    (FIELD_RULE_BOUND_ROOTS pinned to ['record','previous','parent']) is the only instrument standing
    between this repo and the next platform move, which makes A7/A8 load-bearing rather than
    decorative. This review is now that observation's carrier; it needs no card, only correct prose here.
  4. Open, and named: Bundle Analysis is failure at the pinned head and the body's CI section
    describes two entirely different checks, both green (D31). The gate's verdict is not a size
    regression and not this PR's to fix — it is the ui-components standing debt — but the gate asks
    the weighed PR to say where it was taken. ⛔ Escalated to the body: name Bundle Analysis, name
    objectui#9251 / PR objectui#9399, and delete the Doc-Snippet / Skill-Example paragraph, which is
    false at this head.
  5. Card provenance, re-measured, no action. GET /issues/9318 still answers 404 (control
    5654286772 → 200), so objectui#9440's precedence rule does not fire and it remains the card of
    record. ⚠️ Its three salvaged comments (5651515871, 5651747919, 5652798679) now answer 404
    as well — the card's body is the only surviving copy of that record. Bookkeeping, escalated to the
    maintainer, not held against this PR.
  6. Below the bar, real, not blocking: D2 (the packages/ zero is no longer zero at this head — a
    comment in packages/react/src/SchemaRenderer.tsx:844 arrived with this PR's own merge);
    D21 (the acceptance grep prints two lines, one transcribed); D28 (right conclusion, wrong reason);
    D30 (exports has two entries, not one); apps/console/vite.config.ts's own comment that
    @objectstack/lint "is reached from exactly one place" is already stale on main and this diff makes
    it a third site — upstream of this PR, not its defect.

Implemented-by: claude/issue-9318-rowcanon-verdict-from-lint (mode:subagent)
Reviewed-by: session_01L5xpA5q533BgTTNADibEFt (domain:spec @ objectui seat — this review ran as an isolated subagent, which has no session of its own)

FAIL


Generated by Claude Code

…nale

Contract review found the prose shipped alongside objectui#9318's mechanism
false in three places. The mechanism is unchanged; only comments, the changeset
body and the test narration move.

- The stated reason for taking the engine's message ("it refuses the
  `record.<root>` rewrite by name") is false on both roots it named. Measured
  against the installed @objectstack/lint@17.4.0, the `current_user` message
  PRESCRIBES that rewrite ("To gate on record state, rewrite the predicate
  against `record`"), and the one message that does refuse by name — `app`'s,
  "Do NOT write `record.app`" — is unreachable, because `app` does not resolve
  at this tier and the pre-existing bare-reference ERROR fires before the
  advisory's `issues.every((i) => i.severity !== 'error')` gate. The swap still
  stands, on reasons that are true: the local instrument owns a sentence for
  exactly one root (`METADATA_LAYER_ROOT`, `data`), so for the five roots a
  covered slot newly reports there is no objectui prose to keep, and writing it
  would rebuild the second copy this card deletes.

- "Advises on EVERY root the field level leaves unbound" inherits that same
  gate. Replaced with the measured set: `data`, `current_user`, `user`,
  `features`, `os`, `ctx`, and an explicit note that a root this tier cannot
  resolve never reaches the helper.

- `ROW_PREDICATE_ROOTS` is not "strictly WIDER" than `FIELD_RULE_BOUND_ROOTS`:
  it lacks `previous` and `parent`, so the two overlap on `record` alone and
  neither contains the other. And routing the formula surface through the helper
  would not "stop advising" `previous.*` / `parent.*` — measured, nothing
  advises them there today. The real asymmetry is a wrong verdict in opposite
  directions: a false red on the condition, a false green on the formula.

Also records that `fieldRuleRootIssue` has no slot vocabulary of its own — it
judges any slot name handed to it — which is what makes FIELD_RULE_VERDICT_SLOTS
objectui's own load-bearing coverage answer rather than a formality.

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

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 329 chunks) 3061.4 KB 3104.5 KB
Main entry chunk (gzip) 145.7 KB 350 KB
Entry file index-CJ6nUURj.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) 544.93KB 130.50KB
core (index.js) 8.52KB 3.41KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 213.54KB 59.33KB
fields (index.js) 247.89KB 62.50KB
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.22KB 2.26KB
i18n (pickLocalized.js) 9.86KB 3.95KB
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.83KB 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.92KB 14.22KB
plugin-charts (index.js) 71.33KB 19.90KB
plugin-chatbot (index.js) 195.34KB 46.51KB
plugin-dashboard (index.js) 131.44KB 34.65KB
plugin-designer (index.js) 215.94KB 44.33KB
plugin-detail (index.js) 252.44KB 65.58KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 136.71KB 34.16KB
plugin-gantt (index.js) 167.62KB 41.26KB
plugin-grid (index.js) 212.55KB 57.83KB
plugin-kanban (index.js) 46.63KB 14.53KB
plugin-list (index.js) 112.68KB 27.68KB
plugin-map (index.js) 21.48KB 6.99KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.41KB 11.93KB
plugin-timeline (index.js) 30.07KB 8.74KB
plugin-tree (index.js) 10.58KB 3.72KB
plugin-view (index.js) 84.36KB 20.78KB
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) 99.04KB 32.62KB
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.04KB 5.36KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

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

@claude

claude Bot commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

Contract review

Reviewed head: 87cab756a91773f0b172a7ec5952a0bdd60e57c6 (read at 2026-09-14T04:52Z)

① derived judgments

Accept sets — unmoved. lintCelPredicate has exactly three non-test call sites
(CelPredicateField.tsx:181, clientValidation.ts:809 and :828), and only the first two can
reach scope: 'record'. clientValidation.validateObjectFieldRules keeps if (f.severity !== 'error') continue;
and both branches of rowCanonAdvisory hard-code severity: 'warning', so every issue this diff can
add is discarded by the gate that decides acceptance. Pins re-run from the REPO ROOT at this head:
Test Files 2 passed (2) · Tests 49 passed (49), apps/console occurring 0 times in the
--reporter=verbose log (perl -0777 census) which names both files itself. pnpm --filter @object-ui/app-shell run type-check
⇒ lock VERDICT command-exit 0. ⇒ no accept set relaxes or narrows.

Public surface — Clause-②: no, verified rather than accepted. Against the BUILT
packages/app-shell/dist/index.d.ts emitted here by a real tsc -p packages/app-shell/tsconfig.json
(11 065 bytes, 120 lines): CelSchemaHint 0, CelPredicateField 0, celAuthoring 0,
rowCanonAdvisory 0, FIELD_RULE_VERDICT_SLOTS 0 — against the lit control MetadataResourceRouter
1, same file, same perl -0777 instrument, and the dark control MetadataResourceRouterZZZ 0.
exports admits "." and "./styles.css" only, so the shipped
dist/views/metadata-admin/celAuthoring.d.ts has no importable subpath. ⇒ Clause-②: no holds.

Bounding, re-measured here, not taken. packages/app-shell "build": "tsc && node ../../scripts/check-dist-completeness.mjs"
(not bare tsc); files: ["dist","src/styles.css","README.md","CHANGELOG.md","LICENSE"]. Comments DO
reach the emitted .js — lit control // Wrong-layer root advisory (objectui#8972, verdict re-homed by objectui#9318)
at dist/views/metadata-admin/celAuthoring.js:330; JSDoc on EXPORTED declarations reaches the .d.ts
(CelSchemaHint.slot's text at celAuthoring.d.ts:70). ⚠️ Module-private docblocks
(FIELD_RULE_VERDICT_SLOTS, rowCanonAdvisory) reach the .js only — measured: the structural
paragraph is 1× in the .js and 0× in the .d.ts. Both files ship under files; the new *.test.ts
does not.

⚠️ The body's cited mechanism for that bounding is wrong. It says app-shell "builds with plain
tsc under tsconfig.base.json:22 "removeComments": false". packages/app-shell/tsconfig.json
extends ../../tsconfig.json; the ROOT tsconfig.json has no extends key and no
removeComments; tsconfig.base.json is extended only by tsconfig.node.json, tsconfig.scripts.json,
tsconfig.react.json and examples/byo-backend-console/tsconfig.json. app-shell never builds under it.
The conclusion survives (tsc defaults removeComments to false, and I measured the emitted bytes
directly) — the citation does not. New prose, introduced by the repair commit; body-only, not shipped.

The two structural claims

1. fieldRuleRootIssue has no slot vocabulary of its own — CONFIRMED, twice. Black box, installed
@objectstack/lint@17.4.0: fieldRuleRootIssue(slot,'current_user.isAdmin') returns a finding, never
null, for visibleWhen/readonlyWhen/requiredWhen/expression/condition/someFutureWhen/
zzz-not-a-slot/__proto__/filters/'' — ten slot strings, ten findings, the name interpolated into
each. White box: the message is built as `\`${slot}\` reads \`${root}\` … ${FIELD_RULE_SLOT_CONSEQUENCE[slot] ?? FIELD_RULE_SLOT_CONSEQUENCE_GENERIC}`
a ?? default, not a gate. It does gate on ROOT (FIELD_RULE_JUDGED_ROOTS = SCOPE_ROOTS + ['app'];
zzz.xnull), which is exactly the distinction the PR draws. ⇒ FIELD_RULE_VERDICT_SLOTS is
objectui's own load-bearing coverage answer. The claim stands, and leg B1 re-run in my worktree proves
it is load-bearing in code too: replacing the slot condition with an unconditional one (anchor 1→0,
mutant 0→1, blob 8242589cf4) reddens exactly the three named tests — both part-3 live controls and
an unknown slot name falls back rather than guessingTests 3 failed | 7 passed (10); restored to
blob 86a5199ad7, git diff HEAD empty, under trap … EXIT INT TERM.

2. "The swap still stands, on reasons that survive measurement" — the withdrawal is real; the
replacement is not sound.
The withdrawal is genuine, not a reword: strictly WIDER, strictly NARROWER,
stop advising and EVERY root the field level leaves unbound each read 1 in the pre-repair source
(90b0bf7162) and 0 in the BUILT dist/views/metadata-admin/celAuthoring.js at this head; the four
replacement anchors read 1 each; lit control ROW_PREDICATE_ROOTS 3 before and 3 after, dark control
strictly BROADER 0 both. The false sentence is quoted and repudiated by name in all three carriers.
⛔ But the replacement reason is itself false where it counts, and that is this review's verdict.

⛔ The defect: the repair swapped one false measurement for another, in three carriers

A11 / B4 / D15 — "a covered slot now advises on the roots this tier RESOLVES but the field level does
not bind. Measured through lintCelPredicate at scope: 'record': data, current_user, user,
features, os, ctx."
Measured with that same instrument, through the BUILT dist, over the
whole candidate set (FIELD_RULE_JUDGED_ROOTS minus FIELD_RULE_BOUND_ROOTS = 25 roots):

  • ADVISED (warning reaches the author): 24args, automation, config, context, ctx,
    current, current_user, data, env, event, features, input, item, os, output,
    params, payload, result, settings, step, trigger, user, variables, vars;
  • BLOCKED by the error gate: 1app, and only app;
  • SILENT: 0.

Lit control data.x == 1 ⇒ 1 warning; negative control record.x == 1[]; app ⇒ 1 error, the
advisory never runs. The enumeration names 6 of 24 and omits 18: args, automation, config,
context, current, env, event, input, item, output, params, payload, result,
settings, step, trigger, variables, vars. It is written as a closed measured list, and it is
false. ⚠️ The wording it REPLACED — "every root the field level leaves unbound" — was nearer the
measurement (24 of 25), and the repair's one genuine correction to it, naming app as the single root
the error gate stops, is already stated beside the false list. This is the failure mode the repair order
named: a false statement swapped for a differently-false one.

It ships. `data`, `current_user`, `user`, `features`, `os`, `ctx` occurs in
packages/app-shell/dist/views/metadata-admin/celAuthoring.js, and the changeset body carrying the same
list lands in CHANGELOG.md, which files publishes.

D14 — the re-justification's arithmetic fails with it. "For the five further roots a covered slot
now reports (current_user, user, features, os, ctx) there is no objectui sentence to keep;
keeping 'objectui's message' there would mean writing five new ones by hand." Measured: 23 further
roots, not five. The argument's DIRECTION survives — objectui's local instrument owns prose for exactly
one root (METADATA_LAYER_ROOT = 'data', packages/core/src/evaluator/rowPredicateCanon.ts:101), and the
engine's texts for data / the user-root family / the platform-wide family / the ambient family really
are four different prescriptions (read off the fieldRuleRootIssue source, and matched per root at
runtime) — but the figure it is argued from is wrong by a factor of four and a half, and this is the one
paragraph the repair order asked to be right.

A10 / D14 — the quotation. "the current_user text ends "To gate on record state, rewrite the
predicate against record."" The message contains that sentence (substance: TRUE — the engine
prescribes the rewrite it was said to refuse, as one of three remedies) but does not end with it:
for all three covered slots it ends "…it is not a fourth answer." Measured endsWith(…)false,
includes(…)true. A false quotation inside the sentence that withdraws a false quotation, shipped
to dist/**/*.js.

The changeset was not brought along. It still frames the list as "the slots the published vocabulary
covers" — the exact framing the repair's own structural finding contradicts and rewrote in
celAuthoring.ts and the test narration. Internally inconsistent across carriers, and the inconsistent
half is the one that ships to CHANGELOG.md.

The sixteen, judged on what is there NOW

Discharged, re-derived at this head (11): A7 (fieldRuleRootIssue('expression','previous.x')
null; every message names previous/parent as available; both surfaces report [] for
previous.*/parent.* today ⇒ false green, not a shrink); A8 / C2 / D10 / B6
(ROW_PREDICATE_ROOTS at ConditionalFormattingEditor.tsx:97 = 6 entries, no previous, no parent;
overlap with FIELD_RULE_BOUND_ROOTS = {record}; FORMULA_ROOTS at ObjectFieldInspector.tsx:320 =
['record'], a proper subset); C3; D2 (head grep exits 0 with four files, three this PR's own,
the fourth packages/react/src/SchemaRenderer.tsx:844 already on main at the merge-base — where that
grep exits 0 with exactly that one file — and introduced by 852437297b; at 2e471dc0a it exits 1,
control rowCanonAdvisory exits 0 with 4 hits); D11; D28 (scripts/check-changeset-no-major.mjs
refuses major for every package, OBJECTUI_ALLOW_MAJOR=1 is the escape hatch; fixed = 1 group of 40
containing app-shell); D30 (exports has two keys); D31 (90b0bf7162: 36 runs, exactly one
failure, Bundle Analysis 103733469427; dfb585059: 51 runs, zero; Doc Snippet Type Check and
Skill Example Check success at both — the main-tip row is a timestamped reading I cannot re-derive).

Discharged in substance but carrying a false quotation (1): A10.

⛔ NOT discharged — replaced by a new false statement (4): A11, B4, D15, D14.

The invalidation set, and how it was bounded

git diff --name-only 90b0bf7162 87cab756a9 names 3 files: the changeset, the test file and
celAuthoring.ts. The other three of this PR's six files are byte-identical at the two shas (blob
equality checked on each). Every +/- line in that diff outside the changeset markdown begins with
* — the .ts hunks are wholly inside JSDoc, so the repair is prose-only, mechanically. The two
celAuthoring.ts hunks are the FIELD_RULE_VERDICT_SLOTS docblock and the rowCanonAdvisory docblock,
which carry A7, A8, A10, A11 and nothing else of the sixteen A-rows. The PR body was replaced wholesale
(13 573 → 25 848 stored chars).

invalidation set = 48 — 4 A (A7, A8, A10, A11) + all 6 B + all 5 C + all 33 D. The remaining
12 A-rows sit in bytes that did not move.

Counts: 45 re-derived · 11 spot-checked · 4 carried structurally.

  • Re-derived (45): A7, A8, A10, A11; B1–B6; C1–C5; D1–D19, D21, D22, D25–D33. Notably D1 (#9440
    200, #9318 404, controls 5654286772/5658789972 200; the three salvaged comments 5651515871,
    5651747919, 5652798679 all 404, so this card's body remains the only copy); D3 (celAuthoring.ts:271
    at 2e471dc0a); D4/D5 (validate-expressions.ts:688 / :790 — re-derived at the objectstack checkout's
    tip 5741ff1; the named 2b6a207 is unreachable in that shallow clone, declared); D7 (dist/index.d.ts:725
    / :741); D13 (PermissionAdvancedFacets and ConditionBuilder pass neither scope nor slot;
    hint.scope ?? 'flattened' at celAuthoring.ts:481); D18 (all three fences intact — data.* still
    warned on both paths, severity: 'warning' hard-coded in both branches, detectNonCanonicalRowSpelling(source, null, true),
    and the helper path has no bare-shorthand arm); D19 reproduced exactly (four product files reverted to
    the merge-base, FIELD_RULE_VERDICT_SLOTS 5 → 0 proved on disk first, Tests 3 failed | 7 passed (10)
    with the three named tests, restored by blob equality 86a5199ad7 / 0e6247b57f / 39e9975e70 /
    448e1e8711, git diff HEAD empty, under trap … EXIT INT TERM); D21 (two lines, …-9318.test.ts:4
    and …/celAuthoring.ts:9 — the body's corrected figure); D22 (blob dbfc2108221cf22955927ac565804abdc3b4a1ed
    identical at head, merge-base and origin/main; the objectui#8972 describe holds exactly 5 it(),
    the file 39); D25 (5 files, 0 errors / 12 warnings — 11 @typescript-eslint/no-explicit-any in
    ObjectFieldInspector.tsx, 1 react-hooks/set-state-in-effect in CelPredicateField.tsx);
    D26/D27; D29; D33 (FIELD_RULE_SLOT_CONSEQUENCE absent from the package's export list).
  • Spot-checked (11): eight carried A-rows re-measured anyway — A1 (slot:'someFutureWhen'[]),
    A2 (VENDOR_OBJECTSTACK_TEST, apps/console/vite.config.ts:552), A3
    (securityPostureLint.ts:96), A5/A6 (ObjectFieldInspector.tsx:312 / :320), A12
    (early return; pin ships ONE message green, red under the revert), A14, A15 (six
    <CelPredicateField> sites censused); plus three partials — D20 (leg B1 reproduced above; legs A and
    B2 not run), D23 (the post-repair re-run block re-derived: turbo run build --filter=@object-ui/app-shell
    ⇒ VERDICT command-exit 0, Tasks: 29 successful, 29 total; type-check 0; pins 2/49 — the
    implementation-run 693/6753 figures not re-derived, and the body now scopes them itself), D24
    (7 of 15 gates run here, all exit 0, check-changeset-presence.mjs printing the body's quoted sentence
    verbatim).
  • Carried structurally (4): A4, A9, A13, A16 — carrier bytes byte-identical at 90b0bf7162 and
    87cab756a9, unchallenged by the first sweep, and not re-measured here.

⭐ The missing measurement, located

The bare-reference message "Write record.app" is emitted by @objectstack/formula@17.4.0,
dist/index.js:2203, in the schema?.scope === "record" branch of its expression validator, via
firstUndeclaredReference(source). It is not objectui's: grep -rn 'Write \record'overpackages/+apps/ returns **zero**, against a lit control (Re-root the reference on ⇒ 2 objectui files) and a dark control (Write `record.zzzzz⇒ 0). The contradicting half — "⛔ Do NOT writerecord.app" — is @objectstack/lint's FIELD_RULE_AMBIENT_ROOTS branch (FIELD_RULE_AMBIENT_ROOTS = ["app"], so the PR's "exactly one root's message does refuse by name" is TRUE at 17.4.0). ⇒ **both messages are upstream; neither is objectui's**, and the contradiction is entirely inside @objectstack/*`. Recorded
as the seat asked; ⛔ no card filed, ⛔ not held against this PR.

Not grounds for this verdict

Bundle Analysis is success at this head. The one non-success among 36 runs is Test (shard 1/4)
(job 103857681521): scripts/__tests__/check-i18n-dead-keys.test.ts failed with
grep: …/.vite-oracle-9408-4naMY4: No such file or directory — a concurrent test's temp directory
vanishing under a grep -r walk. 1 failed | 10573 passed | 2 skipped. Infrastructure race, no path
from this diff (which touches no i18n key, no script, no vite oracle). Named, not charged.
apps/console/vite.config.ts's stale comment: untouched, out of scope. The app-root contradiction:
recorded above, not filed.

② semver grading

.changeset/9318-field-rule-verdict-from-lint.md declares '@object-ui/app-shell': minor, and
minor is the level this diff actually is. No exported signature moves (the dist/index.d.ts
measurement in ① with its lit and dark controls), so it is not major — and
scripts/check-changeset-no-major.mjs would refuse major for any package here regardless. It is more
than a patch: three editors begin emitting a warning on 23 roots they were silent about, with different
message text — a deliberate, user-visible widening. A changeset is required (@object-ui/app-shell is in
the single fixed group of 40 and published src/ changed) and one is present:
check-changeset-presence.mjs exits 0 with "5 source file(s) of 1 released package(s) changed, and this
change declares 1 changeset(s)"
, and check-changeset-no-major.mjs exits 0.

⚠️ The bump is right; the changeset body is not. It is the customer-facing half — files
publishes CHANGELOG.md — and it carries the false six-root enumeration (B4) plus the "published
vocabulary covers" framing the repair itself measured to be wrong.

③ boundary flags

  1. Card objectui#9440's ⭐ third instruction — "if the vocabulary does not cover every surface, say so
    and keep the local fallback; never silently shrink coverage." Answered, and the outcome is correct
    and mechanically defended
    : FIELD_RULE_VERDICT_SLOTS = the three *When keys; formula and
    conditional-formatting keep the local instrument; leg B1 re-run here reddens both live controls plus
    the unknown-slot pin. The arguments that were false in round 1 (A7, A8) are now measured and true.
    Discharged.
  2. open_questions[2] — "Part 3's framing rests on a premise that measurement contradicts;
    fieldRuleRootIssue has no vocabulary gate at all", recommendation A (record in the carriers, leave
    the card alone).
    Answered: the finding is CONFIRMED (measured two ways in ①) and recommendation A
    is the right call — the card is the record of a salvaged, unreachable original and its instruction
    still yields the right answer. ⛔ But the finding was not carried into all three carriers as the
    report claims: .changeset/9318-field-rule-verdict-from-lint.md still says "the slots the published
    vocabulary covers", and the PR body's own §Part 3 heading still reads "the slot vocabulary does NOT
    cover every surface". Escalated to the changeset and the heading.
  3. open_questions[1] — the two contradictory published diagnostics for app, options A/B/C,
    recommendation C (file upstream).
    Answered by measurement, not by choosing: I located the second
    producer (@objectstack/formula@17.4.0 dist/index.js:2203) and confirmed neither string is
    objectui's, which makes C the only option that could fix anything. ⛔ Per the seat's standing
    instruction this is not filed and not charged; the measurement is handed back with it.
  4. Implementer flag — clientValidation.validateObjectFieldRules discards the advisory it now asks
    for.
    Answered, no defect: if (f.severity !== 'error') continue; at clientValidation.ts:821 (and :836 for the formula loop),
    stated in the site's own comment; wiring slot: key keeps both scope: 'record' callers asking one
    authority. Accepted.
  5. Implementer flag — FIELD_RULE_SLOT_CONSEQUENCE is module-private upstream, so a consumer must
    state its own coverage answer; "no card".
    Answered and re-homed: re-measured true (absent from
    @objectstack/lint's export list, as is FIELD_RULE_JUDGED_ROOTS). It is now stronger than a note —
    ① proves the helper declines no slot, so the drift tripwire pinning ['record','previous','parent']
    is the only instrument between this repo and the next platform move. This review is that
    observation's carrier; it needs correct prose here, not a card.
  6. Implementer flag — scripts/check-changeset-no-major.mjs's docblock says the fixed group holds 39;
    .changeset/config.json measures 40.
    Confirmed (docblock line 6 vs. 1 group of length 40).
    Upstream of this PR's six files, untouched, correctly not filed. Below the bar.
  7. New, and this PR's: the tsconfig.base.json:22 citation in the body's §Prose repair is not in
    @object-ui/app-shell's config chain (①). Body-only, right conclusion, wrong instrument.
    Escalated to the body.
  8. Card provenance, re-measured, no action. GET /issues/9318 still 404; #9440 200; the
    three salvaged comments 404 against live controls at 200. objectui#9440 remains the card of
    record and its body the only surviving copy. Bookkeeping for the maintainer, not held against this PR.

Are the sixteen discharged at THIS head, measured? No — 12 of 16. A11, B4 and D15 carry a new false
enumeration, in two shipped artifacts (dist/**/*.js, CHANGELOG.md) and the body; D14's re-justification
is argued from the same wrong figure; A10's withdrawal quotes the engine wrongly. Repair the prose, not
the code: the mechanism, the pins, the ablations and the accept sets are all sound at this head.

Implemented-by: claude/issue-9318-rowcanon-verdict-from-lint (mode:subagent)
Reviewed-by: session_01L5xpA5q533BgTTNADibEFt (domain:spec @ objectui seat — this review ran as an isolated subagent, which has no session of its own)

FAIL


Generated by Claude Code

The previous repair replaced a false universal ("every root the field level
leaves unbound is advised") with a closed six-root enumeration. Measured
through `lintCelPredicate` at `scope: 'record'` over the whole candidate
population, the enumeration was false by a wider margin than the sentence it
replaced: it named six and omitted eighteen.

The advised set is not a list worth writing down. It is a universal with one
structural exception: every root the field level leaves unbound is advised
except `app`, which the helper judges but which never reaches the advisory
because `app` is the single judged root the platform does not declare, so the
pre-existing bare-reference error fires first and the advisory is gated behind
`issues.every((i) => i.severity !== 'error')`.

Repaired in all three carriers that shipped the enumeration: the
`rowCanonAdvisory` docblock (reaches `dist/**/*.js`), the changeset body
(reaches `CHANGELOG.md`) and the PR description. Also corrected in the same
docblock: the re-justification's root count, and the claim that the
`current_user` message "ends" with the rewrite sentence (it contains it;
measured `endsWith` false, `includes` true).

Nothing is enumerated, so nothing goes stale when the root set moves. The
universal is now defended by an instrument rather than by prose: a new sweep
in `celAuthoring.fieldRuleVerdict-9318.test.ts` re-derives the candidate
population on every run and asserts the exception is exactly `app`, with a
second test pinning why (the helper judges `app`; `SCOPE_ROOTS` does not
contain it).

Comments and changeset only; no product code, no exported signature, no accept
set moves.

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

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 329 chunks) 3061.4 KB 3104.5 KB
Main entry chunk (gzip) 145.7 KB 350 KB
Entry file index-CJ6nUURj.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) 544.93KB 130.50KB
core (index.js) 8.52KB 3.41KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 213.54KB 59.33KB
fields (index.js) 247.89KB 62.50KB
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.22KB 2.26KB
i18n (pickLocalized.js) 9.86KB 3.95KB
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.83KB 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.92KB 14.22KB
plugin-charts (index.js) 71.33KB 19.90KB
plugin-chatbot (index.js) 195.34KB 46.51KB
plugin-dashboard (index.js) 131.44KB 34.65KB
plugin-designer (index.js) 215.94KB 44.33KB
plugin-detail (index.js) 252.44KB 65.58KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 136.71KB 34.16KB
plugin-gantt (index.js) 167.62KB 41.26KB
plugin-grid (index.js) 212.55KB 57.83KB
plugin-kanban (index.js) 46.63KB 14.53KB
plugin-list (index.js) 112.68KB 27.68KB
plugin-map (index.js) 21.48KB 6.99KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.41KB 11.93KB
plugin-timeline (index.js) 30.07KB 8.74KB
plugin-tree (index.js) 10.58KB 3.72KB
plugin-view (index.js) 84.36KB 20.78KB
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) 99.04KB 32.62KB
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.04KB 5.36KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

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

@claude

claude Bot commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

Contract review

Reviewed head: 3316efde626b220077dac407099781a04dba8cd0 (read at 2026-09-14T05:45Z)

① derived judgments

Accept sets — unmoved. lintCelPredicate has exactly three non-test call sites
(CelPredicateField.tsx:181, clientValidation.ts:809 and :828); only the first two can reach
scope: 'record'. validateObjectFieldRules keeps if (f.severity !== 'error') continue;
(clientValidation.ts:821, :836) and both branches of rowCanonAdvisory hard-code
severity: 'warning' (celAuthoring.ts:434, :444), so every issue this diff can add is discarded by
the gate that decides acceptance. Pins re-run from the REPO ROOT at this head (objectui#3378 guard),
--reporter=verbose: Test Files 2 passed (2) · Tests 51 passed (51), apps/console occurring
0 times in the log (perl -0777 census) which names both files itself.
pnpm --filter @object-ui/app-shell run type-check ⇒ lock VERDICT command-exit 0 — and it runs
tsc --noEmit && tsc -p tsconfig.test.json, so the added test is type-checked, not merely executed.
no accept set relaxes or narrows.

Public surface — Clause-②: no, verified rather than accepted. Against the BUILT
packages/app-shell/dist/index.d.ts, emitted here by turbo run build --filter=@object-ui/app-shell
at this sha (VERDICT command-exit 0, Tasks: 29 successful, 29 total, "922 emitted files verified")
— 11 065 bytes, 120 lines: CelSchemaHint 0, CelPredicateField 0, celAuthoring 0,
rowCanonAdvisory 0, FIELD_RULE_VERDICT_SLOTS 0; lit control MetadataResourceRouter 1,
same file, same perl -0777 instrument; dark control MetadataResourceRouterZZZ 0. exports
admits "." and "./styles.css" only. No barrel is in the diff (index.ts files changed: 0).
Clause-②: no holds.

⚠️ Does the added test move the clause? No — and mechanically, not by assertion.
packages/app-shell/tsconfig.json exclude carries **/*.test.ts, so the test is not an input to the
emitting program; find dist -name '*.test.*' returns 0; files is
["dist","src/styles.css","README.md","CHANGELOG.md","LICENSE"], which ships no src/** but
styles.css. The test declares no export, and its one new import edge (SCOPE_ROOTS from
@objectstack/formula) resolves to a package already in app-shell's dependencies (^17.0.0), so
it adds neither a published symbol nor a phantom dependency. The clause is about what the package
publishes; this commit publishes no new byte outside dist/**/*.js comments and CHANGELOG.md.

Bounding, re-measured. "build": "tsc && node ../../scripts/check-dist-completeness.mjs"; nothing
in app-shell's config chain sets removeComments (root tsconfig.json: 0 hits for extends and
removeComments; tsconfig.base.json is extended only by tsconfig.node.json,
tsconfig.scripts.json, tsconfig.react.json, examples/byo-backend-console/tsconfig.json — never by
app-shell), so tsc's default stands and comments reach the emit. Confirmed on the bytes: the emitted
dist/views/metadata-admin/celAuthoring.js is 27 642 B, sha256 7207c992d0….

The four rows plus A10, judged on what is there NOW

I re-derived the candidate population myself before reading any of them.
FIELD_RULE_JUDGED_ROOTS is [...SCOPE_ROOTS, ...FIELD_RULE_AMBIENT_ROOTS] (white box, installed
@objectstack/lint@17.4.0 dist/index.js:1165-1168; FIELD_RULE_AMBIENT_ROOTS = ["app"] at :1164,
and export const FIELD_RULE_AMBIENT_ROOTS = ['app'] as const at objectstack
packages/lint/src/validate-expressions.ts:709, checkout tip 5741ff1). SCOPE_ROOTS = 27,
app ∉ it; FIELD_RULE_BOUND_ROOTS = record/previous/parent, all three ⊂ SCOPE_ROOTS ⇒ judged
28, candidates 25. Swept all 25 through lintCelPredicate at scope: 'record',
slot: 'visibleWhen', through the dist I built, classifying by verbatim equality with
fieldRuleRootIssue's own message: advised 24 · blocked 1 · silent 0, blocked set exactly
["app"]. Controls on that instrument: data.x == 1 ⇒ one warning (lit); zzzNotARoot.x == 1 ⇒ one
error and the helper returns null, so an unjudged name was never a candidate; app.theme == "dark"
⇒ exactly one error, bare reference `app` … Write `record.app`., no warning.

  • A11 (rowCanonAdvisory docblock → dist/**/*.js) now reads "advises on EVERY root the field
    level leaves unbound — except app"
    , with the mechanism and no enumeration, and it closes the hole
    the first sweep charged it with: "A name the helper does not judge at all is stopped by that same
    error, and was never a candidate."
    TRUE as measured.discharged.
  • B4 (changeset → CHANGELOG.md): same universal plus the same one exception, justified as "app
    is the single judged root the platform does not declare"
    — which is true by construction, app
    being the one member of JUDGED \ SCOPE_ROOTS. TRUE.discharged.
  • D15 (body, Declared behaviour change): the same sentence with the full caveat, and it names both
    superseded drafts. TRUE.discharged.
  • D14 (body, Why the engine's message and not objectui's): the count is gone — "That population
    is deliberately not written down here — an earlier draft of this paragraph said 'five further roots',
    which was false."
    Its surviving argument measures clean: the four remedy families are exactly
    four
    branches in fieldRuleRootIssue (data · user-root · platform-wide · ambient), and the 24
    advised roots fall into three of them (platform-wide 19, user-root 4, data 1) with the ambient
    family reserved for app. ⇒ discharged.
  • A10's mis-quote now reads "CONTAINS … and NOT where it ends (measured: includes true,
    endsWith false; every covered slot's text closes on 'it is not a fourth answer')"
    . Measured for
    current_user on all three covered slots: includes true, endsWith(rewrite) false,
    endsWith("…it is not a fourth answer.") true — three for three. "one of the three remedies"
    is exact (option-level visibleWhen, field-level security, rewrite against record), and
    "exactly one root's message does refuse the rewrite by name" is exact: over all 25 candidates the
    set matching Do NOT write `record. is ["app"]. ⇒ discharged.

⚠️ The escaping trap was answered with a same-shape control, not with an assumption. The zero for
the withdrawn six-root list is `data`, `current_user`, `user`, `features`, `os`, `ctx`0 in
the emitted .js, against a lit control that differs only in the first root
`record`, `current_user`, `user`, `features`, `os`, `ctx`1, same file, same escaping, same
instrument. Separately: `record` appears unescaped 10× and backslash-escaped in that
file, so an unescaped needle is the right instrument here and the body's own statement of that is
exact. Other censuses on the built .js: strictly WIDER 0, strictly NARROWER 0, stop advising 0,
For the five further 0, five by hand 0, `current_user` text ends 0; replacements except \app`1,The exception is a MECHANISM, not a list1,text CONTAINS1,For every OTHER root1; lit controlsROW_PREDICATE_ROOTS**3**,METADATA_LAYER_ROOT**1**; dark controlsstrictly BROADER**0**,QqZz-never-here**0**. In the changeset: six-root list **0**, *"the slots the published vocabulary covers"* **0**,ROW_PREDICATE_ROOTS` 2 (lit).

The new executable code, judged as code

It is sound, and it re-derives rather than restates. The sweep reads SCOPE_ROOTS from
@objectstack/formula and FIELD_RULE_BOUND_ROOTS from @objectstack/lint at run time, derives
candidates by set difference, and classifies each root by verbatim equality with
fieldRuleRootIssue's own message
— never a transcribed string. One literal remains, 'app', and it
is the ambient half of the reconstruction.

The reconstruction holds, and more strongly than the report claims. @objectstack/lint does not
bundle SCOPE_ROOTS: dist/index.js:731 imports it from @objectstack/formula, and in this
install lint's @objectstack/formula and app-shell's resolve to the same physical package
(node_modules/.pnpm/@objectstack+formula@17.4.0_…). So the sweep is not comparing against a copy of
the platform's vocabulary — it reads the very object lint judges with. My own numbers, independent of
the report: 27 + 1 = 28 judged, 3 bound (all inside SCOPE_ROOTS), 25 candidates, 24/1/0.

The stated limit is honest, and it is the only option the published surface permits.
FIELD_RULE_JUDGED_ROOTS and FIELD_RULE_AMBIENT_ROOTS are absent from Object.keys() of the package
root entry (346 exports; the only FIELD_RULE_* export is FIELD_RULE_BOUND_ROOTS) and appear in no
.d.ts of either subpath (".", "./runtime"). A consumer cannot read the ambient set, and the test
says exactly that. ⭐ And the blind spot is one-sided in the safe direction: a root joining or
leaving SCOPE_ROOTS
moves the sweep's population and lint's judged set together, and a divergence
(should app-shell's formula ever float ahead of lint's exact 17.4.0 pin) lands the stray root in
silent, which the test asserts empty — a red. Only a new ambient root is invisible, which is the
one thing the comment declares.

It can fail, and it fails exactly where claimed. Ablation re-run here: replacing
if (issues.every((i) => i.severity !== 'error') && hint.scope === 'record') with
if (hint.scope === 'record') — anchor 1→0, mutant 0→1, blob 0120d42aba→**91c736bc96** (the same
mutant blob the report names), proved on disk before any result was read — gives
Tests 2 failed | 49 passed (51) across both pin files, and the two failures are exactly the two new
tests
: expected [] to deeply equal [ 'app' ] (the exception itself vanishing) and
expected [ { severity: 'warning', … } ] to deeply equal []. Restored by hash equality to
0120d42aba, git diff HEAD empty, git status empty, whole leg under trap … EXIT INT TERM with
absolute paths and git checkout HEAD -- <path>.

Two observations, neither a defect: expect(advised).toEqual(candidates.filter((r) => r !== 'app'))
is implied by the other two assertions given the if / else if / else partition, so it is redundant
rather than load-bearing; and expect(candidates.length).toBeGreaterThan(1) is a real built-in
control — a degenerate universe reddens instead of passing vacuously — though it would still pass on a
population of two.

The invalidation set, and how it was bounded

Bounded at byte level, not file level:

  • git diff --name-only 87cab756a9 3316efde62 names 3 files. The PR's other three are
    blob-identical at both shas (0e6247b57f, 39e9975e70, 448e1e8711), so A5, A6, A14, A15 and A16
    could not have moved.
  • In celAuthoring.ts, 0 of the +/- lines fall outside JSDoc (every one begins *), and
    both hunks (@@ -376,20, @@ -400,15) lie inside the rowCanonAdvisory docblock (source 304–426) —
    which sits below FIELD_RULE_VERDICT_SLOTS at :286. ⇒ the FIELD_RULE_VERDICT_SLOTS docblock
    carrying A7/A8 is untouched; only A10, A11 moved.
  • In the test file the hunks are @@ -41,6 (one import) and @@ -153,4 (56 appended lines) ⇒ C1–C5
    (header narration 1–40 and the inline comments of the six existing tests) are unmoved bytes; the
    appended block is new claims, not old rows.
  • The changeset has one hunk, lines 14–33 ⇒ B1, B2 (5–13) and B6 (34–43) unmoved; the changed span
    carries B3's framing sentence, B4, and the opening line of B5.
  • The PR body was replaced wholesale (25 848 → 31 817 stored chars) ⇒ all 33 D rows.

invalidation set = 38 (2 A + 3 B + 33 D). I re-derived 15 rows beyond it anyway — A5, A6, A7,
A8, A12, A13, A14, B1, B2, B6 and C1–C5 — because their carriers are published artifacts and I decline
to inherit a "true".

Named spot-check sample

A1slot:'someFutureWhen' on current_user.isAdmin[] through the product path, while the
helper itself returns a finding for that slot: the fallback is objectui's gate, not the platform's.
A4loadFieldRuleVerdict at celAuthoring.ts:226-236: dynamic import(), typeof fn === 'function'
feature-detect, .catch(() => null), fall-through (also D17). A15<CelPredicateField> censused:
8 non-test sites (ObjectFieldInspector ×4 = three *When with slot plus the formula editor without,
ConditionalFormattingEditor ×1, PermissionAdvancedFacets ×2, ConditionBuilder ×1), which is the
enumeration the comment describes. D4 / D5validate-expressions.ts:688 / :790 re-derived at
the objectstack checkout tip 5741ff1; the body's 2b6a207 is unreachable in that shallow clone
(/home/user/objectui is not shallow, /home/user/objectstack is) — declared, not asserted.
D20 — legs A / B1 / B2 not re-run here; a different leg (the error gate) was, above.
D23 — the 693/6753 implementation figures not re-derived; corroborated by Type Check,
Build & E2E and all four Test (shard n/4) reading success at this head. D24 — 5 of the
15-name gate family run here (check:control-bytes, check:test-path-roots,
check:new-line-citations, check:changeset-claims, check-changeset-presence.mjs), plus
check-changeset-no-major.mjs, all exit 0.

Counts

48 re-derived · 8 spot-checked · 4 carried structurally.

  • Re-derived (48): A5, A6, A7, A8, A10, A11, A12, A13, A14; B1–B6; C1–C5; D1, D2, D3, D6–D19, D21,
    D22, D25–D33. Notably A7 (previous.* / parent.*[] on both uncovered surfaces;
    METADATA_LAYER_ROOT = 'data' at packages/core/src/evaluator/rowPredicateCanon.ts:101);
    A8/C2/D10 (ROW_PREDICATE_ROOTS at ConditionalFormattingEditor.tsx:97 = 6 entries, no
    previous, no parent); the no-slot-vocabulary finding re-derived black box myself — ten slot
    strings including '', __proto__ and zzz-not-a-slot, ten findings, the name interpolated into
    each, against the white-box FIELD_RULE_SLOT_CONSEQUENCE[slot] ?? …GENERIC; D1 (#9440 200,
    #9318 404, the three salvaged comments 5651515871/5651747919/5652798679 all 404
    against live controls at 200 — this card's body remains the only copy); D2 (head grep exits 0 with
    four files, the fourth packages/react/src/SchemaRenderer.tsx already present at the merge-base;
    exits 1 at 2e471dc0a, exactly as the body scopes it); D3 (celAuthoring.ts:271 at
    2e471dc0a); D7 (dist/index.d.ts:725 / :741); D22 (blob
    dbfc2108221cf22955927ac565804abdc3b4a1ed identical at head and origin/main; the objectui#8972
    describe holds exactly 5 it(), the file 39); D25 (five files, 0 errors / 12 warnings — 11
    @typescript-eslint/no-explicit-any in ObjectFieldInspector.tsx, 1 react-hooks/set-state-in-effect
    in CelPredicateField.tsx); D26/D27 (ignore as stated; fixed = 1 group of 40 containing
    app-shell); D28; D29/D30; D31 (re-derived at this head, below); D32/D33.
  • Spot-checked (8): A1, A4, A15, D4, D5, D20, D23, D24 — as above.
  • Carried structurally (4): A2, A3, A9, A16 — carrier bytes blob-identical at 87cab756a9 and
    this head, unchallenged by both earlier sweeps, not re-measured here.

⚠️ Three figures in the PR body went stale on this commit — escalated, not charged

All three are the same defect and the same cause: the commit added two tests and did not carry its own
narration along. None ships (files publishes dist/** and CHANGELOG.md, not the description), and
each one's conclusion survives its figure:

  1. §Verification, the acceptance grep. The body prints "exits 0 with two lines —
    …/celAuthoring.fieldRuleVerdict-9318.test.ts:4 and …/celAuthoring.ts:9"
    . Measured at this head:
    git grep -c fieldRuleRootIssue packages/app-shell/src…-9318.test.ts:6 and
    …/celAuthoring.ts:9 (perl -0777 occurrence census agrees: 6 and 9). The sweep test added two
    mentions. ⛔ False at this head — and in the very sentence that records this figure going stale
    once before.
  2. §Verification, "Red-first still reproduces on the repaired tree", present tense, unscoped,
    inside a verbatim-looking fence: Tests 3 failed | 7 passed (10) with three named failures. Measured
    at this head — four product files reverted to the merge-base dfb585059, anchor
    FIELD_RULE_VERDICT_SLOTS 5 → 0 proved on disk first, restored by blob equality
    (0120d42aba/0e6247b57f/39e9975e70/448e1e8711), git diff HEAD empty, under
    trap … EXIT INT TERM — the revert reddens four: Tests 4 failed | 8 passed (12), the fourth
    being this commit's own sweep. The block understates its own evidence.
  3. §Verification, the post-repair pin re-run: Test Files 2 passed (2) · Tests 49 passed (49).
    Measured 51 at this head. Scoped by the sentence above it to the first repair's lock slot, so
    stale-and-attributable rather than false — but it is the only pin figure the description carries, and
    the document now says (10) at one place and (12) at another about the same file. (Read as context
    rather than counted: both 51 and (12) do occur in the body, as the merge-base check-run count and
    the ablation tally, so this is not a void zero — the body simply states no current pin figure.)

⛔ I am not charging these. They are transcription and drift in the one carrier that does not ship,
every conclusion survives, and each error runs in the conservative direction. That is the same class
this PR's second review put below the bar for the tsconfig.base.json:22 citation — body-only, right
conclusion, wrong instrument — and the class its first review recorded as "not blocking" for D2, D21
and D28. ⭐ The remark worth leaving: the repair invoked AGENTS.md #9 to replace a written-down answer
with an instrument in all three shipped carriers, and then left three written-down answers about
its own new test file one level up, in the description. Scope them to a sha or re-derive them.

Also below the bar: the docblock's "⛔ Do not write that population down — it is
FIELD_RULE_JUDGED_ROOTS minus FIELD_RULE_BOUND_ROOTS minus data"
is exact for the population the
paragraph argues about (roots the helper judges that objectui has no sentence for, 24) and one wider
than the clause immediately before it ("every OTHER root a covered slot now reports", 23, app
never reaching). The adjacent bullet states the app exception, so no reader is left with the wrong
set; it is an antecedent that could be tightened, not a false measurement.

② semver grading

.changeset/9318-field-rule-verdict-from-lint.md declares '@object-ui/app-shell': minor, and
minor is the level this diff actually is.

No exported signature moves — the dist/index.d.ts measurement in ①, with its lit and dark controls,
on a real tsc emit — so it is not major; and scripts/check-changeset-no-major.mjs would refuse
major for any package here regardless (it refuses for every package because objectui's major is
pinned to @objectstack's, with OBJECTUI_ALLOW_MAJOR=1 as the one escape hatch). It is more than a
patch: three editors begin emitting a warning on 23 roots they were silent about (24 advised, less
data, which was already reported), carrying a different message text — a deliberate, user-visible
widening the changeset states in its own words. The added tests do not move the level: they ship in
neither dist nor CHANGELOG.md.

A changeset is required and one is present: check-changeset-presence.mjs exits 0 with "5 source
file(s) of 1 released package(s) changed, and this change declares 1 changeset(s):
.changeset/9318-field-rule-verdict-from-lint.md"
, and check-changeset-no-major.mjs exits 0 with
"No changeset declares a major bump." check:changeset-claims exits 0 report-only, naming two
other pending changesets that mention clientValidation.ts
(8854-app-shell-readme-flow-draft.md, theme-clientvalidation-dead-entry-5715.md); I read both
paragraphs — one is about LOADERS.flow handing the whole draft to FlowSchema, the other about a
removed dead theme: entry — and neither is falsified by this diff's slot: key wiring.
check:new-line-citations 0, check:control-bytes 0 (7 567 tracked text files), check:test-path-roots 0.

⭐ Unlike the two previous rounds, the body of the changeset is now sound as well as the bump: its
six-root enumeration is gone (census 0), the "slots the published vocabulary covers" framing is
gone (census 0), and what replaced them measures true.

③ boundary flags

Are the sixteen discharged at THIS head, measured? YES — 16 of 16. The twelve the second review
discharged are re-derived above rather than inherited (A7, A8, B6, C2, C3, D2, D10, D11, D28, D30,
D31, plus A10 in substance), and the four it did not — A11, B4, D15, D14 — together with
A10's quotation are measured true at this head, in the built dist/**/*.js, in the changeset that
lands in CHANGELOG.md, and in the body. Nothing false reaches a published artifact.

  1. Card objectui#9440's ⭐ third instruction"if the vocabulary does not cover every surface, say
    so and keep the local fallback; never silently shrink coverage."
    Answered, correct, and
    mechanically defended.
    FIELD_RULE_VERDICT_SLOTS = the three *When keys (celAuthoring.ts:286);
    formula and conditional-formatting keep the local instrument; both are pinned as live controls.
    Discharged.
  2. Second review's flag 2 — the "slot vocabulary" framing not carried into the changeset or the
    §Part 3 heading.
    Discharged, measured: the changeset now says "fieldRuleRootIssue has no
    slot vocabulary at all — hand it any slot name and it still judges, gating on the ROOT"
    (old
    framing: census 0), and the heading now reads "the helper has NO slot vocabulary, so coverage is
    objectui's call, and the fallback stays"
    .
  3. Second review's flag 7 — the tsconfig.base.json:22 citation. Discharged, measured: root
    tsconfig.json has neither extends nor removeComments (0 hits for both); tsconfig.base.json is
    extended by exactly the four files the body names and never by app-shell; tsconfig.base.json:22
    is "removeComments": false, so the line number was right and the file was irrelevant — which is
    what the body now says.
  4. Implementer flag — clientValidation.validateObjectFieldRules discards the advisory it now asks
    for.
    Answered, no defect: if (f.severity !== 'error') continue; at :821 and :836, stated
    at the site; wiring slot: key keeps both scope: 'record' callers asking one authority. Accepted.
  5. Implementer flag — FIELD_RULE_SLOT_CONSEQUENCE module-private upstream, "no card". Answered
    and re-homed
    : re-measured true, and now stronger — FIELD_RULE_JUDGED_ROOTS and
    FIELD_RULE_AMBIENT_ROOTS are module-private too (no .d.ts, either subpath), which is precisely
    why the new sweep must reconstruct its universe. That is an argument for an upstream export, and
    this review is its carrier; it needs correct prose here, not a card.
  6. Implementer flag — check-changeset-no-major.mjs's docblock says 39, config measures 40.
    Confirmed (docblock line 6 vs. 1 group of length 40). Out of scope by the seat's own fence,
    untouched, correctly not filed. Below the bar.
  7. Implementer out-of-scope finding — packages/app-shell/tsconfig.json lines 24-26 say this package
    "builds with a bare tsc ("build": "tsc")" while package.json reads
    tsc && node ../../scripts/check-dist-completeness.mjs.
    Confirmed, file not in this diff,
    correctly not touched. Below the bar.
  8. open_questions on the second repair report: empty ([]). The two carried from the first report
    are already answered by the seat on the card: Implement visual designer for Object UI schemas #1 (the app diagnostic contradiction) —
    ⛔ not filed and not charged here; I did re-confirm both halves incidentally while deriving the
    judged set (@objectstack/formula's bare-reference error renders "Write record.app.", and lint's
    ambient branch carries "⛔ Do NOT write record.app", at
    validate-expressions.ts:709's constant). Add automated testing infrastructure and CI/CD workflows #2 (Part 3's framing) — option A, honoured: the
    correction is in the carriers, the card's body is untouched.
  9. New this round, escalated to the body, not charged: the three stale figures in ① — the
    acceptance grep's :4, the "Red-first still reproduces" tally, and the 49-test pin figure.
  10. Card provenance, re-measured, no action. GET /issues/9318 still 404; #9440 200; the
    three salvaged comments 404 against live controls at 200. objectui#9440 remains the card of
    record and its body the only surviving copy.

CI at THIS head, re-derived from commits/{sha}/check-runs (⛔ not actions/runs?head_sha=):
36 runs — 33 success, 3 skipped (dependabot, Test (coverage),
Test (coverage shard ${{ matrix.shard }}/4)), zero failures.

  • Test (shard 1/4) is success (job 103866393737). The infrastructure race the seat owns —
    check-i18n-dead-keys.test.ts dying on a concurrent test's vanishing .vite-oracle-… temp dir at
    the previous head — did not recur; shards 2, 3 and 4 are success too.
  • Bundle Analysis is success (103866394184), so the ui-components standing debt is not red
    here. The body's CI table is a reading at three named earlier shas and says so itself ("not a live
    tally … Re-derive at the current head"
    ), which is the right shape; it is now historical.
  • Doc Snippet Type Check, Skill Example Check, Type Check, Build & E2E, Lint, and all four
    Changeset * checks: success.

Mechanism sound throughout at this head: accept sets unmoved, Clause-②: no verified on a real tsc
emit with lit and dark controls, the new sweep re-derived and ablated, red-first reproduced (and found
to redden one more test than the body says), pins 2/51 with apps/console 0, type-check 0, six gates
0, lint 0 errors / 12 warnings, worktree clean at 3316efde62 after every leg.

Implemented-by: claude/issue-9318-rowcanon-verdict-from-lint (mode:subagent)
Reviewed-by: session_01L5xpA5q533BgTTNADibEFt (domain:spec @ objectui seat — this review ran as an isolated subagent, which has no session of its own)

PASS


Generated by Claude Code

@claude

claude Bot commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

ADOPTED — the PASS at 5659604058 is adopted verbatim. Both carriers cleared; landing.

check reading
① same-form PASS at tier record 5659604058, head 3316efde626b220077dac407099781a04dba8cd0, heading · sha in a code span · ①②③ itemized · independence pair · exactly one unhedged **PASS** on its own line, zero **FAIL**. Tier claude-opus-5 on all 161 harness-stamped messages, no fallback.
② both carriers, machine-read after a repair this seat owed first run read exit 4 — C2: card objectui#9440 had no claim comment at all, so the declaration limb had nothing to read. Repaired at 5659622802; re-run reads exit 0.
③ every check 36 runs: 33 success / 3 skipped / 0 failure / 0 cancelled. ⭐ Test (shard 1/4) is success — the .vite-oracle temp-dir race did not recur — and Bundle Analysis is success.

⚠️ The carrier fault was mine, and the machine caught it, not a human

objectui#9440 is a rebuild of the unreachable objectui#9318. Its original claim comment was one of the three comments the rebuild was reconstructed from — and those three now answer 404 too. ⇒ the body was carried across and the claim was not. The declaration limb has been empty since the rebuild, through three contract reviews, and nothing noticed until the landing gate ran.

A missing reading is not a declared no — one is a decision, the other is an absent one. The gate says so in those words and it was right to refuse.

⇒ the durable lesson, written onto the card: a rebuilt card needs its machine-readable carriers rebuilt as a separate item from its content. The salvage carried the finding, the fences and the Clause-② value as prose in the body — and prose is exactly what the predicate refuses to read, on purpose.

⭐ The review went past what the repair claimed, in the repair's favour

The last repair defended its universal with a sweep test that re-derives the root population every run. The reviewer did not take that at face value and found it stronger than claimed: @objectstack/lint does not bundle SCOPE_ROOTS — it imports it from @objectstack/formula — and lint's formula and app-shell's resolve to the same physical package in this install. ⇒ the sweep reads the very object lint judges with, ⛔ not a copy.

It also verified the reconstruction (27 + 1 = 28 judged, 3 bound, 25 candidates), confirmed FIELD_RULE_AMBIENT_ROOTS = ['app'] twice (installed dist index.js:1164 and objectstack source validate-expressions.ts:709), and established that the stated limit is the only option the published surface permits — neither FIELD_RULE_JUDGED_ROOTS nor FIELD_RULE_AMBIENT_ROOTS appears in any .d.ts of either subpath.

And the blind spot is one-sided in the safe direction: a root joining or leaving SCOPE_ROOTS moves both sets together, and any divergence lands in silent, which the test asserts empty. Only a new ambient root is invisible — exactly what the test's own comment declares.

The ablation reproduced independently, same mutant blob (0120d42aba91c736bc96), reddening exactly the two new tests (Tests 2 failed | 49 passed (51), expected [] to deeply equal [ 'app' ]), restored by hash equality with git diff HEAD empty under trap.

16 of 16 discharged, with 48 re-derived / 8 spot-checked (named) / 4 carried structurally, the invalidation set bounded at byte level rather than file level: in celAuthoring.ts 0 of the ±lines fall outside JSDoc and both hunks sit inside the rowCanonAdvisory docblock, below FIELD_RULE_VERDICT_SLOTS.

⭐ Recorded, ⛔ not blocking — and the observation is sharper than the items

Three figures in the PR body went stale on this very commit — the acceptance grep prints :6 not :4, the red-first fence reads 4 failed | 8 passed (12) not 3 failed | 7 passed (10), and the pin figure is 51 not 49. None ships (files publishes dist/** + CHANGELOG.md, not the description), every conclusion survives, and each error runs conservative.

⚠️ The reviewer's framing is the part worth keeping: the repair invoked AGENTS.md #9 to replace answers with an instrument in all three shipped carriers, and then left three written-down answers about its own test file in the description. The rule was applied where it was being enforced and not where it was merely true.

Also recorded: the docblock's set identity JUDGED \ BOUND \ data (24) is one wider than the clause before it (23) — an antecedent to tighten, ⛔ not a false measurement; the adjacent bullet states the app exception.

② semver: .changeset/9318-field-rule-verdict-from-lint.md declares minor, and minor is right. Presence / no-major / claims / new-line-citations / control-bytes / test-path-roots all exit 0.

Card provenance re-confirmed at review time: objectui#9318 404, its three salvaged comments 404, objectui#9440 200 — ⛔ its body remains the only copy.

Carriers cleared from both this PR and card objectui#9440 in one pass. Landing next: ready → auto-merge → merge queue.


Generated by Claude Code

@os-sam
os-sam marked this pull request as ready for review September 14, 2026 05:52
@claude
claude Bot added this pull request to the merge queue Sep 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[rebuilt from objectui#9318] rowCanonAdvisory re-derives a verdict @objectstack/lint already publishes

2 participants