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