@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,272 @@
|
|
|
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 {
|
|
13
|
+
buildCatalogModel,
|
|
14
|
+
emitJsonRenderSpec,
|
|
15
|
+
EmitJsonRenderError,
|
|
16
|
+
emitSurface,
|
|
17
|
+
EmitSurfaceError,
|
|
18
|
+
transform,
|
|
19
|
+
type A2uiVersion,
|
|
20
|
+
type DspackDoc,
|
|
21
|
+
type DspackSurface,
|
|
22
|
+
type EmitSurfaceResult,
|
|
23
|
+
type Profile,
|
|
24
|
+
validateSpecAgainstModel,
|
|
25
|
+
} from "@aestheticfunction/dspack-emit";
|
|
26
|
+
import type { Contract } from "../core/contract.js";
|
|
27
|
+
import { applicableRules, compileContext, type CompileOptions } from "../core/compiler.js";
|
|
28
|
+
import { lintSurface, type Finding, type GateReport } from "../core/lint/index.js";
|
|
29
|
+
import { AdapterOutputError, type GenerateMessage, type GenerationAdapter } from "../adapters/types.js";
|
|
30
|
+
import { renderRepairMessage, type RepairTemplate } from "../repair/render.js";
|
|
31
|
+
import {
|
|
32
|
+
contractDigest,
|
|
33
|
+
sha256,
|
|
34
|
+
REPORT_VERSION,
|
|
35
|
+
type AttemptRecord,
|
|
36
|
+
type AuditReportV1,
|
|
37
|
+
type EmitterGate,
|
|
38
|
+
type EmittedValidation,
|
|
39
|
+
type Outcome,
|
|
40
|
+
} from "../audit/report.js";
|
|
41
|
+
|
|
42
|
+
export interface RunOptions {
|
|
43
|
+
contract: Contract;
|
|
44
|
+
intent: string;
|
|
45
|
+
prompt: string;
|
|
46
|
+
adapter: GenerationAdapter;
|
|
47
|
+
/** Regeneration attempts after the first (default 2 ⇒ ≤3 generations). */
|
|
48
|
+
maxRepairs?: number;
|
|
49
|
+
/** ADR-7 repair template variant (default "standard"); recorded in the report. */
|
|
50
|
+
repairTemplate?: RepairTemplate;
|
|
51
|
+
compile?: CompileOptions;
|
|
52
|
+
/** A2UI versions to validate the emitted surface against. */
|
|
53
|
+
a2uiVersions?: A2uiVersion[];
|
|
54
|
+
/**
|
|
55
|
+
* Emission target (PR-21). Default "a2ui" — byte-identical behavior for
|
|
56
|
+
* existing callers. "json-render" emits via emitJsonRenderSpec against the
|
|
57
|
+
* contract-generated catalog model (empty profile — the asymmetry
|
|
58
|
+
* finding's pairing) with per-run gates J2 (emission accepted) and J3
|
|
59
|
+
* (instance validates against the generated model). J1 (generated modules
|
|
60
|
+
* compile under real zod) is a dspack-emit CI gate, not per-run.
|
|
61
|
+
*/
|
|
62
|
+
emitTarget?: "a2ui" | "json-render";
|
|
63
|
+
/**
|
|
64
|
+
* A2UI mapping profile for the "a2ui" target (default: the emitter's
|
|
65
|
+
* shadcn profile, byte-identical behavior for existing callers). Pass the
|
|
66
|
+
* contract's own profile to emit non-shadcn contracts (e.g. Astryx).
|
|
67
|
+
*/
|
|
68
|
+
emitProfile?: Profile;
|
|
69
|
+
/** Injectable clock for deterministic reports in tests. */
|
|
70
|
+
now?: () => Date;
|
|
71
|
+
/** Live progress events (the demo's NDJSON stream). Purely observational. */
|
|
72
|
+
onEvent?: (event: PipelineEvent) => void;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Progress events streamed by `dspack-gen serve` (observational; the report is the artifact). */
|
|
76
|
+
export type PipelineEvent =
|
|
77
|
+
| { type: "start"; intent: string; prompt: string; adapterId: string; ruleIds: string[] }
|
|
78
|
+
| { type: "attempt"; index: number; model?: string; surface: unknown; gates: GateReport[]; findings: Finding[] }
|
|
79
|
+
| { type: "repair"; index: number; message: string }
|
|
80
|
+
| { type: "emitted"; validations: EmittedValidation[]; warnings: Array<{ code: string; message: string }> }
|
|
81
|
+
| { type: "done"; outcome: Outcome; exitCode: number; report: AuditReportV1; surfaceMessages?: unknown };
|
|
82
|
+
|
|
83
|
+
export interface RunResult {
|
|
84
|
+
report: AuditReportV1;
|
|
85
|
+
exitCode: 0 | 1 | 2 | 3;
|
|
86
|
+
/** The governed surface and its A2UI emission, when the pipeline passed. */
|
|
87
|
+
surface?: DspackSurface;
|
|
88
|
+
surfaceMessages?: unknown;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Map the emitter's ajv gate names onto the architecture-wide A1/A2/A3. */
|
|
92
|
+
const A_GATE: Record<string, "A1" | "A2" | "A3"> = {
|
|
93
|
+
"schema-compile + no-external-ref": "A1",
|
|
94
|
+
"catalog-shape": "A2",
|
|
95
|
+
instance: "A3",
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
export async function runPipeline(options: RunOptions): Promise<RunResult> {
|
|
99
|
+
const { contract, intent, prompt, adapter } = options;
|
|
100
|
+
const maxRepairs = options.maxRepairs ?? 2;
|
|
101
|
+
const repairTemplate = options.repairTemplate ?? "standard";
|
|
102
|
+
const now = options.now ?? (() => new Date());
|
|
103
|
+
const startedAt = now();
|
|
104
|
+
|
|
105
|
+
const context = compileContext(contract, intent, options.compile);
|
|
106
|
+
const conversation: GenerateMessage[] = [...context.fewshot, { role: "user", content: prompt }];
|
|
107
|
+
// Purely observational, enforced: a throwing hook (e.g. a stream write
|
|
108
|
+
// after the client disconnected) must never abort the pipeline or change
|
|
109
|
+
// its outcome — the audit report is the artifact, events are a view.
|
|
110
|
+
const emit = (event: PipelineEvent): void => {
|
|
111
|
+
try {
|
|
112
|
+
options.onEvent?.(event);
|
|
113
|
+
} catch {
|
|
114
|
+
/* observational — swallowed by contract */
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
emit({
|
|
118
|
+
type: "start",
|
|
119
|
+
intent,
|
|
120
|
+
prompt,
|
|
121
|
+
adapterId: adapter.id,
|
|
122
|
+
ruleIds: applicableRules(contract, intent).map((rule) => rule.id),
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const attempts: AttemptRecord[] = [];
|
|
126
|
+
const repairMessages: string[] = [];
|
|
127
|
+
|
|
128
|
+
const finalize = (outcome: Outcome, exitCode: RunResult["exitCode"], extra: Partial<RunResult> = {}, emitted?: AuditReportV1["emitted"]): RunResult => ({
|
|
129
|
+
report: {
|
|
130
|
+
reportVersion: REPORT_VERSION,
|
|
131
|
+
createdAt: startedAt.toISOString(),
|
|
132
|
+
request: { prompt, intent, contract: contractDigest(contract) },
|
|
133
|
+
generation: {
|
|
134
|
+
adapterId: adapter.id,
|
|
135
|
+
schemaSha256: sha256(context.schema),
|
|
136
|
+
maxRepairs,
|
|
137
|
+
ruleSteering: !options.compile?.omitRuleSteering,
|
|
138
|
+
repairTemplate,
|
|
139
|
+
},
|
|
140
|
+
attempts,
|
|
141
|
+
repairMessages,
|
|
142
|
+
outcome,
|
|
143
|
+
...(emitted ? { emitted } : {}),
|
|
144
|
+
timings: { totalMs: now().getTime() - startedAt.getTime() },
|
|
145
|
+
},
|
|
146
|
+
exitCode,
|
|
147
|
+
...extra,
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
for (let index = 0; index <= maxRepairs; index++) {
|
|
151
|
+
let generated;
|
|
152
|
+
try {
|
|
153
|
+
// Snapshot per attempt: adapters may hold the request beyond the call.
|
|
154
|
+
generated = await adapter.generate({ system: context.system, messages: [...conversation], jsonSchema: context.schema });
|
|
155
|
+
} catch (error) {
|
|
156
|
+
if (error instanceof AdapterOutputError) {
|
|
157
|
+
attempts.push({ index, adapterError: error.message });
|
|
158
|
+
const failed = finalize("failed-adapter", 1);
|
|
159
|
+
emit({ type: "done", outcome: failed.report.outcome, exitCode: failed.exitCode, report: failed.report });
|
|
160
|
+
return failed;
|
|
161
|
+
}
|
|
162
|
+
throw error;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Surface gates S1–S3 over the artifact (UnknownRuleTypeError propagates: CLI exit 4).
|
|
166
|
+
const lint = lintSurface(generated.json, contract);
|
|
167
|
+
attempts.push({
|
|
168
|
+
index,
|
|
169
|
+
surface: generated.json,
|
|
170
|
+
model: generated.model,
|
|
171
|
+
usage: generated.usage,
|
|
172
|
+
meta: generated.meta,
|
|
173
|
+
gates: lint.gates,
|
|
174
|
+
findings: lint.findings,
|
|
175
|
+
});
|
|
176
|
+
emit({ type: "attempt", index, model: generated.model, surface: generated.json, gates: lint.gates, findings: lint.findings });
|
|
177
|
+
|
|
178
|
+
if (lint.pass) {
|
|
179
|
+
const surface = generated.json as DspackSurface;
|
|
180
|
+
const doc = contract as unknown as DspackDoc;
|
|
181
|
+
|
|
182
|
+
if (options.emitTarget === "json-render") {
|
|
183
|
+
// Second-emitter path (PR-21): same refusal semantics as a2ui — a
|
|
184
|
+
// typed emit error is the gate-failure class, never a crash.
|
|
185
|
+
try {
|
|
186
|
+
const model = buildCatalogModel(doc, {});
|
|
187
|
+
const { spec } = emitJsonRenderSpec(surface, doc, {});
|
|
188
|
+
const findings = validateSpecAgainstModel(spec, model);
|
|
189
|
+
const gates = [
|
|
190
|
+
{ gate: "J2" as const, name: "emission accepted", pass: true },
|
|
191
|
+
{
|
|
192
|
+
gate: "J3" as const,
|
|
193
|
+
name: "instance validates against the generated catalog model",
|
|
194
|
+
pass: findings.length === 0,
|
|
195
|
+
...(findings.length ? { errors: findings.map((f) => f.message) } : {}),
|
|
196
|
+
},
|
|
197
|
+
];
|
|
198
|
+
const pass = gates.every((g) => g.pass);
|
|
199
|
+
const emitted = { target: "json-render" as const, spec, warnings: [], validations: [{ gates }] };
|
|
200
|
+
emit({ type: "emitted", validations: [{ gates }], warnings: [] });
|
|
201
|
+
const result = pass ? finalize("passed", 0, { surface }, emitted) : finalize("failed-gate", 3, {}, emitted);
|
|
202
|
+
emit({ type: "done", outcome: result.report.outcome, exitCode: result.exitCode, report: result.report });
|
|
203
|
+
return result;
|
|
204
|
+
} catch (error) {
|
|
205
|
+
if (error instanceof EmitJsonRenderError) {
|
|
206
|
+
const emitted = { target: "json-render" as const, refusal: error.message, warnings: [], validations: [] };
|
|
207
|
+
emit({ type: "emitted", validations: [], warnings: [] });
|
|
208
|
+
const result = finalize("failed-gate", 3, {}, emitted);
|
|
209
|
+
emit({ type: "done", outcome: result.report.outcome, exitCode: result.exitCode, report: result.report });
|
|
210
|
+
return result;
|
|
211
|
+
}
|
|
212
|
+
throw error;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// The emitter can REFUSE a lint-clean surface outright (typed
|
|
217
|
+
// EmitSurfaceError — e.g. a sub-component outside its compound parent:
|
|
218
|
+
// in-vocabulary for S2, ungoverned by S3, unprojectable by the
|
|
219
|
+
// profile). That is the emitter-gate failure class ("target
|
|
220
|
+
// equivalent" in the exit-code table), not a crash: outcome
|
|
221
|
+
// failed-gate, exit 3, refusal recorded in the report (ADR-D1 family
|
|
222
|
+
// evidence, same as an A3 rejection).
|
|
223
|
+
let emission: EmitSurfaceResult;
|
|
224
|
+
try {
|
|
225
|
+
emission = emitSurface(surface, doc, options.emitProfile ? { profile: options.emitProfile } : {});
|
|
226
|
+
} catch (error) {
|
|
227
|
+
if (error instanceof EmitSurfaceError) {
|
|
228
|
+
const emitted = { target: "a2ui" as const, refusal: error.message, warnings: [], validations: [] };
|
|
229
|
+
emit({ type: "emitted", validations: [], warnings: [] });
|
|
230
|
+
const result = finalize("failed-gate", 3, {}, emitted);
|
|
231
|
+
emit({ type: "done", outcome: result.report.outcome, exitCode: result.exitCode, report: result.report });
|
|
232
|
+
return result;
|
|
233
|
+
}
|
|
234
|
+
throw error;
|
|
235
|
+
}
|
|
236
|
+
const { messages, warnings } = emission;
|
|
237
|
+
|
|
238
|
+
const validations: EmittedValidation[] = [];
|
|
239
|
+
let gatesPass = true;
|
|
240
|
+
for (const version of options.a2uiVersions ?? (["0.9.1", "1.0"] as A2uiVersion[])) {
|
|
241
|
+
const { validation } = transform(doc, version, { messages }, options.emitProfile);
|
|
242
|
+
const gates: EmitterGate[] = validation.gates.map((gate) => ({
|
|
243
|
+
gate: A_GATE[gate.name] ?? "A1",
|
|
244
|
+
name: gate.name,
|
|
245
|
+
pass: gate.pass,
|
|
246
|
+
errors: gate.errors,
|
|
247
|
+
}));
|
|
248
|
+
if (!validation.pass) gatesPass = false;
|
|
249
|
+
validations.push({ a2uiVersion: version, gates });
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const emitted = { target: "a2ui" as const, surfaceMessages: { messages }, warnings, validations };
|
|
253
|
+
emit({ type: "emitted", validations, warnings });
|
|
254
|
+
const result = gatesPass
|
|
255
|
+
? finalize("passed", 0, { surface, surfaceMessages: { messages } }, emitted)
|
|
256
|
+
: finalize("failed-gate", 3, {}, emitted);
|
|
257
|
+
emit({ type: "done", outcome: result.report.outcome, exitCode: result.exitCode, report: result.report, surfaceMessages: result.surfaceMessages });
|
|
258
|
+
return result;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (index < maxRepairs) {
|
|
262
|
+
const repair = renderRepairMessage(lint.findings, contract, repairTemplate);
|
|
263
|
+
repairMessages.push(repair);
|
|
264
|
+
conversation.push({ role: "assistant", content: generated.raw }, { role: "user", content: repair });
|
|
265
|
+
emit({ type: "repair", index, message: repair });
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const exhausted = finalize("failed-lint-exhausted", 2);
|
|
270
|
+
emit({ type: "done", outcome: exhausted.report.outcome, exitCode: exhausted.exitCode, report: exhausted.report });
|
|
271
|
+
return exhausted;
|
|
272
|
+
}
|
package/src/serve.ts
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
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 type { Contract } from "./core/contract.js";
|
|
23
|
+
import { UnknownRuleTypeError } from "./core/lint/index.js";
|
|
24
|
+
import { adapterFor } from "./adapters/index.js";
|
|
25
|
+
import { ScriptedAdapter } from "./adapters/fake.js";
|
|
26
|
+
import { runPipeline } from "./run/orchestrator.js";
|
|
27
|
+
|
|
28
|
+
export interface ServeOptions {
|
|
29
|
+
contractPath: string;
|
|
30
|
+
/** 1–65535 (validated by the CLI); 0 is permitted for tests (ephemeral port). */
|
|
31
|
+
port?: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface RunBody {
|
|
35
|
+
prompt?: string;
|
|
36
|
+
intent?: string;
|
|
37
|
+
model?: string;
|
|
38
|
+
fake?: boolean;
|
|
39
|
+
maxRepairs?: number;
|
|
40
|
+
noSteering?: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Only the demo dev server may read responses cross-origin. */
|
|
44
|
+
const ALLOWED_ORIGINS = new Set(["http://localhost:5173", "http://127.0.0.1:5173"]);
|
|
45
|
+
|
|
46
|
+
/** Pipeline requests are small JSON documents; anything bigger is a mistake. */
|
|
47
|
+
const MAX_BODY_BYTES = 64 * 1024;
|
|
48
|
+
|
|
49
|
+
export function startServer(options: ServeOptions): ReturnType<typeof createServer> {
|
|
50
|
+
const contract = JSON.parse(readFileSync(resolve(options.contractPath), "utf8")) as Contract;
|
|
51
|
+
const violating = JSON.parse(
|
|
52
|
+
readFileSync(resolve("fixtures/golden/violating/F1-dialog-for-delete.dsurface.json"), "utf8"),
|
|
53
|
+
) as unknown;
|
|
54
|
+
|
|
55
|
+
const server = createServer((request, response) => {
|
|
56
|
+
const origin = request.headers.origin;
|
|
57
|
+
if (origin && ALLOWED_ORIGINS.has(origin)) {
|
|
58
|
+
response.setHeader("access-control-allow-origin", origin);
|
|
59
|
+
response.setHeader("vary", "origin");
|
|
60
|
+
response.setHeader("access-control-allow-methods", "GET, POST, OPTIONS");
|
|
61
|
+
response.setHeader("access-control-allow-headers", "content-type");
|
|
62
|
+
}
|
|
63
|
+
if (request.method === "OPTIONS") {
|
|
64
|
+
response.writeHead(204).end();
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (request.method === "GET" && request.url === "/health") {
|
|
68
|
+
response.writeHead(200, { "content-type": "application/json" });
|
|
69
|
+
response.end(JSON.stringify({ ok: true, contract: contract.name }));
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (request.method !== "POST" || request.url !== "/run") {
|
|
73
|
+
response.writeHead(404).end();
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
let raw = "";
|
|
78
|
+
let bytes = 0;
|
|
79
|
+
let tooLarge = false;
|
|
80
|
+
request.on("data", (chunk: Buffer) => {
|
|
81
|
+
if (tooLarge) return;
|
|
82
|
+
bytes += chunk.length; // byte-accurate: string length counts UTF-16 code units
|
|
83
|
+
raw += chunk;
|
|
84
|
+
if (bytes > MAX_BODY_BYTES) {
|
|
85
|
+
tooLarge = true;
|
|
86
|
+
response.writeHead(413, { "content-type": "application/json" });
|
|
87
|
+
response.end(JSON.stringify({ error: `request body exceeds ${MAX_BODY_BYTES} bytes` }));
|
|
88
|
+
request.destroy();
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
request.on("end", () => {
|
|
92
|
+
if (tooLarge) return;
|
|
93
|
+
void (async () => {
|
|
94
|
+
let body: RunBody;
|
|
95
|
+
try {
|
|
96
|
+
body = raw ? (JSON.parse(raw) as RunBody) : {};
|
|
97
|
+
} catch {
|
|
98
|
+
response.writeHead(400, { "content-type": "application/json" });
|
|
99
|
+
response.end(JSON.stringify({ error: "invalid JSON body" }));
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const intent = body.intent ?? "destructive-action";
|
|
104
|
+
// Untrusted JSON: booleans are strict, numbers are validated.
|
|
105
|
+
const fake = body.fake === true;
|
|
106
|
+
const noSteering = body.noSteering === true;
|
|
107
|
+
if (body.maxRepairs !== undefined && (!Number.isInteger(body.maxRepairs) || body.maxRepairs < 0)) {
|
|
108
|
+
response.writeHead(400, { "content-type": "application/json" });
|
|
109
|
+
response.end(JSON.stringify({ error: "maxRepairs must be a non-negative integer" }));
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
let adapter;
|
|
113
|
+
if (fake) {
|
|
114
|
+
// The scripted repair must land on the contract's worked example
|
|
115
|
+
// FOR THIS INTENT — fail fast instead of scripting `undefined`.
|
|
116
|
+
const example = (contract.examples ?? []).find((e) => e.intent === intent);
|
|
117
|
+
if (!example) {
|
|
118
|
+
response.writeHead(400, { "content-type": "application/json" });
|
|
119
|
+
response.end(JSON.stringify({ error: `contract has no example for intent '${intent}' — fake mode needs one` }));
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
adapter = new ScriptedAdapter([{ output: violating }, { output: example.surface }]);
|
|
123
|
+
} else if (typeof body.model === "string") {
|
|
124
|
+
adapter = adapterFor(body.model);
|
|
125
|
+
} else {
|
|
126
|
+
response.writeHead(400, { "content-type": "application/json" });
|
|
127
|
+
response.end(JSON.stringify({ error: "either fake: true or model: 'ollama:<tag>|anthropic:<id>' is required" }));
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
response.writeHead(200, { "content-type": "application/x-ndjson", "cache-control": "no-store" });
|
|
132
|
+
const send = (event: unknown): void => {
|
|
133
|
+
response.write(JSON.stringify(event) + "\n");
|
|
134
|
+
};
|
|
135
|
+
try {
|
|
136
|
+
await runPipeline({
|
|
137
|
+
contract,
|
|
138
|
+
intent,
|
|
139
|
+
prompt: body.prompt ?? "a screen to delete my account",
|
|
140
|
+
adapter,
|
|
141
|
+
maxRepairs: body.maxRepairs,
|
|
142
|
+
compile: { omitRuleSteering: noSteering },
|
|
143
|
+
onEvent: send,
|
|
144
|
+
});
|
|
145
|
+
} catch (error) {
|
|
146
|
+
send({
|
|
147
|
+
type: "error",
|
|
148
|
+
exitCode: error instanceof UnknownRuleTypeError ? 4 : 1,
|
|
149
|
+
message: error instanceof Error ? error.message : String(error),
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
response.end();
|
|
153
|
+
})();
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const port = options.port ?? 8787;
|
|
158
|
+
server.listen(port, "127.0.0.1", () => {
|
|
159
|
+
const address = server.address();
|
|
160
|
+
const actual = typeof address === "object" && address ? address.port : port;
|
|
161
|
+
console.error(`dspack-gen serve: http://127.0.0.1:${actual} (contract: ${contract.name})`);
|
|
162
|
+
});
|
|
163
|
+
return server;
|
|
164
|
+
}
|