@aestheticfunction/dspack-gen 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.
Files changed (79) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/LICENSE +202 -0
  3. package/README.md +147 -0
  4. package/dist/adapters/anthropic.d.ts +16 -0
  5. package/dist/adapters/anthropic.js +71 -0
  6. package/dist/adapters/fake.d.ts +21 -0
  7. package/dist/adapters/fake.js +32 -0
  8. package/dist/adapters/index.d.ts +6 -0
  9. package/dist/adapters/index.js +11 -0
  10. package/dist/adapters/ollama.d.ts +16 -0
  11. package/dist/adapters/ollama.js +91 -0
  12. package/dist/adapters/types.d.ts +66 -0
  13. package/dist/adapters/types.js +47 -0
  14. package/dist/audit/report.d.ts +84 -0
  15. package/dist/audit/report.js +62 -0
  16. package/dist/cli.d.ts +2 -0
  17. package/dist/cli.js +182 -0
  18. package/dist/core/compiler.d.ts +39 -0
  19. package/dist/core/compiler.js +142 -0
  20. package/dist/core/contract.d.ts +175 -0
  21. package/dist/core/contract.js +62 -0
  22. package/dist/core/generation-schema.d.ts +32 -0
  23. package/dist/core/generation-schema.js +85 -0
  24. package/dist/core/index.d.ts +12 -0
  25. package/dist/core/index.js +12 -0
  26. package/dist/core/lint/findings.d.ts +46 -0
  27. package/dist/core/lint/findings.js +28 -0
  28. package/dist/core/lint/index.d.ts +7 -0
  29. package/dist/core/lint/index.js +67 -0
  30. package/dist/core/lint/rules.d.ts +27 -0
  31. package/dist/core/lint/rules.js +212 -0
  32. package/dist/core/lint/vocabulary.d.ts +13 -0
  33. package/dist/core/lint/vocabulary.js +71 -0
  34. package/dist/core/lint/walk.d.ts +14 -0
  35. package/dist/core/lint/walk.js +30 -0
  36. package/dist/core/surface-schema.d.ts +78 -0
  37. package/dist/core/surface-schema.js +85 -0
  38. package/dist/eval/assert.d.ts +2 -0
  39. package/dist/eval/assert.js +50 -0
  40. package/dist/eval/run.d.ts +2 -0
  41. package/dist/eval/run.js +60 -0
  42. package/dist/eval/runner.d.ts +36 -0
  43. package/dist/eval/runner.js +310 -0
  44. package/dist/eval/types.d.ts +143 -0
  45. package/dist/eval/types.js +1 -0
  46. package/dist/index.d.ts +15 -0
  47. package/dist/index.js +14 -0
  48. package/dist/repair/render.d.ts +20 -0
  49. package/dist/repair/render.js +28 -0
  50. package/dist/run/orchestrator.d.ts +90 -0
  51. package/dist/run/orchestrator.js +191 -0
  52. package/dist/serve.d.ts +25 -0
  53. package/dist/serve.js +144 -0
  54. package/package.json +76 -0
  55. package/src/adapters/anthropic.ts +88 -0
  56. package/src/adapters/fake.ts +32 -0
  57. package/src/adapters/index.ts +13 -0
  58. package/src/adapters/ollama.ts +123 -0
  59. package/src/adapters/types.ts +91 -0
  60. package/src/audit/report.ts +139 -0
  61. package/src/cli.ts +191 -0
  62. package/src/core/compiler.ts +205 -0
  63. package/src/core/contract.ts +205 -0
  64. package/src/core/generation-schema.ts +99 -0
  65. package/src/core/index.ts +12 -0
  66. package/src/core/lint/findings.ts +80 -0
  67. package/src/core/lint/index.ts +80 -0
  68. package/src/core/lint/rules.ts +320 -0
  69. package/src/core/lint/vocabulary.ts +80 -0
  70. package/src/core/lint/walk.ts +44 -0
  71. package/src/core/surface-schema.ts +86 -0
  72. package/src/eval/assert.ts +55 -0
  73. package/src/eval/run.ts +62 -0
  74. package/src/eval/runner.ts +366 -0
  75. package/src/eval/types.ts +143 -0
  76. package/src/index.ts +15 -0
  77. package/src/repair/render.ts +68 -0
  78. package/src/run/orchestrator.ts +272 -0
  79. package/src/serve.ts +164 -0
@@ -0,0 +1,366 @@
1
+ /**
2
+ * Eval runner: matrix × pipeline → retained audit reports + aggregated
3
+ * results. Every cell run goes through the SAME runPipeline the CLI and demo
4
+ * use — the harness adds no second pipeline, only iteration and aggregation.
5
+ *
6
+ * Determinism contract (the CI gate runs the fake matrix): with `--adapter
7
+ * fake` every generation is scripted from fixtures and the injected clock is
8
+ * fixed, so retained reports and `results.json` are byte-stable. Live runs
9
+ * (`--adapter live`) are documented, never CI-gating.
10
+ *
11
+ * Containment contract (the 2026-07-03 live-run lesson): one run can never
12
+ * kill the matrix. Adapters type their transport failures (failed-adapter
13
+ * outcome, with a report); anything that still escapes a run is contained
14
+ * here as an explicit `error` run — recorded in the distribution with its
15
+ * message and a retained .error.json, never silent, never fatal. The one
16
+ * deliberate exception is UnknownRuleTypeError: a bad contract fails every
17
+ * run identically, so the matrix fails loudly (the linter's exit-4 class).
18
+ * `resume: true` skips runs whose audit report is already retained
19
+ * (summarizing from the retained report) and retries error records.
20
+ *
21
+ * Metric definitions (stated once, computed nowhere else; every rate is
22
+ * over the NON-ERROR runs — contained errors are infrastructure, not
23
+ * observations of the model, and are reported via `errorRuns`):
24
+ * - schemaValidityRate — runs whose attempt-1 S1 gate passed / non-error runs.
25
+ * - firstAttemptViolationRate — runs whose attempt 1 had ≥1 error-level S3
26
+ * finding / non-error runs.
27
+ * - repairSuccessRate — among runs with a first-attempt violation, the
28
+ * fraction that reached an S3-clean attempt within maxRepairs (outcome
29
+ * `passed` or `failed-gate`); null when no run violated (0/0 is not 0).
30
+ * - endToEndPassRate — outcome === "passed" / non-error runs.
31
+ * - s3CleanGateFailures — count of `failed-gate` outcomes: S3 accepted a
32
+ * surface an emitter gate refused (the ADR-D1 family signal; recorded for
33
+ * every cell, probe-marked or not).
34
+ */
35
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
36
+ import { dirname, join, resolve } from "node:path";
37
+ import type { Contract } from "../core/contract.js";
38
+ import { runPipeline } from "../run/orchestrator.js";
39
+ import { UnknownRuleTypeError } from "../core/lint/index.js";
40
+ import { adapterFor } from "../adapters/index.js";
41
+ import { ScriptedAdapter, type ScriptEntry } from "../adapters/fake.js";
42
+ import { contractDigest, sha256, type AuditReportV1 } from "../audit/report.js";
43
+ import type { RepairTemplate } from "../repair/render.js";
44
+ import type {
45
+ CellMetrics,
46
+ CellResult,
47
+ EvalMatrix,
48
+ EvalResults,
49
+ RunSummary,
50
+ ScriptStep,
51
+ } from "./types.js";
52
+
53
+ export interface RunMatrixOptions {
54
+ matrixPath: string;
55
+ outDir: string;
56
+ adapterMode: "fake" | "live";
57
+ /** Fixed clock for deterministic retained reports (fake mode). */
58
+ now?: () => Date;
59
+ /**
60
+ * Skip runs whose audit report is already retained in outDir (summarize
61
+ * from the retained report instead); contained-error records are retried.
62
+ * Makes an interrupted matrix resumable without re-burning compute.
63
+ */
64
+ resume?: boolean;
65
+ /** Progress lines (CLI); observational. */
66
+ log?: (line: string) => void;
67
+ }
68
+
69
+ export async function runMatrix(options: RunMatrixOptions): Promise<EvalResults> {
70
+ const matrixAbs = resolve(options.matrixPath);
71
+ const matrixRaw = readFileSync(matrixAbs, "utf8");
72
+ const matrix = JSON.parse(matrixRaw) as EvalMatrix;
73
+ const matrixDir = dirname(matrixAbs);
74
+ const log = options.log ?? (() => {});
75
+ const maxRepairs = matrix.maxRepairs ?? 2;
76
+ // PR-21: matrix-level emission target; default a2ui (byte-identical prior behavior).
77
+ const emitTarget = matrix.emitTarget ?? "a2ui";
78
+ const templates: RepairTemplate[] = matrix.repairTemplates ?? ["standard"];
79
+
80
+ const contract = JSON.parse(readFileSync(resolve(matrixDir, matrix.contract), "utf8")) as Contract;
81
+
82
+ const models = resolveModels(matrix, options.adapterMode);
83
+ const outDir = resolve(options.outDir);
84
+ mkdirSync(outDir, { recursive: true });
85
+
86
+ const cells: CellResult[] = [];
87
+ for (const model of models) {
88
+ for (const prompt of matrix.prompts) {
89
+ for (const repairTemplate of templates) {
90
+ const runs: RunSummary[] = [];
91
+ for (let run = 1; run <= matrix.runsPerCell; run++) {
92
+ const reportPath = reportRelPath(model, prompt.id, repairTemplate, run);
93
+ // Resume: a retained audit report is a completed observation —
94
+ // summarize it instead of re-running. Contained-error records
95
+ // (.error.json) are NOT skipped: errors are retryable. A retained
96
+ // file that fails to read/parse (e.g. a partial write from the
97
+ // very crash being resumed from) is treated as not retained and
98
+ // the run re-executes — the containment contract applies to the
99
+ // resume path too.
100
+ if (options.resume && existsSync(join(outDir, reportPath))) {
101
+ let retained: AuditReportV1 | null = null;
102
+ try {
103
+ retained = JSON.parse(readFileSync(join(outDir, reportPath), "utf8")) as AuditReportV1;
104
+ } catch {
105
+ log(`${model} × ${prompt.id} × ${repairTemplate} run ${run}/${matrix.runsPerCell}: retained report unreadable — re-running`);
106
+ }
107
+ if (retained) {
108
+ runs.push(summarize(run, retained, exitCodeFor(retained.outcome), reportPath));
109
+ log(`${model} × ${prompt.id} × ${repairTemplate} run ${run}/${matrix.runsPerCell}: ${retained.outcome} (resumed from retained report)`);
110
+ continue;
111
+ }
112
+ }
113
+ const adapter =
114
+ options.adapterMode === "fake"
115
+ ? scriptedAdapter(matrix, prompt.id, matrixDir)
116
+ : adapterFor(model);
117
+ try {
118
+ const result = await runPipeline({
119
+ contract,
120
+ intent: prompt.intent,
121
+ prompt: prompt.prompt,
122
+ adapter,
123
+ maxRepairs,
124
+ repairTemplate,
125
+ emitTarget,
126
+ now: options.now,
127
+ });
128
+ retain(outDir, reportPath, result.report);
129
+ runs.push(summarize(run, result.report, result.exitCode, reportPath));
130
+ log(
131
+ `${model} × ${prompt.id} × ${repairTemplate} run ${run}/${matrix.runsPerCell}: ${result.report.outcome}`,
132
+ );
133
+ } catch (error) {
134
+ // Per-run containment: a raw crash (anything the pipeline did
135
+ // not convert into an outcome) is recorded as an explicit
136
+ // `error` run — visible in the distribution, never silent, and
137
+ // never fatal to the rest of the matrix. Deliberate exception:
138
+ // UnknownRuleTypeError means the CONTRACT is bad — every run
139
+ // would fail identically, so the matrix fails loudly (exit 4).
140
+ if (error instanceof UnknownRuleTypeError) throw error;
141
+ const message = error instanceof Error ? `${error.constructor.name}: ${error.message}` : String(error);
142
+ const errorPath = reportPath.replace(/\.audit-report\.json$/, ".error.json");
143
+ retainRaw(outDir, errorPath, JSON.stringify({ error: message }, null, 2) + "\n");
144
+ runs.push({
145
+ run,
146
+ outcome: "error",
147
+ exitCode: -1,
148
+ attempts: 0,
149
+ error: message,
150
+ firstAttemptSchemaValid: false,
151
+ firstAttemptViolated: false,
152
+ firstAttemptRuleIds: [],
153
+ s3CleanButGateFailed: false,
154
+ reportPath: errorPath,
155
+ });
156
+ log(`${model} × ${prompt.id} × ${repairTemplate} run ${run}/${matrix.runsPerCell}: ERROR (contained) — ${message}`);
157
+ }
158
+ }
159
+ cells.push({
160
+ model,
161
+ promptId: prompt.id,
162
+ repairShape: prompt.repairShape,
163
+ repairTemplate,
164
+ adrD1Probe: prompt.adrD1Probe ?? false,
165
+ runs,
166
+ metrics: computeMetrics(runs),
167
+ });
168
+ }
169
+ }
170
+ }
171
+
172
+ const results: EvalResults = {
173
+ matrixSha256: sha256(matrixRaw),
174
+ contract: contractDigest(contract),
175
+ maxRepairs,
176
+ cells,
177
+ byModel: rollupByModel(cells),
178
+ byRule: rollupByRule(cells),
179
+ };
180
+ writeFileSync(join(outDir, "results.json"), JSON.stringify(results, null, 2) + "\n");
181
+ writeFileSync(join(outDir, "report.md"), renderReportMarkdown(results));
182
+ return results;
183
+ }
184
+
185
+ function resolveModels(matrix: EvalMatrix, mode: "fake" | "live"): string[] {
186
+ if (mode === "fake") {
187
+ if (!matrix.scripts) throw new Error("fake mode requires `scripts` in the matrix");
188
+ return ["fake:scripted"];
189
+ }
190
+ if (!matrix.models || matrix.models.length === 0) {
191
+ throw new Error("live mode requires `models` in the matrix (model ids are config, never code)");
192
+ }
193
+ return matrix.models;
194
+ }
195
+
196
+ function scriptedAdapter(matrix: EvalMatrix, promptId: string, matrixDir: string): ScriptedAdapter {
197
+ const steps = matrix.scripts?.[promptId];
198
+ if (!steps) throw new Error(`fake matrix has no script for prompt '${promptId}'`);
199
+ const entries: ScriptEntry[] = steps.map((step: ScriptStep) =>
200
+ "error" in step
201
+ ? { error: step.error }
202
+ : { output: JSON.parse(readFileSync(resolve(matrixDir, step.fixture), "utf8")) as unknown },
203
+ );
204
+ return new ScriptedAdapter(entries);
205
+ }
206
+
207
+ function reportRelPath(model: string, promptId: string, template: RepairTemplate, run: number): string {
208
+ const slug = (s: string): string => s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
209
+ return join("reports", `${slug(model)}--${promptId}--${template}--r${run}.audit-report.json`);
210
+ }
211
+
212
+ function retain(outDir: string, rel: string, report: AuditReportV1): void {
213
+ retainRaw(outDir, rel, JSON.stringify(report, null, 2) + "\n");
214
+ }
215
+
216
+ function retainRaw(outDir: string, rel: string, content: string): void {
217
+ mkdirSync(join(outDir, "reports"), { recursive: true });
218
+ writeFileSync(join(outDir, rel), content);
219
+ }
220
+
221
+ /** The CLI's exit-code mapping, reproduced for resumed (retained) reports. */
222
+ function exitCodeFor(outcome: string): number {
223
+ return outcome === "passed" ? 0 : outcome === "failed-lint-exhausted" ? 2 : outcome === "failed-gate" ? 3 : 1;
224
+ }
225
+
226
+ /**
227
+ * dspack-gen#19: classify a failed-adapter from its typed error message.
228
+ * Conservative by design — only KNOWN infrastructure signatures classify as
229
+ * `no-generation` (and thereby leave the denominators); anything else is
230
+ * treated as a model observation.
231
+ */
232
+ export function classifyAdapterFailure(adapterError: string): "no-generation" | "generation-then-bad-output" {
233
+ const infrastructure =
234
+ /HTTP \d{3}/.test(adapterError) ||
235
+ /grammar is too large/i.test(adapterError) ||
236
+ /fetch failed|ECONNREFUSED|ECONNRESET|ETIMEDOUT|socket|network|timed? ?out/i.test(adapterError);
237
+ return infrastructure ? "no-generation" : "generation-then-bad-output";
238
+ }
239
+
240
+ /**
241
+ * Per #19's definition, `no-generation` applies only when NO attempt produced
242
+ * output: a run whose repair attempt dies on transport still observed the
243
+ * model on attempt 1 and stays counted.
244
+ */
245
+ export function classifyFailedAdapterRun(
246
+ attempts: Array<{ surface?: unknown; adapterError?: string }>,
247
+ ): "no-generation" | "generation-then-bad-output" {
248
+ if (attempts.some((a) => a.surface !== undefined)) return "generation-then-bad-output";
249
+ return classifyAdapterFailure(attempts.find((a) => a.adapterError)?.adapterError ?? "");
250
+ }
251
+
252
+ function summarize(run: number, report: AuditReportV1, exitCode: number, reportPath: string): RunSummary {
253
+ const first = report.attempts[0];
254
+ const firstGates = first?.gates ?? [];
255
+ const s1 = firstGates.find((g) => g.gate === "S1");
256
+ const errorFindings = (first?.findings ?? []).filter((f) => f.level === "error");
257
+ const firstAttemptViolated = errorFindings.length > 0;
258
+ const lintCleanReached = report.outcome === "passed" || report.outcome === "failed-gate";
259
+ return {
260
+ run,
261
+ outcome: report.outcome,
262
+ exitCode,
263
+ attempts: report.attempts.length,
264
+ firstAttemptSchemaValid: s1?.status === "PASS",
265
+ firstAttemptViolated,
266
+ firstAttemptRuleIds: [...new Set(errorFindings.map((f) => f.ruleId))].sort(),
267
+ ...(firstAttemptViolated ? { repaired: lintCleanReached } : {}),
268
+ s3CleanButGateFailed: report.outcome === "failed-gate",
269
+ ...(report.outcome === "failed-adapter" ? { adapterFailureClass: classifyFailedAdapterRun(report.attempts) } : {}),
270
+ reportPath,
271
+ };
272
+ }
273
+
274
+ export function computeMetrics(runs: RunSummary[]): CellMetrics {
275
+ // Contained `error` runs and pre-generation failed-adapters (dspack-gen#19)
276
+ // are infrastructure, not model behavior: counted (visible) but excluded
277
+ // from every rate denominator below.
278
+ const errorRuns = runs.filter((r) => r.outcome === "error").length;
279
+ const noGeneration = runs.filter((r) => r.adapterFailureClass === "no-generation").length;
280
+ const observed = runs.filter((r) => r.outcome !== "error" && r.adapterFailureClass !== "no-generation");
281
+ const n = observed.length;
282
+ const violated = observed.filter((r) => r.firstAttemptViolated);
283
+ const rate = (k: number, d: number): number => (d === 0 ? 0 : round4(k / d));
284
+ return {
285
+ runs: runs.length,
286
+ errorRuns,
287
+ noGenerationRuns: noGeneration,
288
+ schemaValidityRate: rate(observed.filter((r) => r.firstAttemptSchemaValid).length, n),
289
+ firstAttemptViolationRate: rate(violated.length, n),
290
+ repairSuccessRate: violated.length === 0 ? null : round4(violated.filter((r) => r.repaired).length / violated.length),
291
+ endToEndPassRate: rate(observed.filter((r) => r.outcome === "passed").length, n),
292
+ s3CleanGateFailures: observed.filter((r) => r.s3CleanButGateFailed).length,
293
+ };
294
+ }
295
+
296
+ function round4(x: number): number {
297
+ return Math.round(x * 10000) / 10000;
298
+ }
299
+
300
+ function rollupByModel(cells: CellResult[]): Record<string, CellMetrics> {
301
+ const byModel: Record<string, CellMetrics> = {};
302
+ const keys = [...new Set(cells.map((c) => c.model))].sort();
303
+ for (const model of keys) {
304
+ byModel[model] = computeMetrics(cells.filter((c) => c.model === model).flatMap((c) => c.runs));
305
+ }
306
+ return byModel;
307
+ }
308
+
309
+ function rollupByRule(cells: CellResult[]): EvalResults["byRule"] {
310
+ const byRule: EvalResults["byRule"] = {};
311
+ for (const cell of cells) {
312
+ for (const run of cell.runs) {
313
+ for (const ruleId of run.firstAttemptRuleIds) {
314
+ const entry = (byRule[ruleId] ??= { firstAttemptViolations: 0, repaired: 0, unrepaired: 0 });
315
+ entry.firstAttemptViolations++;
316
+ if (run.repaired) entry.repaired++;
317
+ else entry.unrepaired++;
318
+ }
319
+ }
320
+ }
321
+ return Object.fromEntries(Object.entries(byRule).sort(([a], [b]) => a.localeCompare(b)));
322
+ }
323
+
324
+ /** Human view — derived, never the artifact. */
325
+ export function renderReportMarkdown(results: EvalResults): string {
326
+ const lines: string[] = [
327
+ "# Eval results",
328
+ "",
329
+ `- Contract: ${results.contract.name} (dspack ${results.contract.dspack}, sha256 ${results.contract.sha256.slice(0, 12)}…)`,
330
+ `- Matrix sha256: ${results.matrixSha256.slice(0, 12)}… · maxRepairs: ${results.maxRepairs}`,
331
+ "",
332
+ "## Per model",
333
+ "",
334
+ "(rates are over observed runs; `errors` are contained crashes and `no-gen` are pre-generation adapter failures — both infrastructure, not model behavior; dspack-gen#19)",
335
+ "",
336
+ "| model | runs | errors | no-gen | schema-valid | 1st-attempt violation | repair success | e2e pass | S3-clean gate failures |",
337
+ "|---|---|---|---|---|---|---|---|---|",
338
+ ];
339
+ for (const [model, m] of Object.entries(results.byModel)) {
340
+ lines.push(
341
+ `| ${model} | ${m.runs} | ${m.errorRuns} | ${m.noGenerationRuns} | ${pct(m.schemaValidityRate)} | ${pct(m.firstAttemptViolationRate)} | ${m.repairSuccessRate === null ? "n/a" : pct(m.repairSuccessRate)} | ${pct(m.endToEndPassRate)} | ${m.s3CleanGateFailures} |`,
342
+ );
343
+ }
344
+ lines.push("", "## Per rule (first-attempt violations across all cells)", "", "| rule | violations | repaired | unrepaired |", "|---|---|---|---|");
345
+ for (const [rule, r] of Object.entries(results.byRule)) {
346
+ lines.push(`| ${rule} | ${r.firstAttemptViolations} | ${r.repaired} | ${r.unrepaired} |`);
347
+ }
348
+ lines.push(
349
+ "",
350
+ "## Per cell",
351
+ "",
352
+ "| model | prompt | shape | template | ADR-D1 probe | errors | no-gen | violation | repair | e2e | gate-fail |",
353
+ "|---|---|---|---|---|---|---|---|---|---|---|",
354
+ );
355
+ for (const c of results.cells) {
356
+ lines.push(
357
+ `| ${c.model} | ${c.promptId} | ${c.repairShape} | ${c.repairTemplate} | ${c.adrD1Probe ? "yes" : ""} | ${c.metrics.errorRuns} | ${c.metrics.noGenerationRuns} | ${pct(c.metrics.firstAttemptViolationRate)} | ${c.metrics.repairSuccessRate === null ? "n/a" : pct(c.metrics.repairSuccessRate)} | ${pct(c.metrics.endToEndPassRate)} | ${c.metrics.s3CleanGateFailures} |`,
358
+ );
359
+ }
360
+ lines.push("");
361
+ return lines.join("\n");
362
+ }
363
+
364
+ function pct(x: number): string {
365
+ return `${Math.round(x * 1000) / 10}%`;
366
+ }
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Eval harness (PR-10) — matrix configuration and result shapes.
3
+ *
4
+ * Model identity is configuration: every model ID lives in the matrix file
5
+ * (`eval/matrix.json`), never in code. The prompt set deliberately spans
6
+ * repair shapes — substitution, addition, deletion, restructuring — and the
7
+ * matrix crosses the resistant (removal-shaped) prompts with a repair
8
+ * template variant (see src/repair/render.ts), per the repair-shape
9
+ * hypothesis recorded as design input in the plan (hypothesis, not finding).
10
+ */
11
+ import type { RepairTemplate } from "../repair/render.js";
12
+
13
+ /** The repair shape a prompt is designed to elicit, per the plan's PR-10 matrix design. */
14
+ export type RepairShape = "substitution" | "addition" | "deletion" | "restructuring";
15
+
16
+ export interface EvalPrompt {
17
+ id: string;
18
+ intent: string;
19
+ prompt: string;
20
+ /** The repair shape this prompt targets (metadata for aggregation, never asserted). */
21
+ repairShape: RepairShape;
22
+ /** Rules this prompt is designed to tempt violations of (metadata, never asserted). */
23
+ targetRules?: string[];
24
+ /**
25
+ * Design-input marker: prompts probing the ADR-D1 gap family (S3-clean
26
+ * surfaces the emitter cannot project — text placement/absence). Scored on
27
+ * the S3-clean → A-gate outcome every cell already records.
28
+ */
29
+ adrD1Probe?: boolean;
30
+ }
31
+
32
+ export interface EvalMatrix {
33
+ /** Path to the contract, relative to the matrix file. */
34
+ contract: string;
35
+ /** Independent runs per cell — rate-with-spread, not anecdote (the n=1 question). */
36
+ runsPerCell: number;
37
+ maxRepairs?: number;
38
+ /**
39
+ * Model refs (`ollama:<tag>` | `anthropic:<id>`), verified against the
40
+ * live providers at config-authoring time. Fake matrices omit this and
41
+ * define `scripts` instead.
42
+ */
43
+ models?: string[];
44
+ /** Emission target for every run in the matrix (default "a2ui"; PR-21). */
45
+ emitTarget?: "a2ui" | "json-render";
46
+ /** Repair templates to cross with the prompts (default: ["standard"]). */
47
+ repairTemplates?: RepairTemplate[];
48
+ prompts: EvalPrompt[];
49
+ /**
50
+ * Fake mode (`--adapter fake`): per prompt id, the scripted generation
51
+ * sequence, as fixture paths (relative to the matrix file) or inline
52
+ * `{ error }` entries. Deterministic — this is the CI gate's mode.
53
+ */
54
+ scripts?: Record<string, ScriptStep[]>;
55
+ }
56
+
57
+ export type ScriptStep = { fixture: string } | { error: string };
58
+
59
+ // ---------------------------------------------------------------------------
60
+ // Results
61
+ // ---------------------------------------------------------------------------
62
+
63
+ export interface RunSummary {
64
+ run: number;
65
+ /**
66
+ * A pipeline outcome (`passed` | `failed-lint-exhausted` | `failed-gate` |
67
+ * `failed-adapter`) or `error` — a contained per-run crash (a raw
68
+ * exception the pipeline did not convert into an outcome). `error` runs
69
+ * are visible in the distribution but are NOT observations of the model:
70
+ * they are excluded from the model-behavior rate denominators.
71
+ */
72
+ outcome: string;
73
+ exitCode: number;
74
+ attempts: number;
75
+ /** The contained crash message (outcome "error" only). */
76
+ error?: string;
77
+ /** Attempt-1 S1 gate passed (schema validity — verified, never assumed). */
78
+ firstAttemptSchemaValid: boolean;
79
+ /** Attempt 1 had ≥1 error-level S3 finding. */
80
+ firstAttemptViolated: boolean;
81
+ /** Rule ids of attempt-1 error findings. */
82
+ firstAttemptRuleIds: string[];
83
+ /**
84
+
85
+ * Reached an S3-clean attempt after at least one repair (defined only
86
+ * when firstAttemptViolated).
87
+ */
88
+ repaired?: boolean;
89
+ /** S3-clean but refused by an emitter gate — the ADR-D1 signal. */
90
+ s3CleanButGateFailed: boolean;
91
+ /**
92
+ * Classification of a `failed-adapter` outcome (dspack-gen#19), from the
93
+ * report's typed adapterError message: `no-generation` — the request never
94
+ * yielded a model turn (transport failure, HTTP/API reject incl. the
95
+ * grammar-too-large 400s) — is infrastructure and excluded from
96
+ * model-behavior denominators exactly like a contained `error`;
97
+ * `generation-then-bad-output` — the request completed and the MODEL
98
+ * produced unusable output (empty, non-JSON, truncated, refusal) — is a
99
+ * real observation and stays counted. Unknown messages default to
100
+ * `generation-then-bad-output`: only known infrastructure signatures may
101
+ * shrink a denominator.
102
+ */
103
+ adapterFailureClass?: "no-generation" | "generation-then-bad-output";
104
+ /** Relative path of the retained audit report. */
105
+ reportPath: string;
106
+ }
107
+
108
+ export interface CellResult {
109
+ model: string;
110
+ promptId: string;
111
+ repairShape: RepairShape;
112
+ repairTemplate: RepairTemplate;
113
+ adrD1Probe: boolean;
114
+ runs: RunSummary[];
115
+ metrics: CellMetrics;
116
+ }
117
+
118
+ export interface CellMetrics {
119
+ /** Total runs, including contained errors. */
120
+ runs: number;
121
+ /** Contained per-run crashes — infrastructure, not model behavior. */
122
+ errorRuns: number;
123
+ /** Pre-generation failed-adapters (dspack-gen#19) — infrastructure, not model behavior. */
124
+ noGenerationRuns: number;
125
+ /** All rates below are over the observed runs (runs − errorRuns − noGenerationRuns). */
126
+ schemaValidityRate: number;
127
+ firstAttemptViolationRate: number;
128
+ /** Over runs with a first-attempt violation; null when none occurred. */
129
+ repairSuccessRate: number | null;
130
+ endToEndPassRate: number;
131
+ s3CleanGateFailures: number;
132
+ }
133
+
134
+ export interface EvalResults {
135
+ matrixSha256: string;
136
+ contract: { name: string; dspack: string; sha256: string };
137
+ maxRepairs: number;
138
+ cells: CellResult[];
139
+ /** Per model × template rollup (the eval:assert surface). */
140
+ byModel: Record<string, CellMetrics>;
141
+ /** Per rule: first-attempt violation and repair-success counts across all cells. */
142
+ byRule: Record<string, { firstAttemptViolations: number; repaired: number; unrepaired: number }>;
143
+ }
package/src/index.ts ADDED
@@ -0,0 +1,15 @@
1
+ /**
2
+ * @aestheticfunction/dspack-gen — public entry.
3
+ *
4
+ * `./core` remains the zero-network, emitter-free subpath (compiler + linter)
5
+ * that ds-mcp depends on; this root entry adds the network-touching pipeline:
6
+ * adapters, orchestrator, repair rendering, audit report.
7
+ */
8
+ export * from "./core/index.js";
9
+ export * from "./adapters/index.js";
10
+ export { ScriptedAdapter, type ScriptEntry } from "./adapters/fake.js";
11
+ export { renderRepairMessage, type RepairTemplate } from "./repair/render.js";
12
+ export * from "./audit/report.js";
13
+ export { runPipeline, type RunOptions, type RunResult } from "./run/orchestrator.js";
14
+ export { runMatrix, computeMetrics, type RunMatrixOptions } from "./eval/runner.js";
15
+ export type { EvalMatrix, EvalPrompt, EvalResults, CellResult, CellMetrics, RepairShape } from "./eval/types.js";
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Repair feedback (ADR-7): ONE findings object, two serializations. This
3
+ * module renders the linter's findings — the same objects embedded verbatim
4
+ * in the audit report — into the deterministic user message appended for a
5
+ * repair attempt. The system prompt is never mutated between attempts; this
6
+ * message is the only delta.
7
+ */
8
+ import type { Contract } from "../core/contract.js";
9
+ import type { Finding } from "../core/lint/findings.js";
10
+
11
+ /**
12
+ * Template variants (PR-10 matrix design input, hypothesis-testing only):
13
+ * the standard template's "Do not change parts that were not flagged" may
14
+ * inhibit removal/restructuring-shaped repairs (e.g. un-nesting an
15
+ * interactive element). `permit-restructuring` swaps exactly that one
16
+ * instruction; everything else — findings serialization, corrected
17
+ * references, schema instruction — is byte-identical, so eval A/Bs isolate
18
+ * the single variable. The variant is recorded in the audit report.
19
+ */
20
+ export type RepairTemplate = "standard" | "permit-restructuring";
21
+
22
+ const CLOSING_INSTRUCTION: Record<RepairTemplate, string> = {
23
+ standard: "Do not change parts of the surface that were not flagged.",
24
+ "permit-restructuring":
25
+ "Remove or restructure whatever is necessary to fix the violations, including deleting the offending elements; preserve unaffected content where possible.",
26
+ };
27
+
28
+ export function renderRepairMessage(
29
+ findings: Finding[],
30
+ contract: Contract,
31
+ template: RepairTemplate = "standard",
32
+ ): string {
33
+ const errors = findings.filter((f) => f.level === "error");
34
+ const lines: string[] = [
35
+ `Your surface violates ${errors.length} governance rule finding(s) of the "${contract.name}" design system:`,
36
+ "",
37
+ ];
38
+
39
+ errors.forEach((finding, index) => {
40
+ const where = finding.location.nodeId
41
+ ? `${finding.location.path} (component: ${finding.location.component}, id: "${finding.location.nodeId}")`
42
+ : `${finding.location.path} (component: ${finding.location.component})`;
43
+ lines.push(
44
+ `Violation ${index + 1}: [${finding.ruleId} / ${finding.requirement}]`,
45
+ ` At: ${where}`,
46
+ ` ${finding.message}`,
47
+ ` Why: ${finding.rationale}`,
48
+ );
49
+ });
50
+
51
+ // Corrected references: each linked example once, verbatim (ADR-3 — the
52
+ // surface format IS the generation format).
53
+ const exampleIds = [...new Set(errors.flatMap((f) => f.exampleIds))];
54
+ for (const exampleId of exampleIds) {
55
+ const example = (contract.examples ?? []).find((e) => e.id === exampleId);
56
+ if (example) {
57
+ lines.push("", `A correct example (${example.id}): ${JSON.stringify(example.surface)}`);
58
+ }
59
+ }
60
+
61
+ lines.push(
62
+ "",
63
+ "Produce a corrected dspack surface document that fixes every violation above.",
64
+ CLOSING_INSTRUCTION[template],
65
+ "Respond with a single JSON object conforming to the provided schema. No commentary.",
66
+ );
67
+ return lines.join("\n");
68
+ }