agenttwin 0.3.10 → 0.3.11
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 +6 -0
- package/dist/cli.js +242 -34
- package/dist/dashboard/assets/next/static/chunks/app/dashboard/page-91a2efe02bd25700.js +1 -0
- package/dist/dashboard/assets/next/static/chunks/app/v2/dashboard/page-eb63255bc8df6433.js +1 -0
- package/dist/dashboard/assets/next/static/chunks/{webpack-351162512698e56b.js → webpack-60f6502c44ed95af.js} +1 -1
- package/dist/dashboard/index.html +2 -2
- package/package.json +1 -1
- package/dist/dashboard/assets/next/static/chunks/app/dashboard/page-03aac4c01adcda82.js +0 -1
- package/dist/dashboard/assets/next/static/chunks/app/v2/dashboard/page-a99a9eeb03937d53.js +0 -1
- /package/dist/dashboard/assets/next/static/{puuDlswFkm304y1LsNJ98 → 8DWbcYS099IMf1RzFG63k}/_buildManifest.js +0 -0
- /package/dist/dashboard/assets/next/static/{puuDlswFkm304y1LsNJ98 → 8DWbcYS099IMf1RzFG63k}/_ssgManifest.js +0 -0
package/README.md
CHANGED
|
@@ -154,3 +154,9 @@ A run receipt's **Fix & retest** tab exports an evidence-based task for your cod
|
|
|
154
154
|
The website’s Settings page saves workspace name, default execution limit (1–300 seconds for new templates), automatic refresh, compact lists and reduced motion in this browser. Existing reviewed bundles retain their limits. The account section supports token visibility, connection status, manual workspace refresh and disconnect; tokens are never persisted with preferences.
|
|
155
155
|
|
|
156
156
|
Top-right initials open account actions. The sidebar closes into a full-width layout. See `docs-site/src/dashboard.mdx` for the complete guide. The website/API UI has received additional fixes after npm 0.2.2; the published package remains 0.2.2.
|
|
157
|
+
|
|
158
|
+
### Workflow discovery boundaries
|
|
159
|
+
|
|
160
|
+
Discovery identifies individual request branches in Fetch/Bun-style URL dispatch, Express-style callbacks, queue workers and exported provider operations. It follows direct local function calls and relative named imports, including aliases, and recognizes provider hosts in literal HTTP calls. Each candidate retains its source entrypoint and evidence. Router containers and pure exported helpers are not treated as complete workflows.
|
|
161
|
+
|
|
162
|
+
These are possible static code paths, not executed or verified business workflows. Dynamic dispatch, computed destinations, external packages, re-exports and runtime-only integrations can remain unresolved. Prepare inputs, operation coverage and correctness checks before running a candidate. Refresh discovery and sync it to update an already-connected dashboard.
|
package/dist/cli.js
CHANGED
|
@@ -1929,7 +1929,7 @@ import { join as join34 } from "path";
|
|
|
1929
1929
|
// package.json
|
|
1930
1930
|
var package_default = {
|
|
1931
1931
|
name: "agenttwin",
|
|
1932
|
-
version: "0.3.
|
|
1932
|
+
version: "0.3.11",
|
|
1933
1933
|
description: "Give your agent a test world before it touches the real one.",
|
|
1934
1934
|
keywords: [
|
|
1935
1935
|
"agent",
|
|
@@ -6720,15 +6720,15 @@ function factoryProviderBindings(source) {
|
|
|
6720
6720
|
constructors.set(specifier.local.name, provider.id);
|
|
6721
6721
|
}
|
|
6722
6722
|
}
|
|
6723
|
-
const
|
|
6723
|
+
const walk2 = (node, visit, skipFunctions = false) => {
|
|
6724
6724
|
if (!node || typeof node !== "object") return;
|
|
6725
6725
|
const value = node;
|
|
6726
6726
|
if (skipFunctions && ["FunctionDeclaration", "FunctionExpression", "ArrowFunctionExpression"].includes(value.type)) return;
|
|
6727
6727
|
visit(value);
|
|
6728
6728
|
for (const [key, child] of Object.entries(value)) {
|
|
6729
6729
|
if (["loc", "start", "end", "comments"].includes(key)) continue;
|
|
6730
|
-
if (Array.isArray(child)) child.forEach((item) =>
|
|
6731
|
-
else if (child && typeof child === "object")
|
|
6730
|
+
if (Array.isArray(child)) child.forEach((item) => walk2(item, visit, skipFunctions));
|
|
6731
|
+
else if (child && typeof child === "object") walk2(child, visit, skipFunctions);
|
|
6732
6732
|
}
|
|
6733
6733
|
};
|
|
6734
6734
|
const factories = /* @__PURE__ */ new Map();
|
|
@@ -6737,7 +6737,7 @@ function factoryProviderBindings(source) {
|
|
|
6737
6737
|
if (node?.type !== "FunctionDeclaration" || !node.id || node.async || node.generator || node.params.length) continue;
|
|
6738
6738
|
const providers = /* @__PURE__ */ new Set();
|
|
6739
6739
|
let unknown = false;
|
|
6740
|
-
|
|
6740
|
+
walk2(node.body, (child) => {
|
|
6741
6741
|
if (child.type !== "ReturnStatement") return;
|
|
6742
6742
|
const argument = child.argument;
|
|
6743
6743
|
if (argument?.type === "NullLiteral") return;
|
|
@@ -6749,18 +6749,18 @@ function factoryProviderBindings(source) {
|
|
|
6749
6749
|
}
|
|
6750
6750
|
const declarations = /* @__PURE__ */ new Map();
|
|
6751
6751
|
const count = (name) => declarations.set(name, (declarations.get(name) ?? 0) + 1);
|
|
6752
|
-
|
|
6752
|
+
walk2(ast.program, (node) => {
|
|
6753
6753
|
if (["FunctionDeclaration", "ClassDeclaration"].includes(node.type) && node.id?.name) count(node.id.name);
|
|
6754
6754
|
if (node.type === "VariableDeclarator" && node.id.type === "Identifier") count(node.id.name);
|
|
6755
6755
|
if (["FunctionDeclaration", "FunctionExpression", "ArrowFunctionExpression"].includes(node.type)) {
|
|
6756
|
-
for (const parameter of node.params ?? [])
|
|
6756
|
+
for (const parameter of node.params ?? []) walk2(parameter, (part) => {
|
|
6757
6757
|
if (part.type === "Identifier") count(part.name);
|
|
6758
6758
|
});
|
|
6759
6759
|
}
|
|
6760
6760
|
});
|
|
6761
6761
|
for (const name of factories.keys()) if ((declarations.get(name) ?? 0) > 1) factories.delete(name);
|
|
6762
6762
|
const ambiguous = /* @__PURE__ */ new Set();
|
|
6763
|
-
|
|
6763
|
+
walk2(ast.program, (node) => {
|
|
6764
6764
|
if (node.type !== "VariableDeclarator" || node.id.type !== "Identifier" || node.init?.type !== "CallExpression" || node.init.callee.type !== "Identifier") return;
|
|
6765
6765
|
const provider = factories.get(node.init.callee.name);
|
|
6766
6766
|
if (!provider) return;
|
|
@@ -6776,6 +6776,195 @@ function factoryProviderBindings(source) {
|
|
|
6776
6776
|
init_entrypoints();
|
|
6777
6777
|
import { createHash as createHash12 } from "crypto";
|
|
6778
6778
|
|
|
6779
|
+
// src/inventory/workflow-graph.ts
|
|
6780
|
+
init_catalog();
|
|
6781
|
+
import { parse as parse3 } from "@babel/parser";
|
|
6782
|
+
import { posix } from "path";
|
|
6783
|
+
function walk(node, visit, skipFunctions = false) {
|
|
6784
|
+
if (!node || typeof node !== "object") return;
|
|
6785
|
+
if (skipFunctions && /^(FunctionDeclaration|FunctionExpression|ArrowFunctionExpression)$/.test(node.type)) return;
|
|
6786
|
+
visit(node);
|
|
6787
|
+
for (const [key, child] of Object.entries(node)) {
|
|
6788
|
+
if (["loc", "start", "end", "comments", "tokens"].includes(key)) continue;
|
|
6789
|
+
if (Array.isArray(child)) child.forEach((item) => walk(item, visit, skipFunctions));
|
|
6790
|
+
else if (child && typeof child === "object") walk(child, visit, skipFunctions);
|
|
6791
|
+
}
|
|
6792
|
+
}
|
|
6793
|
+
function workflowGraph(sources, operations, tools) {
|
|
6794
|
+
const modules = /* @__PURE__ */ new Map();
|
|
6795
|
+
const gaps = [];
|
|
6796
|
+
const resolve19 = (path2, specifier) => {
|
|
6797
|
+
const base = posix.normalize(posix.join(posix.dirname(path2), specifier));
|
|
6798
|
+
return [base, ...[".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"].map((ext) => base + ext), base.replace(/\.js$/, ".ts"), ...[".ts", ".js"].map((ext) => base + "/index" + ext)].find((candidate) => sources.has(candidate));
|
|
6799
|
+
};
|
|
6800
|
+
for (const [path2, source] of sources) {
|
|
6801
|
+
if (!/\.[cm]?[jt]sx?$/.test(path2)) continue;
|
|
6802
|
+
let ast;
|
|
6803
|
+
try {
|
|
6804
|
+
ast = parse3(source, { sourceType: "unambiguous", plugins: ["typescript", "jsx"] });
|
|
6805
|
+
} catch {
|
|
6806
|
+
gaps.push(`Workflow syntax could not be parsed: ${path2}`);
|
|
6807
|
+
continue;
|
|
6808
|
+
}
|
|
6809
|
+
const mod = { functions: /* @__PURE__ */ new Map(), imports: /* @__PURE__ */ new Map(), routes: [], ast };
|
|
6810
|
+
modules.set(path2, mod);
|
|
6811
|
+
const unit = (name, node, exported, trigger = false) => ({ path: path2, name, node, exported, trigger, startLine: node.loc.start.line, endLine: node.loc.end.line });
|
|
6812
|
+
for (const item of ast.program.body) {
|
|
6813
|
+
if (item.type === "ImportDeclaration" && item.importKind !== "type" && item.source.value.startsWith(".")) {
|
|
6814
|
+
const target = resolve19(path2, item.source.value);
|
|
6815
|
+
if (target) for (const spec of item.specifiers) {
|
|
6816
|
+
if (spec.type === "ImportSpecifier" && spec.importKind !== "type") mod.imports.set(spec.local.name, { path: target, name: spec.imported.name ?? spec.imported.value });
|
|
6817
|
+
if (spec.type === "ImportDefaultSpecifier") mod.imports.set(spec.local.name, { path: target, name: "default" });
|
|
6818
|
+
}
|
|
6819
|
+
}
|
|
6820
|
+
const exported = item.type === "ExportNamedDeclaration" || item.type === "ExportDefaultDeclaration";
|
|
6821
|
+
const declaration = exported ? item.declaration : item;
|
|
6822
|
+
if (declaration?.type === "FunctionDeclaration") {
|
|
6823
|
+
const name = declaration.id?.name ?? "default";
|
|
6824
|
+
mod.functions.set(name, { ...unit(name, declaration.body, exported), parameters: declaration.params });
|
|
6825
|
+
if (item.type === "ExportDefaultDeclaration") mod.functions.set("default", mod.functions.get(name));
|
|
6826
|
+
}
|
|
6827
|
+
if (declaration?.type === "VariableDeclaration") for (const variable of declaration.declarations) {
|
|
6828
|
+
if (variable.id.type !== "Identifier") continue;
|
|
6829
|
+
const fn = variable.init;
|
|
6830
|
+
if (fn && ["ArrowFunctionExpression", "FunctionExpression"].includes(fn.type)) mod.functions.set(variable.id.name, { ...unit(variable.id.name, fn.body, exported), parameters: fn.params });
|
|
6831
|
+
}
|
|
6832
|
+
}
|
|
6833
|
+
walk(ast.program, (fn) => {
|
|
6834
|
+
if (!["FunctionDeclaration", "FunctionExpression", "ArrowFunctionExpression"].includes(fn.type) || fn.body?.type !== "BlockStatement") return;
|
|
6835
|
+
const paths = /* @__PURE__ */ new Set();
|
|
6836
|
+
const patterns = /* @__PURE__ */ new Map();
|
|
6837
|
+
walk(fn.body, (node) => {
|
|
6838
|
+
if (node.type !== "VariableDeclarator" || node.id.type !== "Identifier") return;
|
|
6839
|
+
if (node.init?.type === "MemberExpression" && node.init.property.name === "pathname") paths.add(node.id.name);
|
|
6840
|
+
const call = node.init;
|
|
6841
|
+
if (call?.type === "CallExpression" && call.callee.type === "MemberExpression" && paths.has(call.callee.object.name) && call.callee.property.name === "match" && call.arguments[0]?.type === "RegExpLiteral") patterns.set(node.id.name, "/" + call.arguments[0].pattern + "/");
|
|
6842
|
+
}, true);
|
|
6843
|
+
if (paths.size === 0) return;
|
|
6844
|
+
walk(fn.body, (node) => {
|
|
6845
|
+
if (node.type !== "IfStatement") return;
|
|
6846
|
+
const routes = /* @__PURE__ */ new Set();
|
|
6847
|
+
const methods = /* @__PURE__ */ new Set();
|
|
6848
|
+
walk(node.test, (test) => {
|
|
6849
|
+
if (test.type === "Identifier" && patterns.has(test.name)) routes.add(patterns.get(test.name));
|
|
6850
|
+
if (test.type !== "BinaryExpression" || !["===", "=="].includes(test.operator)) return;
|
|
6851
|
+
for (const [left, right] of [[test.left, test.right], [test.right, test.left]]) {
|
|
6852
|
+
if (right.type !== "StringLiteral") continue;
|
|
6853
|
+
if (left.type === "Identifier" && paths.has(left.name) && right.value.startsWith("/")) routes.add(right.value);
|
|
6854
|
+
if (left.type === "MemberExpression" && left.property.name === "method" && /^(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)$/.test(right.value)) methods.add(right.value);
|
|
6855
|
+
}
|
|
6856
|
+
});
|
|
6857
|
+
let handlesRequest = false;
|
|
6858
|
+
walk(node.consequent, (part) => {
|
|
6859
|
+
if (part.type === "CallExpression" || part.type === "NewExpression" || part.type === "ReturnStatement" && part.argument && part.argument.type !== "NullLiteral") handlesRequest = true;
|
|
6860
|
+
}, true);
|
|
6861
|
+
if (!handlesRequest) return;
|
|
6862
|
+
for (const route of routes) {
|
|
6863
|
+
const method = methods.size === 1 ? [...methods][0] : "HTTP";
|
|
6864
|
+
mod.routes.push({ ...unit(`route:${method}:${route}@${node.loc.start.line}`, node.consequent, false, true), parameters: fn.params });
|
|
6865
|
+
}
|
|
6866
|
+
}, true);
|
|
6867
|
+
});
|
|
6868
|
+
walk(ast.program, (node) => {
|
|
6869
|
+
if (node.type !== "CallExpression" || node.callee.type !== "Identifier" || node.callee.name !== "fetch") return;
|
|
6870
|
+
const url = node.arguments[0];
|
|
6871
|
+
const value = url?.type === "StringLiteral" ? url.value : url?.type === "TemplateLiteral" ? url.quasis[0]?.value.cooked : void 0;
|
|
6872
|
+
if (!value || !/^https?:\/\//.test(value)) return;
|
|
6873
|
+
let host;
|
|
6874
|
+
try {
|
|
6875
|
+
host = new URL(value).hostname;
|
|
6876
|
+
} catch {
|
|
6877
|
+
return;
|
|
6878
|
+
}
|
|
6879
|
+
const provider = PROVIDERS.find((p) => p.signatures.hosts?.some((h2) => host === h2 || host.endsWith("." + h2)));
|
|
6880
|
+
const toolId = provider && tools.get(provider.id);
|
|
6881
|
+
if (!toolId) return;
|
|
6882
|
+
const methodNode = node.arguments[1]?.properties?.find((p) => p.key?.name === "method" || p.key?.value === "method")?.value;
|
|
6883
|
+
const method = methodNode ? methodNode.type === "StringLiteral" ? methodNode.value.toUpperCase() : "UNKNOWN" : "GET";
|
|
6884
|
+
const line = node.loc.start.line;
|
|
6885
|
+
operations.push({ path: path2, line, toolId, kind: method === "GET" || method === "HEAD" ? "reads" : "writes", label: `HTTP ${method} ${host}`, resource: provider.id, ref: `${path2}:${line}` });
|
|
6886
|
+
});
|
|
6887
|
+
}
|
|
6888
|
+
const adjacency = /* @__PURE__ */ new Map();
|
|
6889
|
+
const resolveCalls = (entry) => {
|
|
6890
|
+
const cached = adjacency.get(entry);
|
|
6891
|
+
if (cached) return cached;
|
|
6892
|
+
const found = /* @__PURE__ */ new Set();
|
|
6893
|
+
const boundNames = (nodes) => {
|
|
6894
|
+
const names = /* @__PURE__ */ new Set();
|
|
6895
|
+
for (const node of nodes) walk(node, (part) => {
|
|
6896
|
+
if (part.type === "Identifier") names.add(part.name);
|
|
6897
|
+
});
|
|
6898
|
+
return names;
|
|
6899
|
+
};
|
|
6900
|
+
const scan = (node, shadowed) => {
|
|
6901
|
+
if (!node || typeof node !== "object") return;
|
|
6902
|
+
if (node.type === "FunctionDeclaration") return;
|
|
6903
|
+
if (["ArrowFunctionExpression", "FunctionExpression"].includes(node.type)) {
|
|
6904
|
+
scan(node.body, /* @__PURE__ */ new Set([...shadowed, ...boundNames(node.params)]));
|
|
6905
|
+
return;
|
|
6906
|
+
}
|
|
6907
|
+
if (node.type === "BlockStatement") {
|
|
6908
|
+
shadowed = new Set(shadowed);
|
|
6909
|
+
for (const statement of node.body) {
|
|
6910
|
+
if (statement.type === "VariableDeclaration") for (const declaration of statement.declarations) for (const name of boundNames([declaration.id])) shadowed.add(name);
|
|
6911
|
+
if (statement.type === "FunctionDeclaration" && statement.id) shadowed.add(statement.id.name);
|
|
6912
|
+
}
|
|
6913
|
+
}
|
|
6914
|
+
if (node.type === "CallExpression" && node.callee.type === "Identifier" && !shadowed.has(node.callee.name)) {
|
|
6915
|
+
const mod = modules.get(entry.path);
|
|
6916
|
+
const local = mod.functions.get(node.callee.name);
|
|
6917
|
+
const imported = mod.imports.get(node.callee.name);
|
|
6918
|
+
const target = local ?? (imported ? modules.get(imported.path)?.functions.get(imported.name) : void 0);
|
|
6919
|
+
if (target && (local || target.exported)) found.add(target);
|
|
6920
|
+
}
|
|
6921
|
+
for (const [key, child] of Object.entries(node)) {
|
|
6922
|
+
if (["loc", "start", "end", "comments", "tokens"].includes(key)) continue;
|
|
6923
|
+
if (Array.isArray(child)) child.forEach((item) => scan(item, shadowed));
|
|
6924
|
+
else if (child && typeof child === "object") scan(child, shadowed);
|
|
6925
|
+
}
|
|
6926
|
+
};
|
|
6927
|
+
scan(entry.node, boundNames(entry.parameters ?? []));
|
|
6928
|
+
const result = [...found];
|
|
6929
|
+
adjacency.set(entry, result);
|
|
6930
|
+
return result;
|
|
6931
|
+
};
|
|
6932
|
+
const byPath = /* @__PURE__ */ new Map();
|
|
6933
|
+
for (const operation of operations) {
|
|
6934
|
+
const list2 = byPath.get(operation.path) ?? [];
|
|
6935
|
+
list2.push(operation);
|
|
6936
|
+
byPath.set(operation.path, list2);
|
|
6937
|
+
}
|
|
6938
|
+
const directEffects = /* @__PURE__ */ new Map();
|
|
6939
|
+
const effects = (unit) => {
|
|
6940
|
+
let result = directEffects.get(unit);
|
|
6941
|
+
if (!result) {
|
|
6942
|
+
result = (byPath.get(unit.path) ?? []).filter((op) => op.line >= unit.startLine && op.line <= unit.endLine);
|
|
6943
|
+
directEffects.set(unit, result);
|
|
6944
|
+
}
|
|
6945
|
+
return result;
|
|
6946
|
+
};
|
|
6947
|
+
const entries = [];
|
|
6948
|
+
for (const mod of modules.values()) {
|
|
6949
|
+
for (const entry of [...new Set(mod.functions.values()), ...mod.routes]) {
|
|
6950
|
+
if (!entry.exported && !entry.trigger) continue;
|
|
6951
|
+
if (!entry.trigger && mod.routes.some((route) => route.startLine >= entry.startLine && route.endLine <= entry.endLine)) continue;
|
|
6952
|
+
const visited = /* @__PURE__ */ new Set();
|
|
6953
|
+
const pending = [entry];
|
|
6954
|
+
while (pending.length) {
|
|
6955
|
+
const current = pending.pop();
|
|
6956
|
+
if (visited.has(current)) continue;
|
|
6957
|
+
visited.add(current);
|
|
6958
|
+
pending.push(...resolveCalls(current));
|
|
6959
|
+
}
|
|
6960
|
+
const calls = [...new Set([...visited].flatMap(effects))];
|
|
6961
|
+
if (!entry.trigger && !calls.some((call) => call.kind === "writes" || call.kind === "emits")) continue;
|
|
6962
|
+
entries.push({ ...entry, calls, evidence: [...visited].map((u) => ({ source: "static", ref: `${u.path}:${u.startLine}` })) });
|
|
6963
|
+
}
|
|
6964
|
+
}
|
|
6965
|
+
return { entries, gaps };
|
|
6966
|
+
}
|
|
6967
|
+
|
|
6779
6968
|
// src/inventory/operations.ts
|
|
6780
6969
|
var WRITE_VERBS = /* @__PURE__ */ new Set([
|
|
6781
6970
|
"create",
|
|
@@ -7194,32 +7383,47 @@ async function buildRelationships(apps, tools, files, decisions) {
|
|
|
7194
7383
|
verified: false
|
|
7195
7384
|
});
|
|
7196
7385
|
}
|
|
7197
|
-
|
|
7386
|
+
const allowedCalls = entrypointCalls.filter((call) => decidedRecords.some((record2) => record2.toolId === call.toolId && record2.kind === call.kind && record2.evidence.some((e) => e.ref === call.ref)));
|
|
7387
|
+
const graph = workflowGraph(sourceText, allowedCalls, new Map(providerTools.map((t) => [t.providerId, t.id])));
|
|
7388
|
+
gaps.push(...graph.gaps);
|
|
7389
|
+
const entries = graph.entries.map((entry) => ({ ...entry, evidence: entry.evidence }));
|
|
7390
|
+
for (const [path2, text] of sourceText) for (const entry of exportedEntrypoints(text)) {
|
|
7391
|
+
if (!/^(route|job):/.test(entry.name)) continue;
|
|
7392
|
+
entries.push({
|
|
7393
|
+
...entry,
|
|
7394
|
+
path: path2,
|
|
7395
|
+
node: {},
|
|
7396
|
+
exported: false,
|
|
7397
|
+
trigger: true,
|
|
7398
|
+
calls: allowedCalls.filter((call) => call.path === path2 && call.line >= entry.startLine && call.line <= entry.endLine),
|
|
7399
|
+
evidence: [{ source: "static", ref: `${path2}:${entry.startLine}` }]
|
|
7400
|
+
});
|
|
7401
|
+
}
|
|
7402
|
+
for (const entry of entries) {
|
|
7403
|
+
const { path: path2 } = entry;
|
|
7198
7404
|
const app = appForPath(apps, path2);
|
|
7199
|
-
|
|
7200
|
-
|
|
7201
|
-
|
|
7202
|
-
|
|
7203
|
-
|
|
7204
|
-
|
|
7205
|
-
|
|
7206
|
-
|
|
7207
|
-
|
|
7208
|
-
|
|
7209
|
-
|
|
7210
|
-
|
|
7211
|
-
|
|
7212
|
-
|
|
7213
|
-
|
|
7214
|
-
|
|
7215
|
-
|
|
7216
|
-
|
|
7217
|
-
|
|
7218
|
-
|
|
7219
|
-
|
|
7220
|
-
|
|
7221
|
-
});
|
|
7222
|
-
}
|
|
7405
|
+
const calls = entry.calls.filter((call) => !rejected.has(opRelId(appForPath(apps, call.path).id, call.toolId, call.kind)));
|
|
7406
|
+
if (!entry.trigger && !calls.some((call) => call.kind === "writes" || call.kind === "emits")) continue;
|
|
7407
|
+
const dependencies = [...new Set(calls.map((call) => call.toolId))];
|
|
7408
|
+
const evidence = [...new Map([...entry.evidence, ...calls.map((call) => ({ source: "static", ref: call.ref }))].map((ref) => [ref.ref, ref])).values()];
|
|
7409
|
+
workflows.push({
|
|
7410
|
+
id: "entry:" + createHash12("sha256").update(path2 + ":" + entry.name).digest("hex").slice(0, 20),
|
|
7411
|
+
title: entry.name,
|
|
7412
|
+
entrypoint: path2 + "#" + entry.name.replace(/\\/g, "%5C"),
|
|
7413
|
+
apps: [app.id],
|
|
7414
|
+
tools: dependencies,
|
|
7415
|
+
steps: dependencies.map((tool2) => ({ app: app.id, tool: tool2, operations: [...new Set(calls.filter((c) => c.toolId === tool2).map((c) => c.label))], evidence })),
|
|
7416
|
+
sharedEntities: [...new Set(calls.map((call) => singularize(call.resource)))],
|
|
7417
|
+
evidence,
|
|
7418
|
+
confidence: "medium",
|
|
7419
|
+
risk: "high",
|
|
7420
|
+
rank: 0,
|
|
7421
|
+
supported: false,
|
|
7422
|
+
hypothesis: true,
|
|
7423
|
+
linkingSignals: [entry.trigger ? "explicit request or background-job trigger" : "exported operation with statically linked provider effects", "local helper call chains are possible paths, not proof of runtime execution; invocation and correctness require review"],
|
|
7424
|
+
scopeUncertain: true,
|
|
7425
|
+
verified: false
|
|
7426
|
+
});
|
|
7223
7427
|
}
|
|
7224
7428
|
if (sourceFiles.length === MAX_FILES3) gaps.push("Source scan is limited to 2500 files; split large repositories into explicit roots. Not every workflow can be proven by static discovery.");
|
|
7225
7429
|
const finalWorkflows = workflows.filter((w) => !rejected.has(w.id)).map(
|
|
@@ -7878,6 +8082,10 @@ function scenariosFor(kind) {
|
|
|
7878
8082
|
return [...new Set(list2)];
|
|
7879
8083
|
}
|
|
7880
8084
|
function assertionsFor(candidate) {
|
|
8085
|
+
if (candidate.entrypoint) return [
|
|
8086
|
+
`define the expected response/output for ${candidate.entrypoint}`,
|
|
8087
|
+
"review required final state and forbidden effects; discovery does not infer business correctness"
|
|
8088
|
+
];
|
|
7881
8089
|
const entity = candidate.sharedEntities[0] ?? "record";
|
|
7882
8090
|
return [
|
|
7883
8091
|
`exactly one ${entity} write is applied (no duplicates)`,
|
|
@@ -7900,7 +8108,7 @@ function buildWorkflowPlans(candidates, _tools) {
|
|
|
7900
8108
|
),
|
|
7901
8109
|
sharedEntities: candidate.sharedEntities,
|
|
7902
8110
|
permissions: permissionsFor(kind, candidate.sharedEntities),
|
|
7903
|
-
asyncEvents: eventsFor(kind, candidate.sharedEntities),
|
|
8111
|
+
asyncEvents: candidate.entrypoint ? [] : eventsFor(kind, candidate.sharedEntities),
|
|
7904
8112
|
failureScenarios: scenariosFor(kind),
|
|
7905
8113
|
outcomeAssertions: assertionsFor(candidate),
|
|
7906
8114
|
unknowns: [
|