Repository navigation
fix(lint): objectstack validate refuses a blank structural condition, the rule registerFlow already applies (#17495) - #17665
Conversation
…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
📓 Docs Drift CheckThis PR changes 1 package(s): ⛔ 1 release-owned page(s) name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 4 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # 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
|
|
PM 裁决,两个 open question 都判了,并且两条都是本席的错,不是你的。 1. 四文件 —— 采纳方案 A。⭐ 那道两文件围栏是我画错的你量出来的东西我自己复验了一遍(对着
⇒ 卡面(以及照抄卡面的我的派单)把那条沉默记到了一个不同规则族的钉子上。 ⭐ 而你做的比卡面要求的更多:卡面只要求把那一个钉子就地重判,你把两个都重判了 —— 生产改动我也逐行读过:规则是 2.
|
Closes #17495
Clause-②: no
validateStackExpressions— the pass behindobjectstack validate— reportednothing for a structural
conditionthat is blank after trimming, at eithernode slot (a
startnode's trigger gate, adecisionnode's predicate) and onan edge. #17322 / PR #17491 rebound
AutomationEngine.registerFlowto the edgedoor's non-blank rule, so the same value now stops the flow registering at boot
behind a single
warnline. An author ran validate, got a clean bill, deployed,and the trigger was never armed.
checkStructuralConditiongains 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, whichFlowEdgeSchema.conditionhas composed since #15807. Not a second hand-writtennotion 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_REQUIREDsentence verbatim and the three doorscannot answer differently.
Readings
Re-measured at claim time on
d46deba195, ⛔ not taken from the dispatch:(#17491)ingit log --oneline -400 origin/main(#17637): 1)evaluatedSourceRefusalinengine.tsat the base commitmainEvaluatedExpressionInputSchema.safeParse(' ')success: falseOne probe, both structural node slots at once, before → after:
condition: ' 'at the start gate and the decision predicatecondition: ''at bothast-only envelope at bothcondition: ' 'on an edgeThe 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
0proves 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.
validate-expressions.test.ts— "a whitespace-only STRING is untouched —ruled correct, not a defect". That
toHaveLength(0)was deliberate: service-automation:evaluateConditionanswers a silentfalsefor a non-string predicate, and a non-stringconfig.conditionregisters clean #15662ruled the blank string correct on the ground that author time and run time
agreed about it. spec/automation:
FlowEdgeSchema.conditionstill accepts an envelope the engine cannot evaluate (ast-only, whitespace-onlysource) — the evaluated-slot rule of #15430 has not reached the edge condition #15807 removed that ground at the edge door and A whitespace-onlyconfig.conditionstring is a silentfalseat the node door, while #15807 made the edge door refuse the same value at parse #17322removed 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:
evaluateConditionanswers a silentfalsefor a non-string predicate, and a non-stringconfig.conditionregisters clean #15662'sshape refusal stayed distinct — exactly what the registration-side pin
does.
lint-flow-patterns.test.ts(the line the card names) — theflow-inert-node-conditionzero still stands and is kept: that rule isabout 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).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.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-debteach exit 3 — PREREQUISITE NOT MET, they read a whole-repodist/; that build is CI's). 0 red. Reconciled with--rancarrying exit codes.git checkout HEAD -- THE_PATHrestore under a trap): the gate mutated out on disk — anchor1 → 0, injected marker1— and the two test files went 8 failed / 438 passed; restored byte-identically (git hash-objectequals the HEAD blob,git diff HEADempty) and back to 446 passed.Clause-② and the changeset, both measured
Clause-②: no, measured on the delivered diff rather than asserted:SUSPECT_TIER_GLOBSispackages/spec/src/**; this diff touchespackages/lint/srcand.changesetonly.' ','','\t','\n',{dialect:'cel',source:' '},{dialect:'cel',source:''}. The other 17 are byte-identical, including every accepting shape (bare CEL padded and multiline, all three envelope spellings, absent,null). The narrowing is the ruled shape and nothing else, and the contract that rules it was decided in spec/automation:FlowEdgeSchema.conditionstill accepts an envelope the engine cannot evaluate (ast-only, whitespace-onlysource) — the evaluated-slot rule of #15430 has not reached the edge condition #15807 and applied in A whitespace-onlyconfig.conditionstring is a silentfalseat the node door, while #15807 made the edge door refuse the same value at parse #17322 — this PR is the third door catching up, not a new decision.Changeset owed, and written (⛔ not skipped):
@objectstack/lintpublishesfiles: ["dist", …], and the new refusal is measured in the shipped bundles —evaluatedSourceRefusalappears 2× indist/index.jsand 2× indist/index.cjs(positive control:validateStackExpressions4× indist/index.js). It is aminorwith a BREAKING banner and an ADR-0087 disposition, mirroring PR #17491's;check-adr-0087-registration,check-changeset-no-majorandcheck-empty-changesetall pass on it.验收备注
validate-expressions.tsandlint-flow-patterns.test.ts. The pin that actually recorded this pass's silence is invalidate-expressions.test.ts("a whitespace-only STRING is untouched") — the card citedlint-flow-patterns.test.ts:1279, which pinslintFlowPatterns, a different rule family whose zero this change does not move. Landing without editingvalidate-expressions.test.tswas impossible: that assertion becomes false. Both pins are re-judged; the new behavioural tests live beside the pass they test. The changeset is the fourth file.#5017meta-guard needed one entry: the new localblankRefusalis excused as plumbing besideshapeRefusal, with the same argument (its keys are the helper's own{ message, source }, never metadata keys).packages/specsiblings (spec/automation: three residues of #17322's node-door refusal — a stale on-site ruling, an ADR-0087 entry that names only the edge slot, and two sibling predicate slots still admitting a blank string #17493, which also owns widening that ADR-0087 entry'ssurfacetoconfig.condition),packages/services/service-automation(the reference, not the target), and the three neighbours the card judged not duplicates (lint:lintFlowPatternsthrows on a non-record member of a flow'sedgeslist — the sibling list #16751 did not cover #16910 / lint: flow template path rules see neither a variable-rooted token nor a non-record-triggered flow — 6 of 7 real lookup-traversal sites in a downstream app are invisible #17305 / os validate and os lint judge an EMPTY stack when a project declares its metadata only in packages[] — the ADR-0130 D4 union fold (authoringRuleUnionStack) is wired into os build alone #17069).evaluateConditionanswers a silentfalsefor a non-string predicate, and a non-stringconfig.conditionregisters clean #15662's ruling is deliberately untouched: a condition already stored blank still answersfalse. What moved is that it can no longer be authored past validate.🤖 Generated with Claude Code
https://claude.ai/code/session_012GKcPZbMoGq7WPzKLfRBTU
Generated by Claude Code