@sequenceholdings/studio-cli 0.1.9
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 -0
- package/dist/artifact/delegate.d.ts +25 -0
- package/dist/artifact/delegate.js +263 -0
- package/dist/atlas-client.d.ts +44 -0
- package/dist/atlas-client.js +173 -0
- package/dist/auth-cmds/commands.d.ts +15 -0
- package/dist/auth-cmds/commands.js +249 -0
- package/dist/auth.d.ts +26 -0
- package/dist/auth.js +171 -0
- package/dist/bin.d.ts +2 -0
- package/dist/bin.js +8 -0
- package/dist/cli-errors.d.ts +5 -0
- package/dist/cli-errors.js +78 -0
- package/dist/config.d.ts +44 -0
- package/dist/config.js +103 -0
- package/dist/env-flags.d.ts +8 -0
- package/dist/env-flags.js +47 -0
- package/dist/functions/bundle.d.ts +30 -0
- package/dist/functions/bundle.js +137 -0
- package/dist/functions/commands.d.ts +86 -0
- package/dist/functions/commands.js +999 -0
- package/dist/functions/egress-preview.d.ts +32 -0
- package/dist/functions/egress-preview.js +54 -0
- package/dist/functions/lockfile-origin.d.ts +16 -0
- package/dist/functions/lockfile-origin.js +45 -0
- package/dist/functions/manifest.d.ts +89 -0
- package/dist/functions/manifest.js +586 -0
- package/dist/functions/secret-reconcile.d.ts +79 -0
- package/dist/functions/secret-reconcile.js +86 -0
- package/dist/main.d.ts +14 -0
- package/dist/main.js +129 -0
- package/dist/orm/delegate.d.ts +8 -0
- package/dist/orm/delegate.js +61 -0
- package/dist/pat-hints.d.ts +17 -0
- package/dist/pat-hints.js +28 -0
- package/dist/preview.d.ts +89 -0
- package/dist/preview.js +291 -0
- package/dist/process/agent-loader.d.ts +24 -0
- package/dist/process/agent-loader.js +57 -0
- package/dist/process/build.d.ts +14 -0
- package/dist/process/build.js +368 -0
- package/dist/process/codegen.d.ts +18 -0
- package/dist/process/codegen.js +270 -0
- package/dist/process/commands.d.ts +47 -0
- package/dist/process/commands.js +786 -0
- package/dist/process/discover.d.ts +32 -0
- package/dist/process/discover.js +131 -0
- package/dist/process/lint.d.ts +39 -0
- package/dist/process/lint.js +485 -0
- package/dist/process/local-bundle.d.ts +17 -0
- package/dist/process/local-bundle.js +65 -0
- package/dist/process/plan-diff.d.ts +82 -0
- package/dist/process/plan-diff.js +333 -0
- package/dist/process/resolve-process-pin.d.ts +11 -0
- package/dist/process/resolve-process-pin.js +63 -0
- package/dist/process/simulate.d.ts +50 -0
- package/dist/process/simulate.js +328 -0
- package/dist/prompt.d.ts +35 -0
- package/dist/prompt.js +65 -0
- package/dist/repos/commands.d.ts +49 -0
- package/dist/repos/commands.js +548 -0
- package/dist/repos/git-clone.d.ts +10 -0
- package/dist/repos/git-clone.js +49 -0
- package/dist/secrets/commands.d.ts +24 -0
- package/dist/secrets/commands.js +704 -0
- package/dist/templates/process/example-process/process.ts +43 -0
- package/dist/templates/process/package.json +23 -0
- package/dist/templates/process/pnpm-workspace.yaml +21 -0
- package/dist/templates/process/tsconfig.json +17 -0
- package/package.json +78 -0
- package/templates/process/example-process/process.ts +43 -0
- package/templates/process/package.json +23 -0
- package/templates/process/pnpm-workspace.yaml +21 -0
- package/templates/process/tsconfig.json +17 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Load a lattice bundle from disk for `bundle publish`. Mirrors the
|
|
3
|
+
* `seq-studio bundle publish` local store layout.
|
|
4
|
+
*/
|
|
5
|
+
import { type LatticeBundle } from '@sequenceholdings/lattice/bundle';
|
|
6
|
+
export declare function resolveLocalBundleRoot(): string;
|
|
7
|
+
/**
|
|
8
|
+
* Hash-on-read integrity check — same contract as LocalFsBundleStore.get()
|
|
9
|
+
* so hand-edited bundle.json files fail locally before POST /bundles.
|
|
10
|
+
*/
|
|
11
|
+
export declare function validateBundleIntegrity({ bundle, context, }: {
|
|
12
|
+
bundle: LatticeBundle;
|
|
13
|
+
context: string;
|
|
14
|
+
}): void;
|
|
15
|
+
export declare function loadBundleForPublish({ hashOrPath, }: {
|
|
16
|
+
hashOrPath: string;
|
|
17
|
+
}): Promise<LatticeBundle>;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Load a lattice bundle from disk for `bundle publish`. Mirrors the
|
|
3
|
+
* `seq-studio bundle publish` local store layout.
|
|
4
|
+
*/
|
|
5
|
+
import { readFile } from 'node:fs/promises';
|
|
6
|
+
import { resolve } from 'node:path';
|
|
7
|
+
import { BUNDLE_SCHEMA_VERSION, BundleHashMismatchError, computePublishedBundleHash, LocalFsBundleStore, MAX_LATTICE_BUNDLE_BYTES, } from '@sequenceholdings/lattice/bundle';
|
|
8
|
+
const HASH_PATTERN = /^[a-f0-9]{64}$/;
|
|
9
|
+
export function resolveLocalBundleRoot() {
|
|
10
|
+
return (process.env['LATTICE_LOCAL_BUNDLE_ROOT']?.trim() ||
|
|
11
|
+
resolve(process.cwd(), '.lattice'));
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Hash-on-read integrity check — same contract as LocalFsBundleStore.get()
|
|
15
|
+
* so hand-edited bundle.json files fail locally before POST /bundles.
|
|
16
|
+
*/
|
|
17
|
+
export function validateBundleIntegrity({ bundle, context, }) {
|
|
18
|
+
if (bundle.schema_version !== BUNDLE_SCHEMA_VERSION) {
|
|
19
|
+
throw new Error(`${context}: schema_version must be ${BUNDLE_SCHEMA_VERSION}, got ${String(bundle.schema_version)}`);
|
|
20
|
+
}
|
|
21
|
+
if (!HASH_PATTERN.test(bundle.bundle_hash)) {
|
|
22
|
+
throw new Error(`${context}: bundle_hash must be a 64-char hex string`);
|
|
23
|
+
}
|
|
24
|
+
const expected = computePublishedBundleHash({
|
|
25
|
+
manifest: bundle.manifest,
|
|
26
|
+
processes: bundle.processes,
|
|
27
|
+
});
|
|
28
|
+
if (bundle.bundle_hash !== expected) {
|
|
29
|
+
throw new BundleHashMismatchError(expected, bundle.bundle_hash, context);
|
|
30
|
+
}
|
|
31
|
+
const byteLen = Buffer.byteLength(JSON.stringify(bundle), 'utf8');
|
|
32
|
+
if (byteLen > MAX_LATTICE_BUNDLE_BYTES) {
|
|
33
|
+
throw new Error(`${context}: bundle is ${byteLen} bytes; exceeds ${MAX_LATTICE_BUNDLE_BYTES} byte limit`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
export async function loadBundleForPublish({ hashOrPath, }) {
|
|
37
|
+
if (hashOrPath.endsWith('.json')) {
|
|
38
|
+
const path = resolve(hashOrPath);
|
|
39
|
+
let body;
|
|
40
|
+
try {
|
|
41
|
+
body = await readFile(path, 'utf8');
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
throw new Error(`could not read bundle file ${path}: ${err instanceof Error ? err.message : String(err)}`);
|
|
45
|
+
}
|
|
46
|
+
let bundle;
|
|
47
|
+
try {
|
|
48
|
+
bundle = JSON.parse(body);
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
throw new Error(`invalid JSON in ${path}: ${err instanceof Error ? err.message : String(err)}`);
|
|
52
|
+
}
|
|
53
|
+
validateBundleIntegrity({ bundle, context: path });
|
|
54
|
+
return bundle;
|
|
55
|
+
}
|
|
56
|
+
if (!HASH_PATTERN.test(hashOrPath)) {
|
|
57
|
+
throw new Error(`expected a 64-char bundle hash or a .json path, got: ${hashOrPath}`);
|
|
58
|
+
}
|
|
59
|
+
const store = new LocalFsBundleStore({ root: resolveLocalBundleRoot() });
|
|
60
|
+
const bundle = await store.get(hashOrPath);
|
|
61
|
+
if (!bundle) {
|
|
62
|
+
throw new Error(`bundle ${hashOrPath} not found in ${store.describe()}`);
|
|
63
|
+
}
|
|
64
|
+
return bundle;
|
|
65
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Diff a freshly-built bundle against the currently-active bundle for
|
|
3
|
+
* each process. Used by `seq-studio process plan` to surface what changes for new
|
|
4
|
+
* runs (in-flight runs are pinned to their original bundle, so any diff
|
|
5
|
+
* is informational — not a correctness risk).
|
|
6
|
+
*/
|
|
7
|
+
import type { LatticeBundle, SerializedProcess } from '@sequenceholdings/lattice/bundle';
|
|
8
|
+
export interface PlanDiff {
|
|
9
|
+
processes: readonly ProcessDiff[];
|
|
10
|
+
has_breaking_change: boolean;
|
|
11
|
+
}
|
|
12
|
+
export interface ProcessDiff {
|
|
13
|
+
process_id: string;
|
|
14
|
+
status: 'unchanged' | 'new' | 'updated';
|
|
15
|
+
/**
|
|
16
|
+
* Set when `start_node_id` changes between active and next. Always
|
|
17
|
+
* BREAKING for new runs because the orchestrator starts a different
|
|
18
|
+
* node — surface it separately from per-node changes so the plan
|
|
19
|
+
* summary makes the entry point visible.
|
|
20
|
+
*/
|
|
21
|
+
start_node_changed?: {
|
|
22
|
+
from: string;
|
|
23
|
+
to: string;
|
|
24
|
+
};
|
|
25
|
+
added_nodes: readonly string[];
|
|
26
|
+
removed_nodes: readonly string[];
|
|
27
|
+
changed_nodes: readonly NodeChange[];
|
|
28
|
+
added_edges: readonly {
|
|
29
|
+
node_id: string;
|
|
30
|
+
edge_id: string;
|
|
31
|
+
to: string;
|
|
32
|
+
}[];
|
|
33
|
+
removed_edges: readonly {
|
|
34
|
+
node_id: string;
|
|
35
|
+
edge_id: string;
|
|
36
|
+
to: string;
|
|
37
|
+
}[];
|
|
38
|
+
retargeted_edges: readonly {
|
|
39
|
+
node_id: string;
|
|
40
|
+
edge_id: string;
|
|
41
|
+
from: string;
|
|
42
|
+
to: string;
|
|
43
|
+
}[];
|
|
44
|
+
active_version?: string;
|
|
45
|
+
new_version?: string;
|
|
46
|
+
is_breaking: boolean;
|
|
47
|
+
}
|
|
48
|
+
export interface NodeChange {
|
|
49
|
+
node_id: string;
|
|
50
|
+
kind_changed?: {
|
|
51
|
+
from: string;
|
|
52
|
+
to: string;
|
|
53
|
+
};
|
|
54
|
+
agent_ref_changed?: {
|
|
55
|
+
from: string | undefined;
|
|
56
|
+
to: string | undefined;
|
|
57
|
+
};
|
|
58
|
+
outgoing_edges_changed?: boolean;
|
|
59
|
+
/**
|
|
60
|
+
* Set when behavior-affecting metadata on the node changes — for
|
|
61
|
+
* sandboxed automation nodes that means `function_version`,
|
|
62
|
+
* `function_id`, or `input_mapper_source`; for native automation nodes
|
|
63
|
+
* `registered_function_id`, `config`, or `limits`; for agent nodes
|
|
64
|
+
* `input_mapper_source` or `timeout`; for human nodes `artifact_refs`,
|
|
65
|
+
* `comments`, `timeout`, `on_timeout_edge_id`, `artifact_refs_mapper_source`,
|
|
66
|
+
* `advance_allowed`, or `advance_allowed_mapper_source`.
|
|
67
|
+
* Without this check a process whose `fn` changed but graph topology
|
|
68
|
+
* stayed identical would diff as "unchanged" and `process test`
|
|
69
|
+
* would stay green even though new runs would execute different
|
|
70
|
+
* code.
|
|
71
|
+
*/
|
|
72
|
+
metadata_fields_changed?: readonly string[];
|
|
73
|
+
}
|
|
74
|
+
export type LoadActiveProcess = (processId: string) => Promise<{
|
|
75
|
+
version: string;
|
|
76
|
+
process: SerializedProcess;
|
|
77
|
+
} | null>;
|
|
78
|
+
export declare function diffBundleAgainstActive(input: {
|
|
79
|
+
newBundle: LatticeBundle;
|
|
80
|
+
loadActiveProcess: LoadActiveProcess;
|
|
81
|
+
}): Promise<PlanDiff>;
|
|
82
|
+
export declare function formatPlanDiff(diff: PlanDiff): string;
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Diff a freshly-built bundle against the currently-active bundle for
|
|
3
|
+
* each process. Used by `seq-studio process plan` to surface what changes for new
|
|
4
|
+
* runs (in-flight runs are pinned to their original bundle, so any diff
|
|
5
|
+
* is informational — not a correctness risk).
|
|
6
|
+
*/
|
|
7
|
+
export async function diffBundleAgainstActive(input) {
|
|
8
|
+
const processDiffs = [];
|
|
9
|
+
for (const newProc of input.newBundle.processes) {
|
|
10
|
+
const active = await input.loadActiveProcess(newProc.id);
|
|
11
|
+
if (!active) {
|
|
12
|
+
processDiffs.push({
|
|
13
|
+
process_id: newProc.id,
|
|
14
|
+
status: 'new',
|
|
15
|
+
added_nodes: newProc.nodes.map((n) => n.id),
|
|
16
|
+
removed_nodes: [],
|
|
17
|
+
changed_nodes: [],
|
|
18
|
+
added_edges: flattenEdges(newProc.nodes),
|
|
19
|
+
removed_edges: [],
|
|
20
|
+
retargeted_edges: [],
|
|
21
|
+
new_version: input.newBundle.version,
|
|
22
|
+
is_breaking: false,
|
|
23
|
+
});
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
const diff = diffProcesses(active.process, newProc);
|
|
27
|
+
processDiffs.push({
|
|
28
|
+
process_id: newProc.id,
|
|
29
|
+
status: diff.unchanged ? 'unchanged' : 'updated',
|
|
30
|
+
...(diff.startNodeChanged ? { start_node_changed: diff.startNodeChanged } : {}),
|
|
31
|
+
added_nodes: diff.addedNodes,
|
|
32
|
+
removed_nodes: diff.removedNodes,
|
|
33
|
+
changed_nodes: diff.changedNodes,
|
|
34
|
+
added_edges: diff.addedEdges,
|
|
35
|
+
removed_edges: diff.removedEdges,
|
|
36
|
+
retargeted_edges: diff.retargetedEdges,
|
|
37
|
+
active_version: active.version,
|
|
38
|
+
new_version: input.newBundle.version,
|
|
39
|
+
is_breaking: diff.isBreaking,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
processes: processDiffs,
|
|
44
|
+
has_breaking_change: processDiffs.some((p) => p.is_breaking),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function diffProcesses(active, next) {
|
|
48
|
+
const startNodeChanged = active.start_node_id !== next.start_node_id
|
|
49
|
+
? { from: active.start_node_id, to: next.start_node_id }
|
|
50
|
+
: undefined;
|
|
51
|
+
const activeNodeIds = new Set(active.nodes.map((n) => n.id));
|
|
52
|
+
const nextNodeIds = new Set(next.nodes.map((n) => n.id));
|
|
53
|
+
const removedNodes = [...activeNodeIds].filter((id) => !nextNodeIds.has(id));
|
|
54
|
+
const addedNodes = [...nextNodeIds].filter((id) => !activeNodeIds.has(id));
|
|
55
|
+
const changedNodes = [];
|
|
56
|
+
for (const id of activeNodeIds) {
|
|
57
|
+
if (!nextNodeIds.has(id))
|
|
58
|
+
continue;
|
|
59
|
+
const a = active.nodes.find((n) => n.id === id);
|
|
60
|
+
const b = next.nodes.find((n) => n.id === id);
|
|
61
|
+
const change = diffNode(a, b);
|
|
62
|
+
if (change)
|
|
63
|
+
changedNodes.push(change);
|
|
64
|
+
}
|
|
65
|
+
const removedEdges = [];
|
|
66
|
+
const addedEdges = [];
|
|
67
|
+
const retargetedEdges = [];
|
|
68
|
+
for (const node of active.nodes) {
|
|
69
|
+
if (!nextNodeIds.has(node.id))
|
|
70
|
+
continue;
|
|
71
|
+
const nextNode = next.nodes.find((n) => n.id === node.id);
|
|
72
|
+
const activeEdges = new Map(node.outgoing_edges.map((e) => [e.id, e]));
|
|
73
|
+
const nextEdges = new Map(nextNode.outgoing_edges.map((e) => [e.id, e]));
|
|
74
|
+
for (const [id, edge] of activeEdges) {
|
|
75
|
+
const nextEdge = nextEdges.get(id);
|
|
76
|
+
if (!nextEdge) {
|
|
77
|
+
removedEdges.push({ node_id: node.id, edge_id: id, to: edge.to });
|
|
78
|
+
}
|
|
79
|
+
else if (nextEdge.to !== edge.to) {
|
|
80
|
+
retargetedEdges.push({
|
|
81
|
+
node_id: node.id,
|
|
82
|
+
edge_id: id,
|
|
83
|
+
from: edge.to,
|
|
84
|
+
to: nextEdge.to,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
for (const [id, edge] of nextEdges) {
|
|
89
|
+
if (!activeEdges.has(id)) {
|
|
90
|
+
addedEdges.push({ node_id: node.id, edge_id: id, to: edge.to });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
for (const node of next.nodes) {
|
|
95
|
+
if (activeNodeIds.has(node.id))
|
|
96
|
+
continue;
|
|
97
|
+
for (const e of node.outgoing_edges) {
|
|
98
|
+
addedEdges.push({ node_id: node.id, edge_id: e.id, to: e.to });
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const isBreaking = startNodeChanged != null ||
|
|
102
|
+
removedNodes.length > 0 ||
|
|
103
|
+
removedEdges.length > 0 ||
|
|
104
|
+
retargetedEdges.length > 0 ||
|
|
105
|
+
changedNodes.some((c) => c.kind_changed != null || c.agent_ref_changed != null);
|
|
106
|
+
const unchanged = startNodeChanged == null &&
|
|
107
|
+
removedNodes.length === 0 &&
|
|
108
|
+
addedNodes.length === 0 &&
|
|
109
|
+
changedNodes.length === 0 &&
|
|
110
|
+
removedEdges.length === 0 &&
|
|
111
|
+
addedEdges.length === 0 &&
|
|
112
|
+
retargetedEdges.length === 0;
|
|
113
|
+
return {
|
|
114
|
+
unchanged,
|
|
115
|
+
...(startNodeChanged ? { startNodeChanged } : {}),
|
|
116
|
+
addedNodes,
|
|
117
|
+
removedNodes,
|
|
118
|
+
changedNodes,
|
|
119
|
+
addedEdges,
|
|
120
|
+
removedEdges,
|
|
121
|
+
retargetedEdges,
|
|
122
|
+
isBreaking,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Per-kind set of metadata fields that, when changed, alter behavior
|
|
127
|
+
* for new runs. Kept narrow on purpose: cosmetic/non-behavioral fields
|
|
128
|
+
* (like display labels added later) should NOT show up here or we'd
|
|
129
|
+
* report "changed" for no reason.
|
|
130
|
+
*
|
|
131
|
+
* Source of truth for what populates metadata is
|
|
132
|
+
* `serializeNodeMetadata` in `process/build.ts`.
|
|
133
|
+
*/
|
|
134
|
+
const BEHAVIORAL_METADATA_FIELDS = {
|
|
135
|
+
// `registered_function_id` is the discriminator for native automation
|
|
136
|
+
// nodes — switching which registered function a node runs is a behavior
|
|
137
|
+
// change for new runs (and produces a new bundle hash), so it must diff
|
|
138
|
+
// as "updated" rather than silently passing `process test`.
|
|
139
|
+
automation: [
|
|
140
|
+
'function_version',
|
|
141
|
+
'function_id',
|
|
142
|
+
'input_mapper_source',
|
|
143
|
+
'registered_function_id',
|
|
144
|
+
// Native automation nodes: static config (e.g. a target resource id) and
|
|
145
|
+
// limits alter which function runs and with what budget.
|
|
146
|
+
'config',
|
|
147
|
+
'limits',
|
|
148
|
+
'middleware',
|
|
149
|
+
],
|
|
150
|
+
agent: ['input_mapper_source', 'timeout', 'workflow_id', 'middleware', 'on_reset_edge_id'],
|
|
151
|
+
// managed_function: the target ref, version pin, output/timeout budget,
|
|
152
|
+
// input mapper, and middleware (e.g. retry) all alter what runs for new runs.
|
|
153
|
+
managed_function: ['function_id', 'version', 'limits', 'input_mapper_source', 'middleware'],
|
|
154
|
+
human: [
|
|
155
|
+
'artifact_refs',
|
|
156
|
+
'artifact_refs_mapper_source',
|
|
157
|
+
'comments',
|
|
158
|
+
'input_mapper_source',
|
|
159
|
+
'timeout',
|
|
160
|
+
'on_timeout_edge_id',
|
|
161
|
+
'metadata',
|
|
162
|
+
// The advance ACL decides who may advance the step. Both the static
|
|
163
|
+
// object form and the `(ctx) => ACL` mapper source change who gets the
|
|
164
|
+
// advancer tuples on new runs, so a change to either must diff as
|
|
165
|
+
// behavioral (otherwise `process test` stays green on an auth change).
|
|
166
|
+
'advance_allowed',
|
|
167
|
+
'advance_allowed_mapper_source',
|
|
168
|
+
// dueDate fn computes the task's due instant at parking — a change alters
|
|
169
|
+
// run behavior (inbox due pill/sort), so it must diff as behavioral.
|
|
170
|
+
'due_date_source',
|
|
171
|
+
],
|
|
172
|
+
};
|
|
173
|
+
/** Nested `metadata.subprocess` fields that change behavior for new parent runs. */
|
|
174
|
+
const SUBPROCESS_BEHAVIORAL_FIELDS = [
|
|
175
|
+
'process_id',
|
|
176
|
+
'version',
|
|
177
|
+
'bundle_hash',
|
|
178
|
+
'on_child_error',
|
|
179
|
+
'output_mapper_source',
|
|
180
|
+
];
|
|
181
|
+
function diffNode(a, b) {
|
|
182
|
+
const change = { node_id: a.id };
|
|
183
|
+
let any = false;
|
|
184
|
+
if (a.kind !== b.kind) {
|
|
185
|
+
change.kind_changed = { from: a.kind, to: b.kind };
|
|
186
|
+
any = true;
|
|
187
|
+
}
|
|
188
|
+
const aAgentId = a.metadata.agent_id;
|
|
189
|
+
const bAgentId = b.metadata.agent_id;
|
|
190
|
+
if (aAgentId !== bAgentId) {
|
|
191
|
+
change.agent_ref_changed = { from: aAgentId, to: bAgentId };
|
|
192
|
+
any = true;
|
|
193
|
+
}
|
|
194
|
+
const aEdges = a.outgoing_edges.map((e) => `${e.id}->${e.to}`).sort().join(',');
|
|
195
|
+
const bEdges = b.outgoing_edges.map((e) => `${e.id}->${e.to}`).sort().join(',');
|
|
196
|
+
if (aEdges !== bEdges) {
|
|
197
|
+
change.outgoing_edges_changed = true;
|
|
198
|
+
any = true;
|
|
199
|
+
}
|
|
200
|
+
// Behavioral-metadata sweep. Only run when both sides share the same
|
|
201
|
+
// kind — a kind change is already surfaced via `kind_changed` and
|
|
202
|
+
// would double-count metadata noise from the kind transition.
|
|
203
|
+
if (a.kind === b.kind) {
|
|
204
|
+
const fields = BEHAVIORAL_METADATA_FIELDS[a.kind] ?? [];
|
|
205
|
+
const metaA = a.metadata;
|
|
206
|
+
const metaB = b.metadata;
|
|
207
|
+
const changed = [];
|
|
208
|
+
for (const f of fields) {
|
|
209
|
+
// agent_ref_changed already surfaces metadata.agent_id; skip
|
|
210
|
+
// here to avoid double-reporting.
|
|
211
|
+
if (a.kind === 'agent' && f === 'agent_id')
|
|
212
|
+
continue;
|
|
213
|
+
if (!stableEqual(metaA[f], metaB[f])) {
|
|
214
|
+
changed.push(f);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
if (changed.length > 0) {
|
|
218
|
+
change.metadata_fields_changed = changed;
|
|
219
|
+
any = true;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
if (a.kind === 'subprocess' && b.kind === 'subprocess') {
|
|
223
|
+
const metaA = a.metadata;
|
|
224
|
+
const metaB = b.metadata;
|
|
225
|
+
const spA = (metaA.subprocess ?? {});
|
|
226
|
+
const spB = (metaB.subprocess ?? {});
|
|
227
|
+
const subprocessChanged = [];
|
|
228
|
+
for (const f of SUBPROCESS_BEHAVIORAL_FIELDS) {
|
|
229
|
+
if (!stableEqual(spA[f], spB[f])) {
|
|
230
|
+
subprocessChanged.push(`subprocess.${f}`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
if (!stableEqual(metaA.input_mapper_source, metaB.input_mapper_source)) {
|
|
234
|
+
subprocessChanged.push('input_mapper_source');
|
|
235
|
+
}
|
|
236
|
+
if (subprocessChanged.length > 0) {
|
|
237
|
+
change.metadata_fields_changed = [
|
|
238
|
+
...(change.metadata_fields_changed ?? []),
|
|
239
|
+
...subprocessChanged,
|
|
240
|
+
];
|
|
241
|
+
any = true;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return any ? change : null;
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Stable structural equality for JSON-compatible values. Order matters
|
|
248
|
+
* for arrays (intentional — `artifact_refs[0]` vs `[1]` is a real
|
|
249
|
+
* behavior difference) but not for plain object keys.
|
|
250
|
+
*/
|
|
251
|
+
function stableEqual(a, b) {
|
|
252
|
+
if (a === b)
|
|
253
|
+
return true;
|
|
254
|
+
if (a == null || b == null)
|
|
255
|
+
return a === b;
|
|
256
|
+
if (typeof a !== typeof b)
|
|
257
|
+
return false;
|
|
258
|
+
if (Array.isArray(a)) {
|
|
259
|
+
if (!Array.isArray(b) || a.length !== b.length)
|
|
260
|
+
return false;
|
|
261
|
+
for (let i = 0; i < a.length; i++) {
|
|
262
|
+
if (!stableEqual(a[i], b[i]))
|
|
263
|
+
return false;
|
|
264
|
+
}
|
|
265
|
+
return true;
|
|
266
|
+
}
|
|
267
|
+
if (typeof a === 'object') {
|
|
268
|
+
const aObj = a;
|
|
269
|
+
const bObj = b;
|
|
270
|
+
const ak = Object.keys(aObj).sort();
|
|
271
|
+
const bk = Object.keys(bObj).sort();
|
|
272
|
+
if (ak.length !== bk.length)
|
|
273
|
+
return false;
|
|
274
|
+
for (let i = 0; i < ak.length; i++) {
|
|
275
|
+
if (ak[i] !== bk[i])
|
|
276
|
+
return false;
|
|
277
|
+
if (!stableEqual(aObj[ak[i]], bObj[bk[i]]))
|
|
278
|
+
return false;
|
|
279
|
+
}
|
|
280
|
+
return true;
|
|
281
|
+
}
|
|
282
|
+
return false;
|
|
283
|
+
}
|
|
284
|
+
function flattenEdges(nodes) {
|
|
285
|
+
const out = [];
|
|
286
|
+
for (const n of nodes) {
|
|
287
|
+
for (const e of n.outgoing_edges) {
|
|
288
|
+
out.push({ node_id: n.id, edge_id: e.id, to: e.to });
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return out;
|
|
292
|
+
}
|
|
293
|
+
export function formatPlanDiff(diff) {
|
|
294
|
+
const lines = [];
|
|
295
|
+
for (const p of diff.processes) {
|
|
296
|
+
lines.push(`process ${p.process_id}: ${p.status}${p.is_breaking ? ' (BREAKING)' : ''}`);
|
|
297
|
+
if (p.active_version || p.new_version) {
|
|
298
|
+
lines.push(` active=${p.active_version ?? '<none>'} → new=${p.new_version ?? '<none>'}`);
|
|
299
|
+
}
|
|
300
|
+
if (p.start_node_changed) {
|
|
301
|
+
lines.push(` ! start_node_id: ${p.start_node_changed.from} → ${p.start_node_changed.to} (BREAKING)`);
|
|
302
|
+
}
|
|
303
|
+
if (p.added_nodes.length > 0)
|
|
304
|
+
lines.push(` + nodes: ${p.added_nodes.join(', ')}`);
|
|
305
|
+
if (p.removed_nodes.length > 0)
|
|
306
|
+
lines.push(` - nodes: ${p.removed_nodes.join(', ')}`);
|
|
307
|
+
for (const c of p.changed_nodes) {
|
|
308
|
+
const bits = [];
|
|
309
|
+
if (c.kind_changed)
|
|
310
|
+
bits.push(`kind ${c.kind_changed.from}→${c.kind_changed.to}`);
|
|
311
|
+
if (c.agent_ref_changed)
|
|
312
|
+
bits.push(`agent ${c.agent_ref_changed.from ?? '<none>'}→${c.agent_ref_changed.to ?? '<none>'}`);
|
|
313
|
+
if (c.outgoing_edges_changed)
|
|
314
|
+
bits.push(`outgoing_edges changed`);
|
|
315
|
+
if (c.metadata_fields_changed && c.metadata_fields_changed.length > 0) {
|
|
316
|
+
bits.push(`metadata: ${c.metadata_fields_changed.join(', ')}`);
|
|
317
|
+
}
|
|
318
|
+
lines.push(` ~ ${c.node_id}: ${bits.join('; ')}`);
|
|
319
|
+
}
|
|
320
|
+
for (const e of p.added_edges) {
|
|
321
|
+
lines.push(` + edge ${e.node_id}/${e.edge_id} → ${e.to}`);
|
|
322
|
+
}
|
|
323
|
+
for (const e of p.removed_edges) {
|
|
324
|
+
lines.push(` - edge ${e.node_id}/${e.edge_id} → ${e.to}`);
|
|
325
|
+
}
|
|
326
|
+
for (const e of p.retargeted_edges) {
|
|
327
|
+
lines.push(` ~ edge ${e.node_id}/${e.edge_id}: ${e.from} → ${e.to}`);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
if (lines.length === 0)
|
|
331
|
+
lines.push('no changes');
|
|
332
|
+
return lines.join('\n');
|
|
333
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ResolveProcessPin } from './build.js';
|
|
2
|
+
import type { ResolvedEnv } from '../config.js';
|
|
3
|
+
/**
|
|
4
|
+
* Build a registry lookup that resolves a child process id (+ optional version)
|
|
5
|
+
* to { version, bundleHash } for subprocess pin resolution at bundle build.
|
|
6
|
+
*/
|
|
7
|
+
export declare function buildResolveProcessPin(input: {
|
|
8
|
+
baseUrl: string;
|
|
9
|
+
token: string;
|
|
10
|
+
}): ResolveProcessPin;
|
|
11
|
+
export declare function buildResolveProcessPinFromEnv(env: ResolvedEnv, token: string): ResolveProcessPin;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { getJsonOr404 } from '../atlas-client.js';
|
|
2
|
+
/**
|
|
3
|
+
* Build a registry lookup that resolves a child process id (+ optional version)
|
|
4
|
+
* to { version, bundleHash } for subprocess pin resolution at bundle build.
|
|
5
|
+
*/
|
|
6
|
+
export function buildResolveProcessPin(input) {
|
|
7
|
+
return async (processId, version) => {
|
|
8
|
+
if (version) {
|
|
9
|
+
const row = await getJsonOr404({
|
|
10
|
+
baseUrl: input.baseUrl,
|
|
11
|
+
token: input.token,
|
|
12
|
+
path: `/api/lattice/processes/${encodeURIComponent(processId)}/versions/${encodeURIComponent(version)}`,
|
|
13
|
+
});
|
|
14
|
+
if (!row)
|
|
15
|
+
return null;
|
|
16
|
+
const bundleHash = await resolveBundleHashForVersion({
|
|
17
|
+
baseUrl: input.baseUrl,
|
|
18
|
+
token: input.token,
|
|
19
|
+
processId,
|
|
20
|
+
version,
|
|
21
|
+
});
|
|
22
|
+
return bundleHash ? { version, bundleHash } : null;
|
|
23
|
+
}
|
|
24
|
+
const meta = await getJsonOr404({
|
|
25
|
+
baseUrl: input.baseUrl,
|
|
26
|
+
token: input.token,
|
|
27
|
+
path: `/api/lattice/processes/${encodeURIComponent(processId)}`,
|
|
28
|
+
});
|
|
29
|
+
if (!meta?.activeVersion || !meta.activeBundleHash)
|
|
30
|
+
return null;
|
|
31
|
+
return { version: meta.activeVersion, bundleHash: meta.activeBundleHash };
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export function buildResolveProcessPinFromEnv(env, token) {
|
|
35
|
+
return buildResolveProcessPin({ baseUrl: env.url, token });
|
|
36
|
+
}
|
|
37
|
+
async function resolveBundleHashForVersion(input) {
|
|
38
|
+
const meta = await getJsonOr404({
|
|
39
|
+
baseUrl: input.baseUrl,
|
|
40
|
+
token: input.token,
|
|
41
|
+
path: `/api/lattice/processes/${encodeURIComponent(input.processId)}`,
|
|
42
|
+
});
|
|
43
|
+
if (meta?.activeVersion === input.version && meta.activeBundleHash) {
|
|
44
|
+
return meta.activeBundleHash;
|
|
45
|
+
}
|
|
46
|
+
let cursor;
|
|
47
|
+
for (;;) {
|
|
48
|
+
const path = `/api/lattice/processes/${encodeURIComponent(input.processId)}/versions` +
|
|
49
|
+
(cursor ? `?cursor=${encodeURIComponent(cursor)}` : '');
|
|
50
|
+
const page = await getJsonOr404({
|
|
51
|
+
baseUrl: input.baseUrl,
|
|
52
|
+
token: input.token,
|
|
53
|
+
path,
|
|
54
|
+
});
|
|
55
|
+
const hit = page?.versions?.find((v) => v.version === input.version);
|
|
56
|
+
if (hit?.bundle_hash)
|
|
57
|
+
return hit.bundle_hash;
|
|
58
|
+
cursor = page?.next_cursor ?? undefined;
|
|
59
|
+
if (!cursor)
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-process orchestrator walker with stubbed runners. Lets process
|
|
3
|
+
* authors verify their graph topology + routing locally without
|
|
4
|
+
* deploying to Trigger.dev.
|
|
5
|
+
*
|
|
6
|
+
* Mirrors the production orchestrator's `verifyAndAdvance` loop but:
|
|
7
|
+
* - Skips DB persistence
|
|
8
|
+
* - Stubs agent runner with the first outgoing edge + synthetic payload
|
|
9
|
+
* - Stubs human runner identically (or accepts caller-supplied decisions)
|
|
10
|
+
* - Stubs automation runner identically — native handlers live in the
|
|
11
|
+
* Atlas image and can't execute here
|
|
12
|
+
*/
|
|
13
|
+
import { type ProcessDefinition } from '@sequenceholdings/lattice/define';
|
|
14
|
+
import type { JsonObject, NodeKind, NodeOutput } from '@sequenceholdings/lattice';
|
|
15
|
+
export interface SimulateOptions {
|
|
16
|
+
process: ProcessDefinition;
|
|
17
|
+
/** All loaded process definitions — required to simulate subprocess nodes. */
|
|
18
|
+
processesById?: ReadonlyMap<string, ProcessDefinition>;
|
|
19
|
+
entity?: {
|
|
20
|
+
type: string;
|
|
21
|
+
id: string;
|
|
22
|
+
};
|
|
23
|
+
/** Child run metadata — surfaced as ctx.run.metadata (subprocess input mapper). */
|
|
24
|
+
metadata?: JsonObject;
|
|
25
|
+
startNodeId?: string;
|
|
26
|
+
humanDecisions?: Record<string, NodeOutput>;
|
|
27
|
+
maxIterations?: number;
|
|
28
|
+
maxRevisits?: number;
|
|
29
|
+
state?: JsonObject;
|
|
30
|
+
}
|
|
31
|
+
export interface SimulateResult {
|
|
32
|
+
steps: readonly SimulateStep[];
|
|
33
|
+
status: 'completed' | 'failed';
|
|
34
|
+
error?: string;
|
|
35
|
+
}
|
|
36
|
+
export interface SimulateStep {
|
|
37
|
+
iteration: number;
|
|
38
|
+
node_id: string;
|
|
39
|
+
kind: NodeKind;
|
|
40
|
+
input: unknown;
|
|
41
|
+
output: NodeOutput;
|
|
42
|
+
next_edge_id?: string;
|
|
43
|
+
to_node_id?: string;
|
|
44
|
+
/** Set on sub-steps emitted by a parallel block branch. */
|
|
45
|
+
branch_id?: string;
|
|
46
|
+
/** The parallel node id this sub-step belongs to (set on sub-steps). */
|
|
47
|
+
parent_node_id?: string;
|
|
48
|
+
}
|
|
49
|
+
export declare function simulateProcess(opts: SimulateOptions): Promise<SimulateResult>;
|
|
50
|
+
export declare function formatSimulateResult(result: SimulateResult): string;
|