feat: one directory per rule, and path-addressed verify/test - #103
Draft
thecodedrift wants to merge 4 commits into
Draft
feat: one directory per rule, and path-addressed verify/test#103thecodedrift wants to merge 4 commits into
thecodedrift wants to merge 4 commits into
Conversation
A Vale rule is currently three locations, one of which every rule in the project shares. That shared .vale.ini is where all five harness runs found silent failures — an assignment above the first matcher, a glob that missed the fixture extension, three names that had to agree with nothing reporting when they didn't. A single write-contended config is the wrong shape at any scale. Each rule becomes a directory holding its style and its own .vale.ini; check assembles the run config from them and gitignores it. Measured against Vale 3.17.1: rules/<id>/<id>.yml resolves as check <id>.<id> under StylesPath = rules and resolves to nothing under StylesPath = ., Vale rejects unknown keys in a style so scope cannot ride along inside it, and a .yml sidecar in a style directory is loaded as a rule and fails E201 while a .vale.ini in the same place is ignored. Also replaces rule verify <id> with path-addressed verify and test. An id does not name one thing — the same id can exist under two engines, which is why the id form needed an ambiguity error at all. Adds an example/ project so a reader can see an install rather than infer it from tests that build their own fixtures. Stacked on #102 and merging down: a layout change without its migration ships a project whose rules silently stop running.
This was referenced Aug 14, 2026
Widens the change from Vale to every engine, and renames it to match. One directory per rule, identical shape everywhere: `.taskless/rules/<engine>/<id>/` holding the rule, any config that engine requires, and its tests in `.tests/`. A rule becomes one path rather than two, which is what makes `verify <path>` and `test <path>` work without an id lookup. The dot on `.tests/` is load-bearing and measured. ast-grep's ruleDirs recurses and parses every .yml beneath as a rule, so a plain `tests/` directory fails the scan with "missing field 'language'"; `__tests__/` fails the same way; a dot-directory is skipped, and `sg test` still reads it when testDir names it. Vale is unaffected either way — a `.tests/` inside a style directory is harmless even containing a .yml. That is a dependency on undocumented behavior, recorded as one in D2 with two mitigations: the failure is loud (a parse error naming the file, not a silently reinterpreted test), and a test pins it. The rejected alternative — materializing a rules-only tree for ast-grep — is written down as the fallback if the assumption ever breaks. Also drops the per-rule sg config: ast-grep expresses scoping inside the rule, so the slot would be an empty file every author creates, no author fills, and every reader learns to ignore. And renames runtime's capture rules to `captures/`, since "matcher" now means a Vale glob section in this same tree. 0005 layers on 0004; both are unreleased and both ship in this stack, so consumers run them as one upgrade and never see the intermediate layout.
…ting Started the layout refactor and hit something the proposal missed: `.taskless/rules/` is the new root and is also LEGACY_RULES_DIRECTORY, the pre-0004 flat location. Same string, different meaning. The legacy read paths turn out to be removable rather than renameable, because they are unreachable: ensureTasklessDirectory runs migrations before anything reads a rule, so 0004 has already moved .taskless/rules/*.yml to sg/rules/ and 0005 moves it again. Under the new layout a legacy lookup would resolve .taskless/rules/<id>.yml inside a tree whose real contents are rules/<engine>/<id>/ — reading the new root as though it were the old flat directory. A stale read path that resolves into the live tree is worse than no fallback. Also adds the migration precondition that follows: 0005 asserts the new root holds no top-level *.yml before writing engine directories into it, since a file still there means 0004 did not complete. Reverting the partial engines.ts refactor so this PR stays spec-only and green rather than carrying a tree with twelve broken callers.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stack (root → tip):
Why
A rule is spread across locations today, and for Vale one of them is shared by every rule in the project. Five sandboxed harness runs against
create-vale-rule(on #102) found silent failures in that shared.vale.iniand nowhere else — an assignment above the first matcher, a glob that missed the fixture's extension, three names that had to agree with nothing reporting when they didn't.The same reasoning generalizes past Vale:
sgandruntimerules are also split between arules/tree and a parallelrule-tests/tree, so no engine has a single path meaning "this rule". Fixing Vale alone would leave three layouts instead of one.What changes
One directory per rule, identical across engines:
checkassembles the Vale and ast-grep configs from the per-rule tree and gitignores them.rule verify <id>is replaced by path-addressedverify <path>(required components) andtest <path>(tests), withverifyrunning as a prerequisite layer oftest.Adds
example/— a small real project so a reader can see an install rather than infer it from tests that build their own fixtures, with achecktest so it can't drift.The dot on
.tests/is load-bearing, and measuredtests/inside the rule dirmissing field 'language'__tests__/.tests/.ymltestDir: …/.testsworks; snapshots land insideruleDirsrecurses and parses every.ymlbeneath as a rule, so a plaintests/directory hard-fails the scan.This is a dependency on undocumented behavior and D2 records it as one, with two mitigations: the failure is loud — a parse error naming the file, not a test silently reinterpreted as a rule — and a test pins it. The rejected alternative (materialize a rules-only tree for ast-grep, keeping a plain
tests/) is written down as the fallback if the assumption breaks.Other measured constraints
<id>/<id>.ymlresolves as check<id>.<id>only under aStylesPathnaming its parent; underStylesPath = .it resolves to nothing. This reverses the note in migration0004— correct for the flat layout, backwards for this one — so the task list rewrites that docstring rather than deleting it.E201), so scope cannot ride inside the style file. ast-grep can express scoping in the rule, which is why it gets no per-rule config — an empty file per rule is symmetry as decoration.Delivery shape
Stacked, merging down on #102.
0005layers on0004; both are unreleased and both ship in this stack, so consumers run them as one upgrade and never observe the intermediate layout.openspec validate --all --strictwill flag thecli-agent-authoringdelta until #102 archives — that capability is introduced there.Spec-only. Implementation follows on approval.
🤖 Generated with Claude Code
https://claude.ai/code/session_01Jwc9FFroR3mTZ4hLiSkkX3