@saptools/service-flow 0.1.68 → 0.1.69

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.
Files changed (56) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +11 -5
  3. package/TECHNICAL-NOTE.md +9 -0
  4. package/dist/{chunk-AEM4JY22.js → chunk-3N3B5KHV.js} +6983 -5500
  5. package/dist/chunk-3N3B5KHV.js.map +1 -0
  6. package/dist/cli.js +317 -105
  7. package/dist/cli.js.map +1 -1
  8. package/dist/index.d.ts +7 -1
  9. package/dist/index.js +1 -1
  10. package/package.json +1 -1
  11. package/src/cli/001-index-summary.ts +22 -0
  12. package/src/cli.ts +151 -87
  13. package/src/db/000-call-fact-repository.ts +45 -19
  14. package/src/db/003-current-fact-semantics.ts +1 -0
  15. package/src/db/004-package-target-invalidation.ts +10 -0
  16. package/src/db/006-relative-symbol-resolution.ts +28 -7
  17. package/src/db/008-relative-fact-semantics.ts +6 -3
  18. package/src/db/009-binding-fact-semantics.ts +5 -0
  19. package/src/db/012-binding-reference-proof.ts +4 -0
  20. package/src/db/013-index-publication-failure.ts +91 -0
  21. package/src/db/014-binding-helper-provenance.ts +17 -0
  22. package/src/db/repositories.ts +22 -5
  23. package/src/indexer/cds-extension-resolver.ts +27 -3
  24. package/src/indexer/repository-indexer.ts +66 -5
  25. package/src/indexer/workspace-indexer.ts +141 -29
  26. package/src/linker/004-event-subscription-handler-linker.ts +32 -11
  27. package/src/linker/006-event-template-link.ts +72 -0
  28. package/src/linker/007-call-edge-insertion.ts +568 -0
  29. package/src/linker/cross-repo-linker.ts +2 -165
  30. package/src/parsers/000-direct-query-execution.ts +11 -0
  31. package/src/parsers/006-binding-identity.ts +6 -1
  32. package/src/parsers/014-service-binding-helper-flow.ts +70 -4
  33. package/src/parsers/021-binding-visibility.ts +17 -1
  34. package/src/parsers/022-outbound-expression-analysis.ts +700 -0
  35. package/src/parsers/023-outbound-call-classifier.ts +692 -0
  36. package/src/parsers/outbound-call-parser.ts +146 -509
  37. package/src/parsers/symbol-parser.ts +37 -5
  38. package/src/trace/007-implementation-start-diagnostic.ts +1 -0
  39. package/src/trace/011-event-subscriber-traversal.ts +100 -8
  40. package/src/trace/013-trace-root-scopes.ts +2 -3
  41. package/src/trace/014-compact-contract.ts +6 -0
  42. package/src/trace/015-trace-edge-recorder.ts +61 -4
  43. package/src/trace/016-compact-projector.ts +2 -3
  44. package/src/trace/019-trace-edge-semantics.ts +6 -10
  45. package/src/trace/020-compact-field-projection.ts +74 -1
  46. package/src/trace/021-compact-decision-normalization.ts +75 -9
  47. package/src/trace/024-compact-observation-decision.ts +7 -2
  48. package/src/trace/026-trace-start-scope.ts +1 -0
  49. package/src/trace/027-trace-scope-execution.ts +33 -33
  50. package/src/trace/030-event-runtime-resolution.ts +151 -0
  51. package/src/trace/031-local-call-expansion.ts +37 -0
  52. package/src/trace/implementation-hints.ts +9 -6
  53. package/src/trace/selectors.ts +1 -0
  54. package/src/types.ts +2 -1
  55. package/src/version.ts +1 -1
  56. package/dist/chunk-AEM4JY22.js.map +0 -1
package/dist/cli.js CHANGED
@@ -1,8 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  ANALYZER_VERSION,
4
+ PreparedRepositorySnapshotError,
4
5
  VERSION,
5
6
  analyzePackagePublicSurface,
7
+ binaryCompare,
6
8
  bindingSite,
7
9
  classifyODataPathIntent,
8
10
  classifyOutboundCallsInSource,
@@ -18,6 +20,7 @@ import {
18
20
  implementationHintSuggestions,
19
21
  insertCalls,
20
22
  insertSymbolCalls,
23
+ isPreparedRepositorySnapshotError,
21
24
  lexicalIdentifierDeclaration,
22
25
  lexicalIdentifierDeclarations,
23
26
  lexicalScopeChain,
@@ -37,6 +40,7 @@ import {
37
40
  parseServiceBindings,
38
41
  parseVars,
39
42
  projectBoundedInOrder,
43
+ recordPreparedSnapshotFailure,
40
44
  redactValue,
41
45
  selectCallOwner,
42
46
  selectVisibleBinding,
@@ -44,7 +48,7 @@ import {
44
48
  stableLocalValueReference,
45
49
  symbolImportReference,
46
50
  trace
47
- } from "./chunk-AEM4JY22.js";
51
+ } from "./chunk-3N3B5KHV.js";
48
52
 
49
53
  // src/cli.ts
50
54
  import { Command, Option } from "commander";
@@ -493,6 +497,15 @@ function insertBindings(db, repoId, rows) {
493
497
  r.helperChain ? JSON.stringify(r.helperChain) : null
494
498
  );
495
499
  }
500
+ function bindingSnapshotError(failureCode, fact) {
501
+ return new PreparedRepositorySnapshotError(failureCode, {
502
+ factKind: "service_binding",
503
+ sourceFile: fact.sourceFile,
504
+ sourceLine: fact.sourceLine,
505
+ callSiteStartOffset: fact.bindingSiteStartOffset,
506
+ callSiteEndOffset: fact.bindingSiteEndOffset
507
+ });
508
+ }
496
509
  function persistedOwnerCandidates(db, repoId, fact) {
497
510
  const rows = db.prepare(`SELECT id,kind,qualified_name qualifiedName,
498
511
  start_offset startOffset,end_offset endOffset FROM symbols
@@ -519,21 +532,21 @@ function bindingOwnerId(db, repoId, fact) {
519
532
  );
520
533
  if (fact.ownerResolution === "ownerless_file_scope") {
521
534
  if (selected.status !== "none")
522
- throw new Error("invalid_prepared_repository_snapshot:binding_owner_mismatch");
535
+ throw bindingSnapshotError("binding_owner_mismatch", fact);
523
536
  return null;
524
537
  }
525
538
  if (fact.ownerResolution !== "owned_exact" || selected.status !== "resolved" || selected.owner?.qualifiedName !== fact.sourceSymbolQualifiedName)
526
- throw new Error("invalid_prepared_repository_snapshot:binding_owner_mismatch");
539
+ throw bindingSnapshotError("binding_owner_mismatch", fact);
527
540
  const owner = selected.owner;
528
541
  if (!owner)
529
- throw new Error("invalid_prepared_repository_snapshot:binding_owner_mismatch");
542
+ throw bindingSnapshotError("binding_owner_mismatch", fact);
530
543
  return owner.id;
531
544
  }
532
545
  function assertUniquePreparedBindingSites(rows) {
533
546
  const seen = /* @__PURE__ */ new Set();
534
547
  for (const row of rows) {
535
548
  if (row.bindingSiteStartOffset === void 0 || row.bindingSiteEndOffset === void 0)
536
- throw new Error("invalid_prepared_repository_snapshot:binding_site_missing");
549
+ throw bindingSnapshotError("binding_site_missing", row);
537
550
  const key = [
538
551
  row.sourceFile,
539
552
  row.variableName,
@@ -541,7 +554,7 @@ function assertUniquePreparedBindingSites(rows) {
541
554
  row.bindingSiteEndOffset
542
555
  ].join("\0");
543
556
  if (seen.has(key))
544
- throw new Error("invalid_prepared_repository_snapshot:duplicate_service_binding_site");
557
+ throw bindingSnapshotError("duplicate_service_binding_site", row);
545
558
  seen.add(key);
546
559
  }
547
560
  }
@@ -1074,13 +1087,16 @@ function rejectedReference(call, chain, reason, status = "unresolved") {
1074
1087
  };
1075
1088
  }
1076
1089
  function resolvedReference(call, selected, chain, scopeIndex) {
1090
+ const helperReturn = selected.fact.helperChain?.some(
1091
+ (step) => step.bindingOrigin === "single_hop_helper_return"
1092
+ ) ?? false;
1077
1093
  return {
1078
1094
  status: "resolved_exact",
1079
1095
  variableName: call.serviceVariableName,
1080
1096
  bindingSourceFile: selected.fact.sourceFile,
1081
1097
  bindingSiteStartOffset: selected.site.startOffset,
1082
1098
  bindingSiteEndOffset: selected.site.endOffset,
1083
- resolutionStrategy: selected.site.flow === "assignment" ? "deterministic_reaching_assignment" : selected.site.aliasSource ? "lexical_alias_declaration" : "lexical_declaration",
1099
+ resolutionStrategy: selected.site.flow === "assignment" ? "deterministic_reaching_assignment" : selected.site.aliasSource ? "lexical_alias_declaration" : helperReturn ? "single_hop_helper_return" : "lexical_declaration",
1084
1100
  lexicalScopeChain: chain,
1085
1101
  bindingScopeIndex: scopeIndex,
1086
1102
  scopeChainTotal: chain.length,
@@ -1829,6 +1845,11 @@ function isFunctionLike(node) {
1829
1845
  function exported(node) {
1830
1846
  return Boolean(ts8.getCombinedModifierFlags(node) & ts8.ModifierFlags.Export);
1831
1847
  }
1848
+ function defaultExported(node) {
1849
+ return Boolean(
1850
+ ts8.getCombinedModifierFlags(node) & ts8.ModifierFlags.Default
1851
+ );
1852
+ }
1832
1853
  function isPublicClassMethod(node) {
1833
1854
  const flags = ts8.getCombinedModifierFlags(node);
1834
1855
  return (flags & ts8.ModifierFlags.Private) === 0 && (flags & ts8.ModifierFlags.Protected) === 0;
@@ -1922,6 +1943,7 @@ function symbolSourceEvidence(collection, node, options) {
1922
1943
  if (options.classContainerExported && ts8.isMethodDeclaration(node) && isPublicClassMethod(node)) return {
1923
1944
  source: "exported_class_instance_member",
1924
1945
  exportedClass: options.parentRoot,
1946
+ exportedClassExportKind: options.classContainerDefaultExported ? "default" : "named",
1925
1947
  memberKind: "class_method"
1926
1948
  };
1927
1949
  if (options.declaredExportName) return {
@@ -1945,6 +1967,16 @@ function exportedClassMember(collection, kind, parentName, parentRoot, node) {
1945
1967
  if (kind !== "method" || !parentName || !collection.exportedClasses.has(parentRoot) || !ts8.isMethodDeclaration(node)) return false;
1946
1968
  return Boolean(ts8.getCombinedModifierFlags(node) & ts8.ModifierFlags.Static) && isPublicClassMethod(node);
1947
1969
  }
1970
+ function classExportState(collection, parentName, parentRoot) {
1971
+ return {
1972
+ classContainerExported: Boolean(
1973
+ parentName && collection.exportedClasses.has(parentRoot)
1974
+ ),
1975
+ classContainerDefaultExported: Boolean(
1976
+ parentName && collection.defaultExportedClasses.has(parentRoot)
1977
+ )
1978
+ };
1979
+ }
1948
1980
  function symbolNames(collection, kind, localName, node, parentName, exportedName) {
1949
1981
  const parentRoot = parentName?.split(".")[0] ?? "";
1950
1982
  const declaredExportName = exportedName ?? collection.exportNames.get(
@@ -1961,15 +1993,13 @@ function symbolNames(collection, kind, localName, node, parentName, exportedName
1961
1993
  parentRoot,
1962
1994
  node
1963
1995
  );
1964
- const classContainerExported = Boolean(
1965
- parentName && collection.exportedClasses.has(parentRoot)
1966
- );
1996
+ const classState = classExportState(collection, parentName, parentRoot);
1967
1997
  return {
1968
1998
  parentRoot,
1969
1999
  declaredExportName,
1970
2000
  qualifiedName,
1971
2001
  objectExported,
1972
- classContainerExported,
2002
+ ...classState,
1973
2003
  classMemberExported,
1974
2004
  effectiveName: classMemberExported || objectExported ? qualifiedName : declaredExportName
1975
2005
  };
@@ -1988,6 +2018,7 @@ function addExecutableSymbol(collection, kind, localName, node, parentName, expo
1988
2018
  qualifiedName: names.qualifiedName,
1989
2019
  declaredExportName: names.declaredExportName,
1990
2020
  classContainerExported: names.classContainerExported,
2021
+ classContainerDefaultExported: names.classContainerDefaultExported,
1991
2022
  classMemberExported: names.classMemberExported,
1992
2023
  objectExported: names.objectExported,
1993
2024
  evidence
@@ -2135,6 +2166,8 @@ function collectClassDeclaration(collection, node) {
2135
2166
  collection.declaredClasses.add(node.name.text);
2136
2167
  if (exported(node) || collection.exportNames.has(node.name.text))
2137
2168
  collection.exportedClasses.add(node.name.text);
2169
+ if (defaultExported(node))
2170
+ collection.defaultExportedClasses.add(node.name.text);
2138
2171
  for (const member of node.members)
2139
2172
  visitDeclaredSymbol(collection, member, node.name.text);
2140
2173
  return true;
@@ -2231,6 +2264,7 @@ function createCollection(source, sourceFile2, classifiedCalls) {
2231
2264
  exportNames: exportDeclarations(source),
2232
2265
  objectExports: /* @__PURE__ */ new Set(),
2233
2266
  exportedClasses: /* @__PURE__ */ new Set(),
2267
+ defaultExportedClasses: /* @__PURE__ */ new Set(),
2234
2268
  declaredClasses: /* @__PURE__ */ new Set(),
2235
2269
  proxies: /* @__PURE__ */ new Map(),
2236
2270
  instances: /* @__PURE__ */ new Map()
@@ -2451,6 +2485,12 @@ function createPackageInvalidationBatch(publishingRepoIds) {
2451
2485
  affectedWorkspaceIds: /* @__PURE__ */ new Set()
2452
2486
  };
2453
2487
  }
2488
+ function mergePackageInvalidationEffects(target, source) {
2489
+ for (const repoId of source.affectedCallerRepoIds)
2490
+ target.affectedCallerRepoIds.add(repoId);
2491
+ for (const workspaceId of source.affectedWorkspaceIds)
2492
+ target.affectedWorkspaceIds.add(workspaceId);
2493
+ }
2454
2494
  function finalizePackageTargetInvalidations(db, batch) {
2455
2495
  const increment = db.prepare(`UPDATE repositories
2456
2496
  SET fact_generation=fact_generation+1 WHERE id=?`);
@@ -2540,10 +2580,43 @@ function publishPreparedRepositoryIndex(db, prepared, invalidations) {
2540
2580
  insertCalls(db, repoId, prepared.parsed.calls);
2541
2581
  db.prepare("UPDATE repositories SET last_indexed_at=?, index_status='indexed', error_count=0, fingerprint=?, fact_generation=COALESCE(fact_generation,0)+1, graph_stale_reason='facts_changed', graph_stale_at=?, fact_analyzer_version=? WHERE id=?").run(now, prepared.fingerprint, now, ANALYZER_VERSION, repoId);
2542
2582
  }
2583
+ function publishOneRepository(db, prepared, invalidations) {
2584
+ try {
2585
+ db.transaction(() => withPublicationSavepoint(
2586
+ db,
2587
+ prepared.repo.id,
2588
+ () => publishPreparedRepositoryIndex(db, prepared, invalidations)
2589
+ ));
2590
+ return { ok: true };
2591
+ } catch (error) {
2592
+ recordIndexFailure(db, prepared.repo.id, error);
2593
+ return { ok: false, error };
2594
+ }
2595
+ }
2596
+ function withPublicationSavepoint(db, repoId, publish) {
2597
+ const name = `service_flow_repository_${repoId}`;
2598
+ db.exec(`SAVEPOINT ${name}`);
2599
+ try {
2600
+ const result = publish();
2601
+ db.exec(`RELEASE SAVEPOINT ${name}`);
2602
+ return result;
2603
+ } catch (error) {
2604
+ db.exec(`ROLLBACK TO SAVEPOINT ${name}`);
2605
+ db.exec(`RELEASE SAVEPOINT ${name}`);
2606
+ throw error;
2607
+ }
2608
+ }
2543
2609
  function recordIndexFailure(db, repoId, error) {
2610
+ if (isPreparedRepositorySnapshotError(error)) {
2611
+ recordPreparedSnapshotFailure(db, repoId, error);
2612
+ return;
2613
+ }
2544
2614
  const message = errorMessage(error);
2545
2615
  db.prepare("UPDATE repositories SET index_status='failed', error_count=1 WHERE id=?").run(repoId);
2546
- db.prepare("DELETE FROM diagnostics WHERE repo_id=? AND code IN ('index_failed_snapshot_preserved','source_read_failed')").run(repoId);
2616
+ db.prepare(`DELETE FROM diagnostics WHERE repo_id=? AND (
2617
+ code IN ('index_failed_snapshot_preserved','source_read_failed')
2618
+ OR code GLOB 'invalid_prepared_repository_snapshot:*'
2619
+ )`).run(repoId);
2547
2620
  db.prepare("INSERT INTO diagnostics(repo_id,severity,code,message) VALUES(?,?,?,?)").run(repoId, "error", "source_read_failed", `Index failed before publication; previous facts and fingerprint were preserved. ${message}`);
2548
2621
  }
2549
2622
  async function parseAllSourceFacts(root, sources) {
@@ -2624,9 +2697,8 @@ function repositoryFingerprint(sources, facts, packageJsonText) {
2624
2697
  }
2625
2698
 
2626
2699
  // src/indexer/cds-extension-resolver.ts
2627
- function materializeCdsExtensionOperations(db, workspaceId) {
2628
- const extensions = db.prepare(`SELECT s.id,r.id repoId,s.service_name serviceName,s.qualified_name qualifiedName,s.source_file sourceFile,s.extension_module_specifier moduleSpecifier,s.extension_imported_symbol importedSymbol,s.extension_import_kind importKind
2629
- FROM cds_services s JOIN repositories r ON r.id=s.repo_id WHERE r.workspace_id=? AND s.is_extend=1`).all(workspaceId);
2700
+ function materializeCdsExtensionOperations(db, workspaceId, excludedRepoIds = /* @__PURE__ */ new Set()) {
2701
+ const extensions = extensionRows(db, workspaceId, excludedRepoIds);
2630
2702
  db.transaction(() => {
2631
2703
  const changedRepos = /* @__PURE__ */ new Set();
2632
2704
  for (const extension of extensions) {
@@ -2635,6 +2707,21 @@ function materializeCdsExtensionOperations(db, workspaceId) {
2635
2707
  for (const repoId of changedRepos) markRepositoryDerivedFactsChanged(db, repoId);
2636
2708
  });
2637
2709
  }
2710
+ function extensionRows(db, workspaceId, excludedRepoIds) {
2711
+ const excluded = [...excludedRepoIds].sort((left, right) => left - right);
2712
+ const exclusion = excluded.length > 0 ? ` AND r.id NOT IN (${excluded.map(() => "?").join(",")})` : "";
2713
+ return db.prepare(`SELECT s.id,r.id repoId,s.service_name serviceName,
2714
+ s.qualified_name qualifiedName,s.source_file sourceFile,
2715
+ s.extension_module_specifier moduleSpecifier,
2716
+ s.extension_imported_symbol importedSymbol,
2717
+ s.extension_import_kind importKind
2718
+ FROM cds_services s JOIN repositories r ON r.id=s.repo_id
2719
+ WHERE r.workspace_id=? AND s.is_extend=1${exclusion}
2720
+ ORDER BY r.id,s.id`).all(
2721
+ workspaceId,
2722
+ ...excluded
2723
+ );
2724
+ }
2638
2725
  function reconcileExtension(db, workspaceId, extension) {
2639
2726
  const bases = resolveBase(db, workspaceId, extension);
2640
2727
  const status = bases.length === 1 ? "resolved" : bases.length > 1 ? "ambiguous" : "unresolved";
@@ -2788,8 +2875,13 @@ async function indexWorkspace(db, workspaceId, options) {
2788
2875
  };
2789
2876
  try {
2790
2877
  await prepareRepositories(repos, options.force, state);
2791
- publishPreparedRows(db, workspaceId, options, runId, state);
2792
- return indexSummary(repos.length, state);
2878
+ return publishPreparedWorkspaceRows(
2879
+ db,
2880
+ workspaceId,
2881
+ runId,
2882
+ state.rows,
2883
+ options
2884
+ );
2793
2885
  } catch (error) {
2794
2886
  finishFailedRun(db, runId, state, error);
2795
2887
  if (state.activeRepoId && state.rows.length < repos.length)
@@ -2819,31 +2911,90 @@ async function prepareRepositories(repos, force, state) {
2819
2911
  state.skippedCount += result.skipped ? 1 : 0;
2820
2912
  }
2821
2913
  }
2822
- function publishPreparedRows(db, workspaceId, options, runId, state) {
2914
+ function publishPreparedWorkspaceRows(db, workspaceId, runId, rows, options = {}) {
2915
+ const state = publicationState(rows);
2823
2916
  db.transaction(() => {
2824
- const invalidations = createPackageInvalidationBatch(
2825
- state.rows.filter((row) => !row.skipped).map((row) => row.repo.id)
2826
- );
2917
+ const effects = createPackageInvalidationBatch([]);
2918
+ const publishedRepoIds = [];
2827
2919
  for (const row of state.rows) {
2828
2920
  state.activeRepoId = row.repo.id;
2829
- publishPreparedRepositoryIndex(db, row, invalidations);
2921
+ if (row.skipped) continue;
2922
+ const result = publishPreparedRow(db, row);
2923
+ if (result.status === "failed") {
2924
+ recordPublicationFailure(db, state, row, result.error);
2925
+ continue;
2926
+ }
2927
+ state.indexedCount += 1;
2928
+ publishedRepoIds.push(row.repo.id);
2929
+ mergePackageInvalidationEffects(effects, result.effects);
2830
2930
  }
2831
2931
  if (options.injectDerivedMaterializationFailure)
2832
2932
  throw new Error("Injected derived materialization failure");
2833
- materializeCdsExtensionOperations(db, workspaceId);
2933
+ materializeCdsExtensionOperations(
2934
+ db,
2935
+ workspaceId,
2936
+ state.failedRepoIds
2937
+ );
2938
+ const invalidations = createPackageInvalidationBatch(publishedRepoIds);
2939
+ mergePackageInvalidationEffects(invalidations, effects);
2834
2940
  finalizePackageTargetInvalidations(db, invalidations);
2835
- finishSuccessfulRun(db, runId, state);
2941
+ finishCompletedRun(db, runId, state);
2942
+ });
2943
+ return indexSummary(rows.length, state);
2944
+ }
2945
+ function publicationState(rows) {
2946
+ return {
2947
+ rows,
2948
+ fileCount: rows.reduce((total, row) => total + row.fileCount, 0),
2949
+ diagnosticCount: rows.reduce(
2950
+ (total, row) => total + row.diagnosticCount,
2951
+ 0
2952
+ ),
2953
+ skippedCount: rows.filter((row) => row.skipped).length,
2954
+ indexedCount: 0,
2955
+ publicationFailureCount: 0,
2956
+ failedRepoIds: /* @__PURE__ */ new Set(),
2957
+ failedRepos: []
2958
+ };
2959
+ }
2960
+ function publishPreparedRow(db, row) {
2961
+ const effects = createPackageInvalidationBatch([row.repo.id]);
2962
+ const outcome = publishOneRepository(db, row, effects);
2963
+ if (!outcome.ok && !isPreparedRepositorySnapshotError(outcome.error))
2964
+ throw outcome.error;
2965
+ return outcome.ok ? { status: "published", effects } : { status: "failed", error: outcome.error };
2966
+ }
2967
+ function recordPublicationFailure(db, state, row, error) {
2968
+ state.failedRepoIds.add(row.repo.id);
2969
+ state.failedRepos.push({
2970
+ name: row.repo.name,
2971
+ code: isPreparedRepositorySnapshotError(error) ? error.message : "source_read_failed"
2836
2972
  });
2973
+ state.publicationFailureCount += 1;
2837
2974
  }
2838
- function finishSuccessfulRun(db, runId, state) {
2839
- db.prepare(`UPDATE index_runs SET finished_at=?,status='success',
2840
- file_count=?,diagnostic_count=? WHERE id=?`).run(
2975
+ function finishCompletedRun(db, runId, state) {
2976
+ const status = completedRunStatus(
2977
+ state.rows.length,
2978
+ state.publicationFailureCount
2979
+ );
2980
+ const error = status === "success" ? null : `${state.publicationFailureCount} repositories failed index publication.`;
2981
+ db.prepare(`UPDATE index_runs SET finished_at=?,status=?,
2982
+ file_count=?,diagnostic_count=?,error_message=? WHERE id=?`).run(
2841
2983
  (/* @__PURE__ */ new Date()).toISOString(),
2984
+ status,
2842
2985
  state.fileCount,
2843
- state.diagnosticCount,
2986
+ completedDiagnosticCount(state),
2987
+ error,
2844
2988
  runId
2845
2989
  );
2846
2990
  }
2991
+ function completedRunStatus(repoCount, failedCount) {
2992
+ if (failedCount === 0) return "success";
2993
+ return failedCount === repoCount ? "failed" : "partial_failure";
2994
+ }
2995
+ function completedDiagnosticCount(state) {
2996
+ return state.diagnosticCount + state.publicationFailureCount;
2997
+ }
2847
2998
  function finishFailedRun(db, runId, state, error) {
2848
2999
  db.prepare(`UPDATE index_runs SET finished_at=?,status='failed',
2849
3000
  file_count=?,diagnostic_count=?,error_message=? WHERE id=?`).run(
@@ -2857,10 +3008,12 @@ function finishFailedRun(db, runId, state, error) {
2857
3008
  function indexSummary(repoCount, state) {
2858
3009
  return {
2859
3010
  repoCount,
2860
- indexedCount: repoCount - state.skippedCount,
3011
+ indexedCount: state.indexedCount,
2861
3012
  skippedCount: state.skippedCount,
3013
+ failedCount: state.publicationFailureCount,
3014
+ failedRepos: [...state.failedRepos].sort((left, right) => binaryCompare(left.name, right.name) || binaryCompare(left.code, right.code)),
2862
3015
  fileCount: state.fileCount,
2863
- diagnosticCount: state.diagnosticCount
3016
+ diagnosticCount: completedDiagnosticCount(state)
2864
3017
  };
2865
3018
  }
2866
3019
 
@@ -4008,6 +4161,16 @@ async function markCleanClaimFailed(dbPath, runId, error) {
4008
4161
  }
4009
4162
  }
4010
4163
 
4164
+ // src/cli/001-index-summary.ts
4165
+ function indexCommandOutcome(summary) {
4166
+ const failed = summary.failedCount > 0 ? `, failed ${summary.failedCount} (${summary.failedRepos.map(({ name, code }) => `${name}: ${code}`).join(", ")})` : "";
4167
+ return {
4168
+ stdout: `Indexed ${summary.indexedCount} repositories, skipped ${summary.skippedCount}${failed}, ${summary.fileCount} files, ${summary.diagnosticCount} diagnostics
4169
+ `,
4170
+ exitCode: summary.failedCount > 0 ? 1 : 0
4171
+ };
4172
+ }
4173
+
4011
4174
  // src/cli.ts
4012
4175
  var stdout = createStdoutWriter(process.stdout, fail);
4013
4176
  var TRACE_FORMATS = ["table", "json", "mermaid", "compact-json"];
@@ -4155,26 +4318,30 @@ function runGraphCommand(opts) {
4155
4318
  writeTraceOutput(db, start, options, opts.format);
4156
4319
  });
4157
4320
  }
4158
- function createProgram() {
4159
- const program = new Command();
4160
- program.name("service-flow").description(
4321
+ function configuredProgram() {
4322
+ return new Command().name("service-flow").description(
4161
4323
  "Trace SAP CAP service-to-service flows across multi-repository workspaces"
4162
4324
  ).version(VERSION);
4325
+ }
4326
+ function registerInitCommand(program) {
4163
4327
  program.command("init").argument("<workspace>").option("--db <path>").option("--ignore <pattern...>").action(
4164
4328
  (workspace, opts) => void init(workspace, opts).catch(fail)
4165
4329
  );
4330
+ }
4331
+ function registerIndexCommand(program) {
4166
4332
  program.command("index").option("--workspace <path>").option("--repo <name>").option("--force").action(
4167
4333
  (opts) => void withWorkspace(opts.workspace, async (db, workspaceId) => {
4168
4334
  const r = await indexWorkspace(db, workspaceId, {
4169
4335
  repo: opts.repo,
4170
4336
  force: Boolean(opts.force)
4171
4337
  });
4172
- writeStdout(
4173
- `Indexed ${r.indexedCount} repositories, skipped ${r.skippedCount}, ${r.fileCount} files, ${r.diagnosticCount} diagnostics
4174
- `
4175
- );
4338
+ const outcome = indexCommandOutcome(r);
4339
+ writeStdout(outcome.stdout);
4340
+ if (outcome.exitCode !== 0) process.exitCode = outcome.exitCode;
4176
4341
  }).catch(fail)
4177
4342
  );
4343
+ }
4344
+ function registerLinkCommand(program) {
4178
4345
  program.command("link").option("--workspace <path>").option("--force").action(
4179
4346
  (opts) => void withWorkspace(opts.workspace, (db, workspaceId) => {
4180
4347
  const r = linkWorkspace(db, workspaceId);
@@ -4186,90 +4353,121 @@ function createProgram() {
4186
4353
  );
4187
4354
  }).catch(fail)
4188
4355
  );
4356
+ }
4357
+ function registerTraceCommand(program) {
4189
4358
  program.command("trace").option("--workspace <path>").option("--repo <name>").option("--operation <name>").option("--service <path>").option("--path <operationPath>").option("--handler <name>").option("--depth <n>", "trace depth", "25").addOption(traceFormatOption()).option("--include-external").option("--include-db").option("--include-async").option("--implementation-repo <name>").option("--implementation-hint <scope>", "scoped implementation hint", collect, []).option("--var <key=value>", "dynamic variable", collect, []).option("--dynamic-mode <mode>", "strict|candidates|infer", "strict").option("--max-dynamic-candidates <n>", "maximum dynamic candidates to show", "5").action((opts) => void runTraceCommand(opts).catch(fail));
4359
+ }
4360
+ function listRepositoriesCommand(opts) {
4361
+ return withReadOnlyWorkspace(opts.workspace, (db, workspaceId) => writeStdout(
4362
+ renderJson(
4363
+ listRepositories(db, workspaceId).map((repo) => ({
4364
+ name: repo.name,
4365
+ kind: repo.kind,
4366
+ packageName: repo.package_name
4367
+ }))
4368
+ )
4369
+ ));
4370
+ }
4371
+ function listServicesCommand(opts) {
4372
+ return withReadOnlyWorkspace(opts.workspace, (db, workspaceId) => {
4373
+ const selection = opts.repo ? selectRepository(db, opts.repo, workspaceId) : {};
4374
+ if (selection.diagnostic) {
4375
+ writeStdout(renderJson([selection.diagnostic]));
4376
+ return;
4377
+ }
4378
+ const repo = selection.repo;
4379
+ const rows = db.prepare(
4380
+ "SELECT r.name repo,s.service_path servicePath,s.qualified_name qualifiedName FROM cds_services s JOIN repositories r ON r.id=s.repo_id WHERE r.workspace_id=? AND (? IS NULL OR s.repo_id=?) ORDER BY r.name,s.service_path"
4381
+ ).all(workspaceId, repo?.id, repo?.id);
4382
+ writeStdout(renderJson(rows));
4383
+ });
4384
+ }
4385
+ function listOperationsCommand(opts) {
4386
+ return withReadOnlyWorkspace(opts.workspace, (db, workspaceId) => {
4387
+ const selection = opts.repo ? selectRepository(db, opts.repo, workspaceId) : {};
4388
+ if (selection.diagnostic) {
4389
+ writeStdout(renderJson([selection.diagnostic]));
4390
+ return;
4391
+ }
4392
+ const repo = selection.repo;
4393
+ const rows = db.prepare(
4394
+ "SELECT r.name repo,s.service_path servicePath,o.operation_name operation,o.operation_path path FROM cds_operations o JOIN cds_services s ON s.id=o.service_id JOIN repositories r ON r.id=s.repo_id WHERE r.workspace_id=? AND (? IS NULL OR s.repo_id=?) AND (? IS NULL OR s.service_path=?)"
4395
+ ).all(
4396
+ workspaceId,
4397
+ repo?.id,
4398
+ repo?.id,
4399
+ opts.service,
4400
+ opts.service
4401
+ );
4402
+ writeStdout(renderJson(rows));
4403
+ });
4404
+ }
4405
+ function listCallsCommand(opts) {
4406
+ return withReadOnlyWorkspace(opts.workspace, (db, workspaceId) => {
4407
+ const selection = opts.repo ? selectRepository(db, opts.repo, workspaceId) : {};
4408
+ if (selection.diagnostic) {
4409
+ writeStdout(renderJson([selection.diagnostic]));
4410
+ return;
4411
+ }
4412
+ const repo = selection.repo;
4413
+ const rows = db.prepare(
4414
+ "SELECT r.name repo,c.call_type type,c.operation_path_expr path,c.source_file file,c.source_line line FROM outbound_calls c JOIN repositories r ON r.id=c.repo_id WHERE r.workspace_id=? AND (? IS NULL OR c.repo_id=?) AND (? IS NULL OR c.operation_path_expr=? OR c.operation_path_expr=? OR c.payload_summary LIKE ?)"
4415
+ ).all(
4416
+ workspaceId,
4417
+ repo?.id,
4418
+ repo?.id,
4419
+ opts.operation,
4420
+ opts.operation,
4421
+ opts.operation ? `/${opts.operation}` : void 0,
4422
+ opts.operation ? `%${opts.operation}%` : void 0
4423
+ );
4424
+ writeStdout(renderJson(rows));
4425
+ });
4426
+ }
4427
+ function registerListCommands(program) {
4190
4428
  const list = program.command("list");
4191
4429
  list.command("repos").option("--workspace <path>").action(
4192
- (opts) => void withReadOnlyWorkspace(
4193
- opts.workspace,
4194
- (db, workspaceId) => writeStdout(
4195
- renderJson(
4196
- listRepositories(db, workspaceId).map((r) => ({
4197
- name: r.name,
4198
- kind: r.kind,
4199
- packageName: r.package_name
4200
- }))
4201
- )
4202
- )
4203
- ).catch(fail)
4430
+ (opts) => void listRepositoriesCommand(opts).catch(fail)
4204
4431
  );
4205
4432
  list.command("services").option("--workspace <path>").option("--repo <name>").action(
4206
- (opts) => void withReadOnlyWorkspace(opts.workspace, (db, workspaceId) => {
4207
- const selection = opts.repo ? selectRepository(db, opts.repo, workspaceId) : {};
4208
- if (selection.diagnostic) {
4209
- writeStdout(renderJson([selection.diagnostic]));
4210
- return;
4211
- }
4212
- const repo = selection.repo;
4213
- const rows = db.prepare(
4214
- "SELECT r.name repo,s.service_path servicePath,s.qualified_name qualifiedName FROM cds_services s JOIN repositories r ON r.id=s.repo_id WHERE r.workspace_id=? AND (? IS NULL OR s.repo_id=?) ORDER BY r.name,s.service_path"
4215
- ).all(workspaceId, repo?.id, repo?.id);
4216
- writeStdout(renderJson(rows));
4217
- }).catch(fail)
4433
+ (opts) => void listServicesCommand(opts).catch(fail)
4218
4434
  );
4219
4435
  list.command("operations").option("--workspace <path>").option("--repo <name>").option("--service <path>").action(
4220
- (opts) => void withReadOnlyWorkspace(opts.workspace, (db, workspaceId) => {
4221
- const selection = opts.repo ? selectRepository(db, opts.repo, workspaceId) : {};
4222
- if (selection.diagnostic) {
4223
- writeStdout(renderJson([selection.diagnostic]));
4224
- return;
4225
- }
4226
- const repo = selection.repo;
4227
- const rows = db.prepare(
4228
- "SELECT r.name repo,s.service_path servicePath,o.operation_name operation,o.operation_path path FROM cds_operations o JOIN cds_services s ON s.id=o.service_id JOIN repositories r ON r.id=s.repo_id WHERE r.workspace_id=? AND (? IS NULL OR s.repo_id=?) AND (? IS NULL OR s.service_path=?)"
4229
- ).all(workspaceId, repo?.id, repo?.id, opts.service, opts.service);
4230
- writeStdout(renderJson(rows));
4231
- }).catch(fail)
4436
+ (opts) => void listOperationsCommand(opts).catch(fail)
4232
4437
  );
4233
4438
  list.command("calls").option("--workspace <path>").option("--repo <name>").option("--operation <name>").action(
4234
- (opts) => void withReadOnlyWorkspace(opts.workspace, (db, workspaceId) => {
4235
- const selection = opts.repo ? selectRepository(db, opts.repo, workspaceId) : {};
4236
- if (selection.diagnostic) {
4237
- writeStdout(renderJson([selection.diagnostic]));
4238
- return;
4239
- }
4240
- const repo = selection.repo;
4241
- const rows = db.prepare(
4242
- "SELECT r.name repo,c.call_type type,c.operation_path_expr path,c.source_file file,c.source_line line FROM outbound_calls c JOIN repositories r ON r.id=c.repo_id WHERE r.workspace_id=? AND (? IS NULL OR c.repo_id=?) AND (? IS NULL OR c.operation_path_expr=? OR c.operation_path_expr=? OR c.payload_summary LIKE ?)"
4243
- ).all(
4244
- workspaceId,
4245
- repo?.id,
4246
- repo?.id,
4247
- opts.operation,
4248
- opts.operation,
4249
- opts.operation ? `/${opts.operation}` : void 0,
4250
- opts.operation ? `%${opts.operation}%` : void 0
4251
- );
4252
- writeStdout(renderJson(rows));
4253
- }).catch(fail)
4439
+ (opts) => void listCallsCommand(opts).catch(fail)
4254
4440
  );
4441
+ }
4442
+ function registerGraphCommand(program) {
4255
4443
  program.command("graph").option("--workspace <path>").option("--repo <name>").option("--operation <name>").option("--service <path>").option("--path <operationPath>").addOption(graphFormatOption()).option("--implementation-repo <name>").option("--implementation-hint <scope>", "scoped implementation hint", collect, []).option("--var <key=value>", "dynamic variable", collect, []).option("--dynamic-mode <mode>", "strict|candidates|infer", "strict").option("--max-dynamic-candidates <n>", "maximum dynamic candidates to show", "5").action((opts) => void runGraphCommand(opts).catch(fail));
4444
+ }
4445
+ function inspectRepositoryCommand(name, opts) {
4446
+ return withReadOnlyWorkspace(opts.workspace, (db, workspaceId) => {
4447
+ const selection = selectRepository(db, name, workspaceId);
4448
+ writeStdout(renderJson(
4449
+ selection.repo ?? selection.diagnostic ?? { error: "repo not found" }
4450
+ ));
4451
+ });
4452
+ }
4453
+ function inspectOperationCommand(selector, opts) {
4454
+ return withReadOnlyWorkspace(opts.workspace, (db, workspaceId) => {
4455
+ const rows = db.prepare(
4456
+ "SELECT o.* FROM cds_operations o JOIN cds_services s ON s.id=o.service_id JOIN repositories r ON r.id=s.repo_id WHERE r.workspace_id=? AND (o.operation_name=? OR o.operation_path=?)"
4457
+ ).all(workspaceId, selector, selector);
4458
+ writeStdout(renderJson(rows));
4459
+ });
4460
+ }
4461
+ function registerInspectCommands(program) {
4256
4462
  const inspect = program.command("inspect");
4257
4463
  inspect.command("repo").argument("<name>").option("--workspace <path>").action(
4258
- (name, opts) => void withReadOnlyWorkspace(opts.workspace, (db, workspaceId) => {
4259
- const selection = selectRepository(db, name, workspaceId);
4260
- writeStdout(renderJson(
4261
- selection.repo ?? selection.diagnostic ?? { error: "repo not found" }
4262
- ));
4263
- }).catch(fail)
4464
+ (name, opts) => void inspectRepositoryCommand(name, opts).catch(fail)
4264
4465
  );
4265
4466
  inspect.command("operation").argument("<selector>").option("--workspace <path>").action(
4266
- (selector, opts) => void withReadOnlyWorkspace(opts.workspace, (db, workspaceId) => {
4267
- const rows = db.prepare(
4268
- "SELECT o.* FROM cds_operations o JOIN cds_services s ON s.id=o.service_id JOIN repositories r ON r.id=s.repo_id WHERE r.workspace_id=? AND (o.operation_name=? OR o.operation_path=?)"
4269
- ).all(workspaceId, selector, selector);
4270
- writeStdout(renderJson(rows));
4271
- }).catch(fail)
4467
+ (selector, opts) => void inspectOperationCommand(selector, opts).catch(fail)
4272
4468
  );
4469
+ }
4470
+ function registerDoctorCommand(program) {
4273
4471
  program.command("doctor").option("--workspace <path>").option("--strict").option("--detail").option("--format <format>", "json|table").action(
4274
4472
  (opts) => void withReadOnlyWorkspace(opts.workspace, (db, workspaceId) => {
4275
4473
  const allDiagnostics = doctorDiagnostics(db, Boolean(opts.strict), {
@@ -4279,6 +4477,8 @@ function createProgram() {
4279
4477
  writeStdout(renderDoctorDiagnostics(allDiagnostics, opts.format));
4280
4478
  }).catch(fail)
4281
4479
  );
4480
+ }
4481
+ function registerCleanCommand(program) {
4282
4482
  program.command("clean").option("--workspace <path>").option("--db-only").action(
4283
4483
  (opts) => void (async () => {
4284
4484
  const config = await loadWorkspaceConfig(opts.workspace);
@@ -4286,6 +4486,18 @@ function createProgram() {
4286
4486
  writeStdout("Cleaned service-flow state\n");
4287
4487
  })().catch(fail)
4288
4488
  );
4489
+ }
4490
+ function createProgram() {
4491
+ const program = configuredProgram();
4492
+ registerInitCommand(program);
4493
+ registerIndexCommand(program);
4494
+ registerLinkCommand(program);
4495
+ registerTraceCommand(program);
4496
+ registerListCommands(program);
4497
+ registerGraphCommand(program);
4498
+ registerInspectCommands(program);
4499
+ registerDoctorCommand(program);
4500
+ registerCleanCommand(program);
4289
4501
  return program;
4290
4502
  }
4291
4503
  function collect(value, previous) {