@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.js CHANGED
@@ -203,29 +203,41 @@ function captureEngine(bin, args, options) {
203
203
  shell: false,
204
204
  stdio: ["ignore", "pipe", "pipe"]
205
205
  });
206
- let stdout = "";
207
- let stderr = "";
206
+ const stdout = [];
207
+ const stderr = [];
208
+ const bytes = { stdout: 0, stderr: 0 };
208
209
  let overflow = false;
209
210
  let spawnError;
211
+ let closed = false;
212
+ let killTimer;
213
+ const stop = () => {
214
+ child.kill("SIGTERM");
215
+ if (options.killGraceMs === void 0) return;
216
+ killTimer ??= setTimeout(() => {
217
+ if (!closed) child.kill("SIGKILL");
218
+ }, options.killGraceMs);
219
+ killTimer.unref();
220
+ };
210
221
  const append = (stream, chunk) => {
211
222
  if (overflow) return;
212
- if (stream === "stdout") stdout += chunk;
213
- else stderr += chunk;
214
- if (Buffer.byteLength(stdout) > options.bufferBytes || Buffer.byteLength(stderr) > options.bufferBytes) {
223
+ bytes[stream] += chunk.byteLength;
224
+ if (bytes[stream] > options.bufferBytes) {
215
225
  overflow = true;
216
- child.kill("SIGTERM");
226
+ stop();
227
+ return;
217
228
  }
229
+ (stream === "stdout" ? stdout : stderr).push(chunk);
218
230
  };
219
- child.stdout.setEncoding("utf8");
220
- child.stderr.setEncoding("utf8");
221
231
  child.stdout.on("data", (chunk) => append("stdout", chunk));
222
232
  child.stderr.on("data", (chunk) => append("stderr", chunk));
223
- const abort = () => child.kill("SIGTERM");
233
+ const abort = () => stop();
224
234
  options.signal?.addEventListener("abort", abort, { once: true });
225
235
  child.once("error", (cause) => {
226
236
  spawnError = cause;
227
237
  });
228
- child.once("close", (code) => {
238
+ child.once("close", (code, exitSignal) => {
239
+ closed = true;
240
+ if (killTimer) clearTimeout(killTimer);
229
241
  options.signal?.removeEventListener("abort", abort);
230
242
  if (options.signal?.aborted) {
231
243
  reject(new NikaTransportError(options.transport, `${options.label} aborted by caller`));
@@ -241,7 +253,19 @@ function captureEngine(bin, args, options) {
241
253
  `${options.label} exceeded ${options.bufferBytes} bytes`
242
254
  ));
243
255
  } else {
244
- resolve({ exitCode: code ?? 3, stdout, stderr });
256
+ let decoded;
257
+ try {
258
+ decoded = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(Buffer.concat(stdout, bytes.stdout));
259
+ } catch {
260
+ reject(new NikaProtocolError(options.transport, `${options.label} stdout was not valid UTF-8`));
261
+ return;
262
+ }
263
+ resolve({
264
+ exitCode: code ?? 3,
265
+ stdout: decoded,
266
+ stderr: Buffer.concat(stderr, bytes.stderr).toString("utf8"),
267
+ exitSignal
268
+ });
245
269
  }
246
270
  });
247
271
  });
