Skip to content

refactor: honest single-walk + dedupe presets + version pragma#21

Merged
machado144 merged 1 commit into
mainfrom
refactor/single-walk-and-followups
Jul 10, 2026
Merged

refactor: honest single-walk + dedupe presets + version pragma#21
machado144 merged 1 commit into
mainfrom
refactor/single-walk-and-followups

Conversation

@machado144

Copy link
Copy Markdown
Contributor

Summary

Follow-up PR that honestly delivers what the roadmap PRs oversold. Everything gated by broadened parity goldens — 21 subtests across 6 fixtures, all byte-identical.

What actually changed

1. True single-walk rule engine (the fix for spec 005)

The prior "rule engine" (#17) shipped an architectural wrapper but kept the 7 per-rule `filepath.Walk` calls — every rule wrapper called the old `Validate*` method which walked the tree itself. Net cost: 8 walks per run instead of the previous 7. The commit title lied.

This does the actual conversion:

  • Each rule iterates `ctx.Tree.Entries` in walk order.
  • `dirStructureRule` tracks a `skipTracker` to preserve `filepath.SkipDir` semantics (disallowed subtree suppresses descendant checks — locked by the new `disallowed-subtree` fixture).
  • `requiredPathsRule` and `requiredGroupsRule` keep their `os.Stat` / glob helpers — legacy quirk of seeing through ignored directories is a documented parity requirement.
  • `placementRule` and `boundariesRule` preserve their `Successes` counter quirks.
  • Exported `Validate*` methods stay as thin wrappers via a new `runSingleRule` helper so the root `validator_test.go` and any external callers keep working.

Broadened parity fixtures: `boundaries` (Go import parsing, module resolution), `required-groups` (`eachDirMatching` + `oneOf`), `disallowed-subtree` (SkipDir semantics with nested subtrees). Three cases the earlier goldens didn't touch at all.

2. Init templates deduped

Deletes the 220-line `projectTemplates` map in `internal/cli/init.go`. `init --type ` now reads the same embedded preset files that `extends: -standard` resolves against, via a new `config.ReadPreset` API, and prepends a one-line project-typed header.

  • `init.go`: 322 → 168 lines.
  • New `test/init_presets_test.go` proves init output body is byte-identical to the preset AND that `init --type go` vs `extends: go-standard` produce equivalent validation on the same tree.

3. Version pragma is actually enforced

The prior "mitigation" for old-binary/new-config was docs suggesting `# requires structlint >= vX.Y`. Nothing checked it. Users saw the raw yaml `field not found` error.

New `internal/config/versioncheck.go` parses the pragma from raw bytes before strict parsing and prints:

```
.structlint.yaml requires structlint >= v0.6.0, but running version is v0.5.0.
Upgrade the binary (go install github.com/AxeForging/structlint/cmd/structlint@latest) …
```

Because the check runs before strict parsing, it works on binaries too old to know about `extends`. Dev builds (unstamped) skip the check.

  • 10 in-package unit tests over parse/compare edges.
  • 2 binary-based end-to-end tests that build pinned binaries with ldflags-injected `Version` values and prove the CLI surfaces the error cleanly.

4. Deeper SKILL.md validation

`skill_contract_test.go` had shallow checks. `skill_deep_test.go` adds:

  • Frontmatter parses as real YAML with expected shape.
  • Trigger phrases survive block-scalar folding (parse YAML → normalize whitespace → search).
  • Every subcommand referenced in setup recipes actually boots via `app.Run(--help)`.
  • JSON v1 contract mentioned + all four exit codes documented.

Numbers

  • 167 tests before → 191 tests after.
  • `init.go`: 322 → 168 lines.
  • `validator.go`: 494 → 232 lines (all the walk bodies moved into engine.go's rules).
  • 6 parity fixtures with text/json/exit goldens. Zero drift.

Test plan

  • `go test -race ./...` — 191 pass.
  • `make build && ./bin/structlint validate` — self-dogfood, exit 0.
  • `gofumpt -l .` clean, `go vet ./...` clean.

Follow-up PR that honestly delivers what the earlier roadmap PRs
oversold. Everything is behind broadened parity goldens so any drift
is a build failure.

## 1. True single-walk rule engine (fixes spec 005 dishonesty)

The prior "rule engine" PR (#17) shipped an architectural wrapper but
kept the seven per-rule filepath.Walk calls untouched — every rule
still walked the tree via the old Validate* method it was wrapping.
Net cost: 8 walks per run instead of the previous 7.

This commit does the actual conversion:
- Each rule iterates ctx.Tree.Entries in walk order.
- dirStructureRule tracks a skipTracker to preserve filepath.SkipDir
  semantics from the legacy walk (disallowed subtree suppresses
  descendant checks).
- requiredPathsRule and requiredGroupsRule keep their os.Stat / glob
  helpers — the legacy quirk of seeing through ignored directories is
  a parity requirement.
- placementRule and boundariesRule preserve their Successes counter
  quirks (once per (file, rule) pair even on violations).
- The exported Validate* methods stay as thin wrappers over
  runSingleRule(path, rule) so root validator_test.go and any external
  callers keep working.

Broadened parity fixtures cover boundaries, requiredGroups, and
disallowed-subtree SkipDir semantics — three cases the earlier
goldens didn't touch. 21 parity subtests, all byte-identical.

## 2. Init templates now live in one place (fixes template dup)

Deletes the 220-line projectTemplates map in internal/cli/init.go.
`init --type <T>` now reads the same embedded preset files that
`extends: <T>-standard` resolves against, via config.ReadPreset, and
prepends a one-line project-typed header comment. Two ways to reach
the same rules; one source of truth.

- init.go: 322 → 168 lines.
- test/init_presets_test.go proves init output body is byte-identical
  to the corresponding preset AND that init --type go and
  extends: go-standard produce equivalent validation on the same tree.

## 3. Version pragma actually enforced (fixes docs-only mitigation)

The prior mitigation for "old binary + new config with `extends`" was
just a suggestion to add `# requires structlint >= vX.Y` in comments.
Nothing enforced it — users saw the raw yaml `field not found` error.

New internal/config/versioncheck.go parses the pragma from the raw
bytes BEFORE strict parsing and prints an actionable message when the
running binary is older than required. Dev builds (Version=="dev") and
malformed pragmas skip the check.

Tests: 10 in-package unit tests over parse/compare edge cases, plus
2 binary-based end-to-end tests that build pinned binaries with
ldflags-injected versions and prove the error surfaces cleanly.

## 4. Deeper SKILL.md validation

skill_contract_test.go had shallow checks (file exists, frontmatter
delimiter present). skill_deep_test.go adds:
- Frontmatter parses as real YAML with the expected shape.
- Trigger phrases from the description survive block-scalar folding
  (parses YAML first, then normalizes whitespace before searching).
- Every subcommand referenced in setup recipes actually boots via
  app.Run(--help) — catches renames.
- JSON v1 contract mentioned + all four exit codes documented.

## Numbers

167 tests before this PR, 191 tests after. Every existing parity
golden byte-identical. Self-validation still passes.
@github-actions

Copy link
Copy Markdown

StructLint — All checks passed

139 rules validated against .structlint.yaml. No violations found.

View full run · Powered by StructLint

@machado144 machado144 merged commit 06ba2e1 into main Jul 10, 2026
5 checks passed
@machado144 machado144 deleted the refactor/single-walk-and-followups branch July 10, 2026 07:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant