@saptools/service-flow 0.1.40 → 0.1.42

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 CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.42
4
+
5
+ - Continued inherited CDS extension operations into the selected base implementation while retaining concrete routing evidence.
6
+ - Reconciled materialized inherited operations on reindex so renamed or removed base operations do not leave stale effective rows.
7
+ - Kept ambiguous or unresolved extension bases without a selected base id and ignored commented CDS `using` declarations.
8
+ - Added positional remote CAP `Service.send(method, path, ...)` classification for proven CAP clients and tightened catch/loop lexical constant resolution.
9
+
10
+ ## 0.1.41
11
+
12
+ - Added imported CDS extension provenance and materialized inherited operations at concrete extension paths without guessing by simple service name.
13
+ - Resolved TypeScript identifier expressions through lexical bindings so module constants work and inaccessible shadowed block values are excluded.
14
+ - Classified positional CAP `Service.send(...)` dispatch only for proven CAP service receivers while leaving generic `send` calls untouched.
15
+ - Bumped the SQLite schema to persist extension/base and operation provenance metadata.
16
+
3
17
  ## 0.1.40
4
18
 
5
19
  - Hardened operation path expression analysis to respect lexical scope, declaration order, aliases, and bounded branch candidates.
@@ -161,6 +161,37 @@ function maskCommentsAndStrings(text) {
161
161
  }
162
162
  return out;
163
163
  }
164
+ function maskComments(text) {
165
+ let out = "";
166
+ let mode = "code";
167
+ for (let i = 0; i < text.length; i += 1) {
168
+ const c = text[i] ?? "";
169
+ const n = text[i + 1] ?? "";
170
+ if (mode === "code" && c === "/" && n === "/") {
171
+ mode = "line";
172
+ out += " ";
173
+ i += 1;
174
+ continue;
175
+ }
176
+ if (mode === "code" && c === "/" && n === "*") {
177
+ mode = "block";
178
+ out += " ";
179
+ i += 1;
180
+ continue;
181
+ }
182
+ if (mode === "line" && c === "\n") mode = "code";
183
+ if (mode === "block" && c === "*" && n === "/") {
184
+ mode = "code";
185
+ out += " ";
186
+ i += 1;
187
+ continue;
188
+ }
189
+ if (mode === "code" && (c === "'" || c === '"' || c === "`")) mode = c === "'" ? "single" : c === '"' ? "double" : "template";
190
+ else if (mode === "single" && c === "'" || mode === "double" && c === '"' || mode === "template" && c === "`") mode = "code";
191
+ out += mode === "line" || mode === "block" ? c === "\n" ? "\n" : " " : c;
192
+ }
193
+ return out;
194
+ }
164
195
  function readAnnotation(text, index) {
165
196
  if (text[index] !== "@") return void 0;
166
197
  let i = index + 1;
@@ -202,6 +233,21 @@ function annotationRawAt(original, masked, index) {
202
233
  const collected = collectAnnotations(masked, index);
203
234
  return { end: collected.end, raw: original.slice(index, collected.end) };
204
235
  }
236
+ function collectUsings(masked) {
237
+ const imports = /* @__PURE__ */ new Map();
238
+ for (const m of masked.matchAll(/\busing\s*\{([^}]*)\}\s*from\s*(['"])(.*?)\2\s*;/gs)) {
239
+ const moduleSpecifier = m[3] ?? "";
240
+ for (const part of (m[1] ?? "").split(",")) {
241
+ const text = part.trim();
242
+ if (!text) continue;
243
+ const alias = /^(\w+)\s+as\s+(\w+)$/.exec(text) ?? /^(\w+)\s*:\s*(\w+)$/.exec(text);
244
+ const importedSymbol = alias?.[1] ?? text;
245
+ const localAlias = alias?.[2] ?? importedSymbol;
246
+ imports.set(localAlias, { importedSymbol, localAlias, moduleSpecifier, importKind: moduleSpecifier.startsWith(".") ? "relative" : "package" });
247
+ }
248
+ }
249
+ return imports;
250
+ }
205
251
  function operationsFromBody(text, maskedBody, bodyOffset, filePath) {
206
252
  return [...maskedBody.matchAll(/\b(action|function|event)\s+(\w+)\s*(?:\(([^)]*)\))?\s*(?:returns\s+([^;{]+))?/g)].map((m) => ({
207
253
  operationType: m[1] ?? "action",
@@ -220,6 +266,7 @@ async function parseCdsFile(repoPath, filePath) {
220
266
  const namespace = /namespace\s+([\w.]+)\s*;/.exec(masked)?.[1];
221
267
  const services = [];
222
268
  const pendingAnnotations = [];
269
+ const usings = collectUsings(maskComments(text));
223
270
  for (const a of masked.matchAll(/@\s*\(/g)) pendingAnnotations.push(annotationRawAt(text, masked, a.index ?? 0));
224
271
  const serviceRegex = /\b(?:(extend)\s+)?(?:(service)\s+)?([A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*)\b/g;
225
272
  let match;
@@ -239,6 +286,7 @@ async function parseCdsFile(repoPath, filePath) {
239
286
  const body = masked.slice(open + 1, end);
240
287
  const name = match[3] ?? "UnknownService";
241
288
  const serviceName = name.split(".").pop() ?? name;
289
+ const imported = isExtend ? usings.get(name) ?? usings.get(serviceName) : void 0;
242
290
  const servicePath = ensureLeadingSlash(pathAnnotation(annotations) ?? serviceName);
243
291
  services.push({
244
292
  namespace,
@@ -248,14 +296,16 @@ async function parseCdsFile(repoPath, filePath) {
248
296
  isExtend,
249
297
  sourceFile: normalizePath(filePath),
250
298
  sourceLine: lineOf(text, match.index),
251
- operations: operationsFromBody(text, body, open + 1, filePath)
299
+ operations: operationsFromBody(text, body, open + 1, filePath),
300
+ extension: isExtend ? { localReference: name, importedSymbol: imported?.importedSymbol, localAlias: imported?.localAlias, moduleSpecifier: imported?.moduleSpecifier, importKind: imported?.importKind ?? "none" } : void 0
252
301
  });
253
302
  serviceRegex.lastIndex = end + 1;
254
303
  }
255
304
  const baseOps = new Map(services.filter((s) => !s.isExtend).map((s) => [s.qualifiedName, s.operations]));
256
305
  for (const service of services.filter((s) => s.isExtend && s.operations.length === 0)) {
306
+ if (service.extension?.moduleSpecifier) continue;
257
307
  const inherited = baseOps.get(service.qualifiedName) ?? baseOps.get(service.serviceName);
258
- if (inherited) service.operations = inherited.map((op) => ({ ...op, sourceFile: service.sourceFile, sourceLine: service.sourceLine }));
308
+ if (inherited) service.operations = inherited.map((op) => ({ ...op, provenance: "inherited" }));
259
309
  }
260
310
  return services;
261
311
  }
@@ -894,37 +944,54 @@ function placeholders(expr) {
894
944
  function isFunctionLikeScope(node) {
895
945
  return ts4.isFunctionLike(node) || ts4.isSourceFile(node);
896
946
  }
897
- function containingScope(node) {
898
- let current = node.parent;
899
- while (current && !isFunctionLikeScope(current)) current = current.parent;
900
- return current ?? node.getSourceFile();
901
- }
902
947
  function nodeContains(parent, child) {
903
- return child.getStart(child.getSourceFile()) >= parent.getStart(parent.getSourceFile()) && child.getEnd() <= parent.getEnd();
904
- }
905
- function nestedFunctionContains(scope, candidate, use) {
906
- let current = candidate.parent;
907
- while (current && current !== scope) {
908
- if (isFunctionLikeScope(current) && !nodeContains(current, use)) return true;
948
+ const source = child.getSourceFile();
949
+ return child.getStart(source) >= parent.getStart(source) && child.getEnd() <= parent.getEnd();
950
+ }
951
+ function declarationScope(node) {
952
+ if (ts4.isParameter(node)) return node.parent;
953
+ const list = node.parent;
954
+ const blockScoped = (list.flags & (ts4.NodeFlags.Const | ts4.NodeFlags.Let)) !== 0;
955
+ let current = list.parent;
956
+ if (!blockScoped) {
957
+ while (current.parent && !isFunctionLikeScope(current)) current = current.parent;
958
+ return current;
959
+ }
960
+ while (current.parent && !ts4.isBlock(current) && !ts4.isSourceFile(current) && !ts4.isModuleBlock(current) && !ts4.isCaseBlock(current) && !ts4.isCatchClause(current) && !ts4.isForStatement(current) && !ts4.isForInStatement(current) && !ts4.isForOfStatement(current) && !isFunctionLikeScope(current)) current = current.parent;
961
+ return current;
962
+ }
963
+ function declarationScopedAncestor(node) {
964
+ let current = node.parent;
965
+ while (current) {
966
+ if (ts4.isCatchClause(current) || ts4.isForStatement(current) || ts4.isForInStatement(current) || ts4.isForOfStatement(current)) return current;
967
+ if (ts4.isSourceFile(current) || ts4.isFunctionLike(current)) return void 0;
909
968
  current = current.parent;
910
969
  }
911
- return false;
970
+ return void 0;
971
+ }
972
+ function isAccessibleDeclaration(declaration, use) {
973
+ const source = use.getSourceFile();
974
+ if (declaration.name.getStart(source) >= use.getStart(source)) return false;
975
+ const scopedAncestor = declarationScopedAncestor(declaration);
976
+ if (scopedAncestor && ts4.isCatchClause(scopedAncestor)) return nodeContains(scopedAncestor.block, use);
977
+ if (scopedAncestor && (ts4.isForStatement(scopedAncestor) || ts4.isForInStatement(scopedAncestor) || ts4.isForOfStatement(scopedAncestor))) return nodeContains(scopedAncestor.statement, use);
978
+ const scope = declarationScope(declaration);
979
+ if (ts4.isCatchClause(scope)) return nodeContains(scope.block, use);
980
+ if (ts4.isForStatement(scope) || ts4.isForInStatement(scope) || ts4.isForOfStatement(scope)) return nodeContains(scope.statement, use);
981
+ return ts4.isSourceFile(scope) || nodeContains(scope, use);
912
982
  }
913
983
  function resolveBinding(identifier, use) {
914
- const scope = containingScope(use);
915
984
  const source = use.getSourceFile();
916
985
  let best;
917
986
  const visit = (node) => {
918
- if (node !== scope && isFunctionLikeScope(node)) return;
919
- if (node.getStart(source) >= identifier.getStart(source)) return;
920
- if (ts4.isVariableDeclaration(node) && ts4.isIdentifier(node.name) && node.name.text === identifier.text && !nestedFunctionContains(scope, node, use)) best = node;
921
- if (ts4.isParameter(node) && ts4.isIdentifier(node.name) && node.name.text === identifier.text && !nestedFunctionContains(scope, node, use)) best = node;
987
+ if (ts4.isVariableDeclaration(node) && ts4.isIdentifier(node.name) && node.name.text === identifier.text && isAccessibleDeclaration(node, use)) best = node;
988
+ if (ts4.isParameter(node) && ts4.isIdentifier(node.name) && node.name.text === identifier.text && isAccessibleDeclaration(node, use)) best = node;
922
989
  ts4.forEachChild(node, visit);
923
990
  };
924
- visit(scope);
991
+ visit(source);
925
992
  if (!best) return { immutable: false, evidence: ["binding_not_found"] };
926
993
  const immutable = ts4.isVariableDeclaration(best) && (best.parent.flags & ts4.NodeFlags.Const) !== 0;
927
- return { declaration: best, initializer: ts4.isVariableDeclaration(best) ? best.initializer : void 0, immutable, evidence: [immutable ? "const_binding_before_use" : "mutable_or_parameter_binding"] };
994
+ return { declaration: best, initializer: ts4.isVariableDeclaration(best) ? best.initializer : void 0, immutable, evidence: [immutable ? "lexical_const_binding_before_use" : "lexical_mutable_or_parameter_binding"] };
928
995
  }
929
996
  function resolveExpression(expr, use, policy, depth = 0, seen = /* @__PURE__ */ new Set()) {
930
997
  if (!expr) return { status: "unknown", sourceKind: "dynamic_expression", placeholderKeys: [], evidence: ["expression_missing"] };
@@ -964,7 +1031,6 @@ function staticPathCandidates(identifier, call, method) {
964
1031
  const binding = resolveBinding(identifier, call);
965
1032
  const declaration = binding.declaration;
966
1033
  if (!declaration) return void 0;
967
- const scope = containingScope(call);
968
1034
  const source = call.getSourceFile();
969
1035
  const paths = [];
970
1036
  let hasDynamicAssignments = false;
@@ -975,12 +1041,15 @@ function staticPathCandidates(identifier, call, method) {
975
1041
  };
976
1042
  if (ts4.isVariableDeclaration(declaration)) addExpr(declaration.initializer, declaration);
977
1043
  const visit = (node) => {
978
- if (node !== scope && isFunctionLikeScope(node)) return;
1044
+ if (node !== source && isFunctionLikeScope(node) && !nodeContains(node, call)) return;
979
1045
  if (node.getStart(source) >= call.getStart(source)) return;
980
- if (ts4.isBinaryExpression(node) && node.operatorToken.kind === ts4.SyntaxKind.EqualsToken && ts4.isIdentifier(node.left) && node.left.text === identifier.text) addExpr(node.right, node);
1046
+ if (ts4.isBinaryExpression(node) && node.operatorToken.kind === ts4.SyntaxKind.EqualsToken && ts4.isIdentifier(node.left)) {
1047
+ const target = resolveBinding(node.left, node);
1048
+ if (target.declaration === declaration) addExpr(node.right, node);
1049
+ }
981
1050
  ts4.forEachChild(node, visit);
982
1051
  };
983
- visit(scope);
1052
+ visit(source);
984
1053
  const candidatePaths = [...new Set(paths)];
985
1054
  if (candidatePaths.length === 0) return void 0;
986
1055
  const normalizedCandidateOperations = [...new Set(candidatePaths.map((candidate) => classifyODataPathIntent(candidate, method).topLevelOperationName).filter((value) => Boolean(value)))];
@@ -1191,6 +1260,20 @@ function classifyOutboundCallsInSource(source, filePath) {
1191
1260
  const entityCallType = entityCallTypes[intent.kind];
1192
1261
  const isODataQueryRead = method.toUpperCase() === "GET" && ["entity_query", "entity_key_read", "entity_navigation_query"].includes(intent.kind);
1193
1262
  add(node, { callType: query ? "remote_query" : entityCallType ?? (isODataQueryRead ? "remote_query" : "remote_action"), serviceVariableName: receiver, method, operationPathExpr, queryEntity: query ? extractQueryEntity(query) : isODataQueryRead ? intent.entitySegment : void 0, payloadSummary: summarizeExpression(objectArg.getText(source)), confidence: op || query ? 0.8 : 0.4, unresolvedReason: !query && pathExpr && !operationPathExpr ? "dynamic_operation_path_identifier" : void 0 }, { receiver, classifier: "service_client_send_object", operationPathExpression: shorthandPath ? op : void 0, rawPathExpression: resolvedPath.rawExpression, literalPathSource: resolvedPath.text ? resolvedPath.sourceKind === "const" ? "same_scope_const_initializer" : resolvedPath.sourceKind : void 0, odataPathIntent: operationPathExpr ? intent : void 0, staticPathCandidates: candidateEvidence2, parserWarning: !query && pathExpr && !operationPathExpr ? "dynamic_operation_path_identifier" : void 0 });
1263
+ } else {
1264
+ const receiver = receiverName(expr.expression);
1265
+ const rootReceiver = rootReceiverName(expr.expression);
1266
+ const method = resolveExpression(node.arguments[0], node, "literal").value?.toUpperCase();
1267
+ const pathArg = node.arguments[1];
1268
+ const supported = method && ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD"].includes(method);
1269
+ if (receiver && supported && serviceVariables.has(rootReceiver ?? receiver)) {
1270
+ const resolvedPath = staticPathExpression(pathArg, node);
1271
+ const operationPathExpr = operationPathFromStatic(resolvedPath.text);
1272
+ const intent = classifyODataPathIntent(operationPathExpr, method);
1273
+ add(node, { callType: "remote_action", serviceVariableName: rootReceiver ?? receiver, method, operationPathExpr, payloadSummary: summarizeExpression(node.getText(source)), confidence: operationPathExpr ? 0.8 : 0.45, unresolvedReason: operationPathExpr ? void 0 : "dynamic_operation_path_identifier" }, { receiver, rootReceiver, classifier: "service_client_send_method_path", rawPathExpression: resolvedPath.rawExpression, literalPathSource: resolvedPath.text ? resolvedPath.sourceKind === "const" ? "same_scope_const_initializer" : resolvedPath.sourceKind : void 0, odataPathIntent: operationPathExpr ? intent : void 0, parserWarning: operationPathExpr ? void 0 : "dynamic_operation_path_identifier" });
1274
+ } else if (receiver && serviceVariables.has(rootReceiver ?? receiver)) {
1275
+ add(node, { callType: "remote_action", serviceVariableName: rootReceiver ?? receiver, method, payloadSummary: summarizeExpression(node.getText(source)), confidence: 0.35, unresolvedReason: "unsupported_cap_send_signature" }, { receiver, rootReceiver, classifier: "service_client_send_unsupported_signature" });
1276
+ }
1194
1277
  }
1195
1278
  } else if (ts4.isCallExpression(expr) && ts4.isIdentifier(expr.expression) && wrapperSpecs.has(expr.expression.text) || ts4.isIdentifier(expr) && wrapperSpecs.has(expr.text)) {
1196
1279
  const wrapperName = ts4.isIdentifier(expr) ? expr.text : ts4.isCallExpression(expr) && ts4.isIdentifier(expr.expression) ? expr.expression.text : "";
@@ -1251,7 +1334,7 @@ function parseLocalServiceCalls(text, filePath) {
1251
1334
  if (origin) aliases.set(node.name.text, { ...origin, chain: [...origin.chain, node.name.text] });
1252
1335
  }
1253
1336
  if (ts4.isCallExpression(node)) {
1254
- const parsed = serviceOperationCall(node.expression, aliases);
1337
+ const parsed = serviceOperationCall(node, aliases);
1255
1338
  if (parsed && parsed.operation !== "entities") calls.push({
1256
1339
  callType: "local_service_call",
1257
1340
  operationPathExpr: `/${parsed.operation}`,
@@ -1264,7 +1347,8 @@ function parseLocalServiceCalls(text, filePath) {
1264
1347
  confidence: 0.9,
1265
1348
  unresolvedReason: ["send", "emit", "publish", "on"].includes(parsed.operation) ? "transport_client_method" : void 0,
1266
1349
  evidence: parserEvidence(source, node, {
1267
- classifier: "local_cap_service_call",
1350
+ classifier: parsed.classifier,
1351
+ parserCallType: parsed.operation === "send" ? "transport_client_method" : parsed.classifier,
1268
1352
  localServiceLookup: parsed.lookup,
1269
1353
  localServiceName: parsed.service,
1270
1354
  operation: parsed.operation,
@@ -1283,12 +1367,19 @@ function serviceLookup(expr, aliases) {
1283
1367
  if (ts4.isElementAccessExpression(expr) && expr.expression.getText() === "cds.services" && ts4.isStringLiteral(expr.argumentExpression)) return { service: expr.argumentExpression.text, lookup: expr.getText(), chain: [expr.getText()] };
1284
1368
  return void 0;
1285
1369
  }
1286
- function serviceOperationCall(expr, aliases) {
1370
+ function serviceOperationCall(node, aliases) {
1371
+ const expr = node.expression;
1287
1372
  if (!ts4.isPropertyAccessExpression(expr)) return void 0;
1288
- const operation = expr.name.text;
1289
1373
  const origin = serviceLookup(expr.expression, aliases);
1290
1374
  if (!origin) return void 0;
1291
- return { ...origin, operation };
1375
+ if (expr.name.text === "send") {
1376
+ const first = literalText(node.arguments[0]);
1377
+ const second = literalText(node.arguments[1]);
1378
+ const method = first?.toUpperCase();
1379
+ if (method && ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD"].includes(method) && second) return { ...origin, operation: second.replace(/^\//, ""), classifier: "cap_service_send_method_path" };
1380
+ if (first) return { ...origin, operation: first.replace(/^\//, ""), classifier: "cap_service_send_local_dispatch" };
1381
+ }
1382
+ return { ...origin, operation: expr.name.text, classifier: "local_cap_service_call" };
1292
1383
  }
1293
1384
 
1294
1385
  // src/parsers/service-binding-parser.ts
@@ -2416,13 +2507,14 @@ function callEvidence(call, resolution, servicePath, op, destination, normalized
2416
2507
  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, rawOperationPath: normalized?.wasInvocation ? normalized.rawOperationPath : odataPathIntent?.rawPath, normalizedOperationPath: normalized?.wasInvocation ? normalized.normalizedOperationPath : void 0, invocationArgumentPlaceholderKeys: normalized?.invocationArgumentPlaceholderKeys.length ? normalized.invocationArgumentPlaceholderKeys : void 0, odataOperationNormalizationReason: normalized?.normalizationReason, odataOperationNormalizationRejectedReason: normalized?.normalizationRejectedReason, localServiceName: call.local_service_name, localServiceLookup: call.local_service_lookup, aliasChain: parseJson(call.alias_chain_json), transport: call.call_type === "local_service_call" ? "local" : void 0, targetRepo: resolution.target?.repoName, targetServicePath: resolution.target?.servicePath, targetOperationPath: resolution.target?.operationPath, targetOperation: resolution.target?.operationName, helperChain: parseJson(call.helperChainJson), candidates: resolution.candidates, candidateCount: resolution.candidates.length, resolutionStatus: resolution.status, resolutionReasons: resolution.reasons, odataPathIntent, queryStringPresent: odataPathIntent?.hasQueryString || void 0, queryPlaceholderKeys: odataPathIntent?.placeholderKeys.length ? odataPathIntent.placeholderKeys : void 0, bindingHasDynamicExpression: bindingHasDynamicExpression || void 0, outboundEvidence: objectJson(call.evidence_json), analysisCompleteness: call.unresolved_reason ? "partial" : "complete", parserWarning: call.unresolved_reason ? { code: "parser_warning", message: call.unresolved_reason } : void 0 };
2417
2508
  }
2418
2509
  function linkImplementations(db, workspaceId, generation) {
2419
- 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);
2510
+ const operations = db.prepare(`SELECT o.id operationId,o.operation_path operationPath,o.operation_name operationName,o.provenance provenance,o.base_operation_id baseOperationId,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);
2420
2511
  let edgeCount = 0;
2421
2512
  let resolvedCount = 0;
2422
2513
  let ambiguousCount = 0;
2423
2514
  let unresolvedCount = 0;
2424
2515
  for (const operation of operations) {
2425
- const candidates = rankedImplementationCandidates(db, workspaceId, operation);
2516
+ const implementationContext = implementationContextForOperation(db, operation);
2517
+ const candidates = rankedImplementationCandidates(db, workspaceId, implementationContext);
2426
2518
  if (candidates.length === 0) continue;
2427
2519
  const accepted = candidates.filter((candidate) => candidate.accepted);
2428
2520
  const topScore = accepted[0]?.score ?? 0;
@@ -2433,6 +2525,9 @@ function linkImplementations(db, workspaceId, generation) {
2433
2525
  operationPath: operation.operationPath,
2434
2526
  operationName: operation.operationName,
2435
2527
  modelPackage: { id: operation.modelRepoId, name: operation.modelRepo, packageName: operation.modelPackage },
2528
+ implementationSource: implementationContext.operationId === operation.operationId ? "direct_or_concrete_override" : "inherited_from_base_operation",
2529
+ baseOperationId: operation.baseOperationId,
2530
+ implementationOperationId: implementationContext.operationId,
2436
2531
  candidates: candidates.map((candidate, index) => candidateEvidence(candidate, index + 1))
2437
2532
  };
2438
2533
  if (accepted.length === 0) {
@@ -2448,6 +2543,12 @@ function linkImplementations(db, workspaceId, generation) {
2448
2543
  }
2449
2544
  return { edgeCount, resolvedCount, ambiguousCount, unresolvedCount };
2450
2545
  }
2546
+ function implementationContextForOperation(db, operation) {
2547
+ if (operation.provenance !== "inherited" || !operation.baseOperationId) return operation;
2548
+ const base = db.prepare(`SELECT o.id operationId,o.operation_path operationPath,o.operation_name operationName,o.provenance provenance,o.base_operation_id baseOperationId,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 o.id=?`).get(operation.baseOperationId);
2549
+ if (!base) return operation;
2550
+ return { ...base, effectiveOperationId: operation.operationId, effectiveServicePath: operation.servicePath, effectiveOperationPath: operation.operationPath };
2551
+ }
2451
2552
  function rankedImplementationCandidates(db, workspaceId, operation) {
2452
2553
  const rows2 = implementationCandidates(db, workspaceId, operation);
2453
2554
  return deduplicateCandidates(rows2.map((row) => scoreImplementationCandidate(row, operation))).sort((a, b) => b.score - a.score || String(a.className).localeCompare(String(b.className)) || a.methodId - b.methodId);
@@ -3191,4 +3292,4 @@ export {
3191
3292
  linkWorkspace,
3192
3293
  trace
3193
3294
  };
3194
- //# sourceMappingURL=chunk-774PPGGK.js.map
3295
+ //# sourceMappingURL=chunk-RP3BJ64F.js.map