@wkronmiller/lisa 0.1.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/LICENSE +21 -0
- package/README.md +407 -0
- package/bin/lisa-runtime.js +8797 -0
- package/bin/lisa.js +21 -0
- package/completion.ts +58 -0
- package/install.ps1 +51 -0
- package/install.sh +93 -0
- package/lisa.ts +6 -0
- package/package.json +66 -0
- package/skills/README.md +28 -0
- package/skills/claude-code/CLAUDE.md +151 -0
- package/skills/codex/AGENTS.md +151 -0
- package/skills/gemini/GEMINI.md +151 -0
- package/skills/opencode/AGENTS.md +152 -0
- package/src/cli.ts +85 -0
- package/src/harness/base-adapter.ts +47 -0
- package/src/harness/claude-code.ts +106 -0
- package/src/harness/codex.ts +80 -0
- package/src/harness/command.ts +173 -0
- package/src/harness/gemini.ts +74 -0
- package/src/harness/opencode.ts +84 -0
- package/src/harness/registry.ts +29 -0
- package/src/harness/runner.ts +19 -0
- package/src/harness/types.ts +73 -0
- package/src/output-mode.ts +32 -0
- package/src/skill/artifacts.ts +174 -0
- package/src/skill/cli.ts +29 -0
- package/src/skill/install.ts +317 -0
- package/src/spec/agent-guidance.ts +466 -0
- package/src/spec/cli.ts +151 -0
- package/src/spec/commands/check.ts +1 -0
- package/src/spec/commands/config.ts +146 -0
- package/src/spec/commands/diff.ts +1 -0
- package/src/spec/commands/generate.ts +1 -0
- package/src/spec/commands/guide.ts +1 -0
- package/src/spec/commands/harness-list.ts +36 -0
- package/src/spec/commands/implement.ts +1 -0
- package/src/spec/commands/import.ts +1 -0
- package/src/spec/commands/init.ts +1 -0
- package/src/spec/commands/status.ts +87 -0
- package/src/spec/config.ts +63 -0
- package/src/spec/diff.ts +791 -0
- package/src/spec/extensions/benchmark.ts +347 -0
- package/src/spec/extensions/registry.ts +59 -0
- package/src/spec/extensions/types.ts +56 -0
- package/src/spec/grammar/index.ts +14 -0
- package/src/spec/grammar/parser.ts +443 -0
- package/src/spec/grammar/types.ts +70 -0
- package/src/spec/grammar/validator.ts +104 -0
- package/src/spec/loader.ts +174 -0
- package/src/spec/local-config.ts +59 -0
- package/src/spec/parser.ts +226 -0
- package/src/spec/path-utils.ts +73 -0
- package/src/spec/planner.ts +299 -0
- package/src/spec/prompt-renderer.ts +318 -0
- package/src/spec/skill-content.ts +119 -0
- package/src/spec/types.ts +239 -0
- package/src/spec/validator.ts +443 -0
- package/src/spec/workflows/check.ts +1534 -0
- package/src/spec/workflows/diff.ts +209 -0
- package/src/spec/workflows/generate.ts +1270 -0
- package/src/spec/workflows/guide.ts +190 -0
- package/src/spec/workflows/implement.ts +797 -0
- package/src/spec/workflows/import.ts +986 -0
- package/src/spec/workflows/init.ts +548 -0
- package/src/spec/workflows/status.ts +22 -0
- package/src/spec/workspace.ts +541 -0
- package/uninstall.ps1 +21 -0
- package/uninstall.sh +22 -0
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
import { relative } from "path";
|
|
2
|
+
|
|
3
|
+
import type { ParsedEnvironmentConfig, ParsedSpecDocument, SpecCheckEvidence, ValidationIssue } from "../types";
|
|
4
|
+
import type {
|
|
5
|
+
ExtensionVerificationContext,
|
|
6
|
+
ExtensionVerificationResult,
|
|
7
|
+
ExtensionVerificationTarget,
|
|
8
|
+
ParsedImportBundle,
|
|
9
|
+
SpecExtension,
|
|
10
|
+
} from "./types";
|
|
11
|
+
|
|
12
|
+
import { normalizeRelativePath } from "../path-utils";
|
|
13
|
+
|
|
14
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
15
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function extractStringRecord(value: unknown): Record<string, string> {
|
|
19
|
+
if (!isRecord(value)) {
|
|
20
|
+
return {};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return Object.fromEntries(
|
|
24
|
+
Object.entries(value).filter((entry): entry is [string, string] => typeof entry[1] === "string" && entry[1].trim().length > 0),
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function truncateOutput(output: string): string {
|
|
29
|
+
const trimmed = output.trim();
|
|
30
|
+
if (trimmed.length <= 240) {
|
|
31
|
+
return trimmed;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return `${trimmed.slice(0, 237)}...`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function extractJsonPayload(text: string): string | undefined {
|
|
38
|
+
const trimmed = text.trim();
|
|
39
|
+
if (!trimmed) {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if ((trimmed.startsWith("{") && trimmed.endsWith("}")) || (trimmed.startsWith("[") && trimmed.endsWith("]"))) {
|
|
44
|
+
return trimmed;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const fencedMatch = trimmed.match(/```json\s*([\s\S]*?)```/i);
|
|
48
|
+
if (fencedMatch?.[1]) {
|
|
49
|
+
return fencedMatch[1].trim();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const firstBrace = trimmed.indexOf("{");
|
|
53
|
+
const lastBrace = trimmed.lastIndexOf("}");
|
|
54
|
+
if (firstBrace >= 0 && lastBrace > firstBrace) {
|
|
55
|
+
return trimmed.slice(firstBrace, lastBrace + 1).trim();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function parseJsonOutput(output: string): unknown {
|
|
62
|
+
const payload = extractJsonPayload(output);
|
|
63
|
+
if (!payload) {
|
|
64
|
+
throw new Error("Benchmark command did not emit JSON output.");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return JSON.parse(payload);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function extractNumberRecord(value: unknown): Record<string, number> {
|
|
71
|
+
if (!isRecord(value)) {
|
|
72
|
+
return {};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const entries = Object.entries(value)
|
|
76
|
+
.map(([key, entryValue]) => {
|
|
77
|
+
if (typeof entryValue === "number" && Number.isFinite(entryValue)) {
|
|
78
|
+
return [key, entryValue] as [string, number];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (typeof entryValue === "string") {
|
|
82
|
+
const parsed = Number(entryValue.trim());
|
|
83
|
+
if (Number.isFinite(parsed)) {
|
|
84
|
+
return [key, parsed] as [string, number];
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return undefined;
|
|
89
|
+
})
|
|
90
|
+
.filter((entry): entry is [string, number] => entry !== undefined);
|
|
91
|
+
|
|
92
|
+
return Object.fromEntries(entries);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function extractMetrics(payload: unknown, keys: string[]): Record<string, number> {
|
|
96
|
+
if (!isRecord(payload)) {
|
|
97
|
+
return {};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
for (const key of keys) {
|
|
101
|
+
const value = payload[key];
|
|
102
|
+
const metrics = extractNumberRecord(value);
|
|
103
|
+
if (Object.keys(metrics).length > 0) {
|
|
104
|
+
return metrics;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return extractNumberRecord(payload);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function parseThreshold(threshold: string): { operator: string; value: number } {
|
|
112
|
+
const match = threshold.trim().match(/^(<=|>=|==|=|<|>)\s*(.+)$/);
|
|
113
|
+
if (!match) {
|
|
114
|
+
throw new Error(`Unsupported benchmark threshold: ${threshold}`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const value = Number(match[2]);
|
|
118
|
+
if (!Number.isFinite(value)) {
|
|
119
|
+
throw new Error(`Benchmark threshold must compare against a numeric value: ${threshold}`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
operator: match[1] === "=" ? "==" : match[1],
|
|
124
|
+
value,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function compareThreshold(current: number, operator: string, expected: number): boolean {
|
|
129
|
+
if (operator === "<=") {
|
|
130
|
+
return current <= expected;
|
|
131
|
+
}
|
|
132
|
+
if (operator === ">=") {
|
|
133
|
+
return current >= expected;
|
|
134
|
+
}
|
|
135
|
+
if (operator === "<") {
|
|
136
|
+
return current < expected;
|
|
137
|
+
}
|
|
138
|
+
if (operator === ">") {
|
|
139
|
+
return current > expected;
|
|
140
|
+
}
|
|
141
|
+
return current === expected;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function deriveRegressionDirection(operator: string): "lower" | "higher" | "exact" {
|
|
145
|
+
if (operator === "<=" || operator === "<") {
|
|
146
|
+
return "lower";
|
|
147
|
+
}
|
|
148
|
+
if (operator === ">=" || operator === ">") {
|
|
149
|
+
return "higher";
|
|
150
|
+
}
|
|
151
|
+
return "exact";
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function compareRelativeRegression(
|
|
155
|
+
current: number,
|
|
156
|
+
baseline: number,
|
|
157
|
+
direction: "lower" | "higher" | "exact",
|
|
158
|
+
tolerancePct: number,
|
|
159
|
+
): boolean {
|
|
160
|
+
if (direction === "lower") {
|
|
161
|
+
return current <= baseline * (1 + tolerancePct / 100);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (direction === "higher") {
|
|
165
|
+
return current >= baseline * (1 - tolerancePct / 100);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return Math.abs(current - baseline) <= Math.abs(baseline) * (tolerancePct / 100);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function formatEnvironment(environment: ParsedEnvironmentConfig | undefined): string {
|
|
172
|
+
if (!environment?.name) {
|
|
173
|
+
return "(missing environment config)";
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const runtime = environment.runtime ? Bun.YAML.stringify(environment.runtime).trim().replace(/\n/g, "; ") : "runtime: unspecified";
|
|
177
|
+
const resources = environment.resources ? Bun.YAML.stringify(environment.resources).trim().replace(/\n/g, "; ") : "resources: unspecified";
|
|
178
|
+
return `${environment.name} (${runtime}; ${resources})`;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function ensureImportedBenchmarkDraftStatuses(bundle: ParsedImportBundle): ValidationIssue[] {
|
|
182
|
+
const issues: ValidationIssue[] = [];
|
|
183
|
+
|
|
184
|
+
for (const draft of bundle.files) {
|
|
185
|
+
if (!draft.path.includes(".bench.")) {
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (!draft.content.includes("status: draft")) {
|
|
190
|
+
issues.push({
|
|
191
|
+
severity: "error",
|
|
192
|
+
path: draft.path,
|
|
193
|
+
message: "Imported benchmark sidecars must declare `status: draft`.",
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return issues;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async function verifyBenchmarkSidecars(
|
|
202
|
+
target: ExtensionVerificationTarget,
|
|
203
|
+
context: ExtensionVerificationContext,
|
|
204
|
+
): Promise<ExtensionVerificationResult | undefined> {
|
|
205
|
+
if (target.sidecars.length === 0) {
|
|
206
|
+
return undefined;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const evidence: SpecCheckEvidence[] = [];
|
|
210
|
+
const issues: string[] = [];
|
|
211
|
+
|
|
212
|
+
for (const sidecar of target.sidecars) {
|
|
213
|
+
const sidecarPath = normalizeRelativePath(relative(context.workspaceRoot, sidecar.path));
|
|
214
|
+
const required = sidecar.frontmatter.required === true;
|
|
215
|
+
const environmentName = typeof sidecar.frontmatter.environment === "string" ? sidecar.frontmatter.environment : undefined;
|
|
216
|
+
const environment = context.workspace.environments.find((entry) => entry.name === environmentName);
|
|
217
|
+
evidence.push({
|
|
218
|
+
type: "config",
|
|
219
|
+
detail: `Benchmark sidecar ${sidecar.id ?? sidecarPath} uses environment ${formatEnvironment(environment)}.`,
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
const command = typeof sidecar.frontmatter.command === "string" ? sidecar.frontmatter.command : undefined;
|
|
223
|
+
if (!command) {
|
|
224
|
+
if (required) {
|
|
225
|
+
issues.push(`Required benchmark sidecar ${sidecar.id ?? sidecarPath} does not declare a runnable command.`);
|
|
226
|
+
} else {
|
|
227
|
+
evidence.push({ type: "benchmark", detail: `Advisory benchmark sidecar ${sidecar.id ?? sidecarPath} has no runnable command.` });
|
|
228
|
+
}
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const commandResult = context.runCommand(command, context.workspaceRoot);
|
|
233
|
+
if (commandResult.exitCode !== 0) {
|
|
234
|
+
const detail = `Benchmark command failed for ${sidecar.id ?? sidecarPath}: ${command}${commandResult.stderr || commandResult.stdout ? ` (${truncateOutput(commandResult.stderr || commandResult.stdout)})` : ""}`;
|
|
235
|
+
if (required) {
|
|
236
|
+
issues.push(detail);
|
|
237
|
+
} else {
|
|
238
|
+
evidence.push({ type: "benchmark", detail: `Advisory ${detail}` });
|
|
239
|
+
}
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
let payload: unknown;
|
|
244
|
+
try {
|
|
245
|
+
payload = parseJsonOutput(commandResult.stdout || commandResult.stderr);
|
|
246
|
+
} catch (error) {
|
|
247
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
248
|
+
if (required) {
|
|
249
|
+
issues.push(`Required benchmark sidecar ${sidecar.id ?? sidecarPath} produced invalid JSON output: ${detail}`);
|
|
250
|
+
} else {
|
|
251
|
+
evidence.push({ type: "benchmark", detail: `Advisory benchmark sidecar ${sidecar.id ?? sidecarPath} produced invalid JSON output: ${detail}` });
|
|
252
|
+
}
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const metrics = extractMetrics(payload, ["metrics", "results", "current_metrics", "current"]);
|
|
257
|
+
const baselineMetrics = extractMetrics(payload, ["baseline_metrics", "baseline", "baselineResults"]);
|
|
258
|
+
const thresholds = extractStringRecord(sidecar.frontmatter.metrics);
|
|
259
|
+
const baselineLabel = typeof sidecar.frontmatter.baseline === "string" ? sidecar.frontmatter.baseline : undefined;
|
|
260
|
+
const tolerancePct = typeof sidecar.frontmatter.noise_tolerance_pct === "number" ? sidecar.frontmatter.noise_tolerance_pct : 0;
|
|
261
|
+
const failures: string[] = [];
|
|
262
|
+
|
|
263
|
+
for (const [metricName, thresholdText] of Object.entries(thresholds)) {
|
|
264
|
+
const currentValue = metrics[metricName];
|
|
265
|
+
if (currentValue === undefined) {
|
|
266
|
+
failures.push(`missing metric \`${metricName}\``);
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
let operator: string;
|
|
271
|
+
let value: number;
|
|
272
|
+
try {
|
|
273
|
+
({ operator, value } = parseThreshold(thresholdText));
|
|
274
|
+
} catch (error) {
|
|
275
|
+
failures.push(error instanceof Error ? error.message : String(error));
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
if (!compareThreshold(currentValue, operator, value)) {
|
|
279
|
+
failures.push(`metric \`${metricName}\` = ${currentValue} violated ${thresholdText}`);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (baselineLabel) {
|
|
283
|
+
const baselineValue = baselineMetrics[metricName];
|
|
284
|
+
if (baselineValue === undefined) {
|
|
285
|
+
failures.push(`baseline metric \`${metricName}\` missing for ${baselineLabel}`);
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const regressionDirection = deriveRegressionDirection(operator);
|
|
290
|
+
if (!compareRelativeRegression(currentValue, baselineValue, regressionDirection, tolerancePct)) {
|
|
291
|
+
failures.push(
|
|
292
|
+
`metric \`${metricName}\` regressed versus baseline ${baselineLabel}: current ${currentValue}, baseline ${baselineValue}, tolerance ${tolerancePct}%`,
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (failures.length > 0) {
|
|
299
|
+
const detail = `${sidecar.id ?? sidecarPath} failed benchmark enforcement: ${failures.join("; ")}`;
|
|
300
|
+
if (required) {
|
|
301
|
+
issues.push(detail);
|
|
302
|
+
} else {
|
|
303
|
+
evidence.push({ type: "benchmark", detail: `Advisory ${detail}` });
|
|
304
|
+
}
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
evidence.push({
|
|
309
|
+
type: "benchmark",
|
|
310
|
+
detail: `Passed benchmark sidecar ${sidecar.id ?? sidecarPath} via \`${command}\`${baselineLabel ? ` against ${baselineLabel}` : ""}.`,
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (issues.length > 0) {
|
|
315
|
+
return {
|
|
316
|
+
status: "FAIL",
|
|
317
|
+
summary: "Required benchmark checks failed.",
|
|
318
|
+
evidence,
|
|
319
|
+
issues,
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return {
|
|
324
|
+
evidence,
|
|
325
|
+
issues: [],
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
export function createBenchmarkExtension(): SpecExtension {
|
|
330
|
+
return {
|
|
331
|
+
kind: "benchmark",
|
|
332
|
+
sidecarMarkers: [".bench."],
|
|
333
|
+
buildImportPromptGuidance(): string[] {
|
|
334
|
+
return [
|
|
335
|
+
"If you find benchmark scripts, perf tests, or documented latency/throughput budgets, draft `.bench.<environment>.md` sidecars with `status: draft`.",
|
|
336
|
+
"When a benchmark sidecar references a new environment, also draft `.specs/environments/<environment>.yaml`.",
|
|
337
|
+
"Benchmark commands must emit JSON and should stay grounded in existing scripts or test helpers.",
|
|
338
|
+
];
|
|
339
|
+
},
|
|
340
|
+
validateImportBundle(bundle: ParsedImportBundle): ValidationIssue[] {
|
|
341
|
+
return ensureImportedBenchmarkDraftStatuses(bundle);
|
|
342
|
+
},
|
|
343
|
+
async verify(target: ExtensionVerificationTarget, context: ExtensionVerificationContext): Promise<ExtensionVerificationResult | undefined> {
|
|
344
|
+
return verifyBenchmarkSidecars(target, context);
|
|
345
|
+
},
|
|
346
|
+
};
|
|
347
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { LoadedSpecWorkspace, ParsedSpecDocument, ValidationIssue } from "../types";
|
|
2
|
+
import { createBenchmarkExtension } from "./benchmark";
|
|
3
|
+
import type {
|
|
4
|
+
ExtensionImportContext,
|
|
5
|
+
ExtensionVerificationContext,
|
|
6
|
+
ExtensionVerificationResult,
|
|
7
|
+
ExtensionVerificationTarget,
|
|
8
|
+
ParsedImportBundle,
|
|
9
|
+
SpecExtension,
|
|
10
|
+
} from "./types";
|
|
11
|
+
|
|
12
|
+
const EXTENSIONS: SpecExtension[] = [createBenchmarkExtension()];
|
|
13
|
+
|
|
14
|
+
export function getSpecExtensions(): SpecExtension[] {
|
|
15
|
+
return [...EXTENSIONS];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function isRegisteredExtensionKind(kind: string): boolean {
|
|
19
|
+
return getSpecExtensions().some((extension) => extension.kind === kind);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function resolveExtensionKindForPath(path: string): string | undefined {
|
|
23
|
+
return getSpecExtensions().find((extension) => (extension.sidecarMarkers ?? []).some((marker) => path.includes(marker)))?.kind;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function buildImportPromptGuidance(context: ExtensionImportContext): string[] {
|
|
27
|
+
return getSpecExtensions().flatMap((extension) => extension.buildImportPromptGuidance?.(context) ?? []);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function validateImportBundleWithExtensions(bundle: ParsedImportBundle, workspace: LoadedSpecWorkspace): ValidationIssue[] {
|
|
31
|
+
return getSpecExtensions().flatMap((extension) => extension.validateImportBundle?.(bundle, workspace) ?? []);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function verifySpecExtensions(
|
|
35
|
+
target: ExtensionVerificationTarget,
|
|
36
|
+
context: ExtensionVerificationContext,
|
|
37
|
+
): Promise<ExtensionVerificationResult[]> {
|
|
38
|
+
const results: ExtensionVerificationResult[] = [];
|
|
39
|
+
|
|
40
|
+
for (const extension of getSpecExtensions()) {
|
|
41
|
+
const matchingSidecars = target.sidecars.filter((document) => document.extensionKind === extension.kind || document.kind === extension.kind);
|
|
42
|
+
if (matchingSidecars.length === 0) {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const result = await extension.verify?.({ ...target, sidecars: matchingSidecars }, context);
|
|
47
|
+
if (result) {
|
|
48
|
+
results.push(result);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return results;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function getExtensionSidecarsForSpec(workspace: LoadedSpecWorkspace, specId: string): ParsedSpecDocument[] {
|
|
56
|
+
return workspace.documents
|
|
57
|
+
.filter((document) => document.kind !== "base" && document.frontmatter.extends === specId && document.status === "active")
|
|
58
|
+
.sort((left, right) => (left.id ?? left.path).localeCompare(right.id ?? right.path));
|
|
59
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
LoadedSpecWorkspace,
|
|
3
|
+
ParsedSpecDocument,
|
|
4
|
+
SpecCheckEvidence,
|
|
5
|
+
SpecCheckStatus,
|
|
6
|
+
SpecImportUncertainty,
|
|
7
|
+
ValidationIssue,
|
|
8
|
+
} from "../types";
|
|
9
|
+
|
|
10
|
+
export interface ExtensionCommandExecutionResult {
|
|
11
|
+
exitCode: number;
|
|
12
|
+
stdout: string;
|
|
13
|
+
stderr: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface ExtensionVerificationTarget {
|
|
17
|
+
specId: string;
|
|
18
|
+
path: string;
|
|
19
|
+
document: ParsedSpecDocument;
|
|
20
|
+
sidecars: ParsedSpecDocument[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ExtensionVerificationContext {
|
|
24
|
+
workspaceRoot: string;
|
|
25
|
+
workspace: LoadedSpecWorkspace;
|
|
26
|
+
runCommand(command: string, cwd: string): ExtensionCommandExecutionResult;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface ExtensionVerificationResult {
|
|
30
|
+
status?: SpecCheckStatus;
|
|
31
|
+
summary?: string;
|
|
32
|
+
evidence: SpecCheckEvidence[];
|
|
33
|
+
issues: string[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface ExtensionImportContext {
|
|
37
|
+
workspace: LoadedSpecWorkspace;
|
|
38
|
+
selectedPaths: string[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface ParsedImportBundle {
|
|
42
|
+
files: Array<{
|
|
43
|
+
path: string;
|
|
44
|
+
content: string;
|
|
45
|
+
}>;
|
|
46
|
+
notes: string[];
|
|
47
|
+
uncertainties: SpecImportUncertainty[];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface SpecExtension {
|
|
51
|
+
kind: string;
|
|
52
|
+
sidecarMarkers?: string[];
|
|
53
|
+
buildImportPromptGuidance?(context: ExtensionImportContext): string[];
|
|
54
|
+
validateImportBundle?(bundle: ParsedImportBundle, workspace: LoadedSpecWorkspace): ValidationIssue[];
|
|
55
|
+
verify?(target: ExtensionVerificationTarget, context: ExtensionVerificationContext): Promise<ExtensionVerificationResult | undefined>;
|
|
56
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type {
|
|
2
|
+
StatementKind,
|
|
3
|
+
ConstraintStrength,
|
|
4
|
+
RiskSeverity,
|
|
5
|
+
FileRef,
|
|
6
|
+
FieldLine,
|
|
7
|
+
ParsedStatement,
|
|
8
|
+
GrammarWarning,
|
|
9
|
+
GrammarParseResult,
|
|
10
|
+
} from "./types";
|
|
11
|
+
|
|
12
|
+
export { parseGrammarStatements } from "./parser";
|
|
13
|
+
export { validateGrammarCrossReferences } from "./validator";
|
|
14
|
+
export type { GrammarValidationOptions, GrammarValidationResult } from "./validator";
|