@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,123 @@
1
+ /**
2
+ * Ollama adapter — local generation via structured outputs (`format` = the
3
+ * generation schema on /api/chat). Mandatory for the local path per the plan;
4
+ * there is no unconstrained-JSON fallback in this design.
5
+ *
6
+ * S0 spike caveat carried here: on mlx-engine models Ollama silently ignores
7
+ * `format`. The adapter cannot detect the engine from the chat response, so
8
+ * it records the model tag in `meta` and relies on gates S1/S2 to catch
9
+ * unconstrained output; non-JSON output raises AdapterOutputError.
10
+ */
11
+ import { Agent } from "undici";
12
+ import {
13
+ AdapterOutputError,
14
+ parseJsonOutput,
15
+ type FetchLike,
16
+ type GenerateRequest,
17
+ type GenerateResult,
18
+ type GenerationAdapter,
19
+ } from "./types.js";
20
+
21
+ /**
22
+ * Local inference is legitimately slow: a single 35B generation can exceed
23
+ * undici's default 300s headersTimeout, which killed runs at exactly ~301s
24
+ * in the 2026-07-03 eval (reports with zero attempts, "fetch failed").
25
+ * One long-lived dispatcher with the header/body timeouts raised to an hour
26
+ * — a deliberate ceiling, not "no timeout": a truly hung server should
27
+ * still fail rather than block a matrix forever.
28
+ */
29
+ const LOCAL_INFERENCE_TIMEOUT_MS = 60 * 60 * 1000;
30
+
31
+ /** undici's fetch-init extension, typed so the rest of the init stays checked. */
32
+ interface DispatchedRequestInit extends RequestInit {
33
+ dispatcher?: Agent;
34
+ }
35
+ const localInferenceDispatcher = new Agent({
36
+ headersTimeout: LOCAL_INFERENCE_TIMEOUT_MS,
37
+ bodyTimeout: LOCAL_INFERENCE_TIMEOUT_MS,
38
+ });
39
+
40
+ export interface OllamaAdapterOptions {
41
+ /** Required — no default model exists in code (ADR-9 as amended). */
42
+ model: string;
43
+ /** Defaults to OLLAMA_HOST env or http://localhost:11434. */
44
+ host?: string;
45
+ fetch?: FetchLike;
46
+ }
47
+
48
+ interface OllamaChatResponse {
49
+ model?: string;
50
+ message?: { content?: string };
51
+ prompt_eval_count?: number;
52
+ eval_count?: number;
53
+ total_duration?: number;
54
+ load_duration?: number;
55
+ prompt_eval_duration?: number;
56
+ eval_duration?: number;
57
+ }
58
+
59
+ export class OllamaAdapter implements GenerationAdapter {
60
+ readonly id: string;
61
+ private readonly model: string;
62
+ private readonly host: string;
63
+ private readonly fetchImpl: FetchLike;
64
+
65
+ constructor(options: OllamaAdapterOptions) {
66
+ if (!options.model) throw new Error("OllamaAdapter requires an explicit model id (config/env/flag)");
67
+ this.model = options.model;
68
+ this.host = options.host ?? process.env.OLLAMA_HOST ?? "http://localhost:11434";
69
+ this.fetchImpl = options.fetch ?? fetch;
70
+ this.id = `ollama:${this.model}`;
71
+ }
72
+
73
+ async generate(request: GenerateRequest): Promise<GenerateResult> {
74
+ const body = {
75
+ model: this.model,
76
+ stream: false,
77
+ format: request.jsonSchema,
78
+ options: { temperature: request.params?.temperature ?? 0.2 },
79
+ messages: [{ role: "system", content: request.system }, ...request.messages],
80
+ };
81
+ // The whole transport exchange is typed: a rejected fetch (connection
82
+ // reset, timeout under a large model load) must surface as a
83
+ // failed-adapter outcome with a report, never as a raw exception that
84
+ // kills a whole eval matrix (the 2026-07-03 live-run crash).
85
+ let data: OllamaChatResponse;
86
+ try {
87
+ const init: DispatchedRequestInit = {
88
+ method: "POST",
89
+ headers: { "content-type": "application/json" },
90
+ body: JSON.stringify(body),
91
+ // undici extension of fetch(init); ignored by injected test fetches.
92
+ dispatcher: localInferenceDispatcher,
93
+ };
94
+ const response = await this.fetchImpl(`${this.host}/api/chat`, init);
95
+ if (!response.ok) {
96
+ // Body read can itself fail on a broken stream — keep the HTTP
97
+ // status in the error either way.
98
+ const detail = await response.text().catch(() => "(response body unreadable)");
99
+ throw new AdapterOutputError(this.id, `HTTP ${response.status}: ${detail.slice(0, 300)}`);
100
+ }
101
+ data = (await response.json()) as OllamaChatResponse;
102
+ } catch (error) {
103
+ if (error instanceof AdapterOutputError) throw error;
104
+ throw new AdapterOutputError(this.id, `transport failure: ${error instanceof Error ? error.message : String(error)}`);
105
+ }
106
+ const raw = data.message?.content ?? "";
107
+ if (raw === "") throw new AdapterOutputError(this.id, "empty model output");
108
+
109
+ return {
110
+ json: parseJsonOutput(this.id, raw),
111
+ raw,
112
+ model: data.model ?? this.model,
113
+ usage: { inputTokens: data.prompt_eval_count, outputTokens: data.eval_count },
114
+ meta: {
115
+ provider: "ollama",
116
+ total_duration: data.total_duration,
117
+ load_duration: data.load_duration,
118
+ prompt_eval_duration: data.prompt_eval_duration,
119
+ eval_duration: data.eval_duration,
120
+ },
121
+ };
122
+ }
123
+ }
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Generation adapter interface (ADR-9) — the thesis-bearing seam between the
3
+ * pipeline and model providers. Stateless: one attempt per call; the repair
4
+ * loop owns conversation state.
5
+ *
6
+ * Model identity is CONFIGURATION: adapters require an explicit model ID
7
+ * (config/env/CLI flag) and no default model name exists in this codebase —
8
+ * defaults are resolved at implementation/run time and recorded in config.
9
+ *
10
+ * Honesty note (S0 spike, docs/spike-structured-outputs.md): constrained
11
+ * decoding cannot be assumed to have been applied — Ollama's mlx engine
12
+ * silently ignores `format`. Adapters guarantee only that the result parses
13
+ * as JSON (AdapterOutputError otherwise); schema/vocabulary/governance
14
+ * conformance is judged by the surface gates S1/S2/S3 over the artifact.
15
+ */
16
+
17
+ export interface GenerateMessage {
18
+ role: "user" | "assistant";
19
+ content: string;
20
+ }
21
+
22
+ export interface GenerateRequest {
23
+ system: string;
24
+ messages: GenerateMessage[];
25
+ /** The generation schema (constrained decoding / output schema). */
26
+ jsonSchema: Record<string, unknown>;
27
+ params?: {
28
+ /** Sent to providers that accept it (Ollama). Never sent to Anthropic (removed on current models). */
29
+ temperature?: number;
30
+ maxTokens?: number;
31
+ };
32
+ }
33
+
34
+ export interface GenerateResult {
35
+ /** The parsed JSON output — shape-checked by gates, not here. */
36
+ json: unknown;
37
+ /** The raw text the model produced. */
38
+ raw: string;
39
+ /** The model that actually served the request, as reported by the provider. */
40
+ model: string;
41
+ usage?: { inputTokens?: number; outputTokens?: number };
42
+ /** Provider-specific timings/engine info, recorded verbatim in audit reports. */
43
+ meta?: Record<string, unknown>;
44
+ }
45
+
46
+ export interface GenerationAdapter {
47
+ /** Stable identity for audit reports, e.g. "ollama:<model-tag>" or "anthropic:<model-id>". */
48
+ readonly id: string;
49
+ generate(request: GenerateRequest): Promise<GenerateResult>;
50
+ }
51
+
52
+ /** The model produced output the adapter cannot hand to the pipeline (non-JSON, refusal, empty). */
53
+ export class AdapterOutputError extends Error {
54
+ constructor(
55
+ readonly adapterId: string,
56
+ message: string,
57
+ readonly rawHead?: string,
58
+ ) {
59
+ super(`[${adapterId}] ${message}${rawHead ? ` (output head: ${JSON.stringify(rawHead.slice(0, 200))})` : ""}`);
60
+ this.name = "AdapterOutputError";
61
+ }
62
+ }
63
+
64
+ /** Injectable for offline, deterministic tests. */
65
+ export type FetchLike = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
66
+
67
+ export function parseJsonOutput(adapterId: string, raw: string): unknown {
68
+ try {
69
+ return JSON.parse(raw);
70
+ } catch {
71
+ throw new AdapterOutputError(
72
+ adapterId,
73
+ "model output is not valid JSON — constrained decoding was not applied or failed (see docs/spike-structured-outputs.md: some engines silently ignore output schemas)",
74
+ raw,
75
+ );
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Parse a `--model` flag: "ollama:<model-id>" or "anthropic:<model-id>".
81
+ * The model id itself may contain colons (ollama tags carry a size suffix).
82
+ */
83
+ export function parseModelRef(ref: string): { provider: "ollama" | "anthropic"; model: string } {
84
+ const sep = ref.indexOf(":");
85
+ const provider = sep === -1 ? ref : ref.slice(0, sep);
86
+ const model = sep === -1 ? "" : ref.slice(sep + 1);
87
+ if ((provider !== "ollama" && provider !== "anthropic") || model === "") {
88
+ throw new Error(`invalid model reference '${ref}' (expected ollama:<model-id> or anthropic:<model-id>)`);
89
+ }
90
+ return { provider, model };
91
+ }
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Audit report v1 (ADR-8): a versioned, schema'd artifact from day one.
3
+ * JSON is the artifact (schemas/audit-report.v1.schema.json); the Markdown
4
+ * rendering is a derived view. Stability guarantee: additive-only within
5
+ * reportVersion "1" (docs/AUDIT.md).
6
+ */
7
+ import { createHash } from "node:crypto";
8
+ import type { Contract } from "../core/contract.js";
9
+ import type { Finding, GateReport } from "../core/lint/findings.js";
10
+
11
+ export const REPORT_VERSION = "1";
12
+
13
+ export type Outcome = "passed" | "failed-lint-exhausted" | "failed-gate" | "failed-adapter";
14
+
15
+ /** Emitter gate result, keyed by the architecture-wide A-gate names. */
16
+ export interface EmitterGate {
17
+ gate: "A1" | "A2" | "A3" | "J2" | "J3";
18
+ name: string;
19
+ pass: boolean;
20
+ errors?: string[];
21
+ }
22
+
23
+ export interface AttemptRecord {
24
+ index: number;
25
+ /** The generated dspack surface (absent when the adapter failed). */
26
+ surface?: unknown;
27
+ model?: string;
28
+ usage?: { inputTokens?: number; outputTokens?: number };
29
+ meta?: Record<string, unknown>;
30
+ adapterError?: string;
31
+ /** Surface gates S1/S2/S3, independently reported. */
32
+ gates?: GateReport[];
33
+ findings?: Finding[];
34
+ }
35
+
36
+ export interface EmittedValidation {
37
+ /** A2UI catalog version validated against (a2ui target only; absent for json-render). */
38
+ a2uiVersion?: string;
39
+ gates: EmitterGate[];
40
+ }
41
+
42
+ export interface AuditReportV1 {
43
+ reportVersion: typeof REPORT_VERSION;
44
+ createdAt: string;
45
+ request: {
46
+ prompt: string;
47
+ intent: string;
48
+ contract: { name: string; dspack: string; sha256: string };
49
+ };
50
+ generation: {
51
+ adapterId: string;
52
+ schemaSha256: string;
53
+ maxRepairs: number;
54
+ ruleSteering: boolean;
55
+ /** ADR-7 repair template variant used for feedback (additive in v1; absent ⇒ "standard"). */
56
+ repairTemplate?: string;
57
+ };
58
+ attempts: AttemptRecord[];
59
+ /** Repair messages verbatim — rendered from the same findings objects above. */
60
+ repairMessages: string[];
61
+ outcome: Outcome;
62
+ emitted?: {
63
+ target: "a2ui" | "json-render";
64
+ /** Absent when the emitter refused the surface outright (see refusal). */
65
+ surfaceMessages?: unknown;
66
+ /** Emitter warnings — every synthesis/drop, nothing silent. */
67
+ warnings: Array<{ code: string; message: string }>;
68
+ validations: EmittedValidation[];
69
+ /**
70
+ * The emitter's typed refusal message when a lint-clean surface could
71
+ * not be emitted at all (additive in v1) — the target-equivalent
72
+ * emitter-gate failure; validations is empty in that case.
73
+ */
74
+ refusal?: string;
75
+ };
76
+ timings: { totalMs: number };
77
+ }
78
+
79
+ export function sha256(value: unknown): string {
80
+ return createHash("sha256").update(typeof value === "string" ? value : JSON.stringify(value)).digest("hex");
81
+ }
82
+
83
+ export function contractDigest(contract: Contract): { name: string; dspack: string; sha256: string } {
84
+ return { name: contract.name, dspack: contract.dspack, sha256: sha256(contract as unknown as Record<string, unknown>) };
85
+ }
86
+
87
+ /** Human view of the JSON artifact — derived, never the source of truth. */
88
+ export function renderMarkdown(report: AuditReportV1): string {
89
+ const lines: string[] = [
90
+ `# Audit report (v${report.reportVersion})`,
91
+ "",
92
+ `- **Outcome:** ${report.outcome}`,
93
+ `- **Prompt:** ${report.request.prompt}`,
94
+ `- **Intent:** ${report.request.intent}`,
95
+ `- **Contract:** ${report.request.contract.name} (dspack ${report.request.contract.dspack}, sha256 ${report.request.contract.sha256.slice(0, 12)}…)`,
96
+ `- **Adapter:** ${report.generation.adapterId} (max repairs: ${report.generation.maxRepairs}, rule steering: ${report.generation.ruleSteering})`,
97
+ `- **Created:** ${report.createdAt} · **Duration:** ${report.timings.totalMs} ms`,
98
+ ];
99
+
100
+ report.attempts.forEach((attempt) => {
101
+ lines.push("", `## Attempt ${attempt.index + 1}`);
102
+ if (attempt.adapterError) {
103
+ lines.push(`- Adapter error: ${attempt.adapterError}`);
104
+ return;
105
+ }
106
+ if (attempt.model) lines.push(`- Model: ${attempt.model}`);
107
+ for (const gate of attempt.gates ?? []) {
108
+ lines.push(`- gate ${gate.gate} ${gate.name}: **${gate.status}**`);
109
+ }
110
+ for (const finding of attempt.findings ?? []) {
111
+ lines.push(
112
+ ` - ${finding.level} [${finding.requirement}] ${finding.ruleId} at ${finding.location.path} — ${finding.message}`,
113
+ );
114
+ }
115
+ const repair = report.repairMessages[attempt.index];
116
+ if (repair) {
117
+ lines.push("", "### Repair feedback sent", "", "```", repair, "```");
118
+ }
119
+ });
120
+
121
+ if (report.emitted) {
122
+ lines.push("", "## Emitted (A2UI)");
123
+ if (report.emitted.refusal) {
124
+ lines.push(`- **REFUSED by the emitter:** ${report.emitted.refusal}`);
125
+ }
126
+ for (const validation of report.emitted.validations) {
127
+ for (const gate of validation.gates) {
128
+ lines.push(
129
+ `- [${validation.a2uiVersion ?? report.emitted!.target}] gate ${gate.gate} ${gate.name}: **${gate.pass ? "PASS" : "FAIL"}**`,
130
+ );
131
+ }
132
+ }
133
+ for (const warning of report.emitted.warnings) {
134
+ lines.push(`- note ${warning.code}: ${warning.message}`);
135
+ }
136
+ }
137
+ lines.push("");
138
+ return lines.join("\n");
139
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,191 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * dspack-gen CLI.
4
+ *
5
+ * dspack-gen context --dspack <contract.json> --intent <id> [--depth N] [--no-steering]
6
+ * dspack-gen lint --dspack <contract.json> --surface <file.dsurface.json>
7
+ *
8
+ * `context` prints the compiled generation context ({ system, schema, fewshot }).
9
+ * `lint` runs surface gates S1–S3: machine-readable JSON report on stdout,
10
+ * human rendering on stderr. Later PRs add `run` (the full pipeline).
11
+ *
12
+ * Exit codes (full table in README): 0 clean, 1 internal/usage error,
13
+ * 2 governance failure (any S-gate error), 3 emitter-gate failure,
14
+ * 4 unknown rule type.
15
+ */
16
+ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
17
+ import { join, resolve } from "node:path";
18
+ import type { Contract } from "./core/contract.js";
19
+ import { compileContext } from "./core/compiler.js";
20
+ import { lintSurface, renderText, UnknownRuleTypeError } from "./core/lint/index.js";
21
+ import { adapterFor, AdapterOutputError } from "./adapters/index.js";
22
+ import { runPipeline } from "./run/orchestrator.js";
23
+ import { renderMarkdown } from "./audit/report.js";
24
+
25
+ function fail(message: string): never {
26
+ console.error(`error: ${message}`);
27
+ process.exit(1);
28
+ }
29
+
30
+ const BOOLEAN_FLAGS = new Set(["no-steering"]);
31
+
32
+ function parseFlags(argv: string[]): Map<string, string> {
33
+ const flags = new Map<string, string>();
34
+ for (let i = 0; i < argv.length; i++) {
35
+ const token = argv[i];
36
+ if (!token.startsWith("--")) fail(`unexpected argument '${token}' (flags start with --)`);
37
+ const eq = token.indexOf("=");
38
+ if (eq !== -1) {
39
+ flags.set(token.slice(2, eq), token.slice(eq + 1));
40
+ } else if (BOOLEAN_FLAGS.has(token.slice(2))) {
41
+ flags.set(token.slice(2), "true");
42
+ } else {
43
+ const value = argv[++i];
44
+ if (value === undefined) fail(`flag '${token}' is missing a value`);
45
+ flags.set(token.slice(2), value);
46
+ }
47
+ }
48
+ return flags;
49
+ }
50
+
51
+ function commandContext(flags: Map<string, string>): void {
52
+ const contractPath = flags.get("dspack") ?? fail("--dspack <contract.json> is required");
53
+ const intent = flags.get("intent") ?? fail("--intent <id> is required");
54
+
55
+ const contract = JSON.parse(readFileSync(resolve(contractPath), "utf8")) as Contract;
56
+ let context;
57
+ try {
58
+ context = compileContext(contract, intent, {
59
+ depth: flags.has("depth") ? Number(flags.get("depth")) : undefined,
60
+ omitRuleSteering: flags.get("no-steering") === "true",
61
+ });
62
+ } catch (e) {
63
+ fail(e instanceof Error ? e.message : String(e));
64
+ }
65
+ process.stdout.write(JSON.stringify(context, null, 2) + "\n");
66
+ }
67
+
68
+ function commandLint(flags: Map<string, string>): void {
69
+ const contractPath = flags.get("dspack") ?? fail("--dspack <contract.json> is required");
70
+ const surfacePath = flags.get("surface") ?? fail("--surface <file.dsurface.json> is required");
71
+
72
+ const contract = JSON.parse(readFileSync(resolve(contractPath), "utf8")) as Contract;
73
+ const surface = JSON.parse(readFileSync(resolve(surfacePath), "utf8")) as unknown;
74
+
75
+ try {
76
+ const report = lintSurface(surface, contract);
77
+ process.stdout.write(JSON.stringify(report, null, 2) + "\n");
78
+ process.stderr.write(renderText(report) + "\n");
79
+ process.exit(report.pass ? 0 : 2);
80
+ } catch (e) {
81
+ if (e instanceof UnknownRuleTypeError) {
82
+ console.error(`error: ${e.message}`);
83
+ process.exit(4);
84
+ }
85
+ throw e;
86
+ }
87
+ }
88
+
89
+ function parseMaxRepairs(flags: Map<string, string>): number | undefined {
90
+ if (!flags.has("max-repairs")) return undefined;
91
+ const value = Number(flags.get("max-repairs"));
92
+ if (!Number.isInteger(value) || value < 0) {
93
+ fail(`--max-repairs must be a non-negative integer (got '${flags.get("max-repairs")}')`);
94
+ }
95
+ return value;
96
+ }
97
+
98
+ async function commandRun(flags: Map<string, string>): Promise<void> {
99
+ const contractPath = flags.get("dspack") ?? fail("--dspack <contract.json> is required");
100
+ const intent = flags.get("intent") ?? fail("--intent <id> is required");
101
+ const prompt = flags.get("prompt") ?? fail("--prompt <text> is required");
102
+ const modelRef = flags.get("model") ?? fail("--model ollama:<id>|anthropic:<id> is required");
103
+ const outDir = flags.get("out") ?? "out";
104
+
105
+ const contract = JSON.parse(readFileSync(resolve(contractPath), "utf8")) as Contract;
106
+ let result;
107
+ try {
108
+ result = await runPipeline({
109
+ contract,
110
+ intent,
111
+ prompt,
112
+ adapter: adapterFor(modelRef),
113
+ maxRepairs: parseMaxRepairs(flags),
114
+ compile: {
115
+ depth: flags.has("depth") ? Number(flags.get("depth")) : undefined,
116
+ omitRuleSteering: flags.get("no-steering") === "true",
117
+ },
118
+ });
119
+ } catch (e) {
120
+ if (e instanceof UnknownRuleTypeError) {
121
+ console.error(`error: ${e.message}`);
122
+ process.exit(4);
123
+ }
124
+ if (e instanceof AdapterOutputError) {
125
+ console.error(`error: ${e.message}`);
126
+ process.exit(1);
127
+ }
128
+ throw e;
129
+ }
130
+
131
+ mkdirSync(resolve(outDir), { recursive: true });
132
+ writeFileSync(join(resolve(outDir), "audit-report.json"), JSON.stringify(result.report, null, 2) + "\n");
133
+ writeFileSync(join(resolve(outDir), "audit-report.md"), renderMarkdown(result.report));
134
+ if (result.surfaceMessages) {
135
+ writeFileSync(join(resolve(outDir), "generated.surface.json"), JSON.stringify(result.surfaceMessages, null, 2) + "\n");
136
+ }
137
+
138
+ console.error(`outcome: ${result.report.outcome} (${result.report.attempts.length} attempt(s))`);
139
+ console.error(`audit report -> ${join(outDir, "audit-report.json")}`);
140
+ process.exit(result.exitCode);
141
+ }
142
+
143
+ const USAGE = `dspack-gen <command> [flags]
144
+
145
+ Commands:
146
+ context --dspack <contract.json> --intent <id> [--depth N] [--no-steering]
147
+ Print the compiled generation context ({ system, schema, fewshot }).
148
+ lint --dspack <contract.json> --surface <file.dsurface.json>
149
+ Run surface gates S1-S3; JSON report on stdout, human rendering on stderr.
150
+ run --dspack <contract.json> --intent <id> --prompt <text> --model <ref> [--out <dir>] [--max-repairs N]
151
+ Full pipeline: generate -> lint -> repair -> emit -> audit report.
152
+ <ref> is ollama:<id> or anthropic:<id>.
153
+ serve --dspack <contract.json> [--port <n>]
154
+ Localhost NDJSON server streaming the pipeline.
155
+
156
+ Exit codes: 0 clean, 1 usage/internal error, 2 governance failure,
157
+ 3 emitter-gate failure, 4 unknown rule type.`;
158
+
159
+ function main(): void {
160
+ const [command, ...rest] = process.argv.slice(2);
161
+ if (command === undefined || command === "help" || command === "--help" || command === "-h") {
162
+ console.log(USAGE);
163
+ process.exit(command === undefined ? 1 : 0);
164
+ }
165
+ const flags = parseFlags(rest);
166
+ if (command === "context") return commandContext(flags);
167
+ if (command === "lint") return commandLint(flags);
168
+ if (command === "run") {
169
+ void commandRun(flags);
170
+ return;
171
+ }
172
+ if (command === "serve") {
173
+ let port: number | undefined;
174
+ if (flags.has("port")) {
175
+ port = Number(flags.get("port"));
176
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
177
+ fail(`--port must be an integer between 1 and 65535 (got '${flags.get("port")}')`);
178
+ }
179
+ }
180
+ void import("./serve.js").then(({ startServer }) =>
181
+ startServer({
182
+ contractPath: flags.get("dspack") ?? "fixtures/shadcn.v0_4.dspack.json",
183
+ port,
184
+ }),
185
+ );
186
+ return;
187
+ }
188
+ fail(`unknown command '${command ?? ""}' (available: context, lint, run, serve)`);
189
+ }
190
+
191
+ main();