@rune-kit/rune 2.11.0 → 2.12.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 (60) hide show
  1. package/README.md +58 -1
  2. package/compiler/__tests__/detect-invariants.test.js +136 -0
  3. package/compiler/__tests__/doctor-mesh.test.js +229 -0
  4. package/compiler/__tests__/hook-dispatch.test.js +91 -0
  5. package/compiler/__tests__/hooks-antigravity.test.js +118 -0
  6. package/compiler/__tests__/hooks-cursor.test.js +139 -0
  7. package/compiler/__tests__/hooks-install.test.js +305 -0
  8. package/compiler/__tests__/hooks-merge.test.js +204 -0
  9. package/compiler/__tests__/hooks-tiers.test.js +519 -0
  10. package/compiler/__tests__/hooks-windsurf.test.js +115 -0
  11. package/compiler/__tests__/inject-claude-md.test.js +152 -0
  12. package/compiler/__tests__/load-invariants.test.js +408 -0
  13. package/compiler/__tests__/onboard-invariants.test.js +240 -0
  14. package/compiler/adapters/hooks/antigravity.js +140 -0
  15. package/compiler/adapters/hooks/claude.js +166 -0
  16. package/compiler/adapters/hooks/cursor.js +191 -0
  17. package/compiler/adapters/hooks/index.js +82 -0
  18. package/compiler/adapters/hooks/tier-emitter.js +182 -0
  19. package/compiler/adapters/hooks/windsurf.js +202 -0
  20. package/compiler/bin/rune.js +196 -6
  21. package/compiler/commands/hook-dispatch.js +87 -0
  22. package/compiler/commands/hooks/install.js +120 -0
  23. package/compiler/commands/hooks/merge.js +211 -0
  24. package/compiler/commands/hooks/presets.js +116 -0
  25. package/compiler/commands/hooks/status.js +112 -0
  26. package/compiler/commands/hooks/tiers.js +221 -0
  27. package/compiler/commands/hooks/uninstall.js +94 -0
  28. package/compiler/doctor.js +236 -0
  29. package/package.json +2 -2
  30. package/skills/ba/SKILL.md +85 -1
  31. package/skills/brainstorm/SKILL.md +39 -1
  32. package/skills/browser-pilot/SKILL.md +1 -0
  33. package/skills/context-engine/SKILL.md +6 -2
  34. package/skills/design/SKILL.md +1 -0
  35. package/skills/docs-seeker/SKILL.md +1 -0
  36. package/skills/fix/SKILL.md +4 -2
  37. package/skills/hallucination-guard/SKILL.md +1 -0
  38. package/skills/journal/SKILL.md +1 -0
  39. package/skills/logic-guardian/SKILL.md +22 -4
  40. package/skills/marketing/SKILL.md +62 -1
  41. package/skills/neural-memory/SKILL.md +13 -16
  42. package/skills/onboard/SKILL.md +30 -2
  43. package/skills/onboard/references/invariants-template.md +76 -0
  44. package/skills/onboard/scripts/detect-invariants.js +439 -0
  45. package/skills/onboard/scripts/inject-claude-md.js +150 -0
  46. package/skills/onboard/scripts/onboard-invariants.js +194 -0
  47. package/skills/perf/SKILL.md +1 -0
  48. package/skills/plan/SKILL.md +2 -0
  49. package/skills/preflight/SKILL.md +1 -1
  50. package/skills/research/SKILL.md +4 -0
  51. package/skills/review/SKILL.md +4 -2
  52. package/skills/scope-guard/SKILL.md +4 -1
  53. package/skills/scout/SKILL.md +6 -0
  54. package/skills/sentinel/SKILL.md +2 -0
  55. package/skills/session-bridge/SKILL.md +53 -1
  56. package/skills/session-bridge/scripts/load-invariants.js +397 -0
  57. package/skills/slides/SKILL.md +19 -0
  58. package/skills/team/SKILL.md +2 -1
  59. package/skills/test/SKILL.md +6 -0
  60. package/skills/verification/SKILL.md +8 -0
@@ -0,0 +1,152 @@
1
+ import assert from 'node:assert';
2
+ import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
3
+ import { tmpdir } from 'node:os';
4
+ import path from 'node:path';
5
+ import { describe, test } from 'node:test';
6
+ import {
7
+ applyInvariantsPointer,
8
+ buildPointerBlock,
9
+ injectInvariantsPointer,
10
+ MARKER_END,
11
+ MARKER_START,
12
+ SKIP_DIRECTIVE,
13
+ } from '../../skills/onboard/scripts/inject-claude-md.js';
14
+
15
+ describe('buildPointerBlock', () => {
16
+ test('wraps output in start/end markers', () => {
17
+ const block = buildPointerBlock({ globs: ['src/auth/**'] });
18
+ assert.ok(block.startsWith(MARKER_START));
19
+ assert.ok(block.trimEnd().endsWith(MARKER_END));
20
+ assert.ok(block.includes('src/auth/**'));
21
+ });
22
+
23
+ test('dedupes globs and caps the rendered list', () => {
24
+ const globs = Array.from({ length: 20 }, (_, i) => `src/mod${i}/**`);
25
+ globs.push('src/mod0/**');
26
+ const block = buildPointerBlock({ globs });
27
+ const rendered = block.split('\n').filter((l) => l.startsWith('- `'));
28
+ assert.strictEqual(rendered.length, 8, `cap at 8 items, got ${rendered.length}`);
29
+ assert.ok(block.includes('…and 12 more'));
30
+ });
31
+
32
+ test('renders placeholder when no globs provided', () => {
33
+ const block = buildPointerBlock({ globs: [] });
34
+ assert.ok(block.includes('No danger zones detected'));
35
+ });
36
+ });
37
+
38
+ describe('injectInvariantsPointer — idempotency', () => {
39
+ test('creates block when none exists', () => {
40
+ const { action, content } = injectInvariantsPointer({
41
+ claudeMd: '# My Project\n\nSome content.\n',
42
+ globs: ['src/core/**'],
43
+ });
44
+ assert.strictEqual(action, 'created');
45
+ assert.ok(content.includes(MARKER_START));
46
+ assert.ok(content.includes('src/core/**'));
47
+ assert.ok(content.startsWith('# My Project'));
48
+ });
49
+
50
+ test('replaces existing block in place', () => {
51
+ const initial = injectInvariantsPointer({
52
+ claudeMd: '# Proj\n',
53
+ globs: ['src/a/**'],
54
+ });
55
+ const updated = injectInvariantsPointer({
56
+ claudeMd: initial.content,
57
+ globs: ['src/b/**'],
58
+ });
59
+ assert.strictEqual(updated.action, 'updated');
60
+ assert.ok(updated.content.includes('src/b/**'));
61
+ assert.ok(!updated.content.includes('src/a/**'), 'old glob must be removed');
62
+ const startCount = (updated.content.match(/@rune-invariants-pointer:start/g) || []).length;
63
+ assert.strictEqual(startCount, 1, 'exactly one start marker must remain');
64
+ });
65
+
66
+ test('reports unchanged when input is identical', () => {
67
+ const first = injectInvariantsPointer({ claudeMd: '', globs: ['src/a/**'] });
68
+ const second = injectInvariantsPointer({ claudeMd: first.content, globs: ['src/a/**'] });
69
+ assert.strictEqual(second.action, 'unchanged');
70
+ });
71
+
72
+ test('preserves content outside the markers verbatim', () => {
73
+ const before = '# Heading\n\nUser text.\n\n## Conventions\n\n- Rule A\n';
74
+ const after = '\n\n## Footer\n\nend.\n';
75
+ const injected = injectInvariantsPointer({
76
+ claudeMd: before + after,
77
+ globs: ['x/**'],
78
+ });
79
+ assert.ok(injected.content.includes('User text.'));
80
+ assert.ok(injected.content.includes('## Footer'));
81
+ assert.ok(injected.content.includes('end.'));
82
+ });
83
+
84
+ test('honors skip directive — does not inject', () => {
85
+ const input = `# Project\n\n${SKIP_DIRECTIVE}\n`;
86
+ const result = injectInvariantsPointer({ claudeMd: input, globs: ['x/**'] });
87
+ assert.strictEqual(result.action, 'skipped');
88
+ assert.strictEqual(result.content, input);
89
+ });
90
+
91
+ test('reports error on marker mismatch (orphan start only)', () => {
92
+ const input = `# Project\n\n${MARKER_START}\nstuff\n`;
93
+ const result = injectInvariantsPointer({ claudeMd: input, globs: ['x/**'] });
94
+ assert.strictEqual(result.action, 'error');
95
+ assert.strictEqual(result.reason, 'marker-mismatch');
96
+ });
97
+ });
98
+
99
+ describe('applyInvariantsPointer — filesystem', () => {
100
+ test('writes file when action is created', async () => {
101
+ const root = await mkdtemp(path.join(tmpdir(), 'rune-inject-'));
102
+ try {
103
+ const claudeMdPath = path.join(root, 'CLAUDE.md');
104
+ await writeFile(claudeMdPath, '# Proj\n', 'utf8');
105
+ const result = await applyInvariantsPointer({
106
+ claudeMdPath,
107
+ globs: ['src/api/**'],
108
+ });
109
+ assert.strictEqual(result.action, 'created');
110
+ const onDisk = await readFile(claudeMdPath, 'utf8');
111
+ assert.ok(onDisk.includes('src/api/**'));
112
+ } finally {
113
+ await rm(root, { recursive: true, force: true });
114
+ }
115
+ });
116
+
117
+ test('dryRun does not write', async () => {
118
+ const root = await mkdtemp(path.join(tmpdir(), 'rune-inject-'));
119
+ try {
120
+ const claudeMdPath = path.join(root, 'CLAUDE.md');
121
+ await writeFile(claudeMdPath, '# Proj\n', 'utf8');
122
+ const result = await applyInvariantsPointer({
123
+ claudeMdPath,
124
+ globs: ['x/**'],
125
+ dryRun: true,
126
+ });
127
+ assert.strictEqual(result.action, 'created');
128
+ const onDisk = await readFile(claudeMdPath, 'utf8');
129
+ assert.ok(!onDisk.includes(MARKER_START), 'disk must not include marker in dry-run');
130
+ } finally {
131
+ await rm(root, { recursive: true, force: true });
132
+ }
133
+ });
134
+
135
+ test('creates CLAUDE.md from scratch when file does not exist', async () => {
136
+ const root = await mkdtemp(path.join(tmpdir(), 'rune-inject-'));
137
+ try {
138
+ const claudeMdPath = path.join(root, 'CLAUDE.md');
139
+ const result = await applyInvariantsPointer({
140
+ claudeMdPath,
141
+ globs: ['src/new/**'],
142
+ });
143
+ assert.strictEqual(result.action, 'created');
144
+ assert.strictEqual(result.existed, false);
145
+ const onDisk = await readFile(claudeMdPath, 'utf8');
146
+ assert.ok(onDisk.includes(MARKER_START));
147
+ assert.ok(onDisk.includes('src/new/**'));
148
+ } finally {
149
+ await rm(root, { recursive: true, force: true });
150
+ }
151
+ });
152
+ });
@@ -0,0 +1,408 @@
1
+ import assert from 'node:assert';
2
+ import { mkdir, mkdtemp, rm, utimes, writeFile } from 'node:fs/promises';
3
+ import { tmpdir } from 'node:os';
4
+ import path from 'node:path';
5
+ import { describe, test } from 'node:test';
6
+ import {
7
+ findMatchingInvariants,
8
+ loadInvariants,
9
+ matchesInvariant,
10
+ parseInvariants,
11
+ renderPreview,
12
+ stripArchived,
13
+ } from '../../skills/session-bridge/scripts/load-invariants.js';
14
+
15
+ const SAMPLE = `# Project Invariants
16
+
17
+ ## Auto-detected (new)
18
+
19
+ ### Danger Zones
20
+
21
+ #### skill-router
22
+ - **WHAT**: L0 router — every action must route through this
23
+ - **WHERE**: \`skills/skill-router/**\`
24
+ - **WHY**: bypass = architectural rot
25
+
26
+ #### cook
27
+ - **WHAT**: L1 orchestrator, 70% of tasks
28
+ - **WHERE**: \`skills/cook/**\`
29
+ - **WHY**: breaking cook breaks the mesh
30
+
31
+ ### Critical Invariants
32
+
33
+ #### parser-IR-schema
34
+ - **WHAT**: IR shape is the contract across all adapters
35
+ - **WHERE**: \`compiler/parser.js\`, \`compiler/adapters/**\`
36
+ - **WHY**: schema drift silently miscompiles every pack
37
+
38
+ ### State Machine Rules
39
+
40
+ #### hook-dispatch-phases
41
+ - **WHAT**: phases fire in order pre → run → post
42
+ - **WHERE**: \`compiler/hooks/dispatch.js\`
43
+ - **WHY**: reordering breaks PostToolUse invariant
44
+
45
+ ### Cross-File Consistency
46
+
47
+ #### pack-manifest-tuple
48
+ - **WHAT**: pack names mirror between marketplace.json + plugin.json
49
+ - **WHERE**: \`.claude-plugin/marketplace.json\`, \`.claude-plugin/plugin.json\`
50
+ - **WHY**: drift breaks install
51
+
52
+ ## Archived
53
+
54
+ ### Danger Zones
55
+
56
+ #### legacy-emitter
57
+ - **WHAT**: old monolith emitter (retired 2026-01)
58
+ - **WHERE**: \`compiler/legacy/**\`
59
+ - **WHY**: kept for reference only — do not reactivate
60
+ `;
61
+
62
+ describe('stripArchived', () => {
63
+ test('removes the Archived section and everything below', () => {
64
+ const out = stripArchived(SAMPLE);
65
+ assert.ok(!out.includes('legacy-emitter'), 'archived rule body removed');
66
+ assert.ok(!out.includes('## Archived'), 'archived heading removed');
67
+ assert.ok(out.includes('skill-router'), 'active rules preserved');
68
+ });
69
+
70
+ test('is a no-op when no Archived section present', () => {
71
+ const text = '# X\n\n## Auto-detected (new)\n\n#### rule\n';
72
+ assert.strictEqual(stripArchived(text), text);
73
+ });
74
+ });
75
+
76
+ describe('parseInvariants', () => {
77
+ test('extracts rules with section + what/where/why', () => {
78
+ const rules = parseInvariants(stripArchived(SAMPLE));
79
+ assert.strictEqual(rules.length, 5);
80
+
81
+ const router = rules.find((r) => r.title === 'skill-router');
82
+ assert.ok(router, 'skill-router rule parsed');
83
+ assert.strictEqual(router.section, 'danger');
84
+ assert.match(router.what, /L0 router/);
85
+ assert.deepStrictEqual(router.where, ['skills/skill-router/**']);
86
+ assert.match(router.why, /architectural rot/);
87
+ });
88
+
89
+ test('multi-glob WHERE splits on backticks', () => {
90
+ const rules = parseInvariants(stripArchived(SAMPLE));
91
+ const parser = rules.find((r) => r.title === 'parser-IR-schema');
92
+ assert.deepStrictEqual(parser.where, ['compiler/parser.js', 'compiler/adapters/**']);
93
+ });
94
+
95
+ test('ignores archived rules (pre-stripped)', () => {
96
+ const rules = parseInvariants(stripArchived(SAMPLE));
97
+ assert.ok(!rules.some((r) => r.title === 'legacy-emitter'));
98
+ });
99
+
100
+ test('tolerates missing WHAT/WHERE/WHY fields', () => {
101
+ const text = '## Danger Zones\n\n#### partial\n- **WHAT**: only what\n';
102
+ const rules = parseInvariants(text);
103
+ assert.strictEqual(rules.length, 1);
104
+ assert.strictEqual(rules[0].why, '');
105
+ assert.deepStrictEqual(rules[0].where, []);
106
+ });
107
+
108
+ test('returns empty array for empty / malformed input', () => {
109
+ assert.deepStrictEqual(parseInvariants(''), []);
110
+ assert.deepStrictEqual(parseInvariants('just some prose with no headers'), []);
111
+ });
112
+ });
113
+
114
+ describe('renderPreview', () => {
115
+ test('orders rules by priority (danger → critical → state → cross)', () => {
116
+ const rules = parseInvariants(stripArchived(SAMPLE));
117
+ const { preview } = renderPreview(rules, { budgetTokens: 500 });
118
+ const dangerIdx = preview.indexOf('skills/skill-router/**');
119
+ const criticalIdx = preview.indexOf('compiler/parser.js');
120
+ const stateIdx = preview.indexOf('compiler/hooks/dispatch.js');
121
+ const crossIdx = preview.indexOf('marketplace.json');
122
+ assert.ok(dangerIdx > 0 && dangerIdx < criticalIdx, 'danger before critical');
123
+ assert.ok(criticalIdx < stateIdx, 'critical before state');
124
+ assert.ok(stateIdx < crossIdx, 'state before cross');
125
+ });
126
+
127
+ test('includes section icons', () => {
128
+ const rules = parseInvariants(stripArchived(SAMPLE));
129
+ const { preview } = renderPreview(rules, { budgetTokens: 500 });
130
+ assert.match(preview, /⚠/);
131
+ assert.match(preview, /🔒/);
132
+ assert.match(preview, /🔁/);
133
+ assert.match(preview, /🔗/);
134
+ });
135
+
136
+ test('caps output at budget and reports overflow', () => {
137
+ const rules = parseInvariants(stripArchived(SAMPLE));
138
+ const { preview, overflow } = renderPreview(rules, { budgetTokens: 30 });
139
+ assert.ok(overflow >= 1, 'overflow reported when budget exhausted');
140
+ assert.match(preview, /\+\d+ more rule/);
141
+ });
142
+
143
+ test('empty rules → empty preview, zero overflow', () => {
144
+ const { preview, overflow } = renderPreview([], { budgetTokens: 500 });
145
+ assert.strictEqual(preview, '');
146
+ assert.strictEqual(overflow, 0);
147
+ });
148
+ });
149
+
150
+ describe('loadInvariants — end-to-end', () => {
151
+ test('silent no-op when .rune/INVARIANTS.md is missing', async () => {
152
+ const root = await mkdtemp(path.join(tmpdir(), 'rune-load-inv-missing-'));
153
+ try {
154
+ const result = await loadInvariants({ root });
155
+ assert.strictEqual(result.loaded, false);
156
+ assert.strictEqual(result.rules.length, 0);
157
+ assert.strictEqual(result.preview, '');
158
+ assert.strictEqual(result.stale, false);
159
+ } finally {
160
+ await rm(root, { recursive: true, force: true });
161
+ }
162
+ });
163
+
164
+ test('loads + parses + renders when file present', async () => {
165
+ const root = await mkdtemp(path.join(tmpdir(), 'rune-load-inv-'));
166
+ try {
167
+ await mkdir(path.join(root, '.rune'), { recursive: true });
168
+ await writeFile(path.join(root, '.rune', 'INVARIANTS.md'), SAMPLE, 'utf8');
169
+
170
+ const result = await loadInvariants({ root });
171
+ assert.strictEqual(result.loaded, true);
172
+ assert.strictEqual(result.stats.total, 5);
173
+ assert.strictEqual(result.stats.danger, 2);
174
+ assert.strictEqual(result.stats.critical, 1);
175
+ assert.strictEqual(result.stats.state, 1);
176
+ assert.strictEqual(result.stats.cross, 1);
177
+ assert.ok(result.stats.archivedSkipped, 'archived section noted as stripped');
178
+ assert.match(result.preview, /Active Invariants/);
179
+ } finally {
180
+ await rm(root, { recursive: true, force: true });
181
+ }
182
+ });
183
+
184
+ test('flags staleness when mtime older than 30 days', async () => {
185
+ const root = await mkdtemp(path.join(tmpdir(), 'rune-load-inv-stale-'));
186
+ try {
187
+ await mkdir(path.join(root, '.rune'), { recursive: true });
188
+ const file = path.join(root, '.rune', 'INVARIANTS.md');
189
+ await writeFile(file, SAMPLE, 'utf8');
190
+ const old = new Date(Date.now() - 45 * 24 * 60 * 60 * 1000);
191
+ await utimes(file, old, old);
192
+
193
+ const result = await loadInvariants({ root });
194
+ assert.strictEqual(result.stale, true);
195
+ } finally {
196
+ await rm(root, { recursive: true, force: true });
197
+ }
198
+ });
199
+
200
+ test('fresh file is not flagged stale', async () => {
201
+ const root = await mkdtemp(path.join(tmpdir(), 'rune-load-inv-fresh-'));
202
+ try {
203
+ await mkdir(path.join(root, '.rune'), { recursive: true });
204
+ await writeFile(path.join(root, '.rune', 'INVARIANTS.md'), SAMPLE, 'utf8');
205
+ const result = await loadInvariants({ root });
206
+ assert.strictEqual(result.stale, false);
207
+ } finally {
208
+ await rm(root, { recursive: true, force: true });
209
+ }
210
+ });
211
+
212
+ test('malformed file → loaded:false, zero rules, does not throw', async () => {
213
+ const root = await mkdtemp(path.join(tmpdir(), 'rune-load-inv-bad-'));
214
+ try {
215
+ await mkdir(path.join(root, '.rune'), { recursive: true });
216
+ await writeFile(path.join(root, '.rune', 'INVARIANTS.md'), 'not markdown in any structured sense', 'utf8');
217
+ const result = await loadInvariants({ root });
218
+ assert.strictEqual(result.loaded, false);
219
+ assert.strictEqual(result.rules.length, 0);
220
+ } finally {
221
+ await rm(root, { recursive: true, force: true });
222
+ }
223
+ });
224
+
225
+ test('throws if root is not provided', async () => {
226
+ await assert.rejects(() => loadInvariants({}), /root is required/);
227
+ });
228
+
229
+ test('signal contract: returns documented payload shape', async () => {
230
+ // Locks F1: session-bridge docs promise `{ loaded, count, rules, stats,
231
+ // stale, overflow, path }` — consumers in logic-guardian + autopilot
232
+ // depend on every field. Any drift must break this test.
233
+ const root = await mkdtemp(path.join(tmpdir(), 'rune-load-inv-contract-'));
234
+ try {
235
+ await mkdir(path.join(root, '.rune'), { recursive: true });
236
+ await writeFile(path.join(root, '.rune', 'INVARIANTS.md'), SAMPLE, 'utf8');
237
+ const result = await loadInvariants({ root });
238
+ assert.deepStrictEqual(Object.keys(result).sort(), [
239
+ 'count',
240
+ 'loaded',
241
+ 'overflow',
242
+ 'path',
243
+ 'preview',
244
+ 'rules',
245
+ 'stale',
246
+ 'stats',
247
+ ]);
248
+ assert.strictEqual(typeof result.loaded, 'boolean');
249
+ assert.strictEqual(typeof result.count, 'number');
250
+ assert.strictEqual(result.count, result.rules.length);
251
+ assert.strictEqual(result.count, result.stats.total);
252
+ assert.ok(Array.isArray(result.rules));
253
+ const sample = result.rules[0];
254
+ assert.deepStrictEqual(Object.keys(sample).sort(), ['section', 'title', 'what', 'where', 'why']);
255
+ assert.ok(Array.isArray(sample.where));
256
+ } finally {
257
+ await rm(root, { recursive: true, force: true });
258
+ }
259
+ });
260
+
261
+ test('archived-only file → loaded:false, archivedSkipped:true', async () => {
262
+ // F7 edge case: a file that contains ONLY archived rules. loaded must be
263
+ // false (no active rules), but archivedSkipped must be true so callers can
264
+ // distinguish "file absent" from "all rules retired".
265
+ const root = await mkdtemp(path.join(tmpdir(), 'rune-load-inv-archived-'));
266
+ try {
267
+ await mkdir(path.join(root, '.rune'), { recursive: true });
268
+ const archivedOnly = [
269
+ '# Project Invariants',
270
+ '',
271
+ '## Archived',
272
+ '',
273
+ '### Danger Zones',
274
+ '',
275
+ '#### retired-rule',
276
+ '- **WHAT**: no longer relevant',
277
+ '- **WHERE**: `legacy/**`',
278
+ '- **WHY**: retired 2026-01',
279
+ '',
280
+ ].join('\n');
281
+ await writeFile(path.join(root, '.rune', 'INVARIANTS.md'), archivedOnly, 'utf8');
282
+ const result = await loadInvariants({ root });
283
+ assert.strictEqual(result.loaded, false);
284
+ assert.strictEqual(result.count, 0);
285
+ assert.strictEqual(result.stats.archivedSkipped, true);
286
+ } finally {
287
+ await rm(root, { recursive: true, force: true });
288
+ }
289
+ });
290
+ });
291
+
292
+ describe('stripArchived — fence-aware (F4)', () => {
293
+ test('`## Archived` inside a fenced code block does NOT terminate active rules', () => {
294
+ const text = [
295
+ '# X',
296
+ '',
297
+ '## Danger Zones',
298
+ '',
299
+ '#### real-rule',
300
+ '- **WHAT**: stay',
301
+ '- **WHERE**: `src/**`',
302
+ '- **WHY**: keep me',
303
+ '',
304
+ '```markdown',
305
+ '## Archived',
306
+ 'this is an example not a real section',
307
+ '```',
308
+ '',
309
+ '#### another-real-rule',
310
+ '- **WHAT**: also stay',
311
+ '',
312
+ ].join('\n');
313
+ const stripped = stripArchived(text);
314
+ assert.ok(stripped.includes('real-rule'));
315
+ assert.ok(stripped.includes('another-real-rule'), 'rule after the fenced example must survive');
316
+ assert.ok(stripped.includes('this is an example'), 'fence body preserved');
317
+ });
318
+
319
+ test('real `## Archived` after a fence still terminates', () => {
320
+ const text = [
321
+ '## Danger Zones',
322
+ '#### r1',
323
+ '- **WHAT**: x',
324
+ '```',
325
+ '## Archived // fake',
326
+ '```',
327
+ '## Archived',
328
+ '#### old',
329
+ '- **WHAT**: retired',
330
+ '',
331
+ ].join('\n');
332
+ const stripped = stripArchived(text);
333
+ assert.ok(stripped.includes('r1'));
334
+ assert.ok(!stripped.includes('#### old'), 'real archived section terminates');
335
+ });
336
+ });
337
+
338
+ describe('parseInvariants — fence-aware (F3)', () => {
339
+ test('`#### fake` inside a fenced code block does NOT create a phantom rule', () => {
340
+ const text = [
341
+ '## Danger Zones',
342
+ '',
343
+ '#### real-rule',
344
+ '- **WHAT**: the WHY demos markdown:',
345
+ '- **WHERE**: `src/**`',
346
+ '- **WHY**: see example below',
347
+ '',
348
+ '```markdown',
349
+ '#### fake-rule-in-example',
350
+ '- **WHAT**: this is documentation',
351
+ '```',
352
+ '',
353
+ '#### second-real-rule',
354
+ '- **WHAT**: after the fence',
355
+ '',
356
+ ].join('\n');
357
+ const rules = parseInvariants(text);
358
+ const titles = rules.map((r) => r.title);
359
+ assert.deepStrictEqual(titles, ['real-rule', 'second-real-rule']);
360
+ assert.ok(!titles.includes('fake-rule-in-example'));
361
+ });
362
+ });
363
+
364
+ describe('matchesInvariant / findMatchingInvariants (F5)', () => {
365
+ const rules = [
366
+ { section: 'danger', title: 'router', what: '', where: ['skills/skill-router/**'], why: '' },
367
+ { section: 'critical', title: 'parser', what: '', where: ['compiler/parser.js', 'compiler/adapters/**'], why: '' },
368
+ { section: 'cross', title: 'no-where', what: '', where: [], why: '' },
369
+ ];
370
+
371
+ test('matches ** recursive glob', () => {
372
+ assert.ok(matchesInvariant('skills/skill-router/foo.js', rules[0]));
373
+ assert.ok(matchesInvariant('skills/skill-router/nested/deep/file.ts', rules[0]));
374
+ assert.ok(!matchesInvariant('skills/cook/foo.js', rules[0]));
375
+ });
376
+
377
+ test('matches literal file path', () => {
378
+ assert.ok(matchesInvariant('compiler/parser.js', rules[1]));
379
+ assert.ok(!matchesInvariant('compiler/parser.ts', rules[1]));
380
+ });
381
+
382
+ test('matches multi-glob where[] (any-of semantics)', () => {
383
+ assert.ok(matchesInvariant('compiler/adapters/claude.js', rules[1]));
384
+ });
385
+
386
+ test('normalizes Windows backslashes before matching', () => {
387
+ assert.ok(matchesInvariant('skills\\skill-router\\foo.js', rules[0]));
388
+ });
389
+
390
+ test('strips leading ./ before matching', () => {
391
+ assert.ok(matchesInvariant('./compiler/parser.js', rules[1]));
392
+ });
393
+
394
+ test('empty where[] never matches', () => {
395
+ assert.ok(!matchesInvariant('anything.js', rules[2]));
396
+ });
397
+
398
+ test('findMatchingInvariants returns all hits in order', () => {
399
+ const hits = findMatchingInvariants('compiler/adapters/claude.js', rules);
400
+ assert.strictEqual(hits.length, 1);
401
+ assert.strictEqual(hits[0].title, 'parser');
402
+ });
403
+
404
+ test('findMatchingInvariants returns empty when nothing matches', () => {
405
+ const hits = findMatchingInvariants('docs/README.md', rules);
406
+ assert.deepStrictEqual(hits, []);
407
+ });
408
+ });