@rune-kit/rune 2.10.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 (240) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +65 -6
  3. package/commands/rune.md +168 -168
  4. package/compiler/__tests__/detect-invariants.test.js +136 -0
  5. package/compiler/__tests__/doctor-mesh.test.js +229 -0
  6. package/compiler/__tests__/hook-dispatch.test.js +91 -0
  7. package/compiler/__tests__/hooks-antigravity.test.js +118 -0
  8. package/compiler/__tests__/hooks-cursor.test.js +139 -0
  9. package/compiler/__tests__/hooks-install.test.js +305 -0
  10. package/compiler/__tests__/hooks-merge.test.js +204 -0
  11. package/compiler/__tests__/hooks-tiers.test.js +519 -0
  12. package/compiler/__tests__/hooks-windsurf.test.js +115 -0
  13. package/compiler/__tests__/inject-claude-md.test.js +152 -0
  14. package/compiler/__tests__/load-invariants.test.js +408 -0
  15. package/compiler/__tests__/onboard-invariants.test.js +240 -0
  16. package/compiler/adapters/hooks/antigravity.js +140 -0
  17. package/compiler/adapters/hooks/claude.js +166 -0
  18. package/compiler/adapters/hooks/cursor.js +191 -0
  19. package/compiler/adapters/hooks/index.js +82 -0
  20. package/compiler/adapters/hooks/tier-emitter.js +182 -0
  21. package/compiler/adapters/hooks/windsurf.js +202 -0
  22. package/compiler/bin/rune.js +196 -6
  23. package/compiler/commands/hook-dispatch.js +87 -0
  24. package/compiler/commands/hooks/install.js +120 -0
  25. package/compiler/commands/hooks/merge.js +211 -0
  26. package/compiler/commands/hooks/presets.js +116 -0
  27. package/compiler/commands/hooks/status.js +112 -0
  28. package/compiler/commands/hooks/tiers.js +221 -0
  29. package/compiler/commands/hooks/uninstall.js +94 -0
  30. package/compiler/doctor.js +236 -0
  31. package/contexts/dev.md +34 -34
  32. package/contexts/research.md +43 -43
  33. package/contexts/review.md +55 -55
  34. package/extensions/ai-ml/PACK.md +88 -88
  35. package/extensions/ai-ml/skills/ai-agents.md +172 -172
  36. package/extensions/ai-ml/skills/code-sandbox.md +187 -187
  37. package/extensions/ai-ml/skills/deep-research.md +146 -146
  38. package/extensions/ai-ml/skills/embedding-search.md +66 -66
  39. package/extensions/ai-ml/skills/fine-tuning-guide.md +74 -74
  40. package/extensions/ai-ml/skills/llm-architect.md +125 -125
  41. package/extensions/ai-ml/skills/llm-integration.md +64 -64
  42. package/extensions/ai-ml/skills/prompt-patterns.md +72 -72
  43. package/extensions/ai-ml/skills/rag-patterns.md +66 -66
  44. package/extensions/ai-ml/skills/web-extraction.md +114 -114
  45. package/extensions/analytics/PACK.md +92 -92
  46. package/extensions/analytics/skills/ab-testing.md +72 -72
  47. package/extensions/analytics/skills/dashboard-patterns.md +83 -83
  48. package/extensions/analytics/skills/data-validation.md +68 -68
  49. package/extensions/analytics/skills/funnel-analysis.md +81 -81
  50. package/extensions/analytics/skills/sql-patterns.md +57 -57
  51. package/extensions/analytics/skills/statistical-analysis.md +79 -79
  52. package/extensions/analytics/skills/tracking-setup.md +71 -71
  53. package/extensions/backend/PACK.md +104 -104
  54. package/extensions/backend/skills/api-patterns.md +84 -84
  55. package/extensions/backend/skills/async-pipeline.md +193 -193
  56. package/extensions/backend/skills/auth-patterns.md +97 -97
  57. package/extensions/backend/skills/background-jobs.md +133 -133
  58. package/extensions/backend/skills/caching-patterns.md +108 -108
  59. package/extensions/backend/skills/cli-generation.md +133 -133
  60. package/extensions/backend/skills/database-patterns.md +87 -87
  61. package/extensions/backend/skills/middleware-patterns.md +104 -104
  62. package/extensions/chrome-ext/PACK.md +93 -93
  63. package/extensions/chrome-ext/skills/cws-preflight.md +143 -143
  64. package/extensions/chrome-ext/skills/cws-publish.md +104 -104
  65. package/extensions/chrome-ext/skills/ext-ai-integration.md +251 -251
  66. package/extensions/chrome-ext/skills/ext-messaging.md +139 -139
  67. package/extensions/chrome-ext/skills/ext-storage.md +133 -133
  68. package/extensions/chrome-ext/skills/mv3-scaffold.md +164 -164
  69. package/extensions/content/PACK.md +96 -96
  70. package/extensions/content/skills/blog-patterns.md +88 -88
  71. package/extensions/content/skills/cms-integration.md +131 -131
  72. package/extensions/content/skills/content-scoring.md +107 -107
  73. package/extensions/content/skills/i18n.md +83 -83
  74. package/extensions/content/skills/mdx-authoring.md +137 -137
  75. package/extensions/content/skills/reference.md +1014 -1014
  76. package/extensions/content/skills/seo-patterns.md +67 -67
  77. package/extensions/content/skills/video-repurpose.md +153 -153
  78. package/extensions/devops/PACK.md +101 -101
  79. package/extensions/devops/skills/chaos-testing.md +67 -67
  80. package/extensions/devops/skills/ci-cd.md +75 -75
  81. package/extensions/devops/skills/docker.md +58 -58
  82. package/extensions/devops/skills/edge-serverless.md +163 -163
  83. package/extensions/devops/skills/infra-as-code.md +158 -158
  84. package/extensions/devops/skills/kubernetes.md +110 -110
  85. package/extensions/devops/skills/monitoring.md +57 -57
  86. package/extensions/devops/skills/server-setup.md +64 -64
  87. package/extensions/devops/skills/ssl-domain.md +42 -42
  88. package/extensions/ecommerce/PACK.md +116 -116
  89. package/extensions/ecommerce/skills/cart-system.md +79 -79
  90. package/extensions/ecommerce/skills/inventory-mgmt.md +102 -102
  91. package/extensions/ecommerce/skills/order-management.md +126 -126
  92. package/extensions/ecommerce/skills/payment-integration.md +472 -472
  93. package/extensions/ecommerce/skills/shopify-dev.md +69 -69
  94. package/extensions/ecommerce/skills/subscription-billing.md +93 -93
  95. package/extensions/ecommerce/skills/tax-compliance.md +117 -117
  96. package/extensions/gamedev/PACK.md +142 -142
  97. package/extensions/gamedev/skills/asset-pipeline.md +74 -74
  98. package/extensions/gamedev/skills/audio-system.md +129 -129
  99. package/extensions/gamedev/skills/camera-system.md +87 -87
  100. package/extensions/gamedev/skills/ecs.md +98 -98
  101. package/extensions/gamedev/skills/game-loops.md +72 -72
  102. package/extensions/gamedev/skills/input-system.md +199 -199
  103. package/extensions/gamedev/skills/multiplayer.md +180 -180
  104. package/extensions/gamedev/skills/particles.md +105 -105
  105. package/extensions/gamedev/skills/physics-engine.md +89 -89
  106. package/extensions/gamedev/skills/scene-management.md +146 -146
  107. package/extensions/gamedev/skills/threejs-patterns.md +90 -90
  108. package/extensions/gamedev/skills/webgl.md +71 -71
  109. package/extensions/mobile/PACK.md +106 -106
  110. package/extensions/mobile/skills/app-store-connect.md +152 -152
  111. package/extensions/mobile/skills/app-store-prep.md +66 -66
  112. package/extensions/mobile/skills/deep-linking.md +109 -109
  113. package/extensions/mobile/skills/flutter.md +60 -60
  114. package/extensions/mobile/skills/ios-build-pipeline.md +142 -142
  115. package/extensions/mobile/skills/native-bridge.md +66 -66
  116. package/extensions/mobile/skills/ota-updates.md +97 -97
  117. package/extensions/mobile/skills/push-notifications.md +111 -111
  118. package/extensions/mobile/skills/react-native.md +82 -82
  119. package/extensions/saas/PACK.md +116 -116
  120. package/extensions/saas/skills/billing-integration.md +200 -200
  121. package/extensions/saas/skills/feature-flags.md +130 -130
  122. package/extensions/saas/skills/multi-tenant.md +103 -103
  123. package/extensions/saas/skills/onboarding-flow.md +139 -139
  124. package/extensions/saas/skills/subscription-flow.md +95 -95
  125. package/extensions/saas/skills/team-management.md +144 -144
  126. package/extensions/security/PACK.md +99 -99
  127. package/extensions/security/skills/api-security.md +140 -140
  128. package/extensions/security/skills/compliance.md +68 -68
  129. package/extensions/security/skills/owasp-audit.md +64 -64
  130. package/extensions/security/skills/pentest-patterns.md +77 -77
  131. package/extensions/security/skills/secret-mgmt.md +65 -65
  132. package/extensions/security/skills/supply-chain.md +65 -65
  133. package/extensions/trading/PACK.md +80 -80
  134. package/extensions/trading/skills/chart-components.md +55 -55
  135. package/extensions/trading/skills/experiment-loop.md +125 -125
  136. package/extensions/trading/skills/fintech-patterns.md +47 -47
  137. package/extensions/trading/skills/indicator-library.md +58 -58
  138. package/extensions/trading/skills/quant-analysis.md +111 -111
  139. package/extensions/trading/skills/realtime-data.md +58 -58
  140. package/extensions/trading/skills/trade-logic.md +104 -104
  141. package/extensions/ui/PACK.md +130 -130
  142. package/extensions/ui/skills/a11y-audit.md +91 -91
  143. package/extensions/ui/skills/animation-patterns.md +127 -127
  144. package/extensions/ui/skills/component-patterns.md +100 -100
  145. package/extensions/ui/skills/design-decision.md +108 -108
  146. package/extensions/ui/skills/design-system.md +68 -68
  147. package/extensions/ui/skills/landing-patterns.md +155 -155
  148. package/extensions/ui/skills/palette-picker.md +173 -173
  149. package/extensions/ui/skills/react-health.md +90 -90
  150. package/extensions/ui/skills/type-system.md +125 -125
  151. package/extensions/ui/skills/web-vitals.md +153 -153
  152. package/extensions/zalo/PACK.md +145 -145
  153. package/extensions/zalo/skills/zalo-oa-mcp.md +317 -317
  154. package/extensions/zalo/skills/zalo-oa-messaging.md +429 -429
  155. package/extensions/zalo/skills/zalo-oa-setup.md +236 -236
  156. package/extensions/zalo/skills/zalo-oa-webhook.md +189 -189
  157. package/extensions/zalo/skills/zalo-personal-messaging.md +194 -194
  158. package/extensions/zalo/skills/zalo-personal-setup.md +153 -153
  159. package/extensions/zalo/skills/zalo-rate-guard.md +219 -219
  160. package/hooks/auto-format/index.cjs +48 -48
  161. package/hooks/hooks.json +111 -111
  162. package/hooks/post-session-reflect/index.cjs +189 -189
  163. package/hooks/pre-compact/index.cjs +95 -95
  164. package/hooks/run-hook.cmd +1 -1
  165. package/hooks/secrets-scan/index.cjs +100 -100
  166. package/hooks/session-start/index.cjs +71 -71
  167. package/hooks/typecheck/index.cjs +65 -65
  168. package/package.json +63 -63
  169. package/references/ui-pro-max-data/LICENSE-UI-PRO-MAX +21 -21
  170. package/references/ui-pro-max-data/charts.csv +26 -26
  171. package/references/ui-pro-max-data/colors.csv +161 -161
  172. package/references/ui-pro-max-data/styles.csv +68 -68
  173. package/references/ui-pro-max-data/typography.csv +74 -74
  174. package/references/ui-pro-max-data/ui-reasoning.csv +162 -162
  175. package/references/ui-pro-max-data/ux-guidelines.csv +99 -99
  176. package/skills/adversary/SKILL.md +283 -283
  177. package/skills/asset-creator/SKILL.md +157 -157
  178. package/skills/audit/SKILL.md +147 -2
  179. package/skills/autopsy/SKILL.md +335 -335
  180. package/skills/ba/SKILL.md +85 -1
  181. package/skills/brainstorm/SKILL.md +380 -342
  182. package/skills/browser-pilot/SKILL.md +169 -168
  183. package/skills/constraint-check/SKILL.md +165 -165
  184. package/skills/context-engine/SKILL.md +408 -404
  185. package/skills/cook/SKILL.md +917 -863
  186. package/skills/db/SKILL.md +273 -273
  187. package/skills/debug/SKILL.md +465 -465
  188. package/skills/dependency-doctor/SKILL.md +265 -235
  189. package/skills/deploy/SKILL.md +274 -231
  190. package/skills/design/DESIGN-REFERENCE.md +365 -365
  191. package/skills/design/SKILL.md +590 -589
  192. package/skills/doc-processor/SKILL.md +254 -254
  193. package/skills/docs/SKILL.md +374 -374
  194. package/skills/docs-seeker/SKILL.md +178 -177
  195. package/skills/fix/SKILL.md +332 -330
  196. package/skills/git/SKILL.md +339 -339
  197. package/skills/hallucination-guard/SKILL.md +220 -219
  198. package/skills/incident/SKILL.md +254 -253
  199. package/skills/integrity-check/SKILL.md +169 -169
  200. package/skills/journal/SKILL.md +241 -240
  201. package/skills/launch/SKILL.md +344 -344
  202. package/skills/logic-guardian/SKILL.md +269 -251
  203. package/skills/marketing/SKILL.md +351 -289
  204. package/skills/mcp-builder/SKILL.md +425 -425
  205. package/skills/neural-memory/SKILL.md +359 -362
  206. package/skills/onboard/SKILL.md +432 -403
  207. package/skills/onboard/references/invariants-template.md +76 -0
  208. package/skills/onboard/scripts/detect-invariants.js +439 -0
  209. package/skills/onboard/scripts/inject-claude-md.js +150 -0
  210. package/skills/onboard/scripts/onboard-invariants.js +194 -0
  211. package/skills/perf/SKILL.md +347 -346
  212. package/skills/plan/SKILL.md +435 -428
  213. package/skills/preflight/SKILL.md +415 -415
  214. package/skills/problem-solver/SKILL.md +380 -284
  215. package/skills/rescue/SKILL.md +474 -474
  216. package/skills/research/SKILL.md +4 -0
  217. package/skills/retro/SKILL.md +3 -1
  218. package/skills/review/SKILL.md +614 -588
  219. package/skills/review-intake/SKILL.md +249 -249
  220. package/skills/safeguard/SKILL.md +200 -200
  221. package/skills/sast/SKILL.md +190 -190
  222. package/skills/scaffold/SKILL.md +328 -287
  223. package/skills/scope-guard/SKILL.md +183 -180
  224. package/skills/scout/SKILL.md +269 -263
  225. package/skills/sentinel/SKILL.md +384 -381
  226. package/skills/sentinel-env/SKILL.md +254 -254
  227. package/skills/sequential-thinking/SKILL.md +234 -234
  228. package/skills/session-bridge/SKILL.md +595 -543
  229. package/skills/session-bridge/scripts/load-invariants.js +397 -0
  230. package/skills/skill-forge/SKILL.md +581 -581
  231. package/skills/skill-router/SKILL.md +3 -0
  232. package/skills/slides/SKILL.md +19 -0
  233. package/skills/surgeon/SKILL.md +215 -215
  234. package/skills/team/SKILL.md +557 -537
  235. package/skills/test/SKILL.md +620 -614
  236. package/skills/trend-scout/SKILL.md +145 -145
  237. package/skills/verification/SKILL.md +334 -326
  238. package/skills/video-creator/SKILL.md +201 -201
  239. package/skills/watchdog/SKILL.md +168 -168
  240. package/skills/worktree/SKILL.md +140 -140
@@ -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
+ });