@rune-kit/rune 2.13.0 → 2.15.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 +31 -12
- package/compiler/__tests__/adapter-model-mapping.test.js +152 -0
- 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__/context-preview-signal.test.js +166 -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__/oracle-bundle-format.test.js +210 -0
- package/compiler/__tests__/oracle-pending-schema.test.js +209 -0
- package/compiler/__tests__/out-of-scope-format.test.js +303 -0
- package/compiler/__tests__/skill-description-quality.test.js +147 -0
- package/compiler/__tests__/zoom-out-output.test.js +112 -0
- package/compiler/adapters/antigravity.js +17 -1
- package/compiler/adapters/claude.js +4 -0
- package/compiler/adapters/codex.js +16 -1
- package/compiler/adapters/cursor.js +4 -0
- package/compiler/adapters/generic.js +14 -1
- package/compiler/adapters/openclaw.js +15 -1
- package/compiler/adapters/opencode.js +17 -1
- package/compiler/adapters/windsurf.js +4 -0
- package/package.json +2 -2
- package/skills/adversary/SKILL.md +61 -1
- package/skills/adversary/evals.md +79 -0
- package/skills/adversary/references/context-bundle-format.md +122 -0
- package/skills/adversary/references/oracle-mode.md +116 -0
- package/skills/asset-creator/SKILL.md +1 -1
- package/skills/audit/SKILL.md +3 -1
- package/skills/autopsy/SKILL.md +1 -1
- package/skills/ba/SKILL.md +242 -83
- 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 +78 -2
- package/skills/brainstorm/references/design-it-twice.md +155 -0
- package/skills/browser-pilot/SKILL.md +1 -1
- package/skills/completion-gate/SKILL.md +1 -1
- package/skills/constraint-check/SKILL.md +1 -1
- package/skills/context-engine/SKILL.md +85 -1
- package/skills/context-engine/references/preview-gate.md +139 -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 +5 -4
- package/skills/db/SKILL.md +1 -1
- package/skills/debug/SKILL.md +7 -4
- package/skills/dependency-doctor/SKILL.md +1 -1
- package/skills/design/SKILL.md +1 -1
- package/skills/doc-processor/SKILL.md +1 -1
- package/skills/docs/SKILL.md +1 -1
- package/skills/docs-seeker/SKILL.md +1 -1
- package/skills/fix/SKILL.md +5 -3
- package/skills/git/SKILL.md +1 -1
- package/skills/hallucination-guard/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/integrity-check/SKILL.md +1 -1
- package/skills/journal/SKILL.md +34 -7
- package/skills/journal/references/adr-criteria.md +109 -0
- package/skills/logic-guardian/SKILL.md +1 -1
- package/skills/marketing/SKILL.md +1 -1
- package/skills/mcp-builder/SKILL.md +1 -1
- package/skills/onboard/SKILL.md +1 -1
- package/skills/perf/SKILL.md +1 -1
- package/skills/plan/SKILL.md +1 -1
- package/skills/preflight/SKILL.md +1 -1
- package/skills/research/SKILL.md +1 -1
- package/skills/retro/SKILL.md +1 -1
- package/skills/review/SKILL.md +3 -2
- package/skills/review-intake/SKILL.md +26 -3
- package/skills/safeguard/SKILL.md +1 -1
- package/skills/scaffold/SKILL.md +1 -1
- package/skills/scout/SKILL.md +33 -1
- package/skills/sentinel/SKILL.md +1 -1
- package/skills/sentinel-env/SKILL.md +1 -1
- package/skills/session-bridge/SKILL.md +91 -4
- package/skills/session-bridge/references/detach-protocol.md +111 -0
- package/skills/skill-forge/SKILL.md +1 -1
- package/skills/slides/SKILL.md +1 -1
- package/skills/surgeon/SKILL.md +17 -2
- package/skills/team/SKILL.md +3 -0
- 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/skills/trend-scout/SKILL.md +1 -1
- package/skills/video-creator/SKILL.md +1 -1
- package/skills/watchdog/SKILL.md +1 -1
- package/skills/worktree/SKILL.md +1 -1
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,32 @@ _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.15.0 — Second Opinion + Cross-Provider + Routing Clarity)
|
|
87
87
|
|
|
88
|
-
-
|
|
89
|
-
- **`
|
|
90
|
-
- **`
|
|
91
|
-
- **
|
|
92
|
-
- **
|
|
88
|
+
- **`adversary` v0.2.0 — Mode: oracle** — when `agent.stuck` fires from `debug` (3 disproved hypotheses) or `fix` (2+ failed attempts), oracle-mode dispatches a stateless second-model pass with explicit "no prior context" framing. Bundle format is regex-validated (`[SYSTEM]` invariant role-priming + `[USER]` template + `### File N`), token-capped (100k bundle, 4k per file, 12 files max), citation-required reply contract. Secrets auto-redacted. Breaks the confirmation-bias loop that scout's zoom-out (structural pivot) cannot.
|
|
89
|
+
- **`session-bridge` v0.8.0 — Detach Mode** — async escalation primitive. Heavy-model second-opinion calls (1-10 min wall time) no longer block the primary agent. `.rune/oracle-pending/<sessionId>.json` is the rendezvous file; idempotent dispatch (bundleHash-keyed); 10min default timeout; 24h orphan cleanup on session start. `cook` Phase 4 and `team` Phase 3 reattach via filesystem poll between adjacent tasks.
|
|
90
|
+
- **`context-engine` v1.1.0 — Mode: preview** — pre-flight cost gate. Caller emits `context.preview` BEFORE bundle build with file list + estimated tokens (chars × 0.25). Per-caller thresholds: adversary 50k/100k, team 30k/80k (per worker), review 40k/100k, audit 60k/120k. Action enum `proceed | warn | block`. Override via `RUNE_CONTEXT_THRESHOLDS_<CALLER>`. Stops `team` parallel workstreams from silently blowing $20 of Opus tokens.
|
|
91
|
+
- **Cross-provider model mapping** — 5 non-Anthropic adapters now translate `model: opus|sonnet|haiku` to provider-correct names. **codex** → gpt-5-pro / gpt-5 / gpt-5-mini. **antigravity** → gemini-3-pro / gemini-3-flash / gemini-3-flash-lite. **opencode / openclaw / generic** → tier:heavy / tier:mid / tier:light (provider-agnostic). claude / cursor / windsurf remain no-op (Anthropic backend understands native names).
|
|
92
|
+
- **Routing clarity sweep** — all 63 SKILL.md descriptions now double-quoted (YAML safety). 13 ambiguous-name skills got explicit "Use when…" routing hints so skill-router doesn't have to guess: ba, completion-gate, constraint-check, doc-processor, integrity-check, logic-guardian, onboard, preflight, sentinel-env, watchdog, worktree, hallucination-guard, mcp-builder.
|
|
93
|
+
- **4 new mesh signals** — `oracle.dispatched`, `oracle.response`, `oracle.failed`, `context.preview`. All registered in Signal Catalog with full emit/listen mapping. `agent.stuck` listeners updated to include adversary in addition to scout.
|
|
94
|
+
- **1,331 tests** — +71 from v2.14.0 across 5 new test files: adapter-model-mapping (18), oracle-bundle-format (19), oracle-pending-schema (16), context-preview-signal (13), skill-description-quality (5).
|
|
95
|
+
|
|
96
|
+
### Previous (v2.14.0 — Deep Modules)
|
|
97
|
+
|
|
98
|
+
- **`improve-architecture` skill (NEW L2, opus)** — controlled vocabulary (Module / Interface / Implementation / Depth / Seam / Adapter / Leverage / Locality), numeric depth-leverage-locality scoring (1–5 each), 4 dependency categories, structured proposal payloads.
|
|
99
|
+
- **TDD vertical-slicing HARD-GATE** — `test` v1.3.0 catches "horizontal slicing" (5 tests before any GREEN), commit-pair audit trail, shape-test smell detector.
|
|
100
|
+
- **`.out-of-scope/` knowledge base** — `ba` v0.11.0 reads, `review-intake` v1.2.0 writes. Stops re-litigation of rejected features.
|
|
101
|
+
- **CONTEXT.md inline-sharpen + ADR 3-criteria gate** — `journal` v0.4.0 only opens an ADR when sum ≥ 11 + each axis ≥ 3.
|
|
102
|
+
- **Agent Brief durability** — `context-pack` v0.2.0 regex smell tests block stale paths/line numbers.
|
|
103
|
+
- **Design-It-Twice mode** — `brainstorm` v0.6.0 with constraint-pinned parallel subagents + diversity score gate.
|
|
104
|
+
- **Zoom-out + explore-first micro-utilities** — `scout` v0.4.0 listens for `agent.stuck`; `ba` Step 2.0 HARD-GATE requires tool-call evidence.
|
|
105
|
+
|
|
106
|
+
### Previous (v2.13.0 — Script Contract + Media Pack)
|
|
107
|
+
|
|
108
|
+
- **`@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.
|
|
109
|
+
- **`sentinel-env` v0.3.0** — 9-tier binary detection for hard-dependency checks.
|
|
110
|
+
- **`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.
|
|
111
|
+
- **OpenClaw adapter** — `generateManifest` now declares `artifactConvention`.
|
|
93
112
|
|
|
94
113
|
### Previous (v2.12.0 — Auto-Discipline)
|
|
95
114
|
|
|
@@ -158,7 +177,7 @@ cook ───emit:phase.complete────→ session-bridge
|
|
|
158
177
|
|
|
159
178
|
## What Rune Is (and Isn't)
|
|
160
179
|
|
|
161
|
-
Rune started as a **Claude Code plugin** and now compiles to **every major AI IDE**. Same
|
|
180
|
+
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
181
|
|
|
163
182
|
| | Rune Provides | Claude Code Provides |
|
|
164
183
|
|---|---|---|
|
|
@@ -210,7 +229,7 @@ npx @rune-kit/rune init --platform windsurf
|
|
|
210
229
|
npx @rune-kit/rune init --platform antigravity
|
|
211
230
|
```
|
|
212
231
|
|
|
213
|
-
This compiles all
|
|
232
|
+
This compiles all 63 skills into your IDE's rules format. Same knowledge, same workflows.
|
|
214
233
|
|
|
215
234
|
### Platform Comparison
|
|
216
235
|
|
|
@@ -561,7 +580,7 @@ See [docs/MULTI-PLATFORM.md](docs/MULTI-PLATFORM.md) for the full architecture.
|
|
|
561
580
|
| Doc | What's inside |
|
|
562
581
|
|-----|---------------|
|
|
563
582
|
| [`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
|
|
583
|
+
| [`docs/SKILLS.md`](docs/SKILLS.md) | All 63 skills, searchable by intent and layer |
|
|
565
584
|
| [`docs/SIGNALS.md`](docs/SIGNALS.md) | Canonical signal inventory — 25 events, emit/listen graph |
|
|
566
585
|
| [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) | 5-layer mesh architecture reference |
|
|
567
586
|
| [`docs/VISION.md`](docs/VISION.md) | Philosophy — what Rune is and isn't |
|
|
@@ -577,7 +596,7 @@ See [docs/MULTI-PLATFORM.md](docs/MULTI-PLATFORM.md) for the full architecture.
|
|
|
577
596
|
Core Skills: 62 (L0: 1 │ L1: 5 │ L2: 29 │ L3: 27)
|
|
578
597
|
Extension Packs: 14 free + 5 pro + 4 business
|
|
579
598
|
Mesh Connections: 215+ cross-references
|
|
580
|
-
Mesh Signals: 25 signals across
|
|
599
|
+
Mesh Signals: 25 signals across 63 skills (emit/listen graph)
|
|
581
600
|
Connections/Skill: 3.4 avg
|
|
582
601
|
Platforms: 8 (Claude Code, Cursor, Windsurf, Antigravity, Codex, OpenCode, OpenClaw, Generic)
|
|
583
602
|
Compiler: ~1400 LOC (parser + 8 transforms + 8 adapters + CLI)
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adapter model tier mapping (v2.15+) — verify non-Anthropic adapters
|
|
3
|
+
* translate `model: opus|sonnet|haiku` to provider-correct names while
|
|
4
|
+
* Anthropic-backed adapters (claude/cursor/windsurf) remain no-op.
|
|
5
|
+
*
|
|
6
|
+
* Validates:
|
|
7
|
+
* - codex/antigravity emit concrete provider model names
|
|
8
|
+
* - opencode/openclaw/generic emit semantic tier hints (provider-agnostic)
|
|
9
|
+
* - claude/cursor/windsurf do NOT translate (no-op)
|
|
10
|
+
* - Skills without `model:` produce no model line
|
|
11
|
+
* - Unknown tier values pass through unchanged
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import assert from 'node:assert';
|
|
15
|
+
import { describe, test } from 'node:test';
|
|
16
|
+
import { getAdapter } from '../adapters/index.js';
|
|
17
|
+
|
|
18
|
+
const baseSkill = {
|
|
19
|
+
name: 'cook',
|
|
20
|
+
description: 'Feature implementation orchestrator',
|
|
21
|
+
layer: 'L1',
|
|
22
|
+
group: 'orchestration',
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
describe('codex adapter model mapping', () => {
|
|
26
|
+
const codex = getAdapter('codex');
|
|
27
|
+
|
|
28
|
+
test('opus translates to gpt-5-pro', () => {
|
|
29
|
+
const header = codex.generateHeader({ ...baseSkill, model: 'opus' });
|
|
30
|
+
assert.match(header, /model: gpt-5-pro/);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('sonnet translates to gpt-5', () => {
|
|
34
|
+
const header = codex.generateHeader({ ...baseSkill, model: 'sonnet' });
|
|
35
|
+
assert.match(header, /model: gpt-5(?!-)/);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('haiku translates to gpt-5-mini', () => {
|
|
39
|
+
const header = codex.generateHeader({ ...baseSkill, model: 'haiku' });
|
|
40
|
+
assert.match(header, /model: gpt-5-mini/);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('omitted model produces no model line', () => {
|
|
44
|
+
const header = codex.generateHeader({ ...baseSkill });
|
|
45
|
+
assert.doesNotMatch(header, /model:/);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('unknown tier passes through unchanged', () => {
|
|
49
|
+
const header = codex.generateHeader({ ...baseSkill, model: 'custom-fine-tune' });
|
|
50
|
+
assert.match(header, /model: custom-fine-tune/);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe('antigravity adapter model mapping', () => {
|
|
55
|
+
const antigravity = getAdapter('antigravity');
|
|
56
|
+
|
|
57
|
+
test('opus translates to gemini-3-pro', () => {
|
|
58
|
+
const header = antigravity.generateHeader({ ...baseSkill, model: 'opus' });
|
|
59
|
+
assert.match(header, /model: gemini-3-pro/);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('sonnet translates to gemini-3-flash', () => {
|
|
63
|
+
const header = antigravity.generateHeader({ ...baseSkill, model: 'sonnet' });
|
|
64
|
+
assert.match(header, /model: gemini-3-flash(?!-)/);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('haiku translates to gemini-3-flash-lite', () => {
|
|
68
|
+
const header = antigravity.generateHeader({ ...baseSkill, model: 'haiku' });
|
|
69
|
+
assert.match(header, /model: gemini-3-flash-lite/);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe('opencode adapter model mapping (provider-agnostic)', () => {
|
|
74
|
+
const opencode = getAdapter('opencode');
|
|
75
|
+
|
|
76
|
+
test('opus translates to tier:heavy', () => {
|
|
77
|
+
const header = opencode.generateHeader({ ...baseSkill, model: 'opus' });
|
|
78
|
+
assert.match(header, /model: tier:heavy/);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('sonnet translates to tier:mid', () => {
|
|
82
|
+
const header = opencode.generateHeader({ ...baseSkill, model: 'sonnet' });
|
|
83
|
+
assert.match(header, /model: tier:mid/);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('haiku translates to tier:light', () => {
|
|
87
|
+
const header = opencode.generateHeader({ ...baseSkill, model: 'haiku' });
|
|
88
|
+
assert.match(header, /model: tier:light/);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe('openclaw adapter model mapping (markdown header)', () => {
|
|
93
|
+
const openclaw = getAdapter('openclaw');
|
|
94
|
+
|
|
95
|
+
test('opus appears as tier:heavy in metadata line', () => {
|
|
96
|
+
const header = openclaw.generateHeader({ ...baseSkill, model: 'opus' });
|
|
97
|
+
assert.match(header, /model: tier:heavy/);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test('omitted model produces no model line', () => {
|
|
101
|
+
const header = openclaw.generateHeader({ ...baseSkill });
|
|
102
|
+
assert.doesNotMatch(header, /model:/);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
describe('generic adapter model mapping (markdown header)', () => {
|
|
107
|
+
const generic = getAdapter('generic');
|
|
108
|
+
|
|
109
|
+
test('opus appears as tier:heavy in metadata line', () => {
|
|
110
|
+
const header = generic.generateHeader({ ...baseSkill, model: 'opus' });
|
|
111
|
+
assert.match(header, /model: tier:heavy/);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
describe('Anthropic-native adapters are no-op for model field', () => {
|
|
116
|
+
test('claude generateHeader returns empty (passthrough)', () => {
|
|
117
|
+
const claude = getAdapter('claude');
|
|
118
|
+
const header = claude.generateHeader({ ...baseSkill, model: 'opus' });
|
|
119
|
+
assert.strictEqual(header, '');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test('cursor + windsurf do NOT rewrite anthropic tier names', () => {
|
|
123
|
+
const cursor = getAdapter('cursor');
|
|
124
|
+
const windsurf = getAdapter('windsurf');
|
|
125
|
+
const cursorHeader = cursor.generateHeader({ ...baseSkill, model: 'opus' });
|
|
126
|
+
const windsurfHeader = windsurf.generateHeader({ ...baseSkill, model: 'opus' });
|
|
127
|
+
assert.doesNotMatch(cursorHeader, /gpt-5/);
|
|
128
|
+
assert.doesNotMatch(cursorHeader, /gemini/);
|
|
129
|
+
assert.doesNotMatch(windsurfHeader, /gpt-5/);
|
|
130
|
+
assert.doesNotMatch(windsurfHeader, /gemini/);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe('cross-adapter consistency', () => {
|
|
135
|
+
test('all 5 non-Anthropic adapters emit a model line for opus skills', () => {
|
|
136
|
+
const adapterNames = ['codex', 'antigravity', 'opencode', 'openclaw', 'generic'];
|
|
137
|
+
for (const name of adapterNames) {
|
|
138
|
+
const adapter = getAdapter(name);
|
|
139
|
+
const header = adapter.generateHeader({ ...baseSkill, model: 'opus' });
|
|
140
|
+
assert.match(header, /model: /, `${name} should emit a model line for opus skill`);
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test('all 5 non-Anthropic adapters omit model line when skill has no model', () => {
|
|
145
|
+
const adapterNames = ['codex', 'antigravity', 'opencode', 'openclaw', 'generic'];
|
|
146
|
+
for (const name of adapterNames) {
|
|
147
|
+
const adapter = getAdapter(name);
|
|
148
|
+
const header = adapter.generateHeader({ ...baseSkill });
|
|
149
|
+
assert.doesNotMatch(header, /model:/, `${name} should NOT emit a model line for skill without model`);
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
});
|
|
@@ -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
|
+
});
|