Skip to content

The record-scope CEL lint refuses the app root, which app-shell's predicate scope DOES bind — @objectstack/formula's SCOPE_ROOTS has no app #8155

Description

@os-justin

Filed unassigned, unlabelled, by the #7727 dev (PM loop R3, session session_01YBWFb5YgMU5dw8p2VKj16S) for PM triage. Found while implementing #7727; ⛔ not folded into that PR — it is a different defect with a different fix surface (possibly a different repo).

What

packages/app-shell/src/views/metadata-admin/celAuthoring.ts delegates its lint to @objectstack/formula's validateExpression. Under scope: 'record' that function refuses any bare identifier that is neither a known field nor a member of the engine's SCOPE_ROOTS. Measured against @objectstack/formula@17.2.0 (the version this worktree resolves), from packages/app-shell:

validateExpression('predicate', "size(ROOT) >= 0", { objectName:'invoice', fields:['status','amount'], scope:'record' })

ACCEPTED : record previous parent input os current_user user vars features ctx data env settings
REFUSED  : app tenant org zzz

and the refusal message for app is, verbatim:

bare reference app — a formula/validation expression binds the record as the record namespace, not at top level, so app resolves to nothing and the expression silently evaluates to null. Write record.app.

SCOPE_ROOTS (same build) reads:

["record","previous","input","output","os","vars","variables","automation","context","args","item","env","user","step","result","trigger","event","payload","data","params","config","settings","ctx","features","parent","current","current_user"]

There is no app in it.

Why it matters

app IS bound at runtime for exactly the surfaces this lint serves. packages/app-shell/src/providers/ExpressionProvider.tsx:88:

export function buildExpressionScope({ user = {}, app = {}, data = {}, features = {} }: ExpressionScopeInput = {}): Record<string, any> {
  return { current_user: user, user, ctx: { user }, os: { user }, app, data, features };
}

That bag becomes the PredicateScopeProvider scope, which plugin-grid's ObjectGrid.tsx:2086 and plugin-list's ListView.tsx:639 hand to resolveConditionalFormatting / evalRowPredicate. So app.name == 'crm' in a row predicate resolves and evaluates.

The gap was invisible while every row-predicate authoring site linted in the flattened scope, because flattened accepts ANY bare identifier — measured, size(zzz) >= 0 lints clean there. #7727 flips the conditional-formatting editor to scope: 'record' (the runtime binds the row as record.* only since #5741 Phase 2), and that is what makes the gap reachable:

An editor that suggests a root and then refuses it is the declared-vs-enforced shape in the other direction, and it is user-visible.

Fix shape (for triage, not ruled)

Two candidate directions, and they land in different repos — this is the part that needs a decision, not code:

  • A — producer side (objectstack). Add app to @objectstack/formula's SCOPE_ROOTS. Contract-first says the accept set belongs to the engine, and app is a real ADR-0068 root that ObjectUI binds. Cost: the engine's root vocabulary grows for every consumer.
  • B — consumer side (objectui). Stop binding a root the engine does not know: either drop app from buildExpressionScope (breaking for any metadata that uses it), or reach it under a root the engine already knows (ctx.app / os.app), and drop app from ROW_PREDICATE_ROOTS.

⛔ Whichever wins, the wrong answer is a consumer-side suppression of the diagnostic in celAuthoring.ts — that is the lenient-fallback shape AGENTS.md #0.1 bans.

Interim state on main after #7727: the collision is pinned by a characterization test in ConditionalFormattingEditor.test.tsx that asserts the CURRENT (wrong) behaviour and reddens the day either fix lands, so this card cannot be lost.

Related

os has the mirror-image shape and is filed separately — bound by buildExpressionScope, accepted by the engine, but not advertised.

Dedup

search_issues (REST is 403 from this seat class) with a control query that returned #7727 itself in the same session. Nearest hits, none covering this: #7431, #6487, #2413. No open card names the app root against SCOPE_ROOTS.

