@saptools/service-flow 0.1.72 → 0.1.73

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 (41) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +19 -6
  3. package/TECHNICAL-NOTE.md +11 -2
  4. package/dist/{chunk-Z6D433R5.js → chunk-32WOTGTS.js} +6714 -6161
  5. package/dist/chunk-32WOTGTS.js.map +1 -0
  6. package/dist/cli.js +189 -39
  7. package/dist/cli.js.map +1 -1
  8. package/dist/index.d.ts +2 -1
  9. package/dist/index.js +1 -1
  10. package/package.json +1 -1
  11. package/src/cli/doctor-event-quality.ts +90 -15
  12. package/src/cli.ts +35 -13
  13. package/src/config/workspace-config.ts +15 -0
  14. package/src/db/current-fact-semantics.ts +2 -26
  15. package/src/db/event-fact-semantics.ts +260 -61
  16. package/src/db/event-site-semantics.ts +62 -0
  17. package/src/db/fact-lifecycle.ts +70 -12
  18. package/src/db/migrations.ts +4 -1
  19. package/src/db/package-target-invalidation.ts +4 -3
  20. package/src/db/schema.ts +1 -1
  21. package/src/indexer/repository-indexer.ts +50 -6
  22. package/src/indexer/workspace-indexer.ts +13 -3
  23. package/src/linker/cross-repo-linker.ts +4 -2
  24. package/src/linker/event-shape-candidate-linker.ts +63 -20
  25. package/src/linker/event-subscription-handler-linker.ts +121 -30
  26. package/src/linker/package-event-constant-resolver.ts +22 -18
  27. package/src/output/repository-inspection.ts +11 -0
  28. package/src/parsers/environment-declarations.ts +104 -27
  29. package/src/parsers/event-call-analysis.ts +163 -35
  30. package/src/parsers/event-environment-reference.ts +18 -7
  31. package/src/parsers/event-receiver-analysis.ts +17 -27
  32. package/src/parsers/outbound-call-classifier.ts +1 -1
  33. package/src/parsers/stable-local-value.ts +12 -1
  34. package/src/parsers/string-constant-lookups.ts +78 -28
  35. package/src/trace/edge-target.ts +65 -0
  36. package/src/trace/event-subscriber-traversal.ts +71 -1
  37. package/src/trace/evidence.ts +0 -28
  38. package/src/trace/trace-scope-execution.ts +1 -1
  39. package/src/types.ts +3 -1
  40. package/src/version.ts +1 -1
  41. package/dist/chunk-Z6D433R5.js.map +0 -1
package/dist/cli.js CHANGED
@@ -1,6 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  ANALYZER_VERSION,
4
+ DEFAULT_EVENT_ENVIRONMENT_KEYS,
5
+ EVENT_ENVIRONMENT_KEY_CAP,
4
6
  PreparedRepositorySnapshotError,
5
7
  VERSION,
6
8
  analyzePackagePublicSurface,
@@ -34,6 +36,7 @@ import {
34
36
  loadPackageJsonSnapshot,
35
37
  loadRepositorySourceContext,
36
38
  migrate,
39
+ normalizeEventEnvironmentKeys,
37
40
  normalizeODataOperationInvocationPath,
38
41
  normalizePath,
39
42
  parseCdsFile,
@@ -55,8 +58,9 @@ import {
55
58
  sha256Text,
56
59
  stableLocalValueReference,
57
60
  symbolImportReference,
58
- trace
59
- } from "./chunk-Z6D433R5.js";
61
+ trace,
62
+ validEventEnvironmentKey
63
+ } from "./chunk-32WOTGTS.js";
60
64
 
61
65
  // src/cli.ts
62
66
  import { Command, Option } from "commander";
@@ -81,10 +85,18 @@ var DEFAULT_DB_FILE = "service-flow.db";
81
85
  import fs from "fs/promises";
82
86
  import path from "path";
83
87
  import { z } from "zod";
88
+ var environmentKeys = z.array(
89
+ z.string().refine(validEventEnvironmentKey)
90
+ ).min(1).max(EVENT_ENVIRONMENT_KEY_CAP).transform(
91
+ normalizeEventEnvironmentKeys
92
+ );
84
93
  var schema = z.object({
85
94
  rootPath: z.string(),
86
95
  dbPath: z.string(),
87
96
  ignore: z.array(z.string()),
97
+ eventEnvironmentKeys: environmentKeys.default(
98
+ [...DEFAULT_EVENT_ENVIRONMENT_KEYS]
99
+ ),
88
100
  createdAt: z.string(),
89
101
  updatedAt: z.string()
90
102
  });
@@ -121,6 +133,7 @@ function createWorkspaceConfig(rootPath, dbPath, ignore = [...DEFAULT_IGNORES])
121
133
  rootPath: root,
122
134
  dbPath: path.resolve(dbPath ?? defaultDbPath(root)),
123
135
  ignore,
136
+ eventEnvironmentKeys: [...DEFAULT_EVENT_ENVIRONMENT_KEYS],
124
137
  createdAt: now,
125
138
  updatedAt: now
126
139
  };
@@ -2594,8 +2607,12 @@ function resetPackageEventCalls(db, workspaceId, targetRepoId, names, batch) {
2594
2607
  const source = call.evidence.eventNameConstantSourceExpression;
2595
2608
  if (typeof source !== "string" || source.length === 0)
2596
2609
  throw new Error("invalid_current_package_event_constant_evidence");
2597
- const reason = call.evidence.receiverClassification === "unproven" ? call.unresolvedReason : "event_name_constant_resolution_pending";
2598
- update.run(source, reason, pendingEventEvidence(call.evidence), call.id);
2610
+ update.run(
2611
+ source,
2612
+ "event_name_constant_resolution_pending",
2613
+ pendingEventEvidence(call.evidence),
2614
+ call.id
2615
+ );
2599
2616
  batch.affectedCallerRepoIds.add(call.repoId);
2600
2617
  matched = true;
2601
2618
  }
@@ -2696,7 +2713,7 @@ function invalidateEventSurfaceFacts(db, repoId, calls, nextEnvironmentJson) {
2696
2713
  }
2697
2714
 
2698
2715
  // src/indexer/repository-indexer.ts
2699
- async function prepareRepositoryIndex(repo, force, instrumentation) {
2716
+ async function prepareRepositoryIndex(repo, force, instrumentation, eventEnvironmentKeys) {
2700
2717
  const sourceFiles = await findSourceFiles(repo.absolute_path);
2701
2718
  const packageSnapshot = await loadPackageJsonSnapshot(repo.absolute_path, {
2702
2719
  strict: true,
@@ -2708,13 +2725,23 @@ async function prepareRepositoryIndex(repo, force, instrumentation) {
2708
2725
  sourceFiles,
2709
2726
  instrumentation
2710
2727
  );
2728
+ const environmentDeclarations = collectEnvironmentDeclarations(
2729
+ sources,
2730
+ eventEnvironmentKeys
2731
+ );
2711
2732
  const fingerprint = repositoryFingerprint(
2712
2733
  sources,
2713
2734
  packageFacts,
2714
- packageSnapshot.rawText
2735
+ packageSnapshot.rawText,
2736
+ environmentDeclarations
2715
2737
  );
2716
2738
  if (!force && repo.fingerprint === fingerprint) return { repo, fileCount: 0, diagnosticCount: 0, skipped: true };
2717
- const parsedFacts = await parseAllSourceFacts(repo.absolute_path, sources);
2739
+ const parsedFacts = await parseAllSourceFacts(
2740
+ repo.absolute_path,
2741
+ sources,
2742
+ environmentDeclarations,
2743
+ eventEnvironmentKeys
2744
+ );
2718
2745
  const packageSurface = analyzeRepositoryPackageSurface(
2719
2746
  packageFacts,
2720
2747
  packageSnapshot.manifest,
@@ -2731,7 +2758,7 @@ async function prepareRepositoryIndex(repo, force, instrumentation) {
2731
2758
  kind: await classifyRepository(repo.absolute_path, packageFacts),
2732
2759
  parsed,
2733
2760
  packagePublicSurface: packageSurface.surface,
2734
- environmentDeclarations: collectEnvironmentDeclarations(sources),
2761
+ environmentDeclarations,
2735
2762
  fileCount: sourceFiles.length,
2736
2763
  diagnosticCount: parsed.handlers.filter((handler) => handler.hasHandlerDecorator && (handler.methods.length === 0 || handler.methods.some((method) => !handlerMethodIsExecutable(method)))).length,
2737
2764
  skipped: false
@@ -2822,11 +2849,16 @@ function recordIndexFailure(db, repoId, error) {
2822
2849
  )`).run(repoId);
2823
2850
  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}`);
2824
2851
  }
2825
- async function parseAllSourceFacts(root, sources) {
2852
+ async function parseAllSourceFacts(root, sources, environmentDeclarations, eventEnvironmentKeys) {
2826
2853
  const facts = { services: [], handlers: [], registrations: [], bindings: [], calls: [], symbols: [], symbolCalls: [], generatedConstants: [], fileRecords: [] };
2827
2854
  for (const snapshot of sources.entries()) {
2828
2855
  const file = snapshot.filePath;
2829
- facts.fileRecords.push({ relativePath: normalizePath(file), extension: path5.extname(file), sha256: sha256Text(snapshot.text), sizeBytes: snapshot.sizeBytes });
2856
+ facts.fileRecords.push({
2857
+ relativePath: normalizePath(file),
2858
+ extension: path5.extname(file),
2859
+ sha256: sourceFactHash(snapshot, environmentDeclarations),
2860
+ sizeBytes: snapshot.sizeBytes
2861
+ });
2830
2862
  if (file.endsWith(".cds")) facts.services.push(...await parseCdsFile(root, file, sources));
2831
2863
  if (/\.[jt]s$/.test(file)) {
2832
2864
  const source = snapshot.sourceFile();
@@ -2837,7 +2869,12 @@ async function parseAllSourceFacts(root, sources) {
2837
2869
  source,
2838
2870
  file
2839
2871
  ),
2840
- eventEnvironmentReferenceResolver: createEventEnvironmentReferenceResolver(sources, source, file)
2872
+ eventEnvironmentReferenceResolver: createEventEnvironmentReferenceResolver(
2873
+ sources,
2874
+ source,
2875
+ file,
2876
+ eventEnvironmentKeys
2877
+ )
2841
2878
  });
2842
2879
  facts.handlers.push(...await parseDecorators(root, file, sources));
2843
2880
  facts.registrations.push(...await parseHandlerRegistrations(root, file, sources));
@@ -2893,7 +2930,7 @@ function isDefaultTestFile(relativeFile) {
2893
2930
  if (parts.some((part) => ["test", "tests", "__tests__"].includes(part))) return true;
2894
2931
  return /\.(test|spec)\.[jt]s$/.test(parts.at(-1) ?? "");
2895
2932
  }
2896
- function repositoryFingerprint(sources, facts, packageJsonText) {
2933
+ function repositoryFingerprint(sources, facts, packageJsonText, environmentDeclarations) {
2897
2934
  const normalizedFacts = {
2898
2935
  analyzerVersion: ANALYZER_VERSION,
2899
2936
  packageName: facts.packageName,
@@ -2902,13 +2939,33 @@ function repositoryFingerprint(sources, facts, packageJsonText) {
2902
2939
  cdsRequires: [...facts.cdsRequires].sort((a, b) => a.alias.localeCompare(b.alias)),
2903
2940
  scripts: Object.fromEntries(Object.entries(facts.scripts).sort()),
2904
2941
  includeTests: false,
2942
+ eventEnvironmentKeys: environmentDeclarations.allowedKeys,
2905
2943
  packageJsonHash: sha256Text(packageJsonText)
2906
2944
  };
2907
2945
  const entries = [`facts:${JSON.stringify(normalizedFacts)}`];
2908
2946
  for (const snapshot of sources.entries())
2909
- entries.push(`${snapshot.filePath}:${sha256Text(snapshot.text)}`);
2947
+ entries.push(
2948
+ `${snapshot.filePath}:${sourceFactHash(snapshot, environmentDeclarations)}`
2949
+ );
2910
2950
  return sha256Text(entries.join("\n"));
2911
2951
  }
2952
+ function sourceFactHash(snapshot, environmentDeclarations) {
2953
+ if (!isEnvironmentFactInput(snapshot.filePath))
2954
+ return sha256Text(snapshot.text);
2955
+ const declarations = environmentDeclarations.declarations.filter(
2956
+ (item) => item.sourceFile === snapshot.filePath
2957
+ );
2958
+ return sha256Text(JSON.stringify({
2959
+ allowedKeys: environmentDeclarations.allowedKeys,
2960
+ declarations
2961
+ }));
2962
+ }
2963
+ function isEnvironmentFactInput(filePath) {
2964
+ const name = filePath.split("/").at(-1);
2965
+ return ["nodemon.json", ".env", "mta.yaml", "manifest.yml"].includes(
2966
+ name ?? ""
2967
+ );
2968
+ }
2912
2969
 
2913
2970
  // src/indexer/cds-extension-resolver.ts
2914
2971
  function materializeCdsExtensionOperations(db, workspaceId, excludedRepoIds = /* @__PURE__ */ new Set()) {
@@ -3088,7 +3145,12 @@ async function indexWorkspace(db, workspaceId, options) {
3088
3145
  rows: []
3089
3146
  };
3090
3147
  try {
3091
- await prepareRepositories(repos, options.force, state);
3148
+ await prepareRepositories(
3149
+ repos,
3150
+ options.force,
3151
+ state,
3152
+ options.eventEnvironmentKeys
3153
+ );
3092
3154
  return publishPreparedWorkspaceRows(
3093
3155
  db,
3094
3156
  workspaceId,
@@ -3115,10 +3177,15 @@ function selectedRepositories(db, workspaceId, repoName) {
3115
3177
  );
3116
3178
  return repos;
3117
3179
  }
3118
- async function prepareRepositories(repos, force, state) {
3180
+ async function prepareRepositories(repos, force, state, eventEnvironmentKeys) {
3119
3181
  for (const repo of repos) {
3120
3182
  state.activeRepoId = repo.id;
3121
- const result = await prepareRepositoryIndex(repo, force);
3183
+ const result = await prepareRepositoryIndex(
3184
+ repo,
3185
+ force,
3186
+ void 0,
3187
+ eventEnvironmentKeys
3188
+ );
3122
3189
  state.rows.push(result);
3123
3190
  state.fileCount += result.fileCount;
3124
3191
  state.diagnosticCount += result.diagnosticCount;
@@ -3470,6 +3537,16 @@ var eventNameReason = `COALESCE(
3470
3537
  OR c.unresolved_reason GLOB 'event_name_*'
3471
3538
  THEN c.unresolved_reason END
3472
3539
  )`;
3540
+ var receiverReason = `CASE
3541
+ WHEN json_extract(c.evidence_json,
3542
+ '$.receiverClassification')='name_fallback'
3543
+ THEN COALESCE(json_extract(c.evidence_json,
3544
+ '$.receiverFallbackRefusedReason'),'name_fallback')
3545
+ WHEN json_extract(c.evidence_json,
3546
+ '$.receiverClassification')='unproven'
3547
+ THEN COALESCE(json_extract(c.evidence_json,
3548
+ '$.receiverUnresolvedReason'),'unproven')
3549
+ ELSE 'missing' END`;
3473
3550
  function workspacePredicate(alias) {
3474
3551
  return `(? IS NULL OR ${alias}.workspace_id=?)`;
3475
3552
  }
@@ -3507,6 +3584,14 @@ function eventNameResolutionQuality(db, workspaceId) {
3507
3584
  workspaceId
3508
3585
  );
3509
3586
  const unresolved = count2(aggregate?.unresolved);
3587
+ const reasonCount = count2(db.prepare(`SELECT COUNT(DISTINCT reason) count
3588
+ FROM (SELECT ${eventNameReason} reason
3589
+ FROM outbound_calls c JOIN repositories r ON r.id=c.repo_id
3590
+ WHERE c.call_type='async_emit' AND ${workspacePredicate("r")}
3591
+ AND ${eventNameReason} IS NOT NULL)`).get(
3592
+ workspaceId,
3593
+ workspaceId
3594
+ )?.count);
3510
3595
  return {
3511
3596
  severity: unresolved > 0 ? "warning" : "info",
3512
3597
  code: "strict_event_name_resolution_quality",
@@ -3514,6 +3599,9 @@ function eventNameResolutionQuality(db, workspaceId) {
3514
3599
  publicationTotal: count2(aggregate?.total),
3515
3600
  unresolvedPublicationCount: unresolved,
3516
3601
  reasonBuckets: reasons,
3602
+ reasonBucketCount: reasonCount,
3603
+ shownReasonBucketCount: reasons.length,
3604
+ omittedReasonBucketCount: Math.max(0, reasonCount - reasons.length),
3517
3605
  examples: unresolvedEventNameExamples(db, workspaceId),
3518
3606
  exampleCount: unresolved
3519
3607
  };
@@ -3639,8 +3727,12 @@ function unmatchedSubscriptionRows(db, workspaceId) {
3639
3727
  );
3640
3728
  }
3641
3729
  function unmatchedSubscriptionQuality(db, workspaceId) {
3642
- const row = db.prepare(`SELECT COUNT(*) siteCount,
3730
+ const row = db.prepare(`SELECT
3731
+ COUNT(DISTINCT json_extract(e.evidence_json,'$.subscribeCallId'))
3732
+ siteCount,
3643
3733
  SUM(CASE
3734
+ WHEN json_extract(e.evidence_json,'$.materializedLoopEventName')
3735
+ IS NOT NULL THEN 1
3644
3736
  WHEN json_extract(c.evidence_json,
3645
3737
  '$.subscriptionLoopRegistrationStatus')='enumerated'
3646
3738
  THEN CAST(json_extract(c.evidence_json,
@@ -3650,7 +3742,7 @@ function unmatchedSubscriptionQuality(db, workspaceId) {
3650
3742
  ELSE 1 END) count,
3651
3743
  SUM(CASE WHEN json_extract(c.evidence_json,
3652
3744
  '$.subscriptionLoopRegistrationStatus')='unresolved'
3653
- THEN 1 ELSE 0 END) unknownMultiplicitySiteCount
3745
+ THEN 1 ELSE 0 END) unknownMultiplicityEdgeCount
3654
3746
  FROM graph_edges e LEFT JOIN outbound_calls c
3655
3747
  ON c.id=CAST(json_extract(e.evidence_json,'$.subscribeCallId') AS INTEGER)
3656
3748
  WHERE e.edge_type='EVENT_SUBSCRIPTION_HANDLED_BY'
@@ -3663,19 +3755,37 @@ function unmatchedSubscriptionQuality(db, workspaceId) {
3663
3755
  AND publication.to_id=e.from_id)`).get(workspaceId, workspaceId);
3664
3756
  const total = count2(row?.count);
3665
3757
  const siteCount = count2(row?.siteCount);
3758
+ const unknownSites = unknownMultiplicitySiteCount(db, workspaceId);
3666
3759
  return {
3667
3760
  severity: siteCount > 0 ? "warning" : "info",
3668
3761
  code: "strict_event_subscription_without_publication_quality",
3669
3762
  message: "Event subscriptions without a matching publication",
3670
3763
  unmatchedSubscriptionCount: total,
3671
3764
  unmatchedSubscriptionSiteCount: siteCount,
3672
- unknownMultiplicitySiteCount: count2(row?.unknownMultiplicitySiteCount),
3765
+ unknownMultiplicitySiteCount: unknownSites,
3673
3766
  examples: unmatchedSubscriptionRows(db, workspaceId),
3674
3767
  exampleCount: siteCount
3675
3768
  };
3676
3769
  }
3677
- function receiverProofQuality(db, workspaceId) {
3678
- const row = db.prepare(`SELECT COUNT(*) eventTotal,
3770
+ function unknownMultiplicitySiteCount(db, workspaceId) {
3771
+ const row = db.prepare(`SELECT COUNT(DISTINCT
3772
+ json_extract(e.evidence_json,'$.subscribeCallId')) count
3773
+ FROM graph_edges e LEFT JOIN outbound_calls c
3774
+ ON c.id=CAST(json_extract(e.evidence_json,'$.subscribeCallId') AS INTEGER)
3775
+ WHERE e.edge_type='EVENT_SUBSCRIPTION_HANDLED_BY'
3776
+ AND ${workspacePredicate("e")}
3777
+ AND json_extract(c.evidence_json,
3778
+ '$.subscriptionLoopRegistrationStatus')='unresolved'
3779
+ AND NOT EXISTS (SELECT 1 FROM graph_edges publication
3780
+ WHERE publication.workspace_id=e.workspace_id
3781
+ AND publication.generation=e.generation
3782
+ AND publication.edge_type='HANDLER_EMITS_EVENT'
3783
+ AND publication.to_kind='event'
3784
+ AND publication.to_id=e.from_id)`).get(workspaceId, workspaceId);
3785
+ return count2(row?.count);
3786
+ }
3787
+ function receiverProofAggregate(db, workspaceId) {
3788
+ return db.prepare(`SELECT COUNT(*) eventTotal,
3679
3789
  SUM(CASE WHEN json_extract(c.evidence_json,
3680
3790
  '$.receiverClassification')='cap_evidence' THEN 1 ELSE 0 END) proven,
3681
3791
  SUM(CASE WHEN json_extract(c.evidence_json,
@@ -3686,13 +3796,9 @@ function receiverProofQuality(db, workspaceId) {
3686
3796
  FROM outbound_calls c JOIN repositories r ON r.id=c.repo_id
3687
3797
  WHERE c.call_type IN ('async_emit','async_subscribe')
3688
3798
  AND ${workspacePredicate("r")}`).get(workspaceId, workspaceId);
3689
- const buckets = db.prepare(`SELECT CASE
3690
- WHEN json_extract(c.evidence_json,
3691
- '$.receiverClassification')='name_fallback' THEN 'name_fallback'
3692
- WHEN json_extract(c.evidence_json,
3693
- '$.receiverClassification')='unproven'
3694
- THEN COALESCE(c.unresolved_reason,'unproven')
3695
- ELSE 'missing' END reason,COUNT(*) count
3799
+ }
3800
+ function receiverReasonBuckets(db, workspaceId) {
3801
+ return db.prepare(`SELECT ${receiverReason} reason,COUNT(*) count
3696
3802
  FROM outbound_calls c JOIN repositories r ON r.id=c.repo_id
3697
3803
  WHERE c.call_type IN ('async_emit','async_subscribe')
3698
3804
  AND ${workspacePredicate("r")}
@@ -3702,6 +3808,21 @@ function receiverProofQuality(db, workspaceId) {
3702
3808
  workspaceId,
3703
3809
  workspaceId
3704
3810
  );
3811
+ }
3812
+ function receiverReasonBucketCount(db, workspaceId) {
3813
+ const row = db.prepare(`SELECT COUNT(DISTINCT reason) count
3814
+ FROM (SELECT ${receiverReason} reason
3815
+ FROM outbound_calls c JOIN repositories r ON r.id=c.repo_id
3816
+ WHERE c.call_type IN ('async_emit','async_subscribe')
3817
+ AND ${workspacePredicate("r")}
3818
+ AND json_extract(c.evidence_json,'$.receiverClassification')
3819
+ <>'cap_evidence')`).get(workspaceId, workspaceId);
3820
+ return count2(row?.count);
3821
+ }
3822
+ function receiverProofQuality(db, workspaceId) {
3823
+ const row = receiverProofAggregate(db, workspaceId);
3824
+ const buckets = receiverReasonBuckets(db, workspaceId);
3825
+ const bucketCount = receiverReasonBucketCount(db, workspaceId);
3705
3826
  const questionable = count2(row?.nameFallback) + count2(row?.unproven);
3706
3827
  return {
3707
3828
  severity: questionable > 0 ? "warning" : "info",
@@ -3713,6 +3834,9 @@ function receiverProofQuality(db, workspaceId) {
3713
3834
  unproven: count2(row?.unproven),
3714
3835
  questionable,
3715
3836
  reasonBuckets: buckets,
3837
+ reasonBucketCount: bucketCount,
3838
+ shownReasonBucketCount: buckets.length,
3839
+ omittedReasonBucketCount: Math.max(0, bucketCount - buckets.length),
3716
3840
  examples: receiverProofExamples(db, workspaceId),
3717
3841
  exampleCount: questionable
3718
3842
  };
@@ -3721,8 +3845,11 @@ function receiverProofExamples(db, workspaceId) {
3721
3845
  return db.prepare(`SELECT r.name repositoryName,c.call_type callType,
3722
3846
  c.source_file sourceFile,c.source_line sourceLine,
3723
3847
  json_extract(c.evidence_json,'$.receiverClassification')
3724
- receiverClassification,
3725
- c.unresolved_reason reason
3848
+ receiverClassification,
3849
+ COALESCE(json_extract(c.evidence_json,'$.receiverUnresolvedReason'),
3850
+ json_extract(c.evidence_json,'$.receiverFallbackRefusedReason')) reason,
3851
+ json_array_length(json_extract(c.evidence_json,
3852
+ '$.consideredBindingSites')) consideredBindingSiteCount
3726
3853
  FROM outbound_calls c JOIN repositories r ON r.id=c.repo_id
3727
3854
  WHERE c.call_type IN ('async_emit','async_subscribe')
3728
3855
  AND ${workspacePredicate("r")}
@@ -4645,6 +4772,15 @@ function renderCompactJson(value) {
4645
4772
  `;
4646
4773
  }
4647
4774
 
4775
+ // src/output/repository-inspection.ts
4776
+ function projectRepositoryInspection(repository) {
4777
+ return Object.fromEntries(
4778
+ Object.entries(repository).filter(
4779
+ ([key]) => key !== "environment_declarations_json"
4780
+ )
4781
+ );
4782
+ }
4783
+
4648
4784
  // src/cli/clean.ts
4649
4785
  import fs6 from "fs/promises";
4650
4786
  import path6 from "path";
@@ -4758,7 +4894,7 @@ async function withWorkspace(workspace, fn) {
4758
4894
  try {
4759
4895
  const row = getWorkspace(db, config.rootPath);
4760
4896
  const workspaceId = row?.id ?? upsertWorkspace(db, config.rootPath, config.dbPath);
4761
- return await fn(db, workspaceId, config.rootPath);
4897
+ return await fn(db, workspaceId, config.rootPath, config);
4762
4898
  } finally {
4763
4899
  db.close();
4764
4900
  }
@@ -4873,10 +5009,11 @@ function registerInitCommand(program) {
4873
5009
  }
4874
5010
  function registerIndexCommand(program) {
4875
5011
  program.command("index").option("--workspace <path>").option("--repo <name>").option("--force").action(
4876
- (opts) => void withWorkspace(opts.workspace, async (db, workspaceId) => {
5012
+ (opts) => void withWorkspace(opts.workspace, async (db, workspaceId, _root, config) => {
4877
5013
  const r = await indexWorkspace(db, workspaceId, {
4878
5014
  repo: opts.repo,
4879
- force: Boolean(opts.force)
5015
+ force: Boolean(opts.force),
5016
+ eventEnvironmentKeys: config.eventEnvironmentKeys
4880
5017
  });
4881
5018
  const outcome = indexCommandOutcome(r);
4882
5019
  writeStdout(outcome.stdout);
@@ -4891,7 +5028,7 @@ function registerLinkCommand(program) {
4891
5028
  const upgradeWarnings = linkUpgradeWarnings(db, workspaceId);
4892
5029
  writeStdout(
4893
5030
  `${upgradeWarnings.length ? `Warnings: ${upgradeWarnings.map((item) => String(item.code)).join(", ")}. Run service-flow doctor --strict for remediation.
4894
- ` : ""}Linked ${r.edgeCount} edges: ${r.remoteResolvedCount} remote operation calls resolved, ${r.localResolvedCount} local operation calls resolved, ${r.unresolvedCount} unresolved operation calls, ${r.ambiguousCount} ambiguous operation calls, ${r.dynamicCount} dynamic operation calls, ${r.terminalCount} terminal call edges, ${r.dependencyResolvedCount} dependency resolved, ${r.dependencyAmbiguousCount} dependency ambiguous, ${r.implementationResolvedCount} implementation resolved, ${r.implementationAmbiguousCount} implementation ambiguous, ${r.implementationUnresolvedCount} implementation unresolved, ${r.subscriptionHandlerResolvedCount} subscription handlers resolved, ${r.subscriptionHandlerAmbiguousCount} subscription handlers ambiguous, ${r.subscriptionHandlerUnresolvedCount} subscription handlers unresolved, ${r.subscriptionHandlerMissingAssociationCount} subscription handler associations missing, ${r.eventShapeCandidateCount} event shape candidates
5031
+ ` : ""}Linked ${r.edgeCount} edges: ${r.remoteResolvedCount} remote operation calls resolved, ${r.localResolvedCount} local operation calls resolved, ${r.unresolvedCount} unresolved operation calls, ${r.ambiguousCount} ambiguous operation calls, ${r.dynamicCount} dynamic operation calls, ${r.terminalCount} terminal call edges, ${r.dependencyResolvedCount} dependency resolved, ${r.dependencyAmbiguousCount} dependency ambiguous, ${r.implementationResolvedCount} implementation resolved, ${r.implementationAmbiguousCount} implementation ambiguous, ${r.implementationUnresolvedCount} implementation unresolved, ${r.subscriptionHandlerResolvedCount} subscription handlers resolved, ${r.subscriptionHandlerAmbiguousCount} subscription handlers ambiguous, ${r.subscriptionHandlerUnresolvedCount} subscription handlers unresolved, ${r.subscriptionHandlerMissingAssociationCount} subscription handler associations missing, ${r.eventShapeCandidateCount} event shape candidates, ${r.eventShapeCandidateOmittedCount} event shape candidates omitted by the link cap
4895
5032
  `
4896
5033
  );
4897
5034
  }).catch(fail)
@@ -4901,18 +5038,20 @@ function registerTraceCommand(program) {
4901
5038
  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));
4902
5039
  }
4903
5040
  function listRepositoriesCommand(opts) {
4904
- return withReadOnlyWorkspace(opts.workspace, (db, workspaceId) => writeStdout(
4905
- renderJson(
5041
+ return withReadOnlyWorkspace(opts.workspace, (db, workspaceId) => {
5042
+ if (writeLifecycleBlock(db, workspaceId)) return;
5043
+ writeStdout(renderJson(
4906
5044
  listRepositories(db, workspaceId).map((repo) => ({
4907
5045
  name: repo.name,
4908
5046
  kind: repo.kind,
4909
5047
  packageName: repo.package_name
4910
5048
  }))
4911
- )
4912
- ));
5049
+ ));
5050
+ });
4913
5051
  }
4914
5052
  function listServicesCommand(opts) {
4915
5053
  return withReadOnlyWorkspace(opts.workspace, (db, workspaceId) => {
5054
+ if (writeLifecycleBlock(db, workspaceId)) return;
4916
5055
  const selection = opts.repo ? selectRepository(db, opts.repo, workspaceId) : {};
4917
5056
  if (selection.diagnostic) {
4918
5057
  writeStdout(renderJson([selection.diagnostic]));
@@ -4927,6 +5066,7 @@ function listServicesCommand(opts) {
4927
5066
  }
4928
5067
  function listOperationsCommand(opts) {
4929
5068
  return withReadOnlyWorkspace(opts.workspace, (db, workspaceId) => {
5069
+ if (writeLifecycleBlock(db, workspaceId)) return;
4930
5070
  const selection = opts.repo ? selectRepository(db, opts.repo, workspaceId) : {};
4931
5071
  if (selection.diagnostic) {
4932
5072
  writeStdout(renderJson([selection.diagnostic]));
@@ -4947,6 +5087,7 @@ function listOperationsCommand(opts) {
4947
5087
  }
4948
5088
  function listCallsCommand(opts) {
4949
5089
  return withReadOnlyWorkspace(opts.workspace, (db, workspaceId) => {
5090
+ if (writeLifecycleBlock(db, workspaceId)) return;
4950
5091
  const selection = opts.repo ? selectRepository(db, opts.repo, workspaceId) : {};
4951
5092
  if (selection.diagnostic) {
4952
5093
  writeStdout(renderJson([selection.diagnostic]));
@@ -4987,20 +5128,29 @@ function registerGraphCommand(program) {
4987
5128
  }
4988
5129
  function inspectRepositoryCommand(name, opts) {
4989
5130
  return withReadOnlyWorkspace(opts.workspace, (db, workspaceId) => {
5131
+ if (writeLifecycleBlock(db, workspaceId)) return;
4990
5132
  const selection = selectRepository(db, name, workspaceId);
4991
5133
  writeStdout(renderJson(
4992
- selection.repo ?? selection.diagnostic ?? { error: "repo not found" }
5134
+ selection.repo ? projectRepositoryInspection(selection.repo) : selection.diagnostic ?? { error: "repo not found" }
4993
5135
  ));
4994
5136
  });
4995
5137
  }
4996
5138
  function inspectOperationCommand(selector, opts) {
4997
5139
  return withReadOnlyWorkspace(opts.workspace, (db, workspaceId) => {
5140
+ if (writeLifecycleBlock(db, workspaceId)) return;
4998
5141
  const rows = db.prepare(
4999
5142
  "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=?)"
5000
5143
  ).all(workspaceId, selector, selector);
5001
5144
  writeStdout(renderJson(rows));
5002
5145
  });
5003
5146
  }
5147
+ function writeLifecycleBlock(db, workspaceId) {
5148
+ const diagnostic = factLifecycleDiagnostic(db, workspaceId);
5149
+ if (!diagnostic) return false;
5150
+ writeStdout(renderJson([diagnostic]));
5151
+ process.exitCode = 1;
5152
+ return true;
5153
+ }
5004
5154
  function registerInspectCommands(program) {
5005
5155
  const inspect = program.command("inspect");
5006
5156
  inspect.command("repo").argument("<name>").option("--workspace <path>").action(