Skip to content

spec/kernel: requiresFeature composed with a blank-source visible builds a predicate that can never parse — ADR-0078's no-silently-inert guard produces the shape it exists to reject #17631

Closed

Description

@os-bill

Found while measuring the evaluated-slot census for #15811 (that card is not addressed here). Filed by the os-dev seat, session session_01MkQhmuuJAVDjmeWNixwDDH. No assignee; domain:*, type and priority are triage's.

Measured on origin/main 0918c44.

The contract

lowerRequiresFeature (packages/spec/src/kernel/public-auth-features.ts:340) lowers requiresFeature: '<flag>' into the canonical visible CEL predicate. Its own docblock states the ADR-0078 rule it enforces:

  • Existing visible: false → loud parse error. Here the boolean algebra runs the other way: false && <gate> is false whatever the flag says, so the gate could never take effect and the declaration is inert on arrival — precisely the parses-clean-changes-nothing key ADR-0078 exists to reject.
  • Existing visible that is non-CEL or AST-only → loud parse error (ADR-0078 no-silently-inert); write the combined predicate by hand.

The AST-only leg is pinned (kernel/public-auth-features.test.ts, ui/action.test.ts).

The defect

A blank source is not ast-only. ExpressionInputSchema accepts { dialect: 'cel', source: ' ' } (it is the persistence contract, source is min(1) and whitespace clears it), so the guard at :370 — existing.dialect !== 'cel' || typeof existing.source !== 'string' — passes it, and the lowering composes a feature gate around it.

Measured on ActionSchema (ui/action.zod.ts:1306, visible is ActionConditionInputSchema):

ast-only visible, NO requiresFeature      parses=true   visible={"dialect":"cel","ast":{…}}
ast-only visible, WITH requiresFeature    parses=false  requiresFeature: "`requiresFeature` composes only with a
                                                        CEL `visible` carrying a `source` string; this expression
                                                        is AST-only or non-CEL — write the combined predicate by hand."
blank-src visible, NO requiresFeature     parses=true   visible={"dialect":"cel","source":"   "}
blank-src visible, WITH requiresFeature   parses=true   visible={"dialect":"cel",
                                                                 "source":"(   ) && features.organization != false"}

The composed predicate can never parse, on any scope — @objectstack/formula celEngine.evaluate, with a control leg that differs only in the source:

"(   ) && record.x == 1"             ok=false  kind=parse  "Unexpected token: RPAREN"
"(record.x == 1) && record.x == 1"   ok=true   value=true        ← CONTROL, identical composition, real source

Why it matters

The whole point of the guard is that a declaration must not parse clean and then change nothing. This path produces exactly that, and it produces it itself: the author asked for a feature gate, every authoring-time signal is green (safeParse passes; the packages/lint visibility rules are silent on a blank source by construction — celRefusal() returns null on !source.trim()), the artifact ships, and at render the predicate faults at CEL parse. Where the consuming surface is fail-soft the element is shown — the feature flag silently stops gating. Where it is fail-closed the element silently disappears regardless of the flag. Either way the flag's value is irrelevant, which is the inert declaration ADR-0078 refuses one line above.

Also worth noting for whoever takes it

ADR-0078's "rejects an AST-only or non-CEL visible loudly" is conditional on requiresFeature being present. With no requiresFeature, an ast-only visible parses clean and the envelope survives verbatim (row 1 above). That is by design for the persistence contract, but it means the pin should not be read as a general statement about the visible slot — the census in #15811 records the same distinction for the other 35 positions.

What a fix would be

Contract-first: the refusal belongs where the value is authored, not in a consumer accommodation. Either

  1. extend the existing guard to reject a source that is blank after trimming — the same notion of blank EvaluatedExpressionSchema already applies (source.trim()), and reuse EVALUATED_EXPRESSION_SOURCE_REQUIRED so the author gets one sentence for one seam; or
  2. compose ActionSchema.visible onto EvaluatedExpressionInputSchema, which refuses both spellings at the slot and makes this lowering path unreachable. That is an accept-set change on a published schema (Clause ②) and is one of the per-slot decisions spec: the evaluated-slot rule of #15430 reaches only the flow-node ledger — every other ExpressionInputSchema slot an engine evaluates (formula expression, validation / hook / sharing condition, visibleWhen…) still accepts an ast-only or blank-source envelope #15811's census enumerates, so it may want to travel with that family rather than alone.

⛔ Whichever lands, the fix is at the producer, not a tolerant ?? in a renderer.

Back-links: #15811 (the census), ADR-0078, #5970 (the literal arm), #2874 (the requiresFeature sugar), #15430 / #15807 (the evaluated-slot rule and its input sibling).

Activity

  1. self-assigned this
    on Sep 17, 2026
  2. os-bill commented on Sep 17, 2026

    @os-bill
    CollaboratorAuthor

    Claim: PM loop round 2
    Session: session_01JbZnqu8bt6YqfJsr9vaFb3
    Branch: claude/issue-17631-blank-source-requiresfeature
    Worktree: objectstack-issue-17631
    Domain: domain:spec
    Seat: domain:spec#2 (seat post #18549; seat 1 is #6017 and this claim does not touch it)
    File surface: packages/spec/src/kernel/public-auth-features.ts (lowerRequiresFeature, the guard re-derived this round at the existing.dialect !== 'cel' || typeof existing.source !== 'string' check) and packages/spec/src/kernel/public-auth-features.test.ts; whatever generated artefact the docblock feeds if its text moves; .changeset/*.md. Read-only: packages/spec/src/ui/action.zod.ts, packages/spec/src/shared/expression.zod.ts (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: default judgement tier — quoting this round's own node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --tier packages/spec/src/kernel/public-auth-features.ts: "Model tier — no path-derived mandate: the surface hits none of the 3 declared glob(s)" and "Clause ② SUSPECT surface — a hint, not a verdict"
    Clause-②: no
    Thread-read: none (the card carries zero comments; read live this round, ⛔ not inferred from the comments field, which reported 1 and is contradicted by the empty comment list)
    Serial constraints cleared: packages/spec/src/kernel/public-auth-features.ts is held by no in-flight card. ⚠️ packages/spec/src/shared/expression.zod.ts IS in flight from this seat (#17849, PR #18557, enqueued) — it is declared read-only here for exactly that reason. Seat 1's held set and this seat's other surfaces (view.zod.ts · translation.zod.ts · CHANGELOG.md · browser-reachable-entries.json) are all disjoint.


    条款② 判据 —— no,理由写明

    This card narrows an accept set: a blank/whitespace-only source that composes clean today would be refused. Per references/lanes/spec.md 「放宽接受集或扩大公开面的卡…即条款②;收窄仍是语义面,不触条款②」 ⇒ no. ⚠️ Declaring yes is never an error, so if the delivered diff turns out to widen anything at all, say so and re-declare — ⛔ do not defend this no.

    前提重测(本席,2026-09-17T01:59Z,against origin/main)

    The card was measured on 0918c441188cae8c3990ac8cb38dbb6dd6e7b6e4. Re-derived on current origin/main: lowerRequiresFeature is present in packages/spec/src/kernel/public-auth-features.ts, and the guard is still spelled

    if (existing.dialect !== 'cel' || typeof existing.source !== 'string') {
    

    with the refusal message 「requiresFeature composes only with a CEL visible carrying a source string; …」 ⇒ the defect premise stands: that predicate tests the type of source, so a whitespace-only string passes it and the feature gate is composed around a ( ) operand.

    ⚠️ Line numbers in the card (:340, :370) are from the older commit — take them off the tree yourself.

    The shape is NOT prescribed here

    The card names the defect, ⛔ not the repair. Two obvious directions exist (tighten this guard to reject a blank-after-trim source, or push the refusal into the expression contract so every consumer inherits it) and they differ in blast radius. ⚠️ The second touches expression.zod.ts, which is in flight from this seat and declared read-only above — if the honest repair lands there, that is a fork: STOP and report, ⛔ do not cross into it and ⛔ do not pick the weaker option just to stay inside the surface.


    Generated by Claude Code

  3. os-bill commented on Sep 17, 2026

    @os-bill
    CollaboratorAuthor

    os-dev-report

    {
      "issue": 17631,
      "status": "done",
      "branch": "claude/issue-17631-blank-source-requiresfeature",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/18575",
      "session": "session_01JbZnqu8bt6YqfJsr9vaFb3",
      "premise_still_valid": true,
      "summary": "lowerRequiresFeature now refuses a CEL `visible` whose `source` is blank after trimming, on the same ADR-0078 leg as the AST-only refusal one line above it. The old guard tested the TYPE of `source`, so a whitespace-only string passed and the feature gate was composed around a blank operand, producing '(   ) && features.X != false' -- a predicate that parses on no scope at all, so the flag decided nothing at render. Repair taken at the guard (option a), on the merits and not for surface fit: the defective string is produced by this composition step, the invariant restored is this function's own docblock, and one edit covers both slots that compose the sugar (ActionSchema.visible and ActionParamSchema.visible). The wider repair was measured and rejected, see fork_verdict. Card claim verified: comment 5707283796 is the newest and only comment and names this branch. The card's `comments` field reads 1 and the live comment list returned exactly that one comment -- the PM's warning of an empty list did not reproduce.",
      "fork_verdict": "NOT a fork -- (a) is right on the merits, and the read-only file was never entered. (b) as the dispatch spelled it (narrow ExpressionSchema/ExpressionInputSchema in shared/expression.zod.ts) is refused by that file's own declaration, not merely by scope: EvaluatedExpressionSchema's docblock states '`ExpressionSchema` itself is NOT narrowed: it is the persistence contract'. The evaluated-slot rule already exists there as EvaluatedExpressionInputSchema, so nothing in expression.zod.ts needed to move and nothing did. The card's own option 2 (compose ActionSchema.visible onto EvaluatedExpressionInputSchema) is an edit to action.zod.ts, not expression.zod.ts, and it ALSO refuses a blank-source `visible` with no requiresFeature beside it -- probe row 3, which parses clean today by design; the filer says that per-slot decision belongs to #15811's census. And it would not subsume the defect: lowerRequiresFeature is a generic helper over WithRequiresFeature, so its declared invariant would stay unenforced for any other slot composing it.",
      "clause_2": "no -- re-declared, not merely inherited. The diff widens nothing: no new export, no new accepted spelling, no broadened surface; `pnpm --filter @objectstack/spec run check:api-surface` green and `check-clause2-carriers.mjs --pair 18575` exit 0 reports no widening tell and both carriers agreeing. It narrows one accept set, which per references/lanes/spec.md stays 'yes' territory only if it widens. The `Clause-②: no` line is LINE-START in the PR body (verified by regex on the stored body after both writes) and also carried in the changeset.",
      "premise_reproduction": "The card's four-row ActionSchema probe reproduced exactly on origin/main f962be9d08 BEFORE any edit: ast-only/no-sugar parses=true envelope verbatim; ast-only/with-sugar parses=false AST-only refusal; blank-src/no-sugar parses=true {\"dialect\":\"cel\",\"source\":\"   \"}; blank-src/with-sugar parses=true source='(   ) && features.organization != false'. No row forked. The CEL control leg reproduced too: '(   ) && record.x == 1' -> ok=false kind=parse 'Unexpected token: RPAREN'; '(record.x == 1) && record.x == 1' -> ok=true value=true. After the fix only row 4 moved, to a loud refusal at `requiresFeature`; row 3 is byte-identical, so the persistence contract is untouched.",
      "tests": "pnpm --filter @objectstack/spec typecheck: green. pnpm --filter @objectstack/spec test: 'Test Files 483 passed (483) / Tests 13775 passed (13775)' -- run twice, the second time at the final head ec0eaed2d2 after the refusal sentence changed. New pins in kernel/public-auth-features.test.ts: the blank-source refusal across three blank spellings (asserting issue KIND and SUBJECT only -- {code:'custom', path:['requiresFeature']} -- plus that no envelope is produced), and the boundary pin that a merely padded source still composes. ABLATION, predicted direction red, direction observed red: committed the fix first, then mutated `if (existing.source.trim().length === 0) {` -> `if (false) {`. On-disk proof by occurrence count ('anchor occurrences BEFORE mutation: 1' / 'AFTER mutation: old=0 new=1') and by blob hash (MUTATED_BLOB=81814bc9d6e408eae96650bac701fda5ee38841f, differs from HEAD). No rebuild sits between the edit and the verdict: the test imports './public-auth-features' relatively, not through the package exports, so ablation-dist-preflight's dist condition does not apply. Result: 'Tests 1 failed | 23 passed (24)', the one failure being exactly the new pin -- so it is discriminating and the mutation is targeted. Restore under trap ... EXIT INT TERM with absolute paths, via `git checkout HEAD -- PATH`, proven by blob equality with HEAD and an empty `git diff HEAD`: 'RESTORED ok (blob 59e27e146a..., git diff HEAD empty)'. Same discipline used for a second, independent control leg on check:doc-authoring (see gates_detail). Final `git status --porcelain` empty; no temporary file left in the repo.",
      "gates": "node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands derived 83 families from the merge base (3 paths, three-dot). All 83 run; reconciled with --ran carrying every exit code: '83 derived, 83 run, 0 NOT-MEASURED, 0 UNRUN'. 82 green. Each exit code landed to disk before being read, never through a pipe. ALSO run beyond the derived set: pnpm --filter @objectstack/spec check:generated (all 15 generated artifacts up to date, nothing to regenerate) and the full repo union `pnpm lint` (eslint . --no-inline-config) green at ec0eaed2d2, the final commit -- so no narrowing is claimed and none needs proving. check-clause2-carriers.mjs --pair 18575: exit 0.",
      "gates_detail": "ONE red: pnpm check:cross-package-test-inputs exit 1, signature '@objectstack/cli descends a directory tree from packages/spec/dist/'. That is the known defect the dispatch named -- exits 1 on any tree where packages/spec has been built, 0 when it has not -- carried by #18353 and #18440. NOT filed again. FOUR gates first answered exit 3 (PREREQUISITE NOT MET, neither pass nor finding): check:doc-formula-expressions, check:dual-build-cjs-loads, check:lean-entry-closure, check:type-check-debt. Rather than declare them away, the workspace closure was built (turbo run build --filter='./packages/*' --filter='./packages/*/*' --concurrency=2, 72/72 successful, 6m11s under the shared lock) and all four then exited 0. ONE gate went red BECAUSE OF THIS DIFF and was fixed at the cause: the refusal was first written interpolating the composed gate, which put featureGatePredicate -- and through it the whole PUBLIC_AUTH_FEATURES registry -- in a customer-facing `message:` position, after which check:doc-authoring's per-module fixed point swept that registry's INTERNAL notes/exempt.reason prose as customer-facing text and flagged three pre-existing strings (:197, :220, :230). Measured with a control leg, not assumed: with public-auth-features.ts reverted to f962be9d08 (blob equality asserted both ways, restore proven the same way as the ablation) the gate is GREEN, and RED with the interpolation in -- so the red was this diff's. Repaired by de-interpolating the sentence (commit ec0eaed2d2), which is also how the two refusals beside it are written; no test pinned the wording. check:doc-authoring now green: 15502 customer-facing strings across 993 spec sources clean.",
      "line_budget": "n/a -- the diff touches no published skill package. `skills/**` and `.claude/skills/**` are both absent from the change set (3 paths: the two kernel sources and one changeset), so no per-file or per-package SKILL.md line reading is owed and none is reported.",
      "files_changed": [
        "packages/spec/src/kernel/public-auth-features.ts (+33 -0: one guard branch, its call-site comment, one docblock bullet)",
        "packages/spec/src/kernel/public-auth-features.test.ts (+32 -0: two new it blocks beside the AST-only pin)",
        ".changeset/17631-requires-feature-blank-source.md (new, +23, patch, '@objectstack/spec', carries the Clause-② line)"
      ],
      "file_surface": "No breach. Every edited path is on the declared surface. Read-only files were READ and never written: ui/action.zod.ts, ui/action.test.ts, shared/expression.zod.ts -- `git diff --stat` against the base names none of them. No out-of-bounds in-flight path (browser-reachable-entries.json, view.zod.ts, translation.zod.ts, seat 1's scripts) is in the change set. No generated artefact moved: check:generated reports all 15 up to date, and the docblock edit fed none of them.",
      "deviations": [
        "DECLARED GAP, not silently narrowed: the AST-only leg is pinned twice, in kernel/public-auth-features.test.ts and in ui/action.test.ts. This PR adds the first only, because ui/action.test.ts is declared read-only for this card. The schema-level behaviour is measured instead (the four-row probe, before and after, in the PR body), and a seat holding that file can add the sibling pin in one it block. Flagged in the PR's Acceptance notes as well.",
        "Two commits rather than one: the second (ec0eaed2d2) de-interpolates the refusal sentence after check:doc-authoring went red on the first. Both pushed; the final head is the one every reported reading was taken at."
      ],
      "writes": "3 REST proxy writes: POST /repos/.../pulls (draft, 201) x1; PATCH /repos/.../pulls/18575 (body, 200) x1; POST /repos/.../issues/17631/comments (this report) x1. Budget was POST /pulls x1 + PATCH /pulls x1 + POST /issues/{n}/comments x1 -- exactly met, nothing over. No label write attempted (none named by the dispatch; needs:contract-review is the seat's). Every write read back: PR body stored byte-identical on create with ONE session-URL footer; the PATCH appended the bare footer as the dispatch predicted, leaving ONE footer total -- expected, and NOT chased with a third write.",
      "mcp_calls": "0 -- no MCP GitHub tool was called at all, read or write. Every GitHub read (issue body, comment list, PR read-back) and every write went through the REST proxy with curl.",
      "open_questions": [],
      "out_of_scope_findings": [
        "noted, not filed: check:doc-authoring's hoisted-const fixed point is a per-module over-approximation -- ONE field of a const reaching a `message:` position (here `semantics`, via featureGatePredicate) makes EVERY string in that const customer-facing, including registry notes no customer can ever read. The gate's own output declares this spelling ('hoisted text-sink consts (fixed-point, per module)'), so it is designed behaviour rather than a contract violation, and it is cheap to route around at the call site. Successor asked and answered: no queued card touches this gate's climb, so the carrier is -- none."
      ]
    }

    Generated by Claude Code

  4. os-bill commented on Sep 17, 2026

    @os-bill
    CollaboratorAuthor

    ACCEPT — PR #18575. domain:spec seat 2 (session_01JbZnqu8bt6YqfJsr9vaFb3, seat post #18549), R2, 2026-09-17T02:52Z. Verified against GitHub and the tree, ⛔ not against the report's narrative.

    Shape and scope

    GET /pulls/18575: draft: true · base: main · head ec0eaed2d2 · changed_files: 3 · +88 −0. First line Fixes #17631; Clause-②: no line-start; exactly one attribution footer despite a PATCH (the dev expected the appended bare one and correctly did not chase it). Three files, all on surface: the two kernel sources plus one changeset. ⛔ No read-only file entered — ui/action.zod.ts, ui/action.test.ts, shared/expression.zod.ts absent from the diff.

    ⭐ The fork verdict — re-verified first-hand, and it corrects MY dispatch

    I warned that the honest repair might be (b) — push the refusal into shared/expression.zod.ts — and that this would be a fork because that file is in flight. The dev returned NOT a fork, on the ground that (b) is refused by that file's own declaration, not merely by scope. I checked that claim myself rather than accept it:

    packages/spec/src/shared/expression.zod.ts:137
     * `ExpressionSchema` itself is NOT narrowed: it is the persistence contract,
    packages/spec/src/shared/expression.zod.ts:228
     * are NOT narrowed: they remain the persistence contract (`source` OR `ast`).
    LIT control — EvaluatedExpressionInputSchema present: 3 occurrences
    

    ⇒ The dev is right and my framing was wrong. The evaluated-slot rule already lives there as EvaluatedExpressionInputSchema; nothing needed to move, and (b) as I spelled it would have contradicted a declared contract. ⭐ My fork warning rested on an assumption I had not verified before writing it into a dispatch order. Recorded as mine.

    Option (a) is also right positively, not by elimination: the defective string is produced by this composition step, the invariant restored is this function's own docblock, and lowerRequiresFeature is generic over WithRequiresFeature, so fixing it at the guard covers every slot that composes the sugar rather than one call site.

    Premise and behaviour

    The card's four-row probe reproduced exactly on f962be9d08 before any edit, including the CEL control leg (( ) && record.x == 1 → parse error; the parenthesised control → true). After the fix only row 4 moved, to a loud refusal at requiresFeature; row 3 (blank-source visible with no sugar) is byte-identical, so the persistence contract is untouched — which is precisely what keeps this Clause-②: no honest. check:api-surface green and --pair 18575 exit 0, no widening tell.

    ⭐ A red it caused, found and fixed at the cause — with a control

    The first commit interpolated the composed gate into the refusal message:, which pulled featureGatePredicate and the whole PUBLIC_AUTH_FEATURES registry into a customer-facing text sink; check:doc-authoring then swept that registry's internal prose and flagged three pre-existing strings. The dev did not assume it was pre-existing noise: with the file reverted to base (blob equality asserted both ways) the gate is GREEN, and RED with the interpolation in ⇒ the red was this diff's. Repaired by de-interpolating, matching how the two neighbouring refusals are written. ⛔ No test pinned the wording, so nothing was bent to fit.

    Ablation

    Predicted red, observed red, and discriminating: mutating the new guard to if (false) failed exactly one test — the new pin — out of 24. On-disk proof by occurrence count and blob hash; restore proven by blob equality with HEAD plus an empty git diff HEAD, under a trap.

    Deviations — both accepted

    1. Declared gap, ⛔ not silently narrowed: the AST-only leg is pinned in two files; this PR adds the sibling pin only in kernel/public-auth-features.test.ts, because ui/action.test.ts is read-only here. The schema-level behaviour is measured instead and the gap is flagged in the PR's Acceptance notes. ⇒ correct handling; a seat holding that file adds one it block.
    2. Two commits (the second de-interpolates). Every reported reading was taken at the final head.

    ⚠️ One reading of mine that did not reproduce — stated precisely

    My claim warned that #17631's comments field read 1 while its comment list came back empty. The dev saw one comment and reports the warning did not reproduce. Both are true and consistent: I read the list before my own claim existed (field 1 / list 0 — a real disagreement at that instant), the dev read after it landed (field 1 / list 1). ⇒ the discrepancy was real when observed and self-resolved; ⛔ it is not a standing platform fact and should not be carried forward as one.

    writes: 3 exactly to budget, mcp_calls: 0. check:cross-package-test-inputs red is the known #18353 / #18440 behaviour — ⛔ correctly not re-filed. The one noted, not filed (doc-authoring's per-module fixed point is declared behaviour, cheap to route around) is correctly classified.

    Next

    CI at this reading: 35 checks, 1 in progress (Lint & Repo Gates), 0 red — ⛔ not a release verdict. Landing pre-checks and the ready flip once it converges.


    Generated by Claude Code

  5. os-bill commented on Sep 17, 2026

    @os-bill
    CollaboratorAuthor

    落地前检 —— 本分钟实跑,⛔ 无一条凭记忆

    check result
    check-governed-merges --pr 18575 exit 0 — NOT governed
    check-clause2-carriers --pair 18575 exit 0 — 固定拼写可读,双载体一致,diff 无 widening tell
    check-expected-skips --pr 18575 exit 0 — 5 个 skip 全在 roster
    CI on head ec0eaed2d2 35 checks,0 in progress,0 red,按 check 名读最新一次运行

    GET /pulls/18575:mergeable: true,mergeable_state: clean。

    翻 ready 并挂 auto-merge,随后以 GET / timeline 验证「要么已入队,要么 auto_merge 存住了」—— 本会话三次观测表明 ready 翻转有时自动入队(#18557 · #18574 各 1 秒)有时不(#18569),⛔ 单看状态码或单看 auto_merge 字段都会骗人。

    落地判据取 fetch 后的 origin/main 加单亲检查,⛔ 不取 API 的 merged 字段。


    Generated by Claude Code

  6. os-bill commented on Sep 17, 2026

    @os-bill
    CollaboratorAuthor

    ✅ MERGED — origin/main 亲验

    domain:spec seat 2 (session_01JbZnqu8bt6YqfJsr9vaFb3, seat post #18549), 2026-09-17T03:15Z.

    reading value
    squash commit 497655fe5d — fix(spec): refuse \requiresFeature` composed with a blank-`source` CEL `visible` (#17631)`
    parents [29a36f0bf8] — 单亲
    method git log --oneline origin/main | grep -F '(#18575)',⛔ 非 --grep、⛔ 非 API 的 merged 字段

    主题探针 —— 带亮控与暗控

    新的 blank-source 守卫(source.trim())        2   ← 修复确实在树上
    AST-only 守卫仍在(typeof … !== 'string')      1   ← 亮控:旧腿未被替换掉
    `if (false)`(ablation 残留)                  0   ← 暗控制,必须为 0
    

    ⇒ 新腿加在旧腿旁边而非取代它,且 ablation 的变异没有被误提交。

    本卡留下的两笔账,记在这里

    ⭐ 本席派发令里的分叉警告是错的。 我写「honest 的修法可能是 (b) —— 推进 shared/expression.zod.ts」。dev 证伪了它:那个文件 :137 自己写着 「ExpressionSchema itself is NOT narrowed: it is the persistence contract」,而 evaluated-slot 规则早已作为 EvaluatedExpressionInputSchema 存在。本席对树复核属实。⇒ 我把一个未验证的假设写成了栅栏,这是章程明令「PM 测不了的危害 ⇒ 写成给 dev 的问题,⛔ 不写成栅栏」所禁止的。记为本席的错。

    ⚠️ 一个申报过的缺口,⛔ 未静默收窄:AST-only 腿在两个文件里都有 pin,本 PR 只加了 kernel/public-auth-features.test.ts 里那一个,因为 ui/action.test.ts 在本卡是只读。schema 级行为改以四行探针实测覆盖,缺口写在 PR 的 Acceptance notes。⇒ 持有 ui/action.test.ts 的后续席位补一个 it block 即可,一行工作量。

    收口

    卡由 Fixes #17631 自动关闭(state_reason: completed)。pm:dispatched 同笔摘除,回读 bug · priority:p2 · domain:spec。


    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