@supernovae-st/nika 0.120.2 → 0.120.3

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/dist/index.cjs CHANGED
@@ -254,29 +254,41 @@ function captureEngine(bin, args, options) {
254
254
  shell: false,
255
255
  stdio: ["ignore", "pipe", "pipe"]
256
256
  });
257
- let stdout = "";
258
- let stderr = "";
257
+ const stdout = [];
258
+ const stderr = [];
259
+ const bytes = { stdout: 0, stderr: 0 };
259
260
  let overflow = false;
260
261
  let spawnError;
262
+ let closed = false;
263
+ let killTimer;
264
+ const stop = () => {
265
+ child.kill("SIGTERM");
266
+ if (options.killGraceMs === void 0) return;
267
+ killTimer ??= setTimeout(() => {
268
+ if (!closed) child.kill("SIGKILL");
269
+ }, options.killGraceMs);
270
+ killTimer.unref();
271
+ };
261
272
  const append = (stream, chunk) => {
262
273
  if (overflow) return;
263
- if (stream === "stdout") stdout += chunk;
264
- else stderr += chunk;
265
- if (Buffer.byteLength(stdout) > options.bufferBytes || Buffer.byteLength(stderr) > options.bufferBytes) {
274
+ bytes[stream] += chunk.byteLength;
275
+ if (bytes[stream] > options.bufferBytes) {
266
276
  overflow = true;
267
- child.kill("SIGTERM");
277
+ stop();
278
+ return;
268
279
  }
280
+ (stream === "stdout" ? stdout : stderr).push(chunk);
269
281
  };
270
- child.stdout.setEncoding("utf8");
271
- child.stderr.setEncoding("utf8");
272
282
  child.stdout.on("data", (chunk) => append("stdout", chunk));
273
283
  child.stderr.on("data", (chunk) => append("stderr", chunk));
274
- const abort = () => child.kill("SIGTERM");
284
+ const abort = () => stop();
275
285
  options.signal?.addEventListener("abort", abort, { once: true });
276
286
  child.once("error", (cause) => {
277
287
  spawnError = cause;
278
288
  });
279
- child.once("close", (code) => {
289
+ child.once("close", (code, exitSignal) => {
290
+ closed = true;
291
+ if (killTimer) clearTimeout(killTimer);
280
292
  options.signal?.removeEventListener("abort", abort);
281
293
  if (options.signal?.aborted) {
282
294
  reject(new NikaTransportError(options.transport, `${options.label} aborted by caller`));
@@ -292,7 +304,19 @@ function captureEngine(bin, args, options) {
292
304
  `${options.label} exceeded ${options.bufferBytes} bytes`
293
305
  ));
294
306
  } else {
295
- resolve({ exitCode: code ?? 3, stdout, stderr });
307
+ let decoded;
308
+ try {
309
+ decoded = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(Buffer.concat(stdout, bytes.stdout));
310
+ } catch {
311
+ reject(new NikaProtocolError(options.transport, `${options.label} stdout was not valid UTF-8`));
312
+ return;
313
+ }
314
+ resolve({
315
+ exitCode: code ?? 3,
316
+ stdout: decoded,
317
+ stderr: Buffer.concat(stderr, bytes.stderr).toString("utf8"),
318
+ exitSignal
319
+ });
296
320
  }
297
321
  });
298
322
  });
@@ -524,9 +548,9 @@ function incompatible(transport, message) {
524
548
 
525
549
  // src/lib/binary/verify.ts
526
550
  var IDENTITY_BUFFER_BYTES = 16 * 1024;
527
- async function verifyNikaEngine(engine) {
551
+ async function verifyNikaEngine(engine, options = {}) {
528
552
  const expectedVersion = engine.packageRoot ? await verifyManagedPayload(engine) : void 0;
529
- const identity = await probeIdentity(engine.bin);
553
+ const identity = await probeIdentity(engine.bin, options);
530
554
  if (expectedVersion !== void 0 && identity.engineVersion !== expectedVersion) {
531
555
  throw incompatible2(
532
556
  `Packaged engine version ${identity.engineVersion} does not match payload version ${expectedVersion}`
@@ -565,13 +589,15 @@ async function verifyManagedPayload(engine) {
565
589
  }
566
590
  return manifest.version;
567
591
  }
568
- async function probeIdentity(bin) {
592
+ async function probeIdentity(bin, options) {
569
593
  const notAnEngine = `is ${bin} a nika engine? (run "${bin} --sdk-identity" by hand)`;
570
594
  const captured = await captureEngine(bin, ["--sdk-identity"], {
595
+ ...options,
571
596
  bufferBytes: IDENTITY_BUFFER_BYTES,
572
597
  transport: "native-process",
573
598
  label: "Engine identity probe"
574
599
  }).catch((cause) => {
600
+ if (options.signal?.aborted) throw cause;
575
601
  throw incompatible2(
576
602
  `Engine identity probe of ${bin} failed`,
577
603
  cause
@@ -651,25 +677,26 @@ var NATIVE_ARRAY = Function.prototype.toString.call(Array);
651
677
  var LONE_SURROGATE = /[\ud800-\udbff](?![\udc00-\udfff])|(?<![\ud800-\udbff])[\udc00-\udfff]/;
652
678
  var IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
653
679
  var PATH_LIMIT = 240;
654
- function encodeLiteralInputs(inputs) {
680
+ function encodeLiteralInputs(inputs, label = "run({ inputs })") {
655
681
  if (containerKind(inputs) !== "object") {
682
+ const subject = label === "run({ inputs })" ? "inputs must be a plain object mapping declared workflow input names" : "answers must be a plain object mapping stable question keys";
656
683
  throw new NikaConfigurationError(
657
- `run({ inputs }): inputs must be a plain object mapping declared workflow input names to strict JSON values; received ${describe(inputs)}`
684
+ `${label}: ${subject} to strict JSON values; received ${describe(inputs)}`
658
685
  );
659
686
  }
660
687
  const parts = [];
661
688
  let bytes = 0;
662
689
  const write = (chunk) => {
663
690
  bytes += Buffer.byteLength(chunk);
664
- if (bytes > LITERAL_INPUTS_MAX_BYTES) throw tooLarge();
691
+ if (bytes > LITERAL_INPUTS_MAX_BYTES) throw tooLarge(label);
665
692
  parts.push(chunk);
666
693
  };
667
694
  const stack = [];
668
695
  const open = /* @__PURE__ */ new Set();
669
696
  const refuse = (segment, problem) => {
670
- const path3 = stack.map((frame) => frame.segment).join("") + segment;
671
- const shown = path3.length > PATH_LIMIT ? `${path3.slice(0, PATH_LIMIT / 2)}\u2026${path3.slice(-PATH_LIMIT / 2)}` : path3;
672
- return new NikaConfigurationError(`run({ inputs }): ${shown} ${problem}; ${STRICT_JSON}`);
697
+ const path4 = stack.map((frame) => frame.segment).join("") + segment;
698
+ const shown = path4.length > PATH_LIMIT ? `${path4.slice(0, PATH_LIMIT / 2)}\u2026${path4.slice(-PATH_LIMIT / 2)}` : path4;
699
+ return new NikaConfigurationError(`${label}: ${shown} ${problem}; ${STRICT_JSON}`);
673
700
  };
674
701
  const enter = (value, segment, kind) => {
675
702
  open.add(value);
@@ -722,7 +749,7 @@ function encodeLiteralInputs(inputs) {
722
749
  const value = member.value;
723
750
  switch (typeof value) {
724
751
  case "string":
725
- if (value.length > LITERAL_INPUTS_MAX_BYTES) throw tooLarge();
752
+ if (value.length > LITERAL_INPUTS_MAX_BYTES) throw tooLarge(label);
726
753
  if (LONE_SURROGATE.test(value)) {
727
754
  throw refuse(segment, "contains a lone surrogate, which is not valid Unicode");
728
755
  }
@@ -754,7 +781,17 @@ function encodeLiteralInputs(inputs) {
754
781
  }
755
782
  return { json: parts.join(""), bytes };
756
783
  }
757
- function tooLarge() {
784
+ function tooLarge(label) {
785
+ if (label === "compile({ change })") {
786
+ return new NikaConfigurationError(
787
+ `compile({ change }): the serialized literal exceeds ${LITERAL_INPUTS_MAX_BYTES} bytes (1 MiB)`
788
+ );
789
+ }
790
+ if (label === "compile({ answers })") {
791
+ return new NikaConfigurationError(
792
+ `compile({ answers }): the serialized answers exceed ${LITERAL_INPUTS_MAX_BYTES} bytes (1 MiB); answers ride argv, one KEY=JSON element each, never a wire`
793
+ );
794
+ }
758
795
  return new NikaConfigurationError(
759
796
  `run({ inputs }): the serialized inputs map exceeds ${LITERAL_INPUTS_MAX_BYTES} bytes (1 MiB), the bound both transports share with the native engine`
760
797
  );
@@ -811,6 +848,364 @@ function refusedObject(value) {
811
848
  return typeof name === "string" && IDENTIFIER.test(name) && name.length <= 64 ? `is a ${name} instance, not a plain object or array` : custom;
812
849
  }
813
850
 
851
+ // src/lib/compile.ts
852
+ var import_promises2 = require("fs/promises");
853
+ var import_node_os = require("os");
854
+ var import_node_path3 = __toESM(require("path"), 1);
855
+ var import_node_util2 = require("util");
856
+ var import_node_events = require("events");
857
+ var COMPILE_CAPABILITY = "compile";
858
+ var COMPILE_WIRE_VERSION = 1;
859
+ var COMPILE_RESPONSE_MAX_BYTES = 8 * 1024 * 1024;
860
+ var COMPILE_KILL_GRACE_MS = 2e3;
861
+ var STATUSES = ["ready", "incomplete", "refused"];
862
+ var signalAborted = Object.getOwnPropertyDescriptor(AbortSignal.prototype, "aborted").get;
863
+ var removeSignalListener = EventTarget.prototype.removeEventListener;
864
+ var signalInterface = /* @__PURE__ */ new Set([
865
+ "aborted",
866
+ "reason",
867
+ "throwIfAborted",
868
+ "addEventListener",
869
+ "removeEventListener",
870
+ "dispatchEvent"
871
+ ]);
872
+ function normalizeCompileRequest(input) {
873
+ if (typeof input === "string") {
874
+ if (input.length === 0) {
875
+ throw new NikaConfigurationError("compile: an intent string must not be empty");
876
+ }
877
+ return { intent: input };
878
+ }
879
+ const record2 = dataRecord(input, "request");
880
+ for (const key of Reflect.ownKeys(record2)) {
881
+ if (key !== "intent" && key !== "workflow" && key !== "change" && key !== "answers") {
882
+ throw new NikaConfigurationError(
883
+ `compile: unknown request field ${String(key)}; the request is refused rather than silently reinterpreted`
884
+ );
885
+ }
886
+ }
887
+ const answers = record2.answers;
888
+ if (answers !== void 0) encodeLiteralInputs(answers, "compile({ answers })");
889
+ const intent = record2.intent;
890
+ const workflow = record2.workflow;
891
+ const change = record2.change;
892
+ if (intent !== void 0) {
893
+ if (workflow !== void 0 || change !== void 0) {
894
+ throw new NikaConfigurationError(
895
+ "compile: intent (create) never mixes with workflow/change (edit) in one request"
896
+ );
897
+ }
898
+ if (typeof intent !== "string" || intent.length === 0) {
899
+ throw new NikaConfigurationError("compile: intent must be a non-empty string");
900
+ }
901
+ return answers === void 0 ? { intent } : { intent, answers };
902
+ }
903
+ if (workflow === void 0 && change === void 0) {
904
+ throw new NikaConfigurationError(
905
+ "compile: the request carries neither intent nor workflow/change; nothing would compile"
906
+ );
907
+ }
908
+ if (typeof workflow !== "string" || workflow.length === 0) {
909
+ throw new NikaConfigurationError(
910
+ "compile: an edit needs the accepted workflow source as a non-empty string"
911
+ );
912
+ }
913
+ const normalizedChange = normalizeChange(change);
914
+ return answers === void 0 ? { workflow, change: normalizedChange } : { workflow, change: normalizedChange, answers };
915
+ }
916
+ function dataRecord(value, label) {
917
+ if (value === null || typeof value !== "object" || import_node_util2.types.isProxy(value) || Array.isArray(value) || Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) {
918
+ throw new NikaConfigurationError(`compile: ${label} must be ${label === "request" ? "an intent string or " : ""}a plain object with own data fields`);
919
+ }
920
+ const result = /* @__PURE__ */ Object.create(null);
921
+ const descriptors = Object.getOwnPropertyDescriptors(value);
922
+ for (const key of Reflect.ownKeys(descriptors)) {
923
+ const descriptor = descriptors[key];
924
+ if (typeof key !== "string" || !("value" in descriptor) || !descriptor.enumerable) {
925
+ throw new NikaConfigurationError(`compile: ${label} must contain only enumerable string data fields`);
926
+ }
927
+ result[key] = descriptor.value;
928
+ }
929
+ return result;
930
+ }
931
+ function normalizeChange(change) {
932
+ if (typeof change === "string" && change.length > 0) return change;
933
+ if (change === void 0 || change === "") {
934
+ throw new NikaConfigurationError("compile: an edit needs a non-empty change request");
935
+ }
936
+ const encoded = encodeLiteralInputs({ change }, "compile({ change })");
937
+ const object = machineObject(JSON.parse(encoded.json).change);
938
+ const constant = machineObject(object?.set_constant);
939
+ if (!object || Object.keys(object).length !== 1 || !constant || Object.keys(constant).length !== 2 || !("value" in constant) || typeof constant.name !== "string" || !/^[A-Za-z0-9_]+$/.test(constant.name)) {
940
+ throw new NikaConfigurationError(
941
+ "compile: an edit needs a non-empty change string or set_constant with a bare name and JSON value"
942
+ );
943
+ }
944
+ return object;
945
+ }
946
+ function normalizeCompileOptions(options) {
947
+ const record2 = dataRecord(options, "options");
948
+ for (const key of Reflect.ownKeys(record2)) {
949
+ if (key !== "signal" && key !== "timeoutMs") {
950
+ throw new NikaConfigurationError(`compile: unknown option ${String(key)}`);
951
+ }
952
+ }
953
+ const timeoutMs = record2.timeoutMs;
954
+ if (timeoutMs !== void 0 && (typeof timeoutMs !== "number" || !Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 2147483647)) {
955
+ throw new NikaConfigurationError(
956
+ "compile: timeoutMs must be a positive safe integer between 1 and 2147483647 milliseconds"
957
+ );
958
+ }
959
+ const signal = record2.signal;
960
+ if (signal !== void 0) {
961
+ const invalidSignal = () => new NikaConfigurationError("compile: signal must be an AbortSignal");
962
+ if (signal === null || typeof signal !== "object" || import_node_util2.types.isProxy(signal) || Object.getPrototypeOf(signal) !== AbortSignal.prototype) throw invalidSignal();
963
+ const descriptors = Object.getOwnPropertyDescriptors(signal);
964
+ for (const key of Reflect.ownKeys(descriptors)) {
965
+ if (typeof key === "string" && signalInterface.has(key) || !("value" in descriptors[key])) throw invalidSignal();
966
+ }
967
+ try {
968
+ signalAborted.call(signal);
969
+ } catch {
970
+ throw invalidSignal();
971
+ }
972
+ }
973
+ return record2;
974
+ }
975
+ function compileSignal(options) {
976
+ const caller = options.signal;
977
+ let bridge;
978
+ let abort;
979
+ if (caller !== void 0) {
980
+ bridge = new AbortController();
981
+ const controller = bridge;
982
+ abort = () => controller.abort();
983
+ if (signalAborted.call(caller)) abort();
984
+ else (0, import_node_events.addAbortListener)(caller, abort);
985
+ }
986
+ let timeout;
987
+ if (options.timeoutMs !== void 0) timeout = AbortSignal.timeout(options.timeoutMs);
988
+ const signals = [bridge?.signal, timeout].filter((s) => s !== void 0);
989
+ return {
990
+ signal: signals.length === 0 ? void 0 : AbortSignal.any(signals),
991
+ timedOut: () => timeout?.aborted === true,
992
+ // Use the intrinsic even if the caller shadows removeEventListener after
993
+ // starting Compile. Do not retain a listener on a long-lived caller signal.
994
+ dispose: () => {
995
+ if (caller && abort) removeSignalListener.call(caller, "abort", abort);
996
+ }
997
+ };
998
+ }
999
+ async function compileArgv(request) {
1000
+ const args = ["compile", "--json"];
1001
+ for (const answer of compileAnswers(request.answers)) {
1002
+ args.push(`--answer=${answer}`);
1003
+ }
1004
+ if (request.intent !== void 0) {
1005
+ refuseNul(request.intent);
1006
+ args.push("--", request.intent);
1007
+ return { args };
1008
+ }
1009
+ const change = typeof request.change === "string" ? request.change : `Set const.${request.change.set_constant.name} to ${literalJson(request.change.set_constant.value)}`;
1010
+ refuseNul(change);
1011
+ const scratchDir = await (0, import_promises2.mkdtemp)(import_node_path3.default.join((0, import_node_os.tmpdir)(), "nika-sdk-compile-"));
1012
+ const base = import_node_path3.default.join(scratchDir, "base.nika");
1013
+ try {
1014
+ await (0, import_promises2.writeFile)(base, request.workflow, { encoding: "utf8", mode: 384 });
1015
+ } catch (cause) {
1016
+ await (0, import_promises2.rm)(scratchDir, { recursive: true, force: true });
1017
+ throw new NikaTransportError("native-process", "compile could not write its temporary base source", {
1018
+ cause: cause instanceof Error ? cause : void 0
1019
+ });
1020
+ }
1021
+ args.push("--base", base, `--change=${change}`);
1022
+ return { args, scratchDir };
1023
+ }
1024
+ async function removeCompileScratch(invocation) {
1025
+ if (invocation.scratchDir !== void 0) {
1026
+ await (0, import_promises2.rm)(invocation.scratchDir, { recursive: true, force: true });
1027
+ }
1028
+ }
1029
+ function compileAnswers(answers) {
1030
+ if (answers === void 0) return [];
1031
+ encodeLiteralInputs(answers, "compile({ answers })");
1032
+ return Reflect.ownKeys(answers).map((key) => {
1033
+ const name = String(key);
1034
+ if (name.length === 0 || name.includes("=") || name.includes("\0")) {
1035
+ throw new NikaConfigurationError(
1036
+ "compile({ answers }): a question key must be non-empty and cannot carry '='; the engine splits KEY=JSON on its first equals sign"
1037
+ );
1038
+ }
1039
+ return `${name}=${literalJson(answers[name])}`;
1040
+ });
1041
+ }
1042
+ function literalJson(value) {
1043
+ return encodeLiteralInputs({ value }, "compile({ change })").json.slice('{"value":'.length, -1);
1044
+ }
1045
+ function refuseNul(value) {
1046
+ if (value.includes("\0")) throw new NikaConfigurationError("compile: text cannot contain a NUL byte");
1047
+ }
1048
+ function compileBody(request) {
1049
+ compileAnswers(request.answers);
1050
+ const answers = request.answers === void 0 ? "" : `,"answers":${encodeLiteralInputs(request.answers, "compile({ answers })").json}`;
1051
+ if (request.intent !== void 0) {
1052
+ refuseNul(request.intent);
1053
+ return `{"compile_version":1,"mode":"create","intent":${JSON.stringify(request.intent)}${answers}}`;
1054
+ }
1055
+ if (typeof request.change === "string") refuseNul(request.change);
1056
+ const change = typeof request.change === "string" ? `{"text":${JSON.stringify(request.change)}}` : `{"set_constant":{"name":${JSON.stringify(request.change.set_constant.name)},"value":${literalJson(request.change.set_constant.value)}}}`;
1057
+ return `{"compile_version":1,"mode":"edit","source":${JSON.stringify(request.workflow)},"change":${change}${answers}}`;
1058
+ }
1059
+ function compileOutcomeFrom(captured, transport, engine) {
1060
+ const protocol = (message) => new NikaProtocolError(transport, `${message} (compile child at ${engine})`);
1061
+ if (captured.exitSignal !== null) {
1062
+ throw new NikaTransportError(
1063
+ transport,
1064
+ `compile child at ${engine} was ended externally by ${captured.exitSignal}`
1065
+ );
1066
+ }
1067
+ const payload = machineObject(tryParseJson(captured.stdout));
1068
+ if (!payload) {
1069
+ const detail = excerpt(captured.stderr) || excerpt(captured.stdout);
1070
+ throw protocol(
1071
+ `the compile wire object is absent or malformed (exit ${captured.exitCode})` + (detail ? `: ${detail}` : "")
1072
+ );
1073
+ }
1074
+ validateCompileVersion(payload.compile_version, transport, engine, protocol);
1075
+ const hasError = "error" in payload;
1076
+ const hasStatus = "status" in payload;
1077
+ if (hasError && hasStatus) {
1078
+ throw protocol("the payload carries both an outcome and an error");
1079
+ }
1080
+ if (hasError) {
1081
+ const error = machineObject(payload.error);
1082
+ if (!error || typeof error.code !== "string" || typeof error.message !== "string") {
1083
+ throw protocol("the error payload lacks its code/message strings");
1084
+ }
1085
+ if (captured.exitCode !== 2 && captured.exitCode !== 3) {
1086
+ throw protocol(
1087
+ `an engine-stamped error arrived with exit ${captured.exitCode}, not 2 or 3`
1088
+ );
1089
+ }
1090
+ throw new NikaOperationError("compile", transport, error.code, error.message, {
1091
+ status: captured.exitCode,
1092
+ machineCode: error.code
1093
+ });
1094
+ }
1095
+ const status = payload.status;
1096
+ if (typeof status !== "string" || !STATUSES.includes(status)) {
1097
+ throw protocol(`unknown compile status ${JSON.stringify(status ?? null)}`);
1098
+ }
1099
+ if (status === "ready" ? captured.exitCode !== 0 : captured.exitCode !== 2) {
1100
+ throw protocol(
1101
+ `status ${JSON.stringify(status)} contradicts exit ${captured.exitCode}`
1102
+ );
1103
+ }
1104
+ writtenFrom(payload.written, protocol);
1105
+ return compilePayloadFrom(payload, transport, engine);
1106
+ }
1107
+ function compilePayloadFrom(payload, transport, engine) {
1108
+ const protocol = (message) => new NikaProtocolError(transport, `compile at ${engine}: ${message}`);
1109
+ validateCompileVersion(payload.compile_version, transport, engine, protocol);
1110
+ if ("error" in payload) throw protocol("the payload carries an error instead of an outcome");
1111
+ if (transport === "http" && "written" in payload) throw protocol("HTTP outcome claims a written destination");
1112
+ const status = payload.status;
1113
+ if (typeof status !== "string" || !STATUSES.includes(status)) {
1114
+ throw protocol("unknown compile status");
1115
+ }
1116
+ const candidate = payload.candidate;
1117
+ if (candidate !== null && typeof candidate !== "string") {
1118
+ throw protocol("candidate is neither source text nor null");
1119
+ }
1120
+ if (status === "ready" && candidate === null) {
1121
+ throw protocol("a ready outcome carries no candidate");
1122
+ }
1123
+ const outcome = {
1124
+ compile_version: COMPILE_WIRE_VERSION,
1125
+ status,
1126
+ ready: status === "ready",
1127
+ candidate,
1128
+ questions: questionsFrom(payload.questions, protocol),
1129
+ diagnostics: diagnosticsFrom(payload.diagnostics, protocol),
1130
+ requested_boundary: nullableObject(payload.requested_boundary, "requested_boundary", protocol),
1131
+ check_preview: previewFrom(payload.check_preview, protocol),
1132
+ provenance: provenanceFrom(payload.provenance, protocol)
1133
+ };
1134
+ return outcome;
1135
+ }
1136
+ function validateCompileVersion(version, transport, engine, protocol) {
1137
+ if (typeof version !== "number" || !Number.isSafeInteger(version) || version < 1) {
1138
+ throw protocol("compile_version must be a positive safe integer");
1139
+ }
1140
+ if (version !== COMPILE_WIRE_VERSION) {
1141
+ throw new NikaCompatibilityError(
1142
+ COMPILE_CAPABILITY,
1143
+ transport,
1144
+ `Engine at ${engine} speaks compile wire ${version}; expected ${COMPILE_WIRE_VERSION}`
1145
+ );
1146
+ }
1147
+ }
1148
+ function tryParseJson(text) {
1149
+ const trimmed = text.trim();
1150
+ if (!trimmed) return void 0;
1151
+ try {
1152
+ return JSON.parse(trimmed);
1153
+ } catch {
1154
+ return void 0;
1155
+ }
1156
+ }
1157
+ function excerpt(text) {
1158
+ const flat = text.trim().replace(/\s+/g, " ");
1159
+ return flat.length <= 240 ? flat : `${flat.slice(0, 240)}\u2026`;
1160
+ }
1161
+ function nullableObject(value, field, protocol) {
1162
+ if (value === null) return null;
1163
+ const object = machineObject(value);
1164
+ if (!object) throw protocol(`${field} is neither an object nor null`);
1165
+ return object;
1166
+ }
1167
+ function questionsFrom(value, protocol) {
1168
+ if (!Array.isArray(value)) throw protocol("questions is not an array");
1169
+ return value.map((entry) => {
1170
+ const question = machineObject(entry);
1171
+ if (!question || typeof question.key !== "string" || typeof question.label !== "string" || question.type !== "text" && question.type !== "literal" || typeof question.why !== "string" || typeof question.mandatory !== "boolean") {
1172
+ throw protocol("a question lacks its key/label/type/why/mandatory shape");
1173
+ }
1174
+ return question;
1175
+ });
1176
+ }
1177
+ function diagnosticsFrom(value, protocol) {
1178
+ if (!Array.isArray(value)) throw protocol("diagnostics is not an array");
1179
+ return value.map((entry) => {
1180
+ const diagnostic = machineObject(entry);
1181
+ if (!diagnostic || typeof diagnostic.kind !== "string" || !["applied", "missed", "unknown", "requiresHuman", "refused"].includes(diagnostic.kind) || typeof diagnostic.target !== "string" || typeof diagnostic.message !== "string") {
1182
+ throw protocol("a diagnostic lacks its kind/target/message shape");
1183
+ }
1184
+ return diagnostic;
1185
+ });
1186
+ }
1187
+ function previewFrom(value, protocol) {
1188
+ if (value === null) return null;
1189
+ const preview = machineObject(value);
1190
+ if (!preview || preview.scope !== "sourceOnly" || !machineObject(preview.report)) {
1191
+ throw protocol("check_preview lacks its scope/report shape");
1192
+ }
1193
+ return { ...preview, scope: preview.scope, report: preview.report };
1194
+ }
1195
+ function provenanceFrom(value, protocol) {
1196
+ const provenance = machineObject(value);
1197
+ if (!provenance || typeof provenance.compiler_version !== "string" || typeof provenance.spec_pin !== "string" || provenance.skeleton !== null && typeof provenance.skeleton !== "string" || provenance.cognition !== "deterministicOnly") {
1198
+ throw protocol("provenance lacks its compiler_version/spec_pin/skeleton/cognition shape");
1199
+ }
1200
+ return provenance;
1201
+ }
1202
+ function writtenFrom(value, protocol) {
1203
+ if (value !== null) {
1204
+ throw protocol("the outcome claims a written destination the SDK never asked for");
1205
+ }
1206
+ return null;
1207
+ }
1208
+
814
1209
  // src/lib/sse/parser.ts
815
1210
  var SseParseError = class extends Error {
816
1211
  constructor(message, options) {
@@ -972,8 +1367,8 @@ async function* decodeSse(stream, limits, signal) {
972
1367
  var WORKFLOW_SUFFIX = ".nika";
973
1368
  var LEGACY_WORKFLOW_SUFFIXES = [".nika.yaml", ".nika.yml"];
974
1369
  var CONTAINED_CHAR = /[A-Za-z0-9/._-]/;
975
- function workflowBasename(path3) {
976
- const parts = path3.split(/[\\/]/);
1370
+ function workflowBasename(path4) {
1371
+ const parts = path4.split(/[\\/]/);
977
1372
  return parts[parts.length - 1] ?? "";
978
1373
  }
979
1374
  function hasControlChar(value) {
@@ -985,14 +1380,14 @@ function hasControlChar(value) {
985
1380
  }
986
1381
  return false;
987
1382
  }
988
- function isLegacyWorkflowPath(path3) {
989
- if (typeof path3 !== "string" || path3.length === 0) {
1383
+ function isLegacyWorkflowPath(path4) {
1384
+ if (typeof path4 !== "string" || path4.length === 0) {
990
1385
  return false;
991
1386
  }
992
- if (path3.endsWith("/") || path3.endsWith("\\")) {
1387
+ if (path4.endsWith("/") || path4.endsWith("\\")) {
993
1388
  return false;
994
1389
  }
995
- const base = workflowBasename(path3);
1390
+ const base = workflowBasename(path4);
996
1391
  return LEGACY_WORKFLOW_SUFFIXES.some((suffix) => base.endsWith(suffix));
997
1392
  }
998
1393
  function isCanonicalWorkflowFilename(basename) {
@@ -1013,17 +1408,17 @@ function isCanonicalWorkflowFilename(basename) {
1013
1408
  }
1014
1409
  return basename.length > WORKFLOW_SUFFIX.length;
1015
1410
  }
1016
- function isCanonicalWorkflowPath(path3) {
1017
- if (typeof path3 !== "string" || path3.length === 0) {
1411
+ function isCanonicalWorkflowPath(path4) {
1412
+ if (typeof path4 !== "string" || path4.length === 0) {
1018
1413
  return false;
1019
1414
  }
1020
- if (hasControlChar(path3)) {
1415
+ if (hasControlChar(path4)) {
1021
1416
  return false;
1022
1417
  }
1023
- if (path3.endsWith("/") || path3.endsWith("\\")) {
1418
+ if (path4.endsWith("/") || path4.endsWith("\\")) {
1024
1419
  return false;
1025
1420
  }
1026
- return isCanonicalWorkflowFilename(workflowBasename(path3));
1421
+ return isCanonicalWorkflowFilename(workflowBasename(path4));
1027
1422
  }
1028
1423
  function containedRelativeShape(value) {
1029
1424
  if (value.includes("\\") || value.startsWith("/")) {
@@ -1114,6 +1509,69 @@ var HttpTransport = class {
1114
1509
  }
1115
1510
  return captured.report;
1116
1511
  }
1512
+ /**
1513
+ * Stateless authoring through the authenticated Serve compile door.
1514
+ * A remote connection never falls back to a local compile — the candidate
1515
+ * must come from the server the caller connected to, or not exist.
1516
+ */
1517
+ async compile(request, options) {
1518
+ const body = compileBody(request);
1519
+ const timeoutMs = options.timeoutMs ?? this.options.requestTimeout;
1520
+ const composed = compileSignal({ ...options, timeoutMs });
1521
+ const signal = composed.signal;
1522
+ try {
1523
+ if (signal.aborted) throw new NikaTransportError(this.kind, "compile aborted");
1524
+ const identity = this.remoteIdentity ?? await this.probeServerIdentity(signal);
1525
+ if (!identity.supportedCapabilities.includes(COMPILE_CAPABILITY)) {
1526
+ throw this.gap(
1527
+ COMPILE_CAPABILITY,
1528
+ "The connected engine does not advertise compile; the SDK never compiles locally as a substitute"
1529
+ );
1530
+ }
1531
+ const path4 = "/v1/compile";
1532
+ const response = await this.fetchResponse(path4, {
1533
+ method: "POST",
1534
+ headers: { "Content-Type": "application/json", Accept: "application/json" },
1535
+ body,
1536
+ signal
1537
+ }, false, true, false);
1538
+ if (response.headers.get("Content-Type")?.split(";", 1)[0]?.trim().toLowerCase() !== "application/json") {
1539
+ await discardResponse(response);
1540
+ throw new NikaProtocolError(this.kind, "HTTP compile returned an invalid content-type");
1541
+ }
1542
+ const object = await this.readObservationObject(
1543
+ response,
1544
+ path4,
1545
+ signal,
1546
+ Math.min(this.options.machineBufferBytes, COMPILE_RESPONSE_MAX_BYTES),
1547
+ false
1548
+ );
1549
+ if (signal.aborted) throw new NikaTransportError(this.kind, "compile aborted");
1550
+ if (response.status !== 200) {
1551
+ const error = machineObject(object.error);
1552
+ if (response.ok || !error || typeof error.code !== "string" || !/^[A-Za-z][A-Za-z0-9_.:-]{0,63}$/.test(error.code) || error.code.includes(this.options.token) || typeof error.message !== "string" || Object.keys(object).some((key) => key !== "error")) {
1553
+ throw new NikaProtocolError(this.kind, "HTTP compile returned a malformed refusal or non-contract status");
1554
+ }
1555
+ throw this.refused(path4, {
1556
+ operation: "compile",
1557
+ status: response.status,
1558
+ refusal: { code: error.code, message: this.redact(error.message) }
1559
+ });
1560
+ }
1561
+ return compilePayloadFrom(object, this.kind, this.options.url);
1562
+ } catch (cause) {
1563
+ if (signal.aborted) {
1564
+ throw new NikaTransportError(
1565
+ this.kind,
1566
+ composed.timedOut() ? `compile timed out after ${timeoutMs} ms` : "compile aborted by caller",
1567
+ { cause: cause instanceof Error ? cause : void 0 }
1568
+ );
1569
+ }
1570
+ throw cause;
1571
+ } finally {
1572
+ composed.dispose();
1573
+ }
1574
+ }
1117
1575
  async startRun(workflow, options) {
1118
1576
  const inputs = literalInputs(options);
1119
1577
  if (options.vars && Object.keys(options.vars).length > 0 || options.model !== void 0 || options.maxCostUsd !== void 0) {
@@ -1233,14 +1691,14 @@ var HttpTransport = class {
1233
1691
  async schedule(workflow, options) {
1234
1692
  refuseLegacyContainedName(workflow);
1235
1693
  await this.ensureScheduleCapability();
1236
- const path3 = `/v1/schedules/${encodeURIComponent(options.id)}`;
1694
+ const path4 = `/v1/schedules/${encodeURIComponent(options.id)}`;
1237
1695
  const headers = new Headers({ "Content-Type": "application/json" });
1238
1696
  if (options.revision === void 0) {
1239
1697
  headers.set("If-None-Match", "*");
1240
1698
  } else {
1241
1699
  headers.set("If-Match", `"${options.revision}"`);
1242
1700
  }
1243
- const object = await this.operationJson("schedule", path3, {
1701
+ const object = await this.operationJson("schedule", path4, {
1244
1702
  method: "PUT",
1245
1703
  headers,
1246
1704
  body: JSON.stringify(scheduleBody(workflow, options))
@@ -1252,8 +1710,8 @@ var HttpTransport = class {
1252
1710
  }
1253
1711
  async scheduleStatus(id) {
1254
1712
  await this.ensureScheduleCapability();
1255
- const path3 = `/v1/schedules/${encodeURIComponent(id)}`;
1256
- const object = await this.operationJson("scheduleStatus", path3, {
1713
+ const path4 = `/v1/schedules/${encodeURIComponent(id)}`;
1714
+ const object = await this.operationJson("scheduleStatus", path4, {
1257
1715
  method: "GET",
1258
1716
  headers: { Accept: "application/json" }
1259
1717
  });
@@ -1586,16 +2044,16 @@ var HttpTransport = class {
1586
2044
  }
1587
2045
  return event;
1588
2046
  }
1589
- async inspectDurableJob(path3, expectedId, signal) {
2047
+ async inspectDurableJob(path4, expectedId, signal) {
1590
2048
  let response;
1591
2049
  try {
1592
- response = await this.observationRequest(path3, { headers: { Accept: "application/json" }, signal });
2050
+ response = await this.observationRequest(path4, { headers: { Accept: "application/json" }, signal });
1593
2051
  } catch {
1594
2052
  return { retry: true };
1595
2053
  }
1596
2054
  if (response.status === 404) {
1597
2055
  await discardResponse(response);
1598
- throw new NikaTransportError(this.kind, `HTTP 404 for ${path3}; job was durably absent`);
2056
+ throw new NikaTransportError(this.kind, `HTTP 404 for ${path4}; job was durably absent`);
1599
2057
  }
1600
2058
  if (isRetryableStatus(response.status)) {
1601
2059
  const retryAfterMilliseconds = retryAfter(response.headers.get("Retry-After"));
@@ -1605,9 +2063,9 @@ var HttpTransport = class {
1605
2063
  ...retryAfterMilliseconds !== void 0 ? { retryAfterMilliseconds } : {}
1606
2064
  };
1607
2065
  }
1608
- this.assertObservationResponse(response, path3, "application/json");
2066
+ this.assertObservationResponse(response, path4, "application/json");
1609
2067
  try {
1610
- const object = await this.readObservationObject(response, path3);
2068
+ const object = await this.readObservationObject(response, path4);
1611
2069
  return durableJob(object, expectedId, this.kind);
1612
2070
  } catch (cause) {
1613
2071
  if (cause instanceof NikaTransportError && !(cause instanceof NikaProtocolError)) {
@@ -1616,31 +2074,31 @@ var HttpTransport = class {
1616
2074
  throw cause;
1617
2075
  }
1618
2076
  }
1619
- assertObservationResponse(response, path3, contentType) {
2077
+ assertObservationResponse(response, path4, contentType) {
1620
2078
  if (response.status === 204) {
1621
2079
  void discardResponse(response);
1622
- throw new NikaProtocolError(this.kind, `HTTP 204 permanently stopped observation for ${path3}`);
2080
+ throw new NikaProtocolError(this.kind, `HTTP 204 permanently stopped observation for ${path4}`);
1623
2081
  }
1624
2082
  if (response.status === 401 || response.status === 403) {
1625
2083
  void discardResponse(response);
1626
- throw new NikaTransportError(this.kind, `HTTP ${response.status} authorization failure for ${path3}`);
2084
+ throw new NikaTransportError(this.kind, `HTTP ${response.status} authorization failure for ${path4}`);
1627
2085
  }
1628
2086
  if (response.status !== 200) {
1629
2087
  void discardResponse(response);
1630
- throw new NikaTransportError(this.kind, `HTTP ${response.status} permanently failed ${path3}`);
2088
+ throw new NikaTransportError(this.kind, `HTTP ${response.status} permanently failed ${path4}`);
1631
2089
  }
1632
2090
  const actual = response.headers.get("Content-Type")?.split(";", 1)[0]?.trim().toLowerCase();
1633
2091
  if (actual !== contentType) {
1634
2092
  void discardResponse(response);
1635
2093
  throw new NikaProtocolError(
1636
2094
  this.kind,
1637
- `HTTP ${path3} returned an invalid content-type`
2095
+ `HTTP ${path4} returned an invalid content-type`
1638
2096
  );
1639
2097
  }
1640
2098
  }
1641
- async readObservationObject(response, path3, signal) {
2099
+ async readObservationObject(response, path4, signal, maxBytes = this.options.machineBufferBytes, useRequestTimeout = true) {
1642
2100
  if (!response.body) {
1643
- throw new NikaProtocolError(this.kind, `HTTP ${path3} omitted its JSON body`);
2101
+ throw new NikaProtocolError(this.kind, `HTTP ${path4} omitted its JSON body`);
1644
2102
  }
1645
2103
  const reader = response.body.getReader();
1646
2104
  const chunks = [];
@@ -1650,38 +2108,38 @@ var HttpTransport = class {
1650
2108
  rejectBoundary = reject;
1651
2109
  });
1652
2110
  const abort = () => {
1653
- rejectBoundary(new NikaTransportError(this.kind, `HTTP ${path3} response body was aborted`));
2111
+ rejectBoundary(new NikaTransportError(this.kind, `HTTP ${path4} response body was aborted`));
1654
2112
  void reader.cancel().catch(() => {
1655
2113
  });
1656
2114
  };
1657
2115
  signal?.addEventListener("abort", abort, { once: true });
1658
- const timer = setTimeout(() => {
2116
+ const timer = useRequestTimeout ? setTimeout(() => {
1659
2117
  rejectBoundary(new NikaTransportError(
1660
2118
  this.kind,
1661
2119
  `HTTP response body timed out after ${this.options.requestTimeout}ms`
1662
2120
  ));
1663
2121
  void reader.cancel().catch(() => {
1664
2122
  });
1665
- }, this.options.requestTimeout);
2123
+ }, this.options.requestTimeout) : void 0;
1666
2124
  try {
1667
2125
  if (signal?.aborted) abort();
1668
2126
  while (true) {
1669
2127
  const { done, value: value2 } = await Promise.race([reader.read(), boundary]);
1670
2128
  if (done) break;
1671
2129
  bytes += value2.byteLength;
1672
- if (bytes > this.options.machineBufferBytes) {
2130
+ if (bytes > maxBytes) {
1673
2131
  throw new NikaProtocolError(
1674
2132
  this.kind,
1675
- `HTTP ${path3} JSON exceeded ${this.options.machineBufferBytes} bytes`
2133
+ `HTTP ${path4} JSON exceeded ${maxBytes} bytes`
1676
2134
  );
1677
2135
  }
1678
2136
  chunks.push(value2);
1679
2137
  }
1680
2138
  } catch (cause) {
1681
2139
  if (cause instanceof NikaProtocolError) throw cause;
1682
- throw new NikaTransportError(this.kind, `HTTP ${path3} response body reset`);
2140
+ throw new NikaTransportError(this.kind, `HTTP ${path4} response body reset`);
1683
2141
  } finally {
1684
- clearTimeout(timer);
2142
+ if (timer) clearTimeout(timer);
1685
2143
  signal?.removeEventListener("abort", abort);
1686
2144
  await reader.cancel().catch(() => {
1687
2145
  });
@@ -1697,7 +2155,7 @@ var HttpTransport = class {
1697
2155
  try {
1698
2156
  text = new TextDecoder("utf-8", { fatal: true }).decode(body);
1699
2157
  } catch (cause) {
1700
- throw new NikaProtocolError(this.kind, `HTTP ${path3} JSON was not valid UTF-8`, {
2158
+ throw new NikaProtocolError(this.kind, `HTTP ${path4} JSON was not valid UTF-8`, {
1701
2159
  cause: cause instanceof Error ? cause : void 0
1702
2160
  });
1703
2161
  }
@@ -1705,15 +2163,15 @@ var HttpTransport = class {
1705
2163
  try {
1706
2164
  value = JSON.parse(text);
1707
2165
  } catch (cause) {
1708
- throw new NikaProtocolError(this.kind, `HTTP ${path3} did not return valid JSON`, {
2166
+ throw new NikaProtocolError(this.kind, `HTTP ${path4} did not return valid JSON`, {
1709
2167
  cause: cause instanceof Error ? cause : void 0
1710
2168
  });
1711
2169
  }
1712
2170
  const object = machineObject(value);
1713
- if (!object) throw new NikaProtocolError(this.kind, `HTTP ${path3} did not return an object`);
2171
+ if (!object) throw new NikaProtocolError(this.kind, `HTTP ${path4} did not return an object`);
1714
2172
  return object;
1715
2173
  }
1716
- async observationRequest(path3, init, withTimeout = true) {
2174
+ async observationRequest(path4, init, withTimeout = true) {
1717
2175
  const controller = withTimeout ? new AbortController() : void 0;
1718
2176
  const timer = controller ? setTimeout(() => controller.abort(), this.options.requestTimeout) : void 0;
1719
2177
  const callerSignal = init.signal;
@@ -1722,7 +2180,7 @@ var HttpTransport = class {
1722
2180
  const headers = new Headers(init.headers);
1723
2181
  headers.set("Authorization", `Bearer ${this.options.token}`);
1724
2182
  try {
1725
- return await this.options.fetch(`${this.options.url}${path3}`, {
2183
+ return await this.options.fetch(`${this.options.url}${path4}`, {
1726
2184
  ...init,
1727
2185
  headers,
1728
2186
  signal: controller?.signal ?? callerSignal
@@ -1781,8 +2239,18 @@ var HttpTransport = class {
1781
2239
  this.serverIdentity ??= this.probeServerIdentity();
1782
2240
  return this.serverIdentity;
1783
2241
  }
1784
- async probeServerIdentity() {
1785
- const health = await this.json("/health", { method: "GET" }, false);
2242
+ async probeServerIdentity(signal) {
2243
+ const outcome = await this.jsonOutcome(
2244
+ "/health",
2245
+ { method: "GET", signal },
2246
+ false,
2247
+ [200],
2248
+ void 0,
2249
+ false,
2250
+ signal === void 0
2251
+ );
2252
+ if ("refusal" in outcome) throw this.refused("/health", outcome);
2253
+ const health = outcome.object;
1786
2254
  if (health.status !== "ok" || health.service !== "nika-serve") {
1787
2255
  throw new NikaCompatibilityError(
1788
2256
  "engineIdentity",
@@ -1920,9 +2388,9 @@ var HttpTransport = class {
1920
2388
  delete report.execution_snapshot;
1921
2389
  return report;
1922
2390
  }
1923
- async json(path3, init, authenticated = true, acceptedStatuses = [200], operation) {
2391
+ async json(path4, init, authenticated = true, acceptedStatuses = [200], operation) {
1924
2392
  const { object } = await this.jsonWithStatus(
1925
- path3,
2393
+ path4,
1926
2394
  init,
1927
2395
  authenticated,
1928
2396
  acceptedStatuses,
@@ -1935,48 +2403,54 @@ var HttpTransport = class {
1935
2403
  * routes whose accepted statuses mean different things (cancel: 200 is a
1936
2404
  * settled job, 202 an accepted request).
1937
2405
  */
1938
- async jsonWithStatus(path3, init, authenticated, acceptedStatuses, operation) {
1939
- const outcome = await this.jsonOutcome(path3, init, authenticated, acceptedStatuses, operation);
1940
- if ("refusal" in outcome) throw this.refused(path3, outcome);
2406
+ async jsonWithStatus(path4, init, authenticated, acceptedStatuses, operation) {
2407
+ const outcome = await this.jsonOutcome(path4, init, authenticated, acceptedStatuses, operation);
2408
+ if ("refusal" in outcome) throw this.refused(path4, outcome);
1941
2409
  return outcome;
1942
2410
  }
1943
- async jsonOutcome(path3, init, authenticated, acceptedStatuses, operation, strictRefusal = false) {
1944
- const response = await this.fetchResponse(path3, init, true, authenticated, false);
2411
+ async jsonOutcome(path4, init, authenticated, acceptedStatuses, operation, strictRefusal = false, useRequestTimeout = true) {
2412
+ const response = await this.fetchResponse(path4, init, useRequestTimeout, authenticated, false);
1945
2413
  if (!acceptedStatuses.includes(response.status)) {
1946
2414
  if (response.ok) {
1947
2415
  await discardResponse(response);
1948
2416
  throw new NikaProtocolError(
1949
2417
  this.kind,
1950
- `HTTP ${path3} returned non-contract status ${response.status}`
2418
+ `HTTP ${path4} returned non-contract status ${response.status}`
1951
2419
  );
1952
2420
  }
1953
- const refusal = operation === void 0 ? void 0 : await this.readRefusal(response, path3, strictRefusal);
2421
+ const refusal = operation === void 0 ? void 0 : await this.readRefusal(response, path4, strictRefusal);
1954
2422
  if (operation !== void 0 && refusal) {
1955
2423
  return { operation, status: response.status, refusal };
1956
2424
  }
1957
2425
  await discardResponse(response);
1958
2426
  throw new NikaTransportError(
1959
2427
  this.kind,
1960
- `HTTP ${response.status} for ${path3}: [REDACTED]`
2428
+ `HTTP ${response.status} for ${path4}: [REDACTED]`
1961
2429
  );
1962
2430
  }
1963
2431
  const contentType = response.headers.get("Content-Type")?.split(";", 1)[0]?.trim().toLowerCase();
1964
2432
  if (contentType !== "application/json") {
1965
2433
  await discardResponse(response);
1966
- throw new NikaProtocolError(this.kind, `HTTP ${path3} returned an invalid content-type`);
2434
+ throw new NikaProtocolError(this.kind, `HTTP ${path4} returned an invalid content-type`);
1967
2435
  }
1968
2436
  return {
1969
- object: await this.readObservationObject(response, path3, init.signal ?? void 0),
2437
+ object: await this.readObservationObject(
2438
+ response,
2439
+ path4,
2440
+ init.signal ?? void 0,
2441
+ this.options.machineBufferBytes,
2442
+ useRequestTimeout
2443
+ ),
1970
2444
  status: response.status
1971
2445
  };
1972
2446
  }
1973
- refused(path3, outcome) {
2447
+ refused(path4, outcome) {
1974
2448
  const { code, message } = outcome.refusal;
1975
2449
  return new NikaOperationError(
1976
2450
  outcome.operation,
1977
2451
  this.kind,
1978
2452
  code,
1979
- `HTTP ${outcome.status} for ${path3}: ${code}` + (message ? ` (${message})` : ""),
2453
+ `HTTP ${outcome.status} for ${path4}: ${code}` + (message ? ` (${message})` : ""),
1980
2454
  { status: outcome.status, machineCode: code }
1981
2455
  );
1982
2456
  }
@@ -1986,7 +2460,7 @@ var HttpTransport = class {
1986
2460
  * plain identifier, a code that carries the bearer token) is discarded and
1987
2461
  * the caller falls back to the redacted transport error.
1988
2462
  */
1989
- async readRefusal(response, path3, strict = false) {
2463
+ async readRefusal(response, path4, strict = false) {
1990
2464
  const contentType = response.headers.get("Content-Type")?.split(";", 1)[0]?.trim().toLowerCase();
1991
2465
  if (contentType !== "application/json") {
1992
2466
  await discardResponse(response);
@@ -1994,7 +2468,7 @@ var HttpTransport = class {
1994
2468
  }
1995
2469
  let object;
1996
2470
  try {
1997
- object = await this.readObservationObject(response, path3);
2471
+ object = await this.readObservationObject(response, path4);
1998
2472
  } catch {
1999
2473
  return void 0;
2000
2474
  }
@@ -2012,14 +2486,14 @@ var HttpTransport = class {
2012
2486
  const clean = text.replace(/[\u0000-\u001f\u007f]+/g, " ").split(this.options.token).join("[REDACTED]");
2013
2487
  return clean.length > 240 ? `${clean.slice(0, 240)}...` : clean;
2014
2488
  }
2015
- async operationJson(operation, path3, init) {
2016
- const response = await this.fetchResponse(path3, init, true, true, false);
2489
+ async operationJson(operation, path4, init) {
2490
+ const response = await this.fetchResponse(path4, init, true, true, false);
2017
2491
  const contentType = response.headers.get("Content-Type")?.split(";", 1)[0]?.trim().toLowerCase();
2018
2492
  if (contentType !== "application/json") {
2019
2493
  await discardResponse(response);
2020
- throw new NikaProtocolError(this.kind, `HTTP ${path3} returned an invalid content-type`);
2494
+ throw new NikaProtocolError(this.kind, `HTTP ${path4} returned an invalid content-type`);
2021
2495
  }
2022
- const object = await this.readObservationObject(response, path3);
2496
+ const object = await this.readObservationObject(response, path4);
2023
2497
  if (!response.ok) throw this.operationFailure(operation, response.status, object);
2024
2498
  return object;
2025
2499
  }
@@ -2064,16 +2538,17 @@ var HttpTransport = class {
2064
2538
  { status, machineCode: machine.code }
2065
2539
  );
2066
2540
  }
2067
- async fetchResponse(path3, init, withTimeout, authenticated = true, requireOk = true) {
2541
+ async fetchResponse(path4, init, withTimeout, authenticated = true, requireOk = true) {
2068
2542
  const controller = withTimeout ? new AbortController() : void 0;
2069
2543
  const timer = controller ? setTimeout(() => controller.abort(), this.options.requestTimeout) : void 0;
2070
2544
  const callerSignal = init.signal;
2071
2545
  const abort = () => controller?.abort(callerSignal?.reason);
2072
2546
  callerSignal?.addEventListener("abort", abort, { once: true });
2547
+ if (callerSignal?.aborted) abort();
2073
2548
  const headers = new Headers(init.headers);
2074
2549
  if (authenticated) headers.set("Authorization", `Bearer ${this.options.token}`);
2075
2550
  try {
2076
- const response = await this.options.fetch(`${this.options.url}${path3}`, {
2551
+ const response = await this.options.fetch(`${this.options.url}${path4}`, {
2077
2552
  ...init,
2078
2553
  headers,
2079
2554
  signal: controller?.signal ?? callerSignal
@@ -2082,7 +2557,7 @@ var HttpTransport = class {
2082
2557
  await discardResponse(response);
2083
2558
  throw new NikaTransportError(
2084
2559
  this.kind,
2085
- `HTTP ${response.status} for ${path3}: [REDACTED]`
2560
+ `HTTP ${response.status} for ${path4}: [REDACTED]`
2086
2561
  );
2087
2562
  }
2088
2563
  return response;
@@ -2528,15 +3003,65 @@ var NativeProcessTransport = class {
2528
3003
  const captured = await this.capture(args, options.signal);
2529
3004
  const report = reportObject(captured.stdout) ?? reportObject(stripEnginePrefix(captured.stderr));
2530
3005
  if (!report) {
2531
- const excerpt = diagnosticExcerpt(captured.stderr);
3006
+ const excerpt2 = diagnosticExcerpt(captured.stderr);
2532
3007
  throw new NikaCompatibilityError(
2533
3008
  "check",
2534
3009
  this.kind,
2535
- `This engine did not emit the required JSON check report (exit ${captured.exitCode})` + (excerpt ? `: ${excerpt}` : "")
3010
+ `This engine did not emit the required JSON check report (exit ${captured.exitCode})` + (excerpt2 ? `: ${excerpt2}` : "")
2536
3011
  );
2537
3012
  }
2538
3013
  return { ...report, exitCode: captured.exitCode };
2539
3014
  }
3015
+ /**
3016
+ * Authoring, not execution (issue #128): one bounded `nika compile --json`
3017
+ * invocation, projected verbatim. The engine must advertise the compile
3018
+ * capability — an engine from before the door is refused here, before any
3019
+ * spawn, and there is never a TypeScript fallback. No destination is passed:
3020
+ * the candidate stays source in memory, nothing is written, and no Run
3021
+ * exists; `signal`/`timeoutMs` only stop this one child.
3022
+ */
3023
+ async compile(request, options) {
3024
+ const composed = compileSignal(options);
3025
+ let invocation;
3026
+ try {
3027
+ if (composed.signal?.aborted) {
3028
+ throw new NikaTransportError(this.kind, "compile aborted");
3029
+ }
3030
+ invocation = await compileArgv(request);
3031
+ const identity = composed.signal ? await verifyNikaEngine(this.options.engine, {
3032
+ signal: composed.signal,
3033
+ killGraceMs: COMPILE_KILL_GRACE_MS
3034
+ }) : await this.ensureReady();
3035
+ if (!identity.supportedCapabilities.includes(COMPILE_CAPABILITY)) {
3036
+ throw new NikaCompatibilityError(
3037
+ COMPILE_CAPABILITY,
3038
+ this.kind,
3039
+ `Engine ${identity.engineVersion} at ${this.options.engine.bin} does not advertise ${COMPILE_CAPABILITY} (advertised: ${identity.supportedCapabilities.join(", ") || "nothing"}); compile needs an engine that speaks the \`compile_version: 1\` wire (engine nika#1663). The SDK never compiles in TypeScript and never falls back to another channel`
3040
+ );
3041
+ }
3042
+ const captured = await captureEngine(this.options.engine.bin, invocation.args, {
3043
+ cwd: this.options.cwd,
3044
+ signal: composed.signal,
3045
+ bufferBytes: COMPILE_RESPONSE_MAX_BYTES,
3046
+ transport: this.kind,
3047
+ label: "compile",
3048
+ killGraceMs: COMPILE_KILL_GRACE_MS
3049
+ });
3050
+ return compileOutcomeFrom(captured, this.kind, this.options.engine.bin);
3051
+ } catch (cause) {
3052
+ if (composed.signal?.aborted && cause instanceof NikaTransportError) {
3053
+ throw new NikaTransportError(
3054
+ this.kind,
3055
+ composed.timedOut() ? `compile timed out after ${options.timeoutMs} ms` : "compile aborted by caller",
3056
+ { cause }
3057
+ );
3058
+ }
3059
+ throw cause;
3060
+ } finally {
3061
+ composed.dispose();
3062
+ if (invocation) await removeCompileScratch(invocation);
3063
+ }
3064
+ }
2540
3065
  async startRun(workflow, options) {
2541
3066
  const inputs = literalInputs(options);
2542
3067
  const identity = await this.ensureReady();
@@ -2977,8 +3502,8 @@ function quotedProtocolError(cause, text, kind) {
2977
3502
  );
2978
3503
  }
2979
3504
  function quoted(verdict, text) {
2980
- const excerpt = diagnosticExcerpt(text);
2981
- return excerpt ? `${verdict}: ${excerpt}` : verdict;
3505
+ const excerpt2 = diagnosticExcerpt(text);
3506
+ return excerpt2 ? `${verdict}: ${excerpt2}` : verdict;
2982
3507
  }
2983
3508
  function runFlags(options, literal) {
2984
3509
  const flags = [];
@@ -3373,6 +3898,26 @@ var Nika = class {
3373
3898
  check(workflow, options = {}) {
3374
3899
  return this.transport.check(workflowName(workflow), options);
3375
3900
  }
3901
+ /**
3902
+ * Authoring (issue #128): describe work — or name an accepted workflow plus
3903
+ * a change — and get the engine's checked candidate back, without running
3904
+ * anything. `compile()` never means `run()`: the outcome is data (candidate
3905
+ * source, questions, diagnostics, requested boundary, source-only Check
3906
+ * preview, provenance), `incomplete`/`refused` resolve instead of throwing,
3907
+ * and no workflow effect, approval or Proof exists.
3908
+ *
3909
+ * The candidate is ordinary `.nika` SOURCE. `run()` consumes a path,
3910
+ * so the caller materializes the candidate and `run(path)` re-admits it —
3911
+ * the compile preview is a review, never admission. Over `{ url }` this calls
3912
+ * the authenticated Serve compile door, gated by its advertised capability;
3913
+ * the SDK never compiles locally as a fallback.
3914
+ */
3915
+ async compile(request, options = {}) {
3916
+ return this.transport.compile(
3917
+ normalizeCompileRequest(request),
3918
+ normalizeCompileOptions(options)
3919
+ );
3920
+ }
3376
3921
  /**
3377
3922
  * Resolves with the run's handle once the engine admitted it, and rejects
3378
3923
  * without one when the engine refused it. The handle owns the lifecycle: