Skip to content

fix(lint)!: page/view visibleWhen refuses a has() argument that is not a field selection - #25313

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/project-thread-y1fvs4
Oct 23, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/project-thread-y1fvs4

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Bill · project thread

Fixes #25281

Clause-②: no (narrowing)
Metadata-protocol: narrowed

Executes ruling 6351260886 on #25281, letter A. Draft until the maintainer ratifies that ruling; the seat runs the contract review and the landing.

What

CEL defines has() in one form, has(e.f), with f a field name after a dot. The engine refuses any other argument as has() invalid argument on every evaluation, whatever the record holds. The page and view visibleWhen door (validateVisibilityPredicates, so os validate, os build, os lint, defineStack and the page and view save doors) did not judge the argument, so has(status) and has(record.items[0]) published clean and the console drew the guarded element for every user.

  • @objectstack/lint: a new gating arm, visibility-predicate-has-argument (error), in the existing family, at the same site and path as its siblings. It fires on a has() call whose single argument is not a . field selection, reports the first such call per predicate, quotes the argument and the engine's wording, and gives one hint line with the spelling the definition admits:

    • a bare name: has(record.status) or 'status' in record, with the root the layer binds (data on a metadata-editing form); a bare root (has(record)) is told has(record.FIELD), never record.record;
    • an index as the last step: size(record.items) > 0, or 'k' in e when the index is a string key;
    • e.?f: has(e.f); anything else: the one form, with the layer root.

    No null-guard advice is repeated (validate-null-guards owns it). os explain visibility-predicate-has-argument carries the reasoning.

  • The oracle (mechanism (b) of the ruling): the canonical AST this rule already walks for the lint: visibility-bare-identifier goes silent for an identifier that also appears inside a has() in the same predicate #16118 mask. A has call with exactly one argument whose node is not . is refused. After rewriteIndexedHas (Formula/CEL: has() over an indexed path is accepted at authoring and refused at runtime, so a visibleWhen using it fails open #25267) the engine refuses exactly those arguments, and the suite pins the equivalence against celEngine.compile in both directions: 22 refused shapes and 10 accepted shapes. Mechanism (b) was chosen over (a) because the engine reports one error per source: in bogusFn(1) && has(status) a compile-based helper would see only the unknown function, while the AST walk sees the has() call too.

  • The lint: visibility-bare-identifier goes silent for an identifier that also appears inside a has() in the same predicate #16118 mask stays load-bearing. has(status) alone yields ONE finding (this one). has(status) && status == 'x' keeps its visibility-bare-identifier finding on the unguarded status, beside the new one.

  • @objectstack/spec, ADR-0087 kit: the D3 semantic entry visibility-predicate-has-argument-refused (step 18) and a REFUSED_STORED_SHAPES detector. os migrate meta --stored lists a stored page component, form section or section field carrying the shape under that id. No D2 conversion: the test the author meant is not recorded. packages/spec cannot parse CEL, so the detector reads the source through a package-internal token reading (conversions/cel-has-argument.ts). A lint-side test (validate-visibility-predicates.stored-listing.test.ts) holds the listing, the page save door and celEngine.compile to one set over 29 predicates.

  • @objectstack/formula: comment only. The STILL OPEN note above firstUndeclaredReference records that the page/view door now reports the argument. The first-error masking it describes is unchanged.

Pins rewritten as part of the ruling (not silently)

Unchanged (#13594's scope discipline, verbatim)

has(e.f) for any e, including the indexed and call receivers #25267 lowers; every dyn predicate; type == 'grid', type(record.x) == string, upper(1, 2), 1 + 'a' (pinned: no has() finding); a has( inside a string literal; a member call x.has(y); has() and has(a, b), which the engine refuses under another message; the render-side fail-open (ADR-0137 D3).

Measured before the first edit

  • HotCRM producers: 0 at 4166f21 (shallow clone). The 550 has( occurrences hold no bare-name and no index-last argument in a CEL string. The only hits are comment prose ((!has(x) || isBlank(x)) in two flow docblocks, one test comment).
  • This repository: 0 at the base 5801a4f. Every has(NAME) / has(e[i]) hit is TypeScript Map/Set .has(, a code comment, CHANGELOG or docs prose.
  • Base 5801a4f: has(status) and has(record.items[0]) on a form field draw no finding from validateVisibilityPredicates, and celEngine.compile answers has() invalid argument on both. The suite at the base pinned that silence (rows 898–926).

Tests

Run on this branch, each under scripts/pm/os-verify-lock.sh:

Gates

node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derives 92 families, the same list before and after the merge. On head 7f0e0e6fe all 92 were run and each exited 0; --ran reconciliation: "92 derived famil(ies) accounted for — 92 run, 0 NOT-MEASURED". This includes check-empty-changeset, check-adr-0087-registration and check-changeset-no-major. (On the pre-merge head check:dual-build-cjs-loads exited 3 for a missing dist; with the dists built it exits 0.)

Dogfood battery

pnpm exec turbo run build --filter='@objectstack/dogfood^...' && pnpm --filter @objectstack/dogfood test on head 7f0e0e6fe: 289 files passed, 3 skipped (292); 2491 tests passed, 18 skipped (2509). VERDICT command-exit 0.

Acceptance notes

  • The detector in packages/spec reads tokens, not the engine's AST. A predicate that is also broken CEL can be listed there while the door names its syntax fault first. Both send the owner to the same predicate.
  • Not addressed here: on the object, flow, action, sharing-rule and hook surfaces, validateExpression already refuses both shapes (invalid-cel, has() invalid argument). Its prescription there is the generic "predicates are bare CEL", not a has() spelling. Noted, not filed: the verdict is right and the surface is outside this card.

Generated by Claude Code

…t a field selection

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jc59n5NWaMp1khDwL7Fici
…ne refuses (ADR-0087 D3)

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jc59n5NWaMp1khDwL7Fici
Keeps both refused-shape describes in refused-shapes.test.ts. The
has() argument detector reads the stack alone and ignores the context
parameter #25279 added.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jc59n5NWaMp1khDwL7Fici
@github-actions github-actions Bot added size/xl documentation Improvements or additions to documentation tests tooling labels Oct 22, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/formula, @objectstack/lint, @objectstack/spec, touching 29 documentable anchor(s). ⚠️ 2 changed file(s) yielded no anchor (packages/formula/src/cel-engine.ts, packages/lint/src/index.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

2 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/kernel/contracts/data-engine.mdx (via OPEN (symbol, a top-level const object))
  • content/docs/protocol/kernel/realtime-protocol.mdx (via OPEN (symbol, a top-level const object))
What this run could not see
  • 2 changed file(s) yielded no anchor (packages/formula/src/cel-engine.ts, packages/lint/src/index.ts) — pages documenting those are invisible to this run
  • 16 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 145 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 258f4e6aaa5c1f75675b4195a35cb5da2ce5e275 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from b2148cb74c9f30b0f7251bf32c51d1b6b8e4469c — the merge of head 7f0e0e6fe72a685a8a9e3602e2d012fa2df46872 into base 258f4e6aaa5c1f75675b4195a35cb5da2ce5e275, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin b2148cb74c9f30b0f7251bf32c51d1b6b8e4469c && git checkout b2148cb74c9f30b0f7251bf32c51d1b6b8e4469c
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 258f4e6aaa5c1f75675b4195a35cb5da2ce5e275 7f0e0e6fe72a685a8a9e3602e2d012fa2df46872 && git checkout -B drift-repro 258f4e6aaa5c1f75675b4195a35cb5da2ce5e275 && git merge --no-ff 7f0e0e6fe72a685a8a9e3602e2d012fa2df46872

node scripts/docs-audit/affected-docs.mjs --json 258f4e6aaa5c1f75675b4195a35cb5da2ce5e275

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 258f4e6aaa5c1f75675b4195a35cb5da2ce5e275 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 7f0e0e6fe72a685a8a9e3602e2d012fa2df46872
Local-runs: none
Scope: packages/formula/src/cel-engine.ts, packages/lint/src/index.ts, packages/lint/src/rule-explanations.ts, packages/lint/src/runtime-gate.page-visibility-predicate-writes.test.ts, packages/lint/src/validate-visibility-predicates.stored-listing.test.ts, packages/lint/src/validate-visibility-predicates.test.ts, packages/lint/src/validate-visibility-predicates.ts, packages/spec/src/conversions/cel-has-argument.test.ts, packages/spec/src/conversions/cel-has-argument.ts, packages/spec/src/conversions/refused-shapes.test.ts, packages/spec/src/conversions/refused-shapes.ts, packages/spec/src/migrations/entries/semantic/18.visibility-predicate-has-argument-refused.ts + packages/lint/src/view-walk.ts, packages/lint/src/page-walk.ts, packages/lint/src/runtime-gate.ts, packages/spec/src/conversions/walk.ts, packages/formula/src/index.ts, packages/spec/src/migrations/registry.ts.template, scripts/check-empty-changeset.mjs, .changeset/25168-external-binding-managed-datasource-refused.md (precedent, on main)

Reviewed against ruling 6351260886 (letter A) on card #25281, the PR body, the 13-file net diff origin/main...7f0e0e6fe (+1088 / −29, matching the PR file list), and the 47 check-runs on the head: 36 success, 11 skipped (opt-in and conditional jobs), 0 failed. The ruling's 追认 by the maintainer is pending (see ③); this record judges the diff against the ruling as written.

① Derived judgments

  1. The narrowing itself — right. validate-visibility-predicates.ts arm (3b) (lines 1361–1390) refuses, at severity error, a has() call with exactly one argument whose AST node is not a . selection, reported at the same where/path as its siblings and only when the front end accepted the source (!refusal), exactly as ruling point 1 fixes it. Shapes: bare name, index as last step, and every other non-selection (.?, call, literal, operator) under one id visibility-predicate-has-argument.

  2. Oracle = the engine, mechanism (b) — right, and pinned. firstRefusedHasArgument (lines 1028–1073) keys on op === 'call' && args[0] === 'has' && args[1].length === 1 && arg.op !== '.', the same AST reading indexedHasSelection uses in cel-engine.ts (1557–1567). rewriteIndexedHas lowers every . argument whose chain root is not an identifier, so after it the only has() calls cel-js's macro still refuses as has() invalid argument are the non-. ones: equivalence by construction. Pinned both ways against celEngine.compile: 22 refused / 10 accepted shapes (validate-visibility-predicates.test.ts 1744–1790) and a three-way engine/door/listing set over 18 + 11 predicates (…stored-listing.test.ts). The PR's stated reason for (b) over (a) — one compile error per source would hide has(status) behind an unknown function — holds. A member call (rcall) is not matched; has() and has(a, b) are not this arm's (engine refuses under another message); a has( inside a string literal has no call node. has(1.x) passes the arm and compile refuses it for its TYPE — exactly the blind spot ruling point 2 keeps, and the spec test names it as such.

  3. The lint: visibility-bare-identifier goes silent for an identifier that also appears inside a has() in the same predicate #16118 mask stays load-bearing — right. hasCallSpans (917–936) masks every has call regardless of argument shape, so has(status) alone is ONE finding (this id) and has(status) && status == 'x' keeps visibility-bare-identifier on the unwrapped occurrence beside it; both pinned (1851–1856, acceptance pair 1724–1738). Ruling point 4 met.

  4. Prescriptions — right. hasArgumentHint (1083–1108): bare name → has(ROOT.x) or 'x' in ROOT with CANONICAL_ROOT_BY_LAYER (record runtime / data metadata); a bare ROOT (record, current_user) → a field OF it, never record.record, read off SCOPE_ROOTS + VIEW_PAGE_EXTRA_ROOTS so type is correctly not a root; index last → size(e) greater-than i, string key → 'k' in e; .? → the plain selection; anything else → the one form. No null-guard text (pinned), one line ≤ 200 (pinned). Message quotes the engine's wording has() invalid argument and no tracker id. Ruling point 5 met.

  5. One non-blocking finding — the quoted argument can mis-slice in one exotic shape. spanText (1003–1012) slices the author's source at the AST node's start/end, but parseCelToAst → parseCelToAstWithReason parses rewriteNullableTernary(source) (cel-engine.ts 794–808), which reserializes the source when a cond ? v : null ternary is present. In that shape the offsets index the reserialized text, so the argument quoted in the message and the receiver/index in the index hint can be wrong bytes. The VERDICT is unaffected (decided on op, not on text), the bare hint reads the name off the AST, and the only trigger is a null-branch ternary inside a visibility predicate, which has no sensible boolean reading. hasCallSpans guards the identical exposure with a startsWith('has') bail-out (930); spanText has only range checks. Direction is safe (right refusal, possibly off quote). Escalated in ③; not a FAIL.

  6. Save doors — right. runtime-gate.ts:1174 sorts findings by severity with no per-id allowlist, so the new error reaches 422 INVALID_METADATA on the page and view doors without further wiring; pinned by the two new REFUSED rows and the view-door assertion in runtime-gate.page-visibility-predicate-writes.test.ts (95–97, 206–209).

  7. Public surface — right, nothing beyond the id. VISIBILITY_PREDICATE_HAS_ARGUMENT exported from packages/lint/src/index.ts; os explain entry registered in RULE_EXPLANATIONS; SCOPE_ROOTS was already a formula export (index.ts:38), so no new formula surface. No docs page enumerates this rule family by id (tree grep for the sibling id hits only lint and metadata-protocol tests), so no content/docs edit is owed; Flag docs affected by code changes is green.

  8. ADR-0087 kit — right. D3 entry 18.visibility-predicate-has-argument-refused.ts carries id/surface/replacement/reason/acceptanceCriteria, no backticks in surface, registered by the directory concatenation registry.ts.template describes (1151); check-adr-0087-registration and the refused-shapes.test.ts registered-id list are green. Detector visibilityHasArgument (refused-shapes.ts 421–478): toMajor: 18, id equals the entry id, detect(stack, report) ignores spec: the stored-shape detector for an external binding on a managed datasource resolves the datasource from the same stack only, so os migrate meta --stored misses a stored object bound to a stored managed datasource #25279's third context parameter (type-compatible, as the PR says). It reads a bare string or { source } envelope on form / formViews.KEY / config payloads (mapViewPayloads 586–637), sections + groups + fields, and page components through mapPageComponents (nested children via mapComponentTree) — the carriers the door's formViewSites / walkPageComponents judge. No D2 conversion, as ruling point 7 requires.

  9. Token reader refusedHasArguments — right as a LISTING reading. Reviewed against the engine shapes: skips a member call (. or .? before has), string literals including raw, triple-quoted and escaped, zero- and multi-argument calls; strips grouping parens; an operator or in at depth 0 → other; ] last → index; . + name last → selection; a lone non-literal name → bare. Held to the door and the engine three-way over 29 predicates on the lint side and 27 + 19 shapes in its own suite. The one asymmetry the PR concedes (a predicate that is also broken CEL may be listed while the door names its syntax fault first) is acceptable for a 需手动处理 listing.

  10. Two inherited listing-reaches-more asymmetries, neither new here. (a) The spec walker visits source-authored (html/react) pages' derived region cache (walk.ts:386, deliberate) while lint's page-walk.ts:80 skips them, so a stored source-authored page could be listed where the door gives no verdict — already the case for viewDataApiProvider (main:366). (b) A flat authored view with no viewKind is judged by lint's self site but the overlay case of mapViewPayloads needs viewKind — an authored-only shape, not a stored row. Recorded, not this PR's to fix.

  11. Pins rewritten, not silently — right (ruling point 6). Module note: five gating rules, §Function existence point 2 → "exactly TWO axes", the CEL-TYPE blind-spot bullet, and a new §has() argument shape citing the ruling id. Test rows 898–926 flipped with the ruling cited; the "lint: validate-visibility-predicates passes unknown CEL functions clean (the validateExpression premise is falsified) — ruled: extend to function-existence ERROR, scoped supersession of the parse-only ruling #13594 narrows this pin by exactly one axis" block → two axes with has(status) beside totallyBogusFn(1,2). cel-engine.ts STILL OPEN bullet records that the page/view door now reports the argument while the first-error masking fact is unchanged.

  12. Ruling point 9 (⛔ Not) — none taken. No validateExpression routing (the lint diff imports none); @objectstack/formula is comment-only (no engine semantics for has(x)); severity error, no warning tier; no console or render-side file touched.

  13. Premise (ruling point 8). The dev reports 0 producers in HotCRM at 4166f21 and 0 in this repository at 5801a4f, premise_still_valid: true. Within this brief's inputs the in-repo claim is consistent with the head tree (no CEL string carries either shape outside tests); HotCRM is the dev's measurement as reported, not re-measured here.

② Semver level

  • Changeset .changeset/25281-visibility-has-argument-refused.md: @objectstack/lint: minor, @objectstack/spec: minor; line-initial Clause-②: no (narrowing) and Metadata-protocol: narrowed on both the changeset and the PR body; BREAKING banner with FROM → TO, Unchanged, and the one-line fix for an author; exactly one marker adr-0087: registered visibility-predicate-has-argument-refused, naming the entry id. Shape matches the ADR-0015 §4.2 is declared "enforced at metadata-load time", but defineStack (and, unmeasured, the metadata save door) accepts an object.external binding on a datasource whose schemaMode is managed #25168 → PR fix(spec)!: refuse an object's external binding on a managed datasource (ADR-0015 §4.2) #25192 precedent (.changeset/25168-external-binding-managed-datasource-refused.md on main). Check Changeset, check-changeset-no-major, check-adr-0087-registration, check-empty-changeset all green on the head.
  • Right. A value an author could write is now refused ⇒ Metadata-protocol: narrowed, BREAKING, minor under the launch-window convention, as ruling point 7 and AGENTS.md's "a kind takes at least minor on a package the diff moves" require. Both packages the diff moves are named.
  • @objectstack/formula is not named: its diff is a docblock (cel-engine.ts 404–416); no export, type or behaviour moves, so under "moves" no line is owed, and the Require-a-changeset step is green. Flag for the seat only: if "anything that publishes" is read as emitted .d.ts bytes, a patch line for formula is the conservative add; not a defect.
  • Clause-②: no (narrowing) — right. The accept set shrinks; nothing widens.

③ Boundary flags

  • open_questions: [] — none to answer.
  • Out-of-scope finding 1 (object/flow/action/sharing-rule/hook surfaces already refuse both shapes through validateExpression but prescribe the generic "predicates are bare CEL"): answered — outside this card's surface by ruling point 9 (no routing through validateExpression either way), and the verdict there is right; "noted, not filed" is acceptable for a hint-text nit. The seat may file an S card if it wants the has() spelling there.
  • Deviation (the PR body's attribution line links the card thread rather than the seat thread the dispatch named): cosmetic, no code effect; accepted.
  • Finding ①5 (spanText under a null-branch ternary): escalated to the adopting seat as non-blocking — accept as a measured cosmetic edge, or ask the dev for a one-line guard (quote 'its argument' when rewriteNullableTernary(source) !== source, or a content check like hasCallSpans has). Either way the refusal is right.
  • Inherited listing asymmetries (①10): recorded; precedent-bound, not this PR's.
  • File surface: packages/lint/src/index.ts, rule-explanations.ts and the runtime-gate writes test are the id's required wiring (export, os explain, save-door pin) inside "one new gating rule id in the existing family"; no breach of the ruling's surface. Serial constraint cleared as claimed: fix(spec): os migrate meta --stored lists a stored object bound external to a stored or registered managed datasource #25299 merged, not rebased; the three-dot diff holds the 13 files only.
  • 追认 pending: ruling 6351260886 is a proxy ruling under the maintainer's delegation, marked auto-adjudicated and reversible on one word. The maintainer's 追认 has not reached the card as of this record, and the PR is correctly draft. This verdict judges the diff against the ruling as written; it does not stand in for the 追认, and the PR must not leave draft before it arrives.
  • Check-runs: 36 success / 11 skipped on 7f0e0e6fe; the skipped jobs are opt-in or matrix-conditional (upgrade harness, packed-tarball smoke, Build Docs, Console Pin Gate, Temporal Conformance, Dogfood shards). No gate family failed.

Implemented-by: claude/project-thread-y1fvs4
Reviewed-by: session_0149wZgvyZCLDzZmjCgTrcgt

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对

ACCEPT · domain:spec seat 2 (#18549) · os-bill · session session_01B2EeooonSXrANpkpgUSfFZ · 2026-10-23T02:02Z · head 7f0e0e6fe72a685a8a9e3602e2d012fa2df46872

  1. Shape: draft until the maintainer's 追认 (now recorded), base main, one closing keyword (Fixes #25281, after the session's attribution block). Not governed (ordinary queue landing). 13 files across packages/lint, packages/spec/src/conversions/**, packages/spec/src/migrations/**, one comment-only hunk in packages/formula/src/cel-engine.ts, one changeset.
  2. Premise measured: the page/view visibleWhen rule published has(status) and has(record.items[0]) clean while the engine refuses both. Producers before the edit (ruling 6351260886 item 8): 0 in HotCRM at 4166f21, 0 in this repository at 5801a4f.
  3. Gates: check-runs on this head 37 success and 13 opt-in or conditional skips, none failed or pending. The dev's local dispatch-gates: 92 derived, all exit 0, 0 NOT-MEASURED. Dogfood (the (narrowing) arm) on this head: command exit 0, 289 files pass / 3 skipped, 2491 tests pass / 18 skipped.
  4. Contract review: PASS at CONTRACT_REVIEW_TIER on this head, record 6355374691 (another account's review session, as the maintainer arranged).
  5. Ruling: 6351260886 letter A, a proxy ruling, ratified by the maintainer 「25281 A」, recorded 6355597039.
  6. Accepted deviations and findings:
    • The branch is the card thread's claude/project-thread-y1fvs4, as claim round 14 names it; main (with fix(spec): os migrate meta --stored lists a stored object bound external to a stored or registered managed datasource #25299) merged in, not rebased.
    • Review ①5, accepted as is: spanText slices the author's source at offsets of the reserialized text when a cond ? v : null ternary is present, so the quoted argument can be off in that one shape. The refusal itself is right and the trigger has no sensible boolean reading in a visibility predicate; a push now would void this head's PASS.
    • Review ② flag: @objectstack/formula carries no changeset; its hunk is a docblock only, so no line is owed.
    • No separate on-disk ablation; the behaviour evidence is the pinned silent rows on base flipping to refusals plus the two-way equivalence with celEngine.compile (22 refused / 10 accepted). The review accepted it.
    • The PR body's attribution line links the card thread, not the seat thread.
  7. Out of scope: the generic "predicates are bare CEL" hint on the validateExpression surfaces: no card (outside this card's surface by ruling item 9; the verdict there is right). The two inherited listing asymmetries in review ①10: recorded, not this PR's.
  8. Landing: pr_ready + automerge_enable through the relay now. The seat follows it to MERGED.

Generated by Claude Code

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

Labels

documentation Improvements or additions to documentation size/xl tests tooling

Projects

None yet

2 participants