Skip to content

feat(vale): refuse the Vale run on a rejected .vale.ini (slice 2) - #366

Merged
theCodeDrift merged 5 commits into
feat/vale-config-schemafrom
feat/vale-config-refusal
Sep 21, 2026
Merged

theCodeDrift merged 5 commits into
feat/vale-config-schemafrom
feat/vale-config-refusal

Conversation

@theCodeDrift

@theCodeDrift theCodeDrift commented Sep 21, 2026 •

Copy link
Copy Markdown
Member

Stack (root → tip):

Slice 2 of vale-config-schema, stacked on #365 (slice 1). Tip of the stack; this PR archives the change.

What

  • Assembly validates and refuses. assembleValeConfig runs every rule's .vale.ini through validateValeRuleConfig. On any rejection it returns { status: "refused", refusals: [{ ruleId, rejections }] } and writes nothing; every refused rule is named at once, so an author with two broken configs is not sent around twice. On acceptance it writes the header, then per rule # tskl) rule = <id> and the source verbatim (a newline is appended only when the file lacks one), returning { status: "ok", path, sections, advisories } with sections read from the parsed structure. ruleConfigBody and sectionPatternsOf are deleted; the only string edit assembly ever made (dropping a copied-in StylesPath) is now a schema rejection.
  • Dispatch reports the refusal as the Vale engine's failure, so it reaches the exit code while ast-grep and runtime still run. The message names each rule and carries the rejection messages as written (they already carry <id>/.vale.ini line N:), and points at taskless verify. Advisories join the Vale outcome's notice alongside anything Vale itself wrote on a zero exit. DispatchOptions.valeConfigPath became vale: ValeAssembly | undefined; commands/check.ts passes assembled.vale through.
  • Recipe create-vale-rule (topic v10) states the config is schema-checked at verify and refused at check, lists what is rejected and what is advised, and its "scope out with a second matcher" guidance now says the schema rejects a NO that precedes every YES. No .taskless/** matcher existed in its examples. update (topic v8) gains the 0.11.3 ledger paragraph with the four shapes most likely to trip an existing rule and the verify command that names the line.
  • Changeset extended with the refusal and the "why still patch" reasoning from the proposal.
  • Archive of vale-config-schema on this tip.

Why

A rule whose config the schema rejects is one Vale reads as something other than what its author wrote, with a zero exit and an empty report. Slice 1 told the author at verify; this slice stops check from running such a config to a clean pass. Refusing rather than omitting the rule is deliberate: a rule left out with a soft notice would verify, run, and report nothing, which is the silent-disable failure the engine exists to prevent. Refusing rather than stripping is the same decision from the other side.

How verified

  • pnpm build, pnpm typecheck, pnpm lint (including pnpm cli check over the recipes), pnpm --filter @taskless/cli test: 96 files, 1581 tests, all green.
  • assemble.test.ts: existing configs gained breadcrumbs (the schema now requires them); the StylesPath test asserts a refusal under vale-config-no-root-keys; new tests assert a foreign key leaves .taskless/.vale.ini unwritten, every refused rule is named, an accepted config is written byte-for-byte under its breadcrumb (trailing spaces and blank lines intact), and advisories ride on the result.
  • mixed-engine-check.test.ts (built CLI, real binaries): the W101 test now asserts the Vale failure names no-simply/.vale.ini line 1:, exit 1, ast-grep results present, no Vale results, no notices; a second test covers the spec's "foreign assignment refuses the run" scenario (no-simply assigning no-obviously.no-obviously = NO, line 5, neighbour not named); a third asserts a .taskless/** advisory reaches notices on a run that still happens.
  • The standing "Vale diagnostics on a successful run are surfaced as notices" requirement stays pinned by vale-run.test.ts, which hands runVale a hand-written W101 config directly, deliberately around the schema; its comment now says so.
  • Archive dry-run per CLAUDE.md: cli-vale-rule-engine 23 → 32 scenarios, cli-rule-validation 26 → 30, zero lost by name (comm over sorted #### Scenario lines), reset to the wip SHA, then archived for real. pnpm openspec validate --all: 29 passed.

The enforcedBy decision

Left as "verify" for all eight vale-config-* constraints, with the reasoning recorded in the comment block above them in constraints.ts. The field answers a consumer's ordering question (can this be decided from the files alone, before anything executes?), and for every entry here it can. verify is where the author is told which line and which constraint; check runs the same schema but only refuses and points back at verify. Widening the type to include "check" would have touched the published reference.json shape for a distinction no consumer needs to branch on. Happy to widen it if the reference format wants it.

Deviations from tasks.md

  • The byte-identical assembly expectation did not change: "is byte-identical across runs" still compares two assemblies of the same tree. What did change is that assembly no longer .trim()s a rule's source; the verbatim test pins that trailing whitespace survives. The sections field is still deduplicated and sorted as before.
  • The refusal carries every refused rule (refusals[]) rather than the single ruleId the task sketched, so one run reports every broken config.

Refs #359

@theCodeDrift

Copy link
Copy Markdown
Member Author

@claude /review

@claude

claude Bot commented Sep 21, 2026 •

Copy link
Copy Markdown

Claude finished @theCodeDrift's task in 5m 52s —— View job


Review complete

Review mode: incremental — read 0 prior review thread(s) before reviewing. .prior-review.json contains no reviewThreads, reviews, or top-level comments beyond the @claude /review trigger itself, so this is the first review of this PR — nothing to classify as already-addressed.

  • Read .prior-review.json
  • Gather PR diff via gh pr diff
  • Review assembly/dispatch/constraints changes
  • Review recipe docs and spec deltas
  • Review tests
  • Verify the archive didn't silently drop any spec scenario (per CLAUDE.md's openspec archive warning)
  • Post findings

What I checked

  • assemble.ts: refusal-vs-write logic, verbatim writing (valeRuleBlock), the AssembledValeConfig/RefusedValeConfig discriminated union and all its call sites.
  • dispatch.ts: the new refused short-circuit in runValeEngine, how advisories and Vale's own notice/failure combine, describeValeRefusal, runEngines' merge into failures/notices/exitCode.
  • check.ts: the valeConfigPath → vale rename is the only change; no other call site of assembleEngineConfigs/assembleValeConfig was missed.
  • constraints.ts: comment-only change, no logic touched.
  • Recipes (create-vale-rule.md v10, update.md v8): rejected/advised lists cross-checked against the actual schema in schemas/vale-config.ts (8 rejections across 7 bullets, 3 advisories) — consistent.
  • OpenSpec archive: diffed openspec/specs/cli-vale-rule-engine/spec.md and cli-rule-validation/spec.md against origin/feat/vale-config-schema scenario-by-scenario (by title, not just count). Counts match the PR's own claim (23→32, 26→30) and every prior scenario title is still present — no silent drop from the archive.
  • Tests: spot-checked the new assemble.test.ts and mixed-engine-check.test.ts cases against the schema's actual line-numbering (locateLines) — the asserted line numbers match the fixture content.

Findings

1 new finding, posted inline on packages/cli/src/rules/dispatch.ts (lines 206–222):

[New] Config advisories are dropped when Vale times out or crashes in the same run. runValeEngine threads options.vale.advisories into the notice field on the ok branch and the non-blocking (unavailable) branch, but the blocking branch (outcome.blocking === true, i.e. Vale timed out or crashed) returns only { failure: outcome.message } with no reference to advisories at all. A schema-valid config carrying an advisory (e.g. an unnecessary [*] or .taskless/** matcher) that happens to share a check run with a Vale timeout/crash loses that advisory entirely — it reaches neither notice nor failure, and runEngines/check.ts don't recover it from anywhere else. This contradicts the intent stated in the comment above (advisories "travels with whatever Vale itself has to say, both advisory"). No test covers this combination. Independently verified.

Everything else held up: the refusal/verbatim-write split in assemble.ts, the DispatchOptions.vale rename and its consumers, the recipe/schema consistency, and the OpenSpec archive integrity.

@theCodeDrift
theCodeDrift added this pull request to stack #367 September 21, 2026 18:49
Comment thread packages/cli/src/rules/dispatch.ts Outdated
@theCodeDrift

Copy link
Copy Markdown
Member Author

Re: @claude[bot] — "Claude finished @theCodeDrift's task in 5m 52s — Review complete…"
#366 (comment)

Acknowledged. The one finding (advisories dropped on the blocking Vale branch) is fixed in c2421fd and its thread resolved; the rest of the review found nothing to change.

— AI Coding Agent

assembleValeConfig validates every rule's config against the config schema
and, on any rejection, returns a refusal naming each rule and line without
writing .taskless/.vale.ini. ruleConfigBody and sectionPatternsOf are gone:
an accepted config is written verbatim under its breadcrumb, and sections
come from the parsed structure.

dispatch reports a refusal as the Vale engine's failure, so it reaches the
exit code while ast-grep still runs; the schema's advisories join the
notices. The mixed-engine e2e test that expected check to surface W101 at
run time now asserts the refusal, and the run-time notice stays pinned in
vale-run.test.ts through a hand-written config that bypasses the schema.

enforcedBy stays "verify" for the vale-config-* constraints: every one is
decidable from the files alone, which is the ordering question the field
answers; check refuses and points back at verify.
create-vale-rule (topic v10) says the config is schema-checked at verify
and refused at check, lists what is rejected and what is advised, and
notes that a NO matcher preceding every YES is rejected. update (topic
v8) gains a 0.11.3 ledger entry: a config check used to tolerate now
refuses the Vale run, with the four shapes most likely to trip an
existing rule and the verify command that names the line. The changeset
grows by the refusal and the reason the bump stays patch.
runValeEngine threaded the schema's advisories into the notice on the ok
and unavailable branches but returned only the failure when Vale timed
out or crashed, so an advisory was heard only on runs where Vale did not
break. The blocking branch now carries them on notice beside the failure;
they are not folded into the failure because an advisory is not what
failed the run. Pinned for all three outcome shapes.
Dry-run first per CLAUDE.md, against the parent's revised delta (final
per-matcher verdicts): cli-vale-rule-engine 23 -> 33 scenarios,
cli-rule-validation 26 -> 31, no prior scenario lost by name. Then
archived for real on this tip.
The parent slice now judges enabled-somewhere and NO-before-YES by each
matcher's last assignment, so a YES a later NO in the same matcher
overrides no longer counts. The recipe's advisory bullet and the
changeset say so, so neither reads as promising that such a config is
merely advised.
@theCodeDrift
theCodeDrift force-pushed the feat/vale-config-refusal branch from c2421fd to abbe40f Compare September 21, 2026 19:06
@theCodeDrift
theCodeDrift merged commit d04b14f into main Sep 21, 2026
4 checks passed
@theCodeDrift
theCodeDrift deleted the feat/vale-config-refusal branch September 21, 2026 21:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant