@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
@@ -1,12 +1,23 @@
1
1
  import type { Db } from '../db/connection.js';
2
2
  import { listRepositories, reposByName } from '../db/repositories.js';
3
3
  import { errorMessage } from '../utils/diagnostics.js';
4
- import { prepareRepositoryIndex, publishPreparedRepositoryIndex, recordIndexFailure, type PreparedRepositoryIndex } from './repository-indexer.js';
4
+ import {
5
+ prepareRepositoryIndex,
6
+ publishOneRepository,
7
+ recordIndexFailure,
8
+ type PreparedRepositoryIndex,
9
+ } from './repository-indexer.js';
5
10
  import { materializeCdsExtensionOperations } from './cds-extension-resolver.js';
6
11
  import {
7
12
  createPackageInvalidationBatch,
8
13
  finalizePackageTargetInvalidations,
14
+ mergePackageInvalidationEffects,
15
+ type PackageInvalidationBatch,
9
16
  } from '../db/004-package-target-invalidation.js';
17
+ import {
18
+ isPreparedRepositorySnapshotError,
19
+ } from '../db/013-index-publication-failure.js';
20
+ import { binaryCompare } from '../parsers/004-fact-identity.js';
10
21
  // Ownerless rows predate PID coordination; this matches doctor's stale-run threshold without taking over a recent legacy writer.
11
22
  const LEGACY_OWNER_RECOVERY_MS = 60 * 60 * 1_000;
12
23
  type RunningIndexRow = Record<string, unknown>;
@@ -80,11 +91,21 @@ export function claimIndexRun(
80
91
  throw error;
81
92
  }
82
93
  }
