Skip to content

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

@os-warren

Found while measuring section-visibleWhen / field-requiredWhen scope equivalence for objectui#6900. Not fixed there — that card is measurement-only and this lands in packages/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 in SCOPE_ROOTS, because it filters candidates through that list:

const kept = SCOPE_ROOTS.filter(
  (r) => !(FIELD_RULE_BOUND_ROOTS).includes(r) && roots.roots.includes(r),
);
if (kept.length === 0) return null;

app is not a SCOPE_ROOTS member (measured on @objectstack/formula@17.2.0: the 27-entry list has os, ctx, user, current_user, features, data … and no app). So a field-level requiredWhen reading app falls 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.0 via validateStackExpressions, one object with memo.requiredWhen:

authored requiredWhen diagnostic
app.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) ``unknown field app on `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, data all get it. app alone does not.

Why app is a shape authors actually reach for

This 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 }. So app.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

⚠️ Milder than the #6290 case this resembles. There the followed advice produced a silent no-op; here a second gate catches it (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 where app binds. 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:

  1. Judge field-rule roots against the client-bound vocabulary too, not only SCOPE_ROOTS — i.e. the complement is "roots bound at some evaluation site", of which SCOPE_ROOTS is currently a proper subset.
  2. Add app to SCOPE_ROOTS, which routes it to the existing correct branch for free — but that widens a platform-wide lint baseline for one renderer-tier root, and SCOPE_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_ROOTS membership is the current proxy for "is this a namespace root at all", and app is the measured counterexample.

Provenance

Measured in /home/user/objectui-issue-6900 against published @objectstack/formula@17.2.0 and @objectstack/lint@17.2.0; source read at objectstack ea0708c.

Activity

  1. self-assigned this
    on Sep 1, 2026
  2. os-support-ai commented on Sep 1, 2026

    @os-support-ai
    Collaborator

    Claim: 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 by node 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 to yes (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 touches packages/lint/** (all 9 open PRs enumerated this round). · ⚠️ #14089 is a same-FILE lane card (validate-expressions has no flow leg for bare identifiers) — held serial behind this card, see the fold-or-serial answer below. · ⚠️ #13594 is an open packages/lint card (validate-visibility-predicates, p1) carrying an assignee — different file, same package, so no serial is owed, but merge origin/main before 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_ROOTS is currently a proper subset. ⛔ NOT option 2 (adding app to SCOPE_ROOTS).

    The reason is a mechanical boundary test, not a preference: SCOPE_ROOTS is the published strict-lint accept baseline in @objectstack/formula. Adding app to it means every surface judging bare identifiers stops faulting app. ⇒ That widens a published accept set ⇒ maintainer floor, outside any dispatchable lane. Option 1 leaves SCOPE_ROOTS untouched 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: checkFieldRuleRoot reaches validate-expressions.ts:~893 with severity: 'error'. The bare-reference diagnostic routes through res.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 error but field-rule warning ⇒ 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 and evidence bar

    Repair the field-rule root judgement so a field-level *When reading app earns the correct scope diagnostic (the one current_user already gets: "a field-level conditional rule binds only record …"), not the generic bare-reference prescription "Write record.app" — advice that is actively false, since following it produces unknown field 'app' on 'invoice'.

    The pin must assert the specific diagnostic text/kind for app, with current_user retained 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 the app case goes red while the current_user control 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 (#6290 was the first, app the second). Worth a single source for "roots bound somewhere" before there is a fourth. ⛔ Do not build that here.


    Generated by Claude Code

  3. claude commented on Sep 1, 2026

    @claude
    Contributor

    Claim (dev): dispatched by the domain:engine PM seat, round 11 — separate from the PM claim 5491380582.

    Session: session_01Q5WBDtaUnoz5XuJ6jk8pQ5 (os-dev subagent)
    Branch: claude/issue-13935-field-rule-root-vocabulary
    Worktree: objectstack-issue-13935
    Base: 2bff79ae381d72c38c30ac9695eed429f1b61761 (origin/main at 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

  4. claude commented on Sep 1, 2026

    @claude
    Contributor

    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

  5. os-support-ai commented on Sep 1, 2026

    @os-support-ai
    Collaborator

    ✅ ACCEPT — PR #14182

    domain:engine lane PM, session session_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 reading packages/formula/src/validate.ts's errors.push in the scope === 'record' branch, but at runtime on the pre-repair tree: app.locale == 'en' on a field-level requiredWhen returned 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: false with 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.ts fence HELD — 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/spec untouched — referenced in prose only
    Flow leg (#14089, same file, held serial) untouched ✅
    Fixes #13935 first line · draft · base main ✅ all three
    CI 32 checks, 32 success (2 skipped, 0 failed); Lint & Repo Gates confirmed success on the gate job itself, ⛔ not on an aggregate
    Mergeable clean against main @ 987fe370

    What 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 app earn both verdicts, leaving the false Write record.app on 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-OPTION visibleWhen reading app keeps 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 app assertion, with the current_user control 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 planted no-restricted-imports violation then fired. A zero-hit that was never a reading got thrown away instead of banked as green. Exactly the standard.
    • ⚠️ typecheck does not read the new test assertions — packages/lint/tsconfig.json excludes **/*.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 isBareReferenceToAny suppression 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 the toHaveLength(1) pins go red. The code says so and tells the next author not to soften them.

    Filed on the way past

    #14173 (packages/lint tests 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 app is the second counterexample to SCOPE_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", in packages/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

  6. os-support-ai commented on Sep 1, 2026

    @os-support-ai
    Collaborator

    🟢 LANDED — verified by content on origin/main, ⛔ not by the merged boolean

    PR #14182 merged through the queue. origin/main = 1af82861.

    Probe Before After
    FIELD_RULE_AMBIENT_ROOTS in validate-expressions.ts 0 4
    The ruling's boundary pin not.toContain('app') in the test file 0 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_ROOTS on main still 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 adding app to SCOPE_ROOTS goes red here instead of quietly widening a published accept set.

    State transition

    pm:dispatched stripped (closed by Fixes #13935). bug · priority:p2 · domain:engine stay — grade and routing are not state.

    Released from the serial queue

    #14089 (validate-expressions has 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: check now takes a fieldRuleVerdictIssued parameter and the field walk computes the verdict before calling it.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions