diff --git a/openspec/changes/archive/2026-09-16-vale-ini-last-wins/.openspec.yaml b/openspec/changes/archive/2026-09-16-vale-ini-last-wins/.openspec.yaml new file mode 100644 index 00000000..f0807784 --- /dev/null +++ b/openspec/changes/archive/2026-09-16-vale-ini-last-wins/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-09-16 diff --git a/openspec/changes/archive/2026-09-16-vale-ini-last-wins/proposal.md b/openspec/changes/archive/2026-09-16-vale-ini-last-wins/proposal.md new file mode 100644 index 00000000..af87ba63 --- /dev/null +++ b/openspec/changes/archive/2026-09-16-vale-ini-last-wins/proposal.md @@ -0,0 +1,22 @@ +## Why + +The `cli-vale-rule-engine` spec records how Vale resolves a key assigned twice inside one matcher, "measured against Vale 3.17.1": the **first** assignment wins. Vale 3.21.0 changed that (upstream 1e4f6ed, "let the project's rule settings win"): the **last** assignment wins, the same direction that already held between two different matchers. The CLI pins Vale 3.21.0 as of `@taskless/cli` 0.11.2, its vendor contract test asserts last-wins in both orders, and `assemble.ts` says so. The spec is the one place still stating the old measurement. + +The requirement's rule does not change. A disable is declared after the enable it narrows, and assembly is deterministic, because precedence is positional. That rule was written to survive either merge direction, and under last-wins it holds in every shape rather than only across matchers. + +## What Changes + +- The measurement in the per-rule scoping requirement moves from Vale 3.17.1 to Vale 3.21.0, and the repeated-key bullet says **last** wins, matching the between-matchers bullet. +- A new scenario pins the repeated-key direction, so the spec names the shape the vendor contract test measures rather than only the between-matchers shape. + +Nothing here is **BREAKING**, and no code changes. The behaviour already ships in 0.11.2; this brings the spec to what the pinned binary does. + +## Capabilities + +### New Capabilities + +None. + +### Modified Capabilities + +- `cli-vale-rule-engine`: the per-rule scoping requirement restates its precedence measurement against Vale 3.21.0 and gains a scenario for a repeated key within one matcher. diff --git a/openspec/changes/archive/2026-09-16-vale-ini-last-wins/specs/cli-vale-rule-engine/spec.md b/openspec/changes/archive/2026-09-16-vale-ini-last-wins/specs/cli-vale-rule-engine/spec.md new file mode 100644 index 00000000..53c4fcc5 --- /dev/null +++ b/openspec/changes/archive/2026-09-16-vale-ini-last-wins/specs/cli-vale-rule-engine/spec.md @@ -0,0 +1,41 @@ +## MODIFIED Requirements + +### Requirement: Per-rule scoping is expressed in the rule's own Vale config + +The system SHALL express a Vale rule's scope through **matchers** — `[]` sections — declared in that rule's own `.taskless/rules/vale//.vale.ini`. Include is `. = YES`, exclude is `. = NO`. + +Precedence is **positional**, and the system SHALL order matchers accordingly rather than relying on a disable to win on its own. Measured against Vale 3.21.0: + +- Where two matchers both match a file, the **last** one wins for that rule. +- Where the same key is assigned twice inside one matcher — including across duplicate `[]` sections, which Vale merges — the **last** assignment wins. Through Vale 3.20.0 the first assignment won here; 3.21.0 made the two directions agree. + +A disable therefore SHALL be declared **after** the enable it narrows, within the rule's own config. Because precedence is positional and the run config is assembled, **assembly SHALL be deterministic**: rules ordered by id, and each rule's own matcher order preserved verbatim. A non-deterministic assembly would make a rule's effective scope depend on directory iteration order. + +A rule SHALL NOT be able to override another rule's matchers. It cannot know its own position in the assembled file, and cross-rule overriding through a shared file is the coupling the per-rule layout removes. + +#### Scenario: A rule scopes itself + +- **WHEN** a rule's own config enables it under `[marketing/**]` +- **THEN** the rule produces findings in `marketing/` files and none in `api/` files + +#### Scenario: A rule narrows itself + +- **WHEN** a rule's config enables it under `[marketing/**]` and then disables it under `[marketing/legacy/**]` +- **THEN** the rule fires in `marketing/` but not in `marketing/legacy/` + +#### Scenario: A repeated key keeps its last assignment + +- **WHEN** a rule's config assigns `. = YES` and then `. = NO` inside one matcher, or across two `[]` sections with the same glob +- **THEN** the rule does not fire on a matching file +- **AND** the reverse order fires + +#### Scenario: Assembly order is stable + +- **WHEN** the same set of rules is assembled twice +- **THEN** the resulting config SHALL be byte-identical +- **AND** each rule's matchers SHALL appear in the order that rule declared them + +#### Scenario: Duplicate matchers merge + +- **WHEN** two rules each declare a `[*.md]` matcher +- **THEN** both rules run on a matching `.md` file (Vale merges the matchers) diff --git a/openspec/changes/archive/2026-09-16-vale-ini-last-wins/tasks.md b/openspec/changes/archive/2026-09-16-vale-ini-last-wins/tasks.md new file mode 100644 index 00000000..4c33aa94 --- /dev/null +++ b/openspec/changes/archive/2026-09-16-vale-ini-last-wins/tasks.md @@ -0,0 +1,11 @@ +# Tasks + +**Delivery shape: single PR.** A spec correction with no code change; the +behaviour it describes already ships, pinned by +`packages/cli/test/vale-vendor-contract.test.ts` ("LAST assignment wins"). + +## 1. Spec + +- [x] 1.1 Restate the per-rule scoping requirement in full with the measurement moved to Vale 3.21.0 and the repeated-key bullet reading last-wins +- [x] 1.2 Add a scenario for a key repeated inside one matcher +- [x] 1.3 `pnpm openspec validate --strict`, then the archive dry-run from CLAUDE.md: every prior scenario still present after archive diff --git a/openspec/specs/cli-vale-rule-engine/spec.md b/openspec/specs/cli-vale-rule-engine/spec.md index 86824359..52d7313d 100644 --- a/openspec/specs/cli-vale-rule-engine/spec.md +++ b/openspec/specs/cli-vale-rule-engine/spec.md @@ -127,10 +127,10 @@ The assembled config SHALL be written where the run can read it and SHALL be git The system SHALL express a Vale rule's scope through **matchers** — `[]` sections — declared in that rule's own `.taskless/rules/vale//.vale.ini`. Include is `. = YES`, exclude is `. = NO`. -Precedence is **positional**, and the system SHALL order matchers accordingly rather than relying on a disable to win on its own. Measured against Vale 3.17.1: +Precedence is **positional**, and the system SHALL order matchers accordingly rather than relying on a disable to win on its own. Measured against Vale 3.21.0: - Where two matchers both match a file, the **last** one wins for that rule. -- Where the same key is assigned twice inside one matcher — including across duplicate `[]` sections, which Vale merges — the **first** assignment wins. +- Where the same key is assigned twice inside one matcher — including across duplicate `[]` sections, which Vale merges — the **last** assignment wins. Through Vale 3.20.0 the first assignment won here; 3.21.0 made the two directions agree. A disable therefore SHALL be declared **after** the enable it narrows, within the rule's own config. Because precedence is positional and the run config is assembled, **assembly SHALL be deterministic**: rules ordered by id, and each rule's own matcher order preserved verbatim. A non-deterministic assembly would make a rule's effective scope depend on directory iteration order. @@ -146,6 +146,12 @@ A rule SHALL NOT be able to override another rule's matchers. It cannot know its - **WHEN** a rule's config enables it under `[marketing/**]` and then disables it under `[marketing/legacy/**]` - **THEN** the rule fires in `marketing/` but not in `marketing/legacy/` +#### Scenario: A repeated key keeps its last assignment + +- **WHEN** a rule's config assigns `. = YES` and then `. = NO` inside one matcher, or across two `[]` sections with the same glob +- **THEN** the rule does not fire on a matching file +- **AND** the reverse order fires + #### Scenario: Assembly order is stable - **WHEN** the same set of rules is assembled twice