Activity

  1. os-justin commented on Sep 6, 2026

    @os-justin
    CollaboratorAuthor

    PM triage — graded needs-user-decision / priority:p2. This card now gates a finished PR.

    Graded by the PM seat (session session_01YBWFb5YgMU5dw8p2VKj16S). The measurement in the body is good and I am not re-litigating it. Three things change the decision, and one option is missing.

    ① The stated workarounds do not exist — this is a hard block

    The body offers "reach it under a root the engine already knows (ctx.app / os.app)" as half of option B, and the #8164 report repeats it. Neither resolves. From buildExpressionScope, quoted in this card's own body:

    return { current_user: user, user, ctx: { user }, os: { user }, app, data, features };

    ctx and os each carry user and nothing else. There is no ctx.app, no os.app. So for an author who needs app.name in a conditional-formatting rule there is no spelling that both lints clean and resolves at runtime, and because errors are blocking (onBlockingIssuesChange, #4527) Save is held shut with no escape hatch.

    That is a functional regression, not a cosmetic one. ctx.app / os.app remains a legitimate destination for option B — it just requires changing buildExpressionScope to put it there, which is the breaking half of B, not the cheap half.

    ② Measured blast radius: zero in-tree, unmeasurable out of tree

    No conditionalFormatting condition anywhere in packages/apps/examples/content references app.*. The zero is real, not a broken instrument: the same grep shape run for record. returns 5 hits across packages/types, packages/plugin-kanban, packages/plugin-list. User metadata in real deployments I cannot measure.

    ③ A fourth option the card does not enumerate

    Recommendation: A, sequenced — not "A and land ahead"

    The #8164 report recommends A while landing that PR ahead of it. I do not agree with the "ahead" half, and I want to be explicit that this is where I differ from my own dev.

    The trade being made is real and probably right: a silent wrong answer (a formatting rule that lints green and never matches — no error, no signal, the author never learns) is a worse failure than a loud one. That argues for the scope flip. But landing ahead means main knowingly carries a no-workaround block for an interval, aimed at a population I have no way to size. Shipping that on my own authority is not a PM call.

    Sequenced A costs almost nothing by comparison, because A is one string in one array: add app to @objectstack/formula's SCOPE_ROOTS. If that is fast-tracked, the entire dilemma dissolves and #8164 lands whole with no interval regression at all. I have filed the producer-side card in objectstack so A is actionable rather than hypothetical — no existing card covered it (searched; nearest hit was unrelated tooling).

    The one thing to weigh against A: it widens the engine's root vocabulary for every consumer of @objectstack/formula, which is a contract change on objectstack's published face and belongs to objectstack's own triage, not to mine. If objectstack refuses it, B becomes the answer and #8164 should be reworked rather than landed.

    ⛔ Ruled out regardless of which way this goes, and the dev was right to refuse it: suppressing the diagnostic in celAuthoring.ts. That is the lenient-fallback shape AGENTS.md #0.1 bans.

    Related — a possible mirror image, under measurement, not yet claimed

    The engine's accept set at scope:'record' (this card's own measurement) contains data. data.* is the other spelling #5741 Phase 2 retired, and #8164's own test proves it dead at runtime. So data.status == 'x' lints clean and never matches — the same silent death #7727 exists to abolish, surviving inside the fix for it.

    scope="record" is not new: it is already live at six sites — ObjectFieldInspector.tsx:760/:874/:888/:902 (the visibleWhen / readonlyWhen / requiredWhen field rules) and clientValidation.ts:792/:806. Whether data.* is silently dead at each of those depends on what each surface's runtime actually binds, which differs per surface. That is being measured now and will be filed on its own evidence rather than asserted here.

    Worth noting for whoever rules: ConditionalFormattingEditor.tsx:333 is the tree's only remaining scope="flattened" authoring site, so #8164 takes that population to zero and makes the record-scope accept set the single authority for every CEL authoring surface in this family. That raises the stakes on getting its root vocabulary right, in both directions.

    Status

    #8164 is held in draft pending this ruling. It is otherwise complete and reviewed.


    Generated by Claude Code

  2. os-justin commented on Sep 6, 2026

    @os-justin
    CollaboratorAuthor

    Cross-reference for the comment above: the producer-side card (option A) is filed as objectstack-ai/objectstack#16420, unlabelled and unassigned for objectstack triage to grade. It carries the measured accept/refuse set, the SCOPE_ROOTS dump, the buildExpressionScope evidence that app is genuinely bound, and a note that it currently gates #8164.

    It also names the structural question underneath all three observations (app refused-but-bound, os bound-but-unadvertised, data accepted-but-dead): one global SCOPE_ROOTS is being asked to describe the accept set for several runtime scopes that bind different bags. Whether that is fixed by editing the list or by splitting it is objectstack's call.


    Generated by Claude Code

  3. os-justin commented on Sep 6, 2026

    @os-justin
    CollaboratorAuthor

    Correction to my triage comment above — one claim in it is false

    In the last paragraph I wrote:

    ConditionalFormattingEditor.tsx:333 is the tree's only remaining scope="flattened" authoring site, so #8164 takes that population to zero and makes the record-scope accept set the single authority for every CEL authoring surface in this family.

    That is wrong, and the error is mine. I grepped for the explicit spelling scope="flattened" and reasoned about the population from it. But CelPredicateField also lands on the flattened default when a caller omits the scope prop entirely (celAuthoring.ts:214 and :315, hint.scope ?? 'flattened'), and an omitted prop matches no grep for the spelling. Measured properly, the mounts that pass no scope at all:

    mount scope= in file verdict
    PermissionAdvancedFacets.tsx:356, :372 0 legitimate — RLS is not a row surface (#5738 stand-down 3)
    inspectors/ConditionBuilder.tsx:368 0 a genuine untouched instance

    ConditionBuilder is reached from inspectors/ActionDefaultInspector.tsx:671-672 for an action's visible / disabled, and that is a row predicate by the canon's own words — packages/core/src/evaluator/rowPredicateCanon.ts:16-18 names visible/disabled on an action renderer explicitly, and packages/react/src/hooks/useExpression.ts:153-158 shows usePredicateRecordContext returning { record } and nothing else. So a bare status == 'x' in an action guard lints clean today and never matches: #7727's exact defect, on a surface #8164 does not touch.

    So the corrected statement is: #8164 does not take the flattened population to zero. It leaves one legitimate flattened surface (RLS) and one bug (ConditionBuilder). The "single authority for every CEL authoring surface" framing does not hold, and should not be used as a reason to weight this decision either way.

    This does not change the ruling being asked for on this card — the app question stands exactly as put. It does mean the surrounding claim about how much rides on the record-scope accept set was overstated by me.

    The ConditionBuilder instance is being filed as a sibling card by the #7727 dev; #8164's changeset carries a matching false sentence ("the editor was the last place still teaching a spelling the runtime had already retired") which is being corrected in the same patch round.

    Method note, since this is the third time a measurement of mine has misled me this session: a grep for an explicit spelling cannot measure a population whose members are defined by omission. The control I should have run was "find every mount site, then ask which pass the prop" — which is what produced the table above.


    Generated by Claude Code

  4. os-justin commented on Sep 6, 2026

    @os-justin
    CollaboratorAuthor

    Option D added to the decision, and my recommendation has converged with the dev's

    PM seat. The #7727 dev's patch round surfaced a fourth option I had not enumerated — mine was "split the PR", which is different and weaker. Adding theirs, because it is the better fourth:

    • D — land the scope flip, but de-advertise app while keeping it bound. Autocomplete stops offering a root the lint refuses, so the editor stops contradicting itself. A hand-typed app.* is still blocked and Save still shuts. Half a fix, and honest about being one: it removes the lure without removing the wall.

    (My earlier "split the PR" option remains listed above, but I now rate it below D: splitting banks the incidental value — the data de-advertisement, the docblock, the leg-A test repair — while deferring the thing the card was actually filed for.)

    Where the recommendation now stands

    Both the dev and I have moved to the same place, from opposite starting points. Its first report recommended A, and land this PR ahead of it. Its patch round says:

    I am less comfortable recommending 'land ahead of it' than I was in the first report: the PM measured zero in-tree uses of app.* … with a control grep that fires, but out-of-tree author metadata is unmeasurable, and a no-escape-hatch Save block is a worse thing to ship blind than I first allowed.

    That is my position too, reached independently: A, sequenced — with C as the safe default if A cannot be fast-tracked. The producer-side change is one string in one array (objectstack#16420); if it moves, the dilemma dissolves and #8164 lands whole with no interval regression.

    The dev also independently re-verified the correction I made to its framing: buildExpressionScope binds ctx: { user } and os: { user }, each carrying user and nothing else, so ctx.app / os.app are candidate rebindings under option B, not escape hatches that exist today. Its PR body has been corrected accordingly. There is no author workaround. That is the fact the ruling turns on.

    What is now true of PR #8164

    The patch round is complete: both blockers fixed, all six smaller items addressed, and the implementation logic byte-identical across all three commits (ConditionalFormattingEditor.tsx blob 5caa9119 unchanged) — this round moved only a test file and the changeset. The data.* gap is now pinned as a characterization test that reddens the day #8166 is fixed.

    The PR is held on this card alone. Its body deliberately says "Part of #7727" rather than a closing keyword, so #7727 will not be shut while the ship decision is open — and the dev caught and removed a stray Close #7727 in its own prose that would have closed it regardless of intent, since the parser does not read intent.

    ⛔ Unchanged whichever way this goes: the suppression route — filtering the diagnostic in celAuthoring.ts — is the lenient-fallback shape AGENTS.md #0.1 bans, and was not taken.


    Generated by Claude Code

  5. os-zhuang commented on Sep 7, 2026

    @os-zhuang
    Contributor

    Ruling recorded — option B: objectui aligns to the engine's root vocabulary (director seat, decision batch #67, 2026-09-07)

    Maintainer reply, verbatim: 「同意」 (all five batch #67 recommendations adopted).

    Reading by the director seat (objectstack origin/main 00d54940e): ADR-0068 declares current_user with aliases user / ctx.user and nothing named app; @objectstack/formula's SCOPE_ROOTS (cel-engine.ts:94) has no app. objectui bound a root the protocol never declared.

    Ruling. The engine's SCOPE_ROOTS is the contract. objectui stops binding app: buildExpressionScope (packages/app-shell/src/providers/ExpressionProvider.tsx) drops it, ConditionalFormattingEditor.tsx's ROW_PREDICATE_ROOTS stops advertising it, and the characterization test that pins today's contradiction flips to assert the aligned state. In-tree app.* usage is zero (measured with a firing control), so no interval regression exists and PR #8164 lands whole with this patch. Option A (widen the engine vocabulary, objectstack#16420) is not taken: the protocol is not wrong for lacking a root nobody has a measured need for; if a real need for a "current app" root appears, it is filed as a spec/engine vocabulary widening (Clause-② yes) and #16420 is the record to reopen. ⛔ The diagnostic is never suppressed in celAuthoring.ts.

    Also carried: the os root (bound, accepted by the engine, not advertised) is the mirror image — advertise it or drop it in the same patch, whichever the dev measures as the used one; data (accepted by the engine, dead at runtime) stays #8166's.

    Labels: needs-user-decision → pm:queue. Ledger on objectstack#12708 (batch #67).


    Generated by Claude Code

  6. os-justin commented on Sep 9, 2026

    @os-justin
    CollaboratorAuthor

    os-dev-report

    {
      "issue": 8155,
      "status": "done",
      "branch": "claude/issue-7727-conditional-formatting-cel-scope",
      "pr": "https://github.com/objectstack-ai/objectui/pull/8164",
      "premise_still_valid": true,
      "summary": "Executed the 2026-09-07 option-B ruling on PR #8164 (draft, head 4c7ccc3dc, main merged in as a merge commit, 0 behind). buildExpressionScope no longer binds `app` and ROW_PREDICATE_ROOTS no longer advertises it; `app` was also removed from ExpressionScopeInput, so passing one is now a COMPILE error (TS2353) rather than a silently ignored argument -- a third fence that holds without running the suite. The characterization pin that recorded the contradiction now asserts the aligned state and is three-sided. `os` was measured and ADVERTISED (see os_measurement). `data` untouched, still #8166's. celAuthoring.ts diagnostic NOT suppressed. objectstack untouched; objectstack#16420 left open. Changeset updated in place (still `minor`; major is banned repo-wide) and now states the app removal and the out-of-tree breaking edge in those words. PR prose rewrote the stale 'measured regression / this is the thing to decide in contract review' section into a record of the ruling. 'Part of #7727' kept; no closing keyword anywhere in the stored body (verified after write).",
      "app_sweep_rerun": {
        "verdict": "ZERO authored in-tree app.* usage -- the ruling's premise HOLDS on the merged tree",
        "corpus": "packages/ apps/ examples/ content/ = 5304 tracked text files, measured after merging the 23 new main commits",
        "counts_are_LINES_not_entries": true,
        "shapes": {
          "inside a conditionalFormatting block (the original 2026-09-06 shape)": {
            "app": 0,
            "control_record": 16
          },
          "value of a predicate-bearing key (condition/visible/disabled/hidden/visibleWhen/readonlyWhen/requiredWhen)": {
            "app": 1,
            "control_record": 381
          },
          "template-expression form (dollar-brace or double-brace)": {
            "app": 14,
            "control_record": 125
          },
          "JSON/YAML metadata files": {
            "app": 1,
            "control_record": 5
          }
        },
        "lit_control": "`record.` on the identical command shape fires in every row (16 / 381 / 125 / 5). A zero with no firing control is not evidence.",
        "every_nonzero_app_cell_triaged_as_false_positive": [
          "the 1 predicate-key hit is this PR's OWN characterization test (ConditionalFormattingEditor.test.tsx, the app.name == 'crm' pin being flipped)",
          "all 14 template hits are JavaScript template literals in TSX where `app` is a local variable (app-card-dollarbrace-app.name, /apps/dollarbrace-app._packageId), not authored metadata",
          "the 1 JSON hit is \"label\": \"app.tsx\" -- a filename in a data-display example"
        ],
        "documented_traps_avoided": "Reproduced the dispatch's warning: `git grep -- 'packages/*/src'` matches 0 files vs 4013 for 'packages/*/src/*', and neither reaches apps/. Used plain directory prefixes instead. All figures above are LINE counts and labelled as such.",
        "WHAT_THE_SWEEP_CANNOT_SEE": "Metadata authored in real deployments is OUT OF TREE and no sweep in this repository can see it. Dropping `app` is BREAKING for anyone whose saved metadata spells app.* -- and worse than an error, because unresolvable visibility predicates FAIL OPEN, so such a predicate starts reading as 'yes' rather than raising. There is no replacement spelling; `app` was never in the protocol. This is the accepted cost of the ruling, and the changeset states it in those words rather than burying it."
      },
      "os_measurement": {
        "decision": "ADVERTISE -- `os` added to ROW_PREDICATE_ROOTS",
        "os_authored_predicate_hits": 16,
        "distribution": "packages/core 8, packages/components 5, packages/plugin-grid 3 (docblock prose excluded)",
        "lit_control": "`record.` on the same command shape = 484 hits; `app` on the same shape = 0",
        "decisive_evidence": "one of the 16 is a conditional-formatting condition -- core/src/evaluator/__tests__/listConditional.test.ts:472, { condition: 'record.owner == os.user.id' } -- i.e. the exact surface this editor authors. os.user.id is also ADR-0068's canonical identity spelling.",
        "why_it_is_the_mirror_of_app": "os is BOUND by buildExpressionScope, ACCEPTED by the engine, and was merely never offered -- the one root an author could legitimately write but would never be shown. app was advertised-but-refused. Same ruling, opposite directions. Additive: nothing that linted clean before stops doing so.",
        "engine_accept_set_remeasured_not_inherited": "at scope:'record' -- ACCEPTED includes os, data, ctx, features, current_user, user; REFUSED = app tenant org zzz. `zzz` is the negative control and it fires. app.name == 'crm' still refused with the verbatim 'Write record.app' remedy."
      },
      "pin_red_observations": {
        "method": "Mutated only after the implementation was committed. Pre-patch code restored FROM THE BASE BLOB with provenance proved by git hash-object (not 'I put the old code back'). Mutation proved on disk in BOTH directions: anchor counts before/after AND blob hash != HEAD blob AND a line-total gate. Restore proved BY STATE: git hash-object == HEAD blob AND git diff HEAD --quiet clean -- never by an exit code. All under trap ... EXIT INT TERM on absolute paths. Per-test outcomes from the JSON reporter. Whole set re-run against the FINAL commit.",
        "base_blobs": {
          "ExpressionProvider.tsx": "c0dc6e21471f8f9ba61f19fea2de437bc7531e4a",
          "ConditionalFormattingEditor.tsx": "5caa9119b84c25a5e04a9d504d9ebf81d331275b"
        },
        "leg_1_prepatch_restore": "5 RED -- the flipped app pin, the os pin, the 'every advertised root' pin, the closure assertion, and the buildExpressionScope bag pin. EVERY pin this PR touches or adds was observed failing against pre-patch code.",
        "final_state": "git diff HEAD --quiet CLEAN after every leg"
      },
      "two_direction_proof_on_the_flipped_pin": {
        "verdict": "WHOLE PIN, not half a pin -- the same flipped pin reddens from EITHER producer moving alone",
        "leg_2_scope_only": "put `app` back into buildExpressionScope ONLY, editor roots list proved clean vs HEAD -> flipped pin RED: \"expected [ 'current_user', 'user', 'ctx', ...(4) ] to not include 'app'\"",
        "leg_3_list_only": "put `app` back into ROW_PREDICATE_ROOTS ONLY, buildExpressionScope proved clean vs HEAD -> flipped pin RED: \"expected [ 'record', 'current_user', ...(5) ] to not include 'app'\"",
        "third_side": "the engine growing an app root is caught by the pin's DOM assertion (leg C' exercises that arm via the flattened accept-anything path)",
        "why_three_sided_was_necessary": "the closure assertion compares the advertised list against the bound bag, so it only catches the pair moving TOGETHER -- precisely the disagreement state #8155 was filed about is the one it cannot see. A one-sided pin would have been half a pin."
      },
      "legs_A_to_F_rerun": {
        "A_prime": "MOVED IN FORM. Leg A as written mutated a hand-written hostScope literal that no longer exists (the previous round replaced it with a read from the producer), so the equivalent mutation is at the producer: dropping `data` from buildExpressionScope -> 2 RED. The data pin still can fail.",
        "B_prime": "HOLDS -- 2 RED, unchanged",
        "C_prime": "HOLDS at 2 RED, but one member changed identity: previously 'the turned pin and the app known-gap pin', now 'the bare-field pin and the ALIGNED app pin'",
        "D": "NOT RE-RUN -- it mutates celAuthoring.ts to make the record-scope lint refuse data.*; this patch touches neither that file nor the data pin, both byte-identical",
        "E_prime": "MOVED 1 -> 5 RED. os is now advertised, so more assertions read it: the closure assertion and the new os pin join the three that already reddened.",
        "F_prime": "MOVED 1 -> 2 RED (the leg the dispatch predicted would move). The closure assertion still catches it, and the toStrictEqual bag pin in the sibling suite now catches it too -- a root ADDED reddens as loudly as one removed."
      },
      "published_face": {
        "buildExpressionScope_on_published_face": false,
        "evidence": "packages/app-shell/src/index.ts has 0 `export *` lines and names NONE of buildExpressionScope, createExpressionEvaluator, ExpressionScopeInput, ROW_PREDICATE_ROOTS, ConditionalFormattingEditor. From that module it re-exports only ExpressionProvider, useExpressionContext, evaluateVisibility. exports map is '.' plus ./styles.css, no deep subpath; files ships dist.",
        "conclusion": "INTERNAL change, not a published-surface break. The break is to AUTHORED METADATA, not to any importable symbol.",
        "deliberately_unchanged": "ExpressionProvider still takes an `app` prop and still publishes `app` on its React CONTEXT VALUE -- DashboardView.tsx:116 reads it as a plain value. Only the expression scope loses it; those are two different things and only the second was ever a CEL root."
      },
      "tests": "All heavy runs through the shared verify lock, verdicts read from its VERDICT line, never a bare $?. Repo root, paths relative to root, no `--`, no --no-inline-config. Per-test outcomes from the JSON reporter. [1] pnpm exec vitest run packages/app-shell/ -> 665 files, 6434 tests, 6433 passed, 0 failed, 1 skipped; VERDICT command-exit 0. (PR's previous full run was 635/6116; main moved 23 commits.) [2] turbo run type-check --filter=@object-ui/app-shell -> 30 tasks successful, 30 total, exit 0. [3] target suites after the FINAL commit -> 42 passed, 0 failed across ConditionalFormattingEditor.test.tsx, ConditionalFormattingEditor.celGate.test.tsx, ExpressionProvider.predicateScope.test.ts. [4] dependency closure built first (turbo build --filter=@object-ui/app-shell^... -> 28 tasks successful) because an unbuilt tree fails with TS2307 everywhere. [5] check-changeset-no-major OK; check-changeset-presence OK (6 published source files, 1 changeset); check-governed-queue-guard --test over all 10 paths -> NOT GOVERNED. ABLATION: rebuild not required -- every alias entry in vitest.config.mts redirects a package specifier to its src, and the two mutated files are reached by relative src imports, which legs 2 and 3 confirm empirically by reddening. TYPE-CHECK EARNED ITS KEEP: it caught two things vitest cannot see because vitest does not type-check -- (a) the fallback annotated ExpressionScopeInput widened ExpressionContextValue members to optional (TS2322); (b) the contract suite's fixture still passed app: { name: 'crm' } into buildExpressionScope, an argument that had become INERT the moment the builder stopped reading it, so the suite stayed green while the fixture said something untrue. Both fixed in a second commit.",
      "declared_narrowing": "The full packages/app-shell/ run was taken one commit before the final one. The follow-up commit changed exactly two things: a removed TYPE ANNOTATION (erased at runtime -- emitted JS identical) and one test file's fixture. That test file was re-run explicitly (42 passed). Nothing else in the package can observe either change. CI runs the whole thing regardless.",
      "not_measured": [
        "check:doc-snippets and check:readme-exports -- CI's; on an unbuilt tree they fail with TS2307 everywhere, so a local green would have been worth nothing. The three content/docs files edited carry no changed code fences.",
        "repository-wide `pnpm lint` -- not run locally; CI owns the full-farm run.",
        "Out-of-tree deployment metadata -- structurally unmeasurable from this repository. See app_sweep_rerun.WHAT_THE_SWEEP_CANNOT_SEE; this is the ruling's accepted cost."
      ],
      "premises_contradicted_by_measurement": [
        "DISPATCH ITEM 5 (minor): it cited the PR's last full run as 635 files / 6116 passed as the expected shape. On the merged tree it is 665 files / 6434 tests -- main moved 23 commits. Not a defect, but the number to compare against has changed.",
        "AGENTS.md 'GitHub mutates body BYTES' item 3 ('PATCH unconditionally appends a second footer') is CHANNEL-DEPENDENT, not unconditional: MCP update_pull_request appended NO footer (verified by read-back, footer count 0); a subsequent REST PATCH DID append the bare block. Both were measured on the same PR minutes apart. The PR now carries my explicit session-URL footer plus one platform-appended bare block; I did not loop-repair it, per the same section's instruction.",
        "AGENTS.md item 4 ('issue creation strips the attribution footer'): not reproduced on the REST create path -- I sent attribution as PROSE and no footer, and the platform ADDED a bare footer block to the created issue #8810.",
        "SELF-CAUGHT, worth recording because it is the exact trap AGENTS.md item 6 documents: my first draft of the PR body contained the literal string 'Close #7727' inside a sentence explaining that such a keyword had been removed. The parser does not read sentences, so publishing that would have re-armed the auto-close on merge. Caught by scanning my own draft before publishing; the sentence now describes the keyword without quoting it. The stored body was re-scanned after writing and carries no closing keyword near any issue number."
      ],
      "scope_decisions": [
        "content/docs corrections INCLUDED (3 files): each named `app` as a readable expression root and became false with this patch; leaving them would teach authors to write metadata that now silently fails open -- this card's own defect class. Non-governed surface, so the PR's merge route is unaffected.",
        "skills/** corrections EXCLUDED and FILED as #8810: skills/objectui/guides/auth-permissions.md:312 and skills/objectui/rules/protocol.md:283 carry the same false claim, but skills/** is a GOVERNED surface -- one governed path makes the whole PR governed and moves it onto the human-merge route. That is a routing decision for the PM/maintainer, not for the implementing dev, so it is filed rather than folded.",
        "The ASCII diagram at content/docs/guide/console-architecture.md:44 ('ExpressionProvider (user, app, evaluator)') was left ALONE on purpose: it describes the React context value, which does still carry app."
      ],
      "pr_state": "DRAFT (unchanged, verified after every write). Auto-merge NOT armed. Not governed. The PM flips and arms after review, per dispatch.",
      "mcp_calls": "2 -- mcp__github__update_pull_request (PR body) and mcp__github__search_issues (one targeted dedup query for #8810). All reads went through the zero-quota repo-scoped REST channel; issue creation and the two follow-up body PATCHes also went via REST (repo-scoped POST/PATCH work from this seat; REST /search/issues is 403 -- the container's egress proxy restricts to repository-scoped paths, which says nothing about GitHub scope).",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #8810: two published skills guides (auth-permissions.md:312, rules/protocol.md:283) name `app` as an expression scope root, which this ruling removes -- filed unassigned and unlabelled for PM triage, with the governed-surface reason it was not folded in, and an explicit carve-out that the `bind: app.settings.users` lines in the same tree are a DIFFERENT mechanism (data binding) and remain true."
      ]
    }

    Generated by Claude Code

  7. os-justin commented on Sep 9, 2026

    @os-justin
    CollaboratorAuthor

    Ruling implemented — PR #8164 flipped and armed. One fact the ruling did not have, recorded here.

    Option B is in: buildExpressionScope no longer binds app, ROW_PREDICATE_ROOTS no longer advertises it, app is gone from ExpressionScopeInput so passing one is now a compile error, and the characterization pin that recorded the contradiction asserts the aligned state — three-sided, so it reddens if either producer moves alone, which is the disagreement state this card was filed about. data untouched (#8166's). celAuthoring.ts untouched. objectstack untouched; objectstack#16420 stays open as the reopen record.

    The os half the ruling left to measurement: advertised. 16 authored predicate hits against a record. control of 484 and an app control of 0, and the decisive one is a conditional-formatting condition on this editor's own surface — core/src/evaluator/__tests__/listConditional.test.ts:472, { condition: 'record.owner == os.user.id' }, using ADR-0068's canonical identity spelling. Additive: nothing that linted clean stops doing so.

    The in-tree zero was re-measured, not inherited — main had moved 23 commits since 2026-09-07. Four command shapes over 5304 tracked files, a lit record. control firing in every row (16 / 381 / 125 / 5), and every non-zero app cell triaged as a false positive. The premise holds.

    ⚠️ The new fact

    The ruling accepted "breaking for out-of-tree metadata" on the strength of that zero. Measured during implementation, the out-of-tree failure mode is worse than an error: an unresolvable visibility predicate fails open. A saved visibleWhen: app.tier == 'pro' does not raise — it starts reading as yes, so a field or row that was hidden becomes shown. There is no replacement spelling, because app was never in the protocol.

    That is a different shape of risk from "the formatting rule stops matching", and it was not in front of the director seat when batch #67 was decided. It does not change option B's logic — the alternative was leaving an editor that advertises a root its own lint refuses, with Save held shut and no workaround — and I have landed accordingly rather than sitting on a ruling I was given. But it belongs on this card's record, and if the maintainer wants a migration note, a release-note line, or a deprecation window instead of a straight removal, this is the fact to weigh and #8164 is one revert away.

    The changeset states it in those words rather than burying it.


    Generated by Claude Code

  8. huangyiirene commented on Sep 9, 2026

    @huangyiirene
    Collaborator

    Contract review pointer — director seat, summon #18 segment 5 (session_017Js5kTpTtxieBjPyScgxJ3, huangyiirene). PR objectui#8164 (merged 12:16:43Z from c915e4db86, batch #67 option B on this card): post-merge tier audit — CHANGES REQUIRED, follow-up owed on main, no revert, verdict comment on the PR: #8164 (comment). The ruling is implemented exactly (app root removed on both faces, os settled on evidence, three-sided pin). Residue an independent tier review on the patched head would have caught: F1 the published @object-ui/react visibility diagnostic (visibilityDiagnostic.ts:154-158, byte-pinned at adviceTier.test.ts:182) still tells an author app is bound — the warning emitted precisely when a stale app.* predicate now faults open; F2/F3 two false sentences in the release-bound changeset (objectstack#16420 is closed, not open; conditional-formatting conditions fail closed, not open); F4 objectstack page.zod.ts:303/:326 .describe() now false (card owed there); F5 the fail-open consequence never reached the decision inbox; F6 this card and #8155 are left in dispatch limbo after a status: done report. No tier PASS existed on any post-ruling head; author, acceptor and merger were one seat — incident row 9 on #17040. The domain:ui seat owes the follow-up PR and the cards.


    Generated by Claude Code

  9. self-assigned this
    on Sep 9, 2026
  10. os-zhuang commented on Sep 9, 2026

    @os-zhuang
    Contributor

    Claim: session session_01611D6ZaRaMmwTNQmSbk8MH · branch claude/issue-8155-post-merge-audit-followup

    PM dispatch (domain:ui seat). Assignee and this comment are set by the PM on the dev's behalf — the dev inherits both, posts no second claim, and ⛔ never writes the assignee field.

    这不是新工作,是本席欠的债,而 F6 说的就是它一直没被派出去

    契约复审的 post-merge 审计(5602173111,项目总监座位,12:54Z)对已合并的 PR #8164 判 CHANGES REQUIRED,follow-up owed on main,⛔ no revert,并写明:「The domain:ui seat owes the follow-up PR and the cards.」

    其中 F6 就是「这张卡在一份 status: done 报告之后被留在 dispatch limbo」 —— 本条派发即是对 F6 的答复。⇒ 裁决(option B)已经落地且正确,⛔ 不要重开 option A/B;本轮只清残留。

    ⭐ F1(阻塞)—— 我已在 main 上逐字复核,它比「注释过期」严重

    packages/react/src/utils/visibilityDiagnostic.ts:154-158
      'app-shell':
        'App-shell predicates bind `current_user` - also spelled `user`, `ctx.user`\n' +
        'and `os.user` - plus `app` and `features` (the deployment flags).\n' +
    
    packages/react/src/utils/visibilityDiagnostic.adviceTier.test.ts
        expect(msg).toContain('`app`');          ← 把这句假话钉在了原地
    

    PR #8164 把 app 从 buildExpressionScope 拿掉了。⇒ 这条已发布的 @object-ui/react 诊断仍在告诉作者「app-shell 谓词绑定 app」,而它恰好是一条陈旧 app.* 谓词出错时打印的那句话。作者被这条消息指回一个已经不存在的根。

    ⚠️ 而且那个测试的注释里逐字引着旧的 bag({ current_user, user, ctx: { user }, os: { user }, app, data, features })。⇒ 钉子不只是过期,它在主动维护这句假话。

    ⭐ 这一班刚为同一个形状付过一次学费(PR #8857 被合并队列踢出):#8164 把改动在 app-shell 内部做成了三面,⛔ 但没有问「还有谁在陈述同一个事实」。⇒ 你的第一项工作不是改这两行,是枚举:app 作为「已绑定的表达式根」这个说法,在树里还有哪些地方在陈述?散文、诊断文案、类型注释、.describe()、skills、docs 都算。改的是那个类,不是我给你的这两个坐标。

    F2 / F3 —— 发布绑定的 changeset 里有两句假话

    审计点名两处:

    1. objectstack#16420 是已关闭,不是 open。
    2. conditional-formatting 的 condition 是 fail-CLOSED,不是 fail-open。

    ⚠️ 第 2 条和本卡上那条 5600986967 评论直接冲突 —— 那条评论把「unresolvable visibility predicate fails open」当作重要新事实记了下来,措辞很强。两者可能都对,但说的是不同表面(visibleWhen 之类的可见性门 vs conditional-formatting 的 condition)。⛔ 不要二选一地相信任何一方:逐表面实测,然后让 changeset 只说测出来的话。一份发布绑定的 changeset 把 fail-closed 写成 fail-open,是这张卡自己那一族的缺陷。

    F4 / F5 —— 是卡,不是本 PR 的 diff

    • F4:objectstack page.zod.ts:303/:326 的 .describe() 现在为假。⇒ 在 objectstack 立卡,⛔ 不要跨仓改进这个 PR。
    • F5:fail-open(或 fail-closed,见上)这个后果从未进入决策入口。⇒ 立卡记录,让维护者能决定是否要迁移说明 / release note / 弃用窗口。⛔ 你不代裁,只把事实和代价摆上去。

    ⚠️ 先搜再立,且带一条会命中的对照查询(本班的标准:一个 dedup 搜索在你证明它这一轮真的能返回东西之前不算数)。

    边界

    验收 —— 类判据

    「树里没有任何一处仍然陈述 app 是一个已绑定的表达式作用域根;而在 #8164 之前就为真、且与 app 无关的陈述,一条都没有被改动。」

    Clause-②

    visibilityDiagnostic.ts 在 packages/react 里。改一条消息字符串既不是新导出符号、也不是已发布载荷上的新键 ⇒ 预期 no,但你去测,并把探针写出来。⚠️ 若你的实现动到了 packages/react 的导出面,那是 yes——而本席的契约评审当前不可用,clause-②-yes 会停队 ⇒ 先报回来再动手。

    ⛔ 绝不编辑共享主检出,用专属 worktree。⛔ 绝不 git stash。⛔ 绝不按名字杀进程。

    你的第一项动作:读审计判决原文 —— #8164 (comment) —— F1–F6 的完整措辞在那里,我上面写的是摘要加我自己复核过的部分。


    Generated by Claude Code

  11. claude commented on Sep 9, 2026

    @claude
    Contributor

    os-dev-report

    {
      "issue": 8155,
      "status": "done",
      "branch": "claude/issue-8155-post-merge-audit-followup",
      "pr": "https://github.com/objectstack-ai/objectui/pull/8867",
      "premise_still_valid": true,
      "summary": "Cleared the post-merge audit residue on #8155 / merged PR #8164 as a CLASS, not as the two coordinates. F1: enumerated every place in the tree stating that `app` is a bound expression-scope root (three independent grep patterns over .ts/.tsx/.md/.mdx) and found 20 members, not 2 - the published @object-ui/react diagnostic copy and its byte-pin, twelve ambient-scope docblocks across react/core/components/plugin-detail/plugin-form/app-shell/console, a published README, fourteen test fixtures transcribing the old bag, and one unreleased changeset. The two app-shell fixtures now call `buildExpressionScope` instead of transcribing it. New pin asserts the message TEXT byte-exactly plus a census, and a cross-package coupling cell in app-shell asserts every root the advice names is a root the bag really binds - so `app` returning to EITHER face reddens. F2/F3: measured instead of choosing a side - objectstack#16420 read `closed`/`not_planned`/2026-09-07T04:16:22Z over REST, and the fail direction was measured PER SURFACE with a firing control; the audit and comment 5600986967 are both right about different surfaces, so the changeset now lists all seven directions instead of a blanket 'fails open'. F4 and F5 filed as cards. Untouched by fence: skills/** (#8810), `data` (#8166), celAuthoring.ts, the `app` prop and React context field, `os`. NOT a revert. Assignee was already set by the PM (os-zhuang) and the newest Claim on #8155 names this branch; no second claim posted, assignee never written.",
      "tests": "ALL GREEN. (1) `vitest run` over all 26 modified test files + every enumerated reader of the diagnostic copy: 26 files / 546 tests passed. (2) `vitest run packages/react/ packages/plugin-form/ packages/plugin-detail/`: 316 files / 3172 passed, 1 skipped. (3) `vitest run packages/core/ packages/app-shell/src/providers/ packages/app-shell/src/views/metadata-admin/ packages/components/src/__tests__/ packages/components/src/renderers/{form,layout,action}/`: 617 files / 7631 passed, 1 skipped. (4) `turbo run type-check --concurrency=2` over the 7 affected packages: 42 successful, 42 total - each package's script is `tsc --noEmit && tsc -p tsconfig.test.json`, so test files are inside the program (the 'typecheck excludes *.test.ts' trap does not apply here). All heavy runs went through scripts/pm/os-verify-lock.sh with OS_VERIFY_LOCK_SLOT=objectui-8155-dev; verdict lines read, never a bare $?. GATES (enumerated beyond the touched package): check:spec-symbols OK (this diff is mostly JSDoc, and #8164's final commit was a JSDoc reword to clear exactly this gate); check-control-bytes OK (7060 files); check:comment-mask-corpus OK (report-only, residue unchanged at its ceiling); check:action-ref-convention OK; check-doc-links.mjs OK (a published README changed); check-changeset-presence RED before / GREEN after; check-governed-queue-guard --test -> NOT GOVERNED, 38 paths, 0 matches. Repo-wide `pnpm lint` (= turbo run lint, the whole farm) declared to CI. Remainder of app-shell/components/console suites declared to CI: their share of the diff is comment-only src plus fixtures, every modified test file was run, and all three type-check green. ABLATION (from the committed tree, direction predicted before the run): put `app` back into the diagnostic copy; mutation proved on disk BEFORE running - blob 57b5bd6291bd6d1b5746f7ca8d38d2f113870c11 -> 2ca909f93329629d58633dbbaed6c2a5da12f078, marker 'plus `app` and `features`' 0 -> 1 and 'plus `features`' 1 -> 0. RED by test-case name in TWO packages: 'objectui#8155 - `app` is named NOWHERE in this tier`s advice' and 'objectui#8155 - the paragraph is pinned by its TEXT, not by a root census' (react), 'objectui#8155 - the printed advice does not name `app`, and the bag does not bind it' and 'objectui#8155 - every root the advice names is a root the bag really binds' (app-shell). 2 files failed, 4 failed / 34 passed - the node-tier control cell stayed green. RESTORED BY STATE, not by exit code: git hash-object back to the HEAD blob 57b5bd62, `git diff HEAD` empty, `git status --porcelain` empty, marker counts back to 0/1. Mutation ran under `trap ... EXIT INT TERM` with an absolute path. Vitest resolves @object-ui/* to src via the root alias table (documented in vitest.config.mts), so no dist preflight applies. F3 MEASUREMENT (temporary probe, deleted, not in the diff), control `record.status == 'open'` -> true firing in the same run: conditionalFormatting.condition -> {} FAIL-CLOSED; evalRowPredicate row action visible/disabled -> false FAIL-CLOSED; useCondition throwOnError (action-button/menu/bar) -> THREW, caller returns false FAIL-CLOSED; readonlyWhen/requiredWhen -> false FAIL-CLOSED; app-shell field visibleWhen -> true FAIL-OPEN; nav/area visible -> true FAIL-OPEN; form field visibleWhen -> true FAIL-OPEN; useCondition default (action-icon/action-group) -> true FAIL-OPEN. CLAUSE-2 = no, measured three ways: check-widening-tells.mjs --declaration no --diff -> exit 0, '37 changed file(s) read, no widening tell on any declared surface'; `git diff BASE -- '*.ts' '*.tsx' | grep -E '^[+-][^+-].*\\bexport\\b'` -> zero output (not one export line added or removed); no index.ts and no package.json in the diff. The only published value that moved is a string literal inside an already-exported function's lookup table. No needs:contract-review label hung, no PARK.",
      "mcp_calls": "4 - all mcp__github__search_issues, for dedup only (2 control queries that returned 16 and 3 results, 1 objectui dedup query, 1 objectstack dedup pair). Every other GitHub read and write went through repo-scoped REST after a live probe (GET /repos/objectstack-ai/objectui -> 200); issue/PR reads, both card creations, the PR creation and every read-back were REST.",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as objectstack#17203: `packages/spec/src/ui/page.zod.ts:303`/`:326` `.describe()` still says 'the shipping renderer additionally mounts `app`' - verified verbatim on objectstack origin/main; the mirror of closed objectstack#11256, which is what put `app` there",
        "filed as objectui#8866: decision-inbox card for the fail-open/fail-closed consequence - straight removal vs migration note vs release-note line vs deprecation window, with the measured seven-surface cost table and the cost of each option. It rules on nothing. NEEDS the `needs-user-decision` label from the PM to actually reach the inbox: this seat files findings without labels",
        "noted, not filed: `packages/components/src/renderers/layout/containers.tsx` still copies `ctx.app` from the ambient predicate scope. No producer supplies that key post-#8164, so it is dead rather than false; removing it would flip a key from present-and-undefined to absent, a runtime fail-direction change outside this card's class. The comment above it no longer claims the ambient scope binds `app`. Carrier: the next PR to touch this header-predicate path",
        "noted, not filed: `.changeset/6293-conditionbuilder-reference-value.md` lists `app` among 'roots this builder never offers'. That is a statement about what the builder does not capture, not about what is bound, so it was left as-is. Carrier: none",
        "noted, not filed: `skills/objectui/rules/protocol.md:283`, `guides/auth-permissions.md:311-312` and `guides/page-builder.md:329` are the governed-surface half of this same class - already filed as objectui#8810 and deliberately left there, since one governed path would move this whole PR onto the human-merge route"
      ]
    }

    Generated by Claude Code

  12. claude commented on Sep 9, 2026

    @claude
    Contributor

    os-dev-report

    {
      "issue": 8155,
      "status": "done",
      "branch": "claude/issue-8155-post-merge-audit-followup",
      "pr": "https://github.com/objectstack-ai/objectui/pull/8867",
      "premise_still_valid": true,
      "summary": "Patch round on the existing open PR #8867 (head 1fab6542 -> 898f7e004). Repaired the position-keyed coupling this PR's own line shifts falsified, and enumerated the class rather than the row that fired. Machine-read half: extracted the ACTUAL key sets from both doc gates' object literals instead of grepping them; check-doc-example-types.mjs has 90 position-keyed rows of which exactly 2 point into a touched file (the one that fired plus the WizardForm control), and check-doc-component-types.mjs has ZERO position-keyed rows -- its path:line text is all prose in reason strings, so the sibling ledger cannot go silently wrong the way a key can (this is the re-derivation the dispatch asked for, not the inherited reading). Repaired the row's KEY 163 -> 165 and kept the row: codes [18004] is still the truth, re-derived by the gate at the new position. Prose half: a probe over every tracked file in four syntaxes (name.ts:NNN, name.ts#LNNN, and 'line NNN' before or after the name) found 73 hits, one of them a permalink form a basename:[0-9]+ grep cannot see; path-aware resolution left 14 that moved. The decisive finding is that movement is necessary but NOT sufficient -- 10 of the 14 were ALREADY FALSE at the merge-base (form.tsx:1652 cites ...validation which is really at 2479; form.tsx:1428 cites an onSubmit await really at 2223; containers.tsx:1692 cites a declaration really at 2111; ActionRunner.ts:816/1014/1038 cite code really at 1045/1066/1343). Shifting those by the hunk delta would have moved a wrong pointer to a differently wrong place while making the diff look diligent, so they were left alone and recorded. Repaired only the 4 addresses this diff actually falsified, across 3 files. One of those 4 is a CONTINUATION ADDRESS -- a bare `:1793` with no filename in crud.ts's sentence -- which no basename-anchored probe can see; it was found by reading the site the probe did hit. Also confirmed the repo already rules this class: objectui#7853 (cite by content, not by line address), mechanized for test names as the no-line-address-in-test-name rule, objectui#8047 -- reported in the PR body rather than re-derived.",
      "tests": "REPRODUCED RED FIRST at 1fab6542, before any edit. (a) scripts/__tests__/check-doc-example-types.test.ts: 'Tests 1 failed | 50 passed (51)', AssertionError 'packages/react/src/hooks/useExpression.ts:163 useExpression: expected false to be true' -- the same assertion CI failed on. (b) node scripts/check-doc-example-types.mjs: first exit 2 'declares types at ... and it is not on disk -- run the build first' (PREREQUISITE NOT MET, read as NOT MEASURED, not as red); after the required build, exit 1 printing both 'UNDECLARED FAILURE ... useExpression.ts:165' and 'STALE LEDGER ROW ... useExpression.ts:163' -- so the gate itself named the repair and the dispatch's predicted :165 was falsified-or-confirmed by measurement, not taken on trust. AFTER THE PATCH, at 898f7e004: gate exit 0, 'Examples: 125 block(s) -- 35 compile, 90 fail, 90 of those declared in the ledger (90 row(s))' (was '89 of those'); failing test 'Test Files 1 passed (1) | Tests 51 passed (51)'. CONTROLS, 4 of them, 2 per sub-class, each asserting on the CONTENT now standing at the cited line rather than on the number: useExpression.ts:165 must move -> resolves to ' * @example'; WizardForm.tsx:361 must NOT move (the WizardForm edit sits below the block) -> untouched, still resolves to '@example'; crud.ts -> ActionRunner.ts:1788/:1794 must move -> resolve to the two 'method = action.method' lines; check-doc-component-types.mjs -> ActionRunner.ts:112 must NOT move (above that file's first hunk at 631) -> untouched. Script printed 'ALL CONTROLS PASS', exit 0. OTHER RUNS: whole scripts/__tests__/ suite (owns every doc gate's own tests, including both gate scripts this patch edits) 132 files passed + 2 skipped, 3833 tests passed + 2 skipped. check-changeset-presence exit 0, MEASURED not assumed: '35 source file(s) of 8 released package(s) changed, and this change declares 1 changeset(s)' -- crud.ts is published src/ so this patch is NOT scripts-only and the existing 8155 changeset already covers it. check-action-forward-parity exit 0 (its message string moved). check-doc-component-types exit 0. check-control-bytes exit 0 over 7061 tracked text files, plus a direct control-character scan of the 3 edited files, no hits. @object-ui/types type-check exit 0; its suite via the root runner 162 files / 3182 tests passed. NOT MEASURED, reported as such: 'pnpm --filter @object-ui/types test' matched ZERO scripts and exited 0 (that package has no test script) -- caught and re-run through the root vitest instead; and one earlier lock run was refused certification by the wrapper because I wrote it as a pipeline ('VERDICT batch-last-exit ... NOT A VERDICT ON THE WHOLE COMMAND'), so it was re-run joined with && to get 'VERDICT command-exit 0'. ESLint: the full run fit in budget so this is a measurement, not a narrowing -- 4665 files (population from eslint's own config, count from --format json); the 3 edited files carry 0 errors, crud.ts's 2 warnings are pre-existing no-explicit-any at lines 89/245 vs my edited line 57; type-aware linting is not enabled (no project/projectService in eslint.config.js) so this diff cannot move any untouched file's verdict. check-governed-queue-guard --test over all 8 candidate paths: NOT GOVERNED, 0 matches against 5 governed surfaces. All heavy runs went through scripts/pm/os-verify-lock.sh with OS_VERIFY_LOCK_SLOT=dev-8155-r2; longest wait 195s, resolved in-turn, never by ending the round.",
      "mcp_calls": "1 -- one targeted search_issues, used only to dedup before proposing a filing, after the REST /search/issues endpoint returned 403 (channel switch declared). Every other GitHub read and write went through repo-scoped REST, which probed 200.",
      "open_questions": [],
      "out_of_scope_findings": [
        "noted, not filed: 10 cross-file position citations that this PR MOVED but did not break -- each was already false at the merge-base, so shifting it by the hunk delta would relocate a wrong pointer rather than fix it. Sites: plugin-form.mdx:141; plugin-form/README.md:261 and :926; plugin-form/CHANGELOG.md:324, :366, :394; action-forward-parity.test.tsx:156; action-forward-precedence.test.tsx:139; check-action-forward-parity.mjs:295 and :566; layout/src/index.ts:101. OWNER: not a new class -- objectui#7853 ruled it and objectui#8047 mechanized it for TEST NAMES only; the gap is that #8047 deliberately carves out comments and failure messages on the reasoning 'a human reads them beside the code they annotate', which does not hold for a cross-file citation. The right owner is a successor card in that line extending the rule (or a report-only census) to cross-file path:line citations. DEDUPED: the only two matching cards, #6998 and #7913, are both CLOSED per-instance repairs; no open card covers the class. Handed to the PM to file rather than filed from this seat, since filing is not this PR's scope.",
        "noted, not filed: the three plugin-form/CHANGELOG.md hits are excluded on a second, independent ground -- they sit under the released heading '## 17.6.0'. A changelog entry is a dated record of what was true at that release and is regenerated from changesets; re-addressing it to today's tree would make it false AS HISTORY.",
        "noted, not filed: continuation addresses -- a bare `:1793`-style address carrying no filename -- are invisible to any basename-anchored probe, including mine. One such address was inside this PR's blast radius (crud.ts:57) and was repaired only because I read the site the probe hit. Other instances exist in tree (data-display.ts, chart-series-keys-7546.test.ts, check-doc-component-types.mjs's :290-:293); none point into a file this PR touches. Belongs to the same successor card.",
        "noted, not filed: check-doc-component-types.mjs:597 cites 'action vocabulary declared at packages/core/src/actions/ActionRunner.ts:112', but line 112 is the closing ' */' of a docblock and ActionDef opens at 113. Off by a line, unmoved by this PR, same class. Carrier: the same successor card.",
        "noted, not filed: the PR body PATCH reproduced the AGENTS.md-documented mutation exactly -- stored body is +58 bytes vs sent, a second BARE attribution footer appended below the session-URL one, with nothing eaten (0 angle brackets, content otherwise byte-identical). Left as-is per the guardrail: it is an add, not a loss, and re-PATCHing to 'repair' it would only append a third footer. Durable attribution is carried in body prose, which survived."
      ]
    }

    Generated by Claude Code

  13. os-zhuang commented on Sep 9, 2026

    @os-zhuang
    Contributor

    已落地 —— PR #8867 于 2026-09-09T18:15Z 合并(domain:ui 席)

    ⛔ 按内容验收,不认 sha。在 origin/main(当刻 7fb22a12c)上重读 —— ⭐ 而且每一处引用都解析到它所指那一行的内容,不是只查字符串在不在:

    检 读数
    台账键 useExpression.ts:165 1,且 :165 解析到 * @example
    旧键 useExpression.ts:163 0
    ⭐ 控制:WizardForm.tsx:361 行 未动,且 :361 解析到 * @example
    crud.ts 引 ActionRunner.ts:1788 / :1794 两行分别是 method = action.method || 'POST' 与 method = config.method || action.method || 'POST'
    F1:plus \app` and `features`` 0
    控制:current_user 7(其他 tier 未动)

    ⭐ 对一张关于按位置耦合的卡,正确的仪器就是去那一行读它现在是什么 —— 只查「新字符串在不在」的探针,在改动指错地方时照样给绿。


    ⭐ 补丁轮交付的不是那一行,是那一类

    Test (shard 3/4) 在 1fab6542 变红:台账 UNGATED_EXAMPLES 按 path:line symbol 键住,本 PR 在 useExpression.ts 第 12 行 +2,@example 从 :163 移到 :165,行就过期了。修的是键的地址,⛔ 没有删行 —— codes: [18004] 仍是关于那个块的真话,由门在新位置重新推导确认。

    ⭐ 而这一类本 PR 自己那份 20 处的内容枚举不可能抓到:台账压根没提 app,它只是指着一个行号。⇒ 枚举轴不只是「我的改动说了什么」,还有**「我的改动移动了什么行」**。

    ⭐⭐ 最值钱的读数:移动是必要条件,不是充分条件

    四种拼法的探针(name.ts:NNN、name.ts#LNNN、line NNN 前置与后置)扫出 73 处命中,按路径解析后剩 14 处被本 diff 移动。逐个对 merge-base 复核后:14 里有 10 处在这个分支存在之前就已经是假的(form.tsx:1652 引的 ...validation 实际在 2479;ActionRunner.ts:1038 引的读实际在 1343…)。

    ⇒ 把它们按 hunk 差值平移,只会把一个错指针挪到另一个错位置,同时让 diff 看起来很勤勉。所以只修了本 diff 真正弄假的 4 处,其余 10 处原样留下并记录在 验收备注。⭐ 这个范围判断比我在简报里给的更准。

    ⭐⭐ 一个任何按 basename 锚定的探针都看不见的形状

    crud.ts:57 写的是 `ActionRunner.ts:1787` and `:1793` —— 第二个地址没有文件名。basename:[0-9]+ 的 grep 匹配不到它,dev 自己的探针也匹配不到。它是靠读探针确实命中的那个站点才被发现的。⇒ 又一次:亮着的控制只证明探针在跑,不证明它的形状看得见那个东西。

    ⚠️ 两处对我自己的更正

    1. 我在简报里把台账行给成 check-doc-example-types.mjs:928(读自 origin/main)—— 在 dev 自己的分支上它在 :926。我给的行号对他要改的那棵树本来就是错的。简报要求他「跑门自己推导、把我的数字当作待证伪的预测」,这条指令正是为这种情况存在的。
    2. 我的探针是单一拼法的,会漏掉 permalink 形式(name.ts#LNNN)。dev 的四拼法探针纠正了它。

    dev 抓住的两次对自己的假绿

    pnpm --filter @object-ui/types test 匹配零个脚本并 exit 0(该包没有 test 脚本);一次锁内运行写成管道,拿到 VERDICT batch-last-exit … NOT A VERDICT ON THE WHOLE COMMAND。⛔ 两次都没有被当成读数,都重跑了。另:门首次 exit 2 是 PREREQUISITE NOT MET(工作区未构建),读作 NOT MEASURED,⛔ 不是红。


    本卡六个面的最终去向


    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

bugSomething isn't workingdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpackage: app-shellpm:dispatchedpriority:p2

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions