@saptools/service-flow 0.1.31 → 0.1.32

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,12 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.32
4
+
5
+ - Hardened operation-first trace starts to fail closed on ambiguous, rejected, or non-executable implementation evidence.
6
+ - Made decorator normalization explicit and conservative for unsupported expressions.
7
+ - Populated queryable external HTTP target metadata with sanitized labels and kept CAP candidates distinct from HTTP endpoints.
8
+ - Removed accidental fresh-schema external-target columns from symbols and documented migration/re-index expectations.
9
+
3
10
  ## 0.1.31
4
11
 
5
12
  - Resolve operation and path trace selectors from indexed CDS operations and persisted implementation edges before conservative handler fallback, including generated `Action<Name>` and `Func<Name>` decorator constants whose method names differ from public operation names.
package/README.md CHANGED
@@ -490,3 +490,14 @@ The `graph` command accepts repeatable `--var key=value` options, matching `trac
490
490
  ### 0.1.17 parser ownership policy
491
491
 
492
492
  Outbound call extraction is AST-based and ignores comments, block comments, and string literals. CAP/service `.on(...)` registrations are indexed only when the receiver has CAP/service evidence, and top-level registrations receive `module:<relative-file>#event:<event-name>:<line>` synthetic owners. Generic event emitters such as desktop or window events are ignored by default rather than guessed as CAP async edges. Unsupported source shapes are surfaced through diagnostics and strict doctor ownerless categories instead of guessed graph edges.
