@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.
- package/README.md +258 -38
- package/dist/agents/apply-chunks.d.ts +13 -0
- package/dist/agents/apply-chunks.js +43 -0
- package/dist/agents/commands.d.ts +10 -0
- package/dist/agents/commands.js +218 -0
- package/dist/agents/scaffold.d.ts +2 -0
- package/dist/agents/scaffold.js +77 -0
- package/dist/agents/source.d.ts +18 -0
- package/dist/agents/source.js +121 -0
- package/dist/artifact/delegate.d.ts +2 -2
- package/dist/artifact/delegate.js +31 -73
- package/dist/atlas-client.js +52 -37
- package/dist/auth-cmds/commands.d.ts +1 -1
- package/dist/auth-cmds/commands.js +12 -7
- package/dist/auth.d.ts +104 -24
- package/dist/auth.js +456 -94
- package/dist/config.d.ts +3 -3
- package/dist/config.js +18 -13
- package/dist/env-catalog.js +13 -3
- package/dist/env-flags.d.ts +2 -0
- package/dist/env-flags.js +2 -0
- package/dist/env-registry.d.ts +27 -0
- package/dist/env-registry.js +204 -0
- package/dist/envs/commands.d.ts +1 -1
- package/dist/envs/commands.js +41 -3
- package/dist/file-lock.d.ts +5 -0
- package/dist/file-lock.js +187 -0
- package/dist/functions/commands.d.ts +10 -10
- package/dist/functions/commands.js +87 -53
- package/dist/functions/manifest.d.ts +1 -0
- package/dist/functions/manifest.js +36 -0
- package/dist/functions/source-selection.d.ts +24 -0
- package/dist/functions/source-selection.js +67 -0
- package/dist/login.d.ts +8 -3
- package/dist/login.js +46 -34
- package/dist/main.d.ts +3 -1
- package/dist/main.js +41 -12
- package/dist/orm/delegate.js +25 -7
- package/dist/pat-hints.js +2 -2
- package/dist/pipeline/commands.d.ts +58 -0
- package/dist/pipeline/commands.js +330 -0
- package/dist/pipeline/lifecycle.d.ts +58 -0
- package/dist/pipeline/lifecycle.js +348 -0
- package/dist/pipeline/pinning.d.ts +5 -0
- package/dist/pipeline/pinning.js +9 -0
- package/dist/pipeline/templates.d.ts +11 -0
- package/dist/pipeline/templates.js +166 -0
- package/dist/process/build.d.ts +4 -0
- package/dist/process/build.js +33 -2
- package/dist/process/codegen.js +19 -1
- package/dist/process/commands.js +97 -47
- package/dist/process/compiler-subprocess.d.ts +29 -0
- package/dist/process/compiler-subprocess.js +99 -0
- package/dist/process/compiler-worker.d.ts +1 -0
- package/dist/process/compiler-worker.js +38 -0
- package/dist/process/lint.d.ts +8 -0
- package/dist/process/lint.js +84 -29
- package/dist/process/repo-install.js +18 -2
- package/dist/repos/commands.d.ts +1 -1
- package/dist/repos/commands.js +17 -12
- package/dist/secrets/commands.d.ts +1 -1
- package/dist/secrets/commands.js +18 -18
- package/package.json +12 -5
package/dist/process/commands.js
CHANGED
|
@@ -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 {
|
|
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 {
|
|
20
|
-
import {
|
|
19
|
+
import { findProcessRoot } from './discover.js';
|
|
20
|
+
import { formatLintResult, lintSerializedAgentContracts, } from './lint.js';
|
|
21
21
|
import { diffBundleAgainstActive, formatPlanDiff, } from './plan-diff.js';
|
|
22
|
-
import {
|
|
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
|
|
210
|
-
const
|
|
211
|
-
|
|
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 — ${
|
|
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,
|
|
238
|
-
|
|
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
|
|
279
|
+
return await finalizeCompiledBundle({
|
|
280
|
+
bundle: compiled.bundle,
|
|
281
|
+
resolveProcessPin,
|
|
282
|
+
});
|
|
243
283
|
}
|
|
244
284
|
catch (err) {
|
|
245
|
-
|
|
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
|
|
253
|
-
const bundle = await buildBundleForPublish(args,
|
|
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
|
|
306
|
-
const
|
|
307
|
-
|
|
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 — ${
|
|
316
|
-
const bundle = await buildBundleForPublish(args,
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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,
|
|
442
|
+
async function registerAndPromote({ args, env, token, compiled, }) {
|
|
402
443
|
const onlyResult = resolveOnlyIds({
|
|
403
444
|
only: args.flags.only,
|
|
404
|
-
knownIds: new Set(
|
|
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
|
|
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,
|
|
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
|
|
573
|
-
const bundle = await buildBundleForPublish(args,
|
|
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
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
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
|
|
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:
|
|
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
|
+
});
|
package/dist/process/lint.d.ts
CHANGED
|
@@ -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;
|
package/dist/process/lint.js
CHANGED
|
@@ -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
|
|
339
|
-
const
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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
|
-
|
|
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:
|
|
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(
|
|
414
|
+
enumValues = await loadAgentSchemaEdgeIdEnum(agentId);
|
|
368
415
|
}
|
|
369
416
|
catch (err) {
|
|
370
417
|
warnings.push({
|
|
371
418
|
process_id: processId,
|
|
372
|
-
node_id:
|
|
373
|
-
message: `could not load agent definition "${
|
|
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:
|
|
381
|
-
message: `agent "${
|
|
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:
|
|
401
|
-
message: `agent "${
|
|
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({
|