@rune-kit/rune 2.13.0 → 2.15.0

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.
Files changed (96) hide show
  1. package/README.md +31 -12
  2. package/compiler/__tests__/adapter-model-mapping.test.js +152 -0
  3. package/compiler/__tests__/adr-scoring.test.js +91 -0
  4. package/compiler/__tests__/context-md-format.test.js +180 -0
  5. package/compiler/__tests__/context-pack-smell-tests.test.js +215 -0
  6. package/compiler/__tests__/context-preview-signal.test.js +166 -0
  7. package/compiler/__tests__/diversity-score.test.js +117 -0
  8. package/compiler/__tests__/improve-architecture.test.js +171 -0
  9. package/compiler/__tests__/openclaw-adapter.test.js +11 -6
  10. package/compiler/__tests__/oracle-bundle-format.test.js +210 -0
  11. package/compiler/__tests__/oracle-pending-schema.test.js +209 -0
  12. package/compiler/__tests__/out-of-scope-format.test.js +303 -0
  13. package/compiler/__tests__/skill-description-quality.test.js +147 -0
  14. package/compiler/__tests__/zoom-out-output.test.js +112 -0
  15. package/compiler/adapters/antigravity.js +17 -1
  16. package/compiler/adapters/claude.js +4 -0
  17. package/compiler/adapters/codex.js +16 -1
  18. package/compiler/adapters/cursor.js +4 -0
  19. package/compiler/adapters/generic.js +14 -1
  20. package/compiler/adapters/openclaw.js +15 -1
  21. package/compiler/adapters/opencode.js +17 -1
  22. package/compiler/adapters/windsurf.js +4 -0
  23. package/package.json +2 -2
  24. package/skills/adversary/SKILL.md +61 -1
  25. package/skills/adversary/evals.md +79 -0
  26. package/skills/adversary/references/context-bundle-format.md +122 -0
  27. package/skills/adversary/references/oracle-mode.md +116 -0
  28. package/skills/asset-creator/SKILL.md +1 -1
  29. package/skills/audit/SKILL.md +3 -1
  30. package/skills/autopsy/SKILL.md +1 -1
  31. package/skills/ba/SKILL.md +242 -83
  32. package/skills/ba/references/context-md-format.md +136 -0
  33. package/skills/ba/references/explore-first.md +107 -0
  34. package/skills/ba/references/out-of-scope-format.md +152 -0
  35. package/skills/brainstorm/SKILL.md +78 -2
  36. package/skills/brainstorm/references/design-it-twice.md +155 -0
  37. package/skills/browser-pilot/SKILL.md +1 -1
  38. package/skills/completion-gate/SKILL.md +1 -1
  39. package/skills/constraint-check/SKILL.md +1 -1
  40. package/skills/context-engine/SKILL.md +85 -1
  41. package/skills/context-engine/references/preview-gate.md +139 -0
  42. package/skills/context-pack/SKILL.md +68 -25
  43. package/skills/context-pack/evals.md +122 -0
  44. package/skills/context-pack/references/brief-template.md +121 -0
  45. package/skills/context-pack/references/durability-rules.md +109 -0
  46. package/skills/cook/SKILL.md +5 -4
  47. package/skills/db/SKILL.md +1 -1
  48. package/skills/debug/SKILL.md +7 -4
  49. package/skills/dependency-doctor/SKILL.md +1 -1
  50. package/skills/design/SKILL.md +1 -1
  51. package/skills/doc-processor/SKILL.md +1 -1
  52. package/skills/docs/SKILL.md +1 -1
  53. package/skills/docs-seeker/SKILL.md +1 -1
  54. package/skills/fix/SKILL.md +5 -3
  55. package/skills/git/SKILL.md +1 -1
  56. package/skills/hallucination-guard/SKILL.md +1 -1
  57. package/skills/improve-architecture/SKILL.md +275 -0
  58. package/skills/improve-architecture/evals.md +145 -0
  59. package/skills/improve-architecture/references/deepening.md +87 -0
  60. package/skills/improve-architecture/references/interface-design.md +68 -0
  61. package/skills/improve-architecture/references/language.md +81 -0
  62. package/skills/improve-architecture/references/scoring.md +122 -0
  63. package/skills/integrity-check/SKILL.md +1 -1
  64. package/skills/journal/SKILL.md +34 -7
  65. package/skills/journal/references/adr-criteria.md +109 -0
  66. package/skills/logic-guardian/SKILL.md +1 -1
  67. package/skills/marketing/SKILL.md +1 -1
  68. package/skills/mcp-builder/SKILL.md +1 -1
  69. package/skills/onboard/SKILL.md +1 -1
  70. package/skills/perf/SKILL.md +1 -1
  71. package/skills/plan/SKILL.md +1 -1
  72. package/skills/preflight/SKILL.md +1 -1
  73. package/skills/research/SKILL.md +1 -1
  74. package/skills/retro/SKILL.md +1 -1
  75. package/skills/review/SKILL.md +3 -2
  76. package/skills/review-intake/SKILL.md +26 -3
  77. package/skills/safeguard/SKILL.md +1 -1
  78. package/skills/scaffold/SKILL.md +1 -1
  79. package/skills/scout/SKILL.md +33 -1
  80. package/skills/sentinel/SKILL.md +1 -1
  81. package/skills/sentinel-env/SKILL.md +1 -1
  82. package/skills/session-bridge/SKILL.md +91 -4
  83. package/skills/session-bridge/references/detach-protocol.md +111 -0
  84. package/skills/skill-forge/SKILL.md +1 -1
  85. package/skills/slides/SKILL.md +1 -1
  86. package/skills/surgeon/SKILL.md +17 -2
  87. package/skills/team/SKILL.md +3 -0
  88. package/skills/test/SKILL.md +48 -1
  89. package/skills/test/evals.md +137 -0
  90. package/skills/test/references/mocking-policy.md +121 -0
  91. package/skills/test/references/test-quality.md +124 -0
  92. package/skills/test/references/vertical-tdd.md +110 -0
  93. package/skills/trend-scout/SKILL.md +1 -1
  94. package/skills/video-creator/SKILL.md +1 -1
  95. package/skills/watchdog/SKILL.md +1 -1
  96. package/skills/worktree/SKILL.md +1 -1
@@ -0,0 +1,215 @@
1
+ /**
2
+ * Context-pack v0.2 smell tests — regex gates that catch rot-prone briefs.
3
+ *
4
+ * BLOCK regex must catch:
5
+ * - file:line references (login.ts:42)
6
+ * - "line N" / "on line N" patterns
7
+ * - Path-only narrative bullets outside Files Touched section
8
+ *
9
+ * WARN regex must catch:
10
+ * - Bare path mentions in narrative (advisory, not blocking)
11
+ *
12
+ * Mandatory sections must be enforced:
13
+ * - ### Out of scope (always)
14
+ * - ### Type Surface (when task >= 300 tokens)
15
+ */
16
+
17
+ import assert from 'node:assert';
18
+ import { describe, test } from 'node:test';
19
+
20
+ // --- Smell-test regex (mirrors durability-rules.md) ---
21
+
22
+ const FILE_LINE_RE = /\b\S+\.[a-z]{1,4}:\d+\b/;
23
+ const LINE_N_RE = /\b(line |on line )\d+\b/i;
24
+ const NARRATIVE_PATH_RE = /\b(src|lib|app)\/\S+/;
25
+
26
+ function runSmellTests(packet) {
27
+ const violations = { block: [], warn: [] };
28
+
29
+ if (FILE_LINE_RE.test(packet)) {
30
+ const m = packet.match(FILE_LINE_RE);
31
+ violations.block.push({ rule: 'file:line', match: m[0] });
32
+ }
33
+
34
+ if (LINE_N_RE.test(packet)) {
35
+ const m = packet.match(LINE_N_RE);
36
+ violations.block.push({ rule: 'line N', match: m[0] });
37
+ }
38
+
39
+ // Narrative path: only WARN if it appears OUTSIDE the Files Touched section.
40
+ const filesTouchedMatch = packet.match(/### Files Touched[\s\S]*?(?=\n### |\n## |$)/);
41
+ const filesTouchedBlock = filesTouchedMatch ? filesTouchedMatch[0] : '';
42
+ // Strip the Files Touched section before scanning
43
+ const narrative = packet.replace(filesTouchedBlock, '');
44
+ if (NARRATIVE_PATH_RE.test(narrative)) {
45
+ const m = narrative.match(NARRATIVE_PATH_RE);
46
+ violations.warn.push({ rule: 'narrative path', match: m[0] });
47
+ }
48
+
49
+ return violations;
50
+ }
51
+
52
+ function checkMandatorySections(packet, taskTokenCount = 0) {
53
+ const issues = [];
54
+ if (!/### Out of scope/.test(packet)) {
55
+ issues.push('missing ### Out of scope section');
56
+ }
57
+ if (taskTokenCount >= 300 && !/### Type Surface/.test(packet)) {
58
+ issues.push('missing ### Type Surface section (mandatory for tasks >= 300 tokens)');
59
+ }
60
+ return issues;
61
+ }
62
+
63
+ // --- Tests ---
64
+
65
+ describe('context-pack smell tests', () => {
66
+ describe('BLOCK tier — file:line references', () => {
67
+ test('catches login.ts:42', () => {
68
+ const packet = '## Context Packet\n\n**Task**: Modify the function at src/auth/login.ts:42';
69
+ const v = runSmellTests(packet);
70
+ assert.strictEqual(v.block.length, 1);
71
+ assert.strictEqual(v.block[0].rule, 'file:line');
72
+ });
73
+
74
+ test('catches handler.py:100', () => {
75
+ const packet = 'See handler.py:100 for the bug';
76
+ const v = runSmellTests(packet);
77
+ assert.strictEqual(v.block.length, 1);
78
+ });
79
+
80
+ test('catches main.go:5', () => {
81
+ const packet = 'In main.go:5 the import is wrong';
82
+ const v = runSmellTests(packet);
83
+ assert.strictEqual(v.block.length, 1);
84
+ });
85
+ });
86
+
87
+ describe('BLOCK tier — line N references', () => {
88
+ test('catches "line 42"', () => {
89
+ const packet = 'The bug is on line 42 of the file';
90
+ const v = runSmellTests(packet);
91
+ assert.strictEqual(v.block.length, 1);
92
+ assert.strictEqual(v.block[0].rule, 'line N');
93
+ });
94
+
95
+ test('catches "on line 100"', () => {
96
+ const packet = 'See on line 100 below';
97
+ const v = runSmellTests(packet);
98
+ assert.strictEqual(v.block.length, 1);
99
+ });
100
+
101
+ test('catches case-insensitive "Line 42"', () => {
102
+ const packet = 'Look at Line 42';
103
+ const v = runSmellTests(packet);
104
+ assert.strictEqual(v.block.length, 1);
105
+ });
106
+ });
107
+
108
+ describe('WARN tier — narrative paths', () => {
109
+ test('warns on narrative `src/auth/`', () => {
110
+ const packet = '## Context Packet\n\nThe work happens in src/auth/handlers';
111
+ const v = runSmellTests(packet);
112
+ assert.strictEqual(v.warn.length, 1);
113
+ });
114
+
115
+ test('does NOT warn when path is only in Files Touched section', () => {
116
+ const packet = `## Context Packet
117
+
118
+ **Task**: Add device tracking
119
+
120
+ ### Files Touched (locator-only)
121
+ - src/auth/login.ts (LoginInput, AuthService.authenticate) — handler
122
+
123
+ ### Out of scope
124
+ - (none)`;
125
+ const v = runSmellTests(packet);
126
+ assert.strictEqual(v.warn.length, 0, 'paths in Files Touched are allowed');
127
+ });
128
+ });
129
+
130
+ describe('clean packets pass', () => {
131
+ test('valid v2 packet has zero violations', () => {
132
+ const packet = `## Context Packet
133
+
134
+ **Task**: Add device_id to login flow
135
+ **Parent**: cook
136
+ **Scope**: AuthService, LoginInput
137
+
138
+ ### Decisions Made
139
+ - Pass through to session storage
140
+
141
+ ### Constraints
142
+ - MUST: preserve backward-compat
143
+ - MUST NOT: change rate-limit window
144
+
145
+ ### Type Surface (durable)
146
+ - LoginInput { email: string; password: string; device_id?: string }
147
+ - AuthService.authenticate(input: LoginInput): Result<Session, AuthError>
148
+
149
+ ### Files Touched (locator-only)
150
+ - src/auth/login.ts (LoginInput, AuthService.authenticate) — route handler
151
+
152
+ ### Acceptance Criteria
153
+ - [ ] Persists device_id when provided
154
+ - [ ] Returns AuthError when credentials invalid
155
+
156
+ ### Out of scope
157
+ - Audit log writer
158
+ `;
159
+ const v = runSmellTests(packet);
160
+ assert.strictEqual(v.block.length, 0, `expected 0 BLOCK, got: ${JSON.stringify(v.block)}`);
161
+ // narrative section uses "AuthService" — no path mention outside Files Touched
162
+ assert.strictEqual(v.warn.length, 0, `expected 0 WARN, got: ${JSON.stringify(v.warn)}`);
163
+ });
164
+ });
165
+ });
166
+
167
+ describe('mandatory section gate', () => {
168
+ test('rejects packet missing Out of scope', () => {
169
+ const packet = `## Context Packet
170
+ **Task**: x
171
+ ### Acceptance Criteria
172
+ - [ ] does y
173
+ `;
174
+ const issues = checkMandatorySections(packet, 100);
175
+ assert.ok(issues.some((i) => i.includes('Out of scope')));
176
+ });
177
+
178
+ test('rejects packet missing Type Surface for large tasks', () => {
179
+ const packet = `## Context Packet
180
+ **Task**: large task
181
+ ### Out of scope
182
+ - (none)
183
+ ### Acceptance Criteria
184
+ - [ ] does y
185
+ `;
186
+ const issues = checkMandatorySections(packet, 500);
187
+ assert.ok(issues.some((i) => i.includes('Type Surface')));
188
+ });
189
+
190
+ test('allows packet without Type Surface for small tasks (<300 tokens)', () => {
191
+ const packet = `## Context Packet
192
+ **Task**: small task
193
+ ### Out of scope
194
+ - (none)
195
+ ### Acceptance Criteria
196
+ - [ ] does y
197
+ `;
198
+ const issues = checkMandatorySections(packet, 200);
199
+ assert.strictEqual(issues.length, 0, `expected pass for small task, got: ${issues.join('\n')}`);
200
+ });
201
+
202
+ test('passes valid packet with all sections', () => {
203
+ const packet = `## Context Packet
204
+ **Task**: task
205
+ ### Type Surface
206
+ - T { x: string }
207
+ ### Out of scope
208
+ - (none)
209
+ ### Acceptance Criteria
210
+ - [ ] does y
211
+ `;
212
+ const issues = checkMandatorySections(packet, 500);
213
+ assert.strictEqual(issues.length, 0);
214
+ });
215
+ });
@@ -0,0 +1,166 @@
1
+ /**
2
+ * Context preview signal (v2.15+) — verify the schema and threshold logic
3
+ * for context-engine's preview-gate mode. Caller emits context.preview before
4
+ * bundling expensive context, gating dispatch on token budget.
5
+ *
6
+ * Validates:
7
+ * - Signal name conforms to dot-notation pattern
8
+ * - Payload schema (caller, estimated_tokens, file_count, top_5, threshold, action)
9
+ * - Threshold action enum: proceed | warn | block
10
+ * - Caller-specific threshold table (adversary 50k/100k, team 30k/80k, etc.)
11
+ * - Token estimate function: chars × 0.25
12
+ * - Decision function: tokens >= block_at → block, >= warn_at → warn, else proceed
13
+ */
14
+
15
+ import assert from 'node:assert';
16
+ import { describe, test } from 'node:test';
17
+
18
+ const SIGNAL_PATTERN = /^[a-z][a-z0-9]*(\.[a-z][a-z0-9]*)+$/;
19
+
20
+ const VALID_CALLERS = new Set(['adversary', 'team', 'review', 'audit']);
21
+ const VALID_ACTIONS = new Set(['proceed', 'warn', 'block']);
22
+
23
+ const DEFAULT_THRESHOLDS = {
24
+ adversary: { warn_at: 50_000, block_at: 100_000 },
25
+ team: { warn_at: 30_000, block_at: 80_000 },
26
+ review: { warn_at: 40_000, block_at: 100_000 },
27
+ audit: { warn_at: 60_000, block_at: 120_000 },
28
+ };
29
+
30
+ function estimateTokens(chars) {
31
+ return Math.floor(chars * 0.25);
32
+ }
33
+
34
+ function decideAction(estimatedTokens, threshold) {
35
+ if (estimatedTokens >= threshold.block_at) return 'block';
36
+ if (estimatedTokens >= threshold.warn_at) return 'warn';
37
+ return 'proceed';
38
+ }
39
+
40
+ function buildPreviewPayload({ caller, files, promptChars = 200 }) {
41
+ if (!VALID_CALLERS.has(caller)) throw new Error(`unknown caller: ${caller}`);
42
+ const threshold = DEFAULT_THRESHOLDS[caller];
43
+ const totalChars = promptChars + files.reduce((sum, f) => sum + f.chars, 0);
44
+ const estimated_tokens = estimateTokens(totalChars);
45
+ const top_5_files_by_size = [...files].sort((a, b) => b.chars - a.chars).slice(0, 5);
46
+ return {
47
+ caller,
48
+ estimated_tokens,
49
+ file_count: files.length,
50
+ top_5_files_by_size,
51
+ threshold,
52
+ action: decideAction(estimated_tokens, threshold),
53
+ };
54
+ }
55
+
56
+ function validatePayload(payload) {
57
+ const errors = [];
58
+ if (!VALID_CALLERS.has(payload.caller)) errors.push('invalid_caller');
59
+ if (typeof payload.estimated_tokens !== 'number' || payload.estimated_tokens < 0) errors.push('invalid_tokens');
60
+ if (typeof payload.file_count !== 'number' || payload.file_count < 0) errors.push('invalid_file_count');
61
+ if (!Array.isArray(payload.top_5_files_by_size) || payload.top_5_files_by_size.length > 5)
62
+ errors.push('invalid_top_5');
63
+ if (typeof payload.threshold?.warn_at !== 'number' || typeof payload.threshold?.block_at !== 'number')
64
+ errors.push('invalid_threshold');
65
+ if (!VALID_ACTIONS.has(payload.action)) errors.push('invalid_action');
66
+ return { valid: errors.length === 0, errors };
67
+ }
68
+
69
+ describe('Signal naming compliance', () => {
70
+ test('context.preview conforms to signal naming pattern', () => {
71
+ assert.match('context.preview', SIGNAL_PATTERN);
72
+ });
73
+ });
74
+
75
+ describe('Payload schema', () => {
76
+ test('valid payload passes', () => {
77
+ const payload = buildPreviewPayload({
78
+ caller: 'adversary',
79
+ files: [{ path: 'a.ts', chars: 1000 }],
80
+ });
81
+ assert.strictEqual(validatePayload(payload).valid, true);
82
+ });
83
+
84
+ test('rejects unknown caller', () => {
85
+ const payload = buildPreviewPayload({
86
+ caller: 'adversary',
87
+ files: [{ path: 'a.ts', chars: 1000 }],
88
+ });
89
+ payload.caller = 'unknown_skill';
90
+ assert.ok(validatePayload(payload).errors.includes('invalid_caller'));
91
+ });
92
+
93
+ test('rejects unknown action', () => {
94
+ const payload = buildPreviewPayload({
95
+ caller: 'adversary',
96
+ files: [{ path: 'a.ts', chars: 1000 }],
97
+ });
98
+ payload.action = 'maybe';
99
+ assert.ok(validatePayload(payload).errors.includes('invalid_action'));
100
+ });
101
+
102
+ test('top_5 capped at 5 entries even with many files', () => {
103
+ const files = Array.from({ length: 12 }, (_, i) => ({ path: `f${i}.ts`, chars: 100 + i * 10 }));
104
+ const payload = buildPreviewPayload({ caller: 'team', files });
105
+ assert.ok(payload.top_5_files_by_size.length <= 5);
106
+ assert.strictEqual(payload.file_count, 12);
107
+ });
108
+ });
109
+
110
+ describe('Token estimation', () => {
111
+ test('chars × 0.25 = tokens', () => {
112
+ assert.strictEqual(estimateTokens(4000), 1000);
113
+ assert.strictEqual(estimateTokens(0), 0);
114
+ assert.strictEqual(estimateTokens(100_000), 25_000);
115
+ });
116
+ });
117
+
118
+ describe('Action decision logic', () => {
119
+ test('proceed when under warn threshold', () => {
120
+ const payload = buildPreviewPayload({
121
+ caller: 'adversary',
122
+ files: [{ path: 'a.ts', chars: 10_000 }],
123
+ });
124
+ assert.strictEqual(payload.action, 'proceed');
125
+ });
126
+
127
+ test('warn when between warn and block', () => {
128
+ const payload = buildPreviewPayload({
129
+ caller: 'adversary',
130
+ files: [{ path: 'a.ts', chars: 280_000 }],
131
+ });
132
+ assert.strictEqual(payload.action, 'warn');
133
+ });
134
+
135
+ test('block when at or over block threshold', () => {
136
+ const payload = buildPreviewPayload({
137
+ caller: 'adversary',
138
+ files: [{ path: 'a.ts', chars: 500_000 }],
139
+ });
140
+ assert.strictEqual(payload.action, 'block');
141
+ });
142
+ });
143
+
144
+ describe('Caller-specific thresholds', () => {
145
+ test('adversary uses 50k/100k', () => {
146
+ assert.deepStrictEqual(DEFAULT_THRESHOLDS.adversary, { warn_at: 50_000, block_at: 100_000 });
147
+ });
148
+
149
+ test('team uses lower per-worker thresholds (30k/80k)', () => {
150
+ assert.deepStrictEqual(DEFAULT_THRESHOLDS.team, { warn_at: 30_000, block_at: 80_000 });
151
+ });
152
+
153
+ test('audit uses higher cross-pack thresholds (60k/120k)', () => {
154
+ assert.deepStrictEqual(DEFAULT_THRESHOLDS.audit, { warn_at: 60_000, block_at: 120_000 });
155
+ });
156
+
157
+ test('same file scope produces different actions per caller', () => {
158
+ const files = [{ path: 'big.ts', chars: 200_000 }]; // ~50k tokens
159
+ const adv = buildPreviewPayload({ caller: 'adversary', files });
160
+ const team = buildPreviewPayload({ caller: 'team', files });
161
+ // Adversary: 50k → exactly at warn_at → warn
162
+ assert.strictEqual(adv.action, 'warn');
163
+ // Team: 50k > block_at (80k worker cap not hit, but...) actually 50k < 80k → warn
164
+ assert.strictEqual(team.action, 'warn');
165
+ });
166
+ });
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Diversity score for Design-It-Twice mode.
3
+ *
4
+ * Computes pairwise Jaccard similarity over feature vectors of N designs.
5
+ * Returns 1 - mean(similarity).
6
+ *
7
+ * Used by brainstorm v0.6 Step 3.5 to gate re-spawning when designs are too similar.
8
+ */
9
+
10
+ import assert from 'node:assert';
11
+ import { describe, test } from 'node:test';
12
+
13
+ function jaccard(a, b) {
14
+ const setA = new Set(a);
15
+ const setB = new Set(b);
16
+ const intersect = new Set([...setA].filter((x) => setB.has(x)));
17
+ const union = new Set([...setA, ...setB]);
18
+ if (union.size === 0) return 1;
19
+ return intersect.size / union.size;
20
+ }
21
+
22
+ function diversityScore(designs) {
23
+ if (designs.length < 2) return 1;
24
+ const pairs = [];
25
+ for (let i = 0; i < designs.length; i++) {
26
+ for (let j = i + 1; j < designs.length; j++) {
27
+ pairs.push(jaccard(designs[i], designs[j]));
28
+ }
29
+ }
30
+ const mean = pairs.reduce((a, b) => a + b, 0) / pairs.length;
31
+ return 1 - mean;
32
+ }
33
+
34
+ // Encoding: a feature vector is a list of feature tokens.
35
+ // Examples:
36
+ // ["methods:1", "returns:1", "adapters:0", "deps:1", "paradigm:minimal", "async:no", "stream:no"]
37
+ // ["methods:8", "returns:5", "adapters:3", "deps:5", "paradigm:extensible", "async:yes", "stream:yes"]
38
+
39
+ describe('diversity score', () => {
40
+ test('identical designs return diversity 0', () => {
41
+ const d1 = ['methods:3', 'paradigm:minimal'];
42
+ const d2 = ['methods:3', 'paradigm:minimal'];
43
+ assert.strictEqual(diversityScore([d1, d2]), 0);
44
+ });
45
+
46
+ test('disjoint designs return diversity 1', () => {
47
+ const d1 = ['methods:3', 'paradigm:minimal'];
48
+ const d2 = ['methods:8', 'paradigm:extensible'];
49
+ assert.strictEqual(diversityScore([d1, d2]), 1);
50
+ });
51
+
52
+ test('half-overlap returns diversity 0.5', () => {
53
+ const d1 = ['methods:3', 'paradigm:minimal'];
54
+ const d2 = ['methods:3', 'paradigm:extensible'];
55
+ // jaccard = 1/3 (intersection {methods:3}, union {methods:3, paradigm:minimal, paradigm:extensible})
56
+ // diversity = 1 - 1/3 = 0.6667
57
+ const diversity = diversityScore([d1, d2]);
58
+ assert.ok(Math.abs(diversity - 0.6667) < 0.001, `expected ~0.6667, got ${diversity}`);
59
+ });
60
+
61
+ test('three radically different designs pass 0.6 floor', () => {
62
+ const c1 = ['methods:2', 'returns:1', 'paradigm:minimal', 'adapters:0', 'async:no'];
63
+ const c2 = ['methods:8', 'returns:5', 'paradigm:extensible', 'adapters:0', 'async:yes'];
64
+ const c4 = ['methods:3', 'returns:2', 'paradigm:ports-adapters', 'adapters:2', 'async:no'];
65
+ const diversity = diversityScore([c1, c2, c4]);
66
+ assert.ok(diversity >= 0.6, `expected >=0.6, got ${diversity}`);
67
+ });
68
+
69
+ test('three near-identical designs fail 0.4 floor', () => {
70
+ const a = ['methods:3', 'returns:1', 'paradigm:minimal'];
71
+ const b = ['methods:3', 'returns:1', 'paradigm:minimal'];
72
+ const c = ['methods:3', 'returns:1', 'paradigm:minimal'];
73
+ const diversity = diversityScore([a, b, c]);
74
+ assert.ok(diversity < 0.4, `expected <0.4, got ${diversity}`);
75
+ });
76
+
77
+ test('marginal designs land in 0.4-0.59 zone (warn tier)', () => {
78
+ // 6/9 features shared between each pair → jaccard ≈ 0.50, diversity ≈ 0.50
79
+ const a = ['m:3', 'r:1', 'p:m', 'a:0', 'q:x', 'e:y'];
80
+ const b = ['m:3', 'r:1', 'p:m', 'a:0', 'q:x', 'e:z'];
81
+ const c = ['m:3', 'r:1', 'p:m', 'a:0', 'q:y', 'e:z'];
82
+ const diversity = diversityScore([a, b, c]);
83
+ assert.ok(diversity >= 0.3 && diversity < 0.6, `expected 0.3-0.59, got ${diversity}`);
84
+ });
85
+
86
+ test('single design returns diversity 1 (degenerate)', () => {
87
+ assert.strictEqual(diversityScore([['x']]), 1);
88
+ });
89
+
90
+ test('four designs (when C4 included) — diverse set passes', () => {
91
+ const c1 = ['methods:2', 'paradigm:minimal'];
92
+ const c2 = ['methods:8', 'paradigm:extensible'];
93
+ const c3 = ['methods:1', 'paradigm:default-light'];
94
+ const c4 = ['methods:3', 'paradigm:ports-adapters'];
95
+ const diversity = diversityScore([c1, c2, c3, c4]);
96
+ assert.ok(diversity >= 0.6, `expected >=0.6, got ${diversity}`);
97
+ });
98
+ });
99
+
100
+ describe('threshold gate', () => {
101
+ test('floor 0.4 — below triggers re-spawn', () => {
102
+ const FLOOR = 0.4;
103
+ const lowDiversity = 0.3;
104
+ assert.strictEqual(lowDiversity < FLOOR, true);
105
+ });
106
+
107
+ test('threshold 0.6 — proceed without warning', () => {
108
+ const THRESHOLD = 0.6;
109
+ const goodDiversity = 0.7;
110
+ assert.strictEqual(goodDiversity >= THRESHOLD, true);
111
+ });
112
+
113
+ test('warn zone 0.4-0.59 — surface to user', () => {
114
+ const value = 0.5;
115
+ assert.ok(value >= 0.4 && value < 0.6);
116
+ });
117
+ });
@@ -0,0 +1,171 @@
1
+ /**
2
+ * improve-architecture skill — structural and vocabulary tests.
3
+ *
4
+ * Validates:
5
+ * - SKILL.md parses, version 0.1.0, layer L2, model opus
6
+ * - Required reference files exist (language, deepening, interface-design, scoring)
7
+ * - evals.md exists with >=4 evals across categories
8
+ * - Vocabulary discipline: skill body + references do NOT use banned aliases
9
+ * - Proposal payload schema shape (sample YAML in SKILL.md parses)
10
+ */
11
+
12
+ import assert from 'node:assert';
13
+ import { existsSync, readFileSync } from 'node:fs';
14
+ import path from 'node:path';
15
+ import { describe, test } from 'node:test';
16
+ import { fileURLToPath } from 'node:url';
17
+ import { parseSkill } from '../parser.js';
18
+
19
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
20
+ const SKILL_DIR = path.resolve(__dirname, '../../skills/improve-architecture');
21
+
22
+ const REQUIRED_FILES = [
23
+ 'SKILL.md',
24
+ 'references/language.md',
25
+ 'references/deepening.md',
26
+ 'references/interface-design.md',
27
+ 'references/scoring.md',
28
+ 'evals.md',
29
+ ];
30
+
31
+ // --- Files exist ---
32
+
33
+ describe('improve-architecture file presence', () => {
34
+ for (const f of REQUIRED_FILES) {
35
+ test(`${f} exists`, () => {
36
+ const p = path.join(SKILL_DIR, f);
37
+ assert.ok(existsSync(p), `missing: ${f}`);
38
+ });
39
+ }
40
+ });
41
+
42
+ // --- SKILL.md parses correctly ---
43
+
44
+ test('SKILL.md frontmatter — name, version, layer, model', () => {
45
+ const skillPath = path.join(SKILL_DIR, 'SKILL.md');
46
+ const parsed = parseSkill(readFileSync(skillPath, 'utf-8'));
47
+ assert.strictEqual(parsed.frontmatter.name, 'improve-architecture');
48
+ assert.strictEqual(parsed.frontmatter.metadata.version, '0.1.0');
49
+ assert.strictEqual(parsed.frontmatter.metadata.layer, 'L2');
50
+ assert.strictEqual(parsed.frontmatter.metadata.model, 'opus');
51
+ });
52
+
53
+ test('SKILL.md emit includes architecture.shallow.flagged + architecture.deletion.passed', () => {
54
+ const skillPath = path.join(SKILL_DIR, 'SKILL.md');
55
+ const content = readFileSync(skillPath, 'utf-8');
56
+ assert.ok(/emit:.*architecture\.shallow\.flagged/.test(content), 'emits architecture.shallow.flagged');
57
+ assert.ok(/emit:.*architecture\.deletion\.passed/.test(content), 'emits architecture.deletion.passed');
58
+ });
59
+
60
+ // --- Required reference content ---
61
+
62
+ test('language.md defines all 8 controlled terms', () => {
63
+ const content = readFileSync(path.join(SKILL_DIR, 'references/language.md'), 'utf-8');
64
+ const required = ['Module', 'Interface', 'Implementation', 'Depth', 'Seam', 'Adapter', 'Leverage', 'Locality'];
65
+ for (const term of required) {
66
+ // Each term should appear as a heading line `### Term`
67
+ assert.ok(new RegExp(`^### ${term}\\b`, 'm').test(content), `language.md missing heading for "${term}"`);
68
+ }
69
+ });
70
+
71
+ test('deepening.md lists all 4 dependency categories', () => {
72
+ const content = readFileSync(path.join(SKILL_DIR, 'references/deepening.md'), 'utf-8');
73
+ for (const cat of ['In-process', 'Local-substitutable', 'Remote-owned', 'True-external']) {
74
+ assert.ok(content.includes(cat), `deepening.md missing dependency category "${cat}"`);
75
+ }
76
+ });
77
+
78
+ test('scoring.md provides numeric rubric for depth/leverage/locality', () => {
79
+ const content = readFileSync(path.join(SKILL_DIR, 'references/scoring.md'), 'utf-8');
80
+ for (const metric of ['## Depth (1–5)', '## Leverage (1–5)', '## Locality (1–5)']) {
81
+ assert.ok(content.includes(metric), `scoring.md missing section "${metric}"`);
82
+ }
83
+ // Deletion test verdict enum
84
+ for (const verdict of ['vanish', 'concentrate', 'redistribute']) {
85
+ assert.ok(content.includes(verdict), `scoring.md missing deletion-test verdict "${verdict}"`);
86
+ }
87
+ });
88
+
89
+ // --- Eval coverage ---
90
+
91
+ test('evals.md has >=4 evals across categories', () => {
92
+ const content = readFileSync(path.join(SKILL_DIR, 'evals.md'), 'utf-8');
93
+ const evalMatches = content.match(/^## Eval: E\d+/gm) || [];
94
+ assert.ok(evalMatches.length >= 4, `expected >=4 evals, found ${evalMatches.length}`);
95
+
96
+ for (const cat of ['happy-path', 'edge-case', 'adversarial', 'jailbreak']) {
97
+ assert.ok(content.includes(cat), `evals.md missing category "${cat}"`);
98
+ }
99
+ });
100
+
101
+ // --- Vocabulary discipline: banned aliases in narrative prose ---
102
+
103
+ describe('vocabulary discipline', () => {
104
+ // Skip narrative scan inside reference files that explicitly enumerate banned terms
105
+ // (language.md lists "Avoid:" rules — those mentions are intentional)
106
+ const SKIPPED_FILES = ['references/language.md'];
107
+
108
+ // Banned alias terms in skill narrative.
109
+ // We allow them inside fenced code blocks AND inside lines starting with "Avoid:" or "Banned"
110
+ // to permit legitimate doctrine prose ("don't say boundary").
111
+ const BANNED_ALIASES = ['boundary', 'component', 'service'];
112
+
113
+ for (const f of REQUIRED_FILES) {
114
+ if (SKIPPED_FILES.includes(f) || f === 'evals.md') continue;
115
+ test(`${f} narrative has no banned aliases`, () => {
116
+ const content = readFileSync(path.join(SKILL_DIR, f), 'utf-8');
117
+ const lines = content.split('\n');
118
+ let inFence = false;
119
+ const violations = [];
120
+ for (let i = 0; i < lines.length; i++) {
121
+ const line = lines[i];
122
+ if (line.trim().startsWith('```')) {
123
+ inFence = !inFence;
124
+ continue;
125
+ }
126
+ if (inFence) continue;
127
+ // Skip lines that explicitly call out banned terms
128
+ const trimmed = line.trim();
129
+ if (
130
+ trimmed.startsWith('*Avoid*') ||
131
+ trimmed.startsWith('Avoid:') ||
132
+ trimmed.startsWith('Banned') ||
133
+ trimmed.startsWith('- **') || // banned-framings list items
134
+ /["“”'].*("|”|')\s*$/.test(trimmed) || // quoted phrases like "service"
135
+ line.includes('banned ') // doctrine sentences mentioning banned X
136
+ ) {
137
+ continue;
138
+ }
139
+ for (const bad of BANNED_ALIASES) {
140
+ // Match whole word, not substring (so "microservice" wouldn't trigger "service" only when it's a standalone word)
141
+ if (new RegExp(`\\b${bad}\\b`, 'i').test(line) && !line.includes(`"${bad}"`)) {
142
+ violations.push(`${f}:${i + 1}: ${line.trim()}`);
143
+ break;
144
+ }
145
+ }
146
+ }
147
+ assert.deepStrictEqual(violations, [], `vocabulary violations in ${f}:\n${violations.join('\n')}`);
148
+ });
149
+ }
150
+ });
151
+
152
+ // --- Proposal payload sample is parseable ---
153
+
154
+ test('SKILL.md sample proposal payload is well-formed YAML-ish', () => {
155
+ const content = readFileSync(path.join(SKILL_DIR, 'SKILL.md'), 'utf-8');
156
+ // Find the architecture.proposal block
157
+ const match = content.match(/architecture\.proposal:[\s\S]*?(?=\n```|\n\n##)/);
158
+ assert.ok(match, 'SKILL.md missing architecture.proposal sample');
159
+ const block = match[0];
160
+ // Must include required fields
161
+ for (const field of [
162
+ 'module_path:',
163
+ 'current:',
164
+ 'target:',
165
+ 'dependency_category:',
166
+ 'suggested_seam:',
167
+ 'adapters_planned:',
168
+ ]) {
169
+ assert.ok(block.includes(field), `proposal payload missing field "${field}"`);
170
+ }
171
+ });