@rune-kit/rune 2.13.0 → 2.14.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 (41) hide show
  1. package/README.md +23 -12
  2. package/compiler/__tests__/adr-scoring.test.js +91 -0
  3. package/compiler/__tests__/context-md-format.test.js +180 -0
  4. package/compiler/__tests__/context-pack-smell-tests.test.js +215 -0
  5. package/compiler/__tests__/diversity-score.test.js +117 -0
  6. package/compiler/__tests__/improve-architecture.test.js +171 -0
  7. package/compiler/__tests__/openclaw-adapter.test.js +11 -6
  8. package/compiler/__tests__/out-of-scope-format.test.js +303 -0
  9. package/compiler/__tests__/zoom-out-output.test.js +112 -0
  10. package/package.json +2 -2
  11. package/skills/audit/SKILL.md +1 -0
  12. package/skills/ba/SKILL.md +241 -82
  13. package/skills/ba/references/context-md-format.md +136 -0
  14. package/skills/ba/references/explore-first.md +107 -0
  15. package/skills/ba/references/out-of-scope-format.md +152 -0
  16. package/skills/brainstorm/SKILL.md +77 -1
  17. package/skills/brainstorm/references/design-it-twice.md +155 -0
  18. package/skills/context-pack/SKILL.md +68 -25
  19. package/skills/context-pack/evals.md +122 -0
  20. package/skills/context-pack/references/brief-template.md +121 -0
  21. package/skills/context-pack/references/durability-rules.md +109 -0
  22. package/skills/cook/SKILL.md +4 -4
  23. package/skills/debug/SKILL.md +2 -1
  24. package/skills/fix/SKILL.md +2 -1
  25. package/skills/improve-architecture/SKILL.md +275 -0
  26. package/skills/improve-architecture/evals.md +145 -0
  27. package/skills/improve-architecture/references/deepening.md +87 -0
  28. package/skills/improve-architecture/references/interface-design.md +68 -0
  29. package/skills/improve-architecture/references/language.md +81 -0
  30. package/skills/improve-architecture/references/scoring.md +122 -0
  31. package/skills/journal/SKILL.md +33 -6
  32. package/skills/journal/references/adr-criteria.md +109 -0
  33. package/skills/review/SKILL.md +1 -0
  34. package/skills/review-intake/SKILL.md +25 -2
  35. package/skills/scout/SKILL.md +33 -1
  36. package/skills/surgeon/SKILL.md +16 -1
  37. package/skills/test/SKILL.md +48 -1
  38. package/skills/test/evals.md +137 -0
  39. package/skills/test/references/mocking-policy.md +121 -0
  40. package/skills/test/references/test-quality.md +124 -0
  41. package/skills/test/references/vertical-tdd.md +110 -0
package/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
  <p align="center">
6
6
  <strong>Less skills. Deeper connections.</strong><br>
7
7
  A lean, interconnected skill ecosystem for AI coding assistants.<br>
8
- 62 skills · 215+ mesh connections · 8 platforms · MIT
8
+ 63 skills · 215+ mesh connections · 8 platforms · MIT
9
9
  </p>
10
10
 
11
11
  <p align="center">
@@ -23,7 +23,7 @@
23
23
 
24
24
  Most skill ecosystems are either **too many isolated skills** (540+ that don't talk to each other) or **rigid pipelines** (A → B → C, if B fails everything stops).
25
25
 
26
- Rune is a **mesh** — 62 skills with 215+ connections across a 5-layer architecture. Skills call each other bidirectionally, forming resilient workflows that adapt when things go wrong.
26
+ Rune is a **mesh** — 63 skills with 215+ connections across a 5-layer architecture. Skills call each other bidirectionally, forming resilient workflows that adapt when things go wrong.
27
27
 
28
28
  ```
29
29
  Pipeline: A → B → C → D (B fails = stuck)
@@ -83,13 +83,24 @@ _Methodology: Claude Code CLI headless mode (`claude -p --output-format json`),
83
83
 
84
84
  ---
85
85
 
86
- ## What's New (v2.13.0 — Script Contract + Media Pack)
86
+ ## What's New (v2.14.0 — Deep Modules)
87
87
 
88
- - **`@rune-pro/media` pack v1.0.0** — new Pro pack: raster image generation across 5 providers (Codex CLI, DALL-E, Replicate, Stability AI, local SD), prompt engineering with 4-gate safety check (trademark, public-figure, prompt-injection, uncanny-precondition), batch asset pipeline with multi-resolution variants + WebP/AVIF conversion + EXIF strip. Fills the raster gap left by `asset-creator` (SVG/HTML only) and `video-creator` (planning only).
89
- - **`sentinel-env` v0.3.0** 9-tier binary detection for hard-dependency checks. Catches desktop-app bundles (macOS `.app`), npm-global on Windows (PATH oversight), platform release archives. Eliminates the "binary installed but not detected" failure class.
90
- - **`skill-forge` v1.8.0** — new Phase 5.25 "Script Contract" helper scripts must follow stdout=paths / stderr=diagnostics / `--json` opt-in / `--debug` opt-in pattern, implement semantic exit codes (including `4` timeout-partial vs `124` timeout-zero), honor OpenClaw artifact-directory fallback chain. HARD-GATE on pre-ship verification.
91
- - **OpenClaw adapter** — `generateManifest` now declares `artifactConvention` (output-dir fallback order, output contract, exit-code vocabulary). Formalizes the de-facto in-the-wild convention from [codex-imagen](https://github.com/darkamenosa/codex-imagen).
92
- - **Skill-count scaling** — OpenClaw manifest description now scales with actual skill count rather than hardcoded.
88
+ - **`improve-architecture` skill (NEW L2, opus)** — surfaces deepening opportunities with controlled vocabulary (Module / Interface / Implementation / Depth / Seam / Adapter / Leverage / Locality), numeric depth-leverage-locality scoring (1–5 each), 4 dependency categories that drive test strategy, and structured proposal payloads `surgeon` consumes programmatically. Ban-listed aliases ("boundary", "component", "service", "layer") enforced by compiler test.
89
+ - **TDD vertical-slicing HARD-GATE** — `test` v1.3.0 catches "horizontal slicing" (5 tests written before any GREEN) with a `bulk_test_count <= 1` rule, mandates commit-pair audit trail (`test:` + `feat:` per cycle) verified by `completion-gate`, flags shape-words in test names ("returns", "has property", "is defined") as candidates for behavior-verb rewrite.
90
+ - **`.out-of-scope/` knowledge base** — `ba` v0.11.0 reads it on intake (lexical similarity matching, ≥0.8 confidence surfaces prior rejections), `review-intake` v1.2.0 writes/appends it on every OUT OF SCOPE verdict. Stops the "agent re-litigates a rejected feature next session" failure mode. YAML frontmatter for machine parsing + Markdown body for humans.
91
+ - **CONTEXT.md inline-sharpen + ADR 3-criteria gate** — `ba` builds the project glossary as terms emerge; cross-references user assertions against the codebase via grep before recording. `journal` v0.4.0 only opens an ADR when `reversibility + surprisingness + tradeoff_strength >= 11` AND each axis ≥ 3, with a counter-test (rejected alternative) that can't be faked. Filename includes the score: `ADR-007-postgres-write-model-s13.md`.
92
+ - **Agent Brief durability** — `context-pack` v0.2.0 adds mandatory `### Out of scope` and `### Type Surface (durable)` sections, runs regex smell tests pre-emit (BLOCK on file:line / "line N" / narrative paths; WARN on bare `src/` mentions), enforces behavioral-verb whitelist on Acceptance Criteria. Stops handoff briefs from rotting when files get renamed.
93
+ - **Design-It-Twice mode** — `brainstorm` v0.6.0 spawns 3-4 parallel subagents, each pinned to one constraint (minimize / maximize-flexibility / optimize-common-case / ports-and-adapters), computes diversity score across feature vectors (Jaccard-based, floor 0.4), re-spawns once if too similar, presents sequentially with an opinionated recommendation. "It depends" is BLOCKED.
94
+ - **Zoom-out + explore-first micro-utilities** — `scout` v0.4.0 listens for `agent.stuck` (emitted by `fix` after 2 failed attempts and `debug` after 3 disproved hypotheses), produces a 3-layer Mermaid map (target / siblings / callers, capped at 8 each). `ba` v0.11.0 explore-first HARD-GATE: every elicitation question requires prior tool-call evidence (Read/Glob/Grep) — no more asking "what stack?" when `package.json` answers it.
95
+ - **5 new mesh signals** — `tdd.horizontal.violation`, `architecture.shallow.flagged`, `architecture.deletion.passed`, `outofscope.match`, `agent.stuck`.
96
+ - **1,260 tests** — +81 from v2.13.0 across 7 new test files covering schema validation, vocabulary discipline, ADR scoring, smell-test regex, diversity formula, zoom-out output.
97
+
98
+ ### Previous (v2.13.0 — Script Contract + Media Pack)
99
+
100
+ - **`@rune-pro/media` pack v1.0.0** — new Pro pack: raster image generation across 5 providers (Codex CLI, DALL-E, Replicate, Stability AI, local SD), prompt engineering with 4-gate safety check (trademark, public-figure, prompt-injection, uncanny-precondition), batch asset pipeline with multi-resolution variants + WebP/AVIF conversion + EXIF strip.
101
+ - **`sentinel-env` v0.3.0** — 9-tier binary detection for hard-dependency checks.
102
+ - **`skill-forge` v1.8.0** — new Phase 5.25 "Script Contract" — helper scripts must follow stdout=paths / stderr=diagnostics / `--json` opt-in / semantic exit codes. HARD-GATE on pre-ship verification.
103
+ - **OpenClaw adapter** — `generateManifest` now declares `artifactConvention`.
93
104
 
94
105
  ### Previous (v2.12.0 — Auto-Discipline)
95
106
 
@@ -158,7 +169,7 @@ cook ───emit:phase.complete────→ session-bridge
158
169
 
159
170
  ## What Rune Is (and Isn't)
160
171
 
161
- Rune started as a **Claude Code plugin** and now compiles to **every major AI IDE**. Same 62 skills, same mesh connections, same workflows — zero knowledge loss across platforms.
172
+ Rune started as a **Claude Code plugin** and now compiles to **every major AI IDE**. Same 63 skills, same mesh connections, same workflows — zero knowledge loss across platforms.
162
173
 
163
174
  | | Rune Provides | Claude Code Provides |
164
175
  |---|---|---|
@@ -210,7 +221,7 @@ npx @rune-kit/rune init --platform windsurf
210
221
  npx @rune-kit/rune init --platform antigravity
211
222
  ```
212
223
 
213
- This compiles all 62 skills into your IDE's rules format. Same knowledge, same workflows.
224
+ This compiles all 63 skills into your IDE's rules format. Same knowledge, same workflows.
214
225
 
215
226
  ### Platform Comparison
216
227
 
@@ -561,7 +572,7 @@ See [docs/MULTI-PLATFORM.md](docs/MULTI-PLATFORM.md) for the full architecture.
561
572
  | Doc | What's inside |
562
573
  |-----|---------------|
563
574
  | [`docs/GETTING_STARTED.md`](docs/GETTING_STARTED.md) | Your first 5 minutes with Rune — install to first `/rune cook` |
564
- | [`docs/SKILLS.md`](docs/SKILLS.md) | All 62 skills, searchable by intent and layer |
575
+ | [`docs/SKILLS.md`](docs/SKILLS.md) | All 63 skills, searchable by intent and layer |
565
576
  | [`docs/SIGNALS.md`](docs/SIGNALS.md) | Canonical signal inventory — 25 events, emit/listen graph |
566
577
  | [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) | 5-layer mesh architecture reference |
567
578
  | [`docs/VISION.md`](docs/VISION.md) | Philosophy — what Rune is and isn't |
@@ -577,7 +588,7 @@ See [docs/MULTI-PLATFORM.md](docs/MULTI-PLATFORM.md) for the full architecture.
577
588
  Core Skills: 62 (L0: 1 │ L1: 5 │ L2: 29 │ L3: 27)
578
589
  Extension Packs: 14 free + 5 pro + 4 business
579
590
  Mesh Connections: 215+ cross-references
580
- Mesh Signals: 25 signals across 62 skills (emit/listen graph)
591
+ Mesh Signals: 25 signals across 63 skills (emit/listen graph)
581
592
  Connections/Skill: 3.4 avg
582
593
  Platforms: 8 (Claude Code, Cursor, Windsurf, Antigravity, Codex, OpenCode, OpenClaw, Generic)
583
594
  Compiler: ~1400 LOC (parser + 8 transforms + 8 adapters + CLI)
@@ -0,0 +1,91 @@
1
+ /**
2
+ * ADR scoring gate — verify the 3-criteria threshold logic.
3
+ *
4
+ * The gate: open_adr = (sum >= 11) AND (each axis >= 3)
5
+ * - Prevents single-axis cheating
6
+ * - Counter-test (rejected alternative) is enforced at write time, not numerically
7
+ *
8
+ * Also validates ADR filename pattern includes the score.
9
+ */
10
+
11
+ import assert from 'node:assert';
12
+ import { describe, test } from 'node:test';
13
+
14
+ function shouldOpenAdr({ reversibility, surprisingness, tradeoff_strength }) {
15
+ const sum = reversibility + surprisingness + tradeoff_strength;
16
+ const minAxis = Math.min(reversibility, surprisingness, tradeoff_strength);
17
+ return sum >= 11 && minAxis >= 3;
18
+ }
19
+
20
+ const ADR_FILENAME_PATTERN = /^ADR-\d{3}-[a-z][a-z0-9]*(-[a-z0-9]+)*-s(\d{1,2})\.md$/;
21
+
22
+ describe('ADR scoring gate', () => {
23
+ test('opens ADR when sum >= 11 and all axes >= 3', () => {
24
+ assert.strictEqual(shouldOpenAdr({ reversibility: 4, surprisingness: 4, tradeoff_strength: 3 }), true);
25
+ assert.strictEqual(shouldOpenAdr({ reversibility: 5, surprisingness: 5, tradeoff_strength: 5 }), true);
26
+ assert.strictEqual(shouldOpenAdr({ reversibility: 4, surprisingness: 3, tradeoff_strength: 4 }), true);
27
+ });
28
+
29
+ test('blocks when sum < 11', () => {
30
+ assert.strictEqual(
31
+ shouldOpenAdr({ reversibility: 3, surprisingness: 3, tradeoff_strength: 3 }),
32
+ false,
33
+ 'sum=9 should block',
34
+ );
35
+ assert.strictEqual(
36
+ shouldOpenAdr({ reversibility: 4, surprisingness: 4, tradeoff_strength: 2 }),
37
+ false,
38
+ 'sum=10 should block (also violates min-axis rule)',
39
+ );
40
+ });
41
+
42
+ test('blocks single-axis cheating: 5+5+1 = 11 sum but axis=1 fails min-axis rule', () => {
43
+ assert.strictEqual(shouldOpenAdr({ reversibility: 5, surprisingness: 5, tradeoff_strength: 1 }), false);
44
+ assert.strictEqual(shouldOpenAdr({ reversibility: 5, surprisingness: 1, tradeoff_strength: 5 }), false);
45
+ assert.strictEqual(shouldOpenAdr({ reversibility: 1, surprisingness: 5, tradeoff_strength: 5 }), false);
46
+ });
47
+
48
+ test('boundary: sum=11 with all axes = 3 — Wait, 3+3+3 = 9, not 11. Min sum with each axis >=3 is 9. So sum=11 implies at least one axis is 4', () => {
49
+ // 3+3+5 = 11, all axes >= 3, should open
50
+ assert.strictEqual(shouldOpenAdr({ reversibility: 3, surprisingness: 3, tradeoff_strength: 5 }), true);
51
+ });
52
+
53
+ test('borderline cases (sum 8-10) all block — these go to conventions, not ADRs', () => {
54
+ for (let r = 1; r <= 5; r++) {
55
+ for (let s = 1; s <= 5; s++) {
56
+ for (let t = 1; t <= 5; t++) {
57
+ const sum = r + s + t;
58
+ const result = shouldOpenAdr({ reversibility: r, surprisingness: s, tradeoff_strength: t });
59
+ if (sum >= 8 && sum <= 10) {
60
+ assert.strictEqual(result, false, `sum=${sum} (${r}/${s}/${t}) should block`);
61
+ }
62
+ }
63
+ }
64
+ }
65
+ });
66
+ });
67
+
68
+ describe('ADR filename pattern', () => {
69
+ test('valid filenames with score', () => {
70
+ assert.ok(ADR_FILENAME_PATTERN.test('ADR-007-postgres-write-model-s13.md'));
71
+ assert.ok(ADR_FILENAME_PATTERN.test('ADR-001-monorepo-s12.md'));
72
+ assert.ok(ADR_FILENAME_PATTERN.test('ADR-042-event-sourced-orders-s14.md'));
73
+ });
74
+
75
+ test('rejects filename without score suffix', () => {
76
+ assert.ok(!ADR_FILENAME_PATTERN.test('ADR-007-postgres-write-model.md'));
77
+ });
78
+
79
+ test('rejects non-sequential ADR id format', () => {
80
+ assert.ok(!ADR_FILENAME_PATTERN.test('ADR-7-postgres-s11.md'));
81
+ });
82
+
83
+ test('rejects PascalCase slug', () => {
84
+ assert.ok(!ADR_FILENAME_PATTERN.test('ADR-007-PostgresWriteModel-s13.md'));
85
+ });
86
+
87
+ test('extracts score from filename', () => {
88
+ const m = 'ADR-007-postgres-write-model-s13.md'.match(ADR_FILENAME_PATTERN);
89
+ assert.strictEqual(parseInt(m[2], 10), 13);
90
+ });
91
+ });
@@ -0,0 +1,180 @@
1
+ /**
2
+ * CONTEXT.md format — validates the project glossary structure.
3
+ *
4
+ * Lazy creation rule: empty/absent CONTEXT.md is OK; the validator only runs on existing files.
5
+ *
6
+ * Required sections: # Project Glossary heading, ## Language table, ## Relationships
7
+ * Optional sections: ## Example dialogue, ## Flagged ambiguities
8
+ */
9
+
10
+ import assert from 'node:assert';
11
+ import { describe, test } from 'node:test';
12
+
13
+ function validateContextMd(content) {
14
+ const issues = [];
15
+
16
+ if (!/^# .+/m.test(content.split('\n')[0] || '')) {
17
+ issues.push('first line must be H1 (# Title)');
18
+ }
19
+
20
+ if (!/## Language/.test(content)) {
21
+ issues.push('missing ## Language section');
22
+ }
23
+
24
+ if (!/## Relationships/.test(content)) {
25
+ issues.push('missing ## Relationships section');
26
+ }
27
+
28
+ // Validate Language table shape if present
29
+ const langMatch = content.match(/## Language\s*\n([\s\S]*?)(?=\n## |\n# |$)/);
30
+ if (langMatch) {
31
+ const tableLines = langMatch[1].split('\n').filter((l) => l.trim().startsWith('|'));
32
+ if (tableLines.length < 3) {
33
+ issues.push('## Language section must contain a Markdown table with header + separator + >=1 row');
34
+ } else {
35
+ // Header should mention Term, Definition, Aliases, Status
36
+ const header = tableLines[0].toLowerCase();
37
+ const requiredColumns = [
38
+ { lower: 'term', label: 'Term' },
39
+ { lower: 'definition', label: 'Definition' },
40
+ { lower: 'aliases', label: 'Aliases' },
41
+ { lower: 'status', label: 'Status' },
42
+ ];
43
+ for (const col of requiredColumns) {
44
+ if (!header.includes(col.lower)) {
45
+ issues.push(`Language table header missing column: ${col.label}`);
46
+ }
47
+ }
48
+ // Each row should have a status field with valid value
49
+ const validStatuses = ['canonical', 'pending', 'conflicted'];
50
+ for (let i = 2; i < tableLines.length; i++) {
51
+ const row = tableLines[i];
52
+ const cells = row
53
+ .split('|')
54
+ .map((c) => c.trim())
55
+ .filter(Boolean);
56
+ if (cells.length >= 4) {
57
+ const status = cells[3].toLowerCase();
58
+ if (!validStatuses.includes(status)) {
59
+ issues.push(`row "${cells[0]}" has invalid status "${status}"`);
60
+ }
61
+ }
62
+ }
63
+ }
64
+ }
65
+
66
+ return issues;
67
+ }
68
+
69
+ describe('CONTEXT.md format', () => {
70
+ test('valid context passes', () => {
71
+ const content = `# Project Glossary
72
+
73
+ ## Language
74
+
75
+ | Term | Definition | Aliases to avoid | Status |
76
+ |------|------------|-------------------|--------|
77
+ | **Order** | A customer's request to purchase items | Purchase, transaction | canonical |
78
+ | **Invoice** | A request for payment | Bill | canonical |
79
+
80
+ ## Relationships
81
+
82
+ - An **Order** produces one or more **Invoices**
83
+
84
+ ## Flagged ambiguities
85
+
86
+ - "account" was used for both Customer and User — resolved.
87
+ `;
88
+ assert.deepStrictEqual(validateContextMd(content), []);
89
+ });
90
+
91
+ test('rejects content without H1', () => {
92
+ const content = `## Language
93
+ | Term | Definition | Aliases | Status |
94
+ |------|------------|---------|--------|
95
+ | **X** | y | z | canonical |
96
+ ## Relationships
97
+ - A
98
+ `;
99
+ const issues = validateContextMd(content);
100
+ assert.ok(
101
+ issues.some((i) => i.includes('H1')),
102
+ 'should require H1',
103
+ );
104
+ });
105
+
106
+ test('rejects missing Language section', () => {
107
+ const content = `# Project Glossary
108
+ ## Relationships
109
+ - A
110
+ `;
111
+ const issues = validateContextMd(content);
112
+ assert.ok(
113
+ issues.some((i) => i.includes('Language')),
114
+ 'should require Language section',
115
+ );
116
+ });
117
+
118
+ test('rejects missing Relationships section', () => {
119
+ const content = `# Project Glossary
120
+ ## Language
121
+ | Term | Definition | Aliases | Status |
122
+ |------|------------|---------|--------|
123
+ | **X** | y | z | canonical |
124
+ `;
125
+ const issues = validateContextMd(content);
126
+ assert.ok(
127
+ issues.some((i) => i.includes('Relationships')),
128
+ 'should require Relationships section',
129
+ );
130
+ });
131
+
132
+ test('rejects invalid status value', () => {
133
+ const content = `# Project Glossary
134
+ ## Language
135
+ | Term | Definition | Aliases | Status |
136
+ |------|------------|---------|--------|
137
+ | **Order** | y | z | provisional |
138
+ ## Relationships
139
+ - A
140
+ `;
141
+ const issues = validateContextMd(content);
142
+ assert.ok(
143
+ issues.some((i) => i.includes('invalid status')),
144
+ 'should reject status outside canonical|pending|conflicted',
145
+ );
146
+ });
147
+
148
+ test('accepts pending and conflicted statuses', () => {
149
+ const content = `# Project Glossary
150
+ ## Language
151
+ | Term | Definition | Aliases | Status |
152
+ |------|------------|---------|--------|
153
+ | **Order** | y | z | pending |
154
+ | **Invoice** | y | z | conflicted |
155
+ ## Relationships
156
+ - A
157
+ `;
158
+ assert.deepStrictEqual(validateContextMd(content), []);
159
+ });
160
+
161
+ test('rejects table missing required columns', () => {
162
+ const content = `# Project Glossary
163
+ ## Language
164
+ | Term | Definition |
165
+ |------|------------|
166
+ | **Order** | y |
167
+ ## Relationships
168
+ - A
169
+ `;
170
+ const issues = validateContextMd(content);
171
+ assert.ok(
172
+ issues.some((i) => i.includes('Aliases')),
173
+ 'should require Aliases column',
174
+ );
175
+ assert.ok(
176
+ issues.some((i) => i.includes('Status')),
177
+ 'should require Status column',
178
+ );
179
+ });
180
+ });
@@ -0,0 +1,215 @@
1
+ /**
2
+ * Context-pack v0.2 smell tests — regex gates that catch rot-prone briefs.
3
+ *
4
+ * BLOCK regex must catch:
5
+ * - file:line references (login.ts:42)
6
+ * - "line N" / "on line N" patterns
7
+ * - Path-only narrative bullets outside Files Touched section
8
+ *
9
+ * WARN regex must catch:
10
+ * - Bare path mentions in narrative (advisory, not blocking)
11
+ *
12
+ * Mandatory sections must be enforced:
13
+ * - ### Out of scope (always)
14
+ * - ### Type Surface (when task >= 300 tokens)
15
+ */
16
+
17
+ import assert from 'node:assert';
18
+ import { describe, test } from 'node:test';
19
+
20
+ // --- Smell-test regex (mirrors durability-rules.md) ---
21
+
22
+ const FILE_LINE_RE = /\b\S+\.[a-z]{1,4}:\d+\b/;
23
+ const LINE_N_RE = /\b(line |on line )\d+\b/i;
24
+ const NARRATIVE_PATH_RE = /\b(src|lib|app)\/\S+/;
25
+
26
+ function runSmellTests(packet) {
27
+ const violations = { block: [], warn: [] };
28
+
29
+ if (FILE_LINE_RE.test(packet)) {
30
+ const m = packet.match(FILE_LINE_RE);
31
+ violations.block.push({ rule: 'file:line', match: m[0] });
32
+ }
33
+
34
+ if (LINE_N_RE.test(packet)) {
35
+ const m = packet.match(LINE_N_RE);
36
+ violations.block.push({ rule: 'line N', match: m[0] });
37
+ }
38
+
39
+ // Narrative path: only WARN if it appears OUTSIDE the Files Touched section.
40
+ const filesTouchedMatch = packet.match(/### Files Touched[\s\S]*?(?=\n### |\n## |$)/);
41
+ const filesTouchedBlock = filesTouchedMatch ? filesTouchedMatch[0] : '';
42
+ // Strip the Files Touched section before scanning
43
+ const narrative = packet.replace(filesTouchedBlock, '');
44
+ if (NARRATIVE_PATH_RE.test(narrative)) {
45
+ const m = narrative.match(NARRATIVE_PATH_RE);
46
+ violations.warn.push({ rule: 'narrative path', match: m[0] });
47
+ }
48
+
49
+ return violations;
50
+ }
51
+
52
+ function checkMandatorySections(packet, taskTokenCount = 0) {
53
+ const issues = [];
54
+ if (!/### Out of scope/.test(packet)) {
55
+ issues.push('missing ### Out of scope section');
56
+ }
57
+ if (taskTokenCount >= 300 && !/### Type Surface/.test(packet)) {
58
+ issues.push('missing ### Type Surface section (mandatory for tasks >= 300 tokens)');
59
+ }
60
+ return issues;
61
+ }
62
+
63
+ // --- Tests ---
64
+
65
+ describe('context-pack smell tests', () => {
66
+ describe('BLOCK tier — file:line references', () => {
67
+ test('catches login.ts:42', () => {
68
+ const packet = '## Context Packet\n\n**Task**: Modify the function at src/auth/login.ts:42';
69
+ const v = runSmellTests(packet);
70
+ assert.strictEqual(v.block.length, 1);
71
+ assert.strictEqual(v.block[0].rule, 'file:line');
72
+ });
73
+
74
+ test('catches handler.py:100', () => {
75
+ const packet = 'See handler.py:100 for the bug';
76
+ const v = runSmellTests(packet);
77
+ assert.strictEqual(v.block.length, 1);
78
+ });
79
+
80
+ test('catches main.go:5', () => {
81
+ const packet = 'In main.go:5 the import is wrong';
82
+ const v = runSmellTests(packet);
83
+ assert.strictEqual(v.block.length, 1);
84
+ });
85
+ });
86
+
87
+ describe('BLOCK tier — line N references', () => {
88
+ test('catches "line 42"', () => {
89
+ const packet = 'The bug is on line 42 of the file';
90
+ const v = runSmellTests(packet);
91
+ assert.strictEqual(v.block.length, 1);
92
+ assert.strictEqual(v.block[0].rule, 'line N');
93
+ });
94
+
95
+ test('catches "on line 100"', () => {
96
+ const packet = 'See on line 100 below';
97
+ const v = runSmellTests(packet);
98
+ assert.strictEqual(v.block.length, 1);
99
+ });
100
+
101
+ test('catches case-insensitive "Line 42"', () => {
102
+ const packet = 'Look at Line 42';
103
+ const v = runSmellTests(packet);
104
+ assert.strictEqual(v.block.length, 1);
105
+ });
106
+ });
107
+
108
+ describe('WARN tier — narrative paths', () => {
109
+ test('warns on narrative `src/auth/`', () => {
110
+ const packet = '## Context Packet\n\nThe work happens in src/auth/handlers';
111
+ const v = runSmellTests(packet);
112
+ assert.strictEqual(v.warn.length, 1);
113
+ });
114
+
115
+ test('does NOT warn when path is only in Files Touched section', () => {
116
+ const packet = `## Context Packet
117
+
118
+ **Task**: Add device tracking
119
+
120
+ ### Files Touched (locator-only)
121
+ - src/auth/login.ts (LoginInput, AuthService.authenticate) — handler
122
+
123
+ ### Out of scope
124
+ - (none)`;
125
+ const v = runSmellTests(packet);
126
+ assert.strictEqual(v.warn.length, 0, 'paths in Files Touched are allowed');
127
+ });
128
+ });
129
+
130
+ describe('clean packets pass', () => {
131
+ test('valid v2 packet has zero violations', () => {
132
+ const packet = `## Context Packet
133
+
134
+ **Task**: Add device_id to login flow
135
+ **Parent**: cook
136
+ **Scope**: AuthService, LoginInput
137
+
138
+ ### Decisions Made
139
+ - Pass through to session storage
140
+
141
+ ### Constraints
142
+ - MUST: preserve backward-compat
143
+ - MUST NOT: change rate-limit window
144
+
145
+ ### Type Surface (durable)
146
+ - LoginInput { email: string; password: string; device_id?: string }
147
+ - AuthService.authenticate(input: LoginInput): Result<Session, AuthError>
148
+
149
+ ### Files Touched (locator-only)
150
+ - src/auth/login.ts (LoginInput, AuthService.authenticate) — route handler
151
+
152
+ ### Acceptance Criteria
153
+ - [ ] Persists device_id when provided
154
+ - [ ] Returns AuthError when credentials invalid
155
+
156
+ ### Out of scope
157
+ - Audit log writer
158
+ `;
159
+ const v = runSmellTests(packet);
160
+ assert.strictEqual(v.block.length, 0, `expected 0 BLOCK, got: ${JSON.stringify(v.block)}`);
161
+ // narrative section uses "AuthService" — no path mention outside Files Touched
162
+ assert.strictEqual(v.warn.length, 0, `expected 0 WARN, got: ${JSON.stringify(v.warn)}`);
163
+ });
164
+ });
165
+ });
166
+
167
+ describe('mandatory section gate', () => {
168
+ test('rejects packet missing Out of scope', () => {
169
+ const packet = `## Context Packet
170
+ **Task**: x
171
+ ### Acceptance Criteria
172
+ - [ ] does y
173
+ `;
174
+ const issues = checkMandatorySections(packet, 100);
175
+ assert.ok(issues.some((i) => i.includes('Out of scope')));
176
+ });
177
+
178
+ test('rejects packet missing Type Surface for large tasks', () => {
179
+ const packet = `## Context Packet
180
+ **Task**: large task
181
+ ### Out of scope
182
+ - (none)
183
+ ### Acceptance Criteria
184
+ - [ ] does y
185
+ `;
186
+ const issues = checkMandatorySections(packet, 500);
187
+ assert.ok(issues.some((i) => i.includes('Type Surface')));
188
+ });
189
+
190
+ test('allows packet without Type Surface for small tasks (<300 tokens)', () => {
191
+ const packet = `## Context Packet
192
+ **Task**: small task
193
+ ### Out of scope
194
+ - (none)
195
+ ### Acceptance Criteria
196
+ - [ ] does y
197
+ `;
198
+ const issues = checkMandatorySections(packet, 200);
199
+ assert.strictEqual(issues.length, 0, `expected pass for small task, got: ${issues.join('\n')}`);
200
+ });
201
+
202
+ test('passes valid packet with all sections', () => {
203
+ const packet = `## Context Packet
204
+ **Task**: task
205
+ ### Type Surface
206
+ - T { x: string }
207
+ ### Out of scope
208
+ - (none)
209
+ ### Acceptance Criteria
210
+ - [ ] does y
211
+ `;
212
+ const issues = checkMandatorySections(packet, 500);
213
+ assert.strictEqual(issues.length, 0);
214
+ });
215
+ });