@saptools/service-flow 0.1.24 → 0.1.26

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/dist/cli.js CHANGED
@@ -12,7 +12,7 @@ import {
12
12
  parsePackageJson,
13
13
  parseServiceBindings,
14
14
  trace
15
- } from "./chunk-RXJNPONU.js";
15
+ } from "./chunk-Q3J2QDNX.js";
16
16
 
17
17
  // src/cli.ts
18
18
  import { Command } from "commander";
@@ -188,7 +188,7 @@ function migrate(db) {
188
188
  // package.json
189
189
  var package_default = {
190
190
  name: "@saptools/service-flow",
191
- version: "0.1.24",
191
+ version: "0.1.26",
192
192
  description: "Trace SAP CAP service-to-service flows across multi-repository workspaces with runtime-aware graph resolution",
193
193
  type: "module",
194
194
  publishConfig: {
@@ -704,6 +704,40 @@ function isObjectFunction(node) {
704
704
  var commonTerminalMembers = /* @__PURE__ */ new Set(["push", "includes", "find", "findIndex", "map", "filter", "reduce", "forEach", "some", "every", "toUpperCase", "toLowerCase", "trim", "split", "join", "get", "set", "has"]);
705
705
  var loggerMembers = /* @__PURE__ */ new Set(["trace", "debug", "info", "warn", "error", "fatal", "log"]);
706
706
  var globalObjects = /* @__PURE__ */ new Set(["JSON", "Object", "Array", "String", "Number", "Boolean", "Math", "Date", "Promise", "Reflect"]);
707
+ var builtInConstructors = /* @__PURE__ */ new Set([
708
+ "Set",
709
+ "Map",
710
+ "WeakSet",
711
+ "WeakMap",
712
+ "Date",
713
+ "RegExp",
714
+ "URL",
715
+ "URLSearchParams",
716
+ "Error",
717
+ "EvalError",
718
+ "RangeError",
719
+ "ReferenceError",
720
+ "SyntaxError",
721
+ "TypeError",
722
+ "URIError",
723
+ "AggregateError",
724
+ "ArrayBuffer",
725
+ "SharedArrayBuffer",
726
+ "DataView",
727
+ "Int8Array",
728
+ "Uint8Array",
729
+ "Uint8ClampedArray",
730
+ "Int16Array",
731
+ "Uint16Array",
732
+ "Int32Array",
733
+ "Uint32Array",
734
+ "Float32Array",
735
+ "Float64Array",
736
+ "BigInt64Array",
737
+ "BigUint64Array",
738
+ "Promise",
739
+ "AbortController"
740
+ ]);
707
741
  var capDslRoots = /* @__PURE__ */ new Set(["SELECT", "INSERT", "UPSERT", "UPDATE", "DELETE"]);
708
742
  var requestHelpers = /* @__PURE__ */ new Set(["reject", "error", "info", "warn", "notify"]);
709
743
  var transportMembers = /* @__PURE__ */ new Set(["emit", "publish", "send", "on"]);
@@ -750,6 +784,25 @@ function argumentEvidence(args, source) {
750
784
  return { kind: "unsupported", expression: arg.getText(source) };
751
785
  });
752
786
  }
787
+ function bindingLocalName(name, initializer) {
788
+ if (ts.isIdentifier(name)) return name.text;
789
+ if (initializer && ts.isIdentifier(initializer)) return initializer.text;
790
+ return void 0;
791
+ }
792
+ function parameterBindings(params) {
793
+ return params.flatMap((param, index) => {
794
+ if (ts.isIdentifier(param.name)) return [{ index, kind: "identifier", name: param.name.text }];
795
+ if (!ts.isObjectBindingPattern(param.name)) return [];
796
+ const properties = param.name.elements.flatMap((element) => {
797
+ if (element.dotDotDotToken || ts.isObjectBindingPattern(element.name) || ts.isArrayBindingPattern(element.name)) return [];
798
+ const property = element.propertyName ? nameOf(element.propertyName) : nameOf(element.name);
799
+ if (!property) return [];
800
+ const local = bindingLocalName(element.name, element.initializer);
801
+ return local ? [{ property, local }] : [];
802
+ });
803
+ return properties.length > 0 ? [{ index, kind: "object_pattern", properties }] : [];
804
+ });
805
+ }
753
806
  async function parseExecutableSymbols(repoPath, filePath) {
754
807
  const text = await fs4.readFile(path4.join(repoPath, filePath), "utf8");
755
808
  const source = ts.createSourceFile(filePath, text, ts.ScriptTarget.Latest, true, filePath.endsWith(".ts") ? ts.ScriptKind.TS : ts.ScriptKind.JS);
@@ -760,6 +813,7 @@ async function parseExecutableSymbols(repoPath, filePath) {
760
813
  const exportNames = exportDeclarations(source);
761
814
  const objectExports = /* @__PURE__ */ new Set();
762
815
  const exportedClasses = /* @__PURE__ */ new Set();
816
+ const declaredClasses = /* @__PURE__ */ new Set();
763
817
  const proxyVariables = /* @__PURE__ */ new Map();
764
818
  const classInstances = /* @__PURE__ */ new Map();
765
819
  const addSymbol = (kind, localName, node, parentName, exportedName, evidence) => {
@@ -769,9 +823,11 @@ async function parseExecutableSymbols(repoPath, filePath) {
769
823
  const objectExported = parentName ? objectExports.has(parentRoot) : false;
770
824
  const classMemberExported = kind === "method" && parentName ? exportedClasses.has(parentRoot) && ts.isMethodDeclaration(node) && (ts.getCombinedModifierFlags(node) & ts.ModifierFlags.Static) !== 0 && isPublicClassMethod(node) : false;
771
825
  const effectiveExportedName = classMemberExported || objectExported ? qualifiedName : declaredExportName;
772
- const params = isFunctionLike(node) ? node.parameters.map((param) => nameOf(param.name)).filter((name) => Boolean(name)) : void 0;
826
+ const bindings = isFunctionLike(node) ? parameterBindings(node.parameters) : void 0;
827
+ const params = bindings?.flatMap((binding) => binding.kind === "identifier" ? [binding.name] : []);
773
828
  const sourceEvidence = evidence ?? (classMemberExported ? { source: "exported_class_member", exportedClass: parentRoot, memberKind: (ts.getCombinedModifierFlags(node) & ts.ModifierFlags.Static) !== 0 ? "static_method" : "class_method", parameters: params } : declaredExportName ? { exportedName: declaredExportName, source: "export_declaration" } : objectExported ? { exportedName: qualifiedName, source: "exported_object_literal" } : void 0);
774
- symbols.push({ kind, localName: kind === "object_method" ? qualifiedName : localName, exportedName: effectiveExportedName, qualifiedName, sourceFile, startLine: lineOf(source, node.getStart(source)), endLine: lineOf(source, node.getEnd()), startOffset: node.getStart(source), endOffset: node.getEnd(), exported: exported(node) || Boolean(effectiveExportedName), importExportEvidence: params && sourceEvidence ? { ...sourceEvidence, parameters: params } : sourceEvidence });
829
+ const parameterEvidence = bindings && bindings.length > 0 ? { parameters: params, parameterBindings: bindings } : {};
830
+ symbols.push({ kind, localName: kind === "object_method" ? qualifiedName : localName, exportedName: effectiveExportedName, qualifiedName, sourceFile, startLine: lineOf(source, node.getStart(source)), endLine: lineOf(source, node.getEnd()), startOffset: node.getStart(source), endOffset: node.getEnd(), exported: exported(node) || Boolean(effectiveExportedName), importExportEvidence: sourceEvidence ? { ...sourceEvidence, ...parameterEvidence } : bindings && bindings.length > 0 ? parameterEvidence : void 0 });
775
831
  };
776
832
  const addAliasSymbol = (objectName, propertyName, node, targetImportSource) => {
777
833
  symbols.push({ kind: "object_alias", localName: propertyName, exportedName: propertyName, qualifiedName: `${objectName}.${propertyName}`, sourceFile, startLine: lineOf(source, node.getStart(source)), endLine: lineOf(source, node.getEnd()), startOffset: node.getStart(source), endOffset: node.getEnd(), exported: true, importExportEvidence: { source: "exported_object_shorthand", objectName, propertyName, targetImportSource } });
@@ -799,6 +855,7 @@ async function parseExecutableSymbols(repoPath, filePath) {
799
855
  visitImports(source);
800
856
  const visitSymbols = (node, parentClass) => {
801
857
  if (ts.isClassDeclaration(node) && node.name) {
858
+ declaredClasses.add(node.name.text);
802
859
  if (exported(node) || exportNames.has(node.name.text)) exportedClasses.add(node.name.text);
803
860
  for (const member of node.members) visitSymbols(member, node.name.text);
804
861
  return;
@@ -872,11 +929,17 @@ async function parseExecutableSymbols(repoPath, filePath) {
872
929
  ts.forEachChild(node, visitProxyVariables);
873
930
  };
874
931
  visitProxyVariables(source);
932
+ const rememberClassInstance = (variableName, className, propertyName) => {
933
+ const importSource = imports.get(className);
934
+ if (!builtInConstructors.has(className) && (importSource && isRelativeImport(importSource) || declaredClasses.has(className))) classInstances.set(variableName, { className, importSource, propertyName });
935
+ };
875
936
  const visitClassInstances = (node) => {
876
937
  if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && ts.isNewExpression(node.initializer) && ts.isIdentifier(node.initializer.expression)) {
877
- const className = node.initializer.expression.text;
878
- const importSource = imports.get(className);
879
- if (!importSource || isRelativeImport(importSource)) classInstances.set(node.name.text, { className, importSource });
938
+ rememberClassInstance(node.name.text, node.initializer.expression.text);
939
+ }
940
+ if (ts.isPropertyDeclaration(node) && node.initializer && ts.isNewExpression(node.initializer) && ts.isIdentifier(node.initializer.expression)) {
941
+ const propertyName = nameOf(node.name);
942
+ if (propertyName) rememberClassInstance(`this.${propertyName}`, node.initializer.expression.text, propertyName);
880
943
  }
881
944
  ts.forEachChild(node, visitClassInstances);
882
945
  };
@@ -889,11 +952,12 @@ async function parseExecutableSymbols(repoPath, filePath) {
889
952
  if (caller) {
890
953
  const callee = callName(node.expression);
891
954
  const proxy = callee.local ? proxyVariables.get(callee.local) : void 0;
892
- const instance = callee.local ? classInstances.get(callee.local) : void 0;
955
+ const instance = (callee.local ? classInstances.get(callee.local) : void 0) ?? (callee.receiver ? classInstances.get(callee.receiver) : void 0);
893
956
  const importSource = instance?.importSource ?? proxy?.importSource ?? (callee.local ? imports.get(callee.local) : void 0) ?? (callee.member && callee.local ? imports.get(callee.local) : void 0);
894
- const targetName = instance && callee.member ? `${instance.className}.${callee.member}` : proxy && callee.member ? callee.member : callee.expression.startsWith("this.") ? callee.member : callee.member && callee.local ? `${callee.local}.${callee.member}` : callee.local;
957
+ const directThisMethod = callee.receiver === "this";
958
+ const targetName = instance && callee.member ? `${instance.className}.${callee.member}` : proxy && callee.member ? callee.member : directThisMethod ? callee.member : callee.member && callee.local ? `${callee.local}.${callee.member}` : callee.local;
895
959
  const className = caller.qualifiedName.includes(".") ? caller.qualifiedName.split(".")[0] : void 0;
896
- const thisTarget = className && callee.member ? `${className}.${callee.member}` : void 0;
960
+ const thisTarget = directThisMethod && className && callee.member ? `${className}.${callee.member}` : void 0;
897
961
  const loggerLike = callee.receiver?.endsWith(".logger") || callee.local === "logger" || (callee.expression.startsWith("this.logger.") && callee.member ? loggerMembers.has(callee.member) : false);
898
962
  const terminalMember = callee.member ? commonTerminalMembers.has(callee.member) || loggerMembers.has(callee.member) : false;
899
963
  const provenLocal = Boolean(targetName) && localCallables.has(String(targetName));
@@ -904,7 +968,7 @@ async function parseExecutableSymbols(repoPath, filePath) {
904
968
  const ignored = loggerLike || terminalMember || importedFromPackage || ignoredFrameworkCall(callee);
905
969
  const resolvedTarget = provenThisMethod ? thisTarget : targetName;
906
970
  const keep = Boolean(resolvedTarget) && !ignored && (provenLocal || provenThisMethod || provenRelativeImport || provenClassInstance);
907
- if (keep) calls.push({ callerQualifiedName: caller.qualifiedName, calleeExpression: callee.expression, calleeLocalName: resolvedTarget, receiverLocalName: callee.member ? callee.local : void 0, importSource, sourceFile, sourceLine: line, evidence: { relation: instance ? "class_instance_method" : proxy ? "relative_import_proxy_member" : importSource ? "relative_import" : provenThisMethod ? "indexed_this_method" : "indexed_local_symbol", caller: caller.qualifiedName, targetName: resolvedTarget, instanceVariable: instance ? callee.local : void 0, className: instance?.className, methodName: instance ? callee.member : void 0, classImportSource: instance?.importSource, callArguments: argumentEvidence(node.arguments, source), proxyVariableName: proxy?.variableName, factory: proxy?.factory, factoryExpression: proxy?.factory, factoryImportSource: proxy?.importSource, candidateStrategy: instance ? instance.importSource ? "relative_import_class_instance_method" : "same_file_class_instance_method" : proxy ? "proxy_member_exact_export_or_unique_member" : void 0 } });
971
+ if (keep) calls.push({ callerQualifiedName: caller.qualifiedName, calleeExpression: callee.expression, calleeLocalName: resolvedTarget, receiverLocalName: callee.member ? callee.local ?? callee.receiver : void 0, importSource, sourceFile, sourceLine: line, evidence: { relation: instance ? "class_instance_method" : proxy ? "relative_import_proxy_member" : importSource ? "relative_import" : provenThisMethod ? "indexed_this_method" : "indexed_local_symbol", caller: caller.qualifiedName, targetName: resolvedTarget, instanceVariable: instance ? instance.propertyName ?? callee.local : void 0, className: instance?.className, methodName: instance ? callee.member : void 0, classImportSource: instance?.importSource, callArguments: argumentEvidence(node.arguments, source), proxyVariableName: proxy?.variableName, factory: proxy?.factory, factoryExpression: proxy?.factory, factoryImportSource: proxy?.importSource, candidateStrategy: instance ? instance.importSource ? "relative_import_class_instance_method" : "same_file_class_instance_method" : proxy ? "proxy_member_exact_export_or_unique_member" : void 0 } });
908
972
  }
909
973
  }
910
974
  ts.forEachChild(node, visitCalls);
@@ -1221,12 +1285,18 @@ function parserQualityDiagnostics(db, strict) {
1221
1285
  const aliasQuality = identityAliasBindingQuality(db);
1222
1286
  const noBindingQuality = remoteActionNoBindingQuality(db);
1223
1287
  const contextualQuality = contextualImplementationQuality(db);
1288
+ const classInstanceQuality = classInstanceNoiseQuality(db);
1289
+ const bindingPropagationQuality = contextualBindingPropagationQuality(db);
1224
1290
  const wrapperQuality = wrapperPathPropagationQuality(db);
1291
+ const nestedThisQuality = nestedThisReceiverQuality(db);
1225
1292
  return [
1226
1293
  aliasQuality,
1227
1294
  noBindingQuality,
1228
1295
  contextualQuality,
1296
+ classInstanceQuality,
1297
+ bindingPropagationQuality,
1229
1298
  wrapperQuality,
1299
+ nestedThisQuality,
1230
1300
  remoteQuery,
1231
1301
  invocation,
1232
1302
  remoteAction,
@@ -1253,7 +1323,16 @@ function remoteActionNoBindingQuality(db) {
1253
1323
  WHEN c.unresolved_reason='dynamic_operation_path_identifier' THEN 'dynamic_path_identifier'
1254
1324
  WHEN json_extract(c.evidence_json,'$.classifier')='higher_order_wrapper_literal_path' OR json_extract(c.evidence_json,'$.operationPathExpression') IS NOT NULL THEN 'likely_higher_order_wrapper_path_needed'
1255
1325
  WHEN json_extract(c.evidence_json,'$.receiver') LIKE '%.%' THEN 'likely_parameter_context_needed'
1256
- WHEN EXISTS (SELECT 1 FROM symbol_calls sc WHERE sc.status='resolved' AND json_extract(sc.evidence_json,'$.relation')='class_instance_method' AND sc.source_file=c.source_file) THEN 'likely_instance_method_context_needed'
1326
+ WHEN EXISTS (
1327
+ SELECT 1 FROM symbol_calls sc
1328
+ JOIN symbols caller ON caller.id=sc.caller_symbol_id
1329
+ JOIN symbols callee ON callee.id=sc.callee_symbol_id
1330
+ WHERE sc.status='resolved'
1331
+ AND sc.source_file=c.source_file
1332
+ AND caller.id=c.source_symbol_id
1333
+ AND json_extract(sc.evidence_json,'$.relation')='class_instance_method'
1334
+ AND (callee.evidence_json IS NULL OR json_extract(callee.evidence_json,'$.parameterBindings') IS NULL)
1335
+ ) THEN 'likely_instance_method_parameter_metadata_needed'
1257
1336
  WHEN EXISTS (SELECT 1 FROM service_bindings b WHERE b.repo_id=c.repo_id AND b.source_file=c.source_file AND ABS(b.source_line-c.source_line) < 50) THEN 'likely_missing_assignment_binding'
1258
1337
  WHEN e.status='unresolved' AND COALESCE(e.unresolved_reason,'') LIKE '%No indexed target operation%' THEN 'no_indexed_target_operation'
1259
1338
  WHEN c.operation_path_expr IS NOT NULL AND (c.operation_path_expr LIKE '/%' OR c.operation_path_expr NOT LIKE '%/%') THEN 'operation_path_only_no_static_service_signal'
@@ -1268,6 +1347,46 @@ function remoteActionNoBindingQuality(db) {
1268
1347
  const total = rows.reduce((sum, row) => sum + Number(row.count ?? 0), 0);
1269
1348
  return { severity: total > 0 ? "warning" : "info", code: "strict_remote_action_no_binding_quality", message: "Remote actions with operation paths but no service binding id", total, breakdown: rows, examples };
1270
1349
  }
1350
+ function classInstanceNoiseQuality(db) {
1351
+ const builtIns = ["Set", "Map", "WeakSet", "WeakMap", "Date", "RegExp", "URL", "URLSearchParams", "Error", "EvalError", "RangeError", "ReferenceError", "SyntaxError", "TypeError", "URIError", "AggregateError", "ArrayBuffer", "SharedArrayBuffer", "DataView", "Int8Array", "Uint8Array", "Uint8ClampedArray", "Int16Array", "Uint16Array", "Int32Array", "Uint32Array", "Float32Array", "Float64Array", "BigInt64Array", "BigUint64Array", "Promise", "AbortController"];
1352
+ const placeholders = builtIns.map(() => "?").join(",");
1353
+ const aggregate = db.prepare(`SELECT COUNT(*) total,
1354
+ SUM(CASE WHEN status='unresolved' THEN 1 ELSE 0 END) unresolved,
1355
+ SUM(CASE WHEN status='unresolved' AND json_extract(evidence_json,'$.className') IN (${placeholders}) THEN 1 ELSE 0 END) unresolvedBuiltIn
1356
+ FROM symbol_calls WHERE json_extract(evidence_json,'$.relation')='class_instance_method'`).get(...builtIns);
1357
+ const byConstructor = db.prepare(`SELECT json_extract(evidence_json,'$.className') constructorName,COUNT(*) unresolvedCount
1358
+ FROM symbol_calls WHERE status='unresolved' AND json_extract(evidence_json,'$.relation')='class_instance_method'
1359
+ GROUP BY constructorName ORDER BY unresolvedCount DESC,constructorName LIMIT 10`).all();
1360
+ return { severity: Number(aggregate.unresolvedBuiltIn ?? 0) > 0 ? "warning" : "info", code: "strict_class_instance_noise_quality", message: "Class-instance symbol-call aggregate with built-in constructor guard", totalClassInstanceCalls: Number(aggregate.total ?? 0), unresolvedClassInstanceCalls: Number(aggregate.unresolved ?? 0), unresolvedBuiltInClassInstanceCalls: Number(aggregate.unresolvedBuiltIn ?? 0), unresolvedByConstructor: byConstructor };
1361
+ }
1362
+ function contextualBindingPropagationQuality(db) {
1363
+ const serviceClientCalls = db.prepare(`SELECT COUNT(*) count FROM symbol_calls sc
1364
+ WHERE json_extract(sc.evidence_json,'$.callArguments[0].kind') IN ('identifier','object_literal')`).get();
1365
+ const missingMetadata = db.prepare(`SELECT COUNT(*) count FROM symbol_calls sc JOIN symbols s ON s.id=sc.callee_symbol_id
1366
+ WHERE sc.status='resolved' AND json_extract(sc.evidence_json,'$.callArguments[0].kind') IN ('identifier','object_literal')
1367
+ AND (s.evidence_json IS NULL OR json_extract(s.evidence_json,'$.parameterBindings') IS NULL)`).get();
1368
+ const destructuredUnmapped = db.prepare(`SELECT COUNT(*) count FROM symbol_calls sc JOIN symbols s ON s.id=sc.callee_symbol_id
1369
+ WHERE json_extract(sc.evidence_json,'$.callArguments[0].kind')='object_literal'
1370
+ AND json_extract(s.evidence_json,'$.parameterBindings[0].kind')='object_pattern'
1371
+ AND json_array_length(json_extract(sc.evidence_json,'$.callArguments[0].properties')) > json_array_length(json_extract(s.evidence_json,'$.parameterBindings[0].properties'))`).get();
1372
+ const opportunities = db.prepare(`SELECT c.source_file sourceFile,c.source_line sourceLine,json_extract(c.evidence_json,'$.receiver') receiverName,c.operation_path_expr operationPath,'trace_time_contextual_binding_candidate' opportunity
1373
+ FROM outbound_calls c LEFT JOIN graph_edges e ON e.from_kind='call' AND e.from_id=CAST(c.id AS TEXT)
1374
+ WHERE c.call_type='remote_action' AND c.service_binding_id IS NULL AND json_extract(c.evidence_json,'$.receiver') IS NOT NULL
1375
+ AND (e.status IS NULL OR e.status!='resolved')
1376
+ AND EXISTS (SELECT 1 FROM symbol_calls sc WHERE sc.status='resolved' AND sc.source_file=c.source_file)
1377
+ ORDER BY c.source_file,c.source_line LIMIT 8`).all();
1378
+ return { severity: Number(missingMetadata.count ?? 0) + Number(destructuredUnmapped.count ?? 0) + opportunities.length > 0 ? "warning" : "info", code: "strict_contextual_binding_propagation_quality", message: "Contextual service-client propagation opportunities for trace-time helper resolution", localSymbolCallsWithServiceClientArguments: Number(serviceClientCalls.count ?? 0), calleeSymbolsMissingParameterMetadata: Number(missingMetadata.count ?? 0), destructuredObjectParametersPossiblyUnmapped: Number(destructuredUnmapped.count ?? 0), traceTimeContextualOpportunities: opportunities.length, examples: opportunities };
1379
+ }
1380
+ function nestedThisReceiverQuality(db) {
1381
+ const aggregate = db.prepare(`SELECT COUNT(*) total,
1382
+ SUM(CASE WHEN json_extract(evidence_json,'$.relation')='indexed_this_method' THEN 1 ELSE 0 END) resolvedToCurrentClass,
1383
+ SUM(CASE WHEN json_extract(evidence_json,'$.relation')='class_instance_method' THEN 1 ELSE 0 END) withExplicitHelperInstanceEvidence
1384
+ FROM symbol_calls WHERE callee_expression LIKE 'this.%.%'`).get();
1385
+ const examples = db.prepare(`SELECT source_file sourceFile,source_line sourceLine,callee_expression calleeExpression,json_extract(evidence_json,'$.relation') relation,json_extract(evidence_json,'$.targetName') targetName
1386
+ FROM symbol_calls WHERE callee_expression LIKE 'this.%.%' AND json_extract(evidence_json,'$.relation')='indexed_this_method'
1387
+ ORDER BY source_file,source_line LIMIT 8`).all();
1388
+ return { severity: Number(aggregate.resolvedToCurrentClass ?? 0) > 0 ? "warning" : "info", code: "strict_nested_this_receiver_quality", message: "Nested this receiver symbol-call aggregate", nestedThisReceiverCallsConsidered: Number(aggregate.total ?? 0), nestedThisResolvedToCurrentClass: Number(aggregate.resolvedToCurrentClass ?? 0), nestedThisWithExplicitHelperInstanceEvidence: Number(aggregate.withExplicitHelperInstanceEvidence ?? 0), warningExamples: examples };
1389
+ }
1271
1390
  function contextualImplementationQuality(db) {
1272
1391
  const rows = db.prepare(`SELECT status,COALESCE(unresolved_reason,status) reason,COUNT(*) count
1273
1392
  FROM graph_edges WHERE edge_type='OPERATION_IMPLEMENTED_BY_HANDLER' AND status IN ('ambiguous','unresolved') GROUP BY status,reason ORDER BY status,count DESC,reason`).all();