@rune-kit/rune 2.12.3 → 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 (49) hide show
  1. package/README.md +41 -7
  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 +31 -0
  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/compiler/adapters/openclaw.js +43 -3
  11. package/package.json +2 -2
  12. package/skills/adversary/SKILL.md +1 -1
  13. package/skills/audit/SKILL.md +1 -0
  14. package/skills/autopsy/SKILL.md +1 -1
  15. package/skills/ba/SKILL.md +241 -82
  16. package/skills/ba/references/context-md-format.md +136 -0
  17. package/skills/ba/references/explore-first.md +107 -0
  18. package/skills/ba/references/out-of-scope-format.md +152 -0
  19. package/skills/brainstorm/SKILL.md +77 -1
  20. package/skills/brainstorm/references/design-it-twice.md +155 -0
  21. package/skills/context-pack/SKILL.md +69 -26
  22. package/skills/context-pack/evals.md +122 -0
  23. package/skills/context-pack/references/brief-template.md +121 -0
  24. package/skills/context-pack/references/durability-rules.md +109 -0
  25. package/skills/cook/SKILL.md +4 -4
  26. package/skills/debug/SKILL.md +2 -1
  27. package/skills/fix/SKILL.md +2 -1
  28. package/skills/graft/SKILL.md +1 -1
  29. package/skills/improve-architecture/SKILL.md +275 -0
  30. package/skills/improve-architecture/evals.md +145 -0
  31. package/skills/improve-architecture/references/deepening.md +87 -0
  32. package/skills/improve-architecture/references/interface-design.md +68 -0
  33. package/skills/improve-architecture/references/language.md +81 -0
  34. package/skills/improve-architecture/references/scoring.md +122 -0
  35. package/skills/journal/SKILL.md +34 -7
  36. package/skills/journal/references/adr-criteria.md +109 -0
  37. package/skills/marketing/SKILL.md +2 -1
  38. package/skills/review/SKILL.md +1 -0
  39. package/skills/review-intake/SKILL.md +25 -2
  40. package/skills/safeguard/SKILL.md +1 -1
  41. package/skills/scout/SKILL.md +33 -1
  42. package/skills/sentinel-env/SKILL.md +24 -13
  43. package/skills/skill-forge/SKILL.md +108 -1
  44. package/skills/surgeon/SKILL.md +17 -2
  45. package/skills/test/SKILL.md +48 -1
  46. package/skills/test/evals.md +137 -0
  47. package/skills/test/references/mocking-policy.md +121 -0
  48. package/skills/test/references/test-quality.md +124 -0
  49. 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,7 +83,26 @@ _Methodology: Claude Code CLI headless mode (`claude -p --output-format json`),
83
83
 
84
84
  ---
85
85
 
86
- ## What's New (v2.12.0 — Auto-Discipline)
86
+ ## What's New (v2.14.0 — Deep Modules)
87
+
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`.
104
+
105
+ ### Previous (v2.12.0 — Auto-Discipline)
87
106
 
88
107
  - **Runtime auto-discipline** — `rune hooks install` wires native hooks on Claude Code, Cursor, Windsurf, Antigravity so `preflight`, `sentinel`, `completion-gate` auto-fire before tool use. No more "remember to invoke the skill."
89
108
  - **Three presets** — `strict` (blocking gates), `gentle` (warnings, default), `off` (uninstall). Idempotent install / uninstall with full restore of user hooks.
@@ -150,7 +169,7 @@ cook ───emit:phase.complete────→ session-bridge
150
169
 
151
170
  ## What Rune Is (and Isn't)
152
171
 
153
- 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.
154
173
 
155
174
  | | Rune Provides | Claude Code Provides |
156
175
  |---|---|---|
@@ -202,7 +221,7 @@ npx @rune-kit/rune init --platform windsurf
202
221
  npx @rune-kit/rune init --platform antigravity
203
222
  ```
204
223
 
205
- 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.
206
225
 
207
226
  ### Platform Comparison
208
227
 
@@ -548,17 +567,32 @@ npx @rune-kit/rune doctor
548
567
 
549
568
  See [docs/MULTI-PLATFORM.md](docs/MULTI-PLATFORM.md) for the full architecture.
550
569
 
570
+ ## Documentation
571
+
572
+ | Doc | What's inside |
573
+ |-----|---------------|
574
+ | [`docs/GETTING_STARTED.md`](docs/GETTING_STARTED.md) | Your first 5 minutes with Rune — install to first `/rune cook` |
575
+ | [`docs/SKILLS.md`](docs/SKILLS.md) | All 63 skills, searchable by intent and layer |
576
+ | [`docs/SIGNALS.md`](docs/SIGNALS.md) | Canonical signal inventory — 25 events, emit/listen graph |
577
+ | [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) | 5-layer mesh architecture reference |
578
+ | [`docs/VISION.md`](docs/VISION.md) | Philosophy — what Rune is and isn't |
579
+ | [`docs/HOOKS.md`](docs/HOOKS.md) | Auto-discipline hooks per platform |
580
+ | [`docs/TROUBLESHOOTING.md`](docs/TROUBLESHOOTING.md) | Common issues + fixes |
581
+ | [`CONTRIBUTING.md`](CONTRIBUTING.md) | How to contribute skills, packs, fixes |
582
+ | [`CHANGELOG.md`](CHANGELOG.md) | Release history |
583
+ | [`ROADMAP.md`](ROADMAP.md) | What's next |
584
+
551
585
  ## Numbers
552
586
 
553
587
  ```
554
588
  Core Skills: 62 (L0: 1 │ L1: 5 │ L2: 29 │ L3: 27)
555
589
  Extension Packs: 14 free + 5 pro + 4 business
556
590
  Mesh Connections: 215+ cross-references
557
- Mesh Signals: 23 signals across 62 skills (emit/listen graph)
591
+ Mesh Signals: 25 signals across 63 skills (emit/listen graph)
558
592
  Connections/Skill: 3.4 avg
559
593
  Platforms: 8 (Claude Code, Cursor, Windsurf, Antigravity, Codex, OpenCode, OpenClaw, Generic)
560
594
  Compiler: ~1400 LOC (parser + 8 transforms + 8 adapters + CLI)
561
- Tests: 946 (compiler + signals + status + visualizer + hooks + scripts)
595
+ Tests: 1,152+ (compiler + signals + status + visualizer + hooks + scripts + tier-hooks)
562
596
  Pack Depth: 23 packs total (14 free + 5 pro + 4 business, all free packs rated Deep)
563
597
  ```
564
598
 
@@ -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
+ });