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 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, skill generator
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mustard-claude",
3
- "version": "3.1.26",
3
+ "version": "3.1.27",
4
4
  "description": "Framework-agnostic CLI for Claude Code project setup",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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 (runs automatically at end of skill-generator.js; also callable standalone)
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 sempre os dois comandos de §4.7 (`sync-registry.js --force` + `skill-generator.js --force`), mesmo com registry já v4.0.
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}` (e.g., `api-endpoint-wiring`, `app-mvvm-feature`)
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. Generate Pattern Skills from Registry (OODA: Observe → Act)
345
+ ### 4.7. Refresh Registry
346
346
 
347
- After agent-generated skills (4.6), run the registry-based skill generator to create structural pattern skills from `_patterns`:
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
- This generates skills that the agents in Step 3 may have missedparticularly:
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, not by file:
187
-
188
- | Pattern type | Skill name format | Example |
189
- |-------------|------------------|---------|
190
- | Module/endpoint registration | `{sub}-module-registration` | `api-module-registration` |
191
- | Endpoint wiring | `{sub}-endpoint-wiring` | `api-endpoint-wiring` |
192
- | Repository pattern | `{sub}-repository-pattern` | `api-repository-pattern` |
193
- | Service layer | `{sub}-service-base` | `api-service-base` |
194
- | DTO + validation | `{sub}-dto-validation` | `api-dto-validation` |
195
- | Mapper config | `{sub}-mapster-config` | `api-mapster-config` |
196
- | GraphQL resolver | `{sub}-graphql-resolver` | `api-graphql-resolver` |
197
- | Entity + config | `{sub}-entity-config` | `api-entity-config` |
198
- | Background job | `{sub}-hangfire-job` | `api-hangfire-job` |
199
- | MVVM feature | `{sub}-mvvm-feature` | `app-mvvm-feature` |
200
- | State management | `{sub}-riverpod-state` | `app-riverpod-state` |
201
- | Network/repository | `{sub}-dio-repository` | `app-dio-repository` |
202
- | Routing | `{sub}-go-router` | `app-go-router` |
203
- | Section component | `{sub}-section-component` | `landing-section-component` |
204
- | i18n pattern | `{sub}-i18n-pattern` | `landing-i18n-pattern` |
205
-
206
- Adapt to what the codebase actually has — this table is guidance, not rigid.
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
- {1-2 sentence summary of the pattern.}
248
+ > Pattern detected in this project.
231
249
 
232
- ## Pattern
250
+ ## Convention
233
251
 
234
- {Concise pattern description with file path conventions.}
235
- {Key rules and constraintsexplain WHY, not just WHAT.}
252
+ - Folder: `{detected folder}`
253
+ - {other fields present in the pattern enumerate dynamically, do not assume}
254
+ - Naming: `{detected naming convention}`
236
255
 
237
- ## Example
256
+ ## Real examples in this codebase
238
257
 
239
- {One compact code example (5-10 lines max) showing the happy path.}
240
- Ref: `{path/to/real/file.ext}`
258
+ - `{EntityName}` `{path/to/real/file.ext}`
259
+ - `{OtherEntity}` — `{path/to/other/file.ext}`
241
260
 
242
261
  ## References
243
262
 
244
- For full code examples with variants:
245
- → Read `references/examples.md`
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
- - `skill-generator.js` writes `source: scan` ONLY — never touches `source: manual`.
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-wiring, {sub}-module-registration`
77
- - Mobile screen → `{sub}-mvvm-feature, {sub}-riverpod-state, design-craft`
78
- - Frontend section → `{sub}-section-component, design-craft, react-best-practices`
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
- }
@@ -1,10 +0,0 @@
1
- {
2
- "roles": {
3
- "api": "backend",
4
- "ui": "frontend",
5
- "database": "database",
6
- "mobile": "mobile",
7
- "library": "backend",
8
- "general": "general"
9
- }
10
- }