94
+ export interface IndexWorkspaceSummary {
95
+ repoCount: number;
96
+ indexedCount: number;
97
+ skippedCount: number;
98
+ failedCount: number;
99
+ failedRepos: Array<{ name: string; code: string }>;
100
+ fileCount: number;
101
+ diagnosticCount: number;
102
+ }
103
+
83
104
  export async function indexWorkspace(
84
105
  db: Db,
85
106
  workspaceId: number,
86
107
  options: { repo?: string; force: boolean; injectDerivedMaterializationFailure?: boolean },
87
- ): Promise<{ repoCount: number; indexedCount: number; skippedCount: number; fileCount: number; diagnosticCount: number }> {
108
+ ): Promise<IndexWorkspaceSummary> {
88
109
  const repos = selectedRepositories(db, workspaceId, options.repo);
89
110
  const runId = claimIndexRun(db, workspaceId, repos.length);
90
111
  const state: PreparationState = {
@@ -92,8 +113,9 @@ export async function indexWorkspace(
92
113
  };
93
114
  try {
94
115
  await prepareRepositories(repos, options.force, state);
95
- publishPreparedRows(db, workspaceId, options, runId, state);
96
- return indexSummary(repos.length, state);
116
+ return publishPreparedWorkspaceRows(
117
+ db, workspaceId, runId, state.rows, options,
118
+ );
97
119
  } catch (error) {
98
120
  finishFailedRun(db, runId, state, error);
99
121
  if (state.activeRepoId && state.rows.length < repos.length)
@@ -111,6 +133,18 @@ interface PreparationState {
111
133
  activeRepoId?: number;
112
134
  }
113
135
 
136
+ interface PublicationState {
137
+ fileCount: number;
138
+ diagnosticCount: number;
139
+ skippedCount: number;
140
+ indexedCount: number;
141
+ publicationFailureCount: number;
142
+ failedRepoIds: Set<number>;
143
+ failedRepos: Array<{ name: string; code: string }>;
144
+ rows: readonly PreparedRepositoryIndex[];
145
+ activeRepoId?: number;
146
+ }
147
+
114
148
  function selectedRepositories(
115
149
  db: Db,
116
150
  workspaceId: number,
@@ -145,38 +179,118 @@ async function prepareRepositories(
145
179
  }
146
180
  }
147
181
 
148
- function publishPreparedRows(
182
+ export function publishPreparedWorkspaceRows(
149
183
  db: Db,
150
184
  workspaceId: number,
151
- options: { injectDerivedMaterializationFailure?: boolean },
152
185
  runId: number,
153
- state: PreparationState,
154
- ): void {
186
+ rows: readonly PreparedRepositoryIndex[],
187
+ options: { injectDerivedMaterializationFailure?: boolean } = {},
188
+ ): IndexWorkspaceSummary {
189
+ const state = publicationState(rows);
155
190
  db.transaction(() => {
156
- const invalidations = createPackageInvalidationBatch(
157
- state.rows.filter((row) => !row.skipped).map((row) => row.repo.id),
158
- );
191
+ const effects = createPackageInvalidationBatch([]);
192
+ const publishedRepoIds: number[] = [];
159
193
  for (const row of state.rows) {
160
194
  state.activeRepoId = row.repo.id;
161
- publishPreparedRepositoryIndex(db, row, invalidations);
195
+ if (row.skipped) continue;
196
+ const result = publishPreparedRow(db, row);
197
+ if (result.status === 'failed') {
198
+ recordPublicationFailure(db, state, row, result.error);
199
+ continue;
200
+ }
201
+ state.indexedCount += 1;
202
+ publishedRepoIds.push(row.repo.id);
203
+ mergePackageInvalidationEffects(effects, result.effects);
162
204
  }
163
205
  if (options.injectDerivedMaterializationFailure)
164
206
  throw new Error('Injected derived materialization failure');
165
- materializeCdsExtensionOperations(db, workspaceId);
207
+ materializeCdsExtensionOperations(
208
+ db, workspaceId, state.failedRepoIds,
209
+ );
210
+ const invalidations = createPackageInvalidationBatch(publishedRepoIds);
211
+ mergePackageInvalidationEffects(invalidations, effects);
166
212
  finalizePackageTargetInvalidations(db, invalidations);
167
- finishSuccessfulRun(db, runId, state);
213
+ finishCompletedRun(db, runId, state);
214
+ });
215
+ return indexSummary(rows.length, state);
216
+ }
217
+
218
+ function publicationState(
219
+ rows: readonly PreparedRepositoryIndex[],
220
+ ): PublicationState {
221
+ return {
222
+ rows,
223
+ fileCount: rows.reduce((total, row) => total + row.fileCount, 0),
224
+ diagnosticCount: rows.reduce(
225
+ (total, row) => total + row.diagnosticCount, 0,
226
+ ),
227
+ skippedCount: rows.filter((row) => row.skipped).length,
228
+ indexedCount: 0,
229
+ publicationFailureCount: 0,
230
+ failedRepoIds: new Set(),
231
+ failedRepos: [],
232
+ };
233
+ }
234
+
235
+ type PublicationResult =
236
+ | { status: 'published'; effects: PackageInvalidationBatch }
237
+ | { status: 'failed'; error: unknown };
238
+
239
+ function publishPreparedRow(
240
+ db: Db,
241
+ row: PreparedRepositoryIndex,
242
+ ): PublicationResult {
243
+ const effects = createPackageInvalidationBatch([row.repo.id]);
244
+ const outcome = publishOneRepository(db, row, effects);
245
+ if (!outcome.ok && !isPreparedRepositorySnapshotError(outcome.error))
246
+ throw outcome.error;
247
+ return outcome.ok
248
+ ? { status: 'published', effects }
249
+ : { status: 'failed', error: outcome.error };
250
+ }
251
+
252
+ function recordPublicationFailure(
253
+ db: Db,
254
+ state: PublicationState,
255
+ row: PreparedRepositoryIndex,
256
+ error: unknown,
257
+ ): void {
258
+ state.failedRepoIds.add(row.repo.id);
259
+ state.failedRepos.push({
260
+ name: row.repo.name,
261
+ code: isPreparedRepositorySnapshotError(error)
262
+ ? error.message : 'source_read_failed',
168
263
  });
264
+ state.publicationFailureCount += 1;
169
265
  }
170
266
 
171
- function finishSuccessfulRun(
267
+ function finishCompletedRun(
172
268
  db: Db,
173
269
  runId: number,
174
- state: PreparationState,
270
+ state: PublicationState,
175
271
  ): void {
176
- db.prepare(`UPDATE index_runs SET finished_at=?,status='success',
177
- file_count=?,diagnostic_count=? WHERE id=?`).run(
178
- new Date().toISOString(), state.fileCount, state.diagnosticCount, runId,
272
+ const status = completedRunStatus(
273
+ state.rows.length, state.publicationFailureCount,
179
274
  );
275
+ const error = status === 'success' ? null
276
+ : `${state.publicationFailureCount} repositories failed index publication.`;
277
+ db.prepare(`UPDATE index_runs SET finished_at=?,status=?,
278
+ file_count=?,diagnostic_count=?,error_message=? WHERE id=?`).run(
279
+ new Date().toISOString(), status, state.fileCount,
280
+ completedDiagnosticCount(state), error, runId,
281
+ );
282
+ }
283
+
284
+ function completedRunStatus(
285
+ repoCount: number,
286
+ failedCount: number,
287
+ ): 'success' | 'partial_failure' | 'failed' {
288
+ if (failedCount === 0) return 'success';
289
+ return failedCount === repoCount ? 'failed' : 'partial_failure';
290
+ }
291
+
292
+ function completedDiagnosticCount(state: PublicationState): number {
293
+ return state.diagnosticCount + state.publicationFailureCount;
180
294
  }
181
295
 
182
296
  function finishFailedRun(
@@ -194,19 +308,17 @@ function finishFailedRun(
194
308
 
195
309
  function indexSummary(
196
310
  repoCount: number,
197
- state: PreparationState,
198
- ): {
199
- repoCount: number;
200
- indexedCount: number;
201
- skippedCount: number;
202
- fileCount: number;
203
- diagnosticCount: number;
204
- } {
311
+ state: PublicationState,
312
+ ): IndexWorkspaceSummary {
205
313
  return {
206
314
  repoCount,
207
- indexedCount: repoCount - state.skippedCount,
315
+ indexedCount: state.indexedCount,
208
316
  skippedCount: state.skippedCount,
317
+ failedCount: state.publicationFailureCount,
318
+ failedRepos: [...state.failedRepos].sort((left, right) =>
319
+ binaryCompare(left.name, right.name)
320
+ || binaryCompare(left.code, right.code)),
209
321
  fileCount: state.fileCount,
210
- diagnosticCount: state.diagnosticCount,
322
+ diagnosticCount: completedDiagnosticCount(state),
211
323
  };
212
324
  }
@@ -1,4 +1,8 @@
1
1
  import type { Db } from '../db/connection.js';
2
+ import {
3
+ linkEventTemplate,
4
+ type LinkedEventTemplate,
5
+ } from './006-event-template-link.js';
2
6
 
3
7
  export interface SubscriptionHandlerLinkSummary {
4
8
  edgeCount: number;
@@ -20,6 +24,7 @@ interface SubscriptionRow {
20
24
  startOffset?: number | null;
21
25
  endOffset?: number | null;
22
26
  confidence: number;
27
+ unresolvedReason?: string | null;
23
28
  }
24
29
 
25
30
  interface HandlerCallRow {
@@ -58,7 +63,7 @@ function subscriptionRows(db: Db, workspaceId: number): SubscriptionRow[] {
58
63
  c.source_symbol_id sourceSymbolId,c.event_name_expr eventName,
59
64
  c.source_file sourceFile,c.source_line sourceLine,
60
65
  c.call_site_start_offset startOffset,c.call_site_end_offset endOffset,
61
- c.confidence
66
+ c.confidence,c.unresolved_reason unresolvedReason
62
67
  FROM outbound_calls c JOIN repositories r ON r.id=c.repo_id
63
68
  WHERE r.workspace_id=? AND c.call_type='async_subscribe'
64
69
  AND json_extract(c.evidence_json,'$.handlerReferenceStatus')
@@ -202,11 +207,16 @@ function missingAssociation(
202
207
  function evidenceFor(
203
208
  subscription: SubscriptionRow,
204
209
  association: HandlerAssociation,
210
+ event: LinkedEventTemplate,
205
211
  ): Record<string, unknown> {
206
212
  const call: Partial<HandlerCallRow> = association.call ?? {};
207
213
  const symbolCallReason = boundedSymbolCallReason(call.unresolvedReason);
208
214
  return {
209
215
  eventName: subscription.eventName,
216
+ ...(event.substitution.placeholders.length > 0 ? {
217
+ effectiveEventName: event.targetId,
218
+ eventTemplateResolution: event.substitution,
219
+ } : {}),
210
220
  associationBasis: 'exact_subscription_call_span',
211
221
  dispatchScope: 'workspace_event_name_only',
212
222
  subscribeCallId: subscription.id,
@@ -253,22 +263,27 @@ function insertAssociationEdge(
253
263
  generation: number,
254
264
  subscription: SubscriptionRow,
255
265
  association: HandlerAssociation,
266
+ event: LinkedEventTemplate,
256
267
  ): void {
268
+ const status = event.isDynamic ? 'unresolved' : association.status;
269
+ const reason = event.isDynamic
270
+ ? 'event_template_variables_missing'
271
+ : association.reasonCode ?? association.call?.unresolvedReason ?? null;
257
272
  db.prepare(`INSERT INTO graph_edges(
258
273
  workspace_id,edge_type,status,from_kind,from_id,to_kind,to_id,
259
274
  confidence,evidence_json,is_dynamic,unresolved_reason,generation
260
275
  ) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)`).run(
261
276
  workspaceId,
262
277
  'EVENT_SUBSCRIPTION_HANDLED_BY',
263
- association.status,
264
- 'event',
265
- subscription.eventName,
278
+ status,
279
+ event.targetKind,
280
+ event.targetId,
266
281
  association.toKind,
267
282
  association.toId,
268
283
  association.call?.confidence ?? subscription.confidence,
269
- JSON.stringify(evidenceFor(subscription, association)),
270
- 0,
271
- association.reasonCode ?? association.call?.unresolvedReason ?? null,
284
+ JSON.stringify(evidenceFor(subscription, association, event)),
285
+ event.isDynamic ? 1 : 0,
286
+ reason,
272
287
  generation,
273
288
  );
274
289
  }
@@ -277,6 +292,7 @@ export function linkEventSubscriptionHandlers(
277
292
  db: Db,
278
293
  workspaceId: number,
279
294
  generation: number,
295
+ variables: Record<string, string> = {},
280
296
  ): SubscriptionHandlerLinkSummary {
281
297
  const summary: SubscriptionHandlerLinkSummary = {
282
298
  edgeCount: 0,
@@ -286,16 +302,21 @@ export function linkEventSubscriptionHandlers(
286
302
  missingAssociationCount: 0,
287
303
  };
288
304
  for (const subscription of subscriptionRows(db, workspaceId)) {
305
+ const event = linkEventTemplate(
306
+ subscription.eventName, variables,
307
+ subscription.unresolvedReason ?? undefined,
308
+ );
289
309
  const association = associationFor(
290
310
  subscription, roleSiteRows(db, subscription),
291
311
  );
292
312
  insertAssociationEdge(
293
- db, workspaceId, generation, subscription, association,
313
+ db, workspaceId, generation, subscription, association, event,
294
314
  );
315
+ const status = event.isDynamic ? 'unresolved' : association.status;
295
316
  summary.edgeCount += 1;
296
- summary.resolvedCount += association.status === 'resolved' ? 1 : 0;
297
- summary.ambiguousCount += association.status === 'ambiguous' ? 1 : 0;
298
- summary.unresolvedCount += association.status === 'unresolved' ? 1 : 0;
317
+ summary.resolvedCount += status === 'resolved' ? 1 : 0;
318
+ summary.ambiguousCount += status === 'ambiguous' ? 1 : 0;
319
+ summary.unresolvedCount += status === 'unresolved' ? 1 : 0;
299
320
  summary.missingAssociationCount += association.missing ? 1 : 0;
300
321
  }
301
322
  return summary;
@@ -0,0 +1,72 @@
1
+ import type { Db } from '../db/connection.js';
2
+ import {
3
+ substituteVariables,
4
+ type RuntimeSubstitution,
5
+ } from './dynamic-edge-resolver.js';
6
+
7
+ export interface LinkedEventTemplate {
8
+ targetId: string;
9
+ targetKind: 'event' | 'event_candidate';
10
+ status: 'terminal' | 'dynamic';
11
+ isDynamic: boolean;
12
+ unresolvedReason?: string;
13
+ substitution: RuntimeSubstitution;
14
+ }
15
+
16
+ export function linkEventTemplate(
17
+ template: string,
18
+ variables: Record<string, string>,
19
+ parserReason?: string,
20
+ ): LinkedEventTemplate {
21
+ const substitution = substituteVariables(template, variables);
22
+ const missing = substitution.missing.length > 0;
23
+ const unsupportedDynamic = substitution.placeholders.length === 0
24
+ && parserReason !== undefined;
25
+ const dynamic = missing || unsupportedDynamic;
26
+ return {
27
+ targetId: dynamic
28
+ ? `Event: ${substitution.effective ?? template}`
29
+ : substitution.effective ?? template,
30
+ targetKind: dynamic ? 'event_candidate' : 'event',
31
+ status: dynamic ? 'dynamic' : 'terminal',
32
+ isDynamic: dynamic,
33
+ unresolvedReason: missing
34
+ ? `Dynamic target requires runtime variable overrides: ${
35
+ substitution.missing.join(', ')}`
36
+ : unsupportedDynamic ? parserReason : undefined,
37
+ substitution,
38
+ };
39
+ }
40
+
41
+ export function insertEventCallEdge(
42
+ db: Db,
43
+ workspaceId: number,
44
+ generation: number,
45
+ call: Record<string, unknown>,
46
+ variables: Record<string, string>,
47
+ evidence: Record<string, unknown>,
48
+ ): { status: string; callType: string } {
49
+ const callType = String(call.call_type);
50
+ const event = linkEventTemplate(
51
+ String(call.event_name_expr ?? ''), variables,
52
+ typeof call.unresolved_reason === 'string'
53
+ ? call.unresolved_reason : undefined,
54
+ );
55
+ const edgeType = event.isDynamic
56
+ ? 'DYNAMIC_EDGE_CANDIDATE'
57
+ : callType === 'async_emit'
58
+ ? 'HANDLER_EMITS_EVENT' : 'EVENT_CONSUMED_BY_HANDLER';
59
+ const eventEvidence = event.substitution.placeholders.length > 0
60
+ ? { ...evidence, eventTemplateResolution: event.substitution }
61
+ : evidence;
62
+ db.prepare(`INSERT INTO graph_edges(
63
+ workspace_id,edge_type,status,from_kind,from_id,to_kind,to_id,
64
+ confidence,evidence_json,is_dynamic,unresolved_reason,generation
65
+ ) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)`).run(
66
+ workspaceId, edgeType, event.status, 'call', String(call.id),
67
+ event.targetKind, event.targetId, Number(call.confidence ?? 0.2),
68
+ JSON.stringify(eventEvidence), event.isDynamic ? 1 : 0,
69
+ event.unresolvedReason ?? null, generation,
70
+ );
71
+ return { status: event.status, callType };
72
+ }