Skip to content

ci: structure validation #3

Draft
wzslr321 wants to merge 9 commits into
mainfrom
ci/gh-actions-structure-validation
Draft

ci: structure validation #3
wzslr321 wants to merge 9 commits into
mainfrom
ci/gh-actions-structure-validation

Conversation

@wzslr321
Copy link
Copy Markdown
Member

No description provided.

@wzslr321
Copy link
Copy Markdown
Member Author

Code review

Self-comment, since GitHub doesn't allow formal self-review.

What I ran locally on this branch

  • go test ./... — pass, 76.4% coverage
  • go vet ./... — clean
  • golangci-lint run ./... — 0 issues
  • gofmt -l cmd internal — clean
  • ruff format --check scripts && ruff check scripts — clean
  • go run ./cmd/validate-pluginsOK: plugin structure is valid (12 plugin(s) checked).
  • python scripts/generate-rules.py --checkOK: 20 rule(s) × 2 target(s) in sync.

Overall

Solid, KISS-aligned. Pure stdlib in both Go and Python (no go.sum, no requirements.txt), good package split inside internal/pluginvalidation/, the Report-collecting pattern keeps Validate readable, and the test scaffolding (seedRepo + updateJSON + assertErrorContains) makes future cases cheap to add.

Notes below — most are simplifications. Two small correctness regressions worth fixing.


Must fix

1. scripts/generate-rules.py regressed: only works from the repo root.

Before this PR (main):

REPO_ROOT = Path(__file__).parent.parent

After:

REPO_ROOT = Path.cwd().resolve()
if not (REPO_ROOT / TOOLING_CONFIG_FILE).is_file():
    print('Missing plugin-tooling.json in current directory. ...', file=sys.stderr)
    sys.exit(1)

Repro:

$ cd /tmp
$ python3 /…/scripts/generate-rules.py --check
Missing plugin-tooling.json in current directory. Run this script from the repository root.
exit=1

CI is fine because the workflow runs from the repo root, but scripts/<file>.py is conventionally invocable from anywhere. Suggest reverting to Path(__file__).parent.parent.resolve() and dropping the cwd guard.

2. Dead sys.path mutation in scripts/generate-rules.py:38-40.

from plugin_tooling_config import (   # line 22 — runs first
    TOOLING_CONFIG_FILE, ...
)
...
SCRIPTS_DIR = REPO_ROOT / 'scripts'   # line 38 — runs *after* the import
if str(SCRIPTS_DIR) not in sys.path:
    sys.path.insert(0, str(SCRIPTS_DIR))

The top-level from plugin_tooling_config import … runs before the sys.path.insert. For that import to succeed at all, scripts/ must already be on sys.path — and it is, because CPython auto-adds the script's parent dir at startup. Lines 38–40 never do anything. Just delete them.


Should consider — KISS / simplification

3. sortedKeys generic does more work than it earns — internal/pluginvalidation/files.go:59.

func sortedKeys[K ~string, V any](m map[K]V) []string { ... }

No caller uses a custom string type. The K ~string constraint and the string(key) cast are not pulling weight. Reduce to:

func sortedKeys[V any](m map[string]V) []string { ... }

4. Redundant TrimPrefix("./") in files.go:42 (resolvePluginRelative).
filepath.Join + filepath.Clean already collapse ./foofoo. The whole helper can be:

func resolvePluginRelative(pluginDir, pointer string) (string, bool) {
    if filepath.IsAbs(pointer) {
        return "", false
    }
    target := filepath.Join(pluginDir, filepath.FromSlash(pointer))
    rel, err := filepath.Rel(pluginDir, target)
    if err != nil || rel == "." || rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
        return "", false
    }
    return target, true
}

5. validateManifestVersionParity (plugin.go:70-102) — over-iterated.
The "first non-empty == reference" pattern is one slice op:

names := sortedKeys(versions)
if len(names) < 2 { return }
ref := versions[names[0]]
for _, n := range names[1:] {
    if versions[n] != ref { /* report and return */ }
}

6. isUsageSkillName (plugin.go:172) — collapse to one expression.

return name == "usage" ||
    strings.HasSuffix(name, "-usage") ||
    strings.HasSuffix(name, "_usage")

7. plugin_dir_for_source is more general than the layout it serves (scripts/plugin_tooling_config.py:55).

def plugin_dir_for_source(self, repo_root: Path, src: Path) -> Path:
    relative = src.relative_to(self.plugin_root_path(repo_root))
    return self.plugin_root_path(repo_root) / relative.parts[0]

The glob fixes the layout to <plugin>/<rule_source.dir>/*.md, so this is exactly src.parent.parent (which is what the original main code used). The new method only earns its complexity if rule_source.dir could become nested — and the glob doesn't allow that anyway. Reverting to src.parent.parent is fine.


Test coverage gaps

go tool cover reports 76.4%. The functions worth filling in (each is a one-line tweak inside seedRepo away):

Function Coverage
validateManifestDirPointer 45.5%
isUsageSkillName 40% (only "usage" branch)
readFile (error paths) 42.9%
validateMarketplaceEntries 58.3% (duplicate-name / empty-name)
validateSkills 63.2%
loadManifest 62.5%

Concrete cases I'd add:

  • skills directory missing entirely
  • skills directory with no */SKILL.md
  • skills directory with skills but no usage skill
  • plugin missing README.md
  • manifest.name != pluginName
  • manifest missing rules / skills field
  • frontmatter without description
  • frontmatter without globs / empty list
  • metadata.pluginRoot mismatch in cursor marketplace
  • invalid plugin-tooling.json (each required-field branch in validateToolingConfig)

Minor / style nits

8. Doc drift in AGENTS.md.

Adding a new IDE target requires adding a new generatedRuleTargets entry in plugin-tooling.json.

That's only half the truth. Adding a platform also needs:

  • a new platformDefinition in internal/pluginvalidation/config.go:58
  • a new branch in the cursor/claude switch inside validateMarketplace (marketplace.go:49-79) to express the platform's source rule

Either document the Go side, or drive the platform list from the config (more work; YAGNI for now).

9. CI: pin Go via go.mod.
Both workflows hard-code go-version: '1.26.2'. actions/setup-go@v5 accepts go-version-file: go.mod — one source of truth, no drift.

10. _required_str (plugin_tooling_config.py:96) loses path context.
The Go validator's errors include the file path (plugin-tooling.json: …). The Python equivalent raises bare `pluginRoot` must be a non-empty string. Threading path.relative_to(repo_root) through gives parity.

11. parseFrontmatter close marker is \n--- with no end-of-line check (frontmatter.go:23).
Fine for the rule files in this repo. A markdown horizontal rule somewhere unusual could mis-trigger it. Either tighten to a regex ((?m)^---\s*$) or add a one-line comment that this assumes well-formed frontmatter.


Nothing here blocks the PR — must-fixes are quick. Nice baseline for future structural checks.

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