@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.
- package/README.md +41 -7
- package/compiler/__tests__/adr-scoring.test.js +91 -0
- package/compiler/__tests__/context-md-format.test.js +180 -0
- package/compiler/__tests__/context-pack-smell-tests.test.js +215 -0
- package/compiler/__tests__/diversity-score.test.js +117 -0
- package/compiler/__tests__/improve-architecture.test.js +171 -0
- package/compiler/__tests__/openclaw-adapter.test.js +31 -0
- package/compiler/__tests__/out-of-scope-format.test.js +303 -0
- package/compiler/__tests__/zoom-out-output.test.js +112 -0
- package/compiler/adapters/openclaw.js +43 -3
- package/package.json +2 -2
- package/skills/adversary/SKILL.md +1 -1
- package/skills/audit/SKILL.md +1 -0
- package/skills/autopsy/SKILL.md +1 -1
- package/skills/ba/SKILL.md +241 -82
- package/skills/ba/references/context-md-format.md +136 -0
- package/skills/ba/references/explore-first.md +107 -0
- package/skills/ba/references/out-of-scope-format.md +152 -0
- package/skills/brainstorm/SKILL.md +77 -1
- package/skills/brainstorm/references/design-it-twice.md +155 -0
- package/skills/context-pack/SKILL.md +69 -26
- package/skills/context-pack/evals.md +122 -0
- package/skills/context-pack/references/brief-template.md +121 -0
- package/skills/context-pack/references/durability-rules.md +109 -0
- package/skills/cook/SKILL.md +4 -4
- package/skills/debug/SKILL.md +2 -1
- package/skills/fix/SKILL.md +2 -1
- package/skills/graft/SKILL.md +1 -1
- package/skills/improve-architecture/SKILL.md +275 -0
- package/skills/improve-architecture/evals.md +145 -0
- package/skills/improve-architecture/references/deepening.md +87 -0
- package/skills/improve-architecture/references/interface-design.md +68 -0
- package/skills/improve-architecture/references/language.md +81 -0
- package/skills/improve-architecture/references/scoring.md +122 -0
- package/skills/journal/SKILL.md +34 -7
- package/skills/journal/references/adr-criteria.md +109 -0
- package/skills/marketing/SKILL.md +2 -1
- package/skills/review/SKILL.md +1 -0
- package/skills/review-intake/SKILL.md +25 -2
- package/skills/safeguard/SKILL.md +1 -1
- package/skills/scout/SKILL.md +33 -1
- package/skills/sentinel-env/SKILL.md +24 -13
- package/skills/skill-forge/SKILL.md +108 -1
- package/skills/surgeon/SKILL.md +17 -2
- package/skills/test/SKILL.md +48 -1
- package/skills/test/evals.md +137 -0
- package/skills/test/references/mocking-policy.md +121 -0
- package/skills/test/references/test-quality.md +124 -0
- 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
|
-
|
|
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** —
|
|
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.
|
|
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
|
|
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
|
|
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:
|
|
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:
|
|
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
|
+
});
|