@sequenceholdings/studio-cli 0.1.13 → 0.1.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/README.md +258 -38
  2. package/dist/agents/apply-chunks.d.ts +13 -0
  3. package/dist/agents/apply-chunks.js +43 -0
  4. package/dist/agents/commands.d.ts +10 -0
  5. package/dist/agents/commands.js +218 -0
  6. package/dist/agents/scaffold.d.ts +2 -0
  7. package/dist/agents/scaffold.js +77 -0
  8. package/dist/agents/source.d.ts +18 -0
  9. package/dist/agents/source.js +121 -0
  10. package/dist/artifact/delegate.d.ts +2 -2
  11. package/dist/artifact/delegate.js +31 -73
  12. package/dist/atlas-client.js +52 -37
  13. package/dist/auth-cmds/commands.d.ts +1 -1
  14. package/dist/auth-cmds/commands.js +12 -7
  15. package/dist/auth.d.ts +104 -24
  16. package/dist/auth.js +456 -94
  17. package/dist/config.d.ts +3 -3
  18. package/dist/config.js +18 -13
  19. package/dist/env-catalog.js +13 -3
  20. package/dist/env-flags.d.ts +2 -0
  21. package/dist/env-flags.js +2 -0
  22. package/dist/env-registry.d.ts +27 -0
  23. package/dist/env-registry.js +204 -0
  24. package/dist/envs/commands.d.ts +1 -1
  25. package/dist/envs/commands.js +41 -3
  26. package/dist/file-lock.d.ts +5 -0
  27. package/dist/file-lock.js +187 -0
  28. package/dist/functions/commands.d.ts +10 -10
  29. package/dist/functions/commands.js +87 -53
  30. package/dist/functions/manifest.d.ts +1 -0
  31. package/dist/functions/manifest.js +36 -0
  32. package/dist/functions/source-selection.d.ts +24 -0
  33. package/dist/functions/source-selection.js +67 -0
  34. package/dist/login.d.ts +8 -3
  35. package/dist/login.js +46 -34
  36. package/dist/main.d.ts +3 -1
  37. package/dist/main.js +41 -12
  38. package/dist/orm/delegate.js +25 -7
  39. package/dist/pat-hints.js +2 -2
  40. package/dist/pipeline/commands.d.ts +58 -0
  41. package/dist/pipeline/commands.js +330 -0
  42. package/dist/pipeline/lifecycle.d.ts +58 -0
  43. package/dist/pipeline/lifecycle.js +348 -0
  44. package/dist/pipeline/pinning.d.ts +5 -0
  45. package/dist/pipeline/pinning.js +9 -0
  46. package/dist/pipeline/templates.d.ts +11 -0
  47. package/dist/pipeline/templates.js +166 -0
  48. package/dist/process/build.d.ts +4 -0
  49. package/dist/process/build.js +33 -2
  50. package/dist/process/codegen.js +19 -1
  51. package/dist/process/commands.js +97 -47
  52. package/dist/process/compiler-subprocess.d.ts +29 -0
  53. package/dist/process/compiler-subprocess.js +99 -0
  54. package/dist/process/compiler-worker.d.ts +1 -0
  55. package/dist/process/compiler-worker.js +38 -0
  56. package/dist/process/lint.d.ts +8 -0
  57. package/dist/process/lint.js +84 -29
  58. package/dist/process/repo-install.js +18 -2
  59. package/dist/repos/commands.d.ts +1 -1
  60. package/dist/repos/commands.js +17 -12
  61. package/dist/secrets/commands.d.ts +1 -1
  62. package/dist/secrets/commands.js +18 -18
  63. package/package.json +12 -5
@@ -11,18 +11,19 @@ import { fileURLToPath } from 'node:url';
11
11
  import { forEachSerializedNode, } from '@sequenceholdings/lattice/bundle';
12
12
  import { getJson, getJsonOr404, postJson, AtlasApiError } from '../atlas-client.js';
13
13
  import { generateProcessFiles } from './codegen.js';
14
- import { getAccessToken, NotLoggedInError } from '../auth.js';
14
+ import { getAccessToken, getAccessTokenWithMode, NotLoggedInError, } from '../auth.js';
15
15
  import { readConfig, resolveEnvWithDiscovery } from '../config.js';
16
- import { buildBundleFromProcesses, summarizeBundle } from './build.js';
16
+ import { finalizeCompiledBundle, summarizeBundle } from './build.js';
17
17
  import { buildResolveProcessPinFromEnv } from './resolve-process-pin.js';
18
18
  import { loadBundleForPublish } from './local-bundle.js';
19
- import { loadProcessDefinitions } from './discover.js';
20
- import { lintProcesses, formatLintResult } from './lint.js';
19
+ import { findProcessRoot } from './discover.js';
20
+ import { formatLintResult, lintSerializedAgentContracts, } from './lint.js';
21
21
  import { diffBundleAgainstActive, formatPlanDiff, } from './plan-diff.js';
22
- import { simulateProcess, formatSimulateResult } from './simulate.js';
22
+ import { formatSimulateResult } from './simulate.js';
23
23
  import { buildAgentSchemaLoader } from './agent-loader.js';
24
- import { resolveArtifactSource, } from '@sequenceholdings/artifact-studio/source-resolver';
24
+ import { localGitMetadata, resolveArtifactSource, } from '@sequenceholdings/artifact-studio/source-resolver';
25
25
  import { prepareProcessBuildRoot } from './repo-install.js';
26
+ import { compileProcessSourceIsolated, simulateProcessIsolated, } from './compiler-subprocess.js';
26
27
  export function parseArgs(rest) {
27
28
  const positional = [];
28
29
  const flags = {};
@@ -98,7 +99,7 @@ async function getEnvAndToken(args) {
98
99
  const env = await resolveEnvWithDiscovery({ requested });
99
100
  let token;
100
101
  try {
101
- token = await getAccessToken();
102
+ token = await getAccessToken({ env: env.name, targetUrl: env.url });
102
103
  }
103
104
  catch (err) {
104
105
  if (err instanceof NotLoggedInError) {
@@ -108,6 +109,12 @@ async function getEnvAndToken(args) {
108
109
  }
109
110
  return { env, token };
110
111
  }
112
+ async function getEnvAndAuth(args) {
113
+ const requested = typeof args.flags.env === 'string' ? args.flags.env : undefined;
114
+ const env = await resolveEnvWithDiscovery({ requested });
115
+ const auth = await getAccessTokenWithMode({ env: env.name, targetUrl: env.url });
116
+ return { authMode: auth.authMode, env, token: auth.token };
117
+ }
111
118
  /**
112
119
  * Parse the process-apply source flags. `--repo processes/<name>` materializes
113
120
  * from the platform git service; absent means local (cwd / LATTICE_PROCESSES_ROOT).
@@ -206,9 +213,11 @@ function slugify(value) {
206
213
  // lint
207
214
  // ---------------------------------------------------------------------------
208
215
  export async function lintCommand(args) {
209
- const defs = await loadProcessDefinitions();
210
- const loadAgentSchemaEdgeIdEnum = await tryBuildAgentLoader(args);
211
- const result = await lintProcesses({ defs, loadAgentSchemaEdgeIdEnum });
216
+ const compiled = await compileLocalProcessSource();
217
+ const result = await lintCompiledProcessSource({
218
+ compiled,
219
+ loadAgentSchemaEdgeIdEnum: await tryBuildAgentLoader(args),
220
+ });
212
221
  const formatted = formatLintResult(result);
213
222
  if (formatted.text)
214
223
  console.log(formatted.text);
@@ -216,9 +225,29 @@ export async function lintCommand(args) {
216
225
  console.error(`\nlint FAILED — ${result.errors.length} error(s), ${result.warnings.length} warning(s)`);
217
226
  return 1;
218
227
  }
219
- console.log(`\nlint ok — ${defs.length} process(es) checked, ${result.warnings.length} warning(s)`);
228
+ console.log(`\nlint ok — ${compiled.bundle.processes.length} process(es) checked, ${result.warnings.length} warning(s)`);
220
229
  return 0;
221
230
  }
231
+ async function compileLocalProcessSource() {
232
+ const rootDir = await findProcessRoot();
233
+ return compileProcessSourceIsolated({
234
+ rootDir,
235
+ provenance: localGitMetadata(rootDir),
236
+ });
237
+ }
238
+ async function lintCompiledProcessSource({ compiled, loadAgentSchemaEdgeIdEnum, }) {
239
+ const agentLint = await lintSerializedAgentContracts({
240
+ bundle: compiled.bundle,
241
+ loadAgentSchemaEdgeIdEnum,
242
+ // The worker already performs source-local checks. The credential-owning
243
+ // parent only adds registry-dependent validation (or the offline warning).
244
+ includeStaticValidation: false,
245
+ });
246
+ return {
247
+ errors: [...compiled.lint.errors, ...agentLint.errors],
248
+ warnings: [...compiled.lint.warnings, ...agentLint.warnings],
249
+ };
250
+ }
222
251
  async function tryBuildAgentLoader(args) {
223
252
  // Agent loader is only meaningful when we have a target env + token.
224
253
  // If either fails to resolve, skip the check; lint will emit a warning
@@ -234,23 +263,31 @@ async function tryBuildAgentLoader(args) {
234
263
  // ---------------------------------------------------------------------------
235
264
  // plan
236
265
  // ---------------------------------------------------------------------------
237
- async function buildBundleForPublish(args, defs, provenance) {
238
- const hasSubprocess = defs.some((d) => d.process.nodes.some((n) => n.kind === 'subprocess'));
266
+ async function buildBundleForPublish(args, compiled) {
267
+ let hasSubprocess = false;
268
+ for (const process of compiled.bundle.processes) {
269
+ forEachSerializedNode(process.nodes, (node) => {
270
+ if (node.kind === 'subprocess')
271
+ hasSubprocess = true;
272
+ });
273
+ }
274
+ if (!hasSubprocess)
275
+ return compiled.bundle;
239
276
  try {
240
277
  const { env, token } = await getEnvAndToken(args);
241
278
  const resolveProcessPin = buildResolveProcessPinFromEnv(env, token);
242
- return await buildBundleFromProcesses(defs, { resolveProcessPin, provenance });
279
+ return await finalizeCompiledBundle({
280
+ bundle: compiled.bundle,
281
+ resolveProcessPin,
282
+ });
243
283
  }
244
284
  catch (err) {
245
- if (hasSubprocess) {
246
- throw new Error('subprocess nodes require -e <env> and `seq-studio login` to resolve child process versions', { cause: err });
247
- }
248
- return await buildBundleFromProcesses(defs, { provenance });
285
+ throw new Error('subprocess nodes require -e <env> and `seq-studio login` to resolve child process versions', { cause: err });
249
286
  }
250
287
  }
251
288
  export async function planCommand(args) {
252
- const defs = await loadProcessDefinitions();
253
- const bundle = await buildBundleForPublish(args, defs);
289
+ const compiled = await compileLocalProcessSource();
290
+ const bundle = await buildBundleForPublish(args, compiled);
254
291
  const summary = summarizeBundle(bundle);
255
292
  console.log(JSON.stringify(summary, null, 2));
256
293
  const loadActiveProcess = await tryBuildActiveProcessLoader(args);
@@ -302,9 +339,11 @@ async function tryBuildActiveProcessLoader(args) {
302
339
  // test (CI wrapper)
303
340
  // ---------------------------------------------------------------------------
304
341
  export async function testCommand(args) {
305
- const defs = await loadProcessDefinitions();
306
- const loadAgentSchemaEdgeIdEnum = await tryBuildAgentLoader(args);
307
- const lintResult = await lintProcesses({ defs, loadAgentSchemaEdgeIdEnum });
342
+ const compiled = await compileLocalProcessSource();
343
+ const lintResult = await lintCompiledProcessSource({
344
+ compiled,
345
+ loadAgentSchemaEdgeIdEnum: await tryBuildAgentLoader(args),
346
+ });
308
347
  const formattedLint = formatLintResult(lintResult);
309
348
  if (formattedLint.text)
310
349
  console.log(formattedLint.text);
@@ -312,8 +351,8 @@ export async function testCommand(args) {
312
351
  console.error(`\nlint FAILED — ${lintResult.errors.length} error(s), ${lintResult.warnings.length} warning(s)`);
313
352
  return 1;
314
353
  }
315
- console.log(`\nlint ok — ${defs.length} process(es) checked, ${lintResult.warnings.length} warning(s)`);
316
- const bundle = await buildBundleForPublish(args, defs);
354
+ console.log(`\nlint ok — ${compiled.bundle.processes.length} process(es) checked, ${lintResult.warnings.length} warning(s)`);
355
+ const bundle = await buildBundleForPublish(args, compiled);
317
356
  const offline = args.flags.offline === true;
318
357
  const loadActiveProcess = await tryBuildActiveProcessLoader(args);
319
358
  if (!loadActiveProcess) {
@@ -376,32 +415,34 @@ export async function applyCommand(args) {
376
415
  return applyLocal(args);
377
416
  }
378
417
  async function applyLocal(args) {
418
+ const compiled = await compileLocalProcessSource();
379
419
  const { env, token } = await getEnvAndToken(args);
380
- const defs = await loadProcessDefinitions();
381
- return registerAndPromote({ args, env, token, defs });
420
+ return registerAndPromote({ args, env, token, compiled });
382
421
  }
383
422
  async function applyFromRepo(args, spec) {
384
- const { env, token } = await getEnvAndToken(args);
385
- const source = await resolveArtifactSource({ kind: 'git-service', namespace: spec.namespace, name: spec.name, ref: spec.ref }, { baseUrl: env.url, token });
423
+ const { authMode, env, token } = await getEnvAndAuth(args);
424
+ const source = await resolveArtifactSource({ kind: 'git-service', namespace: spec.namespace, name: spec.name, ref: spec.ref }, { authMode, baseUrl: env.url, token });
386
425
  try {
387
426
  await prepareProcessBuildRoot(source.dir);
388
- const defs = await loadProcessDefinitions(source.dir);
427
+ const compiled = await compileProcessSourceIsolated({
428
+ rootDir: source.dir,
429
+ provenance: source.provenance,
430
+ });
389
431
  return await registerAndPromote({
390
432
  args,
391
433
  env,
392
434
  token,
393
- defs,
394
- provenance: source.provenance,
435
+ compiled,
395
436
  });
396
437
  }
397
438
  finally {
398
439
  await source.cleanup();
399
440
  }
400
441
  }
401
- async function registerAndPromote({ args, env, token, defs, provenance, }) {
442
+ async function registerAndPromote({ args, env, token, compiled, }) {
402
443
  const onlyResult = resolveOnlyIds({
403
444
  only: args.flags.only,
404
- knownIds: new Set(defs.map((d) => d.process.id)),
445
+ knownIds: new Set(compiled.bundle.processes.map((process) => process.id)),
405
446
  });
406
447
  if (onlyResult.error) {
407
448
  console.error(onlyResult.error);
@@ -412,7 +453,10 @@ async function registerAndPromote({ args, env, token, defs, provenance, }) {
412
453
  // again on POST /bundles; this is a local fast-fail so we don't ship
413
454
  // a bundle that will be rejected.
414
455
  const loadAgentSchemaEdgeIdEnum = buildAgentSchemaLoader({ baseUrl: env.url, token });
415
- const lintResult = await lintProcesses({ defs, loadAgentSchemaEdgeIdEnum });
456
+ const lintResult = await lintCompiledProcessSource({
457
+ compiled,
458
+ loadAgentSchemaEdgeIdEnum,
459
+ });
416
460
  const formatted = formatLintResult(lintResult);
417
461
  if (formatted.text)
418
462
  console.log(formatted.text);
@@ -420,7 +464,7 @@ async function registerAndPromote({ args, env, token, defs, provenance, }) {
420
464
  console.error('\napply FAILED — lint rejected the bundle');
421
465
  return 1;
422
466
  }
423
- const bundle = await buildBundleForPublish(args, defs, provenance);
467
+ const bundle = await buildBundleForPublish(args, compiled);
424
468
  console.log(`[seq-studio] built bundle ${bundle.bundle_hash.slice(0, 12)} ` +
425
469
  `(${bundle.processes.length} process(es))`);
426
470
  // We send only the bundle. `created_by` (and `promoted_by` for the
@@ -569,8 +613,8 @@ export async function bundleCommand(args) {
569
613
  const sub = args.positional[0];
570
614
  switch (sub) {
571
615
  case 'build': {
572
- const defs = await loadProcessDefinitions();
573
- const bundle = await buildBundleForPublish(args, defs);
616
+ const compiled = await compileLocalProcessSource();
617
+ const bundle = await buildBundleForPublish(args, compiled);
574
618
  const out = typeof args.flags.out === 'string' ? resolve(args.flags.out) : null;
575
619
  if (out) {
576
620
  await mkdir(dirname(out), { recursive: true });
@@ -705,14 +749,15 @@ export async function simulateCommand(args) {
705
749
  console.error('usage: seq-studio process simulate <process-id>');
706
750
  return 1;
707
751
  }
708
- const defs = await loadProcessDefinitions();
709
- const def = defs.find((d) => d.process.id === processId);
710
- if (!def) {
711
- console.error(`process "${processId}" not found in lattice process roots`);
752
+ const simulation = await simulateProcessIsolated({
753
+ rootDir: await findProcessRoot(),
754
+ processId,
755
+ });
756
+ if (simulation.kind === 'not-found') {
757
+ console.error(`process "${simulation.processId}" not found in lattice process roots`);
712
758
  return 1;
713
759
  }
714
- const processesById = new Map(defs.map((d) => [d.process.id, d.process]));
715
- const result = await simulateProcess({ process: def.process, processesById });
760
+ const result = simulation.result;
716
761
  console.log(formatSimulateResult(result));
717
762
  return result.status === 'failed' ? 1 : 0;
718
763
  }
@@ -748,7 +793,7 @@ export async function doctorCommand(args) {
748
793
  }
749
794
  let token = null;
750
795
  try {
751
- token = await getAccessToken();
796
+ token = await getAccessToken({ env: env?.name, targetUrl: env?.url });
752
797
  lines.push('auth: ok — seqapi token present and not expired');
753
798
  }
754
799
  catch (err) {
@@ -812,9 +857,14 @@ export async function doctorCommand(args) {
812
857
  const currentTotal = current.total ?? current.items.length;
813
858
  if (currentTotal === 0 && env.name !== 'staging' && config?.envs.staging?.url) {
814
859
  try {
860
+ const stagingUrl = config.envs.staging.url;
861
+ const stagingToken = await getAccessToken({
862
+ env: 'staging',
863
+ targetUrl: stagingUrl,
864
+ });
815
865
  const staging = await getJson({
816
- baseUrl: config.envs.staging.url,
817
- token,
866
+ baseUrl: stagingUrl,
867
+ token: stagingToken,
818
868
  path: '/api/git-service/repos?limit=1&offset=0',
819
869
  });
820
870
  const stagingTotal = staging.total ?? staging.items.length;
@@ -0,0 +1,29 @@
1
+ import type { LatticeBundle } from '@sequenceholdings/lattice/bundle';
2
+ import type { BuildBundleOptions } from './build.js';
3
+ import type { LintResult } from './lint.js';
4
+ import type { SimulateResult } from './simulate.js';
5
+ export interface CompiledProcessSource {
6
+ bundle: LatticeBundle;
7
+ lint: LintResult;
8
+ }
9
+ export type IsolatedSimulation = {
10
+ kind: 'not-found';
11
+ processId: string;
12
+ } | {
13
+ kind: 'result';
14
+ result: SimulateResult;
15
+ };
16
+ export declare function compileProcessSourceIsolated({ rootDir, provenance, workerPath, timeoutMs, sourceEnv, }: {
17
+ rootDir: string;
18
+ provenance?: NonNullable<BuildBundleOptions['provenance']>;
19
+ workerPath?: string;
20
+ timeoutMs?: number;
21
+ sourceEnv?: NodeJS.ProcessEnv;
22
+ }): Promise<CompiledProcessSource>;
23
+ export declare function simulateProcessIsolated({ rootDir, processId, workerPath, timeoutMs, sourceEnv, }: {
24
+ rootDir: string;
25
+ processId: string;
26
+ workerPath?: string;
27
+ timeoutMs?: number;
28
+ sourceEnv?: NodeJS.ProcessEnv;
29
+ }): Promise<IsolatedSimulation>;
@@ -0,0 +1,99 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { existsSync } from 'node:fs';
3
+ import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
4
+ import { tmpdir } from 'node:os';
5
+ import { dirname, join } from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { createScrubbedChildEnv } from '@sequenceholdings/artifact-studio/child-environment';
8
+ const MODULE_DIR = dirname(fileURLToPath(import.meta.url));
9
+ const BUILT_WORKER_PATH = join(MODULE_DIR, 'compiler-worker.js');
10
+ const DEFAULT_WORKER_PATH = existsSync(BUILT_WORKER_PATH)
11
+ ? BUILT_WORKER_PATH
12
+ : join(MODULE_DIR, 'compiler-worker.ts');
13
+ const DEFAULT_TIMEOUT_MS = 2 * 60_000;
14
+ const MAX_STDERR_BYTES = 1024 * 1024;
15
+ export async function compileProcessSourceIsolated({ rootDir, provenance, workerPath = DEFAULT_WORKER_PATH, timeoutMs = DEFAULT_TIMEOUT_MS, sourceEnv, }) {
16
+ return runProcessWorker({
17
+ request: {
18
+ action: 'compile',
19
+ rootDir,
20
+ ...(provenance ? { provenance } : {}),
21
+ },
22
+ workerPath,
23
+ timeoutMs,
24
+ ...(sourceEnv ? { sourceEnv } : {}),
25
+ });
26
+ }
27
+ export async function simulateProcessIsolated({ rootDir, processId, workerPath = DEFAULT_WORKER_PATH, timeoutMs = DEFAULT_TIMEOUT_MS, sourceEnv, }) {
28
+ return runProcessWorker({
29
+ request: { action: 'simulate', rootDir, processId },
30
+ workerPath,
31
+ timeoutMs,
32
+ ...(sourceEnv ? { sourceEnv } : {}),
33
+ });
34
+ }
35
+ async function runProcessWorker({ request, workerPath, timeoutMs, sourceEnv, }) {
36
+ const scratch = await mkdtemp(join(tmpdir(), 'process-compiler-'));
37
+ const requestFile = join(scratch, 'request.json');
38
+ const outFile = join(scratch, 'result.json');
39
+ const homeDir = join(scratch, 'home');
40
+ try {
41
+ await mkdir(homeDir, { recursive: true });
42
+ await writeFile(requestFile, JSON.stringify(request), 'utf8');
43
+ await runWorker({
44
+ workerPath,
45
+ requestFile,
46
+ outFile,
47
+ homeDir,
48
+ timeoutMs,
49
+ ...(sourceEnv ? { sourceEnv } : {}),
50
+ });
51
+ return JSON.parse(await readFile(outFile, 'utf8'));
52
+ }
53
+ finally {
54
+ await rm(scratch, { recursive: true, force: true });
55
+ }
56
+ }
57
+ function runWorker({ workerPath, requestFile, outFile, homeDir, timeoutMs, sourceEnv, }) {
58
+ return new Promise((resolve, reject) => {
59
+ const workerArgs = workerPath.endsWith('.ts')
60
+ ? ['--import', 'tsx', workerPath, requestFile, outFile]
61
+ : [workerPath, requestFile, outFile];
62
+ const child = spawn(process.execPath, workerArgs, {
63
+ stdio: ['ignore', 'inherit', 'pipe'],
64
+ env: createScrubbedChildEnv({
65
+ homeDir,
66
+ ...(sourceEnv ? { source: sourceEnv } : {}),
67
+ }),
68
+ });
69
+ let stderr = '';
70
+ let timedOut = false;
71
+ const timer = setTimeout(() => {
72
+ timedOut = true;
73
+ child.kill('SIGKILL');
74
+ }, timeoutMs);
75
+ child.stderr.on('data', (chunk) => {
76
+ if (stderr.length < MAX_STDERR_BYTES) {
77
+ stderr += chunk.toString().slice(0, MAX_STDERR_BYTES - stderr.length);
78
+ }
79
+ });
80
+ child.on('error', (error) => {
81
+ clearTimeout(timer);
82
+ reject(error);
83
+ });
84
+ child.on('close', (code) => {
85
+ clearTimeout(timer);
86
+ if (timedOut) {
87
+ reject(new Error(`process compiler exceeded ${Math.round(timeoutMs / 1000)}s and was terminated`));
88
+ return;
89
+ }
90
+ if (code === 0) {
91
+ if (stderr.trim())
92
+ process.stderr.write(stderr);
93
+ resolve();
94
+ return;
95
+ }
96
+ reject(new Error(stderr.trim() || `process compiler exited with code ${code ?? 'null'}`));
97
+ });
98
+ });
99
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,38 @@
1
+ import { readFile, writeFile } from 'node:fs/promises';
2
+ import { buildBundleFromProcesses } from './build.js';
3
+ import { loadProcessDefinitions } from './discover.js';
4
+ import { lintProcesses } from './lint.js';
5
+ import { simulateProcess } from './simulate.js';
6
+ async function main() {
7
+ const [requestFile, outFile] = process.argv.slice(2);
8
+ if (!requestFile || !outFile) {
9
+ process.stderr.write('usage: compiler-worker <request.json> <result.json>\n');
10
+ process.exitCode = 2;
11
+ return;
12
+ }
13
+ const request = JSON.parse(await readFile(requestFile, 'utf8'));
14
+ const defs = await loadProcessDefinitions(request.rootDir);
15
+ if (request.action === 'simulate') {
16
+ const process = defs.find((definition) => definition.process.id === request.processId)?.process;
17
+ if (!process) {
18
+ await writeFile(outFile, JSON.stringify({ kind: 'not-found', processId: request.processId }), 'utf8');
19
+ return;
20
+ }
21
+ const processesById = new Map(defs.map((definition) => [definition.process.id, definition.process]));
22
+ const result = await simulateProcess({ process, processesById });
23
+ await writeFile(outFile, JSON.stringify({ kind: 'result', result }), 'utf8');
24
+ return;
25
+ }
26
+ const lint = await lintProcesses({
27
+ defs,
28
+ suppressUnavailableAgentWarnings: true,
29
+ });
30
+ const bundle = await buildBundleFromProcesses(defs, {
31
+ ...(request.provenance ? { provenance: request.provenance } : {}),
32
+ });
33
+ await writeFile(outFile, JSON.stringify({ bundle, lint }), 'utf8');
34
+ }
35
+ main().catch((error) => {
36
+ process.stderr.write(`${error instanceof Error ? (error.stack ?? error.message) : String(error)}\n`);
37
+ process.exitCode = 1;
38
+ });
@@ -9,6 +9,7 @@
9
9
  * - Human timeout edge — if timeout is set and outgoing_edges.length > 0
10
10
  * but on_timeout_edge_id is unset, warn
11
11
  */
12
+ import { type LatticeBundle } from '@sequenceholdings/lattice/bundle';
12
13
  import type { ProcessDefinition } from '@sequenceholdings/lattice/define';
13
14
  export interface LintIssue {
14
15
  process_id: string;
@@ -31,8 +32,15 @@ export interface LintInput {
31
32
  * undefined and a warning is emitted instead of failing the build.
32
33
  */
33
34
  loadAgentSchemaEdgeIdEnum?: (agentId: string) => Promise<string[] | null | 'not-found'>;
35
+ /** A scrubbed worker can defer registry-backed agent checks to its parent. */
36
+ suppressUnavailableAgentWarnings?: boolean;
34
37
  }
35
38
  export declare function lintProcesses(input: LintInput): Promise<LintResult>;
39
+ export declare function lintSerializedAgentContracts({ bundle, loadAgentSchemaEdgeIdEnum, includeStaticValidation, }: {
40
+ bundle: LatticeBundle;
41
+ loadAgentSchemaEdgeIdEnum?: LintInput['loadAgentSchemaEdgeIdEnum'];
42
+ includeStaticValidation?: boolean;
43
+ }): Promise<LintResult>;
36
44
  export declare function formatLintResult(result: LintResult): {
37
45
  text: string;
38
46
  hasErrors: boolean;
@@ -10,6 +10,7 @@
10
10
  * but on_timeout_edge_id is unset, warn
11
11
  */
12
12
  import { readFileSync } from 'node:fs';
13
+ import { forEachSerializedNode, } from '@sequenceholdings/lattice/bundle';
13
14
  import { parallelSubNodes, validateEmailReminders } from '@sequenceholdings/lattice/define';
14
15
  export async function lintProcesses(input) {
15
16
  const errors = [];
@@ -67,7 +68,7 @@ export async function lintProcesses(input) {
67
68
  }
68
69
  }
69
70
  for (const node of p.nodes) {
70
- await lintNodeContract(p.id, node, errors, warnings, input.loadAgentSchemaEdgeIdEnum);
71
+ await lintNodeContract(p.id, node, errors, warnings, input.loadAgentSchemaEdgeIdEnum, input.suppressUnavailableAgentWarnings ?? false);
71
72
  }
72
73
  // Closure-scope check: mappers / fn / join / fan_out / output are
73
74
  // serialized via `.toString()` and run in a vm with NOTHING from the
@@ -213,10 +214,10 @@ function lintSandboxClosures(processId, node, importedNames, warnings) {
213
214
  * edge_id-contract checks as a top-level node) and lints the `join` reducer's
214
215
  * own edge_id returns against the parallel node's outgoing_edges.
215
216
  */
216
- async function lintNodeContract(processId, node, errors, warnings, loadAgentSchemaEdgeIdEnum) {
217
+ async function lintNodeContract(processId, node, errors, warnings, loadAgentSchemaEdgeIdEnum, suppressUnavailableAgentWarnings) {
217
218
  switch (node.kind) {
218
219
  case 'agent':
219
- await lintAgent(processId, node, errors, warnings, loadAgentSchemaEdgeIdEnum);
220
+ await lintAgent(processId, node, errors, warnings, loadAgentSchemaEdgeIdEnum, suppressUnavailableAgentWarnings);
220
221
  break;
221
222
  case 'human':
222
223
  lintHuman(processId, node, errors, warnings);
@@ -225,7 +226,7 @@ async function lintNodeContract(processId, node, errors, warnings, loadAgentSche
225
226
  const parallel = node;
226
227
  lintParallelJoin(processId, parallel, errors, warnings);
227
228
  for (const { node: sub } of parallelSubNodes(parallel)) {
228
- await lintNodeContract(processId, sub, errors, warnings, loadAgentSchemaEdgeIdEnum);
229
+ await lintNodeContract(processId, sub, errors, warnings, loadAgentSchemaEdgeIdEnum, suppressUnavailableAgentWarnings);
229
230
  }
230
231
  break;
231
232
  }
@@ -335,28 +336,74 @@ function lintSubprocessOutput(processId, node, errors, warnings) {
335
336
  }
336
337
  }
337
338
  const EDGE_ID_LITERAL_RE = /\bedge_id\s*:\s*['"`]([^'"`]+)['"`]/g;
338
- async function lintAgent(processId, node, errors, warnings, loadAgentSchemaEdgeIdEnum) {
339
- const allEdgeIds = node.outgoing_edges.map((e) => e.id);
340
- // The reset edge (taken out-of-band by the reset intervention, never chosen
341
- // by the agent) must exist on the node but is excluded from the agent's
342
- // edge_id contract same way a human node's on_timeout_edge_id is an
343
- // orchestration-only edge. Validate membership, then verify the agent's
344
- // edge_id enum against the REAL (non-reset) edges only.
345
- if (node.on_reset_edge_id) {
346
- if (!allEdgeIds.includes(node.on_reset_edge_id)) {
347
- errors.push({
348
- process_id: processId,
349
- node_id: node.id,
350
- message: `on_reset_edge_id "${node.on_reset_edge_id}" is not in outgoing_edges`,
351
- });
352
- }
339
+ export async function lintSerializedAgentContracts({ bundle, loadAgentSchemaEdgeIdEnum, includeStaticValidation = true, }) {
340
+ const errors = [];
341
+ const warnings = [];
342
+ const pending = [];
343
+ for (const process of bundle.processes) {
344
+ forEachSerializedNode(process.nodes, (node) => {
345
+ const contract = serializedAgentContract(node);
346
+ if (!contract)
347
+ return;
348
+ pending.push(lintAgentContract({
349
+ processId: process.id,
350
+ nodeId: node.id,
351
+ ...contract,
352
+ errors,
353
+ warnings,
354
+ loadAgentSchemaEdgeIdEnum,
355
+ suppressUnavailableAgentWarnings: false,
356
+ includeStaticValidation,
357
+ }));
358
+ });
353
359
  }
354
- const outgoingIds = allEdgeIds.filter((id) => id !== node.on_reset_edge_id);
360
+ await Promise.all(pending);
361
+ return { errors, warnings };
362
+ }
363
+ function serializedAgentContract(node) {
364
+ if (node.kind !== 'agent' || !isUnknownRecord(node.metadata))
365
+ return null;
366
+ const agentId = node.metadata.agent_id;
367
+ if (typeof agentId !== 'string')
368
+ return null;
369
+ const onResetEdgeId = node.metadata.on_reset_edge_id;
370
+ return {
371
+ agentId,
372
+ allEdgeIds: node.outgoing_edges.map((edge) => edge.id),
373
+ ...(typeof onResetEdgeId === 'string' ? { onResetEdgeId } : {}),
374
+ };
375
+ }
376
+ function isUnknownRecord(value) {
377
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
378
+ }
379
+ async function lintAgent(processId, node, errors, warnings, loadAgentSchemaEdgeIdEnum, suppressUnavailableAgentWarnings) {
380
+ await lintAgentContract({
381
+ processId,
382
+ nodeId: node.id,
383
+ agentId: node.agent.id,
384
+ allEdgeIds: node.outgoing_edges.map((edge) => edge.id),
385
+ onResetEdgeId: node.on_reset_edge_id,
386
+ errors,
387
+ warnings,
388
+ loadAgentSchemaEdgeIdEnum,
389
+ suppressUnavailableAgentWarnings,
390
+ includeStaticValidation: true,
391
+ });
392
+ }
393
+ async function lintAgentContract({ processId, nodeId, agentId, allEdgeIds, onResetEdgeId, errors, warnings, loadAgentSchemaEdgeIdEnum, suppressUnavailableAgentWarnings, includeStaticValidation, }) {
394
+ if (includeStaticValidation && onResetEdgeId && !allEdgeIds.includes(onResetEdgeId)) {
395
+ errors.push({
396
+ process_id: processId,
397
+ node_id: nodeId,
398
+ message: `on_reset_edge_id "${onResetEdgeId}" is not in outgoing_edges`,
399
+ });
400
+ }
401
+ const outgoingIds = allEdgeIds.filter((id) => id !== onResetEdgeId);
355
402
  if (!loadAgentSchemaEdgeIdEnum) {
356
- if (outgoingIds.length > 1) {
403
+ if (outgoingIds.length > 1 && !suppressUnavailableAgentWarnings) {
357
404
  warnings.push({
358
405
  process_id: processId,
359
- node_id: node.id,
406
+ node_id: nodeId,
360
407
  message: `agent node has ${outgoingIds.length} outgoing edges; agent output schema vs outgoing_edges contract not verified (no agent registry loader available)`,
361
408
  });
362
409
  }
@@ -364,21 +411,21 @@ async function lintAgent(processId, node, errors, warnings, loadAgentSchemaEdgeI
364
411
  }
365
412
  let enumValues;
366
413
  try {
367
- enumValues = await loadAgentSchemaEdgeIdEnum(node.agent.id);
414
+ enumValues = await loadAgentSchemaEdgeIdEnum(agentId);
368
415
  }
369
416
  catch (err) {
370
417
  warnings.push({
371
418
  process_id: processId,
372
- node_id: node.id,
373
- message: `could not load agent definition "${node.agent.id}" to verify output contract: ${err instanceof Error ? err.message : String(err)} (run with DB access to enable full check)`,
419
+ node_id: nodeId,
420
+ message: `could not load agent definition "${agentId}" to verify output contract: ${err instanceof Error ? err.message : String(err)} (run with DB access to enable full check)`,
374
421
  });
375
422
  return;
376
423
  }
377
424
  if (enumValues === 'not-found') {
378
425
  errors.push({
379
426
  process_id: processId,
380
- node_id: node.id,
381
- message: `agent "${node.agent.id}" not found in the target env — sync/seed the agent registry before applying`,
427
+ node_id: nodeId,
428
+ message: `agent "${agentId}" not found in the target env — sync/seed the agent registry before applying`,
382
429
  });
383
430
  return;
384
431
  }
@@ -397,8 +444,8 @@ async function lintAgent(processId, node, errors, warnings, loadAgentSchemaEdgeI
397
444
  if (missing.length > 0 || extra.length > 0) {
398
445
  errors.push({
399
446
  process_id: processId,
400
- node_id: node.id,
401
- message: `agent "${node.agent.id}" edge_id enum mismatch — missing: [${missing.join(', ')}], extra: [${extra.join(', ')}]; expected exactly: [${outgoingIds.join(', ')}]`,
447
+ node_id: nodeId,
448
+ message: `agent "${agentId}" edge_id enum mismatch — missing: [${missing.join(', ')}], extra: [${extra.join(', ')}]; expected exactly: [${outgoingIds.join(', ')}]`,
402
449
  });
403
450
  }
404
451
  }
@@ -502,6 +549,14 @@ function lintHuman(processId, node, errors, warnings) {
502
549
  }
503
550
  }
504
551
  if (node.on_timeout_edge_id) {
552
+ if (node.timeout === undefined) {
553
+ errors.push({
554
+ process_id: processId,
555
+ node_id: node.id,
556
+ message: `on_timeout_edge_id "${node.on_timeout_edge_id}" requires timeout — ` +
557
+ `omit on_timeout_edge_id for an indefinite wait, or set timeout to enable the timeout edge`,
558
+ });
559
+ }
505
560
  const ok = node.outgoing_edges.some((e) => e.id === node.on_timeout_edge_id);
506
561
  if (!ok) {
507
562
  errors.push({