@@ -473,9 +497,9 @@ function incompatible(transport, message) {
473
497
 
474
498
  // src/lib/binary/verify.ts
475
499
  var IDENTITY_BUFFER_BYTES = 16 * 1024;
476
- async function verifyNikaEngine(engine) {
500
+ async function verifyNikaEngine(engine, options = {}) {
477
501
  const expectedVersion = engine.packageRoot ? await verifyManagedPayload(engine) : void 0;
478
- const identity = await probeIdentity(engine.bin);
502
+ const identity = await probeIdentity(engine.bin, options);
479
503
  if (expectedVersion !== void 0 && identity.engineVersion !== expectedVersion) {
480
504
  throw incompatible2(
481
505
  `Packaged engine version ${identity.engineVersion} does not match payload version ${expectedVersion}`
@@ -514,13 +538,15 @@ async function verifyManagedPayload(engine) {
514
538
  }
515
539
  return manifest.version;
516
540
  }
517
- async function probeIdentity(bin) {
541
+ async function probeIdentity(bin, options) {
518
542
  const notAnEngine = `is ${bin} a nika engine? (run "${bin} --sdk-identity" by hand)`;
519
543
  const captured = await captureEngine(bin, ["--sdk-identity"], {
544
+ ...options,
520
545
  bufferBytes: IDENTITY_BUFFER_BYTES,
521
546
  transport: "native-process",
522
547
  label: "Engine identity probe"
523
548
  }).catch((cause) => {
549
+ if (options.signal?.aborted) throw cause;
524
550
  throw incompatible2(
525
551
  `Engine identity probe of ${bin} failed`,
526
552
  cause
@@ -600,25 +626,26 @@ var NATIVE_ARRAY = Function.prototype.toString.call(Array);
600
626
  var LONE_SURROGATE = /[\ud800-\udbff](?![\udc00-\udfff])|(?<![\ud800-\udbff])[\udc00-\udfff]/;
601
627
  var IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
602
628
  var PATH_LIMIT = 240;
603
- function encodeLiteralInputs(inputs) {
629
+ function encodeLiteralInputs(inputs, label = "run({ inputs })") {
604
630
  if (containerKind(inputs) !== "object") {
631
+ 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";
605
632
  throw new NikaConfigurationError(
606
- `run({ inputs }): inputs must be a plain object mapping declared workflow input names to strict JSON values; received ${describe(inputs)}`
633
+ `${label}: ${subject} to strict JSON values; received ${describe(inputs)}`
607
634
  );
608
635
  }
609
636
  const parts = [];
610
637
  let bytes = 0;
611
638
  const write = (chunk) => {
612
639
  bytes += Buffer.byteLength(chunk);
613
- if (bytes > LITERAL_INPUTS_MAX_BYTES) throw tooLarge();
640
+ if (bytes > LITERAL_INPUTS_MAX_BYTES) throw tooLarge(label);
614
641
  parts.push(chunk);
615
642
  };
616
643
  const stack = [];
617
644
  const open = /* @__PURE__ */ new Set();
618
645
  const refuse = (segment, problem) => {
619
- const path3 = stack.map((frame) => frame.segment).join("") + segment;
620
- const shown = path3.length > PATH_LIMIT ? `${path3.slice(0, PATH_LIMIT / 2)}\u2026${path3.slice(-PATH_LIMIT / 2)}` : path3;
621
- return new NikaConfigurationError(`run({ inputs }): ${shown} ${problem}; ${STRICT_JSON}`);
646
+ const path4 = stack.map((frame) => frame.segment).join("") + segment;
647
+ const shown = path4.length > PATH_LIMIT ? `${path4.slice(0, PATH_LIMIT / 2)}\u2026${path4.slice(-PATH_LIMIT / 2)}` : path4;
648
+ return new NikaConfigurationError(`${label}: ${shown} ${problem}; ${STRICT_JSON}`);
622
649
  };
623
650
  const enter = (value, segment, kind) => {
624
651
  open.add(value);
@@ -671,7 +698,7 @@ function encodeLiteralInputs(inputs) {
671
698
  const value = member.value;
672
699
  switch (typeof value) {
673
700
  case "string":
674
- if (value.length > LITERAL_INPUTS_MAX_BYTES) throw tooLarge();
701
+ if (value.length > LITERAL_INPUTS_MAX_BYTES) throw tooLarge(label);
675
702
  if (LONE_SURROGATE.test(value)) {
676
703
  throw refuse(segment, "contains a lone surrogate, which is not valid Unicode");
677
704
  }
@@ -703,7 +730,17 @@ function encodeLiteralInputs(inputs) {
703
730
  }
704
731
  return { json: parts.join(""), bytes };
705
732
  }
706
- function tooLarge() {
733
+ function tooLarge(label) {
734
+ if (label === "compile({ change })") {
735
+ return new NikaConfigurationError(
736
+ `compile({ change }): the serialized literal exceeds ${LITERAL_INPUTS_MAX_BYTES} bytes (1 MiB)`
737
+ );
738
+ }
739
+ if (label === "compile({ answers })") {
740
+ return new NikaConfigurationError(
741
+ `compile({ answers }): the serialized answers exceed ${LITERAL_INPUTS_MAX_BYTES} bytes (1 MiB); answers ride argv, one KEY=JSON element each, never a wire`
742
+ );
743
+ }
707
744
  return new NikaConfigurationError(
708
745
  `run({ inputs }): the serialized inputs map exceeds ${LITERAL_INPUTS_MAX_BYTES} bytes (1 MiB), the bound both transports share with the native engine`
709
746
  );
@@ -760,6 +797,364 @@ function refusedObject(value) {
760
797
  return typeof name === "string" && IDENTIFIER.test(name) && name.length <= 64 ? `is a ${name} instance, not a plain object or array` : custom;
761
798
  }
762
799
 
800
+ // src/lib/compile.ts
801
+ import { mkdtemp, rm, writeFile } from "fs/promises";
802
+ import { tmpdir } from "os";
803
+ import path3 from "path";
804
+ import { types as utilTypes } from "util";
805
+ import { addAbortListener } from "events";
806
+ var COMPILE_CAPABILITY = "compile";
807
+ var COMPILE_WIRE_VERSION = 1;
808
+ var COMPILE_RESPONSE_MAX_BYTES = 8 * 1024 * 1024;
809
+ var COMPILE_KILL_GRACE_MS = 2e3;
810
+ var STATUSES = ["ready", "incomplete", "refused"];
811
+ var signalAborted = Object.getOwnPropertyDescriptor(AbortSignal.prototype, "aborted").get;
812
+ var removeSignalListener = EventTarget.prototype.removeEventListener;
813
+ var signalInterface = /* @__PURE__ */ new Set([
814
+ "aborted",
815
+ "reason",
816
+ "throwIfAborted",
817
+ "addEventListener",
818
+ "removeEventListener",
819
+ "dispatchEvent"
820
+ ]);
821
+ function normalizeCompileRequest(input) {
822
+ if (typeof input === "string") {
823
+ if (input.length === 0) {
824
+ throw new NikaConfigurationError("compile: an intent string must not be empty");
825
+ }
826
+ return { intent: input };
827
+ }
828
+ const record2 = dataRecord(input, "request");
829
+ for (const key of Reflect.ownKeys(record2)) {
830
+ if (key !== "intent" && key !== "workflow" && key !== "change" && key !== "answers") {
831
+ throw new NikaConfigurationError(
832
+ `compile: unknown request field ${String(key)}; the request is refused rather than silently reinterpreted`
833
+ );
834
+ }
835
+ }
836
+ const answers = record2.answers;
837
+ if (answers !== void 0) encodeLiteralInputs(answers, "compile({ answers })");
838
+ const intent = record2.intent;
839
+ const workflow = record2.workflow;
840
+ const change = record2.change;
841
+ if (intent !== void 0) {
842
+ if (workflow !== void 0 || change !== void 0) {
843
+ throw new NikaConfigurationError(
844
+ "compile: intent (create) never mixes with workflow/change (edit) in one request"
845
+ );
846
+ }
847
+ if (typeof intent !== "string" || intent.length === 0) {
848
+ throw new NikaConfigurationError("compile: intent must be a non-empty string");
849
+ }
850
+ return answers === void 0 ? { intent } : { intent, answers };
851
+ }
852
+ if (workflow === void 0 && change === void 0) {
853
+ throw new NikaConfigurationError(
854
+ "compile: the request carries neither intent nor workflow/change; nothing would compile"
855
+ );
856
+ }
857
+ if (typeof workflow !== "string" || workflow.length === 0) {
858
+ throw new NikaConfigurationError(
859
+ "compile: an edit needs the accepted workflow source as a non-empty string"
860
+ );
861
+ }
862
+ const normalizedChange = normalizeChange(change);
863
+ return answers === void 0 ? { workflow, change: normalizedChange } : { workflow, change: normalizedChange, answers };
864
+ }
865
+ function dataRecord(value, label) {
866
+ if (value === null || typeof value !== "object" || utilTypes.isProxy(value) || Array.isArray(value) || Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) {
867
+ throw new NikaConfigurationError(`compile: ${label} must be ${label === "request" ? "an intent string or " : ""}a plain object with own data fields`);
868
+ }
869
+ const result = /* @__PURE__ */ Object.create(null);
870
+ const descriptors = Object.getOwnPropertyDescriptors(value);
871
+ for (const key of Reflect.ownKeys(descriptors)) {
872
+ const descriptor = descriptors[key];
873
+ if (typeof key !== "string" || !("value" in descriptor) || !descriptor.enumerable) {
874
+ throw new NikaConfigurationError(`compile: ${label} must contain only enumerable string data fields`);
875
+ }
876
+ result[key] = descriptor.value;
877
+ }
878
+ return result;
879
+ }
880
+ function normalizeChange(change) {
881
+ if (typeof change === "string" && change.length > 0) return change;
882
+ if (change === void 0 || change === "") {
883
+ throw new NikaConfigurationError("compile: an edit needs a non-empty change request");
884
+ }
885
+ const encoded = encodeLiteralInputs({ change }, "compile({ change })");
886
+ const object = machineObject(JSON.parse(encoded.json).change);
887
+ const constant = machineObject(object?.set_constant);
888
+ 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)) {
889
+ throw new NikaConfigurationError(
890
+ "compile: an edit needs a non-empty change string or set_constant with a bare name and JSON value"
891
+ );
892
+ }
893
+ return object;
894
+ }
895
+ function normalizeCompileOptions(options) {
896
+ const record2 = dataRecord(options, "options");
897
+ for (const key of Reflect.ownKeys(record2)) {
898
+ if (key !== "signal" && key !== "timeoutMs") {
899
+ throw new NikaConfigurationError(`compile: unknown option ${String(key)}`);
900
+ }
901
+ }
902
+ const timeoutMs = record2.timeoutMs;
903
+ if (timeoutMs !== void 0 && (typeof timeoutMs !== "number" || !Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 2147483647)) {
904
+ throw new NikaConfigurationError(
905
+ "compile: timeoutMs must be a positive safe integer between 1 and 2147483647 milliseconds"
906
+ );
907
+ }
908
+ const signal = record2.signal;
909
+ if (signal !== void 0) {
910
+ const invalidSignal = () => new NikaConfigurationError("compile: signal must be an AbortSignal");
911
+ if (signal === null || typeof signal !== "object" || utilTypes.isProxy(signal) || Object.getPrototypeOf(signal) !== AbortSignal.prototype) throw invalidSignal();
912
+ const descriptors = Object.getOwnPropertyDescriptors(signal);
913
+ for (const key of Reflect.ownKeys(descriptors)) {
914
+ if (typeof key === "string" && signalInterface.has(key) || !("value" in descriptors[key])) throw invalidSignal();
915
+ }
916
+ try {
917
+ signalAborted.call(signal);
918
+ } catch {
919
+ throw invalidSignal();
920
+ }
921
+ }
922
+ return record2;
923
+ }
924
+ function compileSignal(options) {
925
+ const caller = options.signal;
926
+ let bridge;
927
+ let abort;
928
+ if (caller !== void 0) {
929
+ bridge = new AbortController();
930
+ const controller = bridge;
931
+ abort = () => controller.abort();
932
+ if (signalAborted.call(caller)) abort();
933
+ else addAbortListener(caller, abort);
934
+ }
935
+ let timeout;
936
+ if (options.timeoutMs !== void 0) timeout = AbortSignal.timeout(options.timeoutMs);
937
+ const signals = [bridge?.signal, timeout].filter((s) => s !== void 0);
938
+ return {
939
+ signal: signals.length === 0 ? void 0 : AbortSignal.any(signals),
940
+ timedOut: () => timeout?.aborted === true,
941
+ // Use the intrinsic even if the caller shadows removeEventListener after
942
+ // starting Compile. Do not retain a listener on a long-lived caller signal.
943
+ dispose: () => {
944
+ if (caller && abort) removeSignalListener.call(caller, "abort", abort);
945
+ }
946
+ };
947
+ }
948
+ async function compileArgv(request) {
949
+ const args = ["compile", "--json"];
950
+ for (const answer of compileAnswers(request.answers)) {
951
+ args.push(`--answer=${answer}`);
952
+ }
953
+ if (request.intent !== void 0) {
954
+ refuseNul(request.intent);
955
+ args.push("--", request.intent);
956
+ return { args };
957
+ }
958
+ const change = typeof request.change === "string" ? request.change : `Set const.${request.change.set_constant.name} to ${literalJson(request.change.set_constant.value)}`;
959
+ refuseNul(change);
960
+ const scratchDir = await mkdtemp(path3.join(tmpdir(), "nika-sdk-compile-"));
961
+ const base = path3.join(scratchDir, "base.nika");
962
+ try {
963
+ await writeFile(base, request.workflow, { encoding: "utf8", mode: 384 });
964
+ } catch (cause) {
965
+ await rm(scratchDir, { recursive: true, force: true });
966
+ throw new NikaTransportError("native-process", "compile could not write its temporary base source", {
967
+ cause: cause instanceof Error ? cause : void 0
968
+ });
969
+ }
970
+ args.push("--base", base, `--change=${change}`);
971
+ return { args, scratchDir };
972
+ }
973
+ async function removeCompileScratch(invocation) {
974
+ if (invocation.scratchDir !== void 0) {
975
+ await rm(invocation.scratchDir, { recursive: true, force: true });
976
+ }
977
+ }
978
+ function compileAnswers(answers) {
979
+ if (answers === void 0) return [];
980
+ encodeLiteralInputs(answers, "compile({ answers })");
981
+ return Reflect.ownKeys(answers).map((key) => {
982
+ const name = String(key);
983
+ if (name.length === 0 || name.includes("=") || name.includes("\0")) {
984
+ throw new NikaConfigurationError(
985
+ "compile({ answers }): a question key must be non-empty and cannot carry '='; the engine splits KEY=JSON on its first equals sign"
986
+ );
987
+ }
988
+ return `${name}=${literalJson(answers[name])}`;
989
+ });
990
+ }
991
+ function literalJson(value) {
992
+ return encodeLiteralInputs({ value }, "compile({ change })").json.slice('{"value":'.length, -1);
993
+ }
994
+ function refuseNul(value) {
995
+ if (value.includes("\0")) throw new NikaConfigurationError("compile: text cannot contain a NUL byte");
996
+ }
997
+ function compileBody(request) {
998
+ compileAnswers(request.answers);
999
+ const answers = request.answers === void 0 ? "" : `,"answers":${encodeLiteralInputs(request.answers, "compile({ answers })").json}`;
1000
+ if (request.intent !== void 0) {
1001
+ refuseNul(request.intent);
1002
+ return `{"compile_version":1,"mode":"create","intent":${JSON.stringify(request.intent)}${answers}}`;
1003
+ }
1004
+ if (typeof request.change === "string") refuseNul(request.change);
1005
+ 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)}}}`;
1006
+ return `{"compile_version":1,"mode":"edit","source":${JSON.stringify(request.workflow)},"change":${change}${answers}}`;
1007
+ }
1008
+ function compileOutcomeFrom(captured, transport, engine) {
1009
+ const protocol = (message) => new NikaProtocolError(transport, `${message} (compile child at ${engine})`);
1010
+ if (captured.exitSignal !== null) {
1011
+ throw new NikaTransportError(
1012
+ transport,
1013
+ `compile child at ${engine} was ended externally by ${captured.exitSignal}`
1014
+ );
1015
+ }
1016
+ const payload = machineObject(tryParseJson(captured.stdout));
1017
+ if (!payload) {
1018
+ const detail = excerpt(captured.stderr) || excerpt(captured.stdout);
1019
+ throw protocol(
1020
+ `the compile wire object is absent or malformed (exit ${captured.exitCode})` + (detail ? `: ${detail}` : "")
1021
+ );
1022
+ }
1023
+ validateCompileVersion(payload.compile_version, transport, engine, protocol);
1024
+ const hasError = "error" in payload;
1025
+ const hasStatus = "status" in payload;
1026
+ if (hasError && hasStatus) {
1027
+ throw protocol("the payload carries both an outcome and an error");
1028
+ }
1029
+ if (hasError) {
1030
+ const error = machineObject(payload.error);
1031
+ if (!error || typeof error.code !== "string" || typeof error.message !== "string") {
1032
+ throw protocol("the error payload lacks its code/message strings");
1033
+ }
1034
+ if (captured.exitCode !== 2 && captured.exitCode !== 3) {
1035
+ throw protocol(
1036
+ `an engine-stamped error arrived with exit ${captured.exitCode}, not 2 or 3`
1037
+ );
1038
+ }
1039
+ throw new NikaOperationError("compile", transport, error.code, error.message, {
1040
+ status: captured.exitCode,
1041
+ machineCode: error.code
1042
+ });
1043
+ }
1044
+ const status = payload.status;
1045
+ if (typeof status !== "string" || !STATUSES.includes(status)) {
1046
+ throw protocol(`unknown compile status ${JSON.stringify(status ?? null)}`);
1047
+ }
1048
+ if (status === "ready" ? captured.exitCode !== 0 : captured.exitCode !== 2) {
1049
+ throw protocol(
1050
+ `status ${JSON.stringify(status)} contradicts exit ${captured.exitCode}`
1051
+ );
1052
+ }
1053
+ writtenFrom(payload.written, protocol);
1054
+ return compilePayloadFrom(payload, transport, engine);
1055
+ }
1056
+ function compilePayloadFrom(payload, transport, engine) {
1057
+ const protocol = (message) => new NikaProtocolError(transport, `compile at ${engine}: ${message}`);
1058
+ validateCompileVersion(payload.compile_version, transport, engine, protocol);
1059
+ if ("error" in payload) throw protocol("the payload carries an error instead of an outcome");
1060
+ if (transport === "http" && "written" in payload) throw protocol("HTTP outcome claims a written destination");
1061
+ const status = payload.status;
1062
+ if (typeof status !== "string" || !STATUSES.includes(status)) {
1063
+ throw protocol("unknown compile status");
1064
+ }
1065
+ const candidate = payload.candidate;
1066
+ if (candidate !== null && typeof candidate !== "string") {
1067
+ throw protocol("candidate is neither source text nor null");
1068
+ }
1069
+ if (status === "ready" && candidate === null) {
1070
+ throw protocol("a ready outcome carries no candidate");
1071
+ }
1072
+ const outcome = {
1073
+ compile_version: COMPILE_WIRE_VERSION,
1074
+ status,
1075
+ ready: status === "ready",
1076
+ candidate,
1077
+ questions: questionsFrom(payload.questions, protocol),
1078
+ diagnostics: diagnosticsFrom(payload.diagnostics, protocol),
1079
+ requested_boundary: nullableObject(payload.requested_boundary, "requested_boundary", protocol),
1080
+ check_preview: previewFrom(payload.check_preview, protocol),
1081
+ provenance: provenanceFrom(payload.provenance, protocol)
1082
+ };
1083
+ return outcome;
1084
+ }
1085
+ function validateCompileVersion(version, transport, engine, protocol) {
1086
+ if (typeof version !== "number" || !Number.isSafeInteger(version) || version < 1) {
1087
+ throw protocol("compile_version must be a positive safe integer");
1088
+ }
1089
+ if (version !== COMPILE_WIRE_VERSION) {
1090
+ throw new NikaCompatibilityError(
1091
+ COMPILE_CAPABILITY,
1092
+ transport,
1093
+ `Engine at ${engine} speaks compile wire ${version}; expected ${COMPILE_WIRE_VERSION}`
1094
+ );
1095
+ }
1096
+ }
1097
+ function tryParseJson(text) {
1098
+ const trimmed = text.trim();
1099
+ if (!trimmed) return void 0;
1100
+ try {
1101
+ return JSON.parse(trimmed);
1102
+ } catch {
1103
+ return void 0;
1104
+ }
1105
+ }
1106
+ function excerpt(text) {
1107
+ const flat = text.trim().replace(/\s+/g, " ");
1108
+ return flat.length <= 240 ? flat : `${flat.slice(0, 240)}\u2026`;
1109
+ }
1110
+ function nullableObject(value, field, protocol) {
1111
+ if (value === null) return null;
1112
+ const object = machineObject(value);
1113
+ if (!object) throw protocol(`${field} is neither an object nor null`);
1114
+ return object;
1115
+ }
1116
+ function questionsFrom(value, protocol) {
1117
+ if (!Array.isArray(value)) throw protocol("questions is not an array");
1118
+ return value.map((entry) => {
1119
+ const question = machineObject(entry);
1120
+ if (!question || typeof question.key !== "string" || typeof question.label !== "string" || question.type !== "text" && question.type !== "literal" || typeof question.why !== "string" || typeof question.mandatory !== "boolean") {
1121
+ throw protocol("a question lacks its key/label/type/why/mandatory shape");
1122
+ }
1123
+ return question;
1124
+ });
1125
+ }
1126
+ function diagnosticsFrom(value, protocol) {
1127
+ if (!Array.isArray(value)) throw protocol("diagnostics is not an array");
1128
+ return value.map((entry) => {
1129
+ const diagnostic = machineObject(entry);
1130
+ if (!diagnostic || typeof diagnostic.kind !== "string" || !["applied", "missed", "unknown", "requiresHuman", "refused"].includes(diagnostic.kind) || typeof diagnostic.target !== "string" || typeof diagnostic.message !== "string") {
1131
+ throw protocol("a diagnostic lacks its kind/target/message shape");
1132
+ }
1133
+ return diagnostic;
1134
+ });
1135
+ }
1136
+ function previewFrom(value, protocol) {
1137
+ if (value === null) return null;
1138
+ const preview = machineObject(value);
1139
+ if (!preview || preview.scope !== "sourceOnly" || !machineObject(preview.report)) {
1140
+ throw protocol("check_preview lacks its scope/report shape");
1141
+ }
1142
+ return { ...preview, scope: preview.scope, report: preview.report };
1143
+ }
1144
+ function provenanceFrom(value, protocol) {
1145
+ const provenance = machineObject(value);
1146
+ if (!provenance || typeof provenance.compiler_version !== "string" || typeof provenance.spec_pin !== "string" || provenance.skeleton !== null && typeof provenance.skeleton !== "string" || provenance.cognition !== "deterministicOnly") {
1147
+ throw protocol("provenance lacks its compiler_version/spec_pin/skeleton/cognition shape");
1148
+ }
1149
+ return provenance;
1150
+ }
1151
+ function writtenFrom(value, protocol) {
1152
+ if (value !== null) {
1153
+ throw protocol("the outcome claims a written destination the SDK never asked for");
1154
+ }
1155
+ return null;
1156
+ }
1157
+
763
1158
  // src/lib/sse/parser.ts
764
1159
  var SseParseError = class extends Error {
765
1160
  constructor(message, options) {
@@ -921,8 +1316,8 @@ async function* decodeSse(stream, limits, signal) {
921
1316
  var WORKFLOW_SUFFIX = ".nika";
922
1317
  var LEGACY_WORKFLOW_SUFFIXES = [".nika.yaml", ".nika.yml"];
923
1318
  var CONTAINED_CHAR = /[A-Za-z0-9/._-]/;
924
- function workflowBasename(path3) {
925
- const parts = path3.split(/[\\/]/);
1319
+ function workflowBasename(path4) {
1320
+ const parts = path4.split(/[\\/]/);
926
1321
  return parts[parts.length - 1] ?? "";
927
1322
  }
928
1323
  function hasControlChar(value) {
@@ -934,14 +1329,14 @@ function hasControlChar(value) {
934
1329
  }
935
1330
  return false;
936
1331
  }
937
- function isLegacyWorkflowPath(path3) {
938
- if (typeof path3 !== "string" || path3.length === 0) {
1332
+ function isLegacyWorkflowPath(path4) {
1333
+ if (typeof path4 !== "string" || path4.length === 0) {
939
1334
  return false;
940
1335
  }
941
- if (path3.endsWith("/") || path3.endsWith("\\")) {
1336
+ if (path4.endsWith("/") || path4.endsWith("\\")) {
942
1337
  return false;
943
1338
  }
944
- const base = workflowBasename(path3);
1339
+ const base = workflowBasename(path4);
945
1340
  return LEGACY_WORKFLOW_SUFFIXES.some((suffix) => base.endsWith(suffix));
946
1341
  }
947
1342
  function isCanonicalWorkflowFilename(basename) {
@@ -962,17 +1357,17 @@ function isCanonicalWorkflowFilename(basename) {
962
1357
  }
963
1358
  return basename.length > WORKFLOW_SUFFIX.length;
964
1359
  }
965
- function isCanonicalWorkflowPath(path3) {
966
- if (typeof path3 !== "string" || path3.length === 0) {
1360
+ function isCanonicalWorkflowPath(path4) {
1361
+ if (typeof path4 !== "string" || path4.length === 0) {
967
1362
  return false;
968
1363
  }
969
- if (hasControlChar(path3)) {
1364
+ if (hasControlChar(path4)) {
970
1365
  return false;
971
1366
  }
972
- if (path3.endsWith("/") || path3.endsWith("\\")) {
1367
+ if (path4.endsWith("/") || path4.endsWith("\\")) {
973
1368
  return false;
974
1369
  }
975
- return isCanonicalWorkflowFilename(workflowBasename(path3));
1370
+ return isCanonicalWorkflowFilename(workflowBasename(path4));
976
1371
  }
977
1372
  function containedRelativeShape(value) {
978
1373
  if (value.includes("\\") || value.startsWith("/")) {
@@ -1063,6 +1458,69 @@ var HttpTransport = class {
1063
1458
  }
1064
1459
  return captured.report;
1065
1460
  }
1461
+ /**
1462
+ * Stateless authoring through the authenticated Serve compile door.
1463
+ * A remote connection never falls back to a local compile — the candidate
1464
+ * must come from the server the caller connected to, or not exist.
1465
+ */
1466
+ async compile(request, options) {
1467
+ const body = compileBody(request);
1468
+ const timeoutMs = options.timeoutMs ?? this.options.requestTimeout;
1469
+ const composed = compileSignal({ ...options, timeoutMs });
1470
+ const signal = composed.signal;
1471
+ try {
1472
+ if (signal.aborted) throw new NikaTransportError(this.kind, "compile aborted");
1473
+ const identity = this.remoteIdentity ?? await this.probeServerIdentity(signal);
1474
+ if (!identity.supportedCapabilities.includes(COMPILE_CAPABILITY)) {
1475
+ throw this.gap(
1476
+ COMPILE_CAPABILITY,
1477
+ "The connected engine does not advertise compile; the SDK never compiles locally as a substitute"
1478
+ );
1479
+ }
1480
+ const path4 = "/v1/compile";
1481
+ const response = await this.fetchResponse(path4, {
1482
+ method: "POST",
1483
+ headers: { "Content-Type": "application/json", Accept: "application/json" },
1484
+ body,
1485
+ signal
1486
+ }, false, true, false);
1487
+ if (response.headers.get("Content-Type")?.split(";", 1)[0]?.trim().toLowerCase() !== "application/json") {
1488
+ await discardResponse(response);
1489
+ throw new NikaProtocolError(this.kind, "HTTP compile returned an invalid content-type");
1490
+ }
1491
+ const object = await this.readObservationObject(
1492
+ response,
1493
+ path4,
1494
+ signal,
1495
+ Math.min(this.options.machineBufferBytes, COMPILE_RESPONSE_MAX_BYTES),
1496
+ false
1497
+ );
1498
+ if (signal.aborted) throw new NikaTransportError(this.kind, "compile aborted");
1499
+ if (response.status !== 200) {
1500
+ const error = machineObject(object.error);
1501
+ 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")) {
1502
+ throw new NikaProtocolError(this.kind, "HTTP compile returned a malformed refusal or non-contract status");
1503
+ }
1504
+ throw this.refused(path4, {
1505
+ operation: "compile",
1506
+ status: response.status,
1507
+ refusal: { code: error.code, message: this.redact(error.message) }
1508
+ });
1509
+ }
1510
+ return compilePayloadFrom(object, this.kind, this.options.url);
1511
+ } catch (cause) {
1512
+ if (signal.aborted) {
1513
+ throw new NikaTransportError(
1514
+ this.kind,
1515
+ composed.timedOut() ? `compile timed out after ${timeoutMs} ms` : "compile aborted by caller",
1516
+ { cause: cause instanceof Error ? cause : void 0 }
1517
+ );
1518
+ }
1519
+ throw cause;
1520
+ } finally {
1521
+ composed.dispose();
1522
+ }
1523
+ }
1066
1524
  async startRun(workflow, options) {
1067
1525
  const inputs = literalInputs(options);
1068
1526
  if (options.vars && Object.keys(options.vars).length > 0 || options.model !== void 0 || options.maxCostUsd !== void 0) {
@@ -1182,14 +1640,14 @@ var HttpTransport = class {
1182
1640
  async schedule(workflow, options) {
1183
1641
  refuseLegacyContainedName(workflow);
1184
1642
  await this.ensureScheduleCapability();
1185
- const path3 = `/v1/schedules/${encodeURIComponent(options.id)}`;
1643
+ const path4 = `/v1/schedules/${encodeURIComponent(options.id)}`;
1186
1644
  const headers = new Headers({ "Content-Type": "application/json" });
1187
1645
  if (options.revision === void 0) {
1188
1646
  headers.set("If-None-Match", "*");
1189
1647
  } else {
1190
1648
  headers.set("If-Match", `"${options.revision}"`);
1191
1649
  }
1192
- const object = await this.operationJson("schedule", path3, {
1650
+ const object = await this.operationJson("schedule", path4, {
1193
1651
  method: "PUT",
1194
1652
  headers,
1195
1653
  body: JSON.stringify(scheduleBody(workflow, options))
@@ -1201,8 +1659,8 @@ var HttpTransport = class {
1201
1659
  }
1202
1660
  async scheduleStatus(id) {
1203
1661
  await this.ensureScheduleCapability();
1204
- const path3 = `/v1/schedules/${encodeURIComponent(id)}`;
1205
- const object = await this.operationJson("scheduleStatus", path3, {
1662
+ const path4 = `/v1/schedules/${encodeURIComponent(id)}`;
1663
+ const object = await this.operationJson("scheduleStatus", path4, {
1206
1664
  method: "GET",
1207
1665
  headers: { Accept: "application/json" }
1208
1666
  });
@@ -1535,16 +1993,16 @@ var HttpTransport = class {
1535
1993
  }
1536
1994
  return event;
1537
1995
  }
1538
- async inspectDurableJob(path3, expectedId, signal) {
1996
+ async inspectDurableJob(path4, expectedId, signal) {
1539
1997
  let response;
1540
1998
  try {
1541
- response = await this.observationRequest(path3, { headers: { Accept: "application/json" }, signal });
1999
+ response = await this.observationRequest(path4, { headers: { Accept: "application/json" }, signal });
1542
2000
  } catch {
1543
2001
  return { retry: true };
1544
2002
  }
1545
2003
  if (response.status === 404) {
1546
2004
  await discardResponse(response);
1547
- throw new NikaTransportError(this.kind, `HTTP 404 for ${path3}; job was durably absent`);
2005
+ throw new NikaTransportError(this.kind, `HTTP 404 for ${path4}; job was durably absent`);
1548
2006
  }
1549
2007
  if (isRetryableStatus(response.status)) {
1550
2008
  const retryAfterMilliseconds = retryAfter(response.headers.get("Retry-After"));
@@ -1554,9 +2012,9 @@ var HttpTransport = class {
1554
2012
  ...retryAfterMilliseconds !== void 0 ? { retryAfterMilliseconds } : {}
1555
2013
  };
1556
2014
  }
1557
- this.assertObservationResponse(response, path3, "application/json");
2015
+ this.assertObservationResponse(response, path4, "application/json");
1558
2016
  try {
1559
- const object = await this.readObservationObject(response, path3);
2017
+ const object = await this.readObservationObject(response, path4);
1560
2018
  return durableJob(object, expectedId, this.kind);
1561
2019
  } catch (cause) {
1562
2020
  if (cause instanceof NikaTransportError && !(cause instanceof NikaProtocolError)) {
@@ -1565,31 +2023,31 @@ var HttpTransport = class {
1565
2023
  throw cause;
1566
2024
  }
1567
2025
  }
1568
- assertObservationResponse(response, path3, contentType) {
2026
+ assertObservationResponse(response, path4, contentType) {
1569
2027
  if (response.status === 204) {
1570
2028
  void discardResponse(response);
1571
- throw new NikaProtocolError(this.kind, `HTTP 204 permanently stopped observation for ${path3}`);
2029
+ throw new NikaProtocolError(this.kind, `HTTP 204 permanently stopped observation for ${path4}`);
1572
2030
  }
1573
2031
  if (response.status === 401 || response.status === 403) {
1574
2032
  void discardResponse(response);
1575
- throw new NikaTransportError(this.kind, `HTTP ${response.status} authorization failure for ${path3}`);
2033
+ throw new NikaTransportError(this.kind, `HTTP ${response.status} authorization failure for ${path4}`);
1576
2034
  }
1577
2035
  if (response.status !== 200) {
1578
2036
  void discardResponse(response);
1579
- throw new NikaTransportError(this.kind, `HTTP ${response.status} permanently failed ${path3}`);
2037
+ throw new NikaTransportError(this.kind, `HTTP ${response.status} permanently failed ${path4}`);
1580
2038
  }
1581
2039
  const actual = response.headers.get("Content-Type")?.split(";", 1)[0]?.trim().toLowerCase();
1582
2040
  if (actual !== contentType) {
1583
2041
  void discardResponse(response);
1584
2042
  throw new NikaProtocolError(
1585
2043
  this.kind,
1586
- `HTTP ${path3} returned an invalid content-type`
2044
+ `HTTP ${path4} returned an invalid content-type`
1587
2045
  );
1588
2046
  }
1589
2047
  }
1590
- async readObservationObject(response, path3, signal) {
2048
+ async readObservationObject(response, path4, signal, maxBytes = this.options.machineBufferBytes, useRequestTimeout = true) {
1591
2049
  if (!response.body) {
1592
- throw new NikaProtocolError(this.kind, `HTTP ${path3} omitted its JSON body`);
2050
+ throw new NikaProtocolError(this.kind, `HTTP ${path4} omitted its JSON body`);
1593
2051
  }
1594
2052
  const reader = response.body.getReader();
1595
2053
  const chunks = [];
@@ -1599,38 +2057,38 @@ var HttpTransport = class {
1599
2057
  rejectBoundary = reject;
1600
2058
  });
1601
2059
  const abort = () => {
1602
- rejectBoundary(new NikaTransportError(this.kind, `HTTP ${path3} response body was aborted`));
2060
+ rejectBoundary(new NikaTransportError(this.kind, `HTTP ${path4} response body was aborted`));
1603
2061
  void reader.cancel().catch(() => {
1604
2062
  });
1605
2063
  };
1606
2064
  signal?.addEventListener("abort", abort, { once: true });
1607
- const timer = setTimeout(() => {
2065
+ const timer = useRequestTimeout ? setTimeout(() => {
1608
2066
  rejectBoundary(new NikaTransportError(
1609
2067
  this.kind,
1610
2068
  `HTTP response body timed out after ${this.options.requestTimeout}ms`
1611
2069
  ));
1612
2070
  void reader.cancel().catch(() => {
1613
2071
  });
1614
- }, this.options.requestTimeout);
2072
+ }, this.options.requestTimeout) : void 0;
1615
2073
  try {
1616
2074
  if (signal?.aborted) abort();
1617
2075
  while (true) {
1618
2076
  const { done, value: value2 } = await Promise.race([reader.read(), boundary]);
1619
2077
  if (done) break;
1620
2078
  bytes += value2.byteLength;
1621
- if (bytes > this.options.machineBufferBytes) {
2079
+ if (bytes > maxBytes) {
1622
2080
  throw new NikaProtocolError(
1623
2081
  this.kind,
1624
- `HTTP ${path3} JSON exceeded ${this.options.machineBufferBytes} bytes`
2082
+ `HTTP ${path4} JSON exceeded ${maxBytes} bytes`
1625
2083
  );
1626
2084
  }
1627
2085
  chunks.push(value2);
1628
2086
  }
1629
2087
  } catch (cause) {
1630
2088
  if (cause instanceof NikaProtocolError) throw cause;
1631
- throw new NikaTransportError(this.kind, `HTTP ${path3} response body reset`);
2089
+ throw new NikaTransportError(this.kind, `HTTP ${path4} response body reset`);
1632
2090
  } finally {
1633
- clearTimeout(timer);
2091
+ if (timer) clearTimeout(timer);
1634
2092
  signal?.removeEventListener("abort", abort);
1635
2093
  await reader.cancel().catch(() => {
1636
2094
  });
@@ -1646,7 +2104,7 @@ var HttpTransport = class {
1646
2104
  try {
1647
2105
  text = new TextDecoder("utf-8", { fatal: true }).decode(body);
1648
2106
  } catch (cause) {
1649
- throw new NikaProtocolError(this.kind, `HTTP ${path3} JSON was not valid UTF-8`, {
2107
+ throw new NikaProtocolError(this.kind, `HTTP ${path4} JSON was not valid UTF-8`, {
1650
2108
  cause: cause instanceof Error ? cause : void 0
1651
2109
  });
1652
2110
  }
@@ -1654,15 +2112,15 @@ var HttpTransport = class {
1654
2112
  try {
1655
2113
  value = JSON.parse(text);
1656
2114
  } catch (cause) {
1657
- throw new NikaProtocolError(this.kind, `HTTP ${path3} did not return valid JSON`, {
2115
+ throw new NikaProtocolError(this.kind, `HTTP ${path4} did not return valid JSON`, {
1658
2116
  cause: cause instanceof Error ? cause : void 0
1659
2117
  });
1660
2118
  }
1661
2119
  const object = machineObject(value);
1662
- if (!object) throw new NikaProtocolError(this.kind, `HTTP ${path3} did not return an object`);
2120
+ if (!object) throw new NikaProtocolError(this.kind, `HTTP ${path4} did not return an object`);
1663
2121
  return object;
1664
2122
  }
1665
- async observationRequest(path3, init, withTimeout = true) {
2123
+ async observationRequest(path4, init, withTimeout = true) {
1666
2124
  const controller = withTimeout ? new AbortController() : void 0;
1667
2125
  const timer = controller ? setTimeout(() => controller.abort(), this.options.requestTimeout) : void 0;
1668
2126
  const callerSignal = init.signal;
@@ -1671,7 +2129,7 @@ var HttpTransport = class {
1671
2129
  const headers = new Headers(init.headers);
1672
2130
  headers.set("Authorization", `Bearer ${this.options.token}`);
1673
2131
  try {
1674
- return await this.options.fetch(`${this.options.url}${path3}`, {
2132
+ return await this.options.fetch(`${this.options.url}${path4}`, {
1675
2133
  ...init,
1676
2134
  headers,
1677
2135
  signal: controller?.signal ?? callerSignal
@@ -1730,8 +2188,18 @@ var HttpTransport = class {
1730
2188
  this.serverIdentity ??= this.probeServerIdentity();
1731
2189
  return this.serverIdentity;
1732
2190
  }
1733
- async probeServerIdentity() {
1734
- const health = await this.json("/health", { method: "GET" }, false);
2191
+ async probeServerIdentity(signal) {
2192
+ const outcome = await this.jsonOutcome(
2193
+ "/health",
2194
+ { method: "GET", signal },
2195
+ false,
2196
+ [200],
2197
+ void 0,
2198
+ false,
2199
+ signal === void 0
2200
+ );
2201
+ if ("refusal" in outcome) throw this.refused("/health", outcome);
2202
+ const health = outcome.object;
1735
2203
  if (health.status !== "ok" || health.service !== "nika-serve") {
1736
2204
  throw new NikaCompatibilityError(
1737
2205
  "engineIdentity",
@@ -1869,9 +2337,9 @@ var HttpTransport = class {
1869
2337
  delete report.execution_snapshot;
1870
2338
  return report;
1871
2339
  }
1872
- async json(path3, init, authenticated = true, acceptedStatuses = [200], operation) {
2340
+ async json(path4, init, authenticated = true, acceptedStatuses = [200], operation) {
1873
2341
  const { object } = await this.jsonWithStatus(
1874
- path3,
2342
+ path4,
1875
2343
  init,
1876
2344
  authenticated,
1877
2345
  acceptedStatuses,
@@ -1884,48 +2352,54 @@ var HttpTransport = class {
1884
2352
  * routes whose accepted statuses mean different things (cancel: 200 is a
1885
2353
  * settled job, 202 an accepted request).
1886
2354
  */
1887
- async jsonWithStatus(path3, init, authenticated, acceptedStatuses, operation) {
1888
- const outcome = await this.jsonOutcome(path3, init, authenticated, acceptedStatuses, operation);
1889
- if ("refusal" in outcome) throw this.refused(path3, outcome);
2355
+ async jsonWithStatus(path4, init, authenticated, acceptedStatuses, operation) {
2356
+ const outcome = await this.jsonOutcome(path4, init, authenticated, acceptedStatuses, operation);
2357
+ if ("refusal" in outcome) throw this.refused(path4, outcome);
1890
2358
  return outcome;
1891
2359
  }
1892
- async jsonOutcome(path3, init, authenticated, acceptedStatuses, operation, strictRefusal = false) {
1893
- const response = await this.fetchResponse(path3, init, true, authenticated, false);
2360
+ async jsonOutcome(path4, init, authenticated, acceptedStatuses, operation, strictRefusal = false, useRequestTimeout = true) {
2361
+ const response = await this.fetchResponse(path4, init, useRequestTimeout, authenticated, false);
1894
2362
  if (!acceptedStatuses.includes(response.status)) {
1895
2363
  if (response.ok) {
1896
2364
  await discardResponse(response);
1897
2365
  throw new NikaProtocolError(
1898
2366
  this.kind,
1899
- `HTTP ${path3} returned non-contract status ${response.status}`
2367
+ `HTTP ${path4} returned non-contract status ${response.status}`
1900
2368
  );
1901
2369
  }
1902
- const refusal = operation === void 0 ? void 0 : await this.readRefusal(response, path3, strictRefusal);
2370
+ const refusal = operation === void 0 ? void 0 : await this.readRefusal(response, path4, strictRefusal);
1903
2371
  if (operation !== void 0 && refusal) {
1904
2372
  return { operation, status: response.status, refusal };
1905
2373
  }
1906
2374
  await discardResponse(response);
1907
2375
  throw new NikaTransportError(
1908
2376
  this.kind,
1909
- `HTTP ${response.status} for ${path3}: [REDACTED]`
2377
+ `HTTP ${response.status} for ${path4}: [REDACTED]`
1910
2378
  );
1911
2379
  }
1912
2380
  const contentType = response.headers.get("Content-Type")?.split(";", 1)[0]?.trim().toLowerCase();
1913
2381
  if (contentType !== "application/json") {
1914
2382
  await discardResponse(response);
1915
- throw new NikaProtocolError(this.kind, `HTTP ${path3} returned an invalid content-type`);
2383
+ throw new NikaProtocolError(this.kind, `HTTP ${path4} returned an invalid content-type`);
1916
2384
  }
1917
2385
  return {
1918
- object: await this.readObservationObject(response, path3, init.signal ?? void 0),
2386
+ object: await this.readObservationObject(
2387
+ response,
2388
+ path4,
2389
+ init.signal ?? void 0,
2390
+ this.options.machineBufferBytes,
2391
+ useRequestTimeout
2392
+ ),
1919
2393
  status: response.status
1920
2394
  };
1921
2395
  }
1922
- refused(path3, outcome) {
2396
+ refused(path4, outcome) {
1923
2397
  const { code, message } = outcome.refusal;
1924
2398
  return new NikaOperationError(
1925
2399
  outcome.operation,
1926
2400
  this.kind,
1927
2401
  code,
1928
- `HTTP ${outcome.status} for ${path3}: ${code}` + (message ? ` (${message})` : ""),
2402
+ `HTTP ${outcome.status} for ${path4}: ${code}` + (message ? ` (${message})` : ""),
1929
2403
  { status: outcome.status, machineCode: code }
1930
2404
  );
1931
2405
  }
@@ -1935,7 +2409,7 @@ var HttpTransport = class {
1935
2409
  * plain identifier, a code that carries the bearer token) is discarded and
1936
2410
  * the caller falls back to the redacted transport error.
1937
2411
  */
1938
- async readRefusal(response, path3, strict = false) {
2412
+ async readRefusal(response, path4, strict = false) {
1939
2413
  const contentType = response.headers.get("Content-Type")?.split(";", 1)[0]?.trim().toLowerCase();
1940
2414
  if (contentType !== "application/json") {
1941
2415
  await discardResponse(response);
@@ -1943,7 +2417,7 @@ var HttpTransport = class {
1943
2417
  }
1944
2418
  let object;
1945
2419
  try {
1946
- object = await this.readObservationObject(response, path3);
2420
+ object = await this.readObservationObject(response, path4);
1947
2421
  } catch {
1948
2422
  return void 0;
1949
2423
  }
@@ -1961,14 +2435,14 @@ var HttpTransport = class {
1961
2435
  const clean = text.replace(/[\u0000-\u001f\u007f]+/g, " ").split(this.options.token).join("[REDACTED]");
1962
2436
  return clean.length > 240 ? `${clean.slice(0, 240)}...` : clean;
1963
2437
  }
1964
- async operationJson(operation, path3, init) {
1965
- const response = await this.fetchResponse(path3, init, true, true, false);
2438
+ async operationJson(operation, path4, init) {
2439
+ const response = await this.fetchResponse(path4, init, true, true, false);
1966
2440
  const contentType = response.headers.get("Content-Type")?.split(";", 1)[0]?.trim().toLowerCase();
1967
2441
  if (contentType !== "application/json") {
1968
2442
  await discardResponse(response);
1969
- throw new NikaProtocolError(this.kind, `HTTP ${path3} returned an invalid content-type`);
2443
+ throw new NikaProtocolError(this.kind, `HTTP ${path4} returned an invalid content-type`);
1970
2444
  }
1971
- const object = await this.readObservationObject(response, path3);
2445
+ const object = await this.readObservationObject(response, path4);
1972
2446
  if (!response.ok) throw this.operationFailure(operation, response.status, object);
1973
2447
  return object;
1974
2448
  }
@@ -2013,16 +2487,17 @@ var HttpTransport = class {
2013
2487
  { status, machineCode: machine.code }
2014
2488
  );
2015
2489
  }
2016
- async fetchResponse(path3, init, withTimeout, authenticated = true, requireOk = true) {
2490
+ async fetchResponse(path4, init, withTimeout, authenticated = true, requireOk = true) {
2017
2491
  const controller = withTimeout ? new AbortController() : void 0;
2018
2492
  const timer = controller ? setTimeout(() => controller.abort(), this.options.requestTimeout) : void 0;
2019
2493
  const callerSignal = init.signal;
2020
2494
  const abort = () => controller?.abort(callerSignal?.reason);
2021
2495
  callerSignal?.addEventListener("abort", abort, { once: true });
2496
+ if (callerSignal?.aborted) abort();
2022
2497
  const headers = new Headers(init.headers);
2023
2498
  if (authenticated) headers.set("Authorization", `Bearer ${this.options.token}`);
2024
2499
  try {
2025
- const response = await this.options.fetch(`${this.options.url}${path3}`, {
2500
+ const response = await this.options.fetch(`${this.options.url}${path4}`, {
2026
2501
  ...init,
2027
2502
  headers,
2028
2503
  signal: controller?.signal ?? callerSignal
@@ -2031,7 +2506,7 @@ var HttpTransport = class {
2031
2506
  await discardResponse(response);
2032
2507
  throw new NikaTransportError(
2033
2508
  this.kind,
2034
- `HTTP ${response.status} for ${path3}: [REDACTED]`
2509
+ `HTTP ${response.status} for ${path4}: [REDACTED]`
2035
2510
  );
2036
2511
  }
2037
2512
  return response;
@@ -2477,15 +2952,65 @@ var NativeProcessTransport = class {
2477
2952
  const captured = await this.capture(args, options.signal);
2478
2953
  const report = reportObject(captured.stdout) ?? reportObject(stripEnginePrefix(captured.stderr));
2479
2954
  if (!report) {
2480
- const excerpt = diagnosticExcerpt(captured.stderr);
2955
+ const excerpt2 = diagnosticExcerpt(captured.stderr);
2481
2956
  throw new NikaCompatibilityError(
2482
2957
  "check",
2483
2958
  this.kind,
2484
- `This engine did not emit the required JSON check report (exit ${captured.exitCode})` + (excerpt ? `: ${excerpt}` : "")
2959
+ `This engine did not emit the required JSON check report (exit ${captured.exitCode})` + (excerpt2 ? `: ${excerpt2}` : "")
2485
2960
  );
2486
2961
  }
2487
2962
  return { ...report, exitCode: captured.exitCode };
2488
2963
  }
2964
+ /**
2965
+ * Authoring, not execution (issue #128): one bounded `nika compile --json`
2966
+ * invocation, projected verbatim. The engine must advertise the compile
2967
+ * capability — an engine from before the door is refused here, before any
2968
+ * spawn, and there is never a TypeScript fallback. No destination is passed:
2969
+ * the candidate stays source in memory, nothing is written, and no Run
2970
+ * exists; `signal`/`timeoutMs` only stop this one child.
2971
+ */
2972
+ async compile(request, options) {
2973
+ const composed = compileSignal(options);
2974
+ let invocation;
2975
+ try {
2976
+ if (composed.signal?.aborted) {
2977
+ throw new NikaTransportError(this.kind, "compile aborted");
2978
+ }
2979
+ invocation = await compileArgv(request);
2980
+ const identity = composed.signal ? await verifyNikaEngine(this.options.engine, {
2981
+ signal: composed.signal,
2982
+ killGraceMs: COMPILE_KILL_GRACE_MS
2983
+ }) : await this.ensureReady();
2984
+ if (!identity.supportedCapabilities.includes(COMPILE_CAPABILITY)) {
2985
+ throw new NikaCompatibilityError(
2986
+ COMPILE_CAPABILITY,
2987
+ this.kind,
2988
+ `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`
2989
+ );
2990
+ }
2991
+ const captured = await captureEngine(this.options.engine.bin, invocation.args, {
2992
+ cwd: this.options.cwd,
2993
+ signal: composed.signal,
2994
+ bufferBytes: COMPILE_RESPONSE_MAX_BYTES,
2995
+ transport: this.kind,
2996
+ label: "compile",
2997
+ killGraceMs: COMPILE_KILL_GRACE_MS
2998
+ });
2999
+ return compileOutcomeFrom(captured, this.kind, this.options.engine.bin);
3000
+ } catch (cause) {
3001
+ if (composed.signal?.aborted && cause instanceof NikaTransportError) {
3002
+ throw new NikaTransportError(
3003
+ this.kind,
3004
+ composed.timedOut() ? `compile timed out after ${options.timeoutMs} ms` : "compile aborted by caller",
3005
+ { cause }
3006
+ );
3007
+ }
3008
+ throw cause;
3009
+ } finally {
3010
+ composed.dispose();
3011
+ if (invocation) await removeCompileScratch(invocation);
3012
+ }
3013
+ }
2489
3014
  async startRun(workflow, options) {
2490
3015
  const inputs = literalInputs(options);
2491
3016
  const identity = await this.ensureReady();
@@ -2926,8 +3451,8 @@ function quotedProtocolError(cause, text, kind) {
2926
3451
  );
2927
3452
  }
2928
3453
  function quoted(verdict, text) {
2929
- const excerpt = diagnosticExcerpt(text);
2930
- return excerpt ? `${verdict}: ${excerpt}` : verdict;
3454
+ const excerpt2 = diagnosticExcerpt(text);
3455
+ return excerpt2 ? `${verdict}: ${excerpt2}` : verdict;
2931
3456
  }
2932
3457
  function runFlags(options, literal) {
2933
3458
  const flags = [];
@@ -3322,6 +3847,26 @@ var Nika = class {
3322
3847
  check(workflow, options = {}) {
3323
3848
  return this.transport.check(workflowName(workflow), options);
3324
3849
  }
3850
+ /**
3851
+ * Authoring (issue #128): describe work — or name an accepted workflow plus
3852
+ * a change — and get the engine's checked candidate back, without running
3853
+ * anything. `compile()` never means `run()`: the outcome is data (candidate
3854
+ * source, questions, diagnostics, requested boundary, source-only Check
3855
+ * preview, provenance), `incomplete`/`refused` resolve instead of throwing,
3856
+ * and no workflow effect, approval or Proof exists.
3857
+ *
3858
+ * The candidate is ordinary `.nika` SOURCE. `run()` consumes a path,
3859
+ * so the caller materializes the candidate and `run(path)` re-admits it —
3860
+ * the compile preview is a review, never admission. Over `{ url }` this calls
3861
+ * the authenticated Serve compile door, gated by its advertised capability;
3862
+ * the SDK never compiles locally as a fallback.
3863
+ */
3864
+ async compile(request, options = {}) {
3865
+ return this.transport.compile(
3866
+ normalizeCompileRequest(request),
3867
+ normalizeCompileOptions(options)
3868
+ );
3869
+ }
3325
3870
  /**
3326
3871
  * Resolves with the run's handle once the engine admitted it, and rejects
3327
3872
  * without one when the engine refused it. The handle owns the lifecycle: