@saptools/service-flow 0.1.48 → 0.1.50

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
@@ -14,8 +14,9 @@ import {
14
14
  parseOutboundCalls,
15
15
  parsePackageJson,
16
16
  parseServiceBindings,
17
+ parseVars,
17
18
  trace
18
- } from "./chunk-EGY2A4AT.js";
19
+ } from "./chunk-52OUS3MO.js";
19
20
 
20
21
  // src/cli.ts
21
22
  import { Command } from "commander";
@@ -101,7 +102,7 @@ CREATE TABLE IF NOT EXISTS cds_services (id INTEGER PRIMARY KEY, repo_id INTEGER
101
102
  CREATE TABLE IF NOT EXISTS cds_operations (id INTEGER PRIMARY KEY, service_id INTEGER NOT NULL, operation_type TEXT NOT NULL, operation_name TEXT NOT NULL, operation_path TEXT NOT NULL, params_json TEXT NOT NULL, return_type TEXT, source_file TEXT NOT NULL, source_line INTEGER NOT NULL, provenance TEXT NOT NULL DEFAULT 'direct', base_operation_id INTEGER, FOREIGN KEY(service_id) REFERENCES cds_services(id) ON DELETE CASCADE, FOREIGN KEY(base_operation_id) REFERENCES cds_operations(id) ON DELETE SET NULL);
102
103
  CREATE TABLE IF NOT EXISTS symbols (id INTEGER PRIMARY KEY, repo_id INTEGER NOT NULL, file_id INTEGER, kind TEXT NOT NULL, name TEXT NOT NULL, qualified_name TEXT NOT NULL, exported INTEGER NOT NULL, start_line INTEGER NOT NULL, end_line INTEGER NOT NULL, start_offset INTEGER, end_offset INTEGER, source_file TEXT, exported_name TEXT, evidence_json TEXT, FOREIGN KEY(repo_id) REFERENCES repositories(id) ON DELETE CASCADE, FOREIGN KEY(file_id) REFERENCES files(id) ON DELETE CASCADE);
103
104
  CREATE TABLE IF NOT EXISTS handler_classes (id INTEGER PRIMARY KEY, repo_id INTEGER NOT NULL, symbol_id INTEGER, class_name TEXT NOT NULL, source_file TEXT NOT NULL, source_line INTEGER NOT NULL, FOREIGN KEY(repo_id) REFERENCES repositories(id) ON DELETE CASCADE, FOREIGN KEY(symbol_id) REFERENCES symbols(id) ON DELETE SET NULL);
104
- CREATE TABLE IF NOT EXISTS handler_methods (id INTEGER PRIMARY KEY, handler_class_id INTEGER NOT NULL, method_name TEXT NOT NULL, decorator_kind TEXT NOT NULL, decorator_value TEXT, decorator_raw_expression TEXT NOT NULL, source_file TEXT NOT NULL, source_line INTEGER NOT NULL, FOREIGN KEY(handler_class_id) REFERENCES handler_classes(id) ON DELETE CASCADE);
105
+ CREATE TABLE IF NOT EXISTS handler_methods (id INTEGER PRIMARY KEY, handler_class_id INTEGER NOT NULL, method_name TEXT NOT NULL, decorator_kind TEXT NOT NULL, decorator_value TEXT, decorator_raw_expression TEXT NOT NULL, decorator_resolution_json TEXT NOT NULL DEFAULT '{}', source_file TEXT NOT NULL, source_line INTEGER NOT NULL, FOREIGN KEY(handler_class_id) REFERENCES handler_classes(id) ON DELETE CASCADE);
105
106
  CREATE TABLE IF NOT EXISTS handler_registrations (id INTEGER PRIMARY KEY, repo_id INTEGER NOT NULL, handler_class_id INTEGER, class_name TEXT, import_source TEXT, registration_file TEXT NOT NULL, registration_line INTEGER NOT NULL, registration_kind TEXT NOT NULL, confidence REAL NOT NULL, FOREIGN KEY(repo_id) REFERENCES repositories(id) ON DELETE CASCADE, FOREIGN KEY(handler_class_id) REFERENCES handler_classes(id) ON DELETE SET NULL);
106
107
  CREATE TABLE IF NOT EXISTS service_bindings (id INTEGER PRIMARY KEY, repo_id INTEGER NOT NULL, symbol_id INTEGER, variable_name TEXT NOT NULL, alias TEXT, alias_expr TEXT, destination_expr TEXT, service_path_expr TEXT, is_dynamic INTEGER NOT NULL, placeholders_json TEXT NOT NULL, source_file TEXT NOT NULL, source_line INTEGER NOT NULL, helper_chain_json TEXT, FOREIGN KEY(repo_id) REFERENCES repositories(id) ON DELETE CASCADE, FOREIGN KEY(symbol_id) REFERENCES symbols(id) ON DELETE SET NULL);
107
108
  CREATE TABLE IF NOT EXISTS outbound_calls (id INTEGER PRIMARY KEY, repo_id INTEGER NOT NULL, source_symbol_id INTEGER, call_type TEXT NOT NULL, service_binding_id INTEGER, method TEXT, operation_path_expr TEXT, query_entity TEXT, event_name_expr TEXT, payload_summary TEXT, source_file TEXT NOT NULL, source_line INTEGER NOT NULL, confidence REAL NOT NULL, unresolved_reason TEXT, local_service_name TEXT, local_service_lookup TEXT, alias_chain_json TEXT, evidence_json TEXT, external_target_kind TEXT, external_target_id TEXT, external_target_label TEXT, external_target_dynamic INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(repo_id) REFERENCES repositories(id) ON DELETE CASCADE, FOREIGN KEY(source_symbol_id) REFERENCES symbols(id) ON DELETE SET NULL, FOREIGN KEY(service_binding_id) REFERENCES service_bindings(id) ON DELETE SET NULL);
@@ -120,8 +121,11 @@ CREATE VIRTUAL TABLE IF NOT EXISTS search_index USING fts5(kind, name, path, rep
120
121
  `;
121
122
 
122
123
  // src/db/migrations.ts
123
- var CURRENT_SCHEMA_VERSION = 9;
124
+ var CURRENT_SCHEMA_VERSION = 10;
124
125
  var columns = {
126
+ handler_methods: [
127
+ { name: "decorator_resolution_json", ddl: "ALTER TABLE handler_methods ADD COLUMN decorator_resolution_json TEXT NOT NULL DEFAULT '{}'" }
128
+ ],
125
129
  service_bindings: [
126
130
  { name: "helper_chain_json", ddl: "ALTER TABLE service_bindings ADD COLUMN helper_chain_json TEXT" },
127
131
  { name: "alias_expr", ddl: "ALTER TABLE service_bindings ADD COLUMN alias_expr TEXT" }
@@ -210,7 +214,7 @@ function migrate(db) {
210
214
  // package.json
211
215
  var package_default = {
212
216
  name: "@saptools/service-flow",
213
- version: "0.1.48",
217
+ version: "0.1.50",
214
218
  description: "Trace SAP CAP service-to-service flows across multi-repository workspaces with runtime-aware graph resolution",
215
219
  type: "module",
216
220
  publishConfig: {
@@ -518,7 +522,7 @@ function insertHandler(db, repoId, h) {
518
522
  ).get(repoId, sid, h.className, h.sourceFile, h.sourceLine)?.id
519
523
  );
520
524
  const stmt = db.prepare(
521
- "INSERT INTO handler_methods(handler_class_id,method_name,decorator_kind,decorator_value,decorator_raw_expression,source_file,source_line) VALUES(?,?,?,?,?,?,?)"
525
+ "INSERT INTO handler_methods(handler_class_id,method_name,decorator_kind,decorator_value,decorator_raw_expression,decorator_resolution_json,source_file,source_line) VALUES(?,?,?,?,?,?,?,?)"
522
526
  );
523
527
  for (const m of h.methods)
524
528
  stmt.run(
@@ -527,6 +531,7 @@ function insertHandler(db, repoId, h) {
527
531
  m.decoratorKind,
528
532
  m.decoratorValue,
529
533
  m.decoratorRawExpression,
534
+ JSON.stringify(m.decoratorResolution),
530
535
  m.sourceFile,
531
536
  m.sourceLine
532
537
  );
@@ -554,11 +559,15 @@ function insertRegistrations(db, repoId, rows) {
554
559
  }
555
560
  function insertBindings(db, repoId, rows) {
556
561
  const stmt = db.prepare(
557
- "INSERT INTO service_bindings(repo_id,variable_name,alias,alias_expr,destination_expr,service_path_expr,is_dynamic,placeholders_json,source_file,source_line,helper_chain_json) VALUES(?,?,?,?,?,?,?,?,?,?,?)"
562
+ "INSERT INTO service_bindings(repo_id,symbol_id,variable_name,alias,alias_expr,destination_expr,service_path_expr,is_dynamic,placeholders_json,source_file,source_line,helper_chain_json) VALUES(?,(SELECT id FROM symbols WHERE repo_id=? AND source_file=? AND start_line<=? AND end_line>=? ORDER BY (end_line-start_line),id LIMIT 1),?,?,?,?,?,?,?,?,?,?)"
558
563
  );
559
564
  for (const r of rows)
560
565
  stmt.run(
561
566
  repoId,
567
+ repoId,
568
+ r.sourceFile,
569
+ r.sourceLine,
570
+ r.sourceLine,
562
571
  r.variableName,
563
572
  r.alias,
564
573
  r.aliasExpr,
@@ -612,9 +621,14 @@ function resolveSymbolCallTarget(db, repoId, r) {
612
621
  }
613
622
  function insertCalls(db, repoId, rows) {
614
623
  const stmt = db.prepare(
615
- "INSERT INTO outbound_calls(repo_id,source_symbol_id,call_type,method,operation_path_expr,query_entity,event_name_expr,payload_summary,source_file,source_line,confidence,unresolved_reason,local_service_name,local_service_lookup,alias_chain_json,evidence_json,external_target_kind,external_target_id,external_target_label,external_target_dynamic,service_binding_id) VALUES(?,COALESCE((SELECT id FROM symbols WHERE repo_id=? AND source_file=? AND qualified_name=? ORDER BY id LIMIT 1),(SELECT id FROM symbols WHERE repo_id=? AND source_file=? AND start_line<=? AND end_line>=? ORDER BY (end_line-start_line),id LIMIT 1)),?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,(SELECT id FROM service_bindings WHERE repo_id=? AND variable_name=? AND source_file=? ORDER BY CASE WHEN source_line<=? THEN 0 ELSE 1 END, ABS(source_line-?) ASC, id DESC LIMIT 1))"
624
+ "INSERT INTO outbound_calls(repo_id,source_symbol_id,call_type,method,operation_path_expr,query_entity,event_name_expr,payload_summary,source_file,source_line,confidence,unresolved_reason,local_service_name,local_service_lookup,alias_chain_json,evidence_json,external_target_kind,external_target_id,external_target_label,external_target_dynamic,service_binding_id) VALUES(?,COALESCE((SELECT id FROM symbols WHERE repo_id=? AND source_file=? AND qualified_name=? ORDER BY id LIMIT 1),(SELECT id FROM symbols WHERE repo_id=? AND source_file=? AND start_line<=? AND end_line>=? ORDER BY (end_line-start_line),id LIMIT 1)),?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)"
616
625
  );
617
- for (const r of rows)
626
+ for (const r of rows) {
627
+ const binding = resolvePersistedBinding(db, repoId, r);
628
+ const evidence = {
629
+ ...r.evidence ?? {},
630
+ serviceBindingResolution: binding.evidence
631
+ };
618
632
  stmt.run(
619
633
  repoId,
620
634
  repoId,
@@ -633,21 +647,146 @@ function insertCalls(db, repoId, rows) {
633
647
  r.sourceFile,
634
648
  r.sourceLine,
635
649
  r.confidence,
636
- r.unresolvedReason,
650
+ r.unresolvedReason ?? binding.unresolvedReason,
637
651
  r.localServiceName,
638
652
  r.localServiceLookup,
639
653
  r.aliasChain ? JSON.stringify(r.aliasChain) : null,
640
- r.evidence ? JSON.stringify(r.evidence) : null,
654
+ JSON.stringify(evidence),
641
655
  r.externalTarget?.kind ?? null,
642
656
  r.externalTarget?.stableId ?? null,
643
657
  r.externalTarget?.label ?? null,
644
658
  r.externalTarget?.dynamic ? 1 : 0,
645
- repoId,
646
- r.serviceVariableName,
647
- r.sourceFile,
648
- r.sourceLine,
649
- r.sourceLine
659
+ binding.bindingId
650
660
  );
661
+ }
662
+ }
663
+ function resolvePersistedBinding(db, repoId, call) {
664
+ if (!call.serviceVariableName)
665
+ return { bindingId: null, evidence: { status: "not_applicable", candidateCount: 0 } };
666
+ const candidates = bindingCandidates(db, repoId, call);
667
+ const prior = candidates.filter((candidate) => candidate.sourceLine <= call.sourceLine);
668
+ const families = new Set(prior.map(bindingSignature));
669
+ if (prior.length > 0 && families.size === 1) {
670
+ const selected = prior.at(-1);
671
+ return {
672
+ bindingId: selected?.id ?? null,
673
+ evidence: bindingEvidence("selected", prior, selected)
674
+ };
675
+ }
676
+ if (prior.length > 1) {
677
+ return {
678
+ bindingId: null,
679
+ unresolvedReason: "ambiguous_service_binding_candidates",
680
+ evidence: bindingEvidence("ambiguous", prior)
681
+ };
682
+ }
683
+ if (candidates.length > 0) {
684
+ return {
685
+ bindingId: null,
686
+ unresolvedReason: "service_binding_declared_after_call",
687
+ evidence: bindingEvidence("rejected_future_binding", candidates)
688
+ };
689
+ }
690
+ return {
691
+ bindingId: null,
692
+ evidence: bindingEvidence("unrecoverable", [])
693
+ };
694
+ }
695
+ function bindingCandidates(db, repoId, call) {
696
+ const ownerId = callSymbolId(db, repoId, call);
697
+ const rows = db.prepare(`
698
+ SELECT id,symbol_id symbolId,variable_name variableName,alias,alias_expr aliasExpr,
699
+ destination_expr destinationExpr,service_path_expr servicePathExpr,
700
+ source_file sourceFile,source_line sourceLine,helper_chain_json helperChainJson
701
+ FROM service_bindings
702
+ WHERE repo_id=? AND variable_name=? AND source_file=?
703
+ AND (? IS NULL OR symbol_id IS NULL OR symbol_id=?)
704
+ ORDER BY source_line,id
705
+ `).all(
706
+ repoId,
707
+ call.serviceVariableName,
708
+ call.sourceFile,
709
+ ownerId,
710
+ ownerId
711
+ );
712
+ return rows.flatMap((row) => {
713
+ if (typeof row.id !== "number" || typeof row.variableName !== "string" || typeof row.sourceFile !== "string" || typeof row.sourceLine !== "number")
714
+ return [];
715
+ return [{
716
+ id: row.id,
717
+ symbolId: nullableNumber(row.symbolId),
718
+ variableName: row.variableName,
719
+ alias: nullableString(row.alias),
720
+ aliasExpr: nullableString(row.aliasExpr),
721
+ destinationExpr: nullableString(row.destinationExpr),
722
+ servicePathExpr: nullableString(row.servicePathExpr),
723
+ sourceFile: row.sourceFile,
724
+ sourceLine: row.sourceLine,
725
+ helperChainJson: nullableString(row.helperChainJson)
726
+ }];
727
+ });
728
+ }
729
+ function nullableString(value) {
730
+ return value === null || typeof value === "string" ? value : void 0;
731
+ }
732
+ function nullableNumber(value) {
733
+ return value === null || typeof value === "number" ? value : void 0;
734
+ }
735
+ function callSymbolId(db, repoId, call) {
736
+ const row = db.prepare(`
737
+ SELECT id FROM symbols
738
+ WHERE repo_id=? AND source_file=?
739
+ AND ((? IS NOT NULL AND qualified_name=?)
740
+ OR (start_line<=? AND end_line>=?))
741
+ ORDER BY CASE WHEN qualified_name=? THEN 0 ELSE 1 END,
742
+ (end_line-start_line),id
743
+ LIMIT 1
744
+ `).get(
745
+ repoId,
746
+ call.sourceFile,
747
+ call.sourceSymbolQualifiedName,
748
+ call.sourceSymbolQualifiedName,
749
+ call.sourceLine,
750
+ call.sourceLine,
751
+ call.sourceSymbolQualifiedName
752
+ );
753
+ return typeof row?.id === "number" ? row.id : void 0;
754
+ }
755
+ function bindingEvidence(status, candidates, selected) {
756
+ return {
757
+ status,
758
+ candidateCount: candidates.length,
759
+ selectedBindingId: selected?.id,
760
+ sourceOrderRule: "binding_source_line_must_not_follow_call",
761
+ candidates: candidates.map((candidate) => ({
762
+ bindingId: candidate.id,
763
+ symbolId: candidate.symbolId,
764
+ variableName: candidate.variableName,
765
+ alias: candidate.alias,
766
+ aliasExpr: candidate.aliasExpr,
767
+ destinationExpr: candidate.destinationExpr,
768
+ servicePathExpr: candidate.servicePathExpr,
769
+ sourceFile: candidate.sourceFile,
770
+ sourceLine: candidate.sourceLine,
771
+ helperChain: parseBindingChain(candidate.helperChainJson)
772
+ }))
773
+ };
774
+ }
775
+ function bindingSignature(candidate) {
776
+ return JSON.stringify([
777
+ candidate.alias,
778
+ candidate.aliasExpr,
779
+ candidate.destinationExpr,
780
+ candidate.servicePathExpr
781
+ ]);
782
+ }
783
+ function parseBindingChain(value) {
784
+ if (!value) return void 0;
785
+ try {
786
+ return JSON.parse(value);
787
+ } catch {
788
+ return void 0;
789
+ }
651
790
  }
652
791
 
653
792
  // src/discovery/classify-repository.ts
@@ -1434,6 +1573,9 @@ function parserQualityDiagnostics(db, strict, options) {
1434
1573
  implementationCandidateQuality(db, Boolean(options.detail)),
1435
1574
  classInstanceNoiseQuality(db),
1436
1575
  contextualBindingPropagationQuality(db),
1576
+ serviceBindingQuality(db, Boolean(options.detail)),
1577
+ decoratorResolutionQuality(db),
1578
+ handlerRegistrationPairingQuality(db),
1437
1579
  nestedThisReceiverQuality(db),
1438
1580
  wrapperPathPropagationQuality(db),
1439
1581
  remoteQueryTargetQuality(db),
@@ -1524,9 +1666,18 @@ function addImplementationCategory(grouped, row) {
1524
1666
  const key = [category, baseOperation, reason, family].join("\0");
1525
1667
  const current = grouped.get(key) ?? { category, baseOperation, reason, candidateFamily: family, count: 0, servicePaths: [], examples: [] };
1526
1668
  const hintSuggestions = implementationSuggestions(evidence);
1669
+ const candidates = asRecords(evidence.candidates);
1527
1670
  current.count += 1;
1528
1671
  current.servicePaths.push(String(row.servicePath ?? evidence.servicePath ?? ""));
1529
- current.examples.push({ servicePath: row.servicePath, operation: row.operationName, status: row.status, reason: row.unresolvedReason, implementationHintSuggestions: hintSuggestions });
1672
+ current.examples.push({
1673
+ servicePath: row.servicePath,
1674
+ operation: row.operationName,
1675
+ status: row.status,
1676
+ reason: row.unresolvedReason,
1677
+ candidateCount: candidates.length,
1678
+ candidateEvidence: candidates.slice(0, 3),
1679
+ implementationHintSuggestions: hintSuggestions
1680
+ });
1530
1681
  grouped.set(key, current);
1531
1682
  }
1532
1683
  function implementationSuggestions(evidence) {
@@ -1628,6 +1779,128 @@ function contextualBindingPropagationQuality(db) {
1628
1779
  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 FROM outbound_calls c WHERE c.call_type='remote_action' AND json_extract(c.evidence_json,'$.receiver') IS NOT NULL AND c.service_binding_id IS NULL ORDER BY c.source_file,c.source_line LIMIT 8").all();
1629
1780
  return { severity: missing.count + opportunities.length > 0 ? "warning" : "info", code: "strict_contextual_binding_propagation_quality", message: "Contextual service-client propagation opportunities for trace-time helper resolution", calleeSymbolsMissingParameterMetadata: missing.count, traceTimeContextualOpportunities: opportunities.length, examples: opportunities };
1630
1781
  }
1782
+ function serviceBindingQuality(db, detail) {
1783
+ const rows = db.prepare(`
1784
+ SELECT c.source_file sourceFile,c.source_line sourceLine,
1785
+ c.unresolved_reason unresolvedReason,c.evidence_json evidenceJson,
1786
+ s.evidence_json symbolEvidenceJson
1787
+ FROM outbound_calls c
1788
+ LEFT JOIN symbols s ON s.id=c.source_symbol_id
1789
+ WHERE c.call_type='remote_action'
1790
+ AND c.operation_path_expr IS NOT NULL
1791
+ AND c.service_binding_id IS NULL
1792
+ ORDER BY c.source_file,c.source_line
1793
+ `).all();
1794
+ const groups = /* @__PURE__ */ new Map();
1795
+ for (const row of rows) {
1796
+ const category = bindingCategory(row);
1797
+ groups.set(category, [...groups.get(category) ?? [], bindingExample(row)]);
1798
+ }
1799
+ const categories = [...groups.entries()].map(([category, examples]) => ({
1800
+ category,
1801
+ count: examples.length,
1802
+ severity: "warning",
1803
+ suggestedAction: bindingCategoryAction(category),
1804
+ examples: examples.slice(0, 3),
1805
+ expandedExamples: detail ? examples : void 0
1806
+ }));
1807
+ return {
1808
+ severity: rows.length > 0 ? "warning" : "info",
1809
+ code: "strict_service_binding_quality",
1810
+ message: "Remote service-client binding quality aggregate",
1811
+ total: rows.length,
1812
+ categories
1813
+ };
1814
+ }
1815
+ function bindingCategory(row) {
1816
+ const evidence = parseObject(row.evidenceJson);
1817
+ const resolution = parseObject(evidence.serviceBindingResolution);
1818
+ if (resolution.status === "rejected_future_binding") return "direct_binding_missing";
1819
+ if (resolution.status === "ambiguous") return "ambiguous_binding_candidates";
1820
+ const receiver = evidence.receiver;
1821
+ const symbolEvidence = parseObject(row.symbolEvidenceJson);
1822
+ if (symbolHasReceiverParameter(symbolEvidence, receiver))
1823
+ return "contextual_binding_recoverable";
1824
+ if (!Array.isArray(symbolEvidence.parameterBindings))
1825
+ return "missing_symbol_parameter_metadata";
1826
+ return "unrecoverable_binding";
1827
+ }
1828
+ function symbolHasReceiverParameter(evidence, receiver) {
1829
+ if (typeof receiver !== "string" || !Array.isArray(evidence.parameterBindings))
1830
+ return false;
1831
+ return asRecords(evidence.parameterBindings).some((binding) => {
1832
+ if (binding.kind === "identifier") return binding.name === receiver;
1833
+ if (binding.kind === "object_pattern")
1834
+ return asRecords(binding.properties).some((property) => property.local === receiver);
1835
+ return asRecords(binding.elements).some((element) => element.local === receiver);
1836
+ });
1837
+ }
1838
+ function bindingExample(row) {
1839
+ const evidence = parseObject(row.evidenceJson);
1840
+ return {
1841
+ sourceFile: row.sourceFile,
1842
+ sourceLine: row.sourceLine,
1843
+ receiver: evidence.receiver,
1844
+ unresolvedReason: row.unresolvedReason,
1845
+ bindingResolution: evidence.serviceBindingResolution
1846
+ };
1847
+ }
1848
+ function bindingCategoryAction(category) {
1849
+ if (category === "direct_binding_missing")
1850
+ return "Move the binding before the call or bind the call to an earlier immutable client.";
1851
+ if (category === "contextual_binding_recoverable")
1852
+ return "Trace from the caller so parameter binding evidence can be applied.";
1853
+ if (category === "ambiguous_binding_candidates")
1854
+ return "Split mutable client alternatives or add a statically unique client assignment.";
1855
+ if (category === "missing_symbol_parameter_metadata")
1856
+ return "Use named or destructured parameters on an indexed helper symbol.";
1857
+ return "Add a direct CAP client binding or statically provable helper-return binding.";
1858
+ }
1859
+ function decoratorResolutionQuality(db) {
1860
+ const aggregate = db.prepare(`SELECT
1861
+ SUM(CASE WHEN json_extract(decorator_resolution_json,'$.resolutionKind')
1862
+ IN ('const_identifier','enum_member','const_object_property','generated_constant_name') THEN 1 ELSE 0 END) resolvedFromConstants,
1863
+ SUM(CASE WHEN json_extract(decorator_resolution_json,'$.resolutionKind')
1864
+ ='unresolved' THEN 1 ELSE 0 END) unresolvedExpressions
1865
+ FROM handler_methods`).get();
1866
+ const unresolved = Number(aggregate.unresolvedExpressions ?? 0);
1867
+ const examples = db.prepare(`SELECT hm.method_name methodName,
1868
+ hm.decorator_raw_expression rawExpression,
1869
+ json_extract(hm.decorator_resolution_json,'$.unresolvedReason') unresolvedReason,
1870
+ hm.source_file sourceFile,hm.source_line sourceLine
1871
+ FROM handler_methods hm
1872
+ WHERE json_extract(hm.decorator_resolution_json,'$.resolutionKind')='unresolved'
1873
+ ORDER BY hm.source_file,hm.source_line LIMIT 5`).all();
1874
+ return {
1875
+ severity: unresolved > 0 ? "warning" : "info",
1876
+ code: "strict_decorator_resolution_quality",
1877
+ message: "Handler decorator string-resolution aggregate",
1878
+ resolvedFromConstants: Number(aggregate.resolvedFromConstants ?? 0),
1879
+ unresolvedExpressions: unresolved,
1880
+ unresolvedExamples: examples
1881
+ };
1882
+ }
1883
+ function handlerRegistrationPairingQuality(db) {
1884
+ const mismatch = db.prepare(`SELECT COUNT(*) count
1885
+ FROM handler_registrations hr
1886
+ JOIN handler_classes hc ON hc.id=hr.handler_class_id
1887
+ WHERE hr.handler_class_id IS NOT NULL
1888
+ AND (hc.repo_id<>hr.repo_id OR hc.class_name<>hr.class_name)`).get();
1889
+ const prevented = db.prepare(`SELECT COUNT(*) count
1890
+ FROM handler_registrations hr
1891
+ JOIN handler_classes exactClass ON exactClass.id=hr.handler_class_id
1892
+ JOIN handler_classes otherClass ON otherClass.class_name=hr.class_name
1893
+ AND otherClass.repo_id<>hr.repo_id
1894
+ WHERE hr.handler_class_id IS NOT NULL AND hr.import_source IS NOT NULL`).get();
1895
+ const mismatched = Number(mismatch.count ?? 0);
1896
+ return {
1897
+ severity: mismatched > 0 ? "error" : "info",
1898
+ code: "strict_handler_registration_pairing_quality",
1899
+ message: "Handler registration and class ownership aggregate",
1900
+ mismatchedExactRegistrations: mismatched,
1901
+ preventedSyntheticCrossRepositoryPairs: Number(prevented.count ?? 0)
1902
+ };
1903
+ }
1631
1904
  function wrapperPathPropagationQuality(db) {
1632
1905
  const examples = db.prepare("SELECT source_file sourceFile,source_line sourceLine,json_extract(evidence_json,'$.receiver') receiverName,json_extract(evidence_json,'$.operationPathExpression') pathIdentifier FROM outbound_calls WHERE call_type='remote_action' AND unresolved_reason='dynamic_operation_path_identifier' ORDER BY source_file,source_line LIMIT 5").all();
1633
1906
  return { severity: examples.length > 0 ? "warning" : "info", code: "strict_wrapper_path_propagation_quality", message: "Dynamic path sends where send({ path }) used a path identifier", dynamicPathIdentifierCalls: examples.length, examples };
@@ -1699,10 +1972,33 @@ function externalHttpTargetQuality(db) {
1699
1972
  return { severity: bad > 0 ? "warning" : "info", code: "strict_external_http_target_quality", message: "External HTTP semantic target aggregate", totalExternalHttpCalls: Number(row.total ?? 0), numericTargetCount: Number(row.numericTargets ?? 0), invalidOrMissingExternalTargetEvidence: Number(row.invalidEvidence ?? 0) };
1700
1973
  }
1701
1974
  function odataInvocationResolutionQuality(db) {
1702
- const rows = db.prepare("SELECT c.operation_path_expr operationPathExpr,e.status status FROM outbound_calls c JOIN graph_edges e ON e.from_kind='call' AND e.from_id=CAST(c.id AS TEXT) WHERE c.call_type='remote_action' AND c.operation_path_expr LIKE '%(%'").all();
1975
+ const rows = db.prepare(`SELECT c.operation_path_expr operationPathExpr,
1976
+ c.source_file sourceFile,c.source_line sourceLine,e.id graphEdgeId,
1977
+ e.status status,e.evidence_json evidenceJson
1978
+ FROM outbound_calls c JOIN graph_edges e
1979
+ ON e.from_kind='call' AND e.from_id=CAST(c.id AS TEXT)
1980
+ WHERE c.call_type='remote_action' AND c.operation_path_expr LIKE '%(%'
1981
+ ORDER BY c.source_file,c.source_line`).all();
1703
1982
  const unresolved = rows.filter((row) => row.status === "unresolved" && normalizeODataOperationInvocationPath(row.operationPathExpr)?.wasInvocation).length;
1704
1983
  const ambiguous = rows.filter((row) => row.status === "ambiguous" && normalizeODataOperationInvocationPath(row.operationPathExpr)?.wasInvocation).length;
1705
- return { severity: unresolved + ambiguous > 0 ? "warning" : "info", code: "strict_odata_invocation_resolution_quality", message: "OData invocation-path resolution quality aggregate", totalInvocationRemoteActions: rows.length, resolvedInvocationCalls: rows.filter((row) => row.status === "resolved").length, dynamicInvocationCalls: rows.filter((row) => row.status === "dynamic").length, ambiguousInvocationCalls: rows.filter((row) => row.status === "ambiguous").length, unresolvedInvocationCalls: rows.filter((row) => row.status === "unresolved").length, ambiguousNormalizedCalls: ambiguous, unresolvedNormalizedCallsWithIndexedCandidates: unresolved };
1984
+ const examples = rows.filter((row) => row.status === "ambiguous" || row.status === "unresolved").map(odataInvocationExample).slice(0, 5);
1985
+ return { severity: unresolved + ambiguous > 0 ? "warning" : "info", code: "strict_odata_invocation_resolution_quality", message: "OData invocation-path resolution quality aggregate", totalInvocationRemoteActions: rows.length, resolvedInvocationCalls: rows.filter((row) => row.status === "resolved").length, dynamicInvocationCalls: rows.filter((row) => row.status === "dynamic").length, ambiguousInvocationCalls: rows.filter((row) => row.status === "ambiguous").length, unresolvedInvocationCalls: rows.filter((row) => row.status === "unresolved").length, ambiguousNormalizedCalls: ambiguous, unresolvedNormalizedCallsWithIndexedCandidates: unresolved, examples };
1986
+ }
1987
+ function odataInvocationExample(row) {
1988
+ const evidence = parseObject(row.evidenceJson);
1989
+ const normalized = normalizeODataOperationInvocationPath(row.operationPathExpr);
1990
+ return {
1991
+ sourceFile: row.sourceFile,
1992
+ sourceLine: row.sourceLine,
1993
+ graphEdgeId: row.graphEdgeId,
1994
+ status: row.status,
1995
+ rawPath: row.operationPathExpr,
1996
+ normalizedOperationPath: normalized?.wasInvocation ? normalized.normalizedOperationPath : void 0,
1997
+ indexedOperationCandidateCount: evidence.indexedOperationCandidateCount,
1998
+ candidateScores: evidence.candidateScores,
1999
+ entityOperationPrecedence: evidence.entityOperationPrecedence,
2000
+ resolutionReasons: evidence.resolutionReasons
2001
+ };
1706
2002
  }
1707
2003
  function identityAliasBindingQuality(db) {
1708
2004
  const examples = db.prepare("SELECT c.source_file sourceFile,c.source_line sourceLine FROM outbound_calls c WHERE c.call_type='remote_action' AND c.service_binding_id IS NULL AND json_extract(c.evidence_json,'$.receiver') IS NOT NULL ORDER BY c.source_file,c.source_line LIMIT 5").all();
@@ -1729,16 +2025,6 @@ function asRecords(value) {
1729
2025
  return Array.isArray(value) ? value.filter((item) => Boolean(item && typeof item === "object" && !Array.isArray(item))) : [];
1730
2026
  }
1731
2027
 
1732
- // src/trace/selectors.ts
1733
- function parseVars(values) {
1734
- const out = {};
1735
- for (const value of values ?? []) {
1736
- const [key, ...rest] = value.split("=");
1737
- if (key && rest.length > 0) out[key] = rest.join("=");
1738
- }
1739
- return out;
1740
- }
1741
-
1742
2028
  // src/output/table-output.ts
1743
2029
  function location(evidence) {
1744
2030
  const file = evidence.file ?? evidence.sourceFile ?? evidence.handlerSourceFile ?? evidence.operationSourceFile ?? evidence.registrationSourceFile;
@@ -1755,8 +2041,8 @@ function renderTraceTable(result) {
1755
2041
  const lines = ["Step Type From To Evidence"];
1756
2042
  for (const e of result.edges) {
1757
2043
  lines.push(`${String(e.step).padEnd(5)} ${e.type.padEnd(20)} ${e.from.slice(0, 34).padEnd(35)} ${e.to.slice(0, 35).padEnd(36)} ${location(e.evidence)}`);
1758
- const hint = firstHint(e.evidence);
1759
- if (e.unresolvedReason && hint) lines.push(` try ${hint}`);
2044
+ if (e.unresolvedReason)
2045
+ lines.push(...hintLines(e.evidence).map((hint) => ` ${hint}`));
1760
2046
  }
1761
2047
  if (result.diagnostics.length > 0) lines.push("", "Diagnostics:", ...result.diagnostics.flatMap(diagnosticLines));
1762
2048
  return `${lines.join("\n")}
@@ -1764,14 +2050,20 @@ function renderTraceTable(result) {
1764
2050
  }
1765
2051
  function diagnosticLines(diagnostic) {
1766
2052
  const first = `${String(diagnostic.severity ?? "info")} ${String(diagnostic.code ?? "diagnostic")} ${String(diagnostic.message ?? "")}`;
1767
- const hint = firstHint(diagnostic);
1768
- return hint ? [first, ` try ${hint}`] : [first];
2053
+ return [first, ...hintLines(diagnostic).map((hint) => ` ${hint}`)];
1769
2054
  }
1770
- function firstHint(evidence) {
2055
+ function hintLines(evidence) {
1771
2056
  const suggestions = evidence.implementationHintSuggestions;
1772
- if (!Array.isArray(suggestions)) return void 0;
1773
- const first = suggestions.find((item) => Boolean(item) && typeof item === "object");
1774
- return typeof first?.cli === "string" ? first.cli : void 0;
2057
+ if (!Array.isArray(suggestions)) return [];
2058
+ const hints = suggestions.flatMap((item) => isRecord(item) && typeof item.cli === "string" ? [item.cli] : []);
2059
+ const unique = [...new Set(hints)];
2060
+ const shown = unique.slice(0, 3).map((hint) => `try ${hint}`);
2061
+ if (unique.length > shown.length)
2062
+ shown.push(`... ${unique.length - shown.length} more hint(s) available in JSON`);
2063
+ return shown;
2064
+ }
2065
+ function isRecord(value) {
2066
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
1775
2067
  }
1776
2068
 
1777
2069
  // src/output/json-output.ts
@@ -1843,11 +2135,11 @@ function cliHints(value) {
1843
2135
  function cliHintsFromSuggestions(value) {
1844
2136
  if (!Array.isArray(value)) return [];
1845
2137
  return value.flatMap((item) => {
1846
- if (!isRecord(item)) return [];
2138
+ if (!isRecord2(item)) return [];
1847
2139
  return typeof item.cli === "string" ? [item.cli] : [];
1848
2140
  });
1849
2141
  }
1850
- function isRecord(value) {
2142
+ function isRecord2(value) {
1851
2143
  return Boolean(value && typeof value === "object" && !Array.isArray(value));
1852
2144
  }
1853
2145
  function cappedHints(hints) {