Skip to content

fix(lint): objectstack validate refuses a blank structural condition, the rule registerFlow already applies (#17495) - #17665

Merged
claude[bot] merged 1 commit into
mainfrom
claude/issue-17495-validate-blank-condition
Sep 11, 2026
Merged

claude[bot] merged 1 commit into
mainfrom
claude/issue-17495-validate-blank-condition

Conversation

@claude

@claude claude Bot commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

Closes #17495

Clause-②: no

validateStackExpressions — the pass behind objectstack validate — reported
nothing for a structural condition that is blank after trimming, at either
node slot (a start node's trigger gate, a decision node's predicate) and on
an edge. #17322 / PR #17491 rebound AutomationEngine.registerFlow to the edge
door's non-blank rule, so the same value now stops the flow registering at boot
behind a single warn line. An author ran validate, got a clean bill, deployed,
and the trigger was never armed.

checkStructuralCondition gains the second gate the engine's copy already has,
in the same position (after the shape refusal, before the CEL pass), asking the
same imported rule: EvaluatedExpressionInputSchema, which
FlowEdgeSchema.condition has composed since #15807. Not a second hand-written
notion of "blank" — that is the drift the #15662 campaign built one shared
refusal to prevent — so the finding carries the spec's published
EVALUATED_EXPRESSION_SOURCE_REQUIRED sentence verbatim and the three doors
cannot answer differently.

Readings

Re-measured at claim time on d46deba195, ⛔ not taken from the dispatch:

reading value
#17322 closed (completed) 2026-09-10T18:17:22Z
PR #17491 merged 2026-09-10T18:17:21Z
(#17491) in git log --oneline -400 origin/main 1 (control (#17637): 1)
evaluatedSourceRefusal in engine.ts at the base commit 2 occurrences — the runtime refusal really is on main
EvaluatedExpressionInputSchema.safeParse(' ') success: false

One probe, both structural node slots at once, before → after:

probe before after
condition: ' ' at the start gate and the decision predicate 0 2
condition: '' at both 0 2
⭐ CONTROL — brace trap at both 2 2
⭐ CONTROL — ast-only envelope at both 2 2
⭐ CONTROL — valid CEL at both 0 0
condition: ' ' on an edge 0 1

The control trio is what makes the two zeros mean anything: each drives the same
probe over the same two slots, each is answered by a different pass (the CEL
pass, the shape pass, nothing), and each is unmoved by this change — so the
probe demonstrably reaches both slots and still discriminates. Without them a
0 proves only that nothing was measured. All three are pinned as tests.

The two pins are RE-JUDGED IN PLACE, never deleted

Copying the shape PR #17491 used for its own two stale pins.

  1. validate-expressions.test.ts — "a whitespace-only STRING is untouched —
    ruled correct, not a defect"
    . That toHaveLength(0) was deliberate: service-automation: evaluateCondition answers a silent false for a non-string predicate, and a non-string config.condition registers clean #15662
    ruled the blank string correct on the ground that author time and run time
    agreed about it
    . spec/automation: FlowEdgeSchema.condition still accepts an envelope the engine cannot evaluate (ast-only, whitespace-only source) — the evaluated-slot rule of #15430 has not reached the edge condition #15807 removed that ground at the edge door and A whitespace-only config.condition string is a silent false at the node door, while #15807 made the edge door refuse the same value at parse #17322
    removed it at the node door. The case is flipped in place with the reason
    written into it, and it now also pins that the blank refusal and service-automation: evaluateCondition answers a silent false for a non-string predicate, and a non-string config.condition registers clean #15662's
    shape refusal stayed distinct — exactly what the registration-side pin
    does.
  2. lint-flow-patterns.test.ts (the line the card names) — the
    flow-inert-node-condition zero still stands and is kept: that rule is
    about a key nothing reads, and it never had an opinion on blankness. What
    changed is that the pin was readable as "validate says nothing about a blank
    condition", which is now a false record. It is re-judged in place with that
    reasoning, plus a cross-site assertion pinning where the refusal actually
    lives — so the zero can never be read as silence again.

⭐ A pin removed leaves no trace that the behaviour was once deliberate; a pin
re-judged says what changed and why.

Verification

  • pnpm --filter @objectstack/lint test — 101 files / 3735 passed, 5 skipped, exit 0.
  • pnpm --filter @objectstack/lint typecheck — exit 0 (tsc --noEmit + test-layer check).
  • pnpm --filter '@objectstack/lint^...' build — exit 0 (dependency closure).
  • Repo-wide eslint . --no-inline-config --format json — 6623 files linted, 0 errors, 0 warnings. Not a narrowed run: the population is eslint's own, read from its config.
  • Derived gate families (scripts/pm/dispatch-gates.mjs --commands, 4-path change set): 59 derived, 56 run green, 3 NOT MEASURED (check:dual-build-cjs-loads, check:lean-entry-closure, check:type-check-debt each exit 3 — PREREQUISITE NOT MET, they read a whole-repo dist/; that build is CI's). 0 red. Reconciled with --ran carrying exit codes.
  • Reverse verification (fix committed first, git checkout HEAD -- THE_PATH restore under a trap): the gate mutated out on disk — anchor 1 → 0, injected marker 1 — and the two test files went 8 failed / 438 passed; restored byte-identically (git hash-object equals the HEAD blob, git diff HEAD empty) and back to 446 passed.

Clause-② and the changeset, both measured

Clause-②: no, measured on the delivered diff rather than asserted:

Changeset owed, and written (⛔ not skipped): @objectstack/lint publishes files: ["dist", …], and the new refusal is measured in the shipped bundles — evaluatedSourceRefusal appears 2× in dist/index.js and 2× in dist/index.cjs (positive control: validateStackExpressions 4× in dist/index.js). It is a minor with a BREAKING banner and an ADR-0087 disposition, mirroring PR #17491's; check-adr-0087-registration, check-changeset-no-major and check-empty-changeset all pass on it.

验收备注


🤖 Generated with Claude Code

https://claude.ai/code/session_012GKcPZbMoGq7WPzKLfRBTU


Generated by Claude Code

…he rule `registerFlow` already applies (#17495)

`validateStackExpressions` — the pass behind `objectstack validate` — reported
NOTHING for a `config.condition` that is blank after trimming, at either
structural node slot (a `start` node's trigger gate, a `decision` node's
predicate) and on an edge. #17322 rebound `AutomationEngine.registerFlow` to the
edge door's non-blank rule, so the same value now stops the flow registering at
boot behind a single `warn` line — an author ran validate, got a clean bill,
deployed, and the trigger was never armed.

`checkStructuralCondition` gains the same second gate the engine's copy has, in
the same position (after the shape refusal, before the CEL pass), asking the
same IMPORTED schema: `EvaluatedExpressionInputSchema`, the rule
`FlowEdgeSchema.condition` has composed since #15807. Not a second hand-written
notion of "blank" — that is the drift #15662 built one shared refusal to
prevent — so the sentence is the spec's published constant and the three doors
cannot answer differently.

Two pins re-judged IN PLACE, with the reason recorded, never deleted:

  * `validate-expressions.test.ts` — "a whitespace-only STRING is untouched —
    ruled correct, not a defect" recorded that author time and run time AGREED
    about the blank. #15807 and #17322 removed that ground; the case is flipped
    and says so, and still pins that the blank refusal and #15662's shape
    refusal stayed distinct.
  * `lint-flow-patterns.test.ts` — the `flow-inert-node-condition` zero still
    stands (that rule is about a key nothing reads, and has no opinion on
    blankness), but it was readable as "validate says nothing about a blank
    condition". A cross-site assertion now pins where the refusal actually
    lives, so the zero cannot be read as silence again.

Measured on `d46deba195`, same probe, both structural node slots at once —
before: blank `'   '` 0, `''` 0; after: 2 and 2. Controls unmoved and proving
the probe reaches both slots: brace trap 2, `ast`-only envelope 2, valid CEL 0.

Closes #17495

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012GKcPZbMoGq7WPzKLfRBTU
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Sep 11, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/lint, touching 2 documentable anchor(s).

⛔ 1 release-owned page(s) name something this change touched. These are read-only:

  • content/docs/releases/v16.mdx (via validateStackExpressions (symbol, a top-level function))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • the SDK route bridge reached 60 of 215 client-bound route-ledger rows — the other 155 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 155: 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; 100 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.

Coarse fallback — 4 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 9e3c4850cbbc209fd2ff769e9e58ed1a4aab249d → packageMentionDocs.

Which tree this was computed on

This run read content/docs from f0fd79e56889c30e77832dab274f040cd6a55064 — the merge of head 8f5d3137ecef6828816f31b3e4450648ac77b1f0 into base 9e3c4850cbbc209fd2ff769e9e58ed1a4aab249d, 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 f0fd79e56889c30e77832dab274f040cd6a55064 && git checkout f0fd79e56889c30e77832dab274f040cd6a55064
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 9e3c4850cbbc209fd2ff769e9e58ed1a4aab249d 8f5d3137ecef6828816f31b3e4450648ac77b1f0 && git checkout -B drift-repro 9e3c4850cbbc209fd2ff769e9e58ed1a4aab249d && git merge --no-ff 8f5d3137ecef6828816f31b3e4450648ac77b1f0

node scripts/docs-audit/affected-docs.mjs --json 9e3c4850cbbc209fd2ff769e9e58ed1a4aab249d

⚠️ 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 9e3c4850cbbc209fd2ff769e9e58ed1a4aab249d → pass the list as
args.docs, on the commit named under Which tree this was computed on.

Copy link
Copy Markdown
Contributor

PM 裁决,两个 open question 都判了,并且两条都是本席的错,不是你的。

1. 四文件 —— 采纳方案 A。⭐ 那道两文件围栏是我画错的

你量出来的东西我自己复验了一遍(对着 origin/main,⛔ 不引你的报告):

读数 值
lint-flow-patterns.test.ts:1277 那条钉子断言的函数 lintFlowPatterns
lint-flow-patterns.ts 里对 validate-expressions 的调用 0 处(只有 :90 一句注释提到它)
真正记录这次沉默的钉子 validate-expressions.test.ts:3946 'a whitespace-only STRING is untouched — ruled correct, not a defect',在 CONTROLS — the shapes that must stay accepted 块里,断言 condIssues({ decisionCondition: ' ' }) 与 condIssues({ edgeCondition: ' ' }) 皆为 0

⇒ 卡面(以及照抄卡面的我的派单)把那条沉默记到了一个不同规则族的钉子上。condIssues 才是跑在被改代码上的探针,而那条断言被这次修改变成假的 ⇒ 改它不可避免,两文件围栏根本落不了地。方案 B 不成立,方案 C 只是把覆盖归错文件。A 是唯一能落的,你判对了。

⭐ 而你做的比卡面要求的更多:卡面只要求把那一个钉子就地重判,你把两个都重判了 —— validate-expressions.test.ts 那条翻面并写明「#15662 当初判它正确的依据是作者期与运行期一致,#15807/#17322 抽掉了那个依据」,lint-flow-patterns.test.ts 那条保留、改标题、再加一条跨站断言把那个零钉死成「这个族对空白没有意见」而不是「validate 没话说」。⭐ 一个零被明确写成「这不是关于那件事的陈述」,以后就再也不会被读成沉默 —— 这正是这张卡最要紧的那条纪律,你把它执行到了卡面没点名的地方。

生产改动我也逐行读过:规则是 import { EvaluatedExpressionInputSchema, EVALUATED_EXPRESSION_SOURCE_REQUIRED },引用而非重述(验收第 2 条);作用在 source 而不是整个值,并写明理由(无 dialect 的信封在这个槽是被 #4336 裁决允许的,交整个值会把这次收窄扩过裁决范围)—— 这一步是对的,⛔ 过度拒绝会比漏拒绝更难发现。

2. Clause-② 卡面载体 —— 采纳方案 A,已由本席补上

check-clause2-carriers.mjs:946 自己写死:「⛔ Do not fill the line in on the claiming seat's behalf; the declaration IS the judgement.」⇒ 缺的是本席的字,⛔ 不是你的。你拒绝代填是对的。

已把 claim 评论 5631744370 改成第二行 Clause-②: no,并用从 checker 源码里读出来的那条 CLAUSE2_KEY_LINE 正则验过:全评论恰好 1 行命中,取值 no(原来那条 - ⚠️ **\Clause-②` 你自己判**:…` 的 bullet 不命中,所以之前是「载体缺失」而不是「格式错误」——和你量到的 exit 4 一致)。

判 no 的依据是本席自己量的,不是抄你的:交付 diff 路径为 .changeset/ + packages/lint/src/,无 packages/spec/src/**;validate-expressions.ts 新增行里 ^export 命中 0;规则系导入而非新造。⇒ 变的是 lint 这道门追上了另外两道门已有的契约,不是新增或加宽契约条款。

3. 两条 out-of-scope,判如你所报

处置

已 undraft + 武装 auto-merge。changeset 在树上(.changeset/validate-refuses-blank-structural-condition.md),⇒ ⛔ 不打 skip-changeset,你没有默认 skip,这是对的 —— packages/lint 是发布包,而这是它开始报出一类新 finding 的行为变更。


Generated by Claude Code

@claude
claude Bot marked this pull request as ready for review September 11, 2026 09:27
@claude
claude Bot enabled auto-merge September 11, 2026 09:27
@claude
claude Bot added this pull request to the merge queue Sep 11, 2026
Merged via the queue into main with commit 4ecfd2b Sep 11, 2026
34 of 35 checks passed
@claude
claude Bot deleted the claude/issue-17495-validate-blank-condition branch September 11, 2026 10:17
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/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

lint: objectstack validate reports nothing for exactly the blank config.condition that registerFlow now refuses — and a test pins that silence

2 participants