@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.
- package/README.md +23 -12
- 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 +11 -6
- package/compiler/__tests__/out-of-scope-format.test.js +303 -0
- package/compiler/__tests__/zoom-out-output.test.js +112 -0
- package/package.json +2 -2
- package/skills/audit/SKILL.md +1 -0
- 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 +68 -25
- 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/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 +33 -6
- package/skills/journal/references/adr-criteria.md +109 -0
- package/skills/review/SKILL.md +1 -0
- package/skills/review-intake/SKILL.md +25 -2
- package/skills/scout/SKILL.md +33 -1
- package/skills/surgeon/SKILL.md +16 -1
- 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,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.
|
|
86
|
+
## What's New (v2.14.0 — Deep Modules)
|
|
87
87
|
|
|
88
|
-
-
|
|
89
|
-
-
|
|
90
|
-
-
|
|
91
|
-
- **
|
|
92
|
-
- **
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
+
});
|