Repository navigation
lint: a field-level *When reading app gets the generic bare-reference prescription ("Write record.app") — the #6290 misprescription class, one root over #13935
Description
Activity
os-support-ai commented
on Sep 1, 2026 CollaboratorMore actionsClaim: PM loop round 11
Session:session_01Q5WBDtaUnoz5XuJ6jk8pQ5
Branch:claude/issue-13935-field-rule-root-vocabulary
Worktree:objectstack-issue-13935
Domain:domain:engine
File surface:packages/lint/src/validate-expressions.ts+ its test file · ⛔packages/formula/src/cel-engine.ts(SCOPE_ROOTS) READ-ONLY — excluded from writes, this is the ruling's whole point (stop on breach; explain in the report)
Container & model:S/M,mode:subagent,model: opus— tier derived this round bynode scripts/pm/dispatch-gates.mjs --tier packages/lint/src/validate-expressions.ts: "no path-derived mandate … floor sonnet · default opus · ceiling fable", ⛔ not recalled. ⛔ Not the sonnet floor: this card carries a conditioned ruling whose premise the dev must measure and may have to refuse on — that is judgement, not a mechanical edit.
Clause-②: no — conditional, and the condition is the card's premise. Option 1 changes which diagnostic a field-rule root receives; the lint accept set does not move.⚠️ It flips toyes(and to a maintainer floor) if the premise below is false — see ②. Re-declare from the actual diff at PR time.
Serial constraints cleared: no open PR touchespackages/lint/**(all 9 open PRs enumerated this round). ·⚠️ #14089 is a same-FILE lane card (validate-expressionshas no flow leg for bare identifiers) — held serial behind this card, see the fold-or-serial answer below. ·⚠️ #13594 is an openpackages/lintcard (validate-visibility-predicates, p1) carrying an assignee — different file, same package, so no serial is owed, but mergeorigin/mainbefore opening the PR.
fold-or-serial: SERIAL, and the answer is required rather than defaulted
#13935 and #14089 both edit
packages/lint/src/validate-expressions.ts, so the five admission gates get an explicit answer instead of a silent default:gate verdict ① same defect shape, same repair ❌ FAILS — #13935 is a root-vocabulary judgement (which diagnostic a known-but-unlisted root earns); #14089 is a missing leg (flow conditions never reach the bare-identifier check at all). Different mechanisms, different repairs. ② same package/region ✅ same file ③ every member already adjudicated ✅ both graded ④ each member independently checkable ✅ ⑤ exclusion list nameable ✅ ⇒ Gate ① is a load-bearing gate and it fails. Serial, not folded. #14089 waits for this card to land; ⛔ this dev does not touch the flow leg, and ⛔ does not "while I'm in here" fix it.
⚖️ The ruling — third tier: a ruling, a falsifiable premise, and a prohibition. All three bind.
Triage ruled this on 2026-09-01 (comment
5486907899). Carried across in substance, ⛔ not re-adjudicable:① RULED: take option 1 — judge field-rule roots against the "roots bound at some evaluation site" vocabulary, of which
SCOPE_ROOTSis currently a proper subset. ⛔ NOT option 2 (addingapptoSCOPE_ROOTS).The reason is a mechanical boundary test, not a preference:
SCOPE_ROOTSis the published strict-lint accept baseline in@objectstack/formula. Addingappto it means every surface judging bare identifiers stops faultingapp. ⇒ That widens a published accept set ⇒ maintainer floor, outside any dispatchable lane. Option 1 leavesSCOPE_ROOTSuntouched and only changes which diagnostic the field-rule check emits.② THE PREMISE — measure it FIRST, before writing any repair: the two diagnostics carry the same severity.
Known:
checkFieldRuleRootreachesvalidate-expressions.ts:~893withseverity: 'error'. The bare-reference diagnostic routes throughres.errors/res.warnings(:~875/:~876) and triage could not cheaply pin which side it lands on. Triage labelled that honestly as unpinned rather than assuming — ⛔ do not inherit a guess in either direction. (Line numbers are from triage's read; locate by symbol — this lane just measured a 55-line drift on another card.)- Both
error⇒ option 1 only changes wording, the accept set is unchanged ⇒ proceed, implement ①. - Bare-reference
errorbut field-rulewarning⇒ option 1 would downgrade an error to a warning ⇒ that is a gate weakening, which is a maintainer floor.
③ IF THE PREMISE FAILS: ⛔ stop and report a fork. Do NOT self-adjudicate, and do NOT quietly fall back to option 2. Both options landing on the maintainer floor means this card is promoted to a decision item — that is not the implementer's call to make. Report it; this seat routes it.
What the card has already established — ⛔ do not re-derive
Triage re-verified every measurement in the card, so treat these as given and spend the round on the repair:
SCOPE_ROOTS(packages/formula/src/cel-engine.ts:~74) has 27 entries andappis not among them.- ⭐ The strongest evidence is
SCOPE_ROOTS' own comment, which claims completeness: "the last one this list was missing (finding: packages/formula 一包两话 —— SCOPE_ROOTS 不含 current_user 而 introspectScope 宣告它;字段级 visibleWhen 的 lint 拒绝还附错误修法「Write record.current_user」 #6290) … One package, two accounts of the same root."appis that claim's counterexample — this is the second falsification of the same sentence, not an analogy to it. - objectui's
buildExpressionScopebinds 7 roots as one group (current_user, user, ctx, os, app, data, features);SCOPE_ROOTScovers 6.appis the only gap in that group.
Scope and evidence bar
Repair the field-rule root judgement so a field-level
*Whenreadingappearns the correct scope diagnostic (the onecurrent_useralready gets: "a field-level conditional rule binds onlyrecord…"), not the generic bare-reference prescription "Writerecord.app" — advice that is actively false, since following it producesunknown field 'app' on 'invoice'.The pin must assert the specific diagnostic text/kind for
app, withcurrent_userretained as the positive control that already passes. ⛔ A test that only asserts "some diagnostic fires" does not measure this card — the defect is which diagnostic fires. Ablate: revert the repair and show theappcase goes red while thecurrent_usercontrol stays green.📌 Structural note for the record, ⛔ not this card's work: option 1 is the third sighting of #6713's point — a hand-maintained list doing a per-surface job drifts (
#6290was the first,appthe second). Worth a single source for "roots bound somewhere" before there is a fourth. ⛔ Do not build that here.
Generated by Claude Code
- Both
Claim (dev): dispatched by the
domain:enginePM seat, round 11 — separate from the PM claim5491380582.Session:
session_01Q5WBDtaUnoz5XuJ6jk8pQ5(os-dev subagent)
Branch:claude/issue-13935-field-rule-root-vocabulary
Worktree:objectstack-issue-13935
Base:2bff79ae381d72c38c30ac9695eed429f1b61761(origin/mainat claim time)Write surface:
packages/lint/src/validate-expressions.ts+ its test file (+ a changeset).
⛔packages/formula/src/cel-engine.ts(SCOPE_ROOTS) is READ-ONLY for this card — that is the ruling's whole point.
⛔ Not touching the flow leg (held serial behind this card).Order of work, per the ruling: measure premise ② first (the severity of both diagnostics, located by symbol rather than by the line numbers in triage's read), and only then write a repair. If the premise fails, this returns a fork with no PR rather than a self-adjudicated fallback.
Empty branch pushed before the first edit as the write-route probe; it is green.
Generated by Claude Code
os-dev-report
{ "issue": 13935, "status": "done", "branch": "claude/issue-13935-field-rule-root-vocabulary", "pr": "https://github.com/objectstack-ai/objectstack/pull/14182", "premise_still_valid": true, "Clause-2": "no", "severity_reading_deciding_premise_2": "BOTH ARE 'error' — premise holds, option 1 proceeds. Located by symbol, not by triage's line numbers. (a) checkFieldRuleRoot (now fieldRuleRootVerdict + its push site) pushes severity:'error', unchanged by this PR. (b) The bare-reference diagnostic is produced in packages/formula/src/validate.ts inside the `schema?.scope === 'record'` branch via errors.push({...}); its own comment reads 'In a record-scoped site a bare top-level identifier is a silent bug ... Hard error.' packages/lint maps res.errors to severity:'error' and res.warnings to 'warning', so it lands on the ERROR side. Confirmed at runtime on the PRE-repair tree, not only by reading: app.locale=='en' on a field-level requiredWhen returned exactly 1 issue with severity 'error' carrying the bare-reference text; current_user.id=='U1' returned exactly 1 issue with severity 'error' carrying the scope text. Same severity, different message ⇒ option 1 changes wording only, no accept set moves, no gate is weakened.", "files_changed": [ "packages/lint/src/validate-expressions.ts", "packages/lint/src/validate-expressions.test.ts", ".changeset/lint-field-rule-ambient-roots.md" ], "summary": "Premise measured FIRST, before any repair, and it holds — both diagnostics are severity 'error', so option 1 only changes which message fires. Implemented ①: the field-rule root judgement now runs against FIELD_RULE_JUDGED_ROOTS = SCOPE_ROOTS + FIELD_RULE_AMBIENT_ROOTS(['app']), assembled in packages/lint. packages/formula/src/cel-engine.ts is UNTOUCHED and a pin asserts SCOPE_ROOTS still does not contain 'app', so the published accept baseline does not move. PM's unverified assumption TESTED AND CONFIRMED: the vocabulary has an in-repo source — packages/spec/src/ui/page.zod.ts's 'Ambient roots — renderer behaviour, NOT contract-guaranteed' section names app/features/os.user as ExpressionProvider-mounted at a pinned objectui sha; only 'app' is outside the baseline, so no objectui list is hand-copied. Two halves were needed, not one: widening the vocabulary alone would have made 'app' earn BOTH the scope verdict and the bare-reference verdict, keeping the false 'Write record.app' advice on screen — so the walk now computes the verdict first and check() drops bare-reference errors naming an ambient root. Ambient roots also get a prescription tier of their own, because the existing general tier ('declared platform-wide ... bound at flow/automation/screen/action sites') is false for them in both clauses. Flow leg untouched.", "tests": "All readings from the final commit 74cce241 (merged origin/main b003cf2e8 first, as dispatched; union re-run on the new head after the merge, gate family re-derived and byte-identical). PACKAGE: `pnpm --filter @objectstack/lint test` → 88 files / 2457 tests passed, 0 failed. `pnpm --filter @objectstack/lint typecheck` → clean. SECOND CONSUMER: `pnpm --filter @objectstack/lint check:doc-formula-expressions` (imports fieldRuleRootIssue from this package's dist) → self-test 58 cases pass; corpus clean, incl. '14 predicate(s) on a statically determinable field layer judged clean'. RED-BEFORE: on the pre-repair tree, app.locale=='en' returned the bare-reference message with 'Write `record.app`.'; record.app=='en' returned 'unknown field `app` on `invoice`' — the card's claim reproduced exactly. ABLATION (on the COMMITTED tree, one line: FIELD_RULE_JUDGED_ROOTS → SCOPE_ROOTS at the filter site): mutation confirmed ON DISK before running anything, anchored to the text changed in BOTH directions (new spelling 1→0 hits, old 0→1) and by blob hash 56f78d28…→3a8feaff…; NO REBUILD is owed and none was done because the test imports the subject RELATIVELY ('./validate-expressions.js'), so vitest resolves it from source rather than through a package exports into dist/ — and the mutation leg PROVES that rather than asserting it, since the run went red with no build in between. Result: 4 failures, every one an `app` assertion; the current_user positive control stayed GREEN, as did the vocabulary-shape pin and the per-option blast-radius pin. RESTORE proven, not assumed: `git checkout HEAD -- <absolute path>` from a trap armed before the mutation, then `git diff HEAD` = 0 BYTES, `git status` clean, restored blob hash back to 56f78d28…. GATES: 33 derived at the actual diff via `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands`, exit codes captured by redirect BEFORE any pipe → 30 pass, 3 PREREQUISITE NOT MET (never counted as passes). ESLINT: narrowed and the narrowing is declared with all three evidences — see `deviations`.", "gates": { "derived_at": "node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands, at commit 74cce241, change set 3 paths vs merge base b003cf2e8 (three-dot)", "total": 33, "passed": 30, "failed": 0, "not_measured": [ "scripts/check-test-completeness.mjs — exit 3, its own text: 'the local reading for this gate is NOT MEASURED ... It is not a red'; needs a saved `turbo run test` log", "check:dual-build-cjs-loads — exit 3, 'PREREQUISITE NOT MET — this gate reads built output, and some package has no dist/'; needs a full `pnpm build`", "check:type-check-debt — exit 3, 'PREREQUISITE NOT MET ... 52 workspace dependenc(ies) ... have no built type entry point on disk'; needs the built closure" ], "ratchet_family_on_final_head": "check:type-check-coverage PASS · check:query-options-erasure PASS · check:test-source-alias PASS · check:cross-package-test-inputs PASS · check:slot-lookup PASS · check:where-matcher PASS · check:type-check-debt NOT MEASURED. Re-run on 74cce241 AFTER the origin/main merge, which itself touched scripts/query-options-erasure-baseline.json — results identical to the pre-merge run.", "beyond_the_derived_list": "check:doc-formula-expressions is NOT path-derived onto this diff but consumes fieldRuleRootIssue from @objectstack/lint's dist, so it was run anyway and is green." }, "deviations": [ "ESLINT NARROWED, DECLARED. Repo-wide `pnpm lint` was not run; eslint was run on the changed files only. Three evidences, all present: (1) POPULATION — the repo runs one eslint.config.mjs that never enables type-aware linting for any file (no parserOptions.project, no typed @typescript-eslint rules), stated and positive-control-measured in that config's own header, so no rule reads across files and this diff cannot move an untouched file's verdict; (2) COUNT — `--format json` over the 3 changed paths: 2 linted, 0 errors / 0 warnings, the changeset .md reported 'File ignored because no matching configuration was supplied' (outside eslint's population by config, not by the narrowing); (3) POSITIVE CONTROL — the first control chosen (`debugger;`) did NOT fire, because only 6 rules are enabled for this path, so that zero-hit was discarded rather than reported as green; a planted no-restricted-imports violation then fired (exit 1, 1 error) and was restored with `git diff HEAD` 0 bytes.", "TYPECHECK IS NOT MEASURED FOR THE TEST FILE. packages/lint/tsconfig.json excludes **/*.test.ts with no sibling test project, verified with --listFiles (validate-expressions.test.ts: 0 hits; validate-expressions.ts: 1 hit). So 'typecheck clean' says nothing about the new assertions. Pre-existing, not introduced here; filed as #14173.", "ABLATION SCOPE. The ablation reverted the VOCABULARY half (the filter site). The disjointness half (bare-reference suppression) was deliberately left in, which is why the tie-break test stayed green under ablation; that half is pinned separately by the two blast-radius tests (per-OPTION surface keeps its bare reference; a plain bare field reference on a field-rule slot is untouched).", "TEST EXPECTATIONS CORRECTED MID-ROUND, and both corrections were findings rather than fudges. (a) A predicate reading a baseline root AND an ambient one kept the bare reference for the ambient one, re-emitting the exact false record.app prescription — so the suppression was rescoped from 'the root the tie-break named' to 'the ambient roots, gated on a verdict having been issued'. (b) formula emits ONE bare-reference error per expression (firstUndeclaredReference), so a second bare name in the same predicate never had its own verdict pre-change either; that test was replaced with the two blast-radius pins.", "#5017 RECEIVER SCAN, twice. The new message string registered `page` (from a `page.zod` spelling) and `record` (from a `record.${root}` template literal, since $ is an identifier char) as read receivers — the exact trap the file's existing sectionFields / *.form comments warn about. Fixed by naming the spec module in prose and assembling the record. spelling with `+`. The two new locals are named `verdict` / `diagnostic` rather than `message` so excusing them in the plumbing list cannot mask a genuine validations[].message read." ], "mcp_calls": "0 — every GitHub read and write (card body, both comments, dedup listing, claim comment, filed finding, PR create, PR read-back, this report) went through repo-scoped REST after a 200 probe; zero MCP GitHub calls, zero GraphQL quota.", "open_questions": [], "out_of_scope_findings": [ "filed as #14173: packages/lint has no tsc program that compiles its tests — tsconfig excludes **/*.test.ts, no sibling test project, no test-typecheck ledger, so the ~2,700-line pin file (the #5017 receiver scan, TRACKED_UNDECLARED_READS, the residual-root table) is type-checked by nobody. Same class as #14062 (packages/plugins/**), #13676 (objectql), #12542 (rest, closed); packages/lint is named by none of them. Deduped against 444 open issues pulled via REST and grepped locally. Label `finding`, unassigned, no severity proposed." ], "structural_note_not_built": "Third sighting of #6713's point. SCOPE_ROOTS' own comment claims 'the last one this list was missing (#6290)'; `app` is that sentence's SECOND counterexample. The clean shape: an exported constant for 'roots bound somewhere' in packages/spec, beside the page-component schema that already documents the ambient set in prose, read by both packages/lint and the renderer — so the docblock and the vocabulary cannot disagree. Deliberately not built here." }
Generated by Claude Code
os-support-ai commented
on Sep 1, 2026 CollaboratorMore actions✅ ACCEPT — PR #14182
domain:enginelane PM, sessionsession_01Q5WBDtaUnoz5XuJ6jk8pQ5, R11. Reviewer of record. Checked against GitHub and the diff, ⛔ not against the report's self-description.The ruling's condition — met, and this is the whole review
The triage ruling was third-tier: option 1 was dispatchable only if both diagnostics carry the same severity, because otherwise the change downgrades an error to a warning, which is a gate weakening and a maintainer floor. Triage recorded the bare-reference side as unpinned rather than guessing it.
Measured: both are
error. And measured the right way — not only by readingpackages/formula/src/validate.ts'serrors.pushin thescope === 'record'branch, but at runtime on the pre-repair tree:app.locale == 'en'on a field-levelrequiredWhenreturned exactly one issue,severity: "error", bare-reference text;current_user.id == 'U1'returned exactly one issue,severity: "error", scope text. ⇒ Same severity, different message ⇒ no accept set moves and no gate is weakened. The premise holds and the PR is legitimate rather than a gate weakening in disguise.⭐ Worth naming: had this come back the other way, the correct delivery was
premise_still_valid: falsewith no PR. The dispatch said so and the round was run in that order — premise first, repair second. That ordering is why this is reviewable at all.Spot-checks I ran myself
Check Reading Path face ( get_files, ⛔ not the report)3 files — packages/lint/src/validate-expressions.ts, its test, one changeset. No governed surface ⇒ ready→enqueue path, no maintainer fork⛔ packages/formula/src/cel-engine.tsfenceHELD — absent from the diff. ⭐ And pinned rather than promised: expect(SCOPE_ROOTS).not.toContain('app')goes red on exactly the "simplification" the ruling forbade⛔ packages/specuntouched — referenced in prose only Flow leg (#14089, same file, held serial) untouched ✅ Fixes #13935first line · draft · basemain✅ all three CI 32 checks, 32 success (2 skipped, 0 failed); Lint & Repo Gatesconfirmedsuccesson the gate job itself, ⛔ not on an aggregateMergeable cleanagainstmain@987fe370What I checked hardest, because it is the half that could go wide
The repair needed two changes, not one — the vocabulary widening alone would have made
appearn both verdicts, leaving the falseWrite record.appon screen. So the walk suppresses bare-reference errors naming an ambient root when a field-rule verdict fired. A suppression is exactly the kind of thing that quietly swallows unrelated diagnostics, so I read its blast radius rather than taking it on trust: it is gated on a field-rule verdict, and pinned in both directions — a per-OPTIONvisibleWhenreadingappkeeps its bare reference, and a plain bare field reference on a field-rule slot is untouched.The ablation discriminates: reverting the one mechanism line gave 4 failures, every one an
appassertion, with thecurrent_usercontrol staying green. That is the difference between "some diagnostic fires" and "the right diagnostic fires", which is what this card is about.Deviations — accepted, and two of them are the round's best work
- ⭐ The ESLint positive control did not fire, and the zero was discarded rather than reported. The first control (
debugger;) produced no hit because only 6 rules are enabled on that path; a plantedno-restricted-importsviolation then fired. A zero-hit that was never a reading got thrown away instead of banked as green. Exactly the standard. ⚠️ typecheckdoes not read the new test assertions —packages/lint/tsconfig.jsonexcludes**/*.test.ts, verified with--listFiles(test file: 0 hits). Stated plainly rather than glossed, and filed as [finding] packages/lint has no tsc program that compiles its tests — the 2,700-line rule pin file, receiver scan included, is type-checked by nobody #14173. Pre-existing, not introduced here, and ⛔ not this card's to fix.- The
isBareReferenceToAnysuppression matches on an upstream message prefix, which is fragile. Accepted because the fragility is loud, not silent: an upstream reword makes the suppression miss, the ambient root earns two diagnostics, and thetoHaveLength(1)pins go red. The code says so and tells the next author not to soften them.
Filed on the way past
#14173 (
packages/linttests are in no tsc program) — a real finding, deduped, unassigned, ⛔ correctly left for triage to grade.📌 Structural note recorded, ⛔ not built: this is the third sighting of #6713's point, and
appis the second counterexample toSCOPE_ROOTS' own "the last one this list was missing" claim — not an analogy to #6290 but the same sentence falsified again. The clean shape (a single declared source for "roots bound somewhere", inpackages/spec, read by both lint and the renderer) is worth a card before there is a fourth. Not this PR's job.⇒ Driving to land: ready → merge queue.
Generated by Claude Code
- ⭐ The ESLint positive control did not fire, and the zero was discarded rather than reported. The first control (
os-support-ai commented
on Sep 1, 2026 CollaboratorMore actions🟢 LANDED — verified by content on
origin/main, ⛔ not by themergedbooleanPR #14182 merged through the queue.
origin/main=1af82861.Probe Before After FIELD_RULE_AMBIENT_ROOTSinvalidate-expressions.ts0 4 The ruling's boundary pin not.toContain('app')in the test file0 1 Both discriminate — absent before, present after — so they distinguish landed from not-landed rather than merely returning a number.
⭐ The check that actually mattered, taken AFTER the merge
⛔
SCOPE_ROOTSonmainstill does not contain'app'— measured post-merge, not inferred from the pre-merge diff.That is the one reading worth taking twice on this card. The ruling's whole content was "widen the vocabulary this package assembles, and leave
@objectstack/formula's published accept baseline alone" — and a fence like that is precisely what a bad rebase or a queue-time resolution breaks silently, since both sides look plausible. The boundary pin also landed, so the fence is now enforced by the suite rather than by anyone remembering it: a future edit that "simplifies" this by addingapptoSCOPE_ROOTSgoes red here instead of quietly widening a published accept set.State transition
pm:dispatchedstripped (closed byFixes #13935).bug·priority:p2·domain:enginestay — grade and routing are not state.Released from the serial queue
#14089 (
validate-expressionshas no flow leg for bare identifiers) was held serial behind this card — same file, and fold-or-serial was answered serial because admission gate ① fails (different defect shape, different repair). ⇒ It is now free to dispatch.⚠️ Whoever takes it should note that this card changed the file's structure:checknow takes afieldRuleVerdictIssuedparameter and the field walk computes the verdict before calling it.
Generated by Claude Code
- added a commit that references this issue
on Oct 9, 2026
Found while measuring section-
visibleWhen/ field-requiredWhenscope equivalence for objectui#6900. Not fixed there — that card is measurement-only and this lands inpackages/lint. Filed unassigned for triage.What was measured
fieldRuleRootIssue(packages/lint/src/validate-expressions.ts) produces the good field-level diagnostic only for roots it can see inSCOPE_ROOTS, because it filters candidates through that list:appis not aSCOPE_ROOTSmember (measured on@objectstack/formula@17.2.0: the 27-entry list hasos,ctx,user,current_user,features,data… and noapp). So a field-levelrequiredWhenreadingappfalls through to the generic bare-reference check and is told to write a shape that binds on no layer.Repro against published
@objectstack/lint@17.2.0viavalidateStackExpressions, one object withmemo.requiredWhen:requiredWhenapp.locale == 'en'bare reference `app` — … so `app` resolves to nothing and the expression silently evaluates to null. Write `record.app`.record.app == 'en'(following that advice)appon `invoice```current_user.id == 'U1'(control)`requiredWhen` reads `current_user`, but a field-level conditional rule binds only `record` (plus `previous`, and `parent` on a master-detail line item) …The control shows the correct message exists and fires for every root that IS in
SCOPE_ROOTS—current_user,user,ctx,os,features,dataall get it.appalone does not.Why
appis a shape authors actually reach forThis is not a hypothetical root. objectui's
buildExpressionScope(packages/app-shell/src/providers/ExpressionProvider.tsx) binds it:{ current_user, user, ctx: { user }, os: { user }, app, data, features }. Soapp.locale == 'en'is a working predicate on a form view's section, and an author moving that decision down to a server-enforced field rule is exactly the path that meets this message.Severity — deliberately stated low
unknown field 'app' on 'invoice'), so the author is not left with a silently inert rule. The cost is a misleading first diagnostic and a wasted correction cycle, and the message actively asserts something false about whereappbinds. Diagnostic quality, not a correctness hole. ⛔ I am not proposing a severity — triage's call.The fix shape, not built
Two candidates, no recommendation without the owning seat's read:
SCOPE_ROOTS— i.e. the complement is "roots bound at some evaluation site", of whichSCOPE_ROOTSis currently a proper subset.apptoSCOPE_ROOTS, which routes it to the existing correct branch for free — but that widens a platform-wide lint baseline for one renderer-tier root, andSCOPE_ROOTS' own doc calls itself a deliberately generous "never faults" baseline, so this may be the wrong lever.Note (1) restates the structural point #6713 already made for this rule: a hand-maintained list doing a per-surface job drifts.
SCOPE_ROOTSmembership is the current proxy for "is this a namespace root at all", andappis the measured counterexample.Provenance
Measured in
/home/user/objectui-issue-6900against published@objectstack/formula@17.2.0and@objectstack/lint@17.2.0; source read atobjectstackea0708c.