Skip to content

docs(openspec): partition rules by engine directory - #73

Draft
thecodedrift wants to merge 6 commits into
mainfrom
openspec/partition-rules-by-engine
Draft

docs(openspec): partition rules by engine directory#73
thecodedrift wants to merge 6 commits into
mainfrom
openspec/partition-rules-by-engine

Conversation

@thecodedrift

Copy link
Copy Markdown
Member

Draft — not ready for review. Spec only; no implementation yet. Opened to keep the work on the remote.

Makes the on-disk rule layout multi-engine before any second engine exists, and changes nothing a user can observe: check reports exactly what it did before, from a different path.

Rules partition into .taskless/<engine>/{config, rules/, rule-tests/}sg/, vale/ (scaffolded empty and inert), runtime/. The containing directory determines the engine, so dispatch never parses a file, and each engine's committed native config becomes the source of truth. Ephemeral sgconfig.yml generation leaves the check path entirely; because that is what used to trigger migrations, check now calls ensureTasklessDirectory directly.

Migration 0004 does the moving, content-preserving throughout: rules/sg/, runtime-rules/runtime/rules/. sgconfig.yml's ruleDirs: [rules] is relative to the config, so it survives unedited. Runtime capture bytes are untouched, which matters because those bytes determine reconciliation hashes — editing during the move would invalidate every signature. runMigrations also starts throwing on a scaffold newer than the CLI understands, rather than returning silently, unless --allow-version-mismatches is passed.

Two decisions keep this from needing anyone else's cooperation. Ingest defaults to sg, permanently — the delivery API carries no engine discriminator (verified: no engine, analysisType, or ruleType field anywhere in the generated types), so an engine-less payload is ast-grep by definition; an unrecognized engine fails loudly rather than defaulting into a directory the wrong parser reads. And both layouts stay readable — legacy .taskless/rules/ dispatches as ast-grep alongside sg/rules/, de-duplicated — so an unmigrated checkout still runs and no producer has to cut over.

Why this is separate

Split out of #71, which had grown to 44 tasks across ten groups. Since an OpenSpec change must land complete in one PR, a change that large cannot land at all. This half is where the risk lives — the relayout and the migration — so isolating it means a difference in check output after merge has exactly one candidate explanation.

stack: openspec-archived will be red until the change is implemented and archived. That is expected: archiving now would delete the spec before anything is built against it.

Where this sits

Each change is independently landable: one OpenSpec change, one PR, archived on merge. The archive gate requires it — a tip PR must leave openspec/changes/ empty — so nothing here is stacked and none of these depend on a shared branch.

PR Change Prerequisites
#70 knowledge prompt export none
#72 Vale binary packages none
#73 (this PR) partition rules by engine none
#71 Vale engine + engine-selection topic #73 (executes the vale/ directory it scaffolds) and #72 (needs a binary to resolve)

Only #71 has prerequisites. The other three can land in any order.

#70 and #71 are coupled by exactly one line: whichever lands second adds the engine-selection topic to TOPICS. Ordering between them doesn't matter.

Downstream, the generator's decision router (TSKL-279) needs a published release containing #70 and #71. It consumes a normal release — no prerelease, no path dependency — so it waits without blocking anything here.

Fixes OSS-24

thecodedrift and others added 6 commits July 29, 2026 15:50
Split out of add-vale-rule-engine, which spanned 44 tasks across ten
groups — more than one PR should carry, and the archive gate requires
a change to land complete in a single PR.

This half is the part that carries the risk: the relayout, migration
0004, directory dispatch, ingest defaulting, legacy-path tolerance,
and running ast-grep over the committed config. It adds no engine and
changes nothing a user observes, so a difference in check output after
it lands has exactly one candidate explanation.

Vale itself and the engine-selection topic stay in add-vale-rule-engine,
which needs this layout to exist first.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CDv57zHq7abms3RReSQw6q
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CDv57zHq7abms3RReSQw6q
Move `.taskless/rules/`, `rule-tests/`, and `sgconfig.yml` under `sg/`, and
the runtime tier to `runtime/rules/` and `runtime/rule-tests/`. Every move is
content-preserving: runtime capture bytes determine their server-side
reconciliation hashes, and `sgconfig.yml`'s `ruleDirs: [rules]` is relative to
the config file, so nothing needs rewriting.

Scaffold an inert `vale/` engine directory with its native `.vale.ini`, and
`.gitkeep` every otherwise-empty scaffolded directory.

`runMigrations` now throws when `taskless.json`'s version exceeds the highest
migration this CLI knows, instructing the user to upgrade, unless the global
`--allow-version-mismatches` flag is passed — previously it returned silently
and let an older CLI half-read a newer scaffold.

This is task group 1 of `partition-rules-by-engine`. The readers (`check`,
`verify`, runtime discovery) are updated in groups 2-4; until then 20 tests in
check/verify/runtime-check fail because the rules moved out from under them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Group 1 alone leaves 20 tests failing, so no unit of this change is
independently shippable: the stack merges down and reaches main as one commit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
An unanchored gitignore pattern matches at any depth, so the `sgconfig.yml`
line migration 0001 wrote also matches `.taskless/sg/sgconfig.yml` — the
committed config this layout makes the source of truth would never be tracked.

0001 now writes `/sgconfig.yml`, and 0004 rewrites the unanchored line in
existing checkouts, collapsing it with the anchored form when both are present.
Belongs to task group 1; kept as its own commit since group 1 was already
reviewed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Add `rules/engines.ts` as the single place that answers "which engine owns
this rule": the top-level `.taskless/<engine>/` directory does, and no rule
file is ever parsed to decide. `sg` maps to ast-grep and `runtime` to the
harness; `vale` is recognized but has no executor yet, and a directory that is
not a known engine is ignored rather than handed to someone else's parser.

`check` now calls `ensureTasklessDirectory` itself, keeping the migration
trigger that `generateSgConfig` used to provide, then scans each ast-grep
source it finds. The legacy `.taskless/rules/` stays readable alongside
`sg/rules/`, so a producer that keeps naming the old path still runs; findings
from the two are merged and identical matches collapsed.

Service-delivered rules are filed by the engine their payload identifies —
`sg` when it identifies none, permanently, since the API carries no engine
discriminator. An engine this CLI does not know throws before touching the
filesystem instead of silently defaulting to ast-grep.

The remaining hardcoded `.taskless/rules` literals (verify, files, check,
rules, the detect probe) now resolve through the same module, each tolerating
both layouts. Reconcile needed no change: reported paths derive from the
discovery root and the server joins on content signature, both verified by
test.

Test failures drop from 20 to 9, all in runtime-check.test.ts (group 3).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@thecodedrift
thecodedrift force-pushed the openspec/partition-rules-by-engine branch from e08424d to 9cf8674 Compare July 30, 2026 17:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

skip-changeset PR intentionally ships no release note (bypasses the changeset requirement)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant