mustard-claude 3.1.26 → 3.1.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -3
- package/package.json +1 -1
- package/templates/CLAUDE.md +1 -1
- package/templates/commands/mustard/scan/SKILL.md +5 -22
- package/templates/commands/mustard/scan-format/SKILL.md +66 -30
- package/templates/commands/mustard/skill/SKILL.md +1 -1
- package/templates/commands/mustard/templates/agent-prompt/SKILL.md +4 -4
- package/templates/hooks/__tests__/skill-generator.snapshot.test.js +0 -230
- package/templates/scripts/_fence-languages.json +0 -57
- package/templates/scripts/_skill-meta.json +0 -10
- package/templates/scripts/migrate-skill-paths.js +0 -190
- package/templates/scripts/skill-generator.js +0 -1238
- package/templates/skill-templates/cluster-pattern.skill.md.tmpl +0 -35
package/README.md
CHANGED
|
@@ -29,7 +29,7 @@ Mustard sets up a `.claude/` folder that turns Claude Code into a structured dev
|
|
|
29
29
|
- **16 pipeline commands** — feature, bugfix, approve, complete, resume, scan, scan-format, git, maint, task, knowledge, skill, status, stats, metrics, review, plus the agent-prompt template
|
|
30
30
|
- **23 enforcement hooks** — bash safety, bash native redirect, file guard, registry enforcement, guard verify, auto-format, pre-compact, session cleanup, subagent tracker, RTK rewrite, session memory, review gate, metrics tracker, MCP budget, session knowledge, context budget, spec hygiene, output budget, tool-use counter, model routing gate, debug-loop guard, user-prompt hint, session-knowledge incremental
|
|
31
31
|
- **6 bundled skills** — design-craft, react-best-practices, senior-architect, skill-creator, commit-workflow, pipeline-execution
|
|
32
|
-
- **15 utility scripts** — subproject detection, entity registry sync, statusline, memory persist/write, diff context, knowledge update, metrics collect/report, security scan, pipeline verification, analyze validation, recipe matcher,
|
|
32
|
+
- **15 utility scripts** — subproject detection, entity registry sync, skill validation, statusline, memory persist/write, diff context, knowledge update, metrics collect/report, security scan, pipeline verification, analyze validation, recipe matcher, RTK gain import
|
|
33
33
|
- **Token economy** — auto-installs [RTK (Rust Token Killer)](https://github.com/rtk-ai/rtk) to reduce CLI-output tokens by 60–90%
|
|
34
34
|
- **Hook profiles & env overrides** — minimal/standard/strict profiles via `_lib/hook-env.js`; disable individual hooks with `MUSTARD_DISABLED_HOOKS`
|
|
35
35
|
- **Cursor IDE adapter** (experimental) — `mustard init --cursor` installs a Cursor-compatible hook adapter
|
|
@@ -287,7 +287,8 @@ mustard review --ci --pr 42
|
|
|
287
287
|
│ └── user-prompt-hint.js # Surfaces contextual hints on prompt input
|
|
288
288
|
├── scripts/ # Utility scripts (15)
|
|
289
289
|
│ ├── sync-detect.js # Detects subprojects + roles (SHA-256 incremental)
|
|
290
|
-
│ ├── sync-registry.js # Generates entity-registry.json
|
|
290
|
+
│ ├── sync-registry.js # Generates entity-registry.json (_patterns.discovered[])
|
|
291
|
+
│ ├── skill-validate.js # Validates SKILL.md frontmatter across subprojects
|
|
291
292
|
│ ├── statusline.js # Claude Code statusline
|
|
292
293
|
│ ├── memory-persist.js # Persists decisions/lessons across sessions
|
|
293
294
|
│ ├── memory-write.js # Writes agent memory entries between waves
|
|
@@ -295,11 +296,11 @@ mustard review --ci --pr 42
|
|
|
295
296
|
│ ├── knowledge-update.js # Updates project knowledge base
|
|
296
297
|
│ ├── metrics-collect.js # Collects pipeline metrics
|
|
297
298
|
│ ├── metrics-report.js # Renders enforcement metrics report
|
|
299
|
+
│ ├── rtk-gain-import.js # Imports RTK token-savings data for metrics
|
|
298
300
|
│ ├── security-scan.js # Scans for secrets / security misconfigs
|
|
299
301
|
│ ├── verify-pipeline.js # Runs build/test verification
|
|
300
302
|
│ ├── analyze-validation.js # Validates ANALYZE phase output
|
|
301
303
|
│ ├── recipe-match.js # Structured recipe matcher (entity + operation)
|
|
302
|
-
│ ├── skill-generator.js # Generates subproject pattern skills
|
|
303
304
|
│ └── _metrics-write.js # Internal metrics writer (used by hooks)
|
|
304
305
|
├── memory/ # Persistent memory (auto-created)
|
|
305
306
|
│ ├── decisions.json # Decisions across pipelines
|
package/package.json
CHANGED
package/templates/CLAUDE.md
CHANGED
|
@@ -47,7 +47,7 @@ node scripts/sync-detect.js --no-cache
|
|
|
47
47
|
node scripts/sync-registry.js
|
|
48
48
|
node scripts/sync-registry.js --force
|
|
49
49
|
|
|
50
|
-
# Skill validation (
|
|
50
|
+
# Skill validation (invoked by /scan §4.7; also callable standalone)
|
|
51
51
|
node scripts/skill-validate.js
|
|
52
52
|
node scripts/skill-validate.js --json
|
|
53
53
|
```
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
- Ignora o incremental skip do Step 1/C: **todos** os subprojetos são reprocessados, independente de hash match ou `gitDirty`.
|
|
13
13
|
- Bypassa o fast-path de §2.6 (Bootstrap): sempre regenera `.claude/CLAUDE.md` e afins.
|
|
14
14
|
- Repassa "FORCE MODE" aos Task agents do Step 3 (eles apagam `{subproject}/.claude/skills/*/` com header `mustard:generated` antes de regerar).
|
|
15
|
-
- Roda
|
|
15
|
+
- Roda `sync-registry.js --force` (§4.7) sempre — mesmo com registry já v4.0.
|
|
16
16
|
- Skills sem o header `mustard:generated` (user-authored) são **preservadas**.
|
|
17
17
|
|
|
18
18
|
## Execution Model
|
|
@@ -326,7 +326,7 @@ See `scan-format.md` §10 for decomposition rules, SKILL.md format, and descript
|
|
|
326
326
|
|
|
327
327
|
**Key rules:**
|
|
328
328
|
- One conceptual pattern = one skill (not one file = one skill)
|
|
329
|
-
- Skill name: `{subproject-short}-{pattern-name}`
|
|
329
|
+
- Skill name: `{subproject-short}-{pattern-name}` — pattern-name is a kebab-case concept the codebase itself uses (derived from its folders / file suffixes / domain vocabulary), never a library brand or imported taxonomy
|
|
330
330
|
- Description must be "pushy" — include casual trigger phrases (see scan-format.md §10)
|
|
331
331
|
- Extract real code examples into `references/examples.md`
|
|
332
332
|
- Max 500 lines per SKILL.md body (ideally <200)
|
|
@@ -342,32 +342,15 @@ See `scan-format.md` §10 for decomposition rules, SKILL.md format, and descript
|
|
|
342
342
|
Skills are generated ONLY in `{subproject}/.claude/skills/{skill-name}/` (NOT in root `.claude/skills/`).
|
|
343
343
|
Mark all with `<!-- mustard:generated -->`. Overwrite on next scan.
|
|
344
344
|
|
|
345
|
-
### 4.7.
|
|
345
|
+
### 4.7. Refresh Registry
|
|
346
346
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
> With `--force`, **always** run both commands — even if `entity-registry.json` already exists at v4.0.
|
|
347
|
+
Run the registry scanner so the agent-generated skills of step 3 have the latest `_patterns.discovered[]` clusters available for future scans:
|
|
350
348
|
|
|
351
349
|
```bash
|
|
352
350
|
node .claude/scripts/sync-registry.js --force
|
|
353
|
-
node .claude/scripts/skill-generator.js --force
|
|
354
351
|
```
|
|
355
352
|
|
|
356
|
-
|
|
357
|
-
- `{role}-entity-creation` — entity folder, base class, interfaces, namespace
|
|
358
|
-
- `{role}-enum-placement` — enum folder, decorators, NEVER inline in entities
|
|
359
|
-
- `{role}-route-conventions` — route naming, auth pattern, CRUD standard
|
|
360
|
-
- `{role}-service-pattern` — interface-first, base interface, DI
|
|
361
|
-
- `{role}-repository-pattern` — base class, interface, DI
|
|
362
|
-
- `{role}-dto-conventions` — folder, naming, validation pattern
|
|
363
|
-
- `{role}-module-registration` — DI registration, route wiring
|
|
364
|
-
|
|
365
|
-
These skills are derived from **detected patterns** (not hardcoded). They complement agent-generated skills by covering structural conventions that agents may not explicitly document.
|
|
366
|
-
|
|
367
|
-
**Skip conditions:**
|
|
368
|
-
- `entity-registry.json` version < 4.0 → skip (registry not populated)
|
|
369
|
-
- `skill-generator.js` not present → skip
|
|
370
|
-
- Pattern skill already exists and was NOT generated by mustard → skip (user-edited, unless `--force` — but `skill-generator.js` already preserves user-authored skills by checking the `mustard:generated` header)
|
|
353
|
+
Skill generation itself is **entirely the responsibility of the Step 3 agents** (see `scan-format.md` §10). There is no separate mechanical generator — the agent reads `_patterns[*].discovered[]` and emits cluster skills directly.
|
|
371
354
|
|
|
372
355
|
### 4. Update CLAUDE.md files
|
|
373
356
|
|
|
@@ -183,27 +183,42 @@ Follow the [skill-creator](https://github.com/anthropics/skills) methodology for
|
|
|
183
183
|
|
|
184
184
|
### Decomposition Rules
|
|
185
185
|
|
|
186
|
-
Each detected pattern becomes its own skill. Group by conceptual unit
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
186
|
+
Each detected pattern becomes its own skill. Group by **conceptual unit**, not by file. The agent derives both the skill name and its scope from what the codebase actually shows — no fixed list of technologies, no predetermined taxonomy.
|
|
187
|
+
|
|
188
|
+
**Naming**: `{subproject-short}-{kebab-case-concept}` — the concept is whatever the codebase itself calls the thing. If the project has a folder called `Resolvers/`, the skill is `{sub}-resolver-pattern`. If it has `composables/`, it's `{sub}-composable-pattern`. If it has `Handlers/`, it's `{sub}-handler-pattern`. Never import vocabulary the codebase does not use.
|
|
189
|
+
|
|
190
|
+
**Scope**: one skill per reusable convention the agent would hand to a future agent implementing "add one more like these". If the pattern is a one-off file, it is not a convention — skip it.
|
|
191
|
+
|
|
192
|
+
**Anti-patterns to avoid**:
|
|
193
|
+
- Emitting a skill for every file type (`.ts`, `.css`, `.json` → three skills = noise)
|
|
194
|
+
- Emitting a skill for generic cross-cutting concerns (logging, error handling) unless the codebase has a distinctive, repeated shape for them
|
|
195
|
+
- Naming a skill after a library the codebase uses but whose convention is entirely the library's default (e.g. "using React hooks as documented" is not a codebase convention)
|
|
196
|
+
|
|
197
|
+
### Cluster Skills from the Registry (mandatory)
|
|
198
|
+
|
|
199
|
+
After generating the conceptual skills above, **also** read `.claude/entity-registry.json` and iterate `_patterns[{stackId}].discovered[]` for each detected stack. Each cluster entry looks like:
|
|
200
|
+
|
|
201
|
+
```json
|
|
202
|
+
{
|
|
203
|
+
"suffix": "Service",
|
|
204
|
+
"fileCount": 7,
|
|
205
|
+
"folders": ["src/Services", "src/Modules/Auth/Services"],
|
|
206
|
+
"samples": ["src/Services/UserService.cs", "..."],
|
|
207
|
+
"commonBaseClass": "BaseService",
|
|
208
|
+
"commonInterfaces": ["IService"]
|
|
209
|
+
}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
For each cluster that represents a **reusable convention** (skip one-offs, test-only files, or trivial groupings), emit a skill named `{sub}-{suffix-slug}-pattern` (e.g. `backend-service-pattern`, `frontend-component-pattern`). SKILL.md body:
|
|
213
|
+
|
|
214
|
+
- `## Pattern` — enumerate `suffix`, `fileCount`, `folderPattern`, `commonBaseClass`, `commonInterfaces` as bullets (fields that exist in the cluster — do not invent).
|
|
215
|
+
- `## Rules` — DO/DON'T derived from the cluster (naming, folder placement, base class usage).
|
|
216
|
+
- `## Samples in this project` — bullet list of the `samples` file paths.
|
|
217
|
+
- `## References` — pointer to `references/examples.md`.
|
|
218
|
+
|
|
219
|
+
**Agent judgment filter**: do NOT blindly emit one skill per cluster. If a cluster has fewer than ~3 files OR the suffix is generic noise (e.g. `Test`, `Mock`, `Spec`), skip it. The goal is reusable conventions, not coverage theater.
|
|
220
|
+
|
|
221
|
+
`_patterns.folderFrequency` provides a stopword source for distinctive-keyword extraction: segments appearing in ≥60% of all folders are structural noise and should be ignored when describing the cluster.
|
|
207
222
|
|
|
208
223
|
### Skill Structure
|
|
209
224
|
|
|
@@ -216,35 +231,56 @@ Adapt to what the codebase actually has — this table is guidance, not rigid.
|
|
|
216
231
|
|
|
217
232
|
### SKILL.md Format (skill-creator standard)
|
|
218
233
|
|
|
234
|
+
**CRITICAL — NO CODE IN SKILL.md.** The SKILL.md body describes the pattern in prose + bullet lists + file references. **Never** embed code blocks, language-specific stubs, or synthesized examples (no fake `class Order { ... }`, no TypeScript snippet, no SQL, nothing). All concrete code lives in `references/examples.md`, extracted from real source files.
|
|
235
|
+
|
|
219
236
|
```yaml
|
|
220
237
|
---
|
|
221
238
|
name: {skill-name}
|
|
222
239
|
description: "{What it does}. {When to use it — be specific and 'pushy'}.
|
|
223
240
|
Use when {trigger phrase 1}, {trigger phrase 2}, or {trigger phrase 3}.
|
|
224
241
|
Even if the user just says '{casual phrase}'."
|
|
242
|
+
source: scan
|
|
225
243
|
---
|
|
226
244
|
<!-- mustard:generated at:{ISO} role:{role} -->
|
|
227
245
|
|
|
228
246
|
# {Skill Title}
|
|
229
247
|
|
|
230
|
-
|
|
248
|
+
> Pattern detected in this project.
|
|
231
249
|
|
|
232
|
-
##
|
|
250
|
+
## Convention
|
|
233
251
|
|
|
234
|
-
|
|
235
|
-
{
|
|
252
|
+
- Folder: `{detected folder}`
|
|
253
|
+
- {other fields present in the pattern — enumerate dynamically, do not assume}
|
|
254
|
+
- Naming: `{detected naming convention}`
|
|
236
255
|
|
|
237
|
-
##
|
|
256
|
+
## Real examples in this codebase
|
|
238
257
|
|
|
239
|
-
|
|
240
|
-
|
|
258
|
+
- `{EntityName}` — `{path/to/real/file.ext}`
|
|
259
|
+
- `{OtherEntity}` — `{path/to/other/file.ext}`
|
|
241
260
|
|
|
242
261
|
## References
|
|
243
262
|
|
|
244
|
-
|
|
245
|
-
|
|
263
|
+
See `references/examples.md` for extracted code.
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
**Important:** The `## Convention` section lists detected fields from the registry pattern directly (folder, base class, naming, interfaces, etc.). The `## Real examples` section lists actual file paths from the registry. Concrete code — if any is needed at all — belongs only in `references/examples.md`, extracted from real source files (never handwritten).
|
|
267
|
+
|
|
268
|
+
### references/examples.md Format
|
|
269
|
+
|
|
270
|
+
```markdown
|
|
271
|
+
<!-- mustard:generated at:{ISO} -->
|
|
272
|
+
|
|
273
|
+
# {Pattern} — real examples from this codebase
|
|
274
|
+
|
|
275
|
+
## {EntityName}
|
|
276
|
+
Source: `{path/to/real/file.ext}`
|
|
277
|
+
\`\`\`{lang-from-extension}
|
|
278
|
+
{actual file content or excerpt — ≤80 lines full, or first class declaration ±20 lines}
|
|
279
|
+
\`\`\`
|
|
246
280
|
```
|
|
247
281
|
|
|
282
|
+
File extension maps to fence language: `.ts` → `typescript`, `.cs` → `csharp`, `.py` → `python`, `.dart` → `dart`, etc. Unknown extension: no language tag. If the file does not exist (stale registry), skip that entry silently.
|
|
283
|
+
|
|
248
284
|
### Description Writing Guidelines (from skill-creator)
|
|
249
285
|
|
|
250
286
|
Descriptions are the PRIMARY trigger mechanism — Claude uses them to decide which skills to load.
|
|
@@ -148,6 +148,6 @@ Update the skill-creator from the anthropics/skills repo.
|
|
|
148
148
|
- ALWAYS use skill-creator for `/skill create` — don't write skills from scratch
|
|
149
149
|
- `/skill optimize` and `/skill eval` require Python 3 and `claude` CLI
|
|
150
150
|
- `source:` field semantics (TERRITORIAL):
|
|
151
|
-
- `
|
|
151
|
+
- `/scan` agents (§4.6, §10) write `source: scan` ONLY — never touch `source: manual`.
|
|
152
152
|
- `/skill install`, `/skill create`, skill-creator write `source: manual` ONLY — never touch `source: scan`.
|
|
153
153
|
- Missing `source:` → treat as `manual` (conservative, protects user edits).
|
|
@@ -72,9 +72,9 @@ Based on task analysis, list the most relevant skill names:
|
|
|
72
72
|
- Architecture decisions → `senior-architect`
|
|
73
73
|
- Complex patterns → relevant advanced pattern skills
|
|
74
74
|
|
|
75
|
-
Examples (replace `{sub}` with actual subproject short name):
|
|
76
|
-
- Backend endpoint → `{sub}-endpoint-
|
|
77
|
-
- Mobile screen → `{sub}-
|
|
78
|
-
- Frontend section → `{sub}-section-
|
|
75
|
+
Examples (replace `{sub}` with actual subproject short name; skill names below are placeholders — pick whatever skills the subproject's `.claude/skills/` actually defines):
|
|
76
|
+
- Backend endpoint → `{sub}-{endpoint-skill}, {sub}-{module-skill}`
|
|
77
|
+
- Mobile screen → `{sub}-{screen-skill}, {sub}-{state-skill}, design-craft`
|
|
78
|
+
- Frontend section → `{sub}-{section-skill}, design-craft, react-best-practices`
|
|
79
79
|
|
|
80
80
|
ULTRATHINK
|
|
@@ -1,230 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
const test = require('node:test');
|
|
3
|
-
const assert = require('node:assert');
|
|
4
|
-
const { execSync } = require('node:child_process');
|
|
5
|
-
const fs = require('node:fs');
|
|
6
|
-
const os = require('node:os');
|
|
7
|
-
const path = require('node:path');
|
|
8
|
-
|
|
9
|
-
const ROOT = path.resolve(__dirname, '..', '..', '..');
|
|
10
|
-
const SCRIPT = path.join(ROOT, 'templates', 'scripts', 'skill-generator.js');
|
|
11
|
-
|
|
12
|
-
test('skill-generator: --dry-run output is stable across runs', () => {
|
|
13
|
-
const opts = { encoding: 'utf-8', cwd: ROOT };
|
|
14
|
-
const out1 = execSync(`node "${SCRIPT}" --dry-run`, opts);
|
|
15
|
-
const out2 = execSync(`node "${SCRIPT}" --dry-run`, opts);
|
|
16
|
-
assert.strictEqual(out1, out2, 'dry-run output should be deterministic');
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
test('skill-generator: --check flag passes (JS syntax valid)', () => {
|
|
20
|
-
const out = execSync(`node --check "${SCRIPT}"`, { encoding: 'utf-8', cwd: ROOT });
|
|
21
|
-
// node --check exits 0 on success, no assertion needed beyond no-throw
|
|
22
|
-
assert.ok(true, 'syntax check passed');
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
test('skill-generator: _skill-meta.json is valid JSON with required keys', () => {
|
|
26
|
-
const metaPath = path.join(ROOT, 'templates', 'scripts', '_skill-meta.json');
|
|
27
|
-
const meta = JSON.parse(require('fs').readFileSync(metaPath, 'utf-8'));
|
|
28
|
-
assert.ok(meta.stacks && typeof meta.stacks === 'object', 'must have stacks');
|
|
29
|
-
assert.ok(meta.roles && typeof meta.roles === 'object', 'must have roles');
|
|
30
|
-
assert.ok(meta.stacks.dotnet, 'stacks.dotnet must exist');
|
|
31
|
-
assert.ok(meta.stacks.typescript, 'stacks.typescript must exist');
|
|
32
|
-
assert.strictEqual(meta.stacks.dotnet.lang, 'csharp');
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
test('skill-generator: validateSkill catches missing description', () => {
|
|
36
|
-
// skill-generator exports validateSkill via module.exports when required (not run as main)
|
|
37
|
-
const { validateSkill } = require('../../scripts/skill-generator.js');
|
|
38
|
-
assert.ok(typeof validateSkill === 'function', 'validateSkill must be exported');
|
|
39
|
-
|
|
40
|
-
// Test: missing frontmatter
|
|
41
|
-
let r = validateSkill('no frontmatter here');
|
|
42
|
-
assert.strictEqual(r.ok, false, 'should fail without frontmatter');
|
|
43
|
-
assert.ok(r.errors.some(e => e.includes('frontmatter')), 'error should mention frontmatter');
|
|
44
|
-
|
|
45
|
-
// Test: missing description
|
|
46
|
-
r = validateSkill('---\nname: foo-bar\nsource: scan\n---\n<!-- mustard:generated -->\nhi');
|
|
47
|
-
assert.strictEqual(r.ok, false, 'should fail without description');
|
|
48
|
-
assert.ok(r.errors.some(e => e.includes('description')), 'error should mention description');
|
|
49
|
-
|
|
50
|
-
// Test: missing source
|
|
51
|
-
r = validateSkill('---\nname: foo-bar\ndescription: "Use when creating a new entity, add model, create table, even if the user says new thing. This is long enough."\n---\n<!-- mustard:generated -->\nhi');
|
|
52
|
-
assert.strictEqual(r.ok, false, 'should fail without source');
|
|
53
|
-
assert.ok(r.errors.some(e => e.includes('source')), 'error should mention source');
|
|
54
|
-
|
|
55
|
-
// Test: valid
|
|
56
|
-
r = validateSkill('---\nname: foo-bar\ndescription: "Use when creating a new entity, add model, create table, even if the user says new thing. This is long enough to pass."\nsource: scan\n---\n<!-- mustard:generated -->\nhi');
|
|
57
|
-
assert.strictEqual(r.ok, true, 'valid skill should pass');
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
test('skill-generator: all skill .tmpl files have source: scan in frontmatter', () => {
|
|
61
|
-
const fs = require('fs');
|
|
62
|
-
const tplDir = path.join(ROOT, 'templates', 'skill-templates');
|
|
63
|
-
const files = fs.readdirSync(tplDir).filter(f => f.endsWith('.skill.md.tmpl'));
|
|
64
|
-
assert.ok(files.length > 0, 'should have at least one skill template');
|
|
65
|
-
for (const file of files) {
|
|
66
|
-
const content = fs.readFileSync(path.join(tplDir, file), 'utf-8');
|
|
67
|
-
assert.ok(content.includes('source: scan'), `${file} must contain "source: scan" in frontmatter`);
|
|
68
|
-
}
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
// ---------------------------------------------------------------------------
|
|
72
|
-
// Cluster discovery tests
|
|
73
|
-
// ---------------------------------------------------------------------------
|
|
74
|
-
|
|
75
|
-
test('cluster-discovery: discovers suffix-cluster from synthetic temp dir', () => {
|
|
76
|
-
const fs = require('fs');
|
|
77
|
-
const os = require('os');
|
|
78
|
-
const { discoverClusters } = require('../../scripts/registry/cluster-discovery.js');
|
|
79
|
-
|
|
80
|
-
// Create a temporary directory structure with 5+ files sharing suffix "Handler"
|
|
81
|
-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mustard-test-'));
|
|
82
|
-
const subDir = path.join(tmpDir, 'Commands');
|
|
83
|
-
fs.mkdirSync(subDir, { recursive: true });
|
|
84
|
-
|
|
85
|
-
const files = [
|
|
86
|
-
'CreateUserHandler.cs',
|
|
87
|
-
'UpdateUserHandler.cs',
|
|
88
|
-
'DeleteUserHandler.cs',
|
|
89
|
-
'CreateContractHandler.cs',
|
|
90
|
-
'UpdateContractHandler.cs',
|
|
91
|
-
'CreateInvoiceHandler.cs',
|
|
92
|
-
];
|
|
93
|
-
for (const f of files) {
|
|
94
|
-
fs.writeFileSync(path.join(subDir, f), `public class ${f.replace('.cs', '')} { }`);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
try {
|
|
98
|
-
const clusters = discoverClusters(tmpDir, 'dotnet');
|
|
99
|
-
assert.ok(Array.isArray(clusters), 'should return array');
|
|
100
|
-
assert.ok(clusters.length >= 1, 'should find at least one cluster');
|
|
101
|
-
|
|
102
|
-
const handlerCluster = clusters.find(c => c.suffix === 'Handler');
|
|
103
|
-
assert.ok(handlerCluster, 'should detect "Handler" suffix cluster');
|
|
104
|
-
assert.ok(handlerCluster.fileCount >= 6, `expected fileCount >= 6, got ${handlerCluster.fileCount}`);
|
|
105
|
-
assert.ok(
|
|
106
|
-
handlerCluster.kind === 'folder-cluster' || handlerCluster.kind === 'suffix-cluster',
|
|
107
|
-
`expected folder-cluster or suffix-cluster, got ${handlerCluster.kind}`
|
|
108
|
-
);
|
|
109
|
-
} finally {
|
|
110
|
-
// Cleanup
|
|
111
|
-
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
112
|
-
}
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
test('cluster-discovery: no hardcoded tech names in cluster-discovery.js source', () => {
|
|
116
|
-
const fs = require('fs');
|
|
117
|
-
const src = fs.readFileSync(
|
|
118
|
-
path.join(ROOT, 'templates', 'scripts', 'registry', 'cluster-discovery.js'),
|
|
119
|
-
'utf-8'
|
|
120
|
-
);
|
|
121
|
-
// These technology names must NOT appear in non-comment source lines of the discovery code.
|
|
122
|
-
// We strip comment lines before checking so JSDoc examples don't trigger false positives.
|
|
123
|
-
const forbidden = ['graphql', 'GraphQL', 'cqrs', 'CQRS', 'mediator', 'Mediator'];
|
|
124
|
-
const codeLines = src.split('\n').filter(line => {
|
|
125
|
-
const trimmed = line.trim();
|
|
126
|
-
return !trimmed.startsWith('//') && !trimmed.startsWith('*') && trimmed !== '';
|
|
127
|
-
});
|
|
128
|
-
const codeOnly = codeLines.join('\n');
|
|
129
|
-
for (const word of forbidden) {
|
|
130
|
-
assert.ok(
|
|
131
|
-
!codeOnly.includes(word),
|
|
132
|
-
`cluster-discovery.js must not contain hardcoded tech term "${word}" in non-comment code`
|
|
133
|
-
);
|
|
134
|
-
}
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
test('genClusterSkill: produces valid SKILL.md for Handler cluster', () => {
|
|
138
|
-
const { genClusterSkill, validateSkill } = require('../../scripts/skill-generator.js');
|
|
139
|
-
|
|
140
|
-
const cluster = {
|
|
141
|
-
kind: 'suffix-cluster',
|
|
142
|
-
suffix: 'Handler',
|
|
143
|
-
ext: '.cs',
|
|
144
|
-
fileCount: 7,
|
|
145
|
-
folders: ['Commands/Create', 'Commands/Update', 'Commands/Delete'],
|
|
146
|
-
folderPattern: '**/Commands/',
|
|
147
|
-
samples: ['CreateUserHandler.cs', 'UpdateContractHandler.cs', 'DeleteInvoiceHandler.cs'],
|
|
148
|
-
label: 'Handler',
|
|
149
|
-
};
|
|
150
|
-
|
|
151
|
-
const result = genClusterSkill('backend', 'dotnet', cluster, 'api');
|
|
152
|
-
assert.ok(result !== null, 'genClusterSkill should return a result');
|
|
153
|
-
assert.ok(result.slug === 'handler', `slug should be "handler", got "${result.slug}"`);
|
|
154
|
-
|
|
155
|
-
const validation = validateSkill(result.skillMd);
|
|
156
|
-
assert.ok(validation.ok, `generated skill should be valid. Errors: ${validation.errors.join(', ')}`);
|
|
157
|
-
|
|
158
|
-
// Frontmatter name must start with skill prefix
|
|
159
|
-
assert.ok(result.skillMd.includes('backend-handler-pattern'), 'name should be backend-handler-pattern');
|
|
160
|
-
|
|
161
|
-
// Must NOT contain forbidden tech names as string literals in the output
|
|
162
|
-
const forbidden = ['graphql', 'GraphQL', 'cqrs', 'mediator'];
|
|
163
|
-
for (const word of forbidden) {
|
|
164
|
-
assert.ok(!result.skillMd.includes(word), `output must not contain "${word}"`);
|
|
165
|
-
}
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
test('cleanupOrphanSkills: removes source:scan folders not in expected set', () => {
|
|
169
|
-
const { cleanupOrphanSkills } = require('../../scripts/skill-generator.js');
|
|
170
|
-
|
|
171
|
-
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'mustard-cleanup-'));
|
|
172
|
-
const skillsDir = path.join(tmpRoot, '.claude', 'skills');
|
|
173
|
-
|
|
174
|
-
const mkSkill = (folder, frontmatter) => {
|
|
175
|
-
const dir = path.join(skillsDir, folder);
|
|
176
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
177
|
-
fs.writeFileSync(path.join(dir, 'SKILL.md'), `---\n${frontmatter}\n---\n# skill`);
|
|
178
|
-
};
|
|
179
|
-
|
|
180
|
-
// Expected skills (current run will write these)
|
|
181
|
-
mkSkill('backend-entity-creation', 'name: backend-entity-creation\ndescription: x\nsource: scan');
|
|
182
|
-
mkSkill('backend-service-pattern', 'name: backend-service-pattern\ndescription: x\nsource: scan');
|
|
183
|
-
|
|
184
|
-
// Orphan: was generated previously but pattern no longer exists
|
|
185
|
-
mkSkill('backend-queryresolver-pattern', 'name: backend-queryresolver-pattern\ndescription: x\nsource: scan');
|
|
186
|
-
|
|
187
|
-
// Manual skill — MUST NOT be touched (source: manual)
|
|
188
|
-
mkSkill('backend-custom-helper', 'name: backend-custom-helper\ndescription: x\nsource: manual');
|
|
189
|
-
|
|
190
|
-
// Unrelated sub — MUST NOT be touched (not in processed subs)
|
|
191
|
-
mkSkill('frontend-entity-creation', 'name: frontend-entity-creation\ndescription: x\nsource: scan');
|
|
192
|
-
|
|
193
|
-
const expected = new Set(['backend-entity-creation', 'backend-service-pattern']);
|
|
194
|
-
const log = [];
|
|
195
|
-
const removed = cleanupOrphanSkills(skillsDir, expected, ['backend'], log);
|
|
196
|
-
|
|
197
|
-
try {
|
|
198
|
-
assert.strictEqual(removed, 1, 'should remove exactly 1 orphan');
|
|
199
|
-
assert.ok(!fs.existsSync(path.join(skillsDir, 'backend-queryresolver-pattern')), 'orphan removed');
|
|
200
|
-
assert.ok(fs.existsSync(path.join(skillsDir, 'backend-entity-creation')), 'expected preserved');
|
|
201
|
-
assert.ok(fs.existsSync(path.join(skillsDir, 'backend-custom-helper')), 'source:manual preserved');
|
|
202
|
-
assert.ok(fs.existsSync(path.join(skillsDir, 'frontend-entity-creation')), 'other sub preserved');
|
|
203
|
-
} finally {
|
|
204
|
-
try { fs.rmSync(tmpRoot, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
205
|
-
}
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
test('cluster-discovery: min suffix length filters short suffixes', () => {
|
|
209
|
-
const fs = require('fs');
|
|
210
|
-
const os = require('os');
|
|
211
|
-
const { discoverClusters } = require('../../scripts/registry/cluster-discovery.js');
|
|
212
|
-
|
|
213
|
-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mustard-test-short-'));
|
|
214
|
-
const subDir = path.join(tmpDir, 'Models');
|
|
215
|
-
fs.mkdirSync(subDir, { recursive: true });
|
|
216
|
-
|
|
217
|
-
// Files ending in short suffix "es" should NOT trigger a cluster
|
|
218
|
-
const files = ['Bankes.cs', 'Foxes.cs', 'Boxes.cs', 'Taxes.cs', 'Mixes.cs', 'Fixes.cs'];
|
|
219
|
-
for (const f of files) {
|
|
220
|
-
fs.writeFileSync(path.join(subDir, f), `public class ${f.replace('.cs', '')} { }`);
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
try {
|
|
224
|
-
const clusters = discoverClusters(tmpDir, 'dotnet');
|
|
225
|
-
const shortSuffix = clusters.find(c => c.suffix === 'es' || c.suffix.length < 6);
|
|
226
|
-
assert.ok(!shortSuffix, 'should NOT detect short suffix clusters (< 6 chars)');
|
|
227
|
-
} finally {
|
|
228
|
-
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
229
|
-
}
|
|
230
|
-
});
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
".cs": "csharp",
|
|
3
|
-
".ts": "typescript",
|
|
4
|
-
".tsx": "typescript",
|
|
5
|
-
".js": "javascript",
|
|
6
|
-
".jsx": "javascript",
|
|
7
|
-
".mjs": "javascript",
|
|
8
|
-
".dart": "dart",
|
|
9
|
-
".py": "python",
|
|
10
|
-
".java": "java",
|
|
11
|
-
".go": "go",
|
|
12
|
-
".rs": "rust",
|
|
13
|
-
".rb": "ruby",
|
|
14
|
-
".php": "php",
|
|
15
|
-
".kt": "kotlin",
|
|
16
|
-
".kts": "kotlin",
|
|
17
|
-
".ex": "elixir",
|
|
18
|
-
".exs": "elixir",
|
|
19
|
-
".swift": "swift",
|
|
20
|
-
".elm": "elm",
|
|
21
|
-
".zig": "zig",
|
|
22
|
-
".cr": "crystal",
|
|
23
|
-
".gleam": "gleam",
|
|
24
|
-
".erl": "erlang",
|
|
25
|
-
".hrl": "erlang",
|
|
26
|
-
".scala": "scala",
|
|
27
|
-
".clj": "clojure",
|
|
28
|
-
".cljs": "clojure",
|
|
29
|
-
".hs": "haskell",
|
|
30
|
-
".ml": "ocaml",
|
|
31
|
-
".mli": "ocaml",
|
|
32
|
-
".lua": "lua",
|
|
33
|
-
".nim": "nim",
|
|
34
|
-
".v": "v",
|
|
35
|
-
".rkt": "racket",
|
|
36
|
-
".jl": "julia",
|
|
37
|
-
".r": "r",
|
|
38
|
-
".R": "r",
|
|
39
|
-
".sh": "bash",
|
|
40
|
-
".bash": "bash",
|
|
41
|
-
".zsh": "bash",
|
|
42
|
-
".ps1": "powershell",
|
|
43
|
-
".sql": "sql",
|
|
44
|
-
".proto": "protobuf",
|
|
45
|
-
".tf": "terraform",
|
|
46
|
-
".yaml": "yaml",
|
|
47
|
-
".yml": "yaml",
|
|
48
|
-
".toml": "toml",
|
|
49
|
-
".json": "json",
|
|
50
|
-
".xml": "xml",
|
|
51
|
-
".html": "html",
|
|
52
|
-
".css": "css",
|
|
53
|
-
".scss": "scss",
|
|
54
|
-
".sass": "sass",
|
|
55
|
-
".vue": "vue",
|
|
56
|
-
".svelte":"svelte"
|
|
57
|
-
}
|