@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.
- package/CHANGELOG.md +19 -0
- package/LICENSE +202 -0
- package/README.md +147 -0
- package/dist/adapters/anthropic.d.ts +16 -0
- package/dist/adapters/anthropic.js +71 -0
- package/dist/adapters/fake.d.ts +21 -0
- package/dist/adapters/fake.js +32 -0
- package/dist/adapters/index.d.ts +6 -0
- package/dist/adapters/index.js +11 -0
- package/dist/adapters/ollama.d.ts +16 -0
- package/dist/adapters/ollama.js +91 -0
- package/dist/adapters/types.d.ts +66 -0
- package/dist/adapters/types.js +47 -0
- package/dist/audit/report.d.ts +84 -0
- package/dist/audit/report.js +62 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +182 -0
- package/dist/core/compiler.d.ts +39 -0
- package/dist/core/compiler.js +142 -0
- package/dist/core/contract.d.ts +175 -0
- package/dist/core/contract.js +62 -0
- package/dist/core/generation-schema.d.ts +32 -0
- package/dist/core/generation-schema.js +85 -0
- package/dist/core/index.d.ts +12 -0
- package/dist/core/index.js +12 -0
- package/dist/core/lint/findings.d.ts +46 -0
- package/dist/core/lint/findings.js +28 -0
- package/dist/core/lint/index.d.ts +7 -0
- package/dist/core/lint/index.js +67 -0
- package/dist/core/lint/rules.d.ts +27 -0
- package/dist/core/lint/rules.js +212 -0
- package/dist/core/lint/vocabulary.d.ts +13 -0
- package/dist/core/lint/vocabulary.js +71 -0
- package/dist/core/lint/walk.d.ts +14 -0
- package/dist/core/lint/walk.js +30 -0
- package/dist/core/surface-schema.d.ts +78 -0
- package/dist/core/surface-schema.js +85 -0
- package/dist/eval/assert.d.ts +2 -0
- package/dist/eval/assert.js +50 -0
- package/dist/eval/run.d.ts +2 -0
- package/dist/eval/run.js +60 -0
- package/dist/eval/runner.d.ts +36 -0
- package/dist/eval/runner.js +310 -0
- package/dist/eval/types.d.ts +143 -0
- package/dist/eval/types.js +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +14 -0
- package/dist/repair/render.d.ts +20 -0
- package/dist/repair/render.js +28 -0
- package/dist/run/orchestrator.d.ts +90 -0
- package/dist/run/orchestrator.js +191 -0
- package/dist/serve.d.ts +25 -0
- package/dist/serve.js +144 -0
- package/package.json +76 -0
- package/src/adapters/anthropic.ts +88 -0
- package/src/adapters/fake.ts +32 -0
- package/src/adapters/index.ts +13 -0
- package/src/adapters/ollama.ts +123 -0
- package/src/adapters/types.ts +91 -0
- package/src/audit/report.ts +139 -0
- package/src/cli.ts +191 -0
- package/src/core/compiler.ts +205 -0
- package/src/core/contract.ts +205 -0
- package/src/core/generation-schema.ts +99 -0
- package/src/core/index.ts +12 -0
- package/src/core/lint/findings.ts +80 -0
- package/src/core/lint/index.ts +80 -0
- package/src/core/lint/rules.ts +320 -0
- package/src/core/lint/vocabulary.ts +80 -0
- package/src/core/lint/walk.ts +44 -0
- package/src/core/surface-schema.ts +86 -0
- package/src/eval/assert.ts +55 -0
- package/src/eval/run.ts +62 -0
- package/src/eval/runner.ts +366 -0
- package/src/eval/types.ts +143 -0
- package/src/index.ts +15 -0
- package/src/repair/render.ts +68 -0
- package/src/run/orchestrator.ts +272 -0
- package/src/serve.ts +164 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The pipeline orchestrator: generate → surface gates S1–S3 → bounded repair
|
|
3
|
+
* (≤ maxRepairs, default 2; ADR-7) → emit via the pinned A2UI emitter →
|
|
4
|
+
* emitter gates A1–A3 → audit report v1.
|
|
5
|
+
*
|
|
6
|
+
* Failure exits are first-class artifacts, never silent: every outcome —
|
|
7
|
+
* passed, failed-lint-exhausted (exit 2), failed-gate (exit 3),
|
|
8
|
+
* failed-adapter (exit 1) — produces a complete audit report. The system
|
|
9
|
+
* prompt is immutable across attempts; the only delta between attempts is
|
|
10
|
+
* the model's own output plus the rendered repair feedback.
|
|
11
|
+
*/
|
|
12
|
+
import { buildCatalogModel, emitJsonRenderSpec, EmitJsonRenderError, emitSurface, EmitSurfaceError, transform, validateSpecAgainstModel, } from "@aestheticfunction/dspack-emit";
|
|
13
|
+
import { applicableRules, compileContext } from "../core/compiler.js";
|
|
14
|
+
import { lintSurface } from "../core/lint/index.js";
|
|
15
|
+
import { AdapterOutputError } from "../adapters/types.js";
|
|
16
|
+
import { renderRepairMessage } from "../repair/render.js";
|
|
17
|
+
import { contractDigest, sha256, REPORT_VERSION, } from "../audit/report.js";
|
|
18
|
+
/** Map the emitter's ajv gate names onto the architecture-wide A1/A2/A3. */
|
|
19
|
+
const A_GATE = {
|
|
20
|
+
"schema-compile + no-external-ref": "A1",
|
|
21
|
+
"catalog-shape": "A2",
|
|
22
|
+
instance: "A3",
|
|
23
|
+
};
|
|
24
|
+
export async function runPipeline(options) {
|
|
25
|
+
const { contract, intent, prompt, adapter } = options;
|
|
26
|
+
const maxRepairs = options.maxRepairs ?? 2;
|
|
27
|
+
const repairTemplate = options.repairTemplate ?? "standard";
|
|
28
|
+
const now = options.now ?? (() => new Date());
|
|
29
|
+
const startedAt = now();
|
|
30
|
+
const context = compileContext(contract, intent, options.compile);
|
|
31
|
+
const conversation = [...context.fewshot, { role: "user", content: prompt }];
|
|
32
|
+
// Purely observational, enforced: a throwing hook (e.g. a stream write
|
|
33
|
+
// after the client disconnected) must never abort the pipeline or change
|
|
34
|
+
// its outcome — the audit report is the artifact, events are a view.
|
|
35
|
+
const emit = (event) => {
|
|
36
|
+
try {
|
|
37
|
+
options.onEvent?.(event);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
/* observational — swallowed by contract */
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
emit({
|
|
44
|
+
type: "start",
|
|
45
|
+
intent,
|
|
46
|
+
prompt,
|
|
47
|
+
adapterId: adapter.id,
|
|
48
|
+
ruleIds: applicableRules(contract, intent).map((rule) => rule.id),
|
|
49
|
+
});
|
|
50
|
+
const attempts = [];
|
|
51
|
+
const repairMessages = [];
|
|
52
|
+
const finalize = (outcome, exitCode, extra = {}, emitted) => ({
|
|
53
|
+
report: {
|
|
54
|
+
reportVersion: REPORT_VERSION,
|
|
55
|
+
createdAt: startedAt.toISOString(),
|
|
56
|
+
request: { prompt, intent, contract: contractDigest(contract) },
|
|
57
|
+
generation: {
|
|
58
|
+
adapterId: adapter.id,
|
|
59
|
+
schemaSha256: sha256(context.schema),
|
|
60
|
+
maxRepairs,
|
|
61
|
+
ruleSteering: !options.compile?.omitRuleSteering,
|
|
62
|
+
repairTemplate,
|
|
63
|
+
},
|
|
64
|
+
attempts,
|
|
65
|
+
repairMessages,
|
|
66
|
+
outcome,
|
|
67
|
+
...(emitted ? { emitted } : {}),
|
|
68
|
+
timings: { totalMs: now().getTime() - startedAt.getTime() },
|
|
69
|
+
},
|
|
70
|
+
exitCode,
|
|
71
|
+
...extra,
|
|
72
|
+
});
|
|
73
|
+
for (let index = 0; index <= maxRepairs; index++) {
|
|
74
|
+
let generated;
|
|
75
|
+
try {
|
|
76
|
+
// Snapshot per attempt: adapters may hold the request beyond the call.
|
|
77
|
+
generated = await adapter.generate({ system: context.system, messages: [...conversation], jsonSchema: context.schema });
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
if (error instanceof AdapterOutputError) {
|
|
81
|
+
attempts.push({ index, adapterError: error.message });
|
|
82
|
+
const failed = finalize("failed-adapter", 1);
|
|
83
|
+
emit({ type: "done", outcome: failed.report.outcome, exitCode: failed.exitCode, report: failed.report });
|
|
84
|
+
return failed;
|
|
85
|
+
}
|
|
86
|
+
throw error;
|
|
87
|
+
}
|
|
88
|
+
// Surface gates S1–S3 over the artifact (UnknownRuleTypeError propagates: CLI exit 4).
|
|
89
|
+
const lint = lintSurface(generated.json, contract);
|
|
90
|
+
attempts.push({
|
|
91
|
+
index,
|
|
92
|
+
surface: generated.json,
|
|
93
|
+
model: generated.model,
|
|
94
|
+
usage: generated.usage,
|
|
95
|
+
meta: generated.meta,
|
|
96
|
+
gates: lint.gates,
|
|
97
|
+
findings: lint.findings,
|
|
98
|
+
});
|
|
99
|
+
emit({ type: "attempt", index, model: generated.model, surface: generated.json, gates: lint.gates, findings: lint.findings });
|
|
100
|
+
if (lint.pass) {
|
|
101
|
+
const surface = generated.json;
|
|
102
|
+
const doc = contract;
|
|
103
|
+
if (options.emitTarget === "json-render") {
|
|
104
|
+
// Second-emitter path (PR-21): same refusal semantics as a2ui — a
|
|
105
|
+
// typed emit error is the gate-failure class, never a crash.
|
|
106
|
+
try {
|
|
107
|
+
const model = buildCatalogModel(doc, {});
|
|
108
|
+
const { spec } = emitJsonRenderSpec(surface, doc, {});
|
|
109
|
+
const findings = validateSpecAgainstModel(spec, model);
|
|
110
|
+
const gates = [
|
|
111
|
+
{ gate: "J2", name: "emission accepted", pass: true },
|
|
112
|
+
{
|
|
113
|
+
gate: "J3",
|
|
114
|
+
name: "instance validates against the generated catalog model",
|
|
115
|
+
pass: findings.length === 0,
|
|
116
|
+
...(findings.length ? { errors: findings.map((f) => f.message) } : {}),
|
|
117
|
+
},
|
|
118
|
+
];
|
|
119
|
+
const pass = gates.every((g) => g.pass);
|
|
120
|
+
const emitted = { target: "json-render", spec, warnings: [], validations: [{ gates }] };
|
|
121
|
+
emit({ type: "emitted", validations: [{ gates }], warnings: [] });
|
|
122
|
+
const result = pass ? finalize("passed", 0, { surface }, emitted) : finalize("failed-gate", 3, {}, emitted);
|
|
123
|
+
emit({ type: "done", outcome: result.report.outcome, exitCode: result.exitCode, report: result.report });
|
|
124
|
+
return result;
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
if (error instanceof EmitJsonRenderError) {
|
|
128
|
+
const emitted = { target: "json-render", refusal: error.message, warnings: [], validations: [] };
|
|
129
|
+
emit({ type: "emitted", validations: [], warnings: [] });
|
|
130
|
+
const result = finalize("failed-gate", 3, {}, emitted);
|
|
131
|
+
emit({ type: "done", outcome: result.report.outcome, exitCode: result.exitCode, report: result.report });
|
|
132
|
+
return result;
|
|
133
|
+
}
|
|
134
|
+
throw error;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
// The emitter can REFUSE a lint-clean surface outright (typed
|
|
138
|
+
// EmitSurfaceError — e.g. a sub-component outside its compound parent:
|
|
139
|
+
// in-vocabulary for S2, ungoverned by S3, unprojectable by the
|
|
140
|
+
// profile). That is the emitter-gate failure class ("target
|
|
141
|
+
// equivalent" in the exit-code table), not a crash: outcome
|
|
142
|
+
// failed-gate, exit 3, refusal recorded in the report (ADR-D1 family
|
|
143
|
+
// evidence, same as an A3 rejection).
|
|
144
|
+
let emission;
|
|
145
|
+
try {
|
|
146
|
+
emission = emitSurface(surface, doc, options.emitProfile ? { profile: options.emitProfile } : {});
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
if (error instanceof EmitSurfaceError) {
|
|
150
|
+
const emitted = { target: "a2ui", refusal: error.message, warnings: [], validations: [] };
|
|
151
|
+
emit({ type: "emitted", validations: [], warnings: [] });
|
|
152
|
+
const result = finalize("failed-gate", 3, {}, emitted);
|
|
153
|
+
emit({ type: "done", outcome: result.report.outcome, exitCode: result.exitCode, report: result.report });
|
|
154
|
+
return result;
|
|
155
|
+
}
|
|
156
|
+
throw error;
|
|
157
|
+
}
|
|
158
|
+
const { messages, warnings } = emission;
|
|
159
|
+
const validations = [];
|
|
160
|
+
let gatesPass = true;
|
|
161
|
+
for (const version of options.a2uiVersions ?? ["0.9.1", "1.0"]) {
|
|
162
|
+
const { validation } = transform(doc, version, { messages }, options.emitProfile);
|
|
163
|
+
const gates = validation.gates.map((gate) => ({
|
|
164
|
+
gate: A_GATE[gate.name] ?? "A1",
|
|
165
|
+
name: gate.name,
|
|
166
|
+
pass: gate.pass,
|
|
167
|
+
errors: gate.errors,
|
|
168
|
+
}));
|
|
169
|
+
if (!validation.pass)
|
|
170
|
+
gatesPass = false;
|
|
171
|
+
validations.push({ a2uiVersion: version, gates });
|
|
172
|
+
}
|
|
173
|
+
const emitted = { target: "a2ui", surfaceMessages: { messages }, warnings, validations };
|
|
174
|
+
emit({ type: "emitted", validations, warnings });
|
|
175
|
+
const result = gatesPass
|
|
176
|
+
? finalize("passed", 0, { surface, surfaceMessages: { messages } }, emitted)
|
|
177
|
+
: finalize("failed-gate", 3, {}, emitted);
|
|
178
|
+
emit({ type: "done", outcome: result.report.outcome, exitCode: result.exitCode, report: result.report, surfaceMessages: result.surfaceMessages });
|
|
179
|
+
return result;
|
|
180
|
+
}
|
|
181
|
+
if (index < maxRepairs) {
|
|
182
|
+
const repair = renderRepairMessage(lint.findings, contract, repairTemplate);
|
|
183
|
+
repairMessages.push(repair);
|
|
184
|
+
conversation.push({ role: "assistant", content: generated.raw }, { role: "user", content: repair });
|
|
185
|
+
emit({ type: "repair", index, message: repair });
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
const exhausted = finalize("failed-lint-exhausted", 2);
|
|
189
|
+
emit({ type: "done", outcome: exhausted.report.outcome, exitCode: exhausted.exitCode, report: exhausted.report });
|
|
190
|
+
return exhausted;
|
|
191
|
+
}
|
package/dist/serve.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `dspack-gen serve` — the demo's local pipeline endpoint. Incidental
|
|
3
|
+
* plumbing: node:http, localhost-only, no framework.
|
|
4
|
+
*
|
|
5
|
+
* POST /run {prompt?, intent?, model?, fake?, maxRepairs?, noSteering?}
|
|
6
|
+
* → NDJSON stream of PipelineEvent lines (start / attempt / repair /
|
|
7
|
+
* emitted / done — the `done` event carries the full audit report v1).
|
|
8
|
+
*
|
|
9
|
+
* `fake: true` runs the deterministic ScriptedAdapter (golden violating
|
|
10
|
+
* fixture F1, then the contract's worked example for the requested intent) —
|
|
11
|
+
* the demo's verification mode and the Playwright gate's backend. Live mode
|
|
12
|
+
* requires `model` ("ollama:<tag>" | "anthropic:<id>"); as everywhere, no
|
|
13
|
+
* default model exists in code.
|
|
14
|
+
*
|
|
15
|
+
* Hardening (localhost-only is not a security boundary while a browser is
|
|
16
|
+
* running): CORS is restricted to the demo dev-server origins, request
|
|
17
|
+
* bodies are size-capped, and event emission can never fail a run.
|
|
18
|
+
*/
|
|
19
|
+
import { createServer } from "node:http";
|
|
20
|
+
export interface ServeOptions {
|
|
21
|
+
contractPath: string;
|
|
22
|
+
/** 1–65535 (validated by the CLI); 0 is permitted for tests (ephemeral port). */
|
|
23
|
+
port?: number;
|
|
24
|
+
}
|
|
25
|
+
export declare function startServer(options: ServeOptions): ReturnType<typeof createServer>;
|
package/dist/serve.js
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `dspack-gen serve` — the demo's local pipeline endpoint. Incidental
|
|
3
|
+
* plumbing: node:http, localhost-only, no framework.
|
|
4
|
+
*
|
|
5
|
+
* POST /run {prompt?, intent?, model?, fake?, maxRepairs?, noSteering?}
|
|
6
|
+
* → NDJSON stream of PipelineEvent lines (start / attempt / repair /
|
|
7
|
+
* emitted / done — the `done` event carries the full audit report v1).
|
|
8
|
+
*
|
|
9
|
+
* `fake: true` runs the deterministic ScriptedAdapter (golden violating
|
|
10
|
+
* fixture F1, then the contract's worked example for the requested intent) —
|
|
11
|
+
* the demo's verification mode and the Playwright gate's backend. Live mode
|
|
12
|
+
* requires `model` ("ollama:<tag>" | "anthropic:<id>"); as everywhere, no
|
|
13
|
+
* default model exists in code.
|
|
14
|
+
*
|
|
15
|
+
* Hardening (localhost-only is not a security boundary while a browser is
|
|
16
|
+
* running): CORS is restricted to the demo dev-server origins, request
|
|
17
|
+
* bodies are size-capped, and event emission can never fail a run.
|
|
18
|
+
*/
|
|
19
|
+
import { createServer } from "node:http";
|
|
20
|
+
import { readFileSync } from "node:fs";
|
|
21
|
+
import { resolve } from "node:path";
|
|
22
|
+
import { UnknownRuleTypeError } from "./core/lint/index.js";
|
|
23
|
+
import { adapterFor } from "./adapters/index.js";
|
|
24
|
+
import { ScriptedAdapter } from "./adapters/fake.js";
|
|
25
|
+
import { runPipeline } from "./run/orchestrator.js";
|
|
26
|
+
/** Only the demo dev server may read responses cross-origin. */
|
|
27
|
+
const ALLOWED_ORIGINS = new Set(["http://localhost:5173", "http://127.0.0.1:5173"]);
|
|
28
|
+
/** Pipeline requests are small JSON documents; anything bigger is a mistake. */
|
|
29
|
+
const MAX_BODY_BYTES = 64 * 1024;
|
|
30
|
+
export function startServer(options) {
|
|
31
|
+
const contract = JSON.parse(readFileSync(resolve(options.contractPath), "utf8"));
|
|
32
|
+
const violating = JSON.parse(readFileSync(resolve("fixtures/golden/violating/F1-dialog-for-delete.dsurface.json"), "utf8"));
|
|
33
|
+
const server = createServer((request, response) => {
|
|
34
|
+
const origin = request.headers.origin;
|
|
35
|
+
if (origin && ALLOWED_ORIGINS.has(origin)) {
|
|
36
|
+
response.setHeader("access-control-allow-origin", origin);
|
|
37
|
+
response.setHeader("vary", "origin");
|
|
38
|
+
response.setHeader("access-control-allow-methods", "GET, POST, OPTIONS");
|
|
39
|
+
response.setHeader("access-control-allow-headers", "content-type");
|
|
40
|
+
}
|
|
41
|
+
if (request.method === "OPTIONS") {
|
|
42
|
+
response.writeHead(204).end();
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (request.method === "GET" && request.url === "/health") {
|
|
46
|
+
response.writeHead(200, { "content-type": "application/json" });
|
|
47
|
+
response.end(JSON.stringify({ ok: true, contract: contract.name }));
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (request.method !== "POST" || request.url !== "/run") {
|
|
51
|
+
response.writeHead(404).end();
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
let raw = "";
|
|
55
|
+
let bytes = 0;
|
|
56
|
+
let tooLarge = false;
|
|
57
|
+
request.on("data", (chunk) => {
|
|
58
|
+
if (tooLarge)
|
|
59
|
+
return;
|
|
60
|
+
bytes += chunk.length; // byte-accurate: string length counts UTF-16 code units
|
|
61
|
+
raw += chunk;
|
|
62
|
+
if (bytes > MAX_BODY_BYTES) {
|
|
63
|
+
tooLarge = true;
|
|
64
|
+
response.writeHead(413, { "content-type": "application/json" });
|
|
65
|
+
response.end(JSON.stringify({ error: `request body exceeds ${MAX_BODY_BYTES} bytes` }));
|
|
66
|
+
request.destroy();
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
request.on("end", () => {
|
|
70
|
+
if (tooLarge)
|
|
71
|
+
return;
|
|
72
|
+
void (async () => {
|
|
73
|
+
let body;
|
|
74
|
+
try {
|
|
75
|
+
body = raw ? JSON.parse(raw) : {};
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
response.writeHead(400, { "content-type": "application/json" });
|
|
79
|
+
response.end(JSON.stringify({ error: "invalid JSON body" }));
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const intent = body.intent ?? "destructive-action";
|
|
83
|
+
// Untrusted JSON: booleans are strict, numbers are validated.
|
|
84
|
+
const fake = body.fake === true;
|
|
85
|
+
const noSteering = body.noSteering === true;
|
|
86
|
+
if (body.maxRepairs !== undefined && (!Number.isInteger(body.maxRepairs) || body.maxRepairs < 0)) {
|
|
87
|
+
response.writeHead(400, { "content-type": "application/json" });
|
|
88
|
+
response.end(JSON.stringify({ error: "maxRepairs must be a non-negative integer" }));
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
let adapter;
|
|
92
|
+
if (fake) {
|
|
93
|
+
// The scripted repair must land on the contract's worked example
|
|
94
|
+
// FOR THIS INTENT — fail fast instead of scripting `undefined`.
|
|
95
|
+
const example = (contract.examples ?? []).find((e) => e.intent === intent);
|
|
96
|
+
if (!example) {
|
|
97
|
+
response.writeHead(400, { "content-type": "application/json" });
|
|
98
|
+
response.end(JSON.stringify({ error: `contract has no example for intent '${intent}' — fake mode needs one` }));
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
adapter = new ScriptedAdapter([{ output: violating }, { output: example.surface }]);
|
|
102
|
+
}
|
|
103
|
+
else if (typeof body.model === "string") {
|
|
104
|
+
adapter = adapterFor(body.model);
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
response.writeHead(400, { "content-type": "application/json" });
|
|
108
|
+
response.end(JSON.stringify({ error: "either fake: true or model: 'ollama:<tag>|anthropic:<id>' is required" }));
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
response.writeHead(200, { "content-type": "application/x-ndjson", "cache-control": "no-store" });
|
|
112
|
+
const send = (event) => {
|
|
113
|
+
response.write(JSON.stringify(event) + "\n");
|
|
114
|
+
};
|
|
115
|
+
try {
|
|
116
|
+
await runPipeline({
|
|
117
|
+
contract,
|
|
118
|
+
intent,
|
|
119
|
+
prompt: body.prompt ?? "a screen to delete my account",
|
|
120
|
+
adapter,
|
|
121
|
+
maxRepairs: body.maxRepairs,
|
|
122
|
+
compile: { omitRuleSteering: noSteering },
|
|
123
|
+
onEvent: send,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
send({
|
|
128
|
+
type: "error",
|
|
129
|
+
exitCode: error instanceof UnknownRuleTypeError ? 4 : 1,
|
|
130
|
+
message: error instanceof Error ? error.message : String(error),
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
response.end();
|
|
134
|
+
})();
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
const port = options.port ?? 8787;
|
|
138
|
+
server.listen(port, "127.0.0.1", () => {
|
|
139
|
+
const address = server.address();
|
|
140
|
+
const actual = typeof address === "object" && address ? address.port : port;
|
|
141
|
+
console.error(`dspack-gen serve: http://127.0.0.1:${actual} (contract: ${contract.name})`);
|
|
142
|
+
});
|
|
143
|
+
return server;
|
|
144
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@aestheticfunction/dspack-gen",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Generation + governance pipeline for dspack contracts: prompt/context compiler, surface gates S1\u2013S3, bounded repair, protocol emission, audit reports.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "Apache-2.0",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/aestheticfunction/dspack-gen.git"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/aestheticfunction/dspack-gen#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/aestheticfunction/dspack-gen/issues"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"dspack",
|
|
17
|
+
"design-systems",
|
|
18
|
+
"generative-ui",
|
|
19
|
+
"governance",
|
|
20
|
+
"a2ui",
|
|
21
|
+
"llm"
|
|
22
|
+
],
|
|
23
|
+
"publishConfig": {
|
|
24
|
+
"access": "public"
|
|
25
|
+
},
|
|
26
|
+
"bin": {
|
|
27
|
+
"dspack-gen": "./dist/cli.js"
|
|
28
|
+
},
|
|
29
|
+
"exports": {
|
|
30
|
+
".": {
|
|
31
|
+
"types": "./dist/index.d.ts",
|
|
32
|
+
"import": "./dist/index.js"
|
|
33
|
+
},
|
|
34
|
+
"./core": {
|
|
35
|
+
"types": "./dist/core/index.d.ts",
|
|
36
|
+
"import": "./dist/core/index.js"
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"files": [
|
|
40
|
+
"dist",
|
|
41
|
+
"src",
|
|
42
|
+
"!src/**/*.test.ts",
|
|
43
|
+
"LICENSE",
|
|
44
|
+
"README.md",
|
|
45
|
+
"CHANGELOG.md"
|
|
46
|
+
],
|
|
47
|
+
"scripts": {
|
|
48
|
+
"build": "tsc -p tsconfig.json",
|
|
49
|
+
"prepare": "npm run build",
|
|
50
|
+
"prepublishOnly": "npm run test",
|
|
51
|
+
"test": "vitest run",
|
|
52
|
+
"context": "tsx src/cli.ts context",
|
|
53
|
+
"smoke:ollama": "tsx scripts/smoke-ollama.ts",
|
|
54
|
+
"demo:e2e": "playwright test",
|
|
55
|
+
"eval": "tsx src/eval/run.ts",
|
|
56
|
+
"eval:assert": "tsx src/eval/assert.ts",
|
|
57
|
+
"check:sync": "node scripts/check-sync.mjs"
|
|
58
|
+
},
|
|
59
|
+
"dependencies": {
|
|
60
|
+
"@aestheticfunction/dspack-emit": "^0.3.1",
|
|
61
|
+
"@anthropic-ai/sdk": "^0.109.1",
|
|
62
|
+
"ajv": "^8.17.1",
|
|
63
|
+
"ajv-formats": "^3.0.1",
|
|
64
|
+
"undici": "^6.27.0"
|
|
65
|
+
},
|
|
66
|
+
"devDependencies": {
|
|
67
|
+
"@playwright/test": "^1.61.1",
|
|
68
|
+
"@types/node": "^22.10.2",
|
|
69
|
+
"tsx": "^4.19.2",
|
|
70
|
+
"typescript": "^5.7.2",
|
|
71
|
+
"vitest": "^2.1.8"
|
|
72
|
+
},
|
|
73
|
+
"engines": {
|
|
74
|
+
"node": ">=20.0.0"
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Anthropic adapter — hosted generation via the official SDK with
|
|
3
|
+
* schema-constrained output (`output_config.format`, type json_schema).
|
|
4
|
+
*
|
|
5
|
+
* The generation schema is compatible by construction: depth-unrolled (the
|
|
6
|
+
* structured-outputs API rejects recursive schemas) and additionalProperties:
|
|
7
|
+
* false on every object. No sampling parameters are sent — they are removed
|
|
8
|
+
* on current Claude models. `stop_reason: "refusal"` is surfaced as a typed
|
|
9
|
+
* AdapterOutputError (never retried silently; retries are the repair loop's
|
|
10
|
+
* job and only for lint findings).
|
|
11
|
+
*/
|
|
12
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
13
|
+
import {
|
|
14
|
+
AdapterOutputError,
|
|
15
|
+
parseJsonOutput,
|
|
16
|
+
type FetchLike,
|
|
17
|
+
type GenerateRequest,
|
|
18
|
+
type GenerateResult,
|
|
19
|
+
type GenerationAdapter,
|
|
20
|
+
} from "./types.js";
|
|
21
|
+
|
|
22
|
+
export interface AnthropicAdapterOptions {
|
|
23
|
+
/** Required — no default model exists in code (ADR-9 as amended). */
|
|
24
|
+
model: string;
|
|
25
|
+
/** Defaults to the SDK's environment resolution (ANTHROPIC_API_KEY, auth token, profile). */
|
|
26
|
+
apiKey?: string;
|
|
27
|
+
baseURL?: string;
|
|
28
|
+
fetch?: FetchLike;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export class AnthropicAdapter implements GenerationAdapter {
|
|
32
|
+
readonly id: string;
|
|
33
|
+
private readonly model: string;
|
|
34
|
+
private readonly client: Anthropic;
|
|
35
|
+
|
|
36
|
+
constructor(options: AnthropicAdapterOptions) {
|
|
37
|
+
if (!options.model) throw new Error("AnthropicAdapter requires an explicit model id (config/env/flag)");
|
|
38
|
+
this.model = options.model;
|
|
39
|
+
this.client = new Anthropic({
|
|
40
|
+
apiKey: options.apiKey,
|
|
41
|
+
baseURL: options.baseURL,
|
|
42
|
+
fetch: options.fetch as never,
|
|
43
|
+
});
|
|
44
|
+
this.id = `anthropic:${this.model}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async generate(request: GenerateRequest): Promise<GenerateResult> {
|
|
48
|
+
// Typed transport: SDK connection/API errors surface as failed-adapter
|
|
49
|
+
// outcomes (with a report), never as raw exceptions that kill a matrix.
|
|
50
|
+
let response: unknown;
|
|
51
|
+
try {
|
|
52
|
+
response = await this.client.messages.create({
|
|
53
|
+
model: this.model,
|
|
54
|
+
max_tokens: request.params?.maxTokens ?? 16000,
|
|
55
|
+
system: request.system,
|
|
56
|
+
messages: request.messages,
|
|
57
|
+
output_config: {
|
|
58
|
+
format: { type: "json_schema", schema: request.jsonSchema as never },
|
|
59
|
+
},
|
|
60
|
+
} as never);
|
|
61
|
+
} catch (error) {
|
|
62
|
+
if (error instanceof AdapterOutputError) throw error;
|
|
63
|
+
const name = error instanceof Error ? error.constructor.name : "unknown";
|
|
64
|
+
throw new AdapterOutputError(this.id, `transport failure (${name}): ${error instanceof Error ? error.message : String(error)}`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const message = response as Anthropic.Message;
|
|
68
|
+
if (message.stop_reason === "refusal") {
|
|
69
|
+
throw new AdapterOutputError(this.id, "model refused the request (stop_reason: refusal)");
|
|
70
|
+
}
|
|
71
|
+
const raw = message.content
|
|
72
|
+
.filter((block): block is Anthropic.TextBlock => block.type === "text")
|
|
73
|
+
.map((block) => block.text)
|
|
74
|
+
.join("");
|
|
75
|
+
if (raw === "") throw new AdapterOutputError(this.id, "empty model output");
|
|
76
|
+
if (message.stop_reason === "max_tokens") {
|
|
77
|
+
throw new AdapterOutputError(this.id, "output truncated (stop_reason: max_tokens) — raise params.maxTokens", raw);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
json: parseJsonOutput(this.id, raw),
|
|
82
|
+
raw,
|
|
83
|
+
model: message.model,
|
|
84
|
+
usage: { inputTokens: message.usage.input_tokens, outputTokens: message.usage.output_tokens },
|
|
85
|
+
meta: { provider: "anthropic", stop_reason: message.stop_reason },
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scripted adapter — the deterministic instrument behind CI gates, the demo's
|
|
3
|
+
* verification mode, and the eval harness's golden runs. Returns the scripted
|
|
4
|
+
* outputs in order; a script entry of `{ error: "..." }` raises a typed
|
|
5
|
+
* AdapterOutputError instead.
|
|
6
|
+
*/
|
|
7
|
+
import { AdapterOutputError, type GenerateRequest, type GenerateResult, type GenerationAdapter } from "./types.js";
|
|
8
|
+
|
|
9
|
+
export type ScriptEntry = { output: unknown } | { error: string };
|
|
10
|
+
|
|
11
|
+
export class ScriptedAdapter implements GenerationAdapter {
|
|
12
|
+
readonly id = "fake:scripted";
|
|
13
|
+
/** Every request received, for assertions on repair-loop conversation state. */
|
|
14
|
+
readonly requests: GenerateRequest[] = [];
|
|
15
|
+
private cursor = 0;
|
|
16
|
+
|
|
17
|
+
constructor(private readonly script: ScriptEntry[]) {}
|
|
18
|
+
|
|
19
|
+
async generate(request: GenerateRequest): Promise<GenerateResult> {
|
|
20
|
+
this.requests.push(request);
|
|
21
|
+
const entry = this.script[this.cursor++];
|
|
22
|
+
if (!entry) throw new Error(`ScriptedAdapter: script exhausted after ${this.script.length} generation(s)`);
|
|
23
|
+
if ("error" in entry) throw new AdapterOutputError(this.id, entry.error);
|
|
24
|
+
return {
|
|
25
|
+
json: entry.output,
|
|
26
|
+
raw: JSON.stringify(entry.output),
|
|
27
|
+
model: "fake-model",
|
|
28
|
+
usage: { inputTokens: 0, outputTokens: 0 },
|
|
29
|
+
meta: { provider: "fake" },
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export * from "./types.js";
|
|
2
|
+
export { OllamaAdapter, type OllamaAdapterOptions } from "./ollama.js";
|
|
3
|
+
export { AnthropicAdapter, type AnthropicAdapterOptions } from "./anthropic.js";
|
|
4
|
+
|
|
5
|
+
import { OllamaAdapter } from "./ollama.js";
|
|
6
|
+
import { AnthropicAdapter } from "./anthropic.js";
|
|
7
|
+
import { parseModelRef, type GenerationAdapter } from "./types.js";
|
|
8
|
+
|
|
9
|
+
/** Build an adapter from a `--model` reference (ollama:<id> | anthropic:<id>). */
|
|
10
|
+
export function adapterFor(modelRef: string): GenerationAdapter {
|
|
11
|
+
const { provider, model } = parseModelRef(modelRef);
|
|
12
|
+
return provider === "ollama" ? new OllamaAdapter({ model }) : new AnthropicAdapter({ model });
|
|
13
|
+
}
|