From f26a7b06b8e77307a2995e1d5719af38d985ac3f Mon Sep 17 00:00:00 2001 From: Jakob Heuser Date: Mon, 24 Aug 2026 14:32:23 -0700 Subject: [PATCH] chore(openspec): archive the two changes the stack landed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `main` went red on the unarchived-changes check in validate.yml. A change is archived exactly once, on whichever PR is the tip of the stack, and this stack merged forward — #161 was the tip and carried no OpenSpec change of its own, so nothing archived #155's and #156's. That is the documented consequence of a forward-merging stack, not a surprise: CLAUDE.md says such a stack leaves `main` red until its final slice archives. The slice was missing. Archiving applied both deltas to the permanent specs, which is the point of the step rather than a side effect: `cli-rule-routing` gains the requirement that the routing recipe states each engine's reach derived from the pinned versions rather than transcribed, and `cli-rule-validation` gains the ast-grep fixture-bucket scenario that its Vale counterpart already had. `openspec validate --specs --strict` passes, 24 items. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Cyga14bww8rmazH2XrF8ms --- .../proposal.md | 0 .../specs/cli-rule-routing/spec.md | 0 .../tasks.md | 0 .../proposal.md | 0 .../specs/cli-rule-validation/spec.md | 0 .../2026-08-24-sg-fixture-coverage}/tasks.md | 0 openspec/specs/cli-rule-routing/spec.md | 65 +++++++++++++++++++ openspec/specs/cli-rule-validation/spec.md | 24 +++++-- 8 files changed, 83 insertions(+), 6 deletions(-) rename openspec/changes/{engine-reach-in-route => archive/2026-08-24-engine-reach-in-route}/proposal.md (100%) rename openspec/changes/{engine-reach-in-route => archive/2026-08-24-engine-reach-in-route}/specs/cli-rule-routing/spec.md (100%) rename openspec/changes/{engine-reach-in-route => archive/2026-08-24-engine-reach-in-route}/tasks.md (100%) rename openspec/changes/{sg-fixture-coverage => archive/2026-08-24-sg-fixture-coverage}/proposal.md (100%) rename openspec/changes/{sg-fixture-coverage => archive/2026-08-24-sg-fixture-coverage}/specs/cli-rule-validation/spec.md (100%) rename openspec/changes/{sg-fixture-coverage => archive/2026-08-24-sg-fixture-coverage}/tasks.md (100%) diff --git a/openspec/changes/engine-reach-in-route/proposal.md b/openspec/changes/archive/2026-08-24-engine-reach-in-route/proposal.md similarity index 100% rename from openspec/changes/engine-reach-in-route/proposal.md rename to openspec/changes/archive/2026-08-24-engine-reach-in-route/proposal.md diff --git a/openspec/changes/engine-reach-in-route/specs/cli-rule-routing/spec.md b/openspec/changes/archive/2026-08-24-engine-reach-in-route/specs/cli-rule-routing/spec.md similarity index 100% rename from openspec/changes/engine-reach-in-route/specs/cli-rule-routing/spec.md rename to openspec/changes/archive/2026-08-24-engine-reach-in-route/specs/cli-rule-routing/spec.md diff --git a/openspec/changes/engine-reach-in-route/tasks.md b/openspec/changes/archive/2026-08-24-engine-reach-in-route/tasks.md similarity index 100% rename from openspec/changes/engine-reach-in-route/tasks.md rename to openspec/changes/archive/2026-08-24-engine-reach-in-route/tasks.md diff --git a/openspec/changes/sg-fixture-coverage/proposal.md b/openspec/changes/archive/2026-08-24-sg-fixture-coverage/proposal.md similarity index 100% rename from openspec/changes/sg-fixture-coverage/proposal.md rename to openspec/changes/archive/2026-08-24-sg-fixture-coverage/proposal.md diff --git a/openspec/changes/sg-fixture-coverage/specs/cli-rule-validation/spec.md b/openspec/changes/archive/2026-08-24-sg-fixture-coverage/specs/cli-rule-validation/spec.md similarity index 100% rename from openspec/changes/sg-fixture-coverage/specs/cli-rule-validation/spec.md rename to openspec/changes/archive/2026-08-24-sg-fixture-coverage/specs/cli-rule-validation/spec.md diff --git a/openspec/changes/sg-fixture-coverage/tasks.md b/openspec/changes/archive/2026-08-24-sg-fixture-coverage/tasks.md similarity index 100% rename from openspec/changes/sg-fixture-coverage/tasks.md rename to openspec/changes/archive/2026-08-24-sg-fixture-coverage/tasks.md diff --git a/openspec/specs/cli-rule-routing/spec.md b/openspec/specs/cli-rule-routing/spec.md index bb23c2c7..c2b47b4e 100644 --- a/openspec/specs/cli-rule-routing/spec.md +++ b/openspec/specs/cli-rule-routing/spec.md @@ -3,7 +3,9 @@ ## Purpose TBD - created by archiving change local-rule-routing. Update Purpose after archive. + ## Requirements + ### Requirement: Route is the local authoring classifier The CLI SHALL provide a `route` help recipe that instructs the agent to classify a rule-authoring request into one of five destinations — `create-legacy-rule`, `create-sg-rule`, `create-vale-rule`, `create-runtime-rule`, or `create-remote-rule` — using `taskless detect --json` signals plus the user's intent. The `route` recipe SHALL read the user's login state before dispatching, since it determines which destinations are reachable. It SHALL remain biased to stay local: local authoring that works SHALL NOT be abandoned for the service. @@ -247,3 +249,66 @@ One statement is the point. A criterion copied into each destination is five cop - **WHEN** the embedded recipe set is inspected - **THEN** there SHALL be no topic whose only purpose is selecting among engines +### Requirement: Route states each local engine's reach from the pinned engine versions + +The `route` recipe SHALL state what each local engine can actually read, so +that an agent choosing between `create-sg-rule`, `create-vale-rule`, and the +runtime destinations is not left to guess whether an engine parses the language +in front of it. + +The recipe SHALL state, for the ast-grep engine, the set of languages the +pinned ast-grep release parses, spelled as a rule's `language:` field must +spell them. It SHALL state, for the Vale engine, that Vale treats a file +according to its extension in tiers — markup, comment text only, and a +plaintext fallback that lints an unparsed file as whole-file prose — and that a +set of formats fails rather than lints because Vale delegates their parse to an +external converter this CLI does not ship. The recipe SHALL state that such a +failure aborts the entire Vale pass rather than skipping the offending file. + +These statements SHALL be **derived from the pinned engine versions rather +than transcribed into the recipe text**. The recipe SHALL carry substitution +markers resolved at render time from constants that name the engine version +they were taken from, and those constants SHALL be pinned to the engines' +observable behaviour by tests that invoke the engine binaries. A transcribed +list would go stale on the next engine bump with nothing to detect it, and a +stale claim about engine reach is more harmful than no claim, because an agent +acts on it. + +The recipe SHALL NOT treat a language absent from both engines' reach as +automatically requiring the runtime tier. It SHALL direct the agent to consider +`create-legacy-rule` first, since a linter the repository already runs may +cover the language, and that destination requires no login. + +The recipe SHALL distinguish an engine's reach from an engine's availability. +Reach is a property of the pinned engine version; availability is a property of +the host on which the CLI is running, and a language within reach is still +unusable where the engine's platform binary did not resolve. + +#### Scenario: Route names ast-grep's languages + +- **WHEN** the rendered `route` recipe is read +- **THEN** it SHALL name the languages the pinned ast-grep release parses, including `Yaml` +- **AND** an agent SHALL be able to conclude from it that a rule over a GitHub Actions workflow is expressible as an `sg` rule + +#### Scenario: Route names Vale's tiers and its unreadable formats + +- **WHEN** the rendered `route` recipe is read +- **THEN** it SHALL name the extensions Vale parses as markup, the extensions where Vale lints comment text only, and the plaintext fallback that applies to everything else +- **AND** it SHALL name the formats whose parse Vale delegates to an absent external converter +- **AND** it SHALL state that one such file fails the whole Vale pass rather than only itself + +#### Scenario: The reach statements carry no unresolved marker + +- **WHEN** the `route` recipe is rendered +- **THEN** no `%(…)s` substitution marker SHALL remain in the text an agent receives + +#### Scenario: A bumped engine cannot leave the recipe stale + +- **WHEN** an engine binary is upgraded to a version whose reach differs from the constants the recipe renders +- **THEN** a vendor-contract test that invokes the engine SHALL fail +- **AND** the recipe SHALL NOT be able to state the superseded reach without that failure + +#### Scenario: An unreachable language is not routed to runtime by default + +- **WHEN** the rule's language appears in neither engine's reach +- **THEN** `route` SHALL direct the agent to consider `create-legacy-rule` before escalating to a runtime destination diff --git a/openspec/specs/cli-rule-validation/spec.md b/openspec/specs/cli-rule-validation/spec.md index 187276d6..dc3c62cb 100644 --- a/openspec/specs/cli-rule-validation/spec.md +++ b/openspec/specs/cli-rule-validation/spec.md @@ -1,8 +1,11 @@ # cli-rule-validation Specification ## Purpose + TBD - created by archiving change self-contained-rules. Update Purpose after archive. + ## Requirements + ### Requirement: Rules are validated and tested by path, not by id The CLI SHALL provide `verify ` and `test `. Both SHALL accept a path to a rule's canonical location or to any directory above it, and SHALL resolve the owning engine from the path's position under `.taskless/rules//` rather than by parsing the file. @@ -39,11 +42,11 @@ The two commands split because they have different preconditions. An agent part- Per engine, `verify` SHALL check: -| Engine | Components | -|-----------|--------------------------------------------------------------------------------| -| `sg` | `.yml` against the ast-grep schema and the Taskless required fields | -| `vale` | `.yml` against Vale's own validation, and the rule's `.vale.ini` | -| `runtime` | `check.ts` present, and at least one capture rule under `captures/` | +| Engine | Components | +| --------- | ----------------------------------------------------------------------- | +| `sg` | `.yml` against the ast-grep schema and the Taskless required fields | +| `vale` | `.yml` against Vale's own validation, and the rule's `.vale.ini` | +| `runtime` | `check.ts` present, and at least one capture rule under `captures/` | #### Scenario: A rule with no fixtures still verifies @@ -62,6 +65,8 @@ Per engine, `verify` SHALL check: Ordering is the point. When a rule is both malformed and under-fixtured, the fixture complaint is the less useful of the two errors and is the one that surfaces first if the checks run in the other order — so the author is told their fixtures are incomplete while the reason the rule could never have run goes unmentioned. +A rule that populates only one bucket has proved only half of what a rule claims, whatever its engine. An engine SHALL NOT be trusted to report this itself: `ast-grep test` reports an empty `invalid:` bucket as `1 passed; 0 failed` and exits zero, so a rule that has never matched anything is indistinguishable from one that passed. + #### Scenario: A malformed rule reports the malformation, not the fixtures - **WHEN** `test` runs against a rule that is both invalid and missing a fixture bucket @@ -75,6 +80,14 @@ Ordering is the point. When a rule is both malformed and under-fixtured, the fix - **AND** every `pass/` document SHALL produce none - **AND** a rule populating only one bucket SHALL be reported as unverified rather than passing +#### Scenario: ast-grep fixtures are counted per bucket + +- **WHEN** `test` runs against an ast-grep rule +- **THEN** the `valid:` and `invalid:` entries SHALL be counted across every `-test.yml` file the rule owns +- **AND** a rule populating only one bucket SHALL be reported as unverified rather than passing +- **AND** a rule whose buckets are all empty or absent SHALL be reported as unverified rather than passing +- **AND** a green `ast-grep test` run SHALL NOT on its own be sufficient to report the rule as passing + ### Requirement: The generation loop runs verify and test The rule generation loop SHALL run `verify` and then `test` against a newly authored or newly delivered rule, and SHALL treat a failure of either as a rule that is not ready to report as complete. @@ -84,4 +97,3 @@ The rule generation loop SHALL run `verify` and then `test` against a newly auth - **WHEN** a rule is authored locally or written by the service - **THEN** the loop SHALL run `verify` and `test` against its path - **AND** SHALL surface a failure rather than reporting the rule as written -