@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,66 @@
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
+ export interface GenerateMessage {
17
+ role: "user" | "assistant";
18
+ content: string;
19
+ }
20
+ export interface GenerateRequest {
21
+ system: string;
22
+ messages: GenerateMessage[];
23
+ /** The generation schema (constrained decoding / output schema). */
24
+ jsonSchema: Record<string, unknown>;
25
+ params?: {
26
+ /** Sent to providers that accept it (Ollama). Never sent to Anthropic (removed on current models). */
27
+ temperature?: number;
28
+ maxTokens?: number;
29
+ };
30
+ }
31
+ export interface GenerateResult {
32
+ /** The parsed JSON output — shape-checked by gates, not here. */
33
+ json: unknown;
34
+ /** The raw text the model produced. */
35
+ raw: string;
36
+ /** The model that actually served the request, as reported by the provider. */
37
+ model: string;
38
+ usage?: {
39
+ inputTokens?: number;
40
+ outputTokens?: number;
41
+ };
42
+ /** Provider-specific timings/engine info, recorded verbatim in audit reports. */
43
+ meta?: Record<string, unknown>;
44
+ }
45
+ export interface GenerationAdapter {
46
+ /** Stable identity for audit reports, e.g. "ollama:<model-tag>" or "anthropic:<model-id>". */
47
+ readonly id: string;
48
+ generate(request: GenerateRequest): Promise<GenerateResult>;
49
+ }
50
+ /** The model produced output the adapter cannot hand to the pipeline (non-JSON, refusal, empty). */
51
+ export declare class AdapterOutputError extends Error {
52
+ readonly adapterId: string;
53
+ readonly rawHead?: string | undefined;
54
+ constructor(adapterId: string, message: string, rawHead?: string | undefined);
55
+ }
56
+ /** Injectable for offline, deterministic tests. */
57
+ export type FetchLike = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
58
+ export declare function parseJsonOutput(adapterId: string, raw: string): unknown;
59
+ /**
60
+ * Parse a `--model` flag: "ollama:<model-id>" or "anthropic:<model-id>".
61
+ * The model id itself may contain colons (ollama tags carry a size suffix).
62
+ */
63
+ export declare function parseModelRef(ref: string): {
64
+ provider: "ollama" | "anthropic";
65
+ model: string;
66
+ };
@@ -0,0 +1,47 @@
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
+ /** The model produced output the adapter cannot hand to the pipeline (non-JSON, refusal, empty). */
17
+ export class AdapterOutputError extends Error {
18
+ adapterId;
19
+ rawHead;
20
+ constructor(adapterId, message, rawHead) {
21
+ super(`[${adapterId}] ${message}${rawHead ? ` (output head: ${JSON.stringify(rawHead.slice(0, 200))})` : ""}`);
22
+ this.adapterId = adapterId;
23
+ this.rawHead = rawHead;
24
+ this.name = "AdapterOutputError";
25
+ }
26
+ }
27
+ export function parseJsonOutput(adapterId, raw) {
28
+ try {
29
+ return JSON.parse(raw);
30
+ }
31
+ catch {
32
+ throw new AdapterOutputError(adapterId, "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)", raw);
33
+ }
34
+ }
35
+ /**
36
+ * Parse a `--model` flag: "ollama:<model-id>" or "anthropic:<model-id>".
37
+ * The model id itself may contain colons (ollama tags carry a size suffix).
38
+ */
39
+ export function parseModelRef(ref) {
40
+ const sep = ref.indexOf(":");
41
+ const provider = sep === -1 ? ref : ref.slice(0, sep);
42
+ const model = sep === -1 ? "" : ref.slice(sep + 1);
43
+ if ((provider !== "ollama" && provider !== "anthropic") || model === "") {
44
+ throw new Error(`invalid model reference '${ref}' (expected ollama:<model-id> or anthropic:<model-id>)`);
45
+ }
46
+ return { provider, model };
47
+ }
@@ -0,0 +1,84 @@
1
+ import type { Contract } from "../core/contract.js";
2
+ import type { Finding, GateReport } from "../core/lint/findings.js";
3
+ export declare const REPORT_VERSION = "1";
4
+ export type Outcome = "passed" | "failed-lint-exhausted" | "failed-gate" | "failed-adapter";
5
+ /** Emitter gate result, keyed by the architecture-wide A-gate names. */
6
+ export interface EmitterGate {
7
+ gate: "A1" | "A2" | "A3" | "J2" | "J3";
8
+ name: string;
9
+ pass: boolean;
10
+ errors?: string[];
11
+ }
12
+ export interface AttemptRecord {
13
+ index: number;
14
+ /** The generated dspack surface (absent when the adapter failed). */
15
+ surface?: unknown;
16
+ model?: string;
17
+ usage?: {
18
+ inputTokens?: number;
19
+ outputTokens?: number;
20
+ };
21
+ meta?: Record<string, unknown>;
22
+ adapterError?: string;
23
+ /** Surface gates S1/S2/S3, independently reported. */
24
+ gates?: GateReport[];
25
+ findings?: Finding[];
26
+ }
27
+ export interface EmittedValidation {
28
+ /** A2UI catalog version validated against (a2ui target only; absent for json-render). */
29
+ a2uiVersion?: string;
30
+ gates: EmitterGate[];
31
+ }
32
+ export interface AuditReportV1 {
33
+ reportVersion: typeof REPORT_VERSION;
34
+ createdAt: string;
35
+ request: {
36
+ prompt: string;
37
+ intent: string;
38
+ contract: {
39
+ name: string;
40
+ dspack: string;
41
+ sha256: string;
42
+ };
43
+ };
44
+ generation: {
45
+ adapterId: string;
46
+ schemaSha256: string;
47
+ maxRepairs: number;
48
+ ruleSteering: boolean;
49
+ /** ADR-7 repair template variant used for feedback (additive in v1; absent ⇒ "standard"). */
50
+ repairTemplate?: string;
51
+ };
52
+ attempts: AttemptRecord[];
53
+ /** Repair messages verbatim — rendered from the same findings objects above. */
54
+ repairMessages: string[];
55
+ outcome: Outcome;
56
+ emitted?: {
57
+ target: "a2ui" | "json-render";
58
+ /** Absent when the emitter refused the surface outright (see refusal). */
59
+ surfaceMessages?: unknown;
60
+ /** Emitter warnings — every synthesis/drop, nothing silent. */
61
+ warnings: Array<{
62
+ code: string;
63
+ message: string;
64
+ }>;
65
+ validations: EmittedValidation[];
66
+ /**
67
+ * The emitter's typed refusal message when a lint-clean surface could
68
+ * not be emitted at all (additive in v1) — the target-equivalent
69
+ * emitter-gate failure; validations is empty in that case.
70
+ */
71
+ refusal?: string;
72
+ };
73
+ timings: {
74
+ totalMs: number;
75
+ };
76
+ }
77
+ export declare function sha256(value: unknown): string;
78
+ export declare function contractDigest(contract: Contract): {
79
+ name: string;
80
+ dspack: string;
81
+ sha256: string;
82
+ };
83
+ /** Human view of the JSON artifact — derived, never the source of truth. */
84
+ export declare function renderMarkdown(report: AuditReportV1): string;
@@ -0,0 +1,62 @@
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
+ export const REPORT_VERSION = "1";
9
+ export function sha256(value) {
10
+ return createHash("sha256").update(typeof value === "string" ? value : JSON.stringify(value)).digest("hex");
11
+ }
12
+ export function contractDigest(contract) {
13
+ return { name: contract.name, dspack: contract.dspack, sha256: sha256(contract) };
14
+ }
15
+ /** Human view of the JSON artifact — derived, never the source of truth. */
16
+ export function renderMarkdown(report) {
17
+ const lines = [
18
+ `# Audit report (v${report.reportVersion})`,
19
+ "",
20
+ `- **Outcome:** ${report.outcome}`,
21
+ `- **Prompt:** ${report.request.prompt}`,
22
+ `- **Intent:** ${report.request.intent}`,
23
+ `- **Contract:** ${report.request.contract.name} (dspack ${report.request.contract.dspack}, sha256 ${report.request.contract.sha256.slice(0, 12)}…)`,
24
+ `- **Adapter:** ${report.generation.adapterId} (max repairs: ${report.generation.maxRepairs}, rule steering: ${report.generation.ruleSteering})`,
25
+ `- **Created:** ${report.createdAt} · **Duration:** ${report.timings.totalMs} ms`,
26
+ ];
27
+ report.attempts.forEach((attempt) => {
28
+ lines.push("", `## Attempt ${attempt.index + 1}`);
29
+ if (attempt.adapterError) {
30
+ lines.push(`- Adapter error: ${attempt.adapterError}`);
31
+ return;
32
+ }
33
+ if (attempt.model)
34
+ lines.push(`- Model: ${attempt.model}`);
35
+ for (const gate of attempt.gates ?? []) {
36
+ lines.push(`- gate ${gate.gate} ${gate.name}: **${gate.status}**`);
37
+ }
38
+ for (const finding of attempt.findings ?? []) {
39
+ lines.push(` - ${finding.level} [${finding.requirement}] ${finding.ruleId} at ${finding.location.path} — ${finding.message}`);
40
+ }
41
+ const repair = report.repairMessages[attempt.index];
42
+ if (repair) {
43
+ lines.push("", "### Repair feedback sent", "", "```", repair, "```");
44
+ }
45
+ });
46
+ if (report.emitted) {
47
+ lines.push("", "## Emitted (A2UI)");
48
+ if (report.emitted.refusal) {
49
+ lines.push(`- **REFUSED by the emitter:** ${report.emitted.refusal}`);
50
+ }
51
+ for (const validation of report.emitted.validations) {
52
+ for (const gate of validation.gates) {
53
+ lines.push(`- [${validation.a2uiVersion ?? report.emitted.target}] gate ${gate.gate} ${gate.name}: **${gate.pass ? "PASS" : "FAIL"}**`);
54
+ }
55
+ }
56
+ for (const warning of report.emitted.warnings) {
57
+ lines.push(`- note ${warning.code}: ${warning.message}`);
58
+ }
59
+ }
60
+ lines.push("");
61
+ return lines.join("\n");
62
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,182 @@
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 { compileContext } from "./core/compiler.js";
19
+ import { lintSurface, renderText, UnknownRuleTypeError } from "./core/lint/index.js";
20
+ import { adapterFor, AdapterOutputError } from "./adapters/index.js";
21
+ import { runPipeline } from "./run/orchestrator.js";
22
+ import { renderMarkdown } from "./audit/report.js";
23
+ function fail(message) {
24
+ console.error(`error: ${message}`);
25
+ process.exit(1);
26
+ }
27
+ const BOOLEAN_FLAGS = new Set(["no-steering"]);
28
+ function parseFlags(argv) {
29
+ const flags = new Map();
30
+ for (let i = 0; i < argv.length; i++) {
31
+ const token = argv[i];
32
+ if (!token.startsWith("--"))
33
+ fail(`unexpected argument '${token}' (flags start with --)`);
34
+ const eq = token.indexOf("=");
35
+ if (eq !== -1) {
36
+ flags.set(token.slice(2, eq), token.slice(eq + 1));
37
+ }
38
+ else if (BOOLEAN_FLAGS.has(token.slice(2))) {
39
+ flags.set(token.slice(2), "true");
40
+ }
41
+ else {
42
+ const value = argv[++i];
43
+ if (value === undefined)
44
+ fail(`flag '${token}' is missing a value`);
45
+ flags.set(token.slice(2), value);
46
+ }
47
+ }
48
+ return flags;
49
+ }
50
+ function commandContext(flags) {
51
+ const contractPath = flags.get("dspack") ?? fail("--dspack <contract.json> is required");
52
+ const intent = flags.get("intent") ?? fail("--intent <id> is required");
53
+ const contract = JSON.parse(readFileSync(resolve(contractPath), "utf8"));
54
+ let context;
55
+ try {
56
+ context = compileContext(contract, intent, {
57
+ depth: flags.has("depth") ? Number(flags.get("depth")) : undefined,
58
+ omitRuleSteering: flags.get("no-steering") === "true",
59
+ });
60
+ }
61
+ catch (e) {
62
+ fail(e instanceof Error ? e.message : String(e));
63
+ }
64
+ process.stdout.write(JSON.stringify(context, null, 2) + "\n");
65
+ }
66
+ function commandLint(flags) {
67
+ const contractPath = flags.get("dspack") ?? fail("--dspack <contract.json> is required");
68
+ const surfacePath = flags.get("surface") ?? fail("--surface <file.dsurface.json> is required");
69
+ const contract = JSON.parse(readFileSync(resolve(contractPath), "utf8"));
70
+ const surface = JSON.parse(readFileSync(resolve(surfacePath), "utf8"));
71
+ try {
72
+ const report = lintSurface(surface, contract);
73
+ process.stdout.write(JSON.stringify(report, null, 2) + "\n");
74
+ process.stderr.write(renderText(report) + "\n");
75
+ process.exit(report.pass ? 0 : 2);
76
+ }
77
+ catch (e) {
78
+ if (e instanceof UnknownRuleTypeError) {
79
+ console.error(`error: ${e.message}`);
80
+ process.exit(4);
81
+ }
82
+ throw e;
83
+ }
84
+ }
85
+ function parseMaxRepairs(flags) {
86
+ if (!flags.has("max-repairs"))
87
+ return undefined;
88
+ const value = Number(flags.get("max-repairs"));
89
+ if (!Number.isInteger(value) || value < 0) {
90
+ fail(`--max-repairs must be a non-negative integer (got '${flags.get("max-repairs")}')`);
91
+ }
92
+ return value;
93
+ }
94
+ async function commandRun(flags) {
95
+ const contractPath = flags.get("dspack") ?? fail("--dspack <contract.json> is required");
96
+ const intent = flags.get("intent") ?? fail("--intent <id> is required");
97
+ const prompt = flags.get("prompt") ?? fail("--prompt <text> is required");
98
+ const modelRef = flags.get("model") ?? fail("--model ollama:<id>|anthropic:<id> is required");
99
+ const outDir = flags.get("out") ?? "out";
100
+ const contract = JSON.parse(readFileSync(resolve(contractPath), "utf8"));
101
+ let result;
102
+ try {
103
+ result = await runPipeline({
104
+ contract,
105
+ intent,
106
+ prompt,
107
+ adapter: adapterFor(modelRef),
108
+ maxRepairs: parseMaxRepairs(flags),
109
+ compile: {
110
+ depth: flags.has("depth") ? Number(flags.get("depth")) : undefined,
111
+ omitRuleSteering: flags.get("no-steering") === "true",
112
+ },
113
+ });
114
+ }
115
+ catch (e) {
116
+ if (e instanceof UnknownRuleTypeError) {
117
+ console.error(`error: ${e.message}`);
118
+ process.exit(4);
119
+ }
120
+ if (e instanceof AdapterOutputError) {
121
+ console.error(`error: ${e.message}`);
122
+ process.exit(1);
123
+ }
124
+ throw e;
125
+ }
126
+ mkdirSync(resolve(outDir), { recursive: true });
127
+ writeFileSync(join(resolve(outDir), "audit-report.json"), JSON.stringify(result.report, null, 2) + "\n");
128
+ writeFileSync(join(resolve(outDir), "audit-report.md"), renderMarkdown(result.report));
129
+ if (result.surfaceMessages) {
130
+ writeFileSync(join(resolve(outDir), "generated.surface.json"), JSON.stringify(result.surfaceMessages, null, 2) + "\n");
131
+ }
132
+ console.error(`outcome: ${result.report.outcome} (${result.report.attempts.length} attempt(s))`);
133
+ console.error(`audit report -> ${join(outDir, "audit-report.json")}`);
134
+ process.exit(result.exitCode);
135
+ }
136
+ const USAGE = `dspack-gen <command> [flags]
137
+
138
+ Commands:
139
+ context --dspack <contract.json> --intent <id> [--depth N] [--no-steering]
140
+ Print the compiled generation context ({ system, schema, fewshot }).
141
+ lint --dspack <contract.json> --surface <file.dsurface.json>
142
+ Run surface gates S1-S3; JSON report on stdout, human rendering on stderr.
143
+ run --dspack <contract.json> --intent <id> --prompt <text> --model <ref> [--out <dir>] [--max-repairs N]
144
+ Full pipeline: generate -> lint -> repair -> emit -> audit report.
145
+ <ref> is ollama:<id> or anthropic:<id>.
146
+ serve --dspack <contract.json> [--port <n>]
147
+ Localhost NDJSON server streaming the pipeline.
148
+
149
+ Exit codes: 0 clean, 1 usage/internal error, 2 governance failure,
150
+ 3 emitter-gate failure, 4 unknown rule type.`;
151
+ function main() {
152
+ const [command, ...rest] = process.argv.slice(2);
153
+ if (command === undefined || command === "help" || command === "--help" || command === "-h") {
154
+ console.log(USAGE);
155
+ process.exit(command === undefined ? 1 : 0);
156
+ }
157
+ const flags = parseFlags(rest);
158
+ if (command === "context")
159
+ return commandContext(flags);
160
+ if (command === "lint")
161
+ return commandLint(flags);
162
+ if (command === "run") {
163
+ void commandRun(flags);
164
+ return;
165
+ }
166
+ if (command === "serve") {
167
+ let port;
168
+ if (flags.has("port")) {
169
+ port = Number(flags.get("port"));
170
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
171
+ fail(`--port must be an integer between 1 and 65535 (got '${flags.get("port")}')`);
172
+ }
173
+ }
174
+ void import("./serve.js").then(({ startServer }) => startServer({
175
+ contractPath: flags.get("dspack") ?? "fixtures/shadcn.v0_4.dspack.json",
176
+ port,
177
+ }));
178
+ return;
179
+ }
180
+ fail(`unknown command '${command ?? ""}' (available: context, lint, run, serve)`);
181
+ }
182
+ main();
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Prompt/context compiler: dspack v0.3 contract × declared intent →
3
+ * { system, schema, fewshot }.
4
+ *
5
+ * - `system`: the compiled system prompt — vocabulary, governance rules
6
+ * rendered as instructions with rationales, and design-intent guidance.
7
+ * Deterministic (golden-file tested) and immutable across repair attempts
8
+ * (ADR-7: the only delta between attempts is the repair feedback).
9
+ * - `schema`: the generation schema (see generation-schema.ts). Shape only,
10
+ * never governance — the prompt's rules section is steering; the guarantee
11
+ * is gate S3.
12
+ * - `fewshot`: user/assistant message pairs from the contract's examples for
13
+ * the intent, verbatim (ADR-3: the surface format IS the generation format,
14
+ * so exemplars are exactly in-distribution).
15
+ */
16
+ import { type Contract, type RuleEntry } from "./contract.js";
17
+ import { type GenerationSchemaOptions } from "./generation-schema.js";
18
+ export interface FewshotMessage {
19
+ role: "user" | "assistant";
20
+ content: string;
21
+ }
22
+ export interface CompiledContext {
23
+ system: string;
24
+ schema: Record<string, unknown>;
25
+ fewshot: FewshotMessage[];
26
+ }
27
+ export interface CompileOptions extends GenerationSchemaOptions {
28
+ /**
29
+ * Omit the governance-rules section from the system prompt. The linter (S3)
30
+ * is unaffected — this only removes prompt steering. Used by demos/evals to
31
+ * observe first-attempt violation rates honestly.
32
+ */
33
+ omitRuleSteering?: boolean;
34
+ }
35
+ /** Rules that fire for an intent: universal rules plus intent-scoped ones. */
36
+ export declare function applicableRules(contract: Contract, intentId: string): RuleEntry[];
37
+ export declare function compileContext(contract: Contract, intentId: string, options?: CompileOptions): CompiledContext;
38
+ /** One-sentence imperative rendering of a rule (the rationale is appended by the caller). */
39
+ export declare function ruleInstruction(rule: RuleEntry, contract?: Contract): string;
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Prompt/context compiler: dspack v0.3 contract × declared intent →
3
+ * { system, schema, fewshot }.
4
+ *
5
+ * - `system`: the compiled system prompt — vocabulary, governance rules
6
+ * rendered as instructions with rationales, and design-intent guidance.
7
+ * Deterministic (golden-file tested) and immutable across repair attempts
8
+ * (ADR-7: the only delta between attempts is the repair feedback).
9
+ * - `schema`: the generation schema (see generation-schema.ts). Shape only,
10
+ * never governance — the prompt's rules section is steering; the guarantee
11
+ * is gate S3.
12
+ * - `fewshot`: user/assistant message pairs from the contract's examples for
13
+ * the intent, verbatim (ADR-3: the surface format IS the generation format,
14
+ * so exemplars are exactly in-distribution).
15
+ */
16
+ import { categoryIndex, enumValues, getIntent, } from "./contract.js";
17
+ import { buildGenerationSchema } from "./generation-schema.js";
18
+ /** Rules that fire for an intent: universal rules plus intent-scoped ones. */
19
+ export function applicableRules(contract, intentId) {
20
+ return (contract.rules ?? []).filter((rule) => !rule.appliesTo || rule.appliesTo.intents.includes(intentId));
21
+ }
22
+ export function compileContext(contract, intentId, options = {}) {
23
+ const intent = getIntent(contract, intentId);
24
+ const rules = applicableRules(contract, intentId);
25
+ const examples = (contract.examples ?? []).filter((e) => e.intent === intentId);
26
+ return {
27
+ system: renderSystemPrompt(contract, intent, options.omitRuleSteering ? [] : rules, options),
28
+ schema: buildGenerationSchema(contract, intentId, options),
29
+ fewshot: examples.flatMap((example) => fewshotPair(example)),
30
+ };
31
+ }
32
+ function fewshotPair(example) {
33
+ return [
34
+ { role: "user", content: example.prompt ?? example.description ?? example.id },
35
+ { role: "assistant", content: JSON.stringify(example.surface) },
36
+ ];
37
+ }
38
+ function renderSystemPrompt(contract, intent, rules, options) {
39
+ const lines = [
40
+ `You generate user interface surfaces for the "${contract.name}" design system. You must respond`,
41
+ "with a single JSON object conforming to the provided schema — a dspack surface document.",
42
+ "",
43
+ "## Component vocabulary",
44
+ "You may use only these components (with the listed props and allowed values):",
45
+ ];
46
+ for (const [id, component] of Object.entries(contract.components ?? {})) {
47
+ const props = Object.entries(component.props ?? {})
48
+ .map(([name, descriptor]) => {
49
+ const values = enumValues(descriptor);
50
+ return values ? `${name} ∈ {${values.map(String).join(", ")}}` : name;
51
+ })
52
+ .join("; ");
53
+ const subs = (component.composition?.subComponents ?? []).map((s) => s.id).join(", ");
54
+ let line = `- ${id} — ${component.description}`;
55
+ if (props)
56
+ line += ` Props: ${props}.`;
57
+ if (subs)
58
+ line += ` Sub-components (used as children): ${subs}.`;
59
+ lines.push(line);
60
+ }
61
+ if (rules.length > 0) {
62
+ lines.push("", `## Governance rules in effect (intent: ${intent.id})`, "These are hard requirements. Surfaces violating them will be rejected:");
63
+ rules.forEach((rule, i) => {
64
+ lines.push(`${i + 1}. [${rule.id} / ${rule.severity}] ${ruleInstruction(rule, contract)} Why: ${rule.rationale}`);
65
+ });
66
+ }
67
+ lines.push("", "## Design intent", `Intent "${intent.id}": ${intent.description}`);
68
+ for (const patternId of intent.relatedPatterns ?? []) {
69
+ const pattern = (contract.patterns ?? []).find((p) => p.id === patternId);
70
+ if (pattern?.guidance)
71
+ lines.push(`Related pattern "${pattern.name ?? pattern.id}": ${pattern.guidance}`);
72
+ }
73
+ lines.push("", "Output only the JSON object. No commentary.");
74
+ void options;
75
+ return lines.join("\n");
76
+ }
77
+ /** One-sentence imperative rendering of a rule (the rationale is appended by the caller). */
78
+ export function ruleInstruction(rule, contract) {
79
+ switch (rule.type) {
80
+ case "component-choice": {
81
+ const r = rule;
82
+ const parts = [];
83
+ if (r.require?.length)
84
+ parts.push(`Use ${r.require.join(", ")} for this surface`);
85
+ if (r.forbid?.length)
86
+ parts.push(`${r.forbid.join(", ")} ${r.forbid.length === 1 ? "is" : "are"} forbidden`);
87
+ return `${parts.join("; ")}.`;
88
+ }
89
+ case "required-composition": {
90
+ const r = rule;
91
+ const needs = [];
92
+ if (r.requiredSubComponents?.length)
93
+ needs.push(r.requiredSubComponents.map((s) => s.id).join(" and "));
94
+ if (r.requiredProps?.length)
95
+ needs.push(r.requiredProps.map((p) => `${p.prop} set to one of ${p.oneOf.map(String).join("/")}`).join(" and "));
96
+ return `Every ${r.component} must contain ${needs.join(", plus ")}.`;
97
+ }
98
+ case "forbidden-composition": {
99
+ const r = rule;
100
+ const parts = [];
101
+ if (r.forbiddenDescendants?.length)
102
+ parts.push(`Never place ${r.forbiddenDescendants.join(" or ")} inside a ${r.component}`);
103
+ // Steering names the category AND its resolved member ids, so the
104
+ // model sees concrete vocabulary (mirrors the finding message).
105
+ const memberships = r.forbiddenCategories?.length && contract ? categoryIndex(contract) : undefined;
106
+ for (const category of r.forbiddenCategories ?? []) {
107
+ const members = memberships
108
+ ? [...memberships].filter(([, cats]) => cats.includes(category)).map(([id]) => id)
109
+ : [];
110
+ parts.push(`never place ${category}-category components${members.length ? ` (${members.join(", ")})` : ""} inside ${r.component}`);
111
+ }
112
+ if (r.forbiddenProps?.length)
113
+ parts.push(r.forbiddenProps
114
+ .map((p) => `never set ${p.prop} to ${p.values.map(String).join("/")} on ${r.component}`)
115
+ .join("; "));
116
+ return `${parts.join("; ")}.`;
117
+ }
118
+ case "required-props": {
119
+ const r = rule;
120
+ const scope = r.within ? `At least one ${r.component} inside each ${r.within}` : `Every ${r.component}`;
121
+ const needs = [];
122
+ if (r.requiredText) {
123
+ needs.push(r.textScope === "subtree"
124
+ ? "contain non-empty text (its own `text` field or a descendant's)"
125
+ : "carry its label as its own `text` field (never nested in a child component)");
126
+ }
127
+ for (const p of r.requiredProps ?? []) {
128
+ needs.push(p.oneOf ? `set ${p.prop} to one of ${p.oneOf.map(String).join("/")}` : `set ${p.prop} directly`);
129
+ }
130
+ // Schema-invalid but defensive: no constraints to phrase → generic steering,
131
+ // never a malformed "must ;" sentence (the linter still evaluates the rule).
132
+ if (needs.length === 0)
133
+ return `Follow rule ${rule.id}.`;
134
+ const existence = r.within ? `; every ${r.within} must contain a ${r.component}` : "";
135
+ return `${scope} must ${needs.join(", and ")}${existence}.`;
136
+ }
137
+ default:
138
+ // The linter hard-errors on unknown types (exit 4); the compiler simply
139
+ // does not render steering it cannot phrase.
140
+ return `Follow rule ${rule.id}.`;
141
+ }
142
+ }