493
+
494
+
495
+ ## 0.1.32 hardening notes
496
+
497
+ Operation trace starts resolve indexed CDS operations before handler fallback. Ambiguous operation matches, ambiguous/rejected implementation edges, and resolved edges without an executable handler scope now fail closed with structured `resolutionStage` and `resolutionStatus` diagnostics and no speculative traversal. Handler fallback is only considered when no indexed CDS operation matches and the fallback is unique under the existing registration and ownership policy.
498
+
499
+ Decorator signals are interpreted as resolved, absent, unsupported, or malformed. Only resolved string/template values, generated `Action<Name>.name`/`Func<Name>.name` constants, qualified generated constants ending in `.name`, and narrow safe `String(identifier)` wrappers can contradict method-name fallback. Unsupported expressions are preserved as evidence but are not treated as proof that a handler targets another operation.
500
+
501
+ External HTTP facts persist queryable target kind, stable id, safe label, and dynamic flag on `outbound_calls`; graph evidence uses the same sanitized model. URL user info, fragments, query values, headers, cookies, credentials, tokens, API keys, and request/response bodies are not persisted. Legacy workspaces may relink from old evidence when possible, but full semantic enrichment requires re-index plus relink when old parser facts did not contain safe target metadata.
502
+
503
+ Graph taxonomy now keeps CAP operation candidates separate from external HTTP targets. Dynamic, ambiguous, and unresolved CAP calls use operation-candidate semantics, external destinations/endpoints are produced only by `external_http` calls, and terminal local transport-client methods use transport-method semantics rather than external HTTP edges. `doctor --strict` reports external target quality and taxonomy consistency.
@@ -720,6 +720,52 @@ function topLevelQueryIndex(text) {
720
720
  import fs6 from "fs/promises";
721
721
  import path7 from "path";
722
722
  import ts4 from "typescript";
723
+
724
+ // src/linker/external-http-target.ts
725
+ import { createHash } from "crypto";
726
+ var sensitiveKeys = /* @__PURE__ */ new Set(["token", "access_token", "id_token", "api_key", "apikey", "key", "password", "passwd", "pwd", "secret", "client_secret", "authorization", "cookie", "signature"]);
727
+ function hash(value) {
728
+ return createHash("sha256").update(value).digest("hex").slice(0, 12);
729
+ }
730
+ function methodPrefix(method) {
731
+ return typeof method === "string" && method.length > 0 ? `${method.toUpperCase()} ` : "";
732
+ }
733
+ function redactUrl(value) {
734
+ try {
735
+ const url = new URL(value, value.startsWith("/") ? "https://relative.invalid" : void 0);
736
+ url.username = "";
737
+ url.password = "";
738
+ for (const key of [...url.searchParams.keys()]) url.searchParams.set(key, sensitiveKeys.has(key.toLowerCase()) ? "<redacted>" : "<redacted>");
739
+ const path9 = `${url.pathname}${url.search ? url.search : ""}`;
740
+ return value.startsWith("/") ? path9 : `${url.origin}${path9}`;
741
+ } catch {
742
+ return value.replace(/([?&][^=;&]*(?:token|key|password|secret|cookie|authorization)[^=;&]*=)[^&]*/gi, "$1<redacted>");
743
+ }
744
+ }
745
+ function externalHttpTarget(call) {
746
+ const evidence = typeof call.evidence_json === "string" ? safeParse(call.evidence_json) : {};
747
+ const target = evidence.externalTarget && typeof evidence.externalTarget === "object" && !Array.isArray(evidence.externalTarget) ? evidence.externalTarget : {};
748
+ const method = typeof call.method === "string" ? call.method : typeof target.method === "string" ? target.method : void 0;
749
+ const kind = typeof target.kind === "string" ? target.kind : "unknown";
750
+ const expression = typeof target.expression === "string" ? target.expression : void 0;
751
+ if (kind === "destination" && expression) return { kind, toKind: "external_destination", toId: `destination:${expression}`, label: `External destination: ${expression}`, method, dynamic: false, expression };
752
+ if (kind === "static_url" && expression) {
753
+ const redacted = redactUrl(expression);
754
+ return { kind, toKind: "external_endpoint", toId: `endpoint:${hash(`${method ?? ""}:${redacted}`)}`, label: `External endpoint: ${methodPrefix(method)}${redacted}`, method, dynamic: false, expression: redacted };
755
+ }
756
+ if (kind === "url_expression" && expression) return { kind, toKind: "external_endpoint", toId: `dynamic:${hash(expression)}`, label: `External endpoint: ${methodPrefix(method)}dynamic URL`, method, dynamic: true, expression: `expr:${hash(expression)}` };
757
+ return { kind: "unknown", toKind: "external_endpoint", toId: "unknown", label: "External endpoint: unknown", method, dynamic: false };
758
+ }
759
+ function safeParse(value) {
760
+ try {
761
+ const parsed = JSON.parse(value);
762
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
763
+ } catch {
764
+ return {};
765
+ }
766
+ }
767
+
768
+ // src/parsers/outbound-call-parser.ts
723
769
  function lineOf3(text, idx) {
724
770
  return text.slice(0, idx).split("\n").length;
725
771
  }
@@ -996,7 +1042,11 @@ function classifyOutboundCallsInSource(source, filePath) {
996
1042
  }
997
1043
  } else {
998
1044
  const external = externalHttpEvidence(node, source, initializers);
999
- if (external) add(node, { callType: "external_http", method: external.method, payloadSummary: summarizeExpression(node.arguments.map((arg) => arg.getText(source)).join(", ")), confidence: 0.7, unresolvedReason: "External HTTP destination is outside indexed CAP services" }, { classifier: external.classifier, externalTarget: { ...external.externalTarget, method: external.method, parserClassifier: external.classifier, sourceCallShape: external.sourceCallShape }, sourceCallShape: external.sourceCallShape });
1045
+ if (external) {
1046
+ const evidenceTarget = { ...external.externalTarget, method: external.method, parserClassifier: external.classifier, sourceCallShape: external.sourceCallShape };
1047
+ const safeTarget = externalHttpTarget({ method: external.method, evidence_json: JSON.stringify({ externalTarget: evidenceTarget }) });
1048
+ add(node, { callType: "external_http", method: external.method, payloadSummary: void 0, confidence: 0.7, unresolvedReason: "External HTTP destination is outside indexed CAP services", externalTarget: { kind: safeTarget.kind, stableId: safeTarget.toId, label: safeTarget.label, dynamic: safeTarget.dynamic } }, { classifier: external.classifier, externalTarget: safeTarget, sourceCallShape: external.sourceCallShape });
1049
+ }
1000
1050
  }
1001
1051
  }
1002
1052
  ts4.forEachChild(node, visit);
@@ -1450,16 +1500,16 @@ async function parseServiceBindings(repoPath, filePath) {
1450
1500
  helperChain: aliasKind === "assignment" ? [{ callerVariable: targetName, assignedFrom: call.expression.getText(sourceFileAst), aliasKind, scopeRule: "same-file-source-order" }] : void 0
1451
1501
  });
1452
1502
  else if (ts5.isIdentifier(call.expression)) {
1453
- const resolved = await importedHelper(call.expression.text);
1454
- if (resolved)
1503
+ const resolved2 = await importedHelper(call.expression.text);
1504
+ if (resolved2)
1455
1505
  out.push({
1456
1506
  variableName: targetName,
1457
- alias: resolved.helper.alias,
1458
- aliasExpr: resolved.helper.aliasExpr,
1459
- destinationExpr: resolved.helper.destinationExpr,
1460
- servicePathExpr: resolved.helper.servicePathExpr,
1461
- isDynamic: resolved.helper.isDynamic,
1462
- placeholders: resolved.helper.placeholders,
1507
+ alias: resolved2.helper.alias,
1508
+ aliasExpr: resolved2.helper.aliasExpr,
1509
+ destinationExpr: resolved2.helper.destinationExpr,
1510
+ servicePathExpr: resolved2.helper.servicePathExpr,
1511
+ isDynamic: resolved2.helper.isDynamic,
1512
+ placeholders: resolved2.helper.placeholders,
1463
1513
  sourceFile: normalizePath(filePath),
1464
1514
  sourceLine: lineOf4(sourceFileAst, node),
1465
1515
  helperChain: [
@@ -1467,10 +1517,10 @@ async function parseServiceBindings(repoPath, filePath) {
1467
1517
  callerVariable: targetName,
1468
1518
  ...aliasKind === "assignment" ? { assignedFrom: call.expression.text, aliasKind, scopeRule: "same-file-source-order" } : {},
1469
1519
  importedHelper: call.expression.text,
1470
- importSource: resolved.imp.sourceFile,
1471
- exportedSymbol: resolved.imp.exportedName,
1472
- helperSourceFile: resolved.helper.sourceFile,
1473
- helperSourceLine: resolved.helper.sourceLine
1520
+ importSource: resolved2.imp.sourceFile,
1521
+ exportedSymbol: resolved2.imp.exportedName,
1522
+ helperSourceFile: resolved2.helper.sourceFile,
1523
+ helperSourceLine: resolved2.helper.sourceLine
1474
1524
  }
1475
1525
  ]
1476
1526
  });
@@ -1497,18 +1547,18 @@ async function parseServiceBindings(repoPath, filePath) {
1497
1547
  const propertyName = el.propertyName && ts5.isIdentifier(el.propertyName) ? el.propertyName.text : el.name.text;
1498
1548
  const matches = helpers.filter((row) => row.helper.returnedProperty === propertyName);
1499
1549
  if (matches.length !== 1) continue;
1500
- const resolved = matches[0];
1550
+ const resolved2 = matches[0];
1501
1551
  out.push({
1502
1552
  variableName: el.name.text,
1503
- alias: resolved.helper.alias,
1504
- aliasExpr: resolved.helper.aliasExpr,
1505
- destinationExpr: resolved.helper.destinationExpr,
1506
- servicePathExpr: resolved.helper.servicePathExpr,
1507
- isDynamic: resolved.helper.isDynamic,
1508
- placeholders: resolved.helper.placeholders,
1553
+ alias: resolved2.helper.alias,
1554
+ aliasExpr: resolved2.helper.aliasExpr,
1555
+ destinationExpr: resolved2.helper.destinationExpr,
1556
+ servicePathExpr: resolved2.helper.servicePathExpr,
1557
+ isDynamic: resolved2.helper.isDynamic,
1558
+ placeholders: resolved2.helper.placeholders,
1509
1559
  sourceFile: normalizePath(filePath),
1510
1560
  sourceLine: lineOf4(sourceFileAst, decl),
1511
- helperChain: [{ callerVariable: el.name.text, helperFunction: call.expression.getText(sourceFileAst), returnedProperty: propertyName, importSource: resolved.imp?.sourceFile, exportedSymbol: resolved.imp?.exportedName, helperSourceFile: resolved.helper.sourceFile, helperSourceLine: resolved.helper.sourceLine }]
1561
+ helperChain: [{ callerVariable: el.name.text, helperFunction: call.expression.getText(sourceFileAst), returnedProperty: propertyName, importSource: resolved2.imp?.sourceFile, exportedSymbol: resolved2.imp?.exportedName, helperSourceFile: resolved2.helper.sourceFile, helperSourceLine: resolved2.helper.sourceLine }]
1512
1562
  });
1513
1563
  }
1514
1564
  }
@@ -1530,18 +1580,18 @@ async function parseServiceBindings(repoPath, filePath) {
1530
1580
  if (!propertyName || !targetName) continue;
1531
1581
  const matches = helpers.filter((row) => row.helper.returnedProperty === propertyName);
1532
1582
  if (matches.length !== 1) continue;
1533
- const resolved = matches[0];
1583
+ const resolved2 = matches[0];
1534
1584
  out.push({
1535
1585
  variableName: targetName,
1536
- alias: resolved.helper.alias,
1537
- aliasExpr: resolved.helper.aliasExpr,
1538
- destinationExpr: resolved.helper.destinationExpr,
1539
- servicePathExpr: resolved.helper.servicePathExpr,
1540
- isDynamic: resolved.helper.isDynamic,
1541
- placeholders: resolved.helper.placeholders,
1586
+ alias: resolved2.helper.alias,
1587
+ aliasExpr: resolved2.helper.aliasExpr,
1588
+ destinationExpr: resolved2.helper.destinationExpr,
1589
+ servicePathExpr: resolved2.helper.servicePathExpr,
1590
+ isDynamic: resolved2.helper.isDynamic,
1591
+ placeholders: resolved2.helper.placeholders,
1542
1592
  sourceFile: normalizePath(filePath),
1543
1593
  sourceLine: lineOf4(sourceFileAst, node),
1544
- helperChain: [{ callerVariable: targetName, assignedFrom: call.expression.getText(sourceFileAst), aliasKind: "assignment", scopeRule: "same-file-source-order", returnedProperty: propertyName, importSource: resolved.imp?.sourceFile, exportedSymbol: resolved.imp?.exportedName, helperSourceFile: resolved.helper.sourceFile, helperSourceLine: resolved.helper.sourceLine }]
1594
+ helperChain: [{ callerVariable: targetName, assignedFrom: call.expression.getText(sourceFileAst), aliasKind: "assignment", scopeRule: "same-file-source-order", returnedProperty: propertyName, importSource: resolved2.imp?.sourceFile, exportedSymbol: resolved2.imp?.exportedName, helperSourceFile: resolved2.helper.sourceFile, helperSourceLine: resolved2.helper.sourceLine }]
1545
1595
  });
1546
1596
  }
1547
1597
  }
@@ -1942,62 +1992,28 @@ function generatedFromConstantName(value) {
1942
1992
  }
1943
1993
  return void 0;
1944
1994
  }
1945
- function normalizeDecoratorOperation(value, raw) {
1946
- if (value) {
1947
- const literal = clean(value);
1948
- const generated = generatedFromConstantName(literal);
1949
- return generated ?? normalizedOperationName(literal);
1950
- }
1951
- if (!raw) return void 0;
1995
+ function resolved(value, raw) {
1996
+ const literal = clean(value);
1997
+ const generated = generatedFromConstantName(literal);
1998
+ return { status: "resolved", operationName: generated ?? normalizedOperationName(literal), raw };
1999
+ }
2000
+ function normalizeDecoratorOperationSignal(value, raw, candidateOperation) {
2001
+ if (value) return resolved(value, raw);
2002
+ if (!raw || raw.trim().length === 0) return { status: "none", raw };
1952
2003
  const expression = raw.trim();
1953
2004
  const nameMatch = /(?:^|\.)(Action[A-Z][\w$]*|Func[A-Z][\w$]*)\.name$/.exec(expression);
1954
- if (nameMatch?.[1]) return generatedFromConstantName(nameMatch[1]);
1955
- const tail = expression.split(".").filter(Boolean).at(-1);
1956
- return tail ? normalizedOperationName(clean(tail)) : void 0;
1957
- }
1958
-
1959
- // src/linker/external-http-target.ts
1960
- import { createHash } from "crypto";
1961
- var sensitiveKeys = /* @__PURE__ */ new Set(["token", "access_token", "id_token", "api_key", "apikey", "key", "password", "passwd", "pwd", "secret", "client_secret", "authorization", "cookie", "signature"]);
1962
- function hash(value) {
1963
- return createHash("sha256").update(value).digest("hex").slice(0, 12);
1964
- }
1965
- function methodPrefix(method) {
1966
- return typeof method === "string" && method.length > 0 ? `${method.toUpperCase()} ` : "";
1967
- }
1968
- function redactUrl(value) {
1969
- try {
1970
- const url = new URL(value, value.startsWith("/") ? "https://relative.invalid" : void 0);
1971
- url.username = "";
1972
- url.password = "";
1973
- for (const key of [...url.searchParams.keys()]) url.searchParams.set(key, sensitiveKeys.has(key.toLowerCase()) ? "<redacted>" : "<redacted>");
1974
- const path9 = `${url.pathname}${url.search ? url.search : ""}`;
1975
- return value.startsWith("/") ? path9 : `${url.origin}${path9}`;
1976
- } catch {
1977
- return value.replace(/([?&][^=;&]*(?:token|key|password|secret|cookie|authorization)[^=;&]*=)[^&]*/gi, "$1<redacted>");
1978
- }
1979
- }
1980
- function externalHttpTarget(call) {
1981
- const evidence = typeof call.evidence_json === "string" ? safeParse(call.evidence_json) : {};
1982
- const target = evidence.externalTarget && typeof evidence.externalTarget === "object" && !Array.isArray(evidence.externalTarget) ? evidence.externalTarget : {};
1983
- const method = typeof call.method === "string" ? call.method : typeof target.method === "string" ? target.method : void 0;
1984
- const kind = typeof target.kind === "string" ? target.kind : "unknown";
1985
- const expression = typeof target.expression === "string" ? target.expression : void 0;
1986
- if (kind === "destination" && expression) return { kind, toKind: "external_destination", toId: `destination:${expression}`, label: `External destination: ${expression}`, method, dynamic: false, expression };
1987
- if (kind === "static_url" && expression) {
1988
- const redacted = redactUrl(expression);
1989
- return { kind, toKind: "external_endpoint", toId: `endpoint:${hash(`${method ?? ""}:${redacted}`)}`, label: `External endpoint: ${methodPrefix(method)}${redacted}`, method, dynamic: false, expression: redacted };
1990
- }
1991
- if (kind === "url_expression" && expression) return { kind, toKind: "external_endpoint", toId: `dynamic:${hash(expression)}`, label: `External endpoint: ${methodPrefix(method)}dynamic URL`, method, dynamic: true, expression: `expr:${hash(expression)}` };
1992
- return { kind: "unknown", toKind: "external_endpoint", toId: "unknown", label: "External endpoint: unknown", method, dynamic: false };
1993
- }
1994
- function safeParse(value) {
1995
- try {
1996
- const parsed = JSON.parse(value);
1997
- return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
1998
- } catch {
1999
- return {};
2000
- }
2005
+ if (nameMatch?.[1]) return resolved(nameMatch[1], expression);
2006
+ const stringMatch = /^String\(([A-Za-z_$][\w$]*)\)$/.exec(expression);
2007
+ if (stringMatch?.[1]) {
2008
+ const identifier = stringMatch[1];
2009
+ const generated = generatedFromConstantName(identifier);
2010
+ const normalizedCandidate = candidateOperation ? normalizedOperationName(candidateOperation) : void 0;
2011
+ if (generated) return { status: "resolved", operationName: generated, raw: expression };
2012
+ if (normalizedCandidate && identifier === normalizedCandidate) return { status: "resolved", operationName: identifier, raw: expression };
2013
+ return { status: "unsupported", raw: expression, reason: "string_wrapper_identifier_not_resolved" };
2014
+ }
2015
+ if (/^[`'"]/.test(expression) && !/[`'"]$/.test(expression)) return { status: "malformed", raw: expression, reason: "unterminated_literal" };
2016
+ return { status: "unsupported", raw: expression, reason: "unsupported_decorator_expression" };
2001
2017
  }
2002
2018
 
2003
2019
  // src/linker/cross-repo-linker.ts
@@ -2059,7 +2075,7 @@ function insertCallEdge(db, workspaceId, call, vars, generation) {
2059
2075
  return { status: "terminal", callType };
2060
2076
  }
2061
2077
  if (callType === "local_service_call" && call.unresolved_reason === "transport_client_method" && !resolution.target && resolution.candidates.length === 0) {
2062
- db.prepare("INSERT INTO graph_edges(workspace_id,edge_type,status,from_kind,from_id,to_kind,to_id,confidence,evidence_json,is_dynamic,generation) VALUES(?,?,?,?,?,?,?,?,?,?,?)").run(workspaceId, "HANDLER_CALLS_EXTERNAL_HTTP", "terminal", "call", String(call.id), "external", String(op || "transport_client_method"), Number(call.confidence ?? 0.5), JSON.stringify({ ...evidence, classification: "transport_client_method" }), 0, generation);
2078
+ db.prepare("INSERT INTO graph_edges(workspace_id,edge_type,status,from_kind,from_id,to_kind,to_id,confidence,evidence_json,is_dynamic,generation) VALUES(?,?,?,?,?,?,?,?,?,?,?)").run(workspaceId, "HANDLER_CALLS_TRANSPORT_METHOD", "terminal", "call", String(call.id), "transport_method", String(op || "transport_client_method"), Number(call.confidence ?? 0.5), JSON.stringify({ ...evidence, classification: "transport_client_method" }), 0, generation);
2063
2079
  return { status: "terminal", callType };
2064
2080
  }
2065
2081
  if (resolution.target) {
@@ -2070,7 +2086,7 @@ function insertCallEdge(db, workspaceId, call, vars, generation) {
2070
2086
  const status = edgeType === "DYNAMIC_EDGE_CANDIDATE" ? "dynamic" : resolution.status === "ambiguous" ? "ambiguous" : edgeType === "UNRESOLVED_EDGE" ? "unresolved" : "terminal";
2071
2087
  const unresolvedReason = status === "terminal" ? null : String(call.unresolved_reason ?? unresolvedOperationReason(resolution));
2072
2088
  const externalTarget = callType === "external_http" ? externalHttpTarget(call) : void 0;
2073
- const targetKind = callType === "local_db_query" ? "db_entity" : callType.startsWith("async_") ? "event" : externalTarget?.toKind ?? "external_endpoint";
2089
+ const targetKind = callType === "local_db_query" ? "db_entity" : callType.startsWith("async_") ? "event" : callType === "external_http" ? externalTarget?.toKind ?? "external_endpoint" : "operation_candidate";
2074
2090
  const targetId = callType === "local_db_query" ? String(call.query_entity ?? "unknown") : callType === "remote_action" ? op ? `Remote action: ${op}` : call.unresolved_reason === "dynamic_operation_path_identifier" ? "Remote action: dynamic path" : "Remote action: unknown path" : callType === "external_http" ? String(externalTarget?.toId ?? "unknown") : String(call.event_name_expr ?? op ?? "unknown");
2075
2091
  const graphLevelDynamic = edgeType === "DYNAMIC_EDGE_CANDIDATE" && resolution.status === "dynamic";
2076
2092
  const finalEvidence = externalTarget ? { ...evidence, externalTarget } : evidence;
@@ -2329,9 +2345,9 @@ function candidateEvidence(candidate, rank) {
2329
2345
  }
2330
2346
  function implementationMethodSignal(row, operation) {
2331
2347
  const op = normalizedOperationName(String(operation.operationPath ?? operation.operationName ?? ""));
2332
- const decorator = normalizeDecoratorOperation(typeof row.decoratorValue === "string" ? row.decoratorValue : void 0, typeof row.decoratorRawExpression === "string" ? row.decoratorRawExpression : void 0);
2333
- if (decorator && decorator === op) return { matches: true, contradicted: false, acceptedReasons: ["decorator targets operation"], rejectedReasons: [] };
2334
- if (decorator && decorator !== op) return { matches: false, contradicted: true, acceptedReasons: [], rejectedReasons: ["method_name_matches_but_decorator_targets_different_operation"] };
2348
+ const decorator = normalizeDecoratorOperationSignal(typeof row.decoratorValue === "string" ? row.decoratorValue : void 0, typeof row.decoratorRawExpression === "string" ? row.decoratorRawExpression : void 0, op);
2349
+ if (decorator.status === "resolved" && decorator.operationName === op) return { matches: true, contradicted: false, acceptedReasons: ["decorator targets operation"], rejectedReasons: [] };
2350
+ if (decorator.status === "resolved" && decorator.operationName !== op) return { matches: false, contradicted: true, acceptedReasons: [], rejectedReasons: ["method_name_matches_but_decorator_targets_different_operation"] };
2335
2351
  if (String(row.methodName ?? "") === op) return { matches: true, contradicted: false, acceptedReasons: ["method name fallback matched operation"], rejectedReasons: [] };
2336
2352
  return { matches: false, contradicted: false, acceptedReasons: [], rejectedReasons: ["method name does not match operation"] };
2337
2353
  }
@@ -2363,17 +2379,17 @@ function operationStartScope(db, repoId, start) {
2363
2379
  FROM cds_operations o JOIN cds_services s ON s.id=o.service_id JOIN repositories r ON r.id=s.repo_id
2364
2380
  WHERE (? IS NULL OR r.id=?) AND (? IS NULL OR s.service_path=?) AND (o.operation_name=? OR o.operation_path=? OR o.operation_path=?)
2365
2381
  ORDER BY r.name,s.service_path,o.operation_name,o.id`).all(repoId, repoId, start.servicePath, start.servicePath, requested, requested, requested.startsWith("/") ? requested : `/${requested}`);
2366
- if (rows2.length === 0) return { diagnostics: [{ severity: "warning", code: "trace_start_not_found", message: "No indexed operation matched the requested trace selector", normalizedSelectorValue: requested }] };
2382
+ if (rows2.length === 0) return void 0;
2367
2383
  const repoCount = new Set(rows2.map((row) => String(row.repoName))).size;
2368
2384
  const serviceCount = new Set(rows2.map((row) => `${String(row.repoName)}:${String(row.servicePath)}`)).size;
2369
- if (!repoId && repoCount > 1) return { diagnostics: [{ severity: "warning", code: "trace_start_ambiguous", message: "Operation trace start matched multiple repositories; add --repo to disambiguate", normalizedSelectorValue: requested, candidates: rows2 }] };
2370
- if (!start.servicePath && serviceCount > 1) return { diagnostics: [{ severity: "warning", code: "trace_start_ambiguous", message: "Operation trace start matched multiple services; add --service to disambiguate", normalizedSelectorValue: requested, candidates: rows2 }] };
2371
- if (rows2.length !== 1) return { diagnostics: [{ severity: "warning", code: "trace_start_ambiguous", message: "Operation trace start matched multiple indexed operations", normalizedSelectorValue: requested, candidates: rows2 }] };
2385
+ if (!repoId && repoCount > 1) return { diagnostics: [{ severity: "warning", code: "trace_start_ambiguous", message: "Operation trace start matched multiple repositories; add --repo to disambiguate", normalizedSelectorValue: requested, resolutionStage: "operation", resolutionStatus: "ambiguous_operation", candidates: rows2 }] };
2386
+ if (!start.servicePath && serviceCount > 1) return { diagnostics: [{ severity: "warning", code: "trace_start_ambiguous", message: "Operation trace start matched multiple services; add --service to disambiguate", normalizedSelectorValue: requested, resolutionStage: "operation", resolutionStatus: "ambiguous_operation", candidates: rows2 }] };
2387
+ if (rows2.length !== 1) return { diagnostics: [{ severity: "warning", code: "trace_start_ambiguous", message: "Operation trace start matched multiple indexed operations", normalizedSelectorValue: requested, resolutionStage: "operation", resolutionStatus: "ambiguous_operation", candidates: rows2 }] };
2372
2388
  const operationId = String(rows2[0]?.operationId);
2373
2389
  const impl = implementationScope(db, operationId);
2374
2390
  if (impl.edge?.status === "resolved" && impl.files.size > 0) return { files: impl.files, symbols: impl.symbolId ? /* @__PURE__ */ new Set([impl.symbolId]) : void 0, operationId, diagnostics: [] };
2375
- if (impl.edge) return { operationId, diagnostics: [{ severity: "warning", code: impl.edge.status === "ambiguous" ? "trace_start_ambiguous" : "trace_start_not_found", message: `Indexed operation matched but implementation edge is ${String(impl.edge.status ?? "unresolved")}`, implementationEdgeId: impl.edge.id, implementationStatus: impl.edge.status, candidates: parseEvidence(impl.edge.evidence_json).candidates }] };
2376
- return { operationId, diagnostics: [{ severity: "warning", code: "trace_start_not_found", message: "Indexed operation matched but no implementation candidate exists" }] };
2391
+ if (impl.edge) return { operationId, diagnostics: [{ severity: "warning", code: impl.edge.status === "ambiguous" ? "trace_start_ambiguous" : "trace_start_implementation_unresolved", message: `Indexed operation matched but implementation edge is ${String(impl.edge.status ?? "unresolved")}`, resolutionStage: "implementation", resolutionStatus: impl.edge.status === "ambiguous" ? "ambiguous_implementation" : "rejected_implementation", implementationEdgeId: impl.edge.id, implementationStatus: impl.edge.status, candidates: parseEvidence(impl.edge.evidence_json).candidates }] };
2392
+ return { operationId, diagnostics: [{ severity: "warning", code: "trace_start_implementation_unresolved", message: "Indexed operation matched but no implementation candidate exists", resolutionStage: "implementation", resolutionStatus: "operation_without_implementation" }] };
2377
2393
  }
2378
2394
  function sourceFilesForStart(db, repoId, start) {
2379
2395
  const handler = start.handler;
@@ -2421,7 +2437,8 @@ function startScope(db, start) {
2421
2437
  ).get(start.repo, start.repo) : void 0;
2422
2438
  if (start.repo && !repo) return { repo, selectorMatched: false };
2423
2439
  const operationScope = operationStartScope(db, repo?.id, start);
2424
- const sourceScope = operationScope?.files ? operationScope : sourceFilesForStart(db, repo?.id, start);
2440
+ const terminalOperationScope = operationScope && !operationScope.files && (operationScope.diagnostics ?? []).some((d) => d.resolutionStage === "operation" || d.resolutionStage === "implementation");
2441
+ const sourceScope = operationScope?.files || terminalOperationScope ? operationScope : sourceFilesForStart(db, repo?.id, start);
2425
2442
  const sourceFiles = sourceScope?.files;
2426
2443
  const hasSelector = Boolean(
2427
2444
  start.handler ?? start.operation ?? start.operationPath ?? start.servicePath
@@ -2432,7 +2449,7 @@ function startScope(db, start) {
2432
2449
  repo,
2433
2450
  sourceFiles,
2434
2451
  symbolIds: sourceScope?.symbols,
2435
- selectorMatched: !hasSelector || sourceFiles !== void 0,
2452
+ selectorMatched: !terminalOperationScope && (!hasSelector || sourceFiles !== void 0),
2436
2453
  startOperationId: operationScope?.operationId,
2437
2454
  startDiagnostics: operationScope?.diagnostics
2438
2455
  };
@@ -2711,7 +2728,7 @@ function trace(db, start, options) {
2711
2728
  const op = operationNode(db, scope.startOperationId);
2712
2729
  const impl = implementationScope(db, scope.startOperationId);
2713
2730
  if (op) nodes.set(String(op.id), op);
2714
- if (impl.edge) {
2731
+ if (impl.edge && impl.edge.status === "resolved") {
2715
2732
  const implEvidence = { ...parseEvidence(impl.edge.evidence_json), startResolution: { strategy: "indexed_operation_graph", matchedOperationId: scope.startOperationId, implementationEdgeId: impl.edge.id, implementationStatus: impl.edge.status, selectedHandlerMethodId: impl.edge.status === "resolved" ? impl.edge.to_id : void 0 } };
2716
2733
  const handlerNode = impl.edge.status === "resolved" ? handlerMethodNode(db, impl.edge.to_id) : void 0;
2717
2734
  if (handlerNode) nodes.set(String(handlerNode.id), handlerNode);
@@ -2867,4 +2884,4 @@ export {
2867
2884
  linkWorkspace,
2868
2885
  trace
2869
2886
  };
2870
- //# sourceMappingURL=chunk-AM2JZFQ6.js.map
2887
+ //# sourceMappingURL=chunk-VT5FVMOA.js.map