@thinkingsage/kanon 0.8.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/CHANGELOG.md +410 -0
- package/LICENSE +21 -0
- package/README.md +168 -0
- package/bridge/mcp-server.cjs +14171 -0
- package/package.json +98 -0
- package/src/adapters/capabilities.ts +178 -0
- package/src/adapters/claude-code.ts +110 -0
- package/src/adapters/cline.ts +98 -0
- package/src/adapters/codex.ts +173 -0
- package/src/adapters/copilot.ts +106 -0
- package/src/adapters/cursor.ts +97 -0
- package/src/adapters/degradation.ts +95 -0
- package/src/adapters/index.ts +324 -0
- package/src/adapters/kiro-frontmatter.ts +139 -0
- package/src/adapters/kiro-inclusion.ts +86 -0
- package/src/adapters/kiro.ts +412 -0
- package/src/adapters/qdeveloper.ts +115 -0
- package/src/adapters/types.ts +81 -0
- package/src/adapters/windsurf.ts +96 -0
- package/src/admin.ts +283 -0
- package/src/asset-conventions.ts +118 -0
- package/src/attribution-backfill.ts +319 -0
- package/src/attribution-report.ts +95 -0
- package/src/attribution.ts +239 -0
- package/src/backends/github.ts +194 -0
- package/src/backends/http.ts +122 -0
- package/src/backends/index.ts +39 -0
- package/src/backends/local.ts +47 -0
- package/src/backends/s3.ts +157 -0
- package/src/backends/types.ts +59 -0
- package/src/base-cache.ts +270 -0
- package/src/browse-ui.ts +3754 -0
- package/src/browse.ts +1038 -0
- package/src/build.ts +1108 -0
- package/src/catalog.ts +204 -0
- package/src/cli-deprecated.ts +29 -0
- package/src/cli.ts +773 -0
- package/src/collection-admin.ts +287 -0
- package/src/collection-builder.ts +464 -0
- package/src/collections.ts +116 -0
- package/src/compatibility.ts +105 -0
- package/src/config.ts +743 -0
- package/src/eval/rubrics/kiro-progressive-steering.ts +841 -0
- package/src/eval.ts +1169 -0
- package/src/file-writer.ts +61 -0
- package/src/format-registry.ts +141 -0
- package/src/guild/auto-updater.ts +163 -0
- package/src/guild/backend-resolver.ts +49 -0
- package/src/guild/cli.ts +592 -0
- package/src/guild/collection-expander.ts +47 -0
- package/src/guild/global-cache.ts +247 -0
- package/src/guild/hook-generator.ts +100 -0
- package/src/guild/manifest.ts +154 -0
- package/src/guild/path-utils.ts +12 -0
- package/src/guild/sync.ts +622 -0
- package/src/guild/version-resolver.ts +42 -0
- package/src/help/metadata.ts +445 -0
- package/src/help/renderer.ts +265 -0
- package/src/help/typo-suggester.ts +25 -0
- package/src/hooks/expression.ts +493 -0
- package/src/hooks/pipeline.ts +141 -0
- package/src/import.ts +773 -0
- package/src/importers/claude-code.ts +134 -0
- package/src/importers/cline.ts +103 -0
- package/src/importers/codex.ts +140 -0
- package/src/importers/copilot.ts +103 -0
- package/src/importers/cursor.ts +105 -0
- package/src/importers/index.ts +390 -0
- package/src/importers/kiro.ts +110 -0
- package/src/importers/qdeveloper.ts +103 -0
- package/src/importers/types.ts +54 -0
- package/src/importers/windsurf.ts +104 -0
- package/src/install.ts +1005 -0
- package/src/manifest-admin.ts +306 -0
- package/src/mcp-bridge.ts +240 -0
- package/src/mutation/delta.ts +50 -0
- package/src/mutation/history.ts +66 -0
- package/src/mutation/operators.ts +524 -0
- package/src/mutation/runner.ts +332 -0
- package/src/new.ts +106 -0
- package/src/outcomes/collision.ts +127 -0
- package/src/outcomes/normalize.ts +208 -0
- package/src/outcomes/registry.ts +173 -0
- package/src/parser.ts +446 -0
- package/src/provenance-backfill-cli.ts +319 -0
- package/src/provenance-backfill.ts +520 -0
- package/src/publish.ts +354 -0
- package/src/reconcile-orchestrator.ts +502 -0
- package/src/reconcile-report-renderer.ts +176 -0
- package/src/resolve-body.ts +15 -0
- package/src/rosetta/builtins/compatibility-profiles.ts +297 -0
- package/src/rosetta/builtins/contracts.ts +1033 -0
- package/src/rosetta/builtins/pretty-printers/claude-code-native.ts +122 -0
- package/src/rosetta/builtins/pretty-printers/cline-native.ts +50 -0
- package/src/rosetta/builtins/pretty-printers/codex-native.ts +127 -0
- package/src/rosetta/builtins/pretty-printers/copilot-native.ts +50 -0
- package/src/rosetta/builtins/pretty-printers/cursor-native.ts +50 -0
- package/src/rosetta/builtins/pretty-printers/index.ts +81 -0
- package/src/rosetta/builtins/pretty-printers/kiro-native.ts +166 -0
- package/src/rosetta/builtins/pretty-printers/kiro-power.ts +108 -0
- package/src/rosetta/builtins/pretty-printers/kiro-skill.ts +88 -0
- package/src/rosetta/builtins/pretty-printers/qdeveloper-native.ts +51 -0
- package/src/rosetta/builtins/pretty-printers/superpowers.ts +97 -0
- package/src/rosetta/builtins/pretty-printers/windsurf-native.ts +50 -0
- package/src/rosetta/builtins/sources/claude-code-native.ts +348 -0
- package/src/rosetta/builtins/sources/cline-native.ts +176 -0
- package/src/rosetta/builtins/sources/codex-native.ts +343 -0
- package/src/rosetta/builtins/sources/copilot-native.ts +178 -0
- package/src/rosetta/builtins/sources/cursor-native.ts +176 -0
- package/src/rosetta/builtins/sources/index.ts +95 -0
- package/src/rosetta/builtins/sources/kiro-native.ts +462 -0
- package/src/rosetta/builtins/sources/kiro-power.ts +285 -0
- package/src/rosetta/builtins/sources/kiro-skill.ts +230 -0
- package/src/rosetta/builtins/sources/qdeveloper-native.ts +181 -0
- package/src/rosetta/builtins/sources/superpowers.ts +240 -0
- package/src/rosetta/builtins/sources/windsurf-native.ts +176 -0
- package/src/rosetta/builtins/targets/claude-code.ts +181 -0
- package/src/rosetta/builtins/targets/cline.ts +87 -0
- package/src/rosetta/builtins/targets/codex.ts +226 -0
- package/src/rosetta/builtins/targets/copilot.ts +103 -0
- package/src/rosetta/builtins/targets/cursor.ts +87 -0
- package/src/rosetta/builtins/targets/index.ts +60 -0
- package/src/rosetta/builtins/targets/kiro.ts +278 -0
- package/src/rosetta/builtins/targets/qdeveloper.ts +103 -0
- package/src/rosetta/builtins/targets/windsurf.ts +87 -0
- package/src/rosetta/canonical.ts +729 -0
- package/src/rosetta/compatibility.ts +432 -0
- package/src/rosetta/contracts.ts +329 -0
- package/src/rosetta/detector.ts +724 -0
- package/src/rosetta/diagnostics.ts +630 -0
- package/src/rosetta/engine-bootstrap.ts +103 -0
- package/src/rosetta/engine.ts +744 -0
- package/src/rosetta/index.ts +381 -0
- package/src/rosetta/inspection.ts +530 -0
- package/src/rosetta/plan.ts +448 -0
- package/src/rosetta/provenance-digest.ts +369 -0
- package/src/rosetta/reconcile.ts +812 -0
- package/src/rosetta/redaction.ts +467 -0
- package/src/rosetta/registry.ts +712 -0
- package/src/rosetta/renderers.ts +571 -0
- package/src/rosetta/request-guard.ts +335 -0
- package/src/rosetta/resolution.ts +419 -0
- package/src/rosetta/source-accounting.ts +233 -0
- package/src/rosetta/templates.ts +129 -0
- package/src/rosetta-cli.ts +717 -0
- package/src/rosetta-docs-generator.ts +793 -0
- package/src/rosetta-profiles-cli.ts +367 -0
- package/src/schemas.ts +1712 -0
- package/src/spec-coordination.ts +1141 -0
- package/src/temper.ts +747 -0
- package/src/template-bundle-loader.ts +312 -0
- package/src/template-engine.ts +53 -0
- package/src/translation-application-policy.ts +496 -0
- package/src/translation-orchestrator.ts +1013 -0
- package/src/translation-plan-applier.ts +473 -0
- package/src/tutorial.ts +305 -0
- package/src/validate.ts +1093 -0
- package/src/versioning.ts +553 -0
- package/src/wizard.ts +660 -0
- package/src/workspace.ts +237 -0
- package/templates/eval-contexts/claude-code.md.njk +6 -0
- package/templates/eval-contexts/cline.md.njk +6 -0
- package/templates/eval-contexts/copilot.md.njk +6 -0
- package/templates/eval-contexts/cursor.md.njk +6 -0
- package/templates/eval-contexts/kiro.md.njk +10 -0
- package/templates/eval-contexts/qdeveloper.md.njk +6 -0
- package/templates/eval-contexts/windsurf.md.njk +6 -0
- package/templates/harness-adapters/_base/attribution-footer.md.njk +17 -0
- package/templates/harness-adapters/_base/base.md.njk +16 -0
- package/templates/harness-adapters/claude-code/claude.md.njk +1 -0
- package/templates/harness-adapters/claude-code/mcp.json.njk +1 -0
- package/templates/harness-adapters/claude-code/settings.json.njk +1 -0
- package/templates/harness-adapters/claude-code/skill-library-index.md.njk +13 -0
- package/templates/harness-adapters/claude-code/skill.md.njk +19 -0
- package/templates/harness-adapters/cline/hook.sh.njk +4 -0
- package/templates/harness-adapters/cline/mcp.json.njk +1 -0
- package/templates/harness-adapters/cline/rule.md.njk +1 -0
- package/templates/harness-adapters/codex/agents-md.md.njk +6 -0
- package/templates/harness-adapters/codex/agents-pointer.md.njk +16 -0
- package/templates/harness-adapters/codex/skill.md.njk +27 -0
- package/templates/harness-adapters/copilot/agents.md.njk +1 -0
- package/templates/harness-adapters/copilot/instructions.md.njk +1 -0
- package/templates/harness-adapters/copilot/scoped.md.njk +6 -0
- package/templates/harness-adapters/cursor/mcp.json.njk +1 -0
- package/templates/harness-adapters/cursor/rule.md.njk +6 -0
- package/templates/harness-adapters/kiro/hook.json.njk +1 -0
- package/templates/harness-adapters/kiro/mcp.json.njk +1 -0
- package/templates/harness-adapters/kiro/power-steering.md.njk +3 -0
- package/templates/harness-adapters/kiro/power.md.njk +12 -0
- package/templates/harness-adapters/kiro/steering.md.njk +16 -0
- package/templates/harness-adapters/qdeveloper/agent.md.njk +1 -0
- package/templates/harness-adapters/qdeveloper/mcp.json.njk +1 -0
- package/templates/harness-adapters/qdeveloper/rule.md.njk +1 -0
- package/templates/harness-adapters/windsurf/mcp.json.njk +1 -0
- package/templates/harness-adapters/windsurf/rule.md.njk +1 -0
- package/templates/harness-adapters/windsurf/workflow.md.njk +1 -0
- package/templates/knowledge/hooks.yaml.njk +4 -0
- package/templates/knowledge/knowledge.md.njk +53 -0
- package/templates/knowledge/mcp-servers.yaml.njk +2 -0
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mutation testing — runner orchestration (Req 5.1, 5.2, 5.4, 5.5, 5.9, 5.10).
|
|
3
|
+
*
|
|
4
|
+
* Discovers adapter source files, generates mutants, runs `bun test` against
|
|
5
|
+
* each mutant, and reports kill rate. This is the I/O shell that coordinates
|
|
6
|
+
* the pure-core modules (operators, delta, history).
|
|
7
|
+
*
|
|
8
|
+
* See ADR-0042 for the architectural rationale.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { execSync } from "node:child_process";
|
|
12
|
+
import { readdir, readFile, writeFile } from "node:fs/promises";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import chalk from "chalk";
|
|
15
|
+
import { ALL_MUTATION_OPERATORS, type MutationOperator } from "../config";
|
|
16
|
+
import { selectDeltaTargets } from "./delta";
|
|
17
|
+
import { appendRecord, type MutationRunRecord, parseHistory } from "./history";
|
|
18
|
+
import { generateMutants, type Mutant } from "./operators";
|
|
19
|
+
|
|
20
|
+
// --- Public Interfaces -------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
/** Options passed from the CLI eval command to the mutation runner. */
|
|
23
|
+
export interface MutationRunOptions {
|
|
24
|
+
/** Mutation operators to apply (from config or default all). */
|
|
25
|
+
operators?: MutationOperator[];
|
|
26
|
+
/** Minimum kill rate threshold (0.0–1.0). Default 0.80. */
|
|
27
|
+
threshold?: number;
|
|
28
|
+
/** Only mutate files changed since last run (nightly-delta strategy). */
|
|
29
|
+
delta?: boolean;
|
|
30
|
+
/** Path to history JSONL file. */
|
|
31
|
+
historyPath?: string;
|
|
32
|
+
/** Override adapters directory for testing. Default: "src/adapters". */
|
|
33
|
+
adaptersDir?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Report for a surviving mutant (Req 5.9). */
|
|
37
|
+
export interface SurvivorReport {
|
|
38
|
+
filePath: string;
|
|
39
|
+
line: number;
|
|
40
|
+
operator: string;
|
|
41
|
+
originalSnippet: string;
|
|
42
|
+
mutatedSnippet: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Result of a mutation testing run. */
|
|
46
|
+
export interface MutationRunResult {
|
|
47
|
+
totalMutants: number;
|
|
48
|
+
killed: number;
|
|
49
|
+
survived: number;
|
|
50
|
+
killRate: number;
|
|
51
|
+
survivors: SurvivorReport[];
|
|
52
|
+
operators: MutationOperator[];
|
|
53
|
+
belowThreshold: boolean;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Files excluded from adapter discovery (Req 5.2). */
|
|
57
|
+
const EXCLUDED_FILES = new Set(["types.ts", "index.ts", "capabilities.ts"]);
|
|
58
|
+
|
|
59
|
+
/** Default adapter directory relative to the project root. */
|
|
60
|
+
const DEFAULT_ADAPTERS_DIR = "src/adapters";
|
|
61
|
+
|
|
62
|
+
/** Default history file path. */
|
|
63
|
+
export const MUTATION_HISTORY_PATH = "evals/mutation-history.jsonl";
|
|
64
|
+
|
|
65
|
+
// --- Pure Functions ----------------------------------------------------------
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Compute kill rate = killed / total. Returns 0 when total is 0. Pure. (Req 5.5)
|
|
69
|
+
*/
|
|
70
|
+
export function computeKillRate(killed: number, total: number): number {
|
|
71
|
+
if (total === 0) return 0;
|
|
72
|
+
return killed / total;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// --- Adapter Discovery -------------------------------------------------------
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Discover adapter source files eligible for mutation (Req 5.2).
|
|
79
|
+
* Scans `src/adapters/` for .ts files, excluding types.ts, index.ts,
|
|
80
|
+
* and capabilities.ts.
|
|
81
|
+
*
|
|
82
|
+
* Returns relative paths (e.g. "src/adapters/kiro.ts").
|
|
83
|
+
*/
|
|
84
|
+
export async function discoverAdapterFiles(
|
|
85
|
+
adaptersDir: string = DEFAULT_ADAPTERS_DIR,
|
|
86
|
+
): Promise<string[]> {
|
|
87
|
+
const entries = await readdir(adaptersDir);
|
|
88
|
+
return entries
|
|
89
|
+
.filter((f) => f.endsWith(".ts") && !EXCLUDED_FILES.has(f))
|
|
90
|
+
.map((f) => join(adaptersDir, f))
|
|
91
|
+
.sort();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// --- Internal Helpers --------------------------------------------------------
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Get the current short git SHA.
|
|
98
|
+
*/
|
|
99
|
+
function gitSha(): string {
|
|
100
|
+
try {
|
|
101
|
+
return execSync("git rev-parse --short HEAD", { encoding: "utf-8" }).trim();
|
|
102
|
+
} catch {
|
|
103
|
+
return "unknown";
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Get files changed since a given SHA via `git diff`.
|
|
109
|
+
*/
|
|
110
|
+
function getChangedFiles(sha: string): string[] {
|
|
111
|
+
try {
|
|
112
|
+
const output = execSync(`git diff --name-only ${sha} HEAD`, {
|
|
113
|
+
encoding: "utf-8",
|
|
114
|
+
}).trim();
|
|
115
|
+
if (!output) return [];
|
|
116
|
+
return output.split("\n").filter(Boolean);
|
|
117
|
+
} catch {
|
|
118
|
+
return [];
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Build a SurvivorReport from a Mutant. The snippets from the operators
|
|
124
|
+
* module already include line-granular context (Req 5.9).
|
|
125
|
+
*/
|
|
126
|
+
function toSurvivorReport(mutant: Mutant): SurvivorReport {
|
|
127
|
+
return {
|
|
128
|
+
filePath: mutant.filePath,
|
|
129
|
+
line: mutant.line,
|
|
130
|
+
operator: mutant.operator,
|
|
131
|
+
originalSnippet: mutant.originalSnippet,
|
|
132
|
+
mutatedSnippet: mutant.mutatedSnippet,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Print surviving mutants to stderr for operator review (Req 5.9).
|
|
138
|
+
*/
|
|
139
|
+
function reportSurvivors(survivors: SurvivorReport[]): void {
|
|
140
|
+
if (survivors.length === 0) return;
|
|
141
|
+
console.error(chalk.yellow(`\n Surviving mutants (${survivors.length}):\n`));
|
|
142
|
+
for (const s of survivors) {
|
|
143
|
+
console.error(chalk.dim(` ─── ${s.filePath}:${s.line} [${s.operator}]`));
|
|
144
|
+
console.error(chalk.red(" - Original:"));
|
|
145
|
+
for (const line of s.originalSnippet.split("\n")) {
|
|
146
|
+
console.error(` ${line}`);
|
|
147
|
+
}
|
|
148
|
+
console.error(chalk.green(" + Mutated:"));
|
|
149
|
+
for (const line of s.mutatedSnippet.split("\n")) {
|
|
150
|
+
console.error(` ${line}`);
|
|
151
|
+
}
|
|
152
|
+
console.error("");
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// --- Main Runner Orchestration -----------------------------------------------
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Run mutation testing on adapter source files (Req 5.1).
|
|
160
|
+
*
|
|
161
|
+
* Orchestrates: adapter discovery → optional delta filtering → mutant
|
|
162
|
+
* generation → test execution → result reporting → history persistence.
|
|
163
|
+
*
|
|
164
|
+
* Sets process.exitCode to 1 if:
|
|
165
|
+
* - No adapter files found (Req 5.10)
|
|
166
|
+
* - Kill rate below threshold (Req 5.5)
|
|
167
|
+
*/
|
|
168
|
+
export async function runMutationTesting(
|
|
169
|
+
options: MutationRunOptions = {},
|
|
170
|
+
): Promise<MutationRunResult> {
|
|
171
|
+
const operators = options.operators ?? ALL_MUTATION_OPERATORS;
|
|
172
|
+
const threshold = options.threshold ?? 0.8;
|
|
173
|
+
const historyPath = options.historyPath ?? MUTATION_HISTORY_PATH;
|
|
174
|
+
const adaptersDir = options.adaptersDir ?? DEFAULT_ADAPTERS_DIR;
|
|
175
|
+
|
|
176
|
+
// Step 1: Discover adapter files (Req 5.2, 5.10)
|
|
177
|
+
let adapterFiles = await discoverAdapterFiles(adaptersDir);
|
|
178
|
+
|
|
179
|
+
if (adapterFiles.length === 0) {
|
|
180
|
+
console.error(
|
|
181
|
+
chalk.red("Error: No adapter files detected in src/adapters/."),
|
|
182
|
+
);
|
|
183
|
+
process.exitCode = 1;
|
|
184
|
+
return {
|
|
185
|
+
totalMutants: 0,
|
|
186
|
+
killed: 0,
|
|
187
|
+
survived: 0,
|
|
188
|
+
killRate: 0,
|
|
189
|
+
survivors: [],
|
|
190
|
+
operators,
|
|
191
|
+
belowThreshold: true,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Step 2: Apply delta strategy if requested (Req 5.6, 5.7)
|
|
196
|
+
if (options.delta) {
|
|
197
|
+
let lastSha: string | null = null;
|
|
198
|
+
try {
|
|
199
|
+
const raw = await readFile(historyPath, "utf-8");
|
|
200
|
+
const history = parseHistory(raw);
|
|
201
|
+
if (history.length > 0) {
|
|
202
|
+
lastSha = history[history.length - 1].sha;
|
|
203
|
+
}
|
|
204
|
+
} catch {
|
|
205
|
+
// No history file — fall back to full run
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (lastSha) {
|
|
209
|
+
const changedFiles = getChangedFiles(lastSha);
|
|
210
|
+
const deltaTargets = selectDeltaTargets(adapterFiles, changedFiles);
|
|
211
|
+
if (deltaTargets.length > 0) {
|
|
212
|
+
adapterFiles = deltaTargets;
|
|
213
|
+
} else {
|
|
214
|
+
console.error(
|
|
215
|
+
chalk.yellow(
|
|
216
|
+
" ⚠ No adapter files changed since last run — running full mutation suite.",
|
|
217
|
+
),
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
} else {
|
|
221
|
+
console.error(
|
|
222
|
+
chalk.yellow(
|
|
223
|
+
" ⚠ No baseline found in mutation history — falling back to full run.",
|
|
224
|
+
),
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Step 3: Generate mutants for each adapter file (Req 5.3)
|
|
230
|
+
const allMutants: Mutant[] = [];
|
|
231
|
+
for (const filePath of adapterFiles) {
|
|
232
|
+
const source = await readFile(filePath, "utf-8");
|
|
233
|
+
const mutants = generateMutants(filePath, source, operators);
|
|
234
|
+
allMutants.push(...mutants);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (allMutants.length === 0) {
|
|
238
|
+
console.error(chalk.yellow("No mutants generated — nothing to test."));
|
|
239
|
+
return {
|
|
240
|
+
totalMutants: 0,
|
|
241
|
+
killed: 0,
|
|
242
|
+
survived: 0,
|
|
243
|
+
killRate: 1,
|
|
244
|
+
survivors: [],
|
|
245
|
+
operators,
|
|
246
|
+
belowThreshold: false,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
console.error(
|
|
251
|
+
chalk.dim(
|
|
252
|
+
` Generated ${allMutants.length} mutants across ${adapterFiles.length} adapter files.`,
|
|
253
|
+
),
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
// Step 4: Run tests for each mutant (Req 5.4)
|
|
257
|
+
let killed = 0;
|
|
258
|
+
let survived = 0;
|
|
259
|
+
const survivors: SurvivorReport[] = [];
|
|
260
|
+
|
|
261
|
+
for (const mutant of allMutants) {
|
|
262
|
+
const originalSource = await readFile(mutant.filePath, "utf-8");
|
|
263
|
+
try {
|
|
264
|
+
// Write mutated source in place
|
|
265
|
+
await writeFile(mutant.filePath, mutant.mutatedSource, "utf-8");
|
|
266
|
+
|
|
267
|
+
// Run bun test with 30s timeout (Req 5.4)
|
|
268
|
+
try {
|
|
269
|
+
execSync("bun test", {
|
|
270
|
+
encoding: "utf-8",
|
|
271
|
+
timeout: 30_000,
|
|
272
|
+
stdio: "pipe",
|
|
273
|
+
});
|
|
274
|
+
// Tests passed — mutant survived
|
|
275
|
+
survived++;
|
|
276
|
+
survivors.push(toSurvivorReport(mutant));
|
|
277
|
+
} catch {
|
|
278
|
+
// Tests failed or timed out — mutant killed (Req 5.4)
|
|
279
|
+
killed++;
|
|
280
|
+
}
|
|
281
|
+
} finally {
|
|
282
|
+
// Always restore the original source
|
|
283
|
+
await writeFile(mutant.filePath, originalSource, "utf-8");
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Step 5: Compute kill rate (Req 5.5)
|
|
288
|
+
const killRate = computeKillRate(killed, allMutants.length);
|
|
289
|
+
const belowThreshold = killRate < threshold;
|
|
290
|
+
|
|
291
|
+
// Step 6: Report surviving mutants (Req 5.9)
|
|
292
|
+
reportSurvivors(survivors);
|
|
293
|
+
|
|
294
|
+
// Step 7: Print summary
|
|
295
|
+
const pct = (killRate * 100).toFixed(1);
|
|
296
|
+
if (belowThreshold) {
|
|
297
|
+
console.error(
|
|
298
|
+
chalk.red(
|
|
299
|
+
` Kill rate: ${pct}% (${killed}/${allMutants.length}) — BELOW threshold ${(threshold * 100).toFixed(1)}%`,
|
|
300
|
+
),
|
|
301
|
+
);
|
|
302
|
+
process.exitCode = 1;
|
|
303
|
+
} else {
|
|
304
|
+
console.error(
|
|
305
|
+
chalk.green(
|
|
306
|
+
` Kill rate: ${pct}% (${killed}/${allMutants.length}) — above threshold ${(threshold * 100).toFixed(1)}%`,
|
|
307
|
+
),
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Step 8: Persist to history (Req 5.8)
|
|
312
|
+
const record: MutationRunRecord = {
|
|
313
|
+
ts: new Date().toISOString(),
|
|
314
|
+
sha: gitSha(),
|
|
315
|
+
killRate,
|
|
316
|
+
totalMutants: allMutants.length,
|
|
317
|
+
killed,
|
|
318
|
+
survived,
|
|
319
|
+
operators,
|
|
320
|
+
};
|
|
321
|
+
await appendRecord(historyPath, record);
|
|
322
|
+
|
|
323
|
+
return {
|
|
324
|
+
totalMutants: allMutants.length,
|
|
325
|
+
killed,
|
|
326
|
+
survived,
|
|
327
|
+
killRate,
|
|
328
|
+
survivors,
|
|
329
|
+
operators,
|
|
330
|
+
belowThreshold,
|
|
331
|
+
};
|
|
332
|
+
}
|
package/src/new.ts
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { exists, mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import * as p from "@clack/prompts";
|
|
4
|
+
import chalk from "chalk";
|
|
5
|
+
import { writeWizardResult } from "./file-writer";
|
|
6
|
+
import { type AssetType, AssetTypeSchema } from "./schemas";
|
|
7
|
+
import { createTemplateEnv, renderTemplate } from "./template-engine";
|
|
8
|
+
import { runWizard } from "./wizard";
|
|
9
|
+
|
|
10
|
+
export interface NewCommandOptions {
|
|
11
|
+
yes?: boolean;
|
|
12
|
+
/** Pre-select the asset type, bypassing the wizard type prompt. */
|
|
13
|
+
type?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function toTitleCase(kebab: string): string {
|
|
17
|
+
return kebab
|
|
18
|
+
.split("-")
|
|
19
|
+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
20
|
+
.join(" ");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function newCommand(
|
|
24
|
+
artifactName: string,
|
|
25
|
+
options: NewCommandOptions = {},
|
|
26
|
+
): Promise<void> {
|
|
27
|
+
const knowledgeDir = "knowledge";
|
|
28
|
+
const artifactDir = join(knowledgeDir, artifactName);
|
|
29
|
+
|
|
30
|
+
if (await exists(artifactDir)) {
|
|
31
|
+
console.error(
|
|
32
|
+
chalk.red(
|
|
33
|
+
`Error: Artifact "${artifactName}" already exists at ${artifactDir}. Choose a different name.`,
|
|
34
|
+
),
|
|
35
|
+
);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Validate --type flag if provided
|
|
40
|
+
let preSelectedType: AssetType | undefined;
|
|
41
|
+
if (options.type) {
|
|
42
|
+
const parsed = AssetTypeSchema.safeParse(options.type);
|
|
43
|
+
if (!parsed.success) {
|
|
44
|
+
const valid = AssetTypeSchema.options.join(", ");
|
|
45
|
+
console.error(
|
|
46
|
+
chalk.red(
|
|
47
|
+
`Error: Unknown asset type "${options.type}". Valid types: ${valid}`,
|
|
48
|
+
),
|
|
49
|
+
);
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
preSelectedType = parsed.data;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const templateEnv = createTemplateEnv("templates/knowledge");
|
|
56
|
+
const displayName = toTitleCase(artifactName);
|
|
57
|
+
const context = { name: artifactName, displayName };
|
|
58
|
+
|
|
59
|
+
// Create base directory structure
|
|
60
|
+
// workflow type gets a pre-populated workflows/ directory
|
|
61
|
+
if (preSelectedType === "workflow") {
|
|
62
|
+
await mkdir(join(artifactDir, "workflows"), { recursive: true });
|
|
63
|
+
await writeFile(
|
|
64
|
+
join(artifactDir, "workflows", "main.md"),
|
|
65
|
+
`# Main Workflow\n\nDescribe the steps for ${displayName} here.\n`,
|
|
66
|
+
"utf-8",
|
|
67
|
+
);
|
|
68
|
+
} else {
|
|
69
|
+
await mkdir(join(artifactDir, "workflows"), { recursive: true });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Render and write knowledge.md
|
|
73
|
+
const knowledgeMd = renderTemplate(templateEnv, "knowledge.md.njk", context);
|
|
74
|
+
await writeFile(join(artifactDir, "knowledge.md"), knowledgeMd, "utf-8");
|
|
75
|
+
|
|
76
|
+
// Render and write hooks.yaml
|
|
77
|
+
const hooksYaml = renderTemplate(templateEnv, "hooks.yaml.njk", context);
|
|
78
|
+
await writeFile(join(artifactDir, "hooks.yaml"), hooksYaml, "utf-8");
|
|
79
|
+
|
|
80
|
+
// Render and write mcp-servers.yaml
|
|
81
|
+
const mcpYaml = renderTemplate(templateEnv, "mcp-servers.yaml.njk", context);
|
|
82
|
+
await writeFile(join(artifactDir, "mcp-servers.yaml"), mcpYaml, "utf-8");
|
|
83
|
+
|
|
84
|
+
console.error(chalk.green(`✓ Created new artifact at ${artifactDir}`));
|
|
85
|
+
|
|
86
|
+
if (options.yes) {
|
|
87
|
+
console.error(`\nNext steps:`);
|
|
88
|
+
console.error(
|
|
89
|
+
` 1. Edit ${join(artifactDir, "knowledge.md")} with your content`,
|
|
90
|
+
);
|
|
91
|
+
console.error(
|
|
92
|
+
` 2. Add hooks to ${join(artifactDir, "hooks.yaml")} (optional)`,
|
|
93
|
+
);
|
|
94
|
+
console.error(
|
|
95
|
+
` 3. Add MCP servers to ${join(artifactDir, "mcp-servers.yaml")} (optional)`,
|
|
96
|
+
);
|
|
97
|
+
console.error(` 4. Run \`kanon build\` to compile`);
|
|
98
|
+
} else {
|
|
99
|
+
const result = await runWizard(artifactName, displayName, preSelectedType);
|
|
100
|
+
const writtenFiles = await writeWizardResult(artifactDir, result);
|
|
101
|
+
const fileList = writtenFiles.map((f) => ` • ${f}`).join("\n");
|
|
102
|
+
p.outro(
|
|
103
|
+
`Files written:\n${fileList}\n\n Run \`kanon build\` to compile your artifact.`,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Outcomes registry — pure collision-detection primitives.
|
|
3
|
+
*
|
|
4
|
+
* These functions implement the two-tier collision-detection algorithm
|
|
5
|
+
* (Requirement 2E) over the normalized representation produced by
|
|
6
|
+
* `normalizeShape`. Every function here is pure, deterministic, and free of
|
|
7
|
+
* I/O or shared state, so the subtle comparison logic can be verified
|
|
8
|
+
* exhaustively with property-based tests.
|
|
9
|
+
*
|
|
10
|
+
* - Tier-1: exact match on the normalized (inputShape, outputShape) tuple.
|
|
11
|
+
* - Tier-2: Jaccard similarity over tokenized keyword sets (>= threshold).
|
|
12
|
+
*
|
|
13
|
+
* The verdict matrix combines the two tiers: both -> COLLISION, exactly one ->
|
|
14
|
+
* AMBIGUOUS, neither -> CLEAN. A COLLISION between two outcomes that each list
|
|
15
|
+
* the other in `related` is treated as an acknowledged overlap by callers.
|
|
16
|
+
*
|
|
17
|
+
* See ADR-0041 for the architectural rationale.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import type { Outcome } from "../schemas";
|
|
21
|
+
import { normalizeShape } from "./normalize";
|
|
22
|
+
|
|
23
|
+
/** The three possible collision-detection verdicts (Requirement 2E.3). */
|
|
24
|
+
export type Verdict = "COLLISION" | "AMBIGUOUS" | "CLEAN";
|
|
25
|
+
|
|
26
|
+
/** Default keyword Jaccard threshold for Tier-2 detection (Requirement 2E.2). */
|
|
27
|
+
export const DEFAULT_KEYWORD_THRESHOLD = 0.4;
|
|
28
|
+
|
|
29
|
+
/** Minimum token length to keep; tokens of this length or shorter are dropped. */
|
|
30
|
+
const MIN_TOKEN_LENGTH = 2;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Tokenize a keyword set into a deduplicated set of tokens (Requirement 2E.2).
|
|
34
|
+
*
|
|
35
|
+
* Each keyword is split on hyphens, underscores, and whitespace. Tokens whose
|
|
36
|
+
* length is `<= 2` characters are dropped, and the surviving tokens are
|
|
37
|
+
* collected into a Set (deduplicating across the whole keyword array). Pure.
|
|
38
|
+
*/
|
|
39
|
+
export function tokenizeKeywords(keywords: string[]): Set<string> {
|
|
40
|
+
const tokens = new Set<string>();
|
|
41
|
+
for (const keyword of keywords) {
|
|
42
|
+
for (const token of keyword.split(/[-_\s]+/)) {
|
|
43
|
+
if (token.length > MIN_TOKEN_LENGTH) {
|
|
44
|
+
tokens.add(token);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return tokens;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Jaccard similarity between two token sets: |A∩B| / |A∪B| (Requirement 2E.2).
|
|
53
|
+
*
|
|
54
|
+
* Returns 0 when both sets are empty (the union is empty). The result is always
|
|
55
|
+
* within the range [0, 1]. Pure and symmetric.
|
|
56
|
+
*/
|
|
57
|
+
export function jaccardSimilarity(a: Set<string>, b: Set<string>): number {
|
|
58
|
+
if (a.size === 0 && b.size === 0) {
|
|
59
|
+
return 0;
|
|
60
|
+
}
|
|
61
|
+
let intersection = 0;
|
|
62
|
+
for (const token of a) {
|
|
63
|
+
if (b.has(token)) {
|
|
64
|
+
intersection++;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const union = a.size + b.size - intersection;
|
|
68
|
+
return union === 0 ? 0 : intersection / union;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Tier-1 detection: exact match on the normalized (inputShape, outputShape)
|
|
73
|
+
* tuple (Requirement 2E.1). Pure.
|
|
74
|
+
*/
|
|
75
|
+
export function shapesMatch(a: Outcome, b: Outcome): boolean {
|
|
76
|
+
return (
|
|
77
|
+
normalizeShape(a.inputShape) === normalizeShape(b.inputShape) &&
|
|
78
|
+
normalizeShape(a.outputShape) === normalizeShape(b.outputShape)
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Tier-2 detection: keyword Jaccard similarity meets or exceeds `threshold`
|
|
84
|
+
* (default 0.4) (Requirement 2E.2). Pure.
|
|
85
|
+
*/
|
|
86
|
+
export function keywordsMatch(
|
|
87
|
+
a: Outcome,
|
|
88
|
+
b: Outcome,
|
|
89
|
+
threshold: number = DEFAULT_KEYWORD_THRESHOLD,
|
|
90
|
+
): boolean {
|
|
91
|
+
const similarity = jaccardSimilarity(
|
|
92
|
+
tokenizeKeywords(a.keywords),
|
|
93
|
+
tokenizeKeywords(b.keywords),
|
|
94
|
+
);
|
|
95
|
+
return similarity >= threshold;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Compute the verdict from the two tiers (Requirement 2E.3):
|
|
100
|
+
* - both tiers match -> COLLISION
|
|
101
|
+
* - exactly one matches -> AMBIGUOUS
|
|
102
|
+
* - neither matches -> CLEAN
|
|
103
|
+
* Pure and symmetric in `a` and `b`.
|
|
104
|
+
*/
|
|
105
|
+
export function computeVerdict(
|
|
106
|
+
a: Outcome,
|
|
107
|
+
b: Outcome,
|
|
108
|
+
threshold: number = DEFAULT_KEYWORD_THRESHOLD,
|
|
109
|
+
): Verdict {
|
|
110
|
+
const tier1 = shapesMatch(a, b);
|
|
111
|
+
const tier2 = keywordsMatch(a, b, threshold);
|
|
112
|
+
if (tier1 && tier2) {
|
|
113
|
+
return "COLLISION";
|
|
114
|
+
}
|
|
115
|
+
if (tier1 || tier2) {
|
|
116
|
+
return "AMBIGUOUS";
|
|
117
|
+
}
|
|
118
|
+
return "CLEAN";
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* True when `a` and `b` each list the other's id in their `related` field
|
|
123
|
+
* (Requirement 2E.4 / 2B.2). A one-sided reference is not acknowledged. Pure.
|
|
124
|
+
*/
|
|
125
|
+
export function isAcknowledged(a: Outcome, b: Outcome): boolean {
|
|
126
|
+
return a.related.includes(b.id) && b.related.includes(a.id);
|
|
127
|
+
}
|