@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,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
+ }