Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-09-16
22 changes: 22 additions & 0 deletions openspec/changes/archive/2026-09-16-vale-ini-last-wins/proposal.md
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
@@ -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** — `[<glob>]` sections — declared in that rule's own `.taskless/rules/vale/<id>/.vale.ini`. Include is `<id>.<id> = YES`, exclude is `<id>.<id> = 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 `[<glob>]` 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 `<id>.<id> = YES` and then `<id>.<id> = NO` inside one matcher, or across two `[<glob>]` 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)
11 changes: 11 additions & 0 deletions openspec/changes/archive/2026-09-16-vale-ini-last-wins/tasks.md
Original file line number Diff line number Diff line change
@@ -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
10 changes: 8 additions & 2 deletions openspec/specs/cli-vale-rule-engine/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -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** — `[<glob>]` sections — declared in that rule's own `.taskless/rules/vale/<id>/.vale.ini`. Include is `<id>.<id> = YES`, exclude is `<id>.<id> = 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 `[<glob>]` sections, which Vale merges — the **first** assignment wins.
- Where the same key is assigned twice inside one matcher — including across duplicate `[<glob>]` 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.

Expand All @@ -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 `<id>.<id> = YES` and then `<id>.<id> = NO` inside one matcher, or across two `[<glob>]` 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
Expand Down