@saptools/service-flow 0.1.49 → 0.1.51

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.
@@ -315,6 +315,46 @@ import fs4 from "fs/promises";
315
315
  import path5 from "path";
316
316
  import ts2 from "typescript";
317
317
 
318
+ // src/linker/operation-decorator-normalizer.ts
319
+ function lowerFirst(value) {
320
+ return value ? `${value[0]?.toLowerCase() ?? ""}${value.slice(1)}` : value;
321
+ }
322
+ function normalizedOperationName(value) {
323
+ return value.replace(/^\//, "");
324
+ }
325
+ function clean(value) {
326
+ return value.replace(/^[`'"]|[`'"]$/g, "");
327
+ }
328
+ function generatedOperationNameFromConstant(value) {
329
+ for (const prefix of ["Action", "Func"]) {
330
+ if (value.startsWith(prefix) && value.length > prefix.length && /^[A-Z]/.test(value.slice(prefix.length))) return lowerFirst(value.slice(prefix.length));
331
+ }
332
+ return void 0;
333
+ }
334
+ function resolved(value, raw) {
335
+ const literal2 = clean(value);
336
+ const generated = generatedOperationNameFromConstant(literal2);
337
+ return { status: "resolved", operationName: generated ?? normalizedOperationName(literal2), raw };
338
+ }
339
+ function normalizeDecoratorOperationSignal(value, raw, candidateOperation) {
340
+ if (value) return resolved(value, raw);
341
+ if (!raw || raw.trim().length === 0) return { status: "none", raw };
342
+ const expression = raw.trim();
343
+ const nameMatch = /(?:^|\.)(Action[A-Z][\w$]*|Func[A-Z][\w$]*)\.name$/.exec(expression);
344
+ if (nameMatch?.[1]) return resolved(nameMatch[1], expression);
345
+ const stringMatch = /^String\(([A-Za-z_$][\w$]*)\)$/.exec(expression);
346
+ if (stringMatch?.[1]) {
347
+ const identifier = stringMatch[1];
348
+ const generated = generatedOperationNameFromConstant(identifier);
349
+ const normalizedCandidate2 = candidateOperation ? normalizedOperationName(candidateOperation) : void 0;
350
+ if (generated) return { status: "resolved", operationName: generated, raw: expression };
351
+ if (normalizedCandidate2 && identifier === normalizedCandidate2) return { status: "resolved", operationName: identifier, raw: expression };
352
+ return { status: "unsupported", raw: expression, reason: "string_wrapper_identifier_not_resolved" };
353
+ }
354
+ if (/^[`'"]/.test(expression) && !/[`'"]$/.test(expression)) return { status: "malformed", raw: expression, reason: "unterminated_literal" };
355
+ return { status: "unsupported", raw: expression, reason: "unsupported_decorator_expression" };
356
+ }
357
+
318
358
  // src/parsers/ts-project.ts
319
359
  import ts from "typescript";
320
360
  function createSourceFile(filePath, text) {
@@ -340,16 +380,102 @@ function callName(d) {
340
380
  }
341
381
  function firstArg(d) {
342
382
  const e = d.expression;
343
- return ts2.isCallExpression(e) && e.arguments[0] ? e.arguments[0].getText() : "";
383
+ return ts2.isCallExpression(e) ? e.arguments[0] : void 0;
384
+ }
385
+ function unwrapExpression(expression) {
386
+ let current = expression;
387
+ while (ts2.isParenthesizedExpression(current) || ts2.isAsExpression(current) || ts2.isTypeAssertionExpression(current) || ts2.isSatisfiesExpression(current)) current = current.expression;
388
+ return current;
389
+ }
390
+ function stringValue(expression) {
391
+ if (!expression) return void 0;
392
+ const unwrapped = unwrapExpression(expression);
393
+ return ts2.isStringLiteralLike(unwrapped) ? unwrapped.text : void 0;
394
+ }
395
+ function propertyName(node) {
396
+ if (ts2.isIdentifier(node) || ts2.isStringLiteralLike(node)) return node.text;
397
+ return void 0;
398
+ }
399
+ function collectEnumMembers(statement, lookups) {
400
+ for (const member of statement.members) {
401
+ const name = propertyName(member.name);
402
+ const value = stringValue(member.initializer);
403
+ if (name && value !== void 0)
404
+ lookups.enumMembers.set(`${statement.name.text}.${name}`, value);
405
+ }
406
+ }
407
+ function collectObjectProperties(name, initializer, lookups) {
408
+ const object = unwrapExpression(initializer);
409
+ if (!ts2.isObjectLiteralExpression(object)) return;
410
+ for (const property of object.properties) {
411
+ if (!ts2.isPropertyAssignment(property)) continue;
412
+ const key = propertyName(property.name);
413
+ const value = stringValue(property.initializer);
414
+ if (key && value !== void 0)
415
+ lookups.objectProperties.set(`${name}.${key}`, value);
416
+ }
417
+ }
418
+ function collectStringLookups(source) {
419
+ const lookups = {
420
+ identifiers: /* @__PURE__ */ new Map(),
421
+ enumMembers: /* @__PURE__ */ new Map(),
422
+ objectProperties: /* @__PURE__ */ new Map()
423
+ };
424
+ for (const statement of source.statements) {
425
+ if (ts2.isEnumDeclaration(statement)) collectEnumMembers(statement, lookups);
426
+ if (!ts2.isVariableStatement(statement) || !(statement.declarationList.flags & ts2.NodeFlags.Const)) continue;
427
+ for (const declaration of statement.declarationList.declarations) {
428
+ if (!ts2.isIdentifier(declaration.name) || !declaration.initializer) continue;
429
+ const value = stringValue(declaration.initializer);
430
+ if (value !== void 0) lookups.identifiers.set(declaration.name.text, value);
431
+ collectObjectProperties(declaration.name.text, declaration.initializer, lookups);
432
+ }
433
+ }
434
+ return lookups;
435
+ }
436
+ function unresolved(rawExpression, reason) {
437
+ return { rawExpression, resolutionKind: "unresolved", unresolvedReason: reason };
438
+ }
439
+ function generatedConstant(rawExpression) {
440
+ const match = /(?:^|\.)(Action[A-Z][\w$]*|Func[A-Z][\w$]*)\.name$/.exec(rawExpression.trim());
441
+ return match?.[1] ? generatedOperationNameFromConstant(match[1]) : void 0;
442
+ }
443
+ function resolveDecoratorArgument(argument, lookups) {
444
+ if (!argument) return unresolved("", "decorator_argument_missing");
445
+ const rawExpression = argument.getText();
446
+ const expression = unwrapExpression(argument);
447
+ if (ts2.isStringLiteralLike(expression))
448
+ return { rawExpression, resolvedValue: expression.text, resolutionKind: "literal" };
449
+ if (ts2.isIdentifier(expression)) {
450
+ const value = lookups.identifiers.get(expression.text);
451
+ return value === void 0 ? unresolved(rawExpression, "identifier_not_resolved_to_local_const_string") : { rawExpression, resolvedValue: value, resolutionKind: "const_identifier" };
452
+ }
453
+ if (ts2.isPropertyAccessExpression(expression) && ts2.isIdentifier(expression.expression)) {
454
+ const key = `${expression.expression.text}.${expression.name.text}`;
455
+ const enumValue = lookups.enumMembers.get(key);
456
+ if (enumValue !== void 0)
457
+ return { rawExpression, resolvedValue: enumValue, resolutionKind: "enum_member" };
458
+ const objectValue2 = lookups.objectProperties.get(key);
459
+ if (objectValue2 !== void 0)
460
+ return { rawExpression, resolvedValue: objectValue2, resolutionKind: "const_object_property" };
461
+ }
462
+ const generatedValue = generatedConstant(rawExpression);
463
+ if (generatedValue !== void 0)
464
+ return {
465
+ rawExpression,
466
+ resolvedValue: generatedValue,
467
+ resolutionKind: "generated_constant_name"
468
+ };
469
+ if (ts2.isPropertyAccessExpression(expression))
470
+ return unresolved(rawExpression, "property_access_not_resolved_to_local_string");
471
+ return unresolved(rawExpression, "unsupported_decorator_expression");
344
472
  }
345
473
  async function parseDecorators(repoPath, filePath) {
346
474
  const text = await fs4.readFile(path5.join(repoPath, filePath), "utf8");
347
475
  const sf = createSourceFile(filePath, text);
348
- const constants = /* @__PURE__ */ new Map();
476
+ const lookups = collectStringLookups(sf);
349
477
  const handlers = [];
350
478
  function visit(node) {
351
- if (ts2.isVariableDeclaration(node) && ts2.isIdentifier(node.name) && node.initializer && ts2.isStringLiteralLike(node.initializer))
352
- constants.set(node.name.text, node.initializer.text);
353
479
  if (ts2.isClassDeclaration(node)) {
354
480
  const className = node.name?.text ?? "AnonymousHandler";
355
481
  const hasHandler = decs(node).some((d) => callName(d) === "Handler");
@@ -357,13 +483,13 @@ async function parseDecorators(repoPath, filePath) {
357
483
  (m) => decs(m).filter(
358
484
  (d) => ["Func", "Action", "On", "Event"].includes(callName(d))
359
485
  ).map((d) => {
360
- const raw = firstArg(d);
361
- const value = raw.startsWith('"') || raw.startsWith("'") || raw.startsWith("`") ? stripQuotes(raw) : constants.get(raw) ?? (raw.endsWith(".name") ? raw.split(".").at(-2) : void 0);
486
+ const decoratorResolution = resolveDecoratorArgument(firstArg(d), lookups);
362
487
  return {
363
488
  methodName: m.name.getText(),
364
489
  decoratorKind: callName(d),
365
- decoratorValue: value,
366
- decoratorRawExpression: raw,
490
+ decoratorValue: decoratorResolution.resolvedValue,
491
+ decoratorRawExpression: decoratorResolution.rawExpression,
492
+ decoratorResolution,
367
493
  sourceFile: normalizePath(filePath),
368
494
  sourceLine: line(sf, m.getStart())
369
495
  };
@@ -588,7 +714,7 @@ import ts4 from "typescript";
588
714
  function lineOf3(sf, node) {
589
715
  return sf.getLineAndCharacterOfPosition(node.getStart(sf)).line + 1;
590
716
  }
591
- function stringValue(node) {
717
+ function stringValue2(node) {
592
718
  if (!node) return void 0;
593
719
  if (ts4.isStringLiteralLike(node) || ts4.isNoSubstitutionTemplateLiteral(node)) return node.text;
594
720
  if (ts4.isTemplateExpression(node)) return node.getText().replace(/^`|`$/g, "");
@@ -609,7 +735,7 @@ function connectFactFromCall(call) {
609
735
  let alias;
610
736
  let aliasExpr;
611
737
  if (ts4.isStringLiteralLike(first) || ts4.isNoSubstitutionTemplateLiteral(first)) alias = first.text;
612
- else if (!ts4.isObjectLiteralExpression(first)) aliasExpr = stringValue(first);
738
+ else if (!ts4.isObjectLiteralExpression(first)) aliasExpr = stringValue2(first);
613
739
  if ((ts4.isStringLiteralLike(first) || ts4.isNoSubstitutionTemplateLiteral(first)) && !objectArg) return { alias: first.text, isDynamic: false, placeholders: [] };
614
740
  if (!objectArg && aliasExpr) return { aliasExpr, isDynamic: true, placeholders: placeholders(aliasExpr) };
615
741
  const expressions = objectArg ? objectExpressions(objectArg) : {};
@@ -622,8 +748,8 @@ function objectExpressions(objectArg) {
622
748
  for (const prop of obj.properties) {
623
749
  if (!ts4.isPropertyAssignment(prop)) continue;
624
750
  const name = ts4.isIdentifier(prop.name) || ts4.isStringLiteralLike(prop.name) ? prop.name.text : void 0;
625
- if (name === "destination") out.destinationExpr = stringValue(prop.initializer);
626
- if (name === "path" || name === "servicePath") out.servicePathExpr = stringValue(prop.initializer);
751
+ if (name === "destination") out.destinationExpr = stringValue2(prop.initializer);
752
+ if (name === "path" || name === "servicePath") out.servicePathExpr = stringValue2(prop.initializer);
627
753
  if (ts4.isObjectLiteralExpression(prop.initializer)) visitObject(prop.initializer);
628
754
  }
629
755
  }
@@ -733,8 +859,8 @@ function collectReturnedObjectBindings(fn) {
733
859
  for (const prop of node.expression.properties) {
734
860
  if (ts5.isShorthandPropertyAssignment(prop)) returns.set(prop.name.text, prop.name.text);
735
861
  if (ts5.isPropertyAssignment(prop) && ts5.isIdentifier(prop.initializer)) {
736
- const propertyName2 = ts5.isIdentifier(prop.name) || ts5.isStringLiteralLike(prop.name) ? prop.name.text : void 0;
737
- if (propertyName2) returns.set(propertyName2, prop.initializer.text);
862
+ const propertyName3 = ts5.isIdentifier(prop.name) || ts5.isStringLiteralLike(prop.name) ? prop.name.text : void 0;
863
+ if (propertyName3) returns.set(propertyName3, prop.initializer.text);
738
864
  }
739
865
  }
740
866
  }
@@ -742,9 +868,9 @@ function collectReturnedObjectBindings(fn) {
742
868
  }
743
869
  visit(fn);
744
870
  const out = /* @__PURE__ */ new Map();
745
- for (const [propertyName2, variableName] of returns) {
871
+ for (const [propertyName3, variableName] of returns) {
746
872
  const fact = bindings.get(variableName);
747
- if (fact) out.set(propertyName2, fact);
873
+ if (fact) out.set(propertyName3, fact);
748
874
  }
749
875
  return out;
750
876
  }
@@ -1028,8 +1154,8 @@ async function parseServiceBindings(repoPath, filePath) {
1028
1154
  if (helpers.length === 0) return;
1029
1155
  for (const el of decl.name.elements) {
1030
1156
  if (!ts5.isIdentifier(el.name)) continue;
1031
- const propertyName2 = el.propertyName && ts5.isIdentifier(el.propertyName) ? el.propertyName.text : el.name.text;
1032
- const matches2 = helpers.filter((row) => row.helper.returnedProperty === propertyName2);
1157
+ const propertyName3 = el.propertyName && ts5.isIdentifier(el.propertyName) ? el.propertyName.text : el.name.text;
1158
+ const matches2 = helpers.filter((row) => row.helper.returnedProperty === propertyName3);
1033
1159
  if (matches2.length !== 1) continue;
1034
1160
  const resolved2 = matches2[0];
1035
1161
  out.push({
@@ -1042,7 +1168,7 @@ async function parseServiceBindings(repoPath, filePath) {
1042
1168
  placeholders: resolved2.helper.placeholders,
1043
1169
  sourceFile: normalizePath(filePath),
1044
1170
  sourceLine: lineOf3(sourceFileAst, decl),
1045
- helperChain: [...resolved2.helper.helperChain ?? [], { callerVariable: el.name.text, helperFunction: call.expression.getText(sourceFileAst), returnedProperty: propertyName2, importSource: resolved2.imp?.sourceFile, exportedSymbol: resolved2.imp?.exportedName, helperSourceFile: resolved2.helper.sourceFile, helperSourceLine: resolved2.helper.sourceLine }]
1171
+ helperChain: [...resolved2.helper.helperChain ?? [], { callerVariable: el.name.text, helperFunction: call.expression.getText(sourceFileAst), returnedProperty: propertyName3, importSource: resolved2.imp?.sourceFile, exportedSymbol: resolved2.imp?.exportedName, helperSourceFile: resolved2.helper.sourceFile, helperSourceLine: resolved2.helper.sourceLine }]
1046
1172
  });
1047
1173
  }
1048
1174
  }
@@ -1052,17 +1178,17 @@ async function parseServiceBindings(repoPath, filePath) {
1052
1178
  const helpers = await helpersForCall(call);
1053
1179
  if (helpers.length === 0) return;
1054
1180
  for (const prop of pattern.properties) {
1055
- let propertyName2;
1181
+ let propertyName3;
1056
1182
  let targetName;
1057
1183
  if (ts5.isShorthandPropertyAssignment(prop)) {
1058
- propertyName2 = prop.name.text;
1184
+ propertyName3 = prop.name.text;
1059
1185
  targetName = prop.name.text;
1060
1186
  } else if (ts5.isPropertyAssignment(prop)) {
1061
- propertyName2 = ts5.isIdentifier(prop.name) || ts5.isStringLiteralLike(prop.name) ? prop.name.text : void 0;
1187
+ propertyName3 = ts5.isIdentifier(prop.name) || ts5.isStringLiteralLike(prop.name) ? prop.name.text : void 0;
1062
1188
  targetName = ts5.isIdentifier(prop.initializer) ? prop.initializer.text : void 0;
1063
1189
  }
1064
- if (!propertyName2 || !targetName) continue;
1065
- const matches2 = helpers.filter((row) => row.helper.returnedProperty === propertyName2);
1190
+ if (!propertyName3 || !targetName) continue;
1191
+ const matches2 = helpers.filter((row) => row.helper.returnedProperty === propertyName3);
1066
1192
  if (matches2.length !== 1) continue;
1067
1193
  const resolved2 = matches2[0];
1068
1194
  out.push({
@@ -1075,7 +1201,7 @@ async function parseServiceBindings(repoPath, filePath) {
1075
1201
  placeholders: resolved2.helper.placeholders,
1076
1202
  sourceFile: normalizePath(filePath),
1077
1203
  sourceLine: lineOf3(sourceFileAst, node),
1078
- helperChain: [...resolved2.helper.helperChain ?? [], { callerVariable: targetName, assignedFrom: call.expression.getText(sourceFileAst), aliasKind: "assignment", scopeRule: "same-file-source-order", returnedProperty: propertyName2, importSource: resolved2.imp?.sourceFile, exportedSymbol: resolved2.imp?.exportedName, helperSourceFile: resolved2.helper.sourceFile, helperSourceLine: resolved2.helper.sourceLine }]
1204
+ helperChain: [...resolved2.helper.helperChain ?? [], { callerVariable: targetName, assignedFrom: call.expression.getText(sourceFileAst), aliasKind: "assignment", scopeRule: "same-file-source-order", returnedProperty: propertyName3, importSource: resolved2.imp?.sourceFile, exportedSymbol: resolved2.imp?.exportedName, helperSourceFile: resolved2.helper.sourceFile, helperSourceLine: resolved2.helper.sourceLine }]
1079
1205
  });
1080
1206
  }
1081
1207
  }
@@ -1087,9 +1213,9 @@ async function parseServiceBindings(repoPath, filePath) {
1087
1213
  if (target.expression.kind !== ts5.SyntaxKind.ThisKeyword) return;
1088
1214
  for (const el of decl.name.elements) {
1089
1215
  if (!ts5.isIdentifier(el.name)) continue;
1090
- const propertyName2 = el.propertyName && ts5.isIdentifier(el.propertyName) ? el.propertyName.text : el.name.text;
1216
+ const propertyName3 = el.propertyName && ts5.isIdentifier(el.propertyName) ? el.propertyName.text : el.name.text;
1091
1217
  const helper = classHelpers.find(
1092
- (h) => h.helperName === target.name.text && h.propertyName === propertyName2
1218
+ (h) => h.helperName === target.name.text && h.propertyName === propertyName3
1093
1219
  );
1094
1220
  if (!helper) continue;
1095
1221
  out.push({
@@ -1242,13 +1368,13 @@ function collectClassHelpers(sf) {
1242
1368
  });
1243
1369
  }
1244
1370
  if (ts5.isPropertyAssignment(prop) && ts5.isIdentifier(prop.initializer)) {
1245
- const propertyName2 = ts5.isIdentifier(prop.name) || ts5.isStringLiteralLike(prop.name) ? prop.name.text : void 0;
1246
- const fact = propertyName2 ? bindings.get(prop.initializer.text) : void 0;
1247
- if (propertyName2 && fact)
1371
+ const propertyName3 = ts5.isIdentifier(prop.name) || ts5.isStringLiteralLike(prop.name) ? prop.name.text : void 0;
1372
+ const fact = propertyName3 ? bindings.get(prop.initializer.text) : void 0;
1373
+ if (propertyName3 && fact)
1248
1374
  helpers.push({
1249
1375
  className,
1250
1376
  helperName,
1251
- propertyName: propertyName2,
1377
+ propertyName: propertyName3,
1252
1378
  variableName: prop.initializer.text,
1253
1379
  fact,
1254
1380
  sourceLine: lineOf3(sf, prop)
@@ -1944,12 +2070,12 @@ function mappedParameterIndex(expr, parameters) {
1944
2070
  }
1945
2071
  function propertyExpression(object, key) {
1946
2072
  for (const property of object.properties) {
1947
- if (ts7.isPropertyAssignment(property) && propertyName(property.name) === key) return property.initializer;
2073
+ if (ts7.isPropertyAssignment(property) && propertyName2(property.name) === key) return property.initializer;
1948
2074
  if (ts7.isShorthandPropertyAssignment(property) && property.name.text === key) return property.name;
1949
2075
  }
1950
2076
  return void 0;
1951
2077
  }
1952
- function propertyName(name) {
2078
+ function propertyName2(name) {
1953
2079
  return ts7.isIdentifier(name) || ts7.isStringLiteralLike(name) ? name.text : void 0;
1954
2080
  }
1955
2081
  function literal(expr) {
@@ -2716,8 +2842,8 @@ function buildRemoteQueryTarget(input) {
2716
2842
  // src/linker/service-resolver.ts
2717
2843
  function rows(db, operationPath, workspaceId) {
2718
2844
  const names = operationLookupNames(operationPath);
2719
- return db.prepare(
2720
- `SELECT o.id operationId,r.id repoId,r.name repoName,r.package_name packageName,s.service_name serviceName,s.qualified_name qualifiedName,s.service_path servicePath,o.operation_path operationPath,o.operation_name operationName,o.source_file sourceFile,o.source_line sourceLine,0 score,'' reasons
2845
+ const result = db.prepare(
2846
+ `SELECT o.id operationId,r.id repoId,r.name repoName,r.package_name packageName,s.service_name serviceName,s.qualified_name qualifiedName,s.service_path servicePath,o.operation_path operationPath,o.operation_name operationName,o.source_file sourceFile,o.source_line sourceLine,0 score
2721
2847
  FROM cds_operations o JOIN cds_services s ON s.id=o.service_id JOIN repositories r ON r.id=s.repo_id
2722
2848
  WHERE (? IS NULL OR r.workspace_id=?) AND (o.operation_path IN (?,?) OR o.operation_name IN (?,?)) ORDER BY r.name,s.service_path,o.operation_name`
2723
2849
  ).all(
@@ -2728,6 +2854,11 @@ function rows(db, operationPath, workspaceId) {
2728
2854
  names.name,
2729
2855
  names.simpleName
2730
2856
  );
2857
+ return result.map((row) => ({
2858
+ ...row,
2859
+ score: Number(row.score ?? 0),
2860
+ reasons: []
2861
+ }));
2731
2862
  }
2732
2863
  function operationLookupNames(operationPath) {
2733
2864
  const name = operationPath.replace(/^\//, "");
@@ -2744,7 +2875,11 @@ function resolveOperation(db, signals, workspaceId) {
2744
2875
  if (missing.length > 0)
2745
2876
  return {
2746
2877
  status: "dynamic",
2747
- candidates: signals.operationPath ? rows(db, signals.operationPath, workspaceId) : [],
2878
+ candidates: signals.operationPath ? rows(db, signals.operationPath, workspaceId).map((candidate) => ({
2879
+ ...candidate,
2880
+ score: 0.2,
2881
+ reasons: ["operation_path_match"]
2882
+ })) : [],
2748
2883
  reasons: [...new Set(missing)].map((name) => `missing_variable:${name}`)
2749
2884
  };
2750
2885
  if (!signals.operationPath)
@@ -2918,46 +3053,6 @@ function linkHelperPackages(db, workspaceId, generation) {
2918
3053
  return summary;
2919
3054
  }
2920
3055
 
2921
- // src/linker/operation-decorator-normalizer.ts
2922
- function lowerFirst(value) {
2923
- return value ? `${value[0]?.toLowerCase() ?? ""}${value.slice(1)}` : value;
2924
- }
2925
- function normalizedOperationName(value) {
2926
- return value.replace(/^\//, "");
2927
- }
2928
- function clean(value) {
2929
- return value.replace(/^[`'"]|[`'"]$/g, "");
2930
- }
2931
- function generatedFromConstantName(value) {
2932
- for (const prefix of ["Action", "Func"]) {
2933
- if (value.startsWith(prefix) && value.length > prefix.length && /^[A-Z]/.test(value.slice(prefix.length))) return lowerFirst(value.slice(prefix.length));
2934
- }
2935
- return void 0;
2936
- }
2937
- function resolved(value, raw) {
2938
- const literal2 = clean(value);
2939
- const generated = generatedFromConstantName(literal2);
2940
- return { status: "resolved", operationName: generated ?? normalizedOperationName(literal2), raw };
2941
- }
2942
- function normalizeDecoratorOperationSignal(value, raw, candidateOperation) {
2943
- if (value) return resolved(value, raw);
2944
- if (!raw || raw.trim().length === 0) return { status: "none", raw };
2945
- const expression = raw.trim();
2946
- const nameMatch = /(?:^|\.)(Action[A-Z][\w$]*|Func[A-Z][\w$]*)\.name$/.exec(expression);
2947
- if (nameMatch?.[1]) return resolved(nameMatch[1], expression);
2948
- const stringMatch = /^String\(([A-Za-z_$][\w$]*)\)$/.exec(expression);
2949
- if (stringMatch?.[1]) {
2950
- const identifier = stringMatch[1];
2951
- const generated = generatedFromConstantName(identifier);
2952
- const normalizedCandidate2 = candidateOperation ? normalizedOperationName(candidateOperation) : void 0;
2953
- if (generated) return { status: "resolved", operationName: generated, raw: expression };
2954
- if (normalizedCandidate2 && identifier === normalizedCandidate2) return { status: "resolved", operationName: identifier, raw: expression };
2955
- return { status: "unsupported", raw: expression, reason: "string_wrapper_identifier_not_resolved" };
2956
- }
2957
- if (/^[`'"]/.test(expression) && !/[`'"]$/.test(expression)) return { status: "malformed", raw: expression, reason: "unterminated_literal" };
2958
- return { status: "unsupported", raw: expression, reason: "unsupported_decorator_expression" };
2959
- }
2960
-
2961
3056
  // src/linker/cross-repo-linker.ts
2962
3057
  function linkWorkspace(db, workspaceId, vars = {}) {
2963
3058
  return db.transaction(() => {
@@ -3140,14 +3235,20 @@ function objectJson(value) {
3140
3235
  }
3141
3236
  function callEvidence(call, resolution, servicePath, op, destination, normalized, odataPathIntent) {
3142
3237
  const bindingHasDynamicExpression = Boolean(Number(call.isDynamic ?? 0));
3143
- const routingPlaceholderKeys = placeholderKeys([servicePath, destination, stringValue2(call.aliasExpr), stringValue2(call.alias)]);
3238
+ const routingPlaceholderKeys = placeholderKeys([servicePath, destination, stringValue3(call.aliasExpr), stringValue3(call.alias)]);
3144
3239
  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, invocationArguments: normalized?.wasInvocation ? normalized.invocationArguments : void 0, invocationArgumentPlaceholderKeys: normalized?.invocationArgumentPlaceholderKeys.length ? normalized.invocationArgumentPlaceholderKeys : void 0, routingPlaceholderKeys: routingPlaceholderKeys.length ? routingPlaceholderKeys : 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, candidateScores: compactCandidateScores(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 };
3145
3240
  }
3146
3241
  function compactCandidateScores(candidates) {
3147
3242
  return candidates.flatMap((candidate) => {
3148
3243
  const row = objectValue(candidate);
3149
3244
  if (!row) return [];
3150
- return [{ repo: row.repoName, servicePath: row.servicePath, operationPath: row.operationPath, score: row.score, reasons: row.reasons }];
3245
+ return [{
3246
+ repo: row.repoName,
3247
+ servicePath: row.servicePath,
3248
+ operationPath: row.operationPath,
3249
+ score: row.score,
3250
+ reasons: Array.isArray(row.reasons) ? row.reasons.filter((reason) => typeof reason === "string") : ["operation_path_match"]
3251
+ }];
3151
3252
  });
3152
3253
  }
3153
3254
  function placeholderKeys(values) {
@@ -3157,7 +3258,7 @@ function placeholderKeys(values) {
3157
3258
  function objectValue(value) {
3158
3259
  return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
3159
3260
  }
3160
- function stringValue2(value) {
3261
+ function stringValue3(value) {
3161
3262
  return typeof value === "string" ? value : void 0;
3162
3263
  }
3163
3264
  function linkImplementations(db, workspaceId, generation) {
@@ -3212,13 +3313,54 @@ function implementationContextForOperation(db, operation) {
3212
3313
  }
3213
3314
  function rankedImplementationCandidates(db, workspaceId, operation) {
3214
3315
  const rows2 = implementationCandidates(db, workspaceId, operation);
3215
- 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);
3316
+ const invalid = rows2.filter((row) => !validRegistrationPair(row));
3317
+ recordRegistrationInvariantDiagnostics(db, invalid);
3318
+ return deduplicateCandidates(
3319
+ rows2.filter(validRegistrationPair).map((row) => scoreImplementationCandidate(row, operation))
3320
+ ).sort((a, b) => b.score - a.score || String(a.className).localeCompare(String(b.className)) || a.methodId - b.methodId);
3321
+ }
3322
+ function validRegistrationPair(row) {
3323
+ if (row.registrationHandlerClassId === null || row.registrationHandlerClassId === void 0)
3324
+ return registrationPairingStrategy(row) !== "unproven";
3325
+ return Number(row.registrationHandlerClassId) === Number(row.classId);
3326
+ }
3327
+ function registrationPairingStrategy(row) {
3328
+ if (row.registrationHandlerClassId !== null && row.registrationHandlerClassId !== void 0)
3329
+ return "exact_handler_class_id";
3330
+ if (Number(row.applicationRepoId) === Number(row.handlerRepoId))
3331
+ return "same_repository_class_name_fallback";
3332
+ const source = stringValue3(row.importSource);
3333
+ const separator = source?.lastIndexOf("#") ?? -1;
3334
+ if (!source || separator <= 0) return "unproven";
3335
+ const moduleName = source.slice(0, separator);
3336
+ const importedName = source.slice(separator + 1);
3337
+ const matchesClass = importedName === row.className || importedName === "default" && row.registrationClassName === row.className;
3338
+ return moduleName === row.handlerPackage && matchesClass ? "explicit_package_import" : "unproven";
3339
+ }
3340
+ function recordRegistrationInvariantDiagnostics(db, rows2) {
3341
+ const insert = db.prepare(`INSERT INTO diagnostics(repo_id,severity,code,message,source_file,source_line)
3342
+ SELECT ?,'error','handler_registration_class_mismatch',
3343
+ 'Implementation candidate registration did not match its persisted handler class id',?,?
3344
+ WHERE NOT EXISTS (
3345
+ SELECT 1 FROM diagnostics
3346
+ WHERE repo_id=? AND code='handler_registration_class_mismatch'
3347
+ AND source_file=? AND source_line=?
3348
+ )`);
3349
+ for (const row of rows2)
3350
+ insert.run(
3351
+ row.applicationRepoId,
3352
+ row.registrationFile,
3353
+ row.registrationLine,
3354
+ row.applicationRepoId,
3355
+ row.registrationFile,
3356
+ row.registrationLine
3357
+ );
3216
3358
  }
3217
3359
  function deduplicateCandidates(rows2) {
3218
3360
  const merged = /* @__PURE__ */ new Map();
3219
3361
  for (const row of rows2) {
3220
3362
  const key = [row.methodId, row.classId, row.handlerRepoId].join(":");
3221
- const registration = { file: row.registrationFile, line: row.registrationLine, kind: row.registrationKind, importSource: row.importSource };
3363
+ const registration = registrationEvidence(row);
3222
3364
  const existing = merged.get(key);
3223
3365
  if (!existing) {
3224
3366
  merged.set(key, { ...row, registrations: [registration] });
@@ -3232,6 +3374,17 @@ function deduplicateCandidates(rows2) {
3232
3374
  }
3233
3375
  return [...merged.values()];
3234
3376
  }
3377
+ function registrationEvidence(row) {
3378
+ return {
3379
+ id: row.registrationId,
3380
+ handlerClassId: row.registrationHandlerClassId,
3381
+ file: row.registrationFile,
3382
+ line: row.registrationLine,
3383
+ kind: row.registrationKind,
3384
+ importSource: row.importSource,
3385
+ pairingStrategy: registrationPairingStrategy(row)
3386
+ };
3387
+ }
3235
3388
  function uniqueRegistrations(rows2) {
3236
3389
  const seen = /* @__PURE__ */ new Set();
3237
3390
  return rows2.filter((row) => {
@@ -3260,10 +3413,14 @@ function implementationCandidates(db, workspaceId, operation) {
3260
3413
  hm.method_name methodName,
3261
3414
  hm.decorator_value decoratorValue,
3262
3415
  hm.decorator_raw_expression decoratorRawExpression,
3416
+ hm.decorator_resolution_json decoratorResolutionJson,
3263
3417
  hc.id classId,
3264
3418
  hc.class_name className,
3265
3419
  hc.source_file sourceFile,
3266
3420
  hc.source_line sourceLine,
3421
+ hr.id registrationId,
3422
+ hr.handler_class_id registrationHandlerClassId,
3423
+ hr.class_name registrationClassName,
3267
3424
  hr.repo_id applicationRepoId,
3268
3425
  hr.registration_file registrationFile,
3269
3426
  hr.registration_line registrationLine,
@@ -3286,14 +3443,36 @@ function implementationCandidates(db, workspaceId, operation) {
3286
3443
  CASE WHEN appRepo.id=handlerRepo.id THEN 1 ELSE 0 END sameRepoRegistration,
3287
3444
  CASE WHEN EXISTS (SELECT 1 FROM cds_services localService WHERE localService.repo_id=appRepo.id AND localService.service_path=?) THEN 1 ELSE 0 END localServicePathMatch,
3288
3445
  CASE WHEN EXISTS (SELECT 1 FROM cds_services localService WHERE localService.repo_id=appRepo.id) THEN 1 ELSE 0 END applicationHasLocalServices,
3289
- CASE WHEN EXISTS (SELECT 1 FROM handler_registrations localReg JOIN handler_classes localClass ON (localClass.id=localReg.handler_class_id OR localClass.class_name=localReg.class_name) JOIN handler_methods localMethod ON localMethod.handler_class_id=localClass.id WHERE localReg.repo_id=appRepo.id AND (localMethod.decorator_value=? OR localMethod.decorator_value=? OR localMethod.method_name=? OR localMethod.decorator_raw_expression LIKE ?)) THEN 1 ELSE 0 END applicationHasLocalRegistrationForOperation,
3446
+ CASE WHEN EXISTS (SELECT 1 FROM handler_registrations localReg JOIN handler_classes localClass ON ((localReg.handler_class_id IS NOT NULL AND localClass.id=localReg.handler_class_id) OR (localReg.handler_class_id IS NULL AND localClass.class_name=localReg.class_name AND localClass.repo_id=localReg.repo_id)) JOIN handler_methods localMethod ON localMethod.handler_class_id=localClass.id WHERE localReg.repo_id=appRepo.id AND (localMethod.decorator_value=? OR localMethod.decorator_value=? OR localMethod.method_name=? OR localMethod.decorator_raw_expression LIKE ?)) THEN 1 ELSE 0 END applicationHasLocalRegistrationForOperation,
3290
3447
  CASE WHEN EXISTS (SELECT 1 FROM graph_edges dep WHERE dep.edge_type='REPO_IMPORTS_HELPER_PACKAGE' AND dep.status='resolved' AND dep.from_kind='repo' AND dep.from_id=CAST(appRepo.id AS TEXT) AND dep.to_id=?) THEN 1 ELSE 0 END appDependsOnModel,
3291
3448
  CASE WHEN EXISTS (SELECT 1 FROM graph_edges dep WHERE dep.edge_type='REPO_IMPORTS_HELPER_PACKAGE' AND dep.status='resolved' AND dep.from_kind='repo' AND dep.from_id=CAST(appRepo.id AS TEXT) AND dep.to_id=CAST(handlerRepo.id AS TEXT)) THEN 1 ELSE 0 END appDependsOnHandler,
3292
3449
  CASE WHEN EXISTS (SELECT 1 FROM graph_edges dep WHERE dep.edge_type='REPO_IMPORTS_HELPER_PACKAGE' AND dep.status='resolved' AND dep.from_kind='repo' AND dep.from_id=CAST(handlerRepo.id AS TEXT) AND dep.to_id=?) THEN 1 ELSE 0 END handlerDependsOnModel
3293
3450
  FROM handler_methods hm
3294
3451
  JOIN handler_classes hc ON hc.id=hm.handler_class_id
3295
3452
  JOIN repositories handlerRepo ON handlerRepo.id=hc.repo_id
3296
- JOIN handler_registrations hr ON (hr.handler_class_id=hc.id OR (hr.class_name=hc.class_name AND (hr.repo_id=hc.repo_id OR hr.import_source IS NOT NULL)))
3453
+ JOIN handler_registrations hr ON (
3454
+ (hr.handler_class_id IS NOT NULL AND hr.handler_class_id=hc.id)
3455
+ OR (
3456
+ hr.handler_class_id IS NULL
3457
+ AND (
3458
+ (hr.class_name=hc.class_name AND hr.repo_id=hc.repo_id)
3459
+ OR (
3460
+ instr(hr.import_source,'#')>1
3461
+ AND substr(hr.import_source,1,instr(hr.import_source,'#')-1)
3462
+ =handlerRepo.package_name
3463
+ AND (
3464
+ substr(hr.import_source,instr(hr.import_source,'#')+1)
3465
+ =hc.class_name
3466
+ OR (
3467
+ substr(hr.import_source,instr(hr.import_source,'#')+1)
3468
+ ='default'
3469
+ AND hr.class_name=hc.class_name
3470
+ )
3471
+ )
3472
+ )
3473
+ )
3474
+ )
3475
+ )
3297
3476
  JOIN repositories appRepo ON appRepo.id=hr.repo_id
3298
3477
  WHERE appRepo.workspace_id=?
3299
3478
  AND (hm.decorator_value=? OR hm.decorator_value=? OR hm.method_name=? OR hm.decorator_raw_expression LIKE ?)`).all(
@@ -3395,8 +3574,16 @@ function candidateEvidence(candidate, rank) {
3395
3574
  className: candidate.className,
3396
3575
  sourceFile: candidate.sourceFile,
3397
3576
  sourceLine: candidate.sourceLine,
3398
- registration: { file: candidate.registrationFile, line: candidate.registrationLine, kind: candidate.registrationKind, importSource: candidate.importSource },
3577
+ decoratorResolution: objectJson(candidate.decoratorResolutionJson),
3578
+ registration: registrationEvidence(candidate),
3399
3579
  registrations: candidate.registrations ?? [],
3580
+ registrationPairing: {
3581
+ strategy: registrationPairingStrategy(candidate),
3582
+ registrationId: candidate.registrationId,
3583
+ registrationHandlerClassId: candidate.registrationHandlerClassId,
3584
+ candidateHandlerClassId: candidate.classId,
3585
+ invariantStatus: validRegistrationPair(candidate) ? "valid" : "invalid"
3586
+ },
3400
3587
  applicationPackage: { id: candidate.applicationRepoId, name: candidate.applicationRepo, packageName: candidate.applicationPackage },
3401
3588
  handlerPackage: { id: candidate.handlerRepoId, name: candidate.handlerRepo, packageName: candidate.handlerPackage },
3402
3589
  modelPackage: { id: candidate.modelRepoId, name: candidate.modelRepo, packageName: candidate.modelPackage },
@@ -3436,6 +3623,337 @@ function normalizedOperation(value) {
3436
3623
  return value.startsWith("/") ? value.slice(1) : value;
3437
3624
  }
3438
3625
 
3626
+ // src/trace/selectors.ts
3627
+ function parseVars(values) {
3628
+ const out = {};
3629
+ for (const value of values ?? []) {
3630
+ const [key, ...rest] = value.split("=");
3631
+ if (key && rest.length > 0) out[key] = rest.join("=");
3632
+ }
3633
+ return out;
3634
+ }
3635
+ function ambiguousStartDiagnostic(requested, candidates, message) {
3636
+ const serviceSuggestions = [...new Set(candidates.flatMap((row) => typeof row.servicePath === "string" ? [`--service ${row.servicePath}`] : []))].sort();
3637
+ return {
3638
+ severity: "warning",
3639
+ code: "trace_start_ambiguous",
3640
+ message,
3641
+ normalizedSelectorValue: requested,
3642
+ resolutionStage: "operation",
3643
+ resolutionStatus: "ambiguous_operation",
3644
+ candidates,
3645
+ serviceSuggestions,
3646
+ selectorSuggestions: fullSelectorSuggestions(candidates)
3647
+ };
3648
+ }
3649
+ function fullSelectorSuggestions(candidates) {
3650
+ const includeRepo = new Set(candidates.map((row) => row.repoName)).size > 1;
3651
+ return [...new Set(candidates.flatMap((row) => {
3652
+ if (typeof row.servicePath !== "string" || typeof row.operationPath !== "string") return [];
3653
+ const repoSelector = includeRepo && typeof row.repoName === "string" ? `--repo ${row.repoName} ` : "";
3654
+ return [
3655
+ `${repoSelector}--service ${row.servicePath} --path ${row.operationPath}`
3656
+ ];
3657
+ }))].sort();
3658
+ }
3659
+
3660
+ // src/trace/dynamic-targets.ts
3661
+ function analyzeDynamicTargetCandidates(db, evidence, workspaceId, mode, maxCandidates) {
3662
+ const templates = templatesFromEvidence(evidence);
3663
+ const missingVariables = allMissingVariables(evidence, templates);
3664
+ if (missingVariables.length === 0) return void 0;
3665
+ const order = variableOrder(templates, missingVariables);
3666
+ const candidates = rankedCandidates(
3667
+ db,
3668
+ candidateTargets(db, evidence, workspaceId),
3669
+ referenceRows(db, workspaceId),
3670
+ templates,
3671
+ order
3672
+ );
3673
+ const inference = inferenceDecision(candidates);
3674
+ const shownCandidates = candidates.slice(0, maxCandidates);
3675
+ return {
3676
+ mode,
3677
+ candidateCount: candidates.length,
3678
+ shownCandidateCount: shownCandidates.length,
3679
+ omittedCandidateCount: Math.max(0, candidates.length - shownCandidates.length),
3680
+ missingVariables,
3681
+ candidates,
3682
+ shownCandidates,
3683
+ suggestedVarSets: suggestedVarSets(candidates, missingVariables, order),
3684
+ inference
3685
+ };
3686
+ }
3687
+ function candidateTargets(db, evidence, workspaceId) {
3688
+ const embedded = rowsFromEvidence(evidence.candidates);
3689
+ if (embedded.length > 0) return embedded;
3690
+ const operationPath = stringValue4(evidence.operationPath);
3691
+ if (!operationPath || extractPlaceholders(operationPath).length > 0) return [];
3692
+ return queryOperationTargets(db, operationPath, workspaceId);
3693
+ }
3694
+ function rowsFromEvidence(value) {
3695
+ if (!Array.isArray(value)) return [];
3696
+ return value.flatMap((item) => {
3697
+ const row = record(item);
3698
+ const operationId = numberValue(row.operationId);
3699
+ const repoName = stringValue4(row.repoName);
3700
+ const servicePath = stringValue4(row.servicePath);
3701
+ const operationPath = stringValue4(row.operationPath);
3702
+ const operationName = stringValue4(row.operationName) ?? operationPath?.replace(/^\//, "");
3703
+ if (operationId === void 0 || !repoName || !servicePath || !operationPath || !operationName) return [];
3704
+ return [{
3705
+ operationId,
3706
+ repoName,
3707
+ serviceName: stringValue4(row.serviceName) ?? "",
3708
+ qualifiedName: stringValue4(row.qualifiedName) ?? "",
3709
+ servicePath,
3710
+ operationPath,
3711
+ operationName,
3712
+ sourceFile: stringValue4(row.sourceFile) ?? "",
3713
+ sourceLine: numberValue(row.sourceLine) ?? 0,
3714
+ repoId: numberValue(row.repoId),
3715
+ packageName: stringValue4(row.packageName),
3716
+ score: numberValue(row.score) ?? 0,
3717
+ reasons: stringArray(row.reasons)
3718
+ }];
3719
+ });
3720
+ }
3721
+ function queryOperationTargets(db, operationPath, workspaceId) {
3722
+ const simple = operationPath.replace(/^\//, "").split(".").at(-1) ?? operationPath;
3723
+ const rows2 = db.prepare(
3724
+ `SELECT o.id operationId,r.id repoId,r.name repoName,r.package_name packageName,
3725
+ s.service_name serviceName,s.qualified_name qualifiedName,s.service_path servicePath,
3726
+ o.operation_path operationPath,o.operation_name operationName,o.source_file sourceFile,
3727
+ o.source_line sourceLine
3728
+ FROM cds_operations o JOIN cds_services s ON s.id=o.service_id
3729
+ JOIN repositories r ON r.id=s.repo_id
3730
+ WHERE (? IS NULL OR r.workspace_id=?)
3731
+ AND (o.operation_path IN (?,?) OR o.operation_name=?)
3732
+ ORDER BY r.name,s.service_path,o.operation_name`
3733
+ ).all(workspaceId, workspaceId, operationPath, `/${simple}`, simple);
3734
+ return rows2.map((row) => ({
3735
+ operationId: Number(row.operationId),
3736
+ repoId: numberValue(row.repoId),
3737
+ repoName: String(row.repoName),
3738
+ packageName: stringValue4(row.packageName),
3739
+ serviceName: String(row.serviceName),
3740
+ qualifiedName: String(row.qualifiedName),
3741
+ servicePath: String(row.servicePath),
3742
+ operationPath: String(row.operationPath),
3743
+ operationName: String(row.operationName),
3744
+ sourceFile: String(row.sourceFile),
3745
+ sourceLine: Number(row.sourceLine),
3746
+ score: 0.2,
3747
+ reasons: ["operation_path_match"]
3748
+ }));
3749
+ }
3750
+ function rankedCandidates(db, candidates, references, templates, order) {
3751
+ const ranked = candidates.map((candidate) => candidateEvidence2(db, candidate, references, templates, order));
3752
+ return ranked.sort((a, b) => b.score - a.score || a.repoName.localeCompare(b.repoName) || a.servicePath.localeCompare(b.servicePath));
3753
+ }
3754
+ function candidateEvidence2(db, candidate, references, templates, order) {
3755
+ const state = emptyCandidate(candidate);
3756
+ applyDirectTemplate(state, templates.operationPath, candidate.operationPath, "operation_path");
3757
+ applyDirectTemplate(state, templates.servicePath, candidate.servicePath, "service_path");
3758
+ const refs = references.filter((item) => item.servicePath === candidate.servicePath);
3759
+ applyReferenceTemplate(state, templates.alias, refs, "alias");
3760
+ applyReferenceTemplate(state, templates.destination, refs, "destination");
3761
+ if (hasResolvedImplementation(db, candidate.operationId)) addScore(state, 0.1, "implementation_edge_resolved");
3762
+ state.missingVariables = order.filter((key) => state.derivedVariables[key] === void 0);
3763
+ if (state.missingVariables.length === 0) addScore(state, 0.15, "all_runtime_variables_derived");
3764
+ else state.rejectedReasons.push("missing_required_runtime_variable");
3765
+ state.score = Math.max(0, Math.min(1, state.score));
3766
+ state.cli = state.missingVariables.length === 0 ? cliFor(state.derivedVariables, order) : void 0;
3767
+ return state;
3768
+ }
3769
+ function emptyCandidate(candidate) {
3770
+ return {
3771
+ candidateOperationId: candidate.operationId,
3772
+ repoName: candidate.repoName,
3773
+ packageName: candidate.packageName ?? void 0,
3774
+ servicePath: candidate.servicePath,
3775
+ operationPath: candidate.operationPath,
3776
+ operationName: candidate.operationName,
3777
+ derivedVariables: {},
3778
+ derivedVariableSources: {},
3779
+ missingVariables: [],
3780
+ score: Math.max(0.2, Number(candidate.score ?? 0)),
3781
+ reasons: nonEmptyStrings(candidate.reasons, ["operation_path_match"]),
3782
+ rejectedReasons: []
3783
+ };
3784
+ }
3785
+ function applyDirectTemplate(state, template, concrete, kind) {
3786
+ const matched = matchTemplate(template, concrete);
3787
+ if (!template) return;
3788
+ if (!matched) {
3789
+ state.rejectedReasons.push(`${kind}_template_mismatch`);
3790
+ return;
3791
+ }
3792
+ mergeDerived(state, matched, `${kind}_template`);
3793
+ addScore(state, kind === "service_path" ? 0.35 : 0.25, `${kind}_template_match`);
3794
+ }
3795
+ function applyReferenceTemplate(state, template, refs, kind) {
3796
+ if (!template || extractPlaceholders(template).length === 0) return;
3797
+ for (const ref of refs) {
3798
+ const concrete = kind === "alias" ? ref.alias : ref.destination;
3799
+ const matched = isConcrete(concrete) ? matchTemplate(template, concrete) : void 0;
3800
+ if (!matched) continue;
3801
+ mergeDerived(state, matched, `${ref.sourceKind}.${kind}`);
3802
+ addScore(state, 0.2, `${kind}_template_match`);
3803
+ return;
3804
+ }
3805
+ if (refs.some((ref) => isConcrete(kind === "alias" ? ref.alias : ref.destination)))
3806
+ state.rejectedReasons.push(`${kind}_template_mismatch`);
3807
+ }
3808
+ function inferenceDecision(candidates) {
3809
+ const complete = candidates.filter((candidate) => candidate.missingVariables.length === 0 && candidate.rejectedReasons.length === 0);
3810
+ const first = complete[0];
3811
+ const second = complete[1];
3812
+ if (!first) return { status: "unresolved", reason: "missing_required_runtime_variable" };
3813
+ if (first.score < 0.85) return { status: "unresolved", reason: "candidate_score_below_inference_threshold" };
3814
+ if (second && first.score - second.score <= 0.05) {
3815
+ for (const candidate of complete.filter((item) => first.score - item.score <= 0.05))
3816
+ candidate.rejectedReasons.push("candidate_tied_with_equal_score");
3817
+ return { status: "ambiguous", reason: "candidate_tied_with_equal_score" };
3818
+ }
3819
+ return {
3820
+ status: "resolved",
3821
+ candidateOperationId: first.candidateOperationId,
3822
+ inferredVariables: first.derivedVariables,
3823
+ score: first.score,
3824
+ reasons: first.reasons
3825
+ };
3826
+ }
3827
+ function suggestedVarSets(candidates, required, order) {
3828
+ const seen = /* @__PURE__ */ new Set();
3829
+ return candidates.flatMap((candidate) => {
3830
+ if (required.some((key) => candidate.derivedVariables[key] === void 0)) return [];
3831
+ const cli = cliFor(candidate.derivedVariables, order);
3832
+ if (seen.has(cli)) return [];
3833
+ seen.add(cli);
3834
+ return [{ variables: candidate.derivedVariables, cli }];
3835
+ });
3836
+ }
3837
+ function referenceRows(db, workspaceId) {
3838
+ const rows2 = db.prepare(
3839
+ `SELECT req.alias alias,req.destination destination,req.service_path servicePath,
3840
+ 'cds_require' sourceKind,r.name repoName
3841
+ FROM cds_requires req JOIN repositories r ON r.id=req.repo_id
3842
+ WHERE (? IS NULL OR r.workspace_id=?)
3843
+ UNION ALL
3844
+ SELECT COALESCE(b.alias,b.alias_expr) alias,b.destination_expr destination,
3845
+ b.service_path_expr servicePath,'service_binding' sourceKind,r.name repoName
3846
+ FROM service_bindings b JOIN repositories r ON r.id=b.repo_id
3847
+ WHERE (? IS NULL OR r.workspace_id=?)`
3848
+ ).all(workspaceId, workspaceId, workspaceId, workspaceId);
3849
+ return rows2.map((row) => ({
3850
+ alias: stringValue4(row.alias),
3851
+ destination: stringValue4(row.destination),
3852
+ servicePath: stringValue4(row.servicePath),
3853
+ sourceKind: String(row.sourceKind),
3854
+ repoName: String(row.repoName)
3855
+ }));
3856
+ }
3857
+ function allMissingVariables(evidence, templates) {
3858
+ const fromSubstitutions = Object.values(record(evidence.runtimeSubstitutions)).flatMap((value) => stringArray(record(value).missing));
3859
+ const fromTemplates = [
3860
+ templates.servicePath,
3861
+ templates.operationPath,
3862
+ templates.alias,
3863
+ templates.destination
3864
+ ].flatMap((value) => extractPlaceholders(value));
3865
+ return [.../* @__PURE__ */ new Set([...fromSubstitutions, ...fromTemplates])].sort();
3866
+ }
3867
+ function variableOrder(templates, missingVariables) {
3868
+ const ordered = [
3869
+ templates.servicePath,
3870
+ templates.operationPath,
3871
+ templates.alias,
3872
+ templates.destination
3873
+ ].flatMap((value) => extractPlaceholders(value));
3874
+ return [.../* @__PURE__ */ new Set([...ordered, ...missingVariables])];
3875
+ }
3876
+ function templatesFromEvidence(evidence) {
3877
+ return {
3878
+ servicePath: stringValue4(evidence.servicePath),
3879
+ operationPath: stringValue4(evidence.operationPath),
3880
+ alias: stringValue4(evidence.serviceAliasExpr ?? evidence.serviceAlias),
3881
+ destination: stringValue4(evidence.destination)
3882
+ };
3883
+ }
3884
+ function matchTemplate(template, concrete) {
3885
+ if (!template || !concrete) return void 0;
3886
+ const keys = extractPlaceholders(template);
3887
+ if (keys.length === 0) return template === concrete ? {} : void 0;
3888
+ const regex = new RegExp(`^${templateToPattern(template)}$`);
3889
+ const match = regex.exec(concrete);
3890
+ if (!match) return void 0;
3891
+ const values = {};
3892
+ for (let index = 0; index < keys.length; index += 1) {
3893
+ const key = keys[index];
3894
+ const value = match[index + 1];
3895
+ if (!key || value === void 0) return void 0;
3896
+ if (values[key] !== void 0 && values[key] !== value) return void 0;
3897
+ values[key] = value;
3898
+ }
3899
+ return values;
3900
+ }
3901
+ function templateToPattern(template) {
3902
+ let pattern = "";
3903
+ let lastIndex = 0;
3904
+ for (const match of template.matchAll(/\$\{([^}]*)\}/g)) {
3905
+ pattern += escapeRegex(template.slice(lastIndex, match.index));
3906
+ pattern += "([^/]+?)";
3907
+ lastIndex = (match.index ?? 0) + match[0].length;
3908
+ }
3909
+ return `${pattern}${escapeRegex(template.slice(lastIndex))}`;
3910
+ }
3911
+ function mergeDerived(state, values, sourceKind) {
3912
+ for (const [key, value] of Object.entries(values)) {
3913
+ if (state.derivedVariables[key] !== void 0 && state.derivedVariables[key] !== value) {
3914
+ state.rejectedReasons.push("template_variable_conflict");
3915
+ continue;
3916
+ }
3917
+ state.derivedVariables[key] = value;
3918
+ state.derivedVariableSources[key] = { sourceKind, value };
3919
+ }
3920
+ }
3921
+ function addScore(state, amount, reason) {
3922
+ state.score += amount;
3923
+ if (!state.reasons.includes(reason)) state.reasons.push(reason);
3924
+ }
3925
+ function hasResolvedImplementation(db, operationId) {
3926
+ const row = db.prepare(
3927
+ "SELECT 1 FROM graph_edges WHERE edge_type='OPERATION_IMPLEMENTED_BY_HANDLER' AND status='resolved' AND from_kind='operation' AND from_id=? LIMIT 1"
3928
+ ).get(String(operationId));
3929
+ return Boolean(row);
3930
+ }
3931
+ function cliFor(variables, order) {
3932
+ return order.filter((key) => variables[key] !== void 0).map((key) => `--var ${key}=${variables[key]}`).join(" ");
3933
+ }
3934
+ function record(value) {
3935
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
3936
+ }
3937
+ function stringValue4(value) {
3938
+ return typeof value === "string" ? value : void 0;
3939
+ }
3940
+ function numberValue(value) {
3941
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
3942
+ }
3943
+ function stringArray(value) {
3944
+ return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
3945
+ }
3946
+ function nonEmptyStrings(value, fallback) {
3947
+ const values = stringArray(value).filter((item) => item.length > 0);
3948
+ return values.length > 0 ? values : fallback;
3949
+ }
3950
+ function isConcrete(value) {
3951
+ return typeof value === "string" && value.length > 0 && extractPlaceholders(value).length === 0;
3952
+ }
3953
+ function escapeRegex(value) {
3954
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3955
+ }
3956
+
3439
3957
  // src/trace/evidence.ts
3440
3958
  function baseTraceEvidence(row, call, persistedEvidence, contextualEvidence) {
3441
3959
  const evidence = { ...persistedEvidence, ...contextualEvidence ?? {} };
@@ -3454,23 +3972,45 @@ function baseTraceEvidence(row, call, persistedEvidence, contextualEvidence) {
3454
3972
  persistedResolution: persistedResolution(row)
3455
3973
  };
3456
3974
  }
3457
- function runtimeResolution(db, row, evidence, vars, workspaceId, contextualUnresolvedReason) {
3458
- const substituted = evidenceWithRuntimeVariables(evidence, vars);
3459
- if (!isRemoteRuntimeCandidate(row, evidence, vars)) {
3975
+ function runtimeResolution(db, row, evidence, options, workspaceId, contextualUnresolvedReason) {
3976
+ const substituted = evidenceWithRuntimeVariables(evidence, options.vars);
3977
+ const dynamicMode = options.dynamicMode ?? "strict";
3978
+ const analysis = analyzeDynamicTargetCandidates(
3979
+ db,
3980
+ substituted,
3981
+ workspaceId,
3982
+ dynamicMode,
3983
+ positiveCandidateCap(options.maxDynamicCandidates)
3984
+ );
3985
+ const enriched = analysis ? evidenceWithDynamicAnalysis(substituted, analysis) : substituted;
3986
+ if (dynamicMode === "infer") {
3987
+ const inferred = inferredTarget(analysis);
3988
+ if (inferred) {
3989
+ const resolvedRow = { ...row, status: "resolved", to_kind: "operation", to_id: String(inferred.operationId), unresolved_reason: void 0, confidence: inferred.score };
3990
+ const resolution2 = { status: "resolved", target: inferred, candidates: [], reasons: inferred.reasons };
3991
+ return { row: resolvedRow, evidence: withEffectiveResolution(enriched, resolvedRow, void 0, resolution2), target: inferred };
3992
+ }
3993
+ }
3994
+ if (!isRemoteRuntimeCandidate(row, evidence, options.vars)) {
3460
3995
  const unresolvedReason2 = contextualUnresolvedReason ?? row.unresolved_reason;
3461
- const withSections = withEffectiveResolution(substituted, row, unresolvedReason2);
3996
+ const withSections = withEffectiveResolution(enriched, row, unresolvedReason2);
3462
3997
  return { row, evidence: withSections, unresolvedReason: unresolvedReason2 };
3463
3998
  }
3464
- const resolution = resolveRuntimeOperation(db, substituted, workspaceId);
3999
+ const resolution = resolveRuntimeOperation(db, enriched, workspaceId);
3465
4000
  if (resolution.target) {
3466
4001
  const resolvedRow = { ...row, status: "resolved", to_kind: "operation", to_id: String(resolution.target.operationId), unresolved_reason: void 0, confidence: Math.max(0, Math.min(1, resolution.target.score)) };
3467
- return { row: resolvedRow, evidence: withEffectiveResolution(substituted, resolvedRow, void 0, resolution), target: resolution.target };
4002
+ return { row: resolvedRow, evidence: withEffectiveResolution(enriched, resolvedRow, void 0, resolution), target: resolution.target };
3468
4003
  }
3469
4004
  const unresolvedReason = runtimeUnresolvedReason(resolution);
3470
- return { row, evidence: withEffectiveResolution(substituted, row, unresolvedReason, resolution), unresolvedReason };
4005
+ return { row, evidence: withEffectiveResolution(enriched, row, unresolvedReason, resolution), unresolvedReason };
3471
4006
  }
3472
4007
  function runtimeVariableDiagnostic(edges) {
3473
4008
  const missing = /* @__PURE__ */ new Set();
4009
+ let candidateCount = 0;
4010
+ let shownCandidateCount = 0;
4011
+ let omittedCandidateCount = 0;
4012
+ const candidateSuggestions = [];
4013
+ const suggestedVarSets2 = [];
3474
4014
  for (const edge of edges) {
3475
4015
  const effective = parseObject(edge.evidence.effectiveResolution);
3476
4016
  if (!["dynamic", "unresolved", "ambiguous"].includes(String(effective.status ?? "")))
@@ -3479,6 +4019,12 @@ function runtimeVariableDiagnostic(edges) {
3479
4019
  if (!substitutions || typeof substitutions !== "object" || Array.isArray(substitutions)) continue;
3480
4020
  for (const value of Object.values(substitutions))
3481
4021
  for (const key of value.missing ?? []) missing.add(key);
4022
+ const exploration = parseObject(edge.evidence.dynamicTargetExploration);
4023
+ candidateCount += numeric(exploration.candidateCount);
4024
+ shownCandidateCount += numeric(exploration.shownCandidateCount);
4025
+ omittedCandidateCount += numeric(exploration.omittedCandidateCount);
4026
+ candidateSuggestions.push(...recordArray(edge.evidence.dynamicTargetCandidateSuggestions));
4027
+ suggestedVarSets2.push(...recordArray(exploration.suggestedVarSets));
3482
4028
  }
3483
4029
  const missingVariables = [...missing].sort();
3484
4030
  if (missingVariables.length === 0) return void 0;
@@ -3487,25 +4033,34 @@ function runtimeVariableDiagnostic(edges) {
3487
4033
  code: "trace_runtime_variables_missing",
3488
4034
  message: `Runtime variables are required to resolve dynamic trace targets: ${missingVariables.join(", ")}`,
3489
4035
  missingVariables,
3490
- suggestions: missingVariables.map((key) => `--var ${key}=<value>`)
4036
+ suggestions: missingVariables.map((key) => `--var ${key}=<value>`),
4037
+ candidateCount,
4038
+ shownCandidateCount,
4039
+ omittedCandidateCount,
4040
+ candidateSuggestions: candidateSuggestions.slice(0, 5),
4041
+ suggestedVarSets: uniqueCliRows(suggestedVarSets2).slice(0, 5),
4042
+ copyableExamples: [
4043
+ ...uniqueCliRows(suggestedVarSets2).slice(0, 3).flatMap((row) => typeof row.cli === "string" ? [row.cli] : []),
4044
+ ...candidateCount > 0 ? ["--dynamic-mode candidates --max-dynamic-candidates 20"] : []
4045
+ ]
3491
4046
  };
3492
4047
  }
3493
4048
  function edgeTarget(row, evidence) {
3494
4049
  const effective = parseObject(evidence.effectiveResolution);
3495
- const targetServicePath = stringValue3(effective.targetServicePath ?? evidence.targetServicePath);
3496
- const targetOperationPath = stringValue3(effective.targetOperationPath ?? evidence.targetOperationPath);
4050
+ const targetServicePath = stringValue5(effective.targetServicePath ?? evidence.targetServicePath);
4051
+ const targetOperationPath = stringValue5(effective.targetOperationPath ?? evidence.targetOperationPath);
3497
4052
  if (targetServicePath && targetOperationPath) return `${targetServicePath}${targetOperationPath}`;
3498
4053
  const runtimeCandidate = evidence.runtimeResolvedCandidate;
3499
4054
  if (runtimeCandidate?.servicePath && runtimeCandidate.operationPath) return `${runtimeCandidate.servicePath}${runtimeCandidate.operationPath}`;
3500
- const servicePath = stringValue3(evidence.servicePath);
3501
- const operationPath = stringValue3(evidence.operationPath);
3502
- const targetOperation = stringValue3(evidence.targetOperation);
3503
- const targetRepo = stringValue3(evidence.targetRepo) ?? "";
4055
+ const servicePath = stringValue5(evidence.servicePath);
4056
+ const operationPath = stringValue5(evidence.operationPath);
4057
+ const targetOperation = stringValue5(evidence.targetOperation);
4058
+ const targetRepo = stringValue5(evidence.targetRepo) ?? "";
3504
4059
  if (row.edge_type === "HANDLER_RUNS_DB_QUERY") return `Entity: ${row.to_id || "unknown"}`;
3505
- if (row.edge_type === "HANDLER_RUNS_REMOTE_QUERY") return stringValue3(evidence.remoteQueryTarget) ?? `Remote query: ${row.to_id || "unknown"}`;
4060
+ if (row.edge_type === "HANDLER_RUNS_REMOTE_QUERY") return stringValue5(evidence.remoteQueryTarget) ?? `Remote query: ${row.to_id || "unknown"}`;
3506
4061
  if (row.edge_type === "HANDLER_CALLS_EXTERNAL_HTTP") {
3507
4062
  const target = parseObject(evidence.externalTarget);
3508
- return stringValue3(target.label) ?? `External endpoint: ${row.to_id || "unknown"}`;
4063
+ return stringValue5(target.label) ?? `External endpoint: ${row.to_id || "unknown"}`;
3509
4064
  }
3510
4065
  if (servicePath && operationPath) return `${servicePath}${operationPath}`;
3511
4066
  return targetOperation ? `${targetRepo}:${targetOperation}` : row.to_id;
@@ -3544,12 +4099,12 @@ function withEffectiveResolution(evidence, row, unresolvedReason, resolution) {
3544
4099
  return { ...rest, effectiveResolution: current, linker: { status: current.status, confidence: current.confidence, reason: unresolvedReason, edgeType: current.edgeType } };
3545
4100
  }
3546
4101
  function resolveRuntimeOperation(db, evidence, workspaceId) {
3547
- const servicePath = stringValue3(evidence.servicePath);
3548
- const rawOperationPath = stringValue3(evidence.operationPath);
4102
+ const servicePath = stringValue5(evidence.servicePath);
4103
+ const rawOperationPath = stringValue5(evidence.operationPath);
3549
4104
  const normalized = normalizeODataOperationInvocationPath(rawOperationPath);
3550
- const operationPath = normalized?.wasInvocation ? normalized.normalizedOperationPath : stringValue3(evidence.normalizedOperationPath) ?? rawOperationPath;
3551
- const alias = stringValue3(evidence.serviceAliasExpr ?? evidence.serviceAlias);
3552
- const destination = stringValue3(evidence.destination);
4105
+ const operationPath = normalized?.wasInvocation ? normalized.normalizedOperationPath : stringValue5(evidence.normalizedOperationPath) ?? rawOperationPath;
4106
+ const alias = stringValue5(evidence.serviceAliasExpr ?? evidence.serviceAlias);
4107
+ const destination = stringValue5(evidence.destination);
3553
4108
  return resolveOperation(db, { servicePath, operationPath, alias, destination, hasExplicitOverride: true, isDynamic: true }, workspaceId);
3554
4109
  }
3555
4110
  function evidenceWithRuntimeVariables(evidence, vars) {
@@ -3561,6 +4116,22 @@ function evidenceWithRuntimeVariables(evidence, vars) {
3561
4116
  if (Object.keys(vars ?? {}).length > 0) next.runtimeVariablesApplied = true;
3562
4117
  return next;
3563
4118
  }
4119
+ function evidenceWithDynamicAnalysis(evidence, analysis) {
4120
+ return {
4121
+ ...evidence,
4122
+ dynamicTargetExploration: {
4123
+ mode: analysis.mode,
4124
+ missingVariables: analysis.missingVariables,
4125
+ candidateCount: analysis.candidateCount,
4126
+ shownCandidateCount: analysis.shownCandidateCount,
4127
+ omittedCandidateCount: analysis.omittedCandidateCount,
4128
+ suggestedVarSets: analysis.suggestedVarSets
4129
+ },
4130
+ dynamicTargetCandidates: analysis.candidates,
4131
+ dynamicTargetCandidateSuggestions: analysis.shownCandidates,
4132
+ dynamicTargetInference: analysis.inference
4133
+ };
4134
+ }
3564
4135
  function runtimeSubstitutions(evidence, vars) {
3565
4136
  const substitutions = {};
3566
4137
  for (const key of ["servicePath", "operationPath", "serviceAliasExpr", "serviceAlias", "destination"]) {
@@ -3570,7 +4141,7 @@ function runtimeSubstitutions(evidence, vars) {
3570
4141
  return substitutions;
3571
4142
  }
3572
4143
  function substitutionValue(evidence, key) {
3573
- const value = stringValue3(evidence[key]);
4144
+ const value = stringValue5(evidence[key]);
3574
4145
  if (key !== "operationPath") return value;
3575
4146
  const normalized = normalizeODataOperationInvocationPath(value);
3576
4147
  return normalized?.wasInvocation ? normalized.normalizedOperationPath : value;
@@ -3581,6 +4152,29 @@ function isRemoteRuntimeCandidate(row, evidence, vars) {
3581
4152
  if (!["DYNAMIC_EDGE_CANDIDATE", "UNRESOLVED_EDGE", "REMOTE_CALL_RESOLVES_TO_OPERATION"].includes(row.edge_type)) return false;
3582
4153
  return ["servicePath", "operationPath", "serviceAliasExpr", "serviceAlias", "destination"].some((key) => hasRuntimeVariable(evidence[key], vars));
3583
4154
  }
4155
+ function inferredTarget(analysis) {
4156
+ if (analysis?.inference.status !== "resolved") return void 0;
4157
+ const id = Number(analysis.inference.candidateOperationId);
4158
+ const candidate = analysis.candidates.find((item) => item.candidateOperationId === id);
4159
+ if (!candidate) return void 0;
4160
+ return targetFromCandidate(candidate);
4161
+ }
4162
+ function targetFromCandidate(candidate) {
4163
+ return {
4164
+ operationId: candidate.candidateOperationId,
4165
+ repoName: candidate.repoName,
4166
+ serviceName: "",
4167
+ qualifiedName: "",
4168
+ servicePath: candidate.servicePath,
4169
+ operationPath: candidate.operationPath,
4170
+ operationName: candidate.operationName,
4171
+ packageName: candidate.packageName,
4172
+ score: candidate.score,
4173
+ reasons: candidate.reasons,
4174
+ sourceFile: "",
4175
+ sourceLine: 0
4176
+ };
4177
+ }
3584
4178
  function hasRuntimeVariable(value, vars) {
3585
4179
  return typeof value === "string" && extractPlaceholders(value).some((key) => Object.hasOwn(vars, key));
3586
4180
  }
@@ -3592,9 +4186,56 @@ function runtimeUnresolvedReason(resolution) {
3592
4186
  function parseObject(value) {
3593
4187
  return value && typeof value === "object" && !Array.isArray(value) ? value : {};
3594
4188
  }
3595
- function stringValue3(value) {
4189
+ function stringValue5(value) {
3596
4190
  return typeof value === "string" ? value : void 0;
3597
4191
  }
4192
+ function numeric(value) {
4193
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
4194
+ }
4195
+ function recordArray(value) {
4196
+ return Array.isArray(value) ? value.filter((item) => Boolean(item && typeof item === "object" && !Array.isArray(item))) : [];
4197
+ }
4198
+ function uniqueCliRows(rows2) {
4199
+ const seen = /* @__PURE__ */ new Set();
4200
+ return rows2.filter((row) => {
4201
+ const cli = typeof row.cli === "string" ? row.cli : JSON.stringify(row);
4202
+ if (seen.has(cli)) return false;
4203
+ seen.add(cli);
4204
+ return true;
4205
+ });
4206
+ }
4207
+ function positiveCandidateCap(value) {
4208
+ return Number.isFinite(value) && Number(value) > 0 ? Math.floor(Number(value)) : 5;
4209
+ }
4210
+
4211
+ // src/trace/dynamic-branches.ts
4212
+ function dynamicCandidateBranches(depth, call, evidence) {
4213
+ const exploration = objectRecord(evidence.dynamicTargetExploration);
4214
+ return recordArray2(evidence.dynamicTargetCandidateSuggestions).map((candidate) => ({
4215
+ step: depth,
4216
+ type: "dynamic_candidate_branch",
4217
+ from: `${call.repoName}:${call.source_file}:${call.source_line}`,
4218
+ to: `${String(candidate.servicePath ?? "")}${String(candidate.operationPath ?? "")}`,
4219
+ evidence: {
4220
+ ...candidate,
4221
+ exploratory: true,
4222
+ dynamicMode: String(exploration.mode ?? "candidates"),
4223
+ selected: false,
4224
+ omittedCandidateCount: numericValue(exploration.omittedCandidateCount)
4225
+ },
4226
+ confidence: numericValue(candidate.score),
4227
+ unresolvedReason: "Exploratory dynamic target candidate; provide runtime variables to select it"
4228
+ }));
4229
+ }
4230
+ function objectRecord(value) {
4231
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
4232
+ }
4233
+ function recordArray2(value) {
4234
+ return Array.isArray(value) ? value.filter((item) => Boolean(item && typeof item === "object" && !Array.isArray(item))) : [];
4235
+ }
4236
+ function numericValue(value) {
4237
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
4238
+ }
3598
4239
 
3599
4240
  // src/trace/trace-engine.ts
3600
4241
  function normalizeOperation(value) {
@@ -3641,19 +4282,6 @@ function implementationRejectionReasons(evidence) {
3641
4282
  const reasons = candidates.flatMap((candidate) => Array.isArray(candidate.rejectedReasons) ? candidate.rejectedReasons.filter((reason) => typeof reason === "string") : []);
3642
4283
  return [...new Set(reasons)].sort();
3643
4284
  }
3644
- function ambiguousStartDiagnostic(requested, candidates, message) {
3645
- const serviceSuggestions = [...new Set(candidates.flatMap((row) => typeof row.servicePath === "string" ? [`--service ${row.servicePath}`] : []))].sort();
3646
- return {
3647
- severity: "warning",
3648
- code: "trace_start_ambiguous",
3649
- message,
3650
- normalizedSelectorValue: requested,
3651
- resolutionStage: "operation",
3652
- resolutionStatus: "ambiguous_operation",
3653
- candidates,
3654
- serviceSuggestions
3655
- };
3656
- }
3657
4285
  function sourceFilesForStart(db, repoId, start) {
3658
4286
  const handler = start.handler;
3659
4287
  const operation = normalizeOperation(start.operation ?? start.operationPath);
@@ -4076,7 +4704,11 @@ function trace(db, start, options) {
4076
4704
  String(row.evidence_json || "{}")
4077
4705
  );
4078
4706
  const rawEvidence = baseTraceEvidence(row, call, persistedEvidence, contextual.evidence);
4079
- const effective = runtimeResolution(db, row, rawEvidence, options.vars, workspaceIdForCall(db, String(call.id)), contextual.unresolvedReason);
4707
+ const effective = runtimeResolution(db, row, rawEvidence, {
4708
+ vars: options.vars,
4709
+ dynamicMode: options.dynamicMode ?? "strict",
4710
+ maxDynamicCandidates: options.maxDynamicCandidates
4711
+ }, workspaceIdForCall(db, String(call.id)), contextual.unresolvedReason);
4080
4712
  const evidence = effective.evidence;
4081
4713
  const effectiveRow = effective.row;
4082
4714
  const targetNode = `${effectiveRow.to_kind}:${effectiveRow.to_id}`;
@@ -4096,6 +4728,8 @@ function trace(db, start, options) {
4096
4728
  confidence: Number(effectiveRow.confidence ?? call.confidence),
4097
4729
  unresolvedReason: effective.unresolvedReason
4098
4730
  });
4731
+ if ((options.dynamicMode ?? "strict") === "candidates")
4732
+ edges.push(...dynamicCandidateBranches(current.depth, call, evidence));
4099
4733
  if (effectiveRow.to_kind === "operation") {
4100
4734
  const implementation = implementationScope(db, effectiveRow.to_id);
4101
4735
  const contextSelection = contextImplementationMethodId(implementation.edge, current.repoId, evidence, hintOptions);
@@ -4175,6 +4809,7 @@ export {
4175
4809
  parseImplementationHint,
4176
4810
  implementationHintSuggestions,
4177
4811
  linkWorkspace,
4812
+ parseVars,
4178
4813
  trace
4179
4814
  };
4180
- //# sourceMappingURL=chunk-XOROZHW4.js.map
4815
+ //# sourceMappingURL=chunk-YZJKE5UX.js.map