@saptools/service-flow 0.1.13 → 0.1.14
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 +9 -0
- package/README.md +1 -0
- package/TECHNICAL-NOTE.md +5 -4
- package/dist/{chunk-Q7W7TE3W.js → chunk-UPXFMLUY.js} +37 -13
- package/dist/chunk-UPXFMLUY.js.map +1 -0
- package/dist/cli.js +61 -16
- package/dist/cli.js.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-Q7W7TE3W.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.14
|
|
4
|
+
|
|
5
|
+
- Made local symbol-call indexing opt-in: CAP DSL, request helpers, package namespace/CommonJS calls, global runtime APIs, and service-client transport helpers are filtered unless indexed local or relative-import evidence makes the edge actionable.
|
|
6
|
+
- Expanded local DB query entity extraction for `SELECT.one(Entity)`, `UPSERT.into(Entity)`, `UPDATE.entity(Entity)`, static element access such as `this.model['Books']`, and clearer dynamic-query warning reasons.
|
|
7
|
+
- Persisted unknown DB query graph targets as semantic `db_entity:unknown` terminal nodes with source `callId` and parser-warning evidence, so fresh relinks no longer store numeric call ids as DB targets.
|
|
8
|
+
- Classified unresolved local service-client `.send`, `.emit`, `.publish`, and `.on` calls as terminal transport/client calls when the model does not declare a matching operation, while preserving real declared operations.
|
|
9
|
+
- Tightened doctor default local-service warnings and added compact `doctor --strict` symbol-call and DB-query quality aggregates with capped top unresolved examples.
|
|
10
|
+
- Added neutral regression fixtures for symbol-call noise, DB query forms, and local service-client methods.
|
|
11
|
+
|
|
3
12
|
## 0.1.13
|
|
4
13
|
|
|
5
14
|
- Added implementation-context fallback for local `cds.services.*` calls so helper packages can resolve model-package operations only when handler/dependency/registration evidence ties the caller repository to the target operation.
|
package/README.md
CHANGED
|
@@ -69,6 +69,7 @@ npm install @saptools/service-flow
|
|
|
69
69
|
- Fresh databases include foreign keys for key graph, run, and diagnostic tables. Migrated legacy stores that still lack that metadata are reported by doctor with `legacy_schema_weaker_foreign_keys`; rebuild into a fresh database if strict structural parity is required.
|
|
70
70
|
- Parser warnings describe analysis completeness, while routing status describes graph behavior. A terminal DB edge can remain terminal while still exposing parser warning evidence about an unknown entity.
|
|
71
71
|
|
|
72
|
+
|
|
72
73
|
## 🚀 Quick Start
|
|
73
74
|
|
|
74
75
|
```bash
|
package/TECHNICAL-NOTE.md
CHANGED
|
@@ -6,13 +6,14 @@
|
|
|
6
6
|
- Trace cycle safety: trace queues carry repository IDs, visited scope keys are independent of depth, graph edge IDs are emitted once, and revisiting an already-seen downstream operation scope creates a cycle marker instead of recursive expansion.
|
|
7
7
|
- SQLite reliability: the package uses a persistent SQLite connection per opened database, bound parameters, transactions, WAL, busy timeouts, read-only openings for query commands, and connection-local foreign-key enforcement. Native driver loading failures produce an actionable startup error before output rendering.
|
|
8
8
|
|
|
9
|
-
## 0.1.
|
|
9
|
+
## 0.1.14 audit follow-up notes
|
|
10
10
|
|
|
11
11
|
- Local CAP service calls keep same-repository service ownership as the strongest resolution path. When the caller repository does not own the CDS service model, the linker searches workspace operations by service identity and operation name/path, then requires caller ownership evidence from implementation edges, ambiguous implementation candidates, registration packages, or resolved dependency/import edges before resolving. Candidate operations without caller evidence are retained with `local_service_candidate_without_caller_ownership` rather than guessed.
|
|
12
12
|
- Trace traversal can use local-call context to choose the caller repository's handler from an otherwise ambiguous global implementation edge. This is scoped to the local call and does not rewrite the global `OPERATION_IMPLEMENTED_BY_HANDLER` ambiguity.
|
|
13
|
-
- CAP DB entity extraction for local `cds.run(...)` calls uses TypeScript AST traversal across chained query expressions, including `SELECT.one.from(Entity).
|
|
14
|
-
-
|
|
15
|
-
- Symbol-call indexing is
|
|
13
|
+
- CAP DB entity extraction for local `cds.run(...)` calls uses TypeScript AST traversal across chained query expressions, including `SELECT.from(Entity)`, `SELECT.one.from(Entity)`, `SELECT.one(Entity)`, `INSERT.into(Entity)`, `UPSERT.into(Entity)`, `UPDATE(Entity)`, `UPDATE.entity(Entity)`, `DELETE.from(Entity)`, and static element access such as `this.model['Books']`. Dynamic or unknowable query targets remain terminal graph edges with parser-warning evidence such as `dynamic_entity_expression`.
|
|
14
|
+
- Fresh relinks persist unknown DB query targets semantically as `to_kind = db_entity` and `to_id = unknown`; no schema migration is required for this row-level graph change. JSON, table, and Mermaid output use `Entity: unknown`, while evidence keeps the source `callId` and parser warning for machines.
|
|
15
|
+
- Symbol-call indexing is opt-in. Same-file symbols, indexed `this.method()` calls, exact relative import/export matches, and exported object-literal helper methods are kept; CAP DSL, request helpers, package namespace/CommonJS calls, global runtime APIs, and generic transport helpers are filtered unless local indexed evidence makes the edge actionable.
|
|
16
|
+
- `doctor --strict` reports compact parser-quality aggregates for symbol calls and local DB query known/unknown ratios; default doctor stays focused on actionable warnings.
|
|
16
17
|
- Generated constants remain low-level parser output through `parseGeneratedConstants`. They are not persisted as graph facts; implementation linking only uses deterministic decorator normalization for common generated action/function names.
|
|
17
18
|
|
|
18
19
|
## 0.1.4 trace-correctness additions
|
|
@@ -559,6 +559,7 @@ function entityFromExpression(expr) {
|
|
|
559
559
|
if (!expr) return void 0;
|
|
560
560
|
if (ts4.isIdentifier(expr) || ts4.isStringLiteral(expr) || ts4.isNoSubstitutionTemplateLiteral(expr)) return expr.text;
|
|
561
561
|
if (ts4.isPropertyAccessExpression(expr) && expr.expression.kind === ts4.SyntaxKind.ThisKeyword) return expr.name.text;
|
|
562
|
+
if (ts4.isElementAccessExpression(expr) && expr.argumentExpression && (ts4.isStringLiteral(expr.argumentExpression) || ts4.isNoSubstitutionTemplateLiteral(expr.argumentExpression))) return expr.argumentExpression.text;
|
|
562
563
|
return void 0;
|
|
563
564
|
}
|
|
564
565
|
function expressionName(expr) {
|
|
@@ -566,29 +567,45 @@ function expressionName(expr) {
|
|
|
566
567
|
if (ts4.isPropertyAccessExpression(expr)) return `${expressionName(expr.expression)}.${expr.name.text}`;
|
|
567
568
|
return expr.getText();
|
|
568
569
|
}
|
|
569
|
-
function
|
|
570
|
-
|
|
570
|
+
function variableInitializers(source) {
|
|
571
|
+
const initializers = /* @__PURE__ */ new Map();
|
|
572
|
+
const visit = (node) => {
|
|
573
|
+
if (ts4.isVariableDeclaration(node) && ts4.isIdentifier(node.name) && node.initializer && (node.parent.flags & ts4.NodeFlags.Const) !== 0) initializers.set(node.name.text, node.initializer);
|
|
574
|
+
ts4.forEachChild(node, visit);
|
|
575
|
+
};
|
|
576
|
+
visit(source);
|
|
577
|
+
return initializers;
|
|
578
|
+
}
|
|
579
|
+
function queryEntityFromAst(expr, initializers = /* @__PURE__ */ new Map()) {
|
|
580
|
+
if (ts4.isParenthesizedExpression(expr) || ts4.isAwaitExpression(expr)) return queryEntityFromAst(expr.expression, initializers);
|
|
581
|
+
if (ts4.isIdentifier(expr) && initializers.has(expr.text)) return queryEntityFromAst(initializers.get(expr.text), initializers);
|
|
571
582
|
if (ts4.isCallExpression(expr)) {
|
|
572
583
|
const name = expressionName(expr.expression);
|
|
573
|
-
if (name === "cds.run") return queryEntityFromAst(expr.arguments[0]);
|
|
574
|
-
if (
|
|
584
|
+
if (name === "cds.run") return queryEntityFromAst(expr.arguments[0], initializers);
|
|
585
|
+
if (["SELECT.one.from", "SELECT.from", "SELECT.one", "INSERT.into", "UPSERT.into", "DELETE.from", "UPDATE.entity"].includes(name)) return entityFromExpression(expr.arguments[0]);
|
|
575
586
|
if (name === "UPDATE") return entityFromExpression(expr.arguments[0]);
|
|
576
587
|
const receiver = ts4.isPropertyAccessExpression(expr.expression) ? expr.expression.expression : void 0;
|
|
577
|
-
if (receiver) return queryEntityFromAst(receiver);
|
|
588
|
+
if (receiver) return queryEntityFromAst(receiver, initializers);
|
|
578
589
|
}
|
|
579
590
|
return void 0;
|
|
580
591
|
}
|
|
581
592
|
function extractQueryEntity(expr) {
|
|
582
593
|
const source = ts4.createSourceFile("query.ts", `const __query = (${expr});`, ts4.ScriptTarget.Latest, true, ts4.ScriptKind.TS);
|
|
594
|
+
const initializers = variableInitializers(source);
|
|
583
595
|
let found;
|
|
584
596
|
const visit = (node) => {
|
|
585
597
|
if (found) return;
|
|
586
|
-
if (ts4.isParenthesizedExpression(node)) found = queryEntityFromAst(node.expression);
|
|
598
|
+
if (ts4.isParenthesizedExpression(node)) found = queryEntityFromAst(node.expression, initializers);
|
|
587
599
|
ts4.forEachChild(node, visit);
|
|
588
600
|
};
|
|
589
601
|
visit(source);
|
|
590
602
|
return found;
|
|
591
603
|
}
|
|
604
|
+
function queryWarning(expr) {
|
|
605
|
+
if (/^\s*[`'"]/.test(expr)) return "raw_sql_or_cql_expression";
|
|
606
|
+
if (/^\s*\w+\s*$/.test(expr)) return "query_variable_without_static_initializer";
|
|
607
|
+
return "dynamic_entity_expression";
|
|
608
|
+
}
|
|
592
609
|
async function parseOutboundCalls(repoPath, filePath) {
|
|
593
610
|
const text = await fs6.readFile(path7.join(repoPath, filePath), "utf8");
|
|
594
611
|
const out = [];
|
|
@@ -620,7 +637,7 @@ async function parseOutboundCalls(repoPath, filePath) {
|
|
|
620
637
|
sourceFile: normalizePath(filePath),
|
|
621
638
|
sourceLine: lineOf3(text, m.index ?? 0),
|
|
622
639
|
confidence: entity ? 0.9 : 0.55,
|
|
623
|
-
unresolvedReason: entity ? void 0 :
|
|
640
|
+
unresolvedReason: entity ? void 0 : queryWarning(expr)
|
|
624
641
|
});
|
|
625
642
|
}
|
|
626
643
|
for (const m of text.matchAll(
|
|
@@ -668,7 +685,8 @@ function parseLocalServiceCalls(text, filePath) {
|
|
|
668
685
|
aliasChain: parsed.chain,
|
|
669
686
|
sourceFile: normalizePath(filePath),
|
|
670
687
|
sourceLine: lineOf3(text, node.getStart(source)),
|
|
671
|
-
confidence: 0.9
|
|
688
|
+
confidence: 0.9,
|
|
689
|
+
unresolvedReason: ["send", "emit", "publish", "on"].includes(parsed.operation) ? "transport_client_method" : void 0
|
|
672
690
|
});
|
|
673
691
|
}
|
|
674
692
|
ts4.forEachChild(node, visit);
|
|
@@ -1320,6 +1338,10 @@ function insertCallEdge(db, workspaceId, call, vars, generation) {
|
|
|
1320
1338
|
const isOperationCall = callType.startsWith("remote") || callType === "local_service_call";
|
|
1321
1339
|
const resolution = isOperationCall ? resolveOperation(db, { servicePath, operationPath: op, serviceName: call.local_service_name, repoId: callType === "local_service_call" ? Number(call.repo_id) : void 0, alias: applyVariables(call.aliasExpr ?? call.alias, vars), destination: destination ? applyVariables(destination, vars) : void 0, isDynamic, hasExplicitOverride: Object.keys(vars).length > 0 || callType === "local_service_call" }, workspaceId) : { status: "unresolved", candidates: [], reasons: [] };
|
|
1322
1340
|
const evidence = callEvidence(call, resolution, servicePath, op, destination ? applyVariables(destination, vars) : void 0);
|
|
1341
|
+
if (callType === "local_service_call" && call.unresolved_reason === "transport_client_method" && !resolution.target && resolution.candidates.length === 0) {
|
|
1342
|
+
db.prepare("INSERT INTO graph_edges(workspace_id,edge_type,status,from_kind,from_id,to_kind,to_id,confidence,evidence_json,is_dynamic,generation) VALUES(?,?,?,?,?,?,?,?,?,?,?)").run(workspaceId, "HANDLER_CALLS_EXTERNAL_HTTP", "terminal", "call", String(call.id), "external", String(op || "transport_client_method"), Number(call.confidence ?? 0.5), JSON.stringify({ ...evidence, classification: "transport_client_method" }), 0, generation);
|
|
1343
|
+
return { status: "terminal" };
|
|
1344
|
+
}
|
|
1323
1345
|
if (resolution.target) {
|
|
1324
1346
|
db.prepare("INSERT INTO graph_edges(workspace_id,edge_type,status,from_kind,from_id,to_kind,to_id,confidence,evidence_json,is_dynamic,generation) VALUES(?,?,?,?,?,?,?,?,?,?,?)").run(workspaceId, callType === "local_service_call" ? "LOCAL_CALL_RESOLVES_TO_OPERATION" : "REMOTE_CALL_RESOLVES_TO_OPERATION", "resolved", "call", String(call.id), "operation", String(resolution.target.operationId), resolution.target.score, JSON.stringify(evidence), isDynamic ? 1 : 0, generation);
|
|
1325
1347
|
return { status: "resolved" };
|
|
@@ -1327,11 +1349,13 @@ function insertCallEdge(db, workspaceId, call, vars, generation) {
|
|
|
1327
1349
|
const edgeType = callType === "local_db_query" ? "HANDLER_RUNS_DB_QUERY" : callType === "external_http" ? "HANDLER_CALLS_EXTERNAL_HTTP" : callType === "async_emit" ? "HANDLER_EMITS_EVENT" : callType === "async_subscribe" ? "EVENT_CONSUMED_BY_HANDLER" : resolution.status === "dynamic" ? "DYNAMIC_EDGE_CANDIDATE" : "UNRESOLVED_EDGE";
|
|
1328
1350
|
const status = edgeType === "DYNAMIC_EDGE_CANDIDATE" ? "dynamic" : resolution.status === "ambiguous" ? "ambiguous" : edgeType === "UNRESOLVED_EDGE" ? "unresolved" : "terminal";
|
|
1329
1351
|
const unresolvedReason = status === "terminal" ? null : String(call.unresolved_reason ?? (resolution.status === "ambiguous" ? "Ambiguous operation candidates require a strong service signal" : resolution.status === "dynamic" ? `Dynamic target requires runtime variable overrides: ${(resolution.reasons.length ? resolution.reasons : ["missing runtime variables"]).join(", ")}` : "No indexed target operation matched"));
|
|
1330
|
-
|
|
1352
|
+
const targetKind = callType === "local_db_query" ? "db_entity" : callType.startsWith("async_") ? "event" : "external";
|
|
1353
|
+
const targetId = callType === "local_db_query" ? String(call.query_entity ?? "unknown") : String(call.event_name_expr ?? op ?? call.id);
|
|
1354
|
+
db.prepare("INSERT INTO graph_edges(workspace_id,edge_type,status,from_kind,from_id,to_kind,to_id,confidence,evidence_json,is_dynamic,unresolved_reason,generation) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)").run(workspaceId, edgeType, status, "call", String(call.id), targetKind, targetId, Number(call.confidence ?? 0.2), JSON.stringify(evidence), isDynamic || resolution.status === "dynamic" ? 1 : 0, unresolvedReason, generation);
|
|
1331
1355
|
return { status };
|
|
1332
1356
|
}
|
|
1333
1357
|
function callEvidence(call, resolution, servicePath, op, destination) {
|
|
1334
|
-
return { sourceFile: call.source_file, sourceLine: call.source_line, file: call.source_file, line: call.source_line, repo: call.repoName, serviceAlias: call.alias, serviceAliasExpr: call.aliasExpr, destination, servicePath, operationPath: op, localServiceName: call.local_service_name, localServiceLookup: call.local_service_lookup, aliasChain: call.alias_chain_json ? JSON.parse(String(call.alias_chain_json)) : void 0, transport: call.call_type === "local_service_call" ? "local" : void 0, targetRepo: resolution.target?.repoName, targetOperation: resolution.target?.operationName, helperChain: call.helperChainJson ? JSON.parse(String(call.helperChainJson)) : void 0, candidates: resolution.candidates, candidateCount: resolution.candidates.length, resolutionStatus: resolution.status, resolutionReasons: resolution.reasons, analysisCompleteness: call.unresolved_reason ? "partial" : "complete", parserWarning: call.unresolved_reason ? { code: "parser_warning", message: call.unresolved_reason } : void 0 };
|
|
1358
|
+
return { sourceFile: call.source_file, sourceLine: call.source_line, file: call.source_file, line: call.source_line, callId: call.id, repo: call.repoName, serviceAlias: call.alias, serviceAliasExpr: call.aliasExpr, destination, servicePath, operationPath: op, localServiceName: call.local_service_name, localServiceLookup: call.local_service_lookup, aliasChain: call.alias_chain_json ? JSON.parse(String(call.alias_chain_json)) : void 0, transport: call.call_type === "local_service_call" ? "local" : void 0, targetRepo: resolution.target?.repoName, targetOperation: resolution.target?.operationName, helperChain: call.helperChainJson ? JSON.parse(String(call.helperChainJson)) : void 0, candidates: resolution.candidates, candidateCount: resolution.candidates.length, resolutionStatus: resolution.status, resolutionReasons: resolution.reasons, analysisCompleteness: call.unresolved_reason ? "partial" : "complete", parserWarning: call.unresolved_reason ? { code: "parser_warning", message: call.unresolved_reason } : void 0 };
|
|
1335
1359
|
}
|
|
1336
1360
|
function linkImplementations(db, workspaceId, generation) {
|
|
1337
1361
|
const operations = db.prepare(`SELECT o.id operationId,o.operation_path operationPath,o.operation_name operationName,s.service_path servicePath,s.repo_id modelRepoId,r.name modelRepo,r.package_name modelPackage,r.kind modelKind FROM cds_operations o JOIN cds_services s ON s.id=o.service_id JOIN repositories r ON r.id=s.repo_id WHERE r.workspace_id=?`).all(workspaceId);
|
|
@@ -1782,7 +1806,7 @@ function edgeTarget(row, evidence) {
|
|
|
1782
1806
|
const operationPath = typeof evidence.operationPath === "string" ? evidence.operationPath : void 0;
|
|
1783
1807
|
const targetOperation = typeof evidence.targetOperation === "string" ? evidence.targetOperation : void 0;
|
|
1784
1808
|
const targetRepo = typeof evidence.targetRepo === "string" ? evidence.targetRepo : "";
|
|
1785
|
-
if (row.edge_type === "HANDLER_RUNS_DB_QUERY") return
|
|
1809
|
+
if (row.edge_type === "HANDLER_RUNS_DB_QUERY") return `Entity: ${row.to_id || "unknown"}`;
|
|
1786
1810
|
return servicePath && operationPath ? `${servicePath}${operationPath}` : targetOperation ? `${targetRepo}:${targetOperation}` : row.to_id;
|
|
1787
1811
|
}
|
|
1788
1812
|
function trace(db, start, options) {
|
|
@@ -1880,7 +1904,7 @@ function trace(db, start, options) {
|
|
|
1880
1904
|
nodes.set(targetNode, opNode ?? {
|
|
1881
1905
|
id: targetNode,
|
|
1882
1906
|
kind: effectiveRow.to_kind,
|
|
1883
|
-
label: effectiveRow.to_id
|
|
1907
|
+
label: effectiveRow.to_kind === "db_entity" ? `Entity: ${effectiveRow.to_id || "unknown"}` : effectiveRow.to_id
|
|
1884
1908
|
});
|
|
1885
1909
|
const to = edgeTarget(effectiveRow, evidence);
|
|
1886
1910
|
edges.push({
|
|
@@ -1963,4 +1987,4 @@ export {
|
|
|
1963
1987
|
linkWorkspace,
|
|
1964
1988
|
trace
|
|
1965
1989
|
};
|
|
1966
|
-
//# sourceMappingURL=chunk-
|
|
1990
|
+
//# sourceMappingURL=chunk-UPXFMLUY.js.map
|