@saptools/service-flow 0.1.10 → 0.1.12
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/CHANGELOG.md +20 -0
- package/README.md +11 -3
- package/dist/{chunk-LQT67AU5.js → chunk-MXYYARUP.js} +238 -95
- package/dist/chunk-MXYYARUP.js.map +1 -0
- package/dist/cli.js +220 -46
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-LQT67AU5.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -10,12 +10,12 @@ import {
|
|
|
10
10
|
parsePackageJson,
|
|
11
11
|
parseServiceBindings,
|
|
12
12
|
trace
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-MXYYARUP.js";
|
|
14
14
|
|
|
15
15
|
// src/cli.ts
|
|
16
16
|
import { Command } from "commander";
|
|
17
|
-
import
|
|
18
|
-
import
|
|
17
|
+
import path6 from "path";
|
|
18
|
+
import fs6 from "fs/promises";
|
|
19
19
|
import pc from "picocolors";
|
|
20
20
|
|
|
21
21
|
// src/config/defaults.ts
|
|
@@ -95,12 +95,13 @@ CREATE TABLE IF NOT EXISTS files (id INTEGER PRIMARY KEY, repo_id INTEGER NOT NU
|
|
|
95
95
|
CREATE TABLE IF NOT EXISTS cds_requires (id INTEGER PRIMARY KEY, repo_id INTEGER NOT NULL, alias TEXT NOT NULL, kind TEXT, model TEXT, destination TEXT, service_path TEXT, request_timeout INTEGER, raw_json TEXT NOT NULL, FOREIGN KEY(repo_id) REFERENCES repositories(id) ON DELETE CASCADE);
|
|
96
96
|
CREATE TABLE IF NOT EXISTS cds_services (id INTEGER PRIMARY KEY, repo_id INTEGER NOT NULL, namespace TEXT, service_name TEXT NOT NULL, qualified_name TEXT NOT NULL, service_path TEXT NOT NULL, is_extend INTEGER NOT NULL, source_file TEXT NOT NULL, source_line INTEGER NOT NULL, FOREIGN KEY(repo_id) REFERENCES repositories(id) ON DELETE CASCADE);
|
|
97
97
|
CREATE TABLE IF NOT EXISTS cds_operations (id INTEGER PRIMARY KEY, service_id INTEGER NOT NULL, operation_type TEXT NOT NULL, operation_name TEXT NOT NULL, operation_path TEXT NOT NULL, params_json TEXT NOT NULL, return_type TEXT, source_file TEXT NOT NULL, source_line INTEGER NOT NULL, FOREIGN KEY(service_id) REFERENCES cds_services(id) ON DELETE CASCADE);
|
|
98
|
-
CREATE TABLE IF NOT EXISTS symbols (id INTEGER PRIMARY KEY, repo_id INTEGER NOT NULL, file_id INTEGER, kind TEXT NOT NULL, name TEXT NOT NULL, qualified_name TEXT NOT NULL, exported INTEGER NOT NULL, start_line INTEGER NOT NULL, end_line INTEGER NOT NULL, FOREIGN KEY(repo_id) REFERENCES repositories(id) ON DELETE CASCADE, FOREIGN KEY(file_id) REFERENCES files(id) ON DELETE CASCADE);
|
|
98
|
+
CREATE TABLE IF NOT EXISTS symbols (id INTEGER PRIMARY KEY, repo_id INTEGER NOT NULL, file_id INTEGER, kind TEXT NOT NULL, name TEXT NOT NULL, qualified_name TEXT NOT NULL, exported INTEGER NOT NULL, start_line INTEGER NOT NULL, end_line INTEGER NOT NULL, start_offset INTEGER, end_offset INTEGER, source_file TEXT, exported_name TEXT, evidence_json TEXT, FOREIGN KEY(repo_id) REFERENCES repositories(id) ON DELETE CASCADE, FOREIGN KEY(file_id) REFERENCES files(id) ON DELETE CASCADE);
|
|
99
99
|
CREATE TABLE IF NOT EXISTS handler_classes (id INTEGER PRIMARY KEY, repo_id INTEGER NOT NULL, symbol_id INTEGER, class_name TEXT NOT NULL, source_file TEXT NOT NULL, source_line INTEGER NOT NULL, FOREIGN KEY(repo_id) REFERENCES repositories(id) ON DELETE CASCADE, FOREIGN KEY(symbol_id) REFERENCES symbols(id) ON DELETE SET NULL);
|
|
100
100
|
CREATE TABLE IF NOT EXISTS handler_methods (id INTEGER PRIMARY KEY, handler_class_id INTEGER NOT NULL, method_name TEXT NOT NULL, decorator_kind TEXT NOT NULL, decorator_value TEXT, decorator_raw_expression TEXT NOT NULL, source_file TEXT NOT NULL, source_line INTEGER NOT NULL, FOREIGN KEY(handler_class_id) REFERENCES handler_classes(id) ON DELETE CASCADE);
|
|
101
101
|
CREATE TABLE IF NOT EXISTS handler_registrations (id INTEGER PRIMARY KEY, repo_id INTEGER NOT NULL, handler_class_id INTEGER, class_name TEXT, import_source TEXT, registration_file TEXT NOT NULL, registration_line INTEGER NOT NULL, registration_kind TEXT NOT NULL, confidence REAL NOT NULL, FOREIGN KEY(repo_id) REFERENCES repositories(id) ON DELETE CASCADE, FOREIGN KEY(handler_class_id) REFERENCES handler_classes(id) ON DELETE SET NULL);
|
|
102
102
|
CREATE TABLE IF NOT EXISTS service_bindings (id INTEGER PRIMARY KEY, repo_id INTEGER NOT NULL, symbol_id INTEGER, variable_name TEXT NOT NULL, alias TEXT, alias_expr TEXT, destination_expr TEXT, service_path_expr TEXT, is_dynamic INTEGER NOT NULL, placeholders_json TEXT NOT NULL, source_file TEXT NOT NULL, source_line INTEGER NOT NULL, helper_chain_json TEXT, FOREIGN KEY(repo_id) REFERENCES repositories(id) ON DELETE CASCADE, FOREIGN KEY(symbol_id) REFERENCES symbols(id) ON DELETE SET NULL);
|
|
103
|
-
CREATE TABLE IF NOT EXISTS outbound_calls (id INTEGER PRIMARY KEY, repo_id INTEGER NOT NULL, source_symbol_id INTEGER, call_type TEXT NOT NULL, service_binding_id INTEGER, method TEXT, operation_path_expr TEXT, query_entity TEXT, event_name_expr TEXT, payload_summary TEXT, source_file TEXT NOT NULL, source_line INTEGER NOT NULL, confidence REAL NOT NULL, unresolved_reason TEXT, FOREIGN KEY(repo_id) REFERENCES repositories(id) ON DELETE CASCADE, FOREIGN KEY(source_symbol_id) REFERENCES symbols(id) ON DELETE SET NULL, FOREIGN KEY(service_binding_id) REFERENCES service_bindings(id) ON DELETE SET NULL);
|
|
103
|
+
CREATE TABLE IF NOT EXISTS outbound_calls (id INTEGER PRIMARY KEY, repo_id INTEGER NOT NULL, source_symbol_id INTEGER, call_type TEXT NOT NULL, service_binding_id INTEGER, method TEXT, operation_path_expr TEXT, query_entity TEXT, event_name_expr TEXT, payload_summary TEXT, source_file TEXT NOT NULL, source_line INTEGER NOT NULL, confidence REAL NOT NULL, unresolved_reason TEXT, local_service_name TEXT, local_service_lookup TEXT, alias_chain_json TEXT, FOREIGN KEY(repo_id) REFERENCES repositories(id) ON DELETE CASCADE, FOREIGN KEY(source_symbol_id) REFERENCES symbols(id) ON DELETE SET NULL, FOREIGN KEY(service_binding_id) REFERENCES service_bindings(id) ON DELETE SET NULL);
|
|
104
|
+
CREATE TABLE IF NOT EXISTS symbol_calls (id INTEGER PRIMARY KEY, repo_id INTEGER NOT NULL, caller_symbol_id INTEGER NOT NULL, callee_symbol_id INTEGER, callee_expression TEXT NOT NULL, import_source TEXT, source_file TEXT NOT NULL, source_line INTEGER NOT NULL, status TEXT NOT NULL, confidence REAL NOT NULL, evidence_json TEXT NOT NULL, unresolved_reason TEXT, FOREIGN KEY(repo_id) REFERENCES repositories(id) ON DELETE CASCADE, FOREIGN KEY(caller_symbol_id) REFERENCES symbols(id) ON DELETE CASCADE, FOREIGN KEY(callee_symbol_id) REFERENCES symbols(id) ON DELETE SET NULL);
|
|
104
105
|
CREATE TABLE IF NOT EXISTS graph_edges (id INTEGER PRIMARY KEY, workspace_id INTEGER NOT NULL, edge_type TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'unresolved', from_kind TEXT NOT NULL, from_id TEXT NOT NULL, to_kind TEXT NOT NULL, to_id TEXT NOT NULL, confidence REAL NOT NULL, evidence_json TEXT NOT NULL, is_dynamic INTEGER NOT NULL, unresolved_reason TEXT, generation INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE);
|
|
105
106
|
CREATE TABLE IF NOT EXISTS index_runs (id INTEGER PRIMARY KEY, workspace_id INTEGER NOT NULL, started_at TEXT NOT NULL, finished_at TEXT, status TEXT NOT NULL, repo_count INTEGER NOT NULL, file_count INTEGER NOT NULL, diagnostic_count INTEGER NOT NULL, error_message TEXT, FOREIGN KEY(workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE);
|
|
106
107
|
CREATE TABLE IF NOT EXISTS diagnostics (id INTEGER PRIMARY KEY, repo_id INTEGER, file_id INTEGER, severity TEXT NOT NULL, code TEXT NOT NULL, message TEXT NOT NULL, source_file TEXT, source_line INTEGER, FOREIGN KEY(repo_id) REFERENCES repositories(id) ON DELETE CASCADE, FOREIGN KEY(file_id) REFERENCES files(id) ON DELETE SET NULL);
|
|
@@ -108,11 +109,12 @@ CREATE INDEX IF NOT EXISTS idx_repo_name ON repositories(name);
|
|
|
108
109
|
CREATE INDEX IF NOT EXISTS idx_service_path ON cds_services(service_path);
|
|
109
110
|
CREATE INDEX IF NOT EXISTS idx_operation_name ON cds_operations(operation_name, operation_path);
|
|
110
111
|
CREATE INDEX IF NOT EXISTS idx_calls_repo ON outbound_calls(repo_id, call_type);
|
|
112
|
+
CREATE INDEX IF NOT EXISTS idx_symbol_calls_caller ON symbol_calls(repo_id, caller_symbol_id);
|
|
111
113
|
CREATE VIRTUAL TABLE IF NOT EXISTS search_index USING fts5(kind, name, path, repo);
|
|
112
114
|
`;
|
|
113
115
|
|
|
114
116
|
// src/db/migrations.ts
|
|
115
|
-
var CURRENT_SCHEMA_VERSION =
|
|
117
|
+
var CURRENT_SCHEMA_VERSION = 4;
|
|
116
118
|
var columns = {
|
|
117
119
|
service_bindings: [
|
|
118
120
|
{ name: "helper_chain_json", ddl: "ALTER TABLE service_bindings ADD COLUMN helper_chain_json TEXT" },
|
|
@@ -133,6 +135,18 @@ var columns = {
|
|
|
133
135
|
{ name: "class_name", ddl: "ALTER TABLE handler_registrations ADD COLUMN class_name TEXT" },
|
|
134
136
|
{ name: "import_source", ddl: "ALTER TABLE handler_registrations ADD COLUMN import_source TEXT" }
|
|
135
137
|
],
|
|
138
|
+
symbols: [
|
|
139
|
+
{ name: "start_offset", ddl: "ALTER TABLE symbols ADD COLUMN start_offset INTEGER" },
|
|
140
|
+
{ name: "end_offset", ddl: "ALTER TABLE symbols ADD COLUMN end_offset INTEGER" },
|
|
141
|
+
{ name: "source_file", ddl: "ALTER TABLE symbols ADD COLUMN source_file TEXT" },
|
|
142
|
+
{ name: "exported_name", ddl: "ALTER TABLE symbols ADD COLUMN exported_name TEXT" },
|
|
143
|
+
{ name: "evidence_json", ddl: "ALTER TABLE symbols ADD COLUMN evidence_json TEXT" }
|
|
144
|
+
],
|
|
145
|
+
outbound_calls: [
|
|
146
|
+
{ name: "local_service_name", ddl: "ALTER TABLE outbound_calls ADD COLUMN local_service_name TEXT" },
|
|
147
|
+
{ name: "local_service_lookup", ddl: "ALTER TABLE outbound_calls ADD COLUMN local_service_lookup TEXT" },
|
|
148
|
+
{ name: "alias_chain_json", ddl: "ALTER TABLE outbound_calls ADD COLUMN alias_chain_json TEXT" }
|
|
149
|
+
],
|
|
136
150
|
index_runs: [
|
|
137
151
|
{ name: "error_message", ddl: "ALTER TABLE index_runs ADD COLUMN error_message TEXT" }
|
|
138
152
|
]
|
|
@@ -171,7 +185,7 @@ function migrate(db) {
|
|
|
171
185
|
// package.json
|
|
172
186
|
var package_default = {
|
|
173
187
|
name: "@saptools/service-flow",
|
|
174
|
-
version: "0.1.
|
|
188
|
+
version: "0.1.12",
|
|
175
189
|
description: "Trace SAP CAP service-to-service flows across multi-repository workspaces with runtime-aware graph resolution",
|
|
176
190
|
type: "module",
|
|
177
191
|
publishConfig: {
|
|
@@ -384,11 +398,12 @@ function clearRepoFacts(db, repoId) {
|
|
|
384
398
|
for (const t of [
|
|
385
399
|
"cds_requires",
|
|
386
400
|
"cds_services",
|
|
387
|
-
"symbols",
|
|
388
401
|
"handler_classes",
|
|
402
|
+
"outbound_calls",
|
|
403
|
+
"symbol_calls",
|
|
389
404
|
"handler_registrations",
|
|
390
405
|
"service_bindings",
|
|
391
|
-
"
|
|
406
|
+
"symbols",
|
|
392
407
|
"diagnostics",
|
|
393
408
|
"files"
|
|
394
409
|
])
|
|
@@ -523,13 +538,26 @@ function insertBindings(db, repoId, rows) {
|
|
|
523
538
|
r.helperChain ? JSON.stringify(r.helperChain) : null
|
|
524
539
|
);
|
|
525
540
|
}
|
|
541
|
+
function insertExecutableSymbols(db, repoId, rows) {
|
|
542
|
+
const stmt = db.prepare("INSERT INTO symbols(repo_id,file_id,kind,name,qualified_name,exported,start_line,end_line,start_offset,end_offset,source_file,exported_name,evidence_json) VALUES(?,(SELECT id FROM files WHERE repo_id=? AND relative_path=?),?,?,?,?,?,?,?,?,?,?,?)");
|
|
543
|
+
for (const r of rows) stmt.run(repoId, repoId, r.sourceFile, r.kind, r.localName, r.qualifiedName, r.exported ? 1 : 0, r.startLine, r.endLine, r.startOffset, r.endOffset, r.sourceFile, r.exportedName, r.importExportEvidence ? JSON.stringify(r.importExportEvidence) : null);
|
|
544
|
+
}
|
|
545
|
+
function insertSymbolCalls(db, repoId, rows) {
|
|
546
|
+
const stmt = db.prepare(`INSERT INTO symbol_calls(repo_id,caller_symbol_id,callee_symbol_id,callee_expression,import_source,source_file,source_line,status,confidence,evidence_json,unresolved_reason) VALUES(?,(SELECT id FROM symbols WHERE repo_id=? AND source_file=? AND qualified_name=? ORDER BY id LIMIT 1),(SELECT id FROM symbols WHERE repo_id=? AND ((source_file=? AND (name=? OR qualified_name=?)) OR (source_file<>? AND exported=1 AND (exported_name=? OR name=? OR qualified_name=?))) ORDER BY CASE WHEN source_file=? THEN 0 ELSE 1 END,id LIMIT 1),?,?,?,?,?,0.8,?,CASE WHEN (SELECT id FROM symbols WHERE repo_id=? AND ((source_file=? AND (name=? OR qualified_name=?)) OR (source_file<>? AND exported=1 AND (exported_name=? OR name=? OR qualified_name=?))) ORDER BY CASE WHEN source_file=? THEN 0 ELSE 1 END,id LIMIT 1) IS NULL THEN 'No local symbol target matched exactly' ELSE NULL END)`);
|
|
547
|
+
for (const r of rows) stmt.run(repoId, repoId, r.sourceFile, r.callerQualifiedName, repoId, r.sourceFile, r.calleeLocalName, r.calleeLocalName, r.sourceFile, r.calleeLocalName, r.calleeLocalName, r.calleeLocalName, r.sourceFile, r.calleeExpression, r.importSource, r.sourceFile, r.sourceLine, JSON.stringify(r.evidence), repoId, r.sourceFile, r.calleeLocalName, r.calleeLocalName, r.sourceFile, r.calleeLocalName, r.calleeLocalName, r.calleeLocalName, r.sourceFile);
|
|
548
|
+
db.prepare("UPDATE symbol_calls SET status=CASE WHEN callee_symbol_id IS NULL THEN 'unresolved' ELSE 'resolved' END, unresolved_reason=CASE WHEN callee_symbol_id IS NULL THEN COALESCE(unresolved_reason,'No local symbol target matched exactly') ELSE NULL END WHERE repo_id=?").run(repoId);
|
|
549
|
+
}
|
|
526
550
|
function insertCalls(db, repoId, rows) {
|
|
527
551
|
const stmt = db.prepare(
|
|
528
|
-
"INSERT INTO outbound_calls(repo_id,call_type,method,operation_path_expr,query_entity,event_name_expr,payload_summary,source_file,source_line,confidence,unresolved_reason,service_binding_id) VALUES(
|
|
552
|
+
"INSERT INTO outbound_calls(repo_id,source_symbol_id,call_type,method,operation_path_expr,query_entity,event_name_expr,payload_summary,source_file,source_line,confidence,unresolved_reason,local_service_name,local_service_lookup,alias_chain_json,service_binding_id) VALUES(?,(SELECT id FROM symbols WHERE repo_id=? AND source_file=? AND start_line<=? AND end_line>=? ORDER BY (end_line-start_line),id LIMIT 1),?,?,?,?,?,?,?,?,?,?,?,?,?,(SELECT id FROM service_bindings WHERE repo_id=? AND variable_name=? AND source_file=? ORDER BY CASE WHEN source_line<=? THEN 0 ELSE 1 END, ABS(source_line-?) ASC, id DESC LIMIT 1))"
|
|
529
553
|
);
|
|
530
554
|
for (const r of rows)
|
|
531
555
|
stmt.run(
|
|
532
556
|
repoId,
|
|
557
|
+
repoId,
|
|
558
|
+
r.sourceFile,
|
|
559
|
+
r.sourceLine,
|
|
560
|
+
r.sourceLine,
|
|
533
561
|
r.callType,
|
|
534
562
|
r.method,
|
|
535
563
|
r.operationPathExpr,
|
|
@@ -540,6 +568,9 @@ function insertCalls(db, repoId, rows) {
|
|
|
540
568
|
r.sourceLine,
|
|
541
569
|
r.confidence,
|
|
542
570
|
r.unresolvedReason,
|
|
571
|
+
r.localServiceName,
|
|
572
|
+
r.localServiceLookup,
|
|
573
|
+
r.aliasChain ? JSON.stringify(r.aliasChain) : null,
|
|
543
574
|
repoId,
|
|
544
575
|
r.serviceVariableName,
|
|
545
576
|
r.sourceFile,
|
|
@@ -591,8 +622,130 @@ function errorMessage(error) {
|
|
|
591
622
|
}
|
|
592
623
|
|
|
593
624
|
// src/indexer/repository-indexer.ts
|
|
625
|
+
import fs5 from "fs/promises";
|
|
626
|
+
import path5 from "path";
|
|
627
|
+
|
|
628
|
+
// src/parsers/symbol-parser.ts
|
|
594
629
|
import fs4 from "fs/promises";
|
|
595
630
|
import path4 from "path";
|
|
631
|
+
import ts from "typescript";
|
|
632
|
+
function lineOf(source, pos) {
|
|
633
|
+
return source.getLineAndCharacterOfPosition(pos).line + 1;
|
|
634
|
+
}
|
|
635
|
+
function nameOf(node) {
|
|
636
|
+
if (!node) return void 0;
|
|
637
|
+
if (ts.isIdentifier(node) || ts.isStringLiteral(node) || ts.isNumericLiteral(node)) return node.text;
|
|
638
|
+
return void 0;
|
|
639
|
+
}
|
|
640
|
+
function isFunctionLike(node) {
|
|
641
|
+
return ts.isFunctionDeclaration(node) || ts.isMethodDeclaration(node) || ts.isFunctionExpression(node) || ts.isArrowFunction(node);
|
|
642
|
+
}
|
|
643
|
+
function exported(node) {
|
|
644
|
+
return Boolean(ts.getCombinedModifierFlags(node) & ts.ModifierFlags.Export);
|
|
645
|
+
}
|
|
646
|
+
function exportDeclarations(source) {
|
|
647
|
+
const exports = /* @__PURE__ */ new Map();
|
|
648
|
+
const visit = (node) => {
|
|
649
|
+
if (ts.isExportDeclaration(node) && node.exportClause && ts.isNamedExports(node.exportClause)) {
|
|
650
|
+
for (const el of node.exportClause.elements) exports.set((el.propertyName ?? el.name).text, el.name.text);
|
|
651
|
+
}
|
|
652
|
+
ts.forEachChild(node, visit);
|
|
653
|
+
};
|
|
654
|
+
visit(source);
|
|
655
|
+
return exports;
|
|
656
|
+
}
|
|
657
|
+
function isRelativeImport(value) {
|
|
658
|
+
return Boolean(value?.startsWith("."));
|
|
659
|
+
}
|
|
660
|
+
function isObjectFunction(node) {
|
|
661
|
+
return ts.isFunctionExpression(node) || ts.isArrowFunction(node) || ts.isMethodDeclaration(node);
|
|
662
|
+
}
|
|
663
|
+
function callName(expr) {
|
|
664
|
+
if (ts.isIdentifier(expr)) return { expression: expr.text, local: expr.text };
|
|
665
|
+
if (ts.isPropertyAccessExpression(expr)) {
|
|
666
|
+
const left = expr.expression.getText();
|
|
667
|
+
return { expression: expr.getText(), local: left === "this" ? void 0 : left, member: expr.name.text };
|
|
668
|
+
}
|
|
669
|
+
return { expression: expr.getText() };
|
|
670
|
+
}
|
|
671
|
+
function nearest(symbols, line) {
|
|
672
|
+
return symbols.filter((s) => s.startLine <= line && s.endLine >= line).sort((a, b) => a.endLine - a.startLine - (b.endLine - b.startLine))[0];
|
|
673
|
+
}
|
|
674
|
+
async function parseExecutableSymbols(repoPath, filePath) {
|
|
675
|
+
const text = await fs4.readFile(path4.join(repoPath, filePath), "utf8");
|
|
676
|
+
const source = ts.createSourceFile(filePath, text, ts.ScriptTarget.Latest, true, filePath.endsWith(".ts") ? ts.ScriptKind.TS : ts.ScriptKind.JS);
|
|
677
|
+
const sourceFile = normalizePath(filePath);
|
|
678
|
+
const symbols = [];
|
|
679
|
+
const calls = [];
|
|
680
|
+
const imports = /* @__PURE__ */ new Map();
|
|
681
|
+
const exportNames = exportDeclarations(source);
|
|
682
|
+
const objectExports = /* @__PURE__ */ new Set();
|
|
683
|
+
const addSymbol = (kind, localName, node, parentName, exportedName) => {
|
|
684
|
+
const declaredExportName = exportedName ?? exportNames.get(parentName ? parentName.split(".")[0] ?? localName : localName);
|
|
685
|
+
const qualifiedName = parentName ? `${parentName}.${localName}` : localName;
|
|
686
|
+
const objectExported = parentName ? objectExports.has(parentName.split(".")[0] ?? "") : false;
|
|
687
|
+
symbols.push({ kind, localName: kind === "object_method" ? qualifiedName : localName, exportedName: objectExported ? qualifiedName : declaredExportName, qualifiedName, sourceFile, startLine: lineOf(source, node.getStart(source)), endLine: lineOf(source, node.getEnd()), startOffset: node.getStart(source), endOffset: node.getEnd(), exported: exported(node) || Boolean(declaredExportName) || objectExported, importExportEvidence: declaredExportName ? { exportedName: declaredExportName, source: "export_declaration" } : objectExported ? { exportedName: qualifiedName, source: "exported_object_literal" } : void 0 });
|
|
688
|
+
};
|
|
689
|
+
const visitImports = (node) => {
|
|
690
|
+
if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) {
|
|
691
|
+
const sourceText = node.moduleSpecifier.text;
|
|
692
|
+
const clause = node.importClause;
|
|
693
|
+
if (clause?.name) imports.set(clause.name.text, sourceText);
|
|
694
|
+
const named = clause?.namedBindings;
|
|
695
|
+
if (named && ts.isNamedImports(named)) for (const el of named.elements) imports.set(el.name.text, sourceText);
|
|
696
|
+
if (named && ts.isNamespaceImport(named)) imports.set(named.name.text, sourceText);
|
|
697
|
+
}
|
|
698
|
+
ts.forEachChild(node, visitImports);
|
|
699
|
+
};
|
|
700
|
+
visitImports(source);
|
|
701
|
+
const visitSymbols = (node, parentClass) => {
|
|
702
|
+
if (ts.isClassDeclaration(node) && node.name) {
|
|
703
|
+
for (const member of node.members) visitSymbols(member, node.name.text);
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
if (ts.isMethodDeclaration(node)) {
|
|
707
|
+
const localName = nameOf(node.name);
|
|
708
|
+
if (localName) addSymbol("method", localName, node, parentClass);
|
|
709
|
+
} else if (ts.isFunctionDeclaration(node) && node.name) addSymbol("function", node.name.text, node, void 0, exported(node) ? node.name.text : void 0);
|
|
710
|
+
else if (ts.isVariableStatement(node)) {
|
|
711
|
+
for (const d of node.declarationList.declarations) {
|
|
712
|
+
const localName = nameOf(d.name);
|
|
713
|
+
if (!localName || !d.initializer) continue;
|
|
714
|
+
if (isFunctionLike(d.initializer)) addSymbol("function", localName, d.initializer, void 0, exported(node) ? localName : exportNames.get(localName));
|
|
715
|
+
if (ts.isObjectLiteralExpression(d.initializer)) {
|
|
716
|
+
if (exported(node) || exportNames.has(localName)) objectExports.add(localName);
|
|
717
|
+
for (const prop of d.initializer.properties) {
|
|
718
|
+
if (ts.isPropertyAssignment(prop) && isObjectFunction(prop.initializer)) {
|
|
719
|
+
const propName = nameOf(prop.name);
|
|
720
|
+
if (propName) addSymbol("object_method", propName, prop.initializer, localName);
|
|
721
|
+
} else if (ts.isMethodDeclaration(prop)) {
|
|
722
|
+
const propName = nameOf(prop.name);
|
|
723
|
+
if (propName) addSymbol("object_method", propName, prop, localName);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
} else ts.forEachChild(node, (child) => visitSymbols(child, parentClass));
|
|
729
|
+
};
|
|
730
|
+
visitSymbols(source);
|
|
731
|
+
const localCallables = new Set(symbols.flatMap((sym) => [sym.localName, sym.qualifiedName]));
|
|
732
|
+
const visitCalls = (node) => {
|
|
733
|
+
if (ts.isCallExpression(node)) {
|
|
734
|
+
const line = lineOf(source, node.getStart(source));
|
|
735
|
+
const caller = nearest(symbols, line);
|
|
736
|
+
if (caller) {
|
|
737
|
+
const callee = callName(node.expression);
|
|
738
|
+
const importSource = (callee.local ? imports.get(callee.local) : void 0) ?? (callee.member && callee.local ? imports.get(callee.local) : void 0);
|
|
739
|
+
const targetName = callee.expression.startsWith("this.") ? callee.member : callee.member && callee.local ? `${callee.local}.${callee.member}` : callee.local;
|
|
740
|
+
const keep = Boolean(targetName) && (localCallables.has(String(targetName)) || callee.expression.startsWith("this.") || isRelativeImport(importSource));
|
|
741
|
+
if (keep) calls.push({ callerQualifiedName: caller.qualifiedName, calleeExpression: callee.expression, calleeLocalName: targetName, receiverLocalName: callee.member ? callee.local : void 0, importSource, sourceFile, sourceLine: line, evidence: { relation: importSource ? "relative_import" : callee.expression.startsWith("this.") ? "this_method" : "local", caller: caller.qualifiedName, targetName } });
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
ts.forEachChild(node, visitCalls);
|
|
745
|
+
};
|
|
746
|
+
visitCalls(source);
|
|
747
|
+
return { symbols, calls };
|
|
748
|
+
}
|
|
596
749
|
|
|
597
750
|
// src/utils/hashing.ts
|
|
598
751
|
import { createHash } from "crypto";
|
|
@@ -621,6 +774,8 @@ async function indexRepository(db, repo, force) {
|
|
|
621
774
|
for (const file of parsed.fileRecords) fileStmt.run(repo.id, file.relativePath, file.extension, file.sha256, file.sizeBytes, (/* @__PURE__ */ new Date()).toISOString());
|
|
622
775
|
for (const s of parsed.services) insertService(db, repo.id, s);
|
|
623
776
|
for (const h of parsed.handlers) insertHandler(db, repo.id, h);
|
|
777
|
+
insertExecutableSymbols(db, repo.id, parsed.symbols);
|
|
778
|
+
insertSymbolCalls(db, repo.id, parsed.symbolCalls);
|
|
624
779
|
insertRegistrations(db, repo.id, parsed.registrations);
|
|
625
780
|
insertBindings(db, repo.id, parsed.bindings);
|
|
626
781
|
insertCalls(db, repo.id, parsed.calls);
|
|
@@ -636,16 +791,19 @@ async function indexRepository(db, repo, force) {
|
|
|
636
791
|
}
|
|
637
792
|
}
|
|
638
793
|
async function parseAllSourceFacts(root, files) {
|
|
639
|
-
const facts = { services: [], handlers: [], registrations: [], bindings: [], calls: [], fileRecords: [] };
|
|
794
|
+
const facts = { services: [], handlers: [], registrations: [], bindings: [], calls: [], symbols: [], symbolCalls: [], fileRecords: [] };
|
|
640
795
|
for (const file of files) {
|
|
641
|
-
const abs =
|
|
642
|
-
const stat = await
|
|
643
|
-
facts.fileRecords.push({ relativePath: normalizePath(file), extension:
|
|
796
|
+
const abs = path5.join(root, file);
|
|
797
|
+
const stat = await fs5.stat(abs);
|
|
798
|
+
facts.fileRecords.push({ relativePath: normalizePath(file), extension: path5.extname(file), sha256: await sha256File(abs), sizeBytes: stat.size });
|
|
644
799
|
if (file.endsWith(".cds")) facts.services.push(...await parseCdsFile(root, file));
|
|
645
800
|
if (/\.[jt]s$/.test(file)) {
|
|
646
801
|
facts.handlers.push(...await parseDecorators(root, file));
|
|
647
802
|
facts.registrations.push(...await parseHandlerRegistrations(root, file));
|
|
648
803
|
facts.bindings.push(...await parseServiceBindings(root, file));
|
|
804
|
+
const symbolFacts = await parseExecutableSymbols(root, file);
|
|
805
|
+
facts.symbols.push(...symbolFacts.symbols);
|
|
806
|
+
facts.symbolCalls.push(...symbolFacts.calls);
|
|
649
807
|
facts.calls.push(...await parseOutboundCalls(root, file));
|
|
650
808
|
}
|
|
651
809
|
}
|
|
@@ -654,11 +812,11 @@ async function parseAllSourceFacts(root, files) {
|
|
|
654
812
|
async function findSourceFiles(root) {
|
|
655
813
|
const out = [];
|
|
656
814
|
async function walk(dir, prefix = "") {
|
|
657
|
-
const entries = await
|
|
815
|
+
const entries = await fs5.readdir(dir, { withFileTypes: true }).catch(() => []);
|
|
658
816
|
for (const e of entries) {
|
|
659
817
|
const rel = prefix ? `${prefix}/${e.name}` : e.name;
|
|
660
818
|
if (e.isDirectory()) {
|
|
661
|
-
if (!["node_modules", "dist", "gen", "coverage", ".git"].includes(e.name)) await walk(
|
|
819
|
+
if (!["node_modules", "dist", "gen", "coverage", ".git"].includes(e.name)) await walk(path5.join(dir, e.name), rel);
|
|
662
820
|
} else if (/\.(cds|ts|js)$/.test(e.name) && !isDefaultTestFile(rel)) out.push(rel);
|
|
663
821
|
}
|
|
664
822
|
}
|
|
@@ -671,7 +829,7 @@ function isDefaultTestFile(relativeFile) {
|
|
|
671
829
|
return /\.(test|spec)\.[jt]s$/.test(parts.at(-1) ?? "");
|
|
672
830
|
}
|
|
673
831
|
async function repositoryFingerprint(root, files, facts) {
|
|
674
|
-
const packageJson = await
|
|
832
|
+
const packageJson = await fs5.readFile(path5.join(root, "package.json"), "utf8").catch(() => "");
|
|
675
833
|
const normalizedFacts = {
|
|
676
834
|
analyzerVersion: ANALYZER_VERSION,
|
|
677
835
|
packageName: facts.packageName,
|
|
@@ -684,7 +842,7 @@ async function repositoryFingerprint(root, files, facts) {
|
|
|
684
842
|
};
|
|
685
843
|
const entries = [`facts:${JSON.stringify(normalizedFacts)}`];
|
|
686
844
|
for (const file of files) {
|
|
687
|
-
const content = await
|
|
845
|
+
const content = await fs5.readFile(path5.join(root, file), "utf8");
|
|
688
846
|
entries.push(`${file}:${sha256Text(content)}`);
|
|
689
847
|
}
|
|
690
848
|
return sha256Text(entries.join("\n"));
|
|
@@ -722,25 +880,25 @@ function parseVars(values) {
|
|
|
722
880
|
}
|
|
723
881
|
return out;
|
|
724
882
|
}
|
|
725
|
-
function startLabel(start) {
|
|
726
|
-
return [
|
|
727
|
-
start.repo,
|
|
728
|
-
start.servicePath,
|
|
729
|
-
start.operation ?? start.operationPath ?? start.handler
|
|
730
|
-
].filter(Boolean).join(" ");
|
|
731
|
-
}
|
|
732
883
|
|
|
733
884
|
// src/output/table-output.ts
|
|
734
|
-
function
|
|
735
|
-
const
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
885
|
+
function location(evidence) {
|
|
886
|
+
const file = evidence.file ?? evidence.sourceFile ?? evidence.handlerSourceFile ?? evidence.operationSourceFile ?? evidence.registrationSourceFile;
|
|
887
|
+
const line = evidence.line ?? evidence.sourceLine ?? evidence.handlerSourceLine ?? evidence.operationSourceLine ?? evidence.registrationSourceLine;
|
|
888
|
+
if (file || line) return `${String(file ?? "")}:${String(line ?? "")}`;
|
|
889
|
+
const candidates = evidence.candidates;
|
|
890
|
+
if (Array.isArray(candidates) && candidates.length > 0) {
|
|
891
|
+
const first = candidates[0];
|
|
892
|
+
return `${String(first.sourceFile ?? "")}:${String(first.sourceLine ?? "")}`;
|
|
893
|
+
}
|
|
894
|
+
return ":";
|
|
895
|
+
}
|
|
896
|
+
function renderTraceTable(result) {
|
|
897
|
+
const lines = ["Step Type From To Evidence"];
|
|
898
|
+
for (const e of result.edges) {
|
|
899
|
+
lines.push(`${String(e.step).padEnd(5)} ${e.type.padEnd(20)} ${e.from.slice(0, 34).padEnd(35)} ${e.to.slice(0, 35).padEnd(36)} ${location(e.evidence)}`);
|
|
900
|
+
}
|
|
901
|
+
if (result.diagnostics.length > 0) lines.push("", "Diagnostics:", ...result.diagnostics.map((d) => `${String(d.severity ?? "info")} ${String(d.code ?? "diagnostic")} ${String(d.message ?? "")}`));
|
|
744
902
|
return `${lines.join("\n")}
|
|
745
903
|
`;
|
|
746
904
|
}
|
|
@@ -758,11 +916,15 @@ function renderTraceJson(trace2) {
|
|
|
758
916
|
function safe(value) {
|
|
759
917
|
return value.replace(/[^\w-]/g, "_").slice(0, 60);
|
|
760
918
|
}
|
|
919
|
+
function label(trace2, idOrLabel) {
|
|
920
|
+
const node = trace2.nodes.find((item) => item.id === idOrLabel || item.label === idOrLabel);
|
|
921
|
+
return String(node?.label ?? idOrLabel);
|
|
922
|
+
}
|
|
761
923
|
function renderMermaid(trace2) {
|
|
762
924
|
const lines = ["flowchart TD"];
|
|
763
925
|
for (const e of trace2.edges)
|
|
764
926
|
lines.push(
|
|
765
|
-
` ${safe(e.from)}["${e.from}"] -->|${e.type}| ${safe(e.to)}["${e.to}"]`
|
|
927
|
+
` ${safe(e.from)}["${label(trace2, e.from)}"] -->|${e.type}| ${safe(e.to)}["${label(trace2, e.to)}"]`
|
|
766
928
|
);
|
|
767
929
|
return `${lines.join("\n")}
|
|
768
930
|
`;
|
|
@@ -846,7 +1008,7 @@ function createProgram() {
|
|
|
846
1008
|
(opts) => void withWorkspace(opts.workspace, (db, workspaceId) => {
|
|
847
1009
|
const r = linkWorkspace(db, workspaceId);
|
|
848
1010
|
process.stdout.write(
|
|
849
|
-
`Linked ${r.edgeCount} edges: ${r.resolvedCount} remote resolved, ${r.unresolvedCount} remote unresolved, ${r.ambiguousCount} remote ambiguous, ${r.dynamicCount} dynamic, ${r.terminalCount} terminal, ${r.dependencyResolvedCount} dependency resolved, ${r.dependencyAmbiguousCount} dependency ambiguous, ${r.implementationResolvedCount} implementation resolved, ${r.implementationAmbiguousCount} implementation ambiguous
|
|
1011
|
+
`Linked ${r.edgeCount} edges: ${r.resolvedCount} remote resolved, ${r.unresolvedCount} remote unresolved, ${r.ambiguousCount} remote ambiguous, ${r.dynamicCount} dynamic, ${r.terminalCount} terminal, ${r.dependencyResolvedCount} dependency resolved, ${r.dependencyAmbiguousCount} dependency ambiguous, ${r.implementationResolvedCount} implementation resolved, ${r.implementationAmbiguousCount} implementation ambiguous, ${r.implementationUnresolvedCount} implementation unresolved
|
|
850
1012
|
`
|
|
851
1013
|
);
|
|
852
1014
|
}).catch(fail)
|
|
@@ -1013,12 +1175,24 @@ function createProgram() {
|
|
|
1013
1175
|
JOIN cds_services s ON s.id=o.service_id
|
|
1014
1176
|
WHERE remote.edge_type='REMOTE_CALL_RESOLVES_TO_OPERATION' AND remote.to_kind='operation' AND NOT EXISTS (SELECT 1 FROM graph_edges impl WHERE impl.edge_type='OPERATION_IMPLEMENTED_BY_HANDLER' AND impl.from_kind='operation' AND impl.from_id=remote.to_id) AND ?
|
|
1015
1177
|
UNION ALL
|
|
1178
|
+
SELECT CASE WHEN ? THEN 'warning' ELSE 'error' END,'local_service_calls_all_unresolved','All local service calls are unresolved; verify local service alias parsing and linking',NULL,NULL
|
|
1179
|
+
WHERE EXISTS (SELECT 1 FROM outbound_calls WHERE call_type='local_service_call') AND NOT EXISTS (SELECT 1 FROM graph_edges e JOIN outbound_calls c ON e.from_kind='call' AND c.id=CAST(e.from_id AS INTEGER) WHERE c.call_type='local_service_call' AND e.status='resolved')
|
|
1180
|
+
UNION ALL
|
|
1181
|
+
SELECT 'error','local_service_accessor_misclassified','Entity accessor calls were indexed as /entities operations',source_file,source_line
|
|
1182
|
+
FROM outbound_calls WHERE call_type='local_service_call' AND operation_path_expr='/entities' AND (? OR 1)
|
|
1183
|
+
UNION ALL
|
|
1184
|
+
SELECT 'warning','outbound_calls_without_source_symbol','Outbound calls lack source symbol ownership: ' || COUNT(*),NULL,NULL
|
|
1185
|
+
FROM outbound_calls WHERE source_symbol_id IS NULL AND ? HAVING COUNT(*) >= 1
|
|
1186
|
+
UNION ALL
|
|
1187
|
+
SELECT 'warning','trace_scope_fell_back_to_file','Trace may fall back to source-file scope for calls without symbols',NULL,NULL
|
|
1188
|
+
WHERE EXISTS (SELECT 1 FROM outbound_calls WHERE source_symbol_id IS NULL) AND ?
|
|
1189
|
+
UNION ALL
|
|
1016
1190
|
SELECT 'warning','graph_stale','Graph is stale after repository fact changes; run service-flow link',NULL,NULL
|
|
1017
1191
|
WHERE EXISTS (SELECT 1 FROM repositories WHERE graph_stale_reason IS NOT NULL)
|
|
1018
1192
|
UNION ALL
|
|
1019
1193
|
SELECT 'warning','index_run_abandoned','Index run ' || id || ' started at ' || started_at || ' is still running after the 60 minute abandonment threshold',NULL,NULL
|
|
1020
1194
|
FROM index_runs WHERE status='running' AND datetime(started_at) < datetime('now','-60 minutes')`
|
|
1021
|
-
).all(Boolean(opts.strict), Boolean(opts.strict), Boolean(opts.strict), Boolean(opts.strict));
|
|
1195
|
+
).all(Boolean(opts.strict), Boolean(opts.strict), Boolean(opts.strict), Boolean(opts.strict), Boolean(opts.strict), Boolean(opts.strict), Boolean(opts.strict), Boolean(opts.strict));
|
|
1022
1196
|
const allDiagnostics = [...diagnostics, ...health];
|
|
1023
1197
|
process.stdout.write(
|
|
1024
1198
|
allDiagnostics.length ? renderJson(allDiagnostics) : `${pc.green("No diagnostics recorded")}
|
|
@@ -1029,20 +1203,20 @@ function createProgram() {
|
|
|
1029
1203
|
program.command("clean").option("--workspace <path>").option("--db-only").action(
|
|
1030
1204
|
(opts) => void (async () => {
|
|
1031
1205
|
const config = await loadWorkspaceConfig(opts.workspace);
|
|
1032
|
-
const dbDir =
|
|
1033
|
-
const workspaceRoot =
|
|
1034
|
-
await
|
|
1206
|
+
const dbDir = path6.resolve(path6.dirname(config.dbPath));
|
|
1207
|
+
const workspaceRoot = path6.resolve(config.rootPath);
|
|
1208
|
+
await fs6.rm(config.dbPath, { force: true });
|
|
1035
1209
|
if (!opts.dbOnly) {
|
|
1036
|
-
const marker =
|
|
1210
|
+
const marker = path6.join(dbDir, ".service-flow-state");
|
|
1037
1211
|
const dangerous = /* @__PURE__ */ new Set([
|
|
1038
|
-
|
|
1212
|
+
path6.parse(dbDir).root,
|
|
1039
1213
|
"/tmp",
|
|
1040
|
-
process.env.HOME ?
|
|
1214
|
+
process.env.HOME ? path6.resolve(process.env.HOME) : "",
|
|
1041
1215
|
workspaceRoot
|
|
1042
1216
|
]);
|
|
1043
1217
|
let ownsState;
|
|
1044
1218
|
try {
|
|
1045
|
-
ownsState = (await
|
|
1219
|
+
ownsState = (await fs6.stat(marker)).isFile();
|
|
1046
1220
|
} catch {
|
|
1047
1221
|
ownsState = false;
|
|
1048
1222
|
}
|
|
@@ -1050,7 +1224,7 @@ function createProgram() {
|
|
|
1050
1224
|
throw new Error(
|
|
1051
1225
|
`Refusing to recursively delete unowned or dangerous state directory: ${dbDir}. Use --db-only to remove only the database file.`
|
|
1052
1226
|
);
|
|
1053
|
-
await
|
|
1227
|
+
await fs6.rm(dbDir, { recursive: true, force: true });
|
|
1054
1228
|
}
|
|
1055
1229
|
process.stdout.write("Cleaned service-flow state\n");
|
|
1056
1230
|
})().catch(fail)
|