@saptools/service-flow 0.1.33 → 0.1.35
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 +13 -0
- package/TECHNICAL-NOTE.md +5 -0
- package/dist/{chunk-CWJYVIG2.js → chunk-R47GA5VW.js} +52 -7
- package/dist/chunk-R47GA5VW.js.map +1 -0
- package/dist/cli.js +74 -8
- package/dist/cli.js.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-CWJYVIG2.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.35
|
|
4
|
+
|
|
5
|
+
- Hardened OData path precedence so entity key, navigation, and media/property paths with placeholders remain terminal entity evidence instead of dynamic operation candidates.
|
|
6
|
+
- Preserved separate evidence for service-routing placeholders, operation invocation argument placeholders, and entity key placeholders.
|
|
7
|
+
- Render operation-resolved parser entity calls as operation calls in traces while retaining the original parser call type for auditability.
|
|
8
|
+
- Added strict doctor coverage for dynamic remote-entity false positives without indexed operation evidence.
|
|
9
|
+
|
|
10
|
+
## 0.1.34
|
|
11
|
+
|
|
12
|
+
- Prefer indexed CDS operation evidence over heuristic remote-entity classification for service-client operation invocations, while keeping true collection, entity, delete, navigation, and media paths terminal.
|
|
13
|
+
- Added strict doctor collision diagnostics for terminal remote entity edges that look like operation invocations with indexed operation candidates.
|
|
14
|
+
- Persist repository fact analyzer versions and warn during link/strict doctor when force reindex is required after an analyzer upgrade.
|
|
15
|
+
|
|
3
16
|
## 0.1.33
|
|
4
17
|
|
|
5
18
|
- Preserved persisted graph decisions and call-site evidence during trace and graph rendering while keeping contextual runtime resolution as enrichment.
|
package/TECHNICAL-NOTE.md
CHANGED
|
@@ -137,3 +137,8 @@ Schema version 6 adds queryable external target metadata columns to `outbound_ca
|
|
|
137
137
|
### 0.1.17 parser ownership policy
|
|
138
138
|
|
|
139
139
|
Outbound call extraction is AST-based and ignores comments, block comments, and string literals. CAP/service `.on(...)` registrations are indexed only when the receiver has CAP/service evidence, and top-level registrations receive `module:<relative-file>#event:<event-name>:<line>` synthetic owners. Generic event emitters such as desktop or window events are ignored by default rather than guessed as CAP async edges. Unsupported source shapes are surfaced through diagnostics and strict doctor ownerless categories instead of guessed graph edges.
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
## 0.1.35 OData placeholder semantics
|
|
143
|
+
|
|
144
|
+
Service-flow now records OData placeholders by semantic layer. Service-routing placeholders belong to service bindings and can make an operation edge dynamic until runtime variables are supplied. Operation invocation argument placeholders, such as action/function call arguments, remain operation evidence but are not service-routing variables. Entity key placeholders belong to entity addressing, so key, navigation, and media/property paths remain terminal remote entity/query edges unless indexed CDS operation evidence provides a credible operation match.
|
|
@@ -557,13 +557,29 @@ function classifyODataPathIntent(path9, method) {
|
|
|
557
557
|
const hasNavigationSegments = segments.length > 1;
|
|
558
558
|
const entitySegment = entitySegmentFromPath(pathWithoutQuery);
|
|
559
559
|
const placeholderKeys = [...new Set(extractTemplatePlaceholders(rawPath))];
|
|
560
|
-
const
|
|
560
|
+
const firstOpen = firstSegment.indexOf("(");
|
|
561
|
+
const firstClose = firstOpen >= 0 ? matchingClose(firstSegment, firstOpen) : void 0;
|
|
562
|
+
const keyPredicateText = firstOpen >= 0 && firstClose !== void 0 ? firstSegment.slice(firstOpen + 1, firstClose) : "";
|
|
563
|
+
const rawKeyPredicatePlaceholderKeys = [...new Set(extractTemplatePlaceholders(keyPredicateText))];
|
|
564
|
+
const navigationSuffix = hasNavigationSegments ? segments.slice(1).join("/") : void 0;
|
|
565
|
+
const lastSegment = segments.at(-1) ?? "";
|
|
566
|
+
const mediaOrPropertySuffix = hasNavigationSegments ? lastSegment : void 0;
|
|
567
|
+
const invocation = normalizeODataOperationInvocationPath(pathWithoutQuery);
|
|
568
|
+
const operationNameFromInvocation = invocation?.wasInvocation ? invocation.normalizedOperationPath.replace(/^\//, "").split(".").at(-1) : void 0;
|
|
569
|
+
const topLevelName = firstSegment.split("(")[0]?.replace(/^\//, "");
|
|
570
|
+
const topLevelOperationName = operationNameFromInvocation ?? (!hasNavigationSegments && !firstSegment.includes("(") ? topLevelName : void 0);
|
|
571
|
+
const topLevelOperationInvocation = Boolean(invocation?.wasInvocation && looksLikeLowerCamelInvocation(firstSegment));
|
|
572
|
+
const keyPredicatePlaceholderKeys = topLevelOperationInvocation ? [] : rawKeyPredicatePlaceholderKeys;
|
|
573
|
+
const topLevelOperationNameCandidate = Boolean(topLevelOperationName && !hasNavigationSegments && !firstSegment.includes("("));
|
|
574
|
+
const base = { rawPath, method: normalizedMethod, pathWithoutQuery, queryString, hasQueryString: queryIndex >= 0, entitySegment, placeholderKeys, keyPredicatePlaceholderKeys, invocationArgumentPlaceholderKeys: invocation?.invocationArgumentPlaceholderKeys ?? [], navigationSuffix, mediaOrPropertySuffix, hasEntityKeyPredicate: firstOpen >= 0 && firstClose !== void 0, hasNavigationSuffix: hasNavigationSegments, hasMediaOrPropertySuffix: Boolean(mediaOrPropertySuffix && isMediaOrPropertySuffix(mediaOrPropertySuffix)), topLevelOperationName, topLevelOperationNameCandidate, topLevelOperationInvocation };
|
|
561
575
|
if (!rawPath || !rawPath.startsWith("/")) return { ...base, kind: "unknown", reason: "path_missing_or_not_absolute" };
|
|
562
576
|
const upperEntityLike = /^[A-Z][A-Za-z0-9_]*$/.test(entitySegment ?? firstSegment);
|
|
563
|
-
const mediaLike =
|
|
577
|
+
const mediaLike = isMediaOrPropertySuffix(segments.at(-1) ?? "");
|
|
564
578
|
if (normalizedMethod !== "GET") {
|
|
579
|
+
if (invocation?.wasInvocation && looksLikeLowerCamelInvocation(firstSegment)) return { ...base, kind: "operation_invocation", reason: "non_get_balanced_top_level_operation_invocation" };
|
|
565
580
|
if (mediaLike) return { ...base, kind: "entity_media", reason: "non_get_entity_media_stream_path" };
|
|
566
|
-
if (
|
|
581
|
+
if (hasNavigationSegments || firstSegment.includes("(")) return { ...base, kind: normalizedMethod === "DELETE" ? "entity_delete" : "entity_mutation", reason: firstSegment.includes("(") ? "non_get_entity_key_or_navigation_path_shape" : "non_get_entity_navigation_path_shape" };
|
|
582
|
+
if (upperEntityLike) return { ...base, kind: normalizedMethod === "DELETE" ? "entity_delete" : "entity_mutation", reason: "non_get_entity_path_shape" };
|
|
567
583
|
return { ...base, kind: "operation_invocation", reason: "non_get_lowercase_path_may_be_operation" };
|
|
568
584
|
}
|
|
569
585
|
if (queryIndex >= 0) {
|
|
@@ -573,6 +589,7 @@ function classifyODataPathIntent(path9, method) {
|
|
|
573
589
|
}
|
|
574
590
|
if (hasNavigationSegments) return mediaLike ? { ...base, kind: "entity_media", reason: "get_entity_media_stream_path" } : { ...base, kind: "entity_navigation_query", reason: "get_path_has_navigation_segments" };
|
|
575
591
|
if (firstSegment.includes("(")) {
|
|
592
|
+
if (invocation?.wasInvocation && looksLikeLowerCamelInvocation(firstSegment)) return { ...base, kind: "operation_invocation", reason: "get_balanced_top_level_operation_invocation" };
|
|
576
593
|
return looksLikeLowerCamelInvocation(firstSegment) ? { ...base, kind: "operation_invocation", reason: "get_single_lower_camel_segment_has_top_level_invocation" } : { ...base, kind: "entity_key_read", reason: "get_entity_segment_has_key_predicate" };
|
|
577
594
|
}
|
|
578
595
|
if (/^[A-Z][A-Za-z0-9_]*$/.test(firstSegment)) return { ...base, kind: "entity_candidate", reason: "uppercase_collection_segment_without_indexed_entity_evidence" };
|
|
@@ -585,6 +602,9 @@ function entitySegmentFromPath(path9) {
|
|
|
585
602
|
const entity = (open >= 0 ? first.slice(0, open) : first).trim();
|
|
586
603
|
return entity || void 0;
|
|
587
604
|
}
|
|
605
|
+
function isMediaOrPropertySuffix(segment) {
|
|
606
|
+
return ["file", "content", "$value", "metadata", "items"].includes(segment.toLowerCase());
|
|
607
|
+
}
|
|
588
608
|
function looksLikeLowerCamelInvocation(segment) {
|
|
589
609
|
const open = segment.indexOf("(");
|
|
590
610
|
if (open <= 0) return false;
|
|
@@ -2102,9 +2122,22 @@ function insertCallEdge(db, workspaceId, call, vars, generation) {
|
|
|
2102
2122
|
const destination = call.destinationExpr ?? call.requireDestination;
|
|
2103
2123
|
const isDynamic = Boolean(Number(call.isDynamic ?? 0));
|
|
2104
2124
|
const isRemoteEntityCall = callType.startsWith("remote_entity_");
|
|
2105
|
-
const
|
|
2125
|
+
const indexedOperationCandidateCount = operationCandidateCount(db, workspaceId, op, intent.topLevelOperationName);
|
|
2126
|
+
const credibleOperationSignal = Boolean(normalized?.wasInvocation) || Boolean(intent.topLevelOperationNameCandidate) && indexedOperationCandidateCount > 0;
|
|
2127
|
+
const strongEntitySignal = ["entity_media", "entity_delete", "entity_key_read", "entity_navigation_query"].includes(intent.kind) || intent.kind === "entity_mutation" && (intent.hasEntityKeyPredicate || intent.hasNavigationSuffix);
|
|
2128
|
+
const operationLikeRemoteEntity = isRemoteEntityCall && Boolean(op) && credibleOperationSignal && (!strongEntitySignal || indexedOperationCandidateCount > 0);
|
|
2129
|
+
const isOperationCall = operationLikeRemoteEntity || (callType === "remote_action" || callType === "local_service_call" || callType === "remote_query" && Boolean(op));
|
|
2106
2130
|
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: [] };
|
|
2107
|
-
const evidence = callEvidence(call, resolution, servicePath, op, destination ? applyVariables(destination, vars) : void 0, normalized, intent);
|
|
2131
|
+
const evidence = { ...callEvidence(call, resolution, servicePath, op, destination ? applyVariables(destination, vars) : void 0, normalized, intent), indexedOperationCandidateCount, parserCallType: callType };
|
|
2132
|
+
if (isRemoteEntityCall && (resolution.target || resolution.candidates.length > 0 || resolution.status === "dynamic")) {
|
|
2133
|
+
if (resolution.target) {
|
|
2134
|
+
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, "REMOTE_CALL_RESOLVES_TO_OPERATION", "resolved", "call", String(call.id), "operation", String(resolution.target.operationId), resolution.target.score, JSON.stringify({ ...evidence, operationEntityPrecedence: "indexed_operation_over_parser_entity" }), 0, generation);
|
|
2135
|
+
return { status: "resolved", callType };
|
|
2136
|
+
}
|
|
2137
|
+
const status2 = resolution.status === "dynamic" ? "dynamic" : resolution.status === "ambiguous" ? "ambiguous" : "unresolved";
|
|
2138
|
+
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, status2 === "dynamic" ? "DYNAMIC_EDGE_CANDIDATE" : "UNRESOLVED_EDGE", status2, "call", String(call.id), "operation_candidate", op ? `Remote action: ${op}` : "Remote action: unknown path", Number(call.confidence ?? 0.2), JSON.stringify({ ...evidence, operationEntityPrecedence: resolution.candidates.length > 0 ? "parser_entity_with_indexed_operation_candidates" : "parser_entity_operation_candidate_without_indexed_match" }), status2 === "dynamic" ? 1 : 0, unresolvedOperationReason(resolution), generation);
|
|
2139
|
+
return { status: status2, callType };
|
|
2140
|
+
}
|
|
2108
2141
|
if (isRemoteEntityCall) {
|
|
2109
2142
|
const target = buildRemoteQueryTarget({ queryEntity: intent.entitySegment ?? call.query_entity, servicePath, serviceAlias: call.alias, serviceAliasExpr: call.aliasExpr, destination: destination ? applyVariables(destination, vars) : void 0, isDynamic, parserWarning: evidence.parserWarning });
|
|
2110
2143
|
const entityKind = callType.replace("remote_entity_", "remote_entity_");
|
|
@@ -2135,6 +2168,12 @@ function insertCallEdge(db, workspaceId, call, vars, generation) {
|
|
|
2135
2168
|
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(finalEvidence), graphLevelDynamic ? 1 : 0, unresolvedReason, generation);
|
|
2136
2169
|
return { status, callType };
|
|
2137
2170
|
}
|
|
2171
|
+
function operationCandidateCount(db, workspaceId, operationPath, operationName) {
|
|
2172
|
+
if (!operationPath && !operationName) return 0;
|
|
2173
|
+
const normalizedName = operationName ?? operationPath?.replace(/^\//, "").split(".").at(-1);
|
|
2174
|
+
const row = db.prepare(`SELECT COUNT(*) count 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=? AND (o.operation_path=? OR o.operation_path=? OR o.operation_name=?)`).get(workspaceId, operationPath, normalizedName ? `/${normalizedName}` : operationPath, normalizedName);
|
|
2175
|
+
return Number(row?.count ?? 0);
|
|
2176
|
+
}
|
|
2138
2177
|
function unresolvedOperationReason(resolution) {
|
|
2139
2178
|
if (resolution.status === "dynamic") return `Dynamic target requires runtime variable overrides: ${(resolution.reasons.length ? resolution.reasons : ["missing runtime variables"]).join(", ")}`;
|
|
2140
2179
|
if (resolution.candidates.length === 0) return "No indexed target operation matched";
|
|
@@ -2555,6 +2594,11 @@ function handlerScope(db, methodId) {
|
|
|
2555
2594
|
if (!row) return void 0;
|
|
2556
2595
|
return { repoId: row.repoId, files: new Set(row.sourceFile ? [row.sourceFile] : []), symbolId: row.symbolId };
|
|
2557
2596
|
}
|
|
2597
|
+
function traceEdgeType(call, row) {
|
|
2598
|
+
if (row.to_kind === "operation" && row.edge_type === "REMOTE_CALL_RESOLVES_TO_OPERATION") return "remote_action";
|
|
2599
|
+
if (row.to_kind === "operation" && row.edge_type === "LOCAL_CALL_RESOLVES_TO_OPERATION") return "local_service_call";
|
|
2600
|
+
return String(call.call_type);
|
|
2601
|
+
}
|
|
2558
2602
|
function includeCall(type, options) {
|
|
2559
2603
|
if (!options.includeDb && type === "local_db_query") return false;
|
|
2560
2604
|
if (!options.includeExternal && type === "external_http") return false;
|
|
@@ -2848,7 +2892,7 @@ function trace(db, start, options) {
|
|
|
2848
2892
|
const to = edgeTarget(effectiveRow, evidence);
|
|
2849
2893
|
edges.push({
|
|
2850
2894
|
step: current.depth,
|
|
2851
|
-
type:
|
|
2895
|
+
type: traceEdgeType(call, effectiveRow),
|
|
2852
2896
|
from: `${call.repoName}:${call.source_file}:${call.source_line}`,
|
|
2853
2897
|
to,
|
|
2854
2898
|
evidence,
|
|
@@ -2920,6 +2964,7 @@ export {
|
|
|
2920
2964
|
redactText,
|
|
2921
2965
|
redactValue,
|
|
2922
2966
|
normalizeODataOperationInvocationPath,
|
|
2967
|
+
classifyODataPathIntent,
|
|
2923
2968
|
containsSupportedOutboundCall,
|
|
2924
2969
|
parseOutboundCalls,
|
|
2925
2970
|
parseServiceBindings,
|
|
@@ -2929,4 +2974,4 @@ export {
|
|
|
2929
2974
|
linkWorkspace,
|
|
2930
2975
|
trace
|
|
2931
2976
|
};
|
|
2932
|
-
//# sourceMappingURL=chunk-
|
|
2977
|
+
//# sourceMappingURL=chunk-R47GA5VW.js.map
|