@saptools/service-flow 0.1.67 → 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 (101) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +27 -9
  3. package/TECHNICAL-NOTE.md +36 -0
  4. package/dist/chunk-3N3B5KHV.js +19596 -0
  5. package/dist/chunk-3N3B5KHV.js.map +1 -0
  6. package/dist/cli.js +2645 -521
  7. package/dist/cli.js.map +1 -1
  8. package/dist/index.d.ts +67 -2
  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/003-doctor-package-resolution.ts +68 -0
  13. package/src/cli/doctor.ts +25 -14
  14. package/src/cli.ts +151 -87
  15. package/src/db/000-call-fact-repository.ts +499 -340
  16. package/src/db/001-fact-lifecycle.ts +60 -30
  17. package/src/db/002-fact-json-inventory.ts +169 -0
  18. package/src/db/003-current-fact-semantics.ts +699 -0
  19. package/src/db/004-package-target-invalidation.ts +183 -0
  20. package/src/db/005-schema-structure.ts +201 -0
  21. package/src/db/006-relative-symbol-resolution.ts +464 -0
  22. package/src/db/007-package-fact-semantics.ts +573 -0
  23. package/src/db/008-relative-fact-semantics.ts +210 -0
  24. package/src/db/009-binding-fact-semantics.ts +352 -0
  25. package/src/db/010-package-symbol-surface-semantics.ts +320 -0
  26. package/src/db/011-symbol-call-semantics.ts +144 -0
  27. package/src/db/012-binding-reference-proof.ts +268 -0
  28. package/src/db/013-index-publication-failure.ts +91 -0
  29. package/src/db/014-binding-helper-provenance.ts +17 -0
  30. package/src/db/migrations.ts +16 -3
  31. package/src/db/repositories.ts +130 -6
  32. package/src/db/schema.ts +4 -2
  33. package/src/index.ts +12 -0
  34. package/src/indexer/cds-extension-resolver.ts +27 -3
  35. package/src/indexer/repository-indexer.ts +135 -13
  36. package/src/indexer/workspace-indexer.ts +237 -34
  37. package/src/linker/003-package-import-symbol-resolver.ts +363 -131
  38. package/src/linker/004-event-subscription-handler-linker.ts +34 -11
  39. package/src/linker/005-odata-path-structure.ts +371 -0
  40. package/src/linker/006-event-template-link.ts +72 -0
  41. package/src/linker/007-call-edge-insertion.ts +568 -0
  42. package/src/linker/cross-repo-linker.ts +4 -166
  43. package/src/linker/odata-path-normalizer.ts +273 -180
  44. package/src/linker/service-resolver.ts +197 -77
  45. package/src/parsers/000-direct-query-execution.ts +11 -0
  46. package/src/parsers/002-symbol-import-bindings.ts +516 -0
  47. package/src/parsers/003-package-public-surface.ts +661 -0
  48. package/src/parsers/004-fact-identity.ts +108 -0
  49. package/src/parsers/005-event-subscription-facts.ts +281 -0
  50. package/src/parsers/006-binding-identity.ts +348 -0
  51. package/src/parsers/007-source-fact-reconciliation.ts +105 -0
  52. package/src/parsers/008-package-surface-publication.ts +82 -0
  53. package/src/parsers/009-symbol-call-facts.ts +528 -0
  54. package/src/parsers/010-package-public-surface-analysis.ts +352 -0
  55. package/src/parsers/011-binding-lexical-scope.ts +583 -0
  56. package/src/parsers/012-package-fact-contract.ts +306 -0
  57. package/src/parsers/013-executable-body-eligibility.ts +35 -0
  58. package/src/parsers/014-service-binding-helper-flow.ts +306 -0
  59. package/src/parsers/015-service-binding-collector.ts +693 -0
  60. package/src/parsers/016-local-symbol-reference.ts +261 -0
  61. package/src/parsers/017-symbol-derived-contexts.ts +268 -0
  62. package/src/parsers/018-package-commonjs-syntax.ts +142 -0
  63. package/src/parsers/019-binding-assignment-targets.ts +76 -0
  64. package/src/parsers/020-stable-local-value.ts +217 -0
  65. package/src/parsers/021-binding-visibility.ts +168 -0
  66. package/src/parsers/022-outbound-expression-analysis.ts +700 -0
  67. package/src/parsers/023-outbound-call-classifier.ts +692 -0
  68. package/src/parsers/operation-path-analysis.ts +6 -1
  69. package/src/parsers/outbound-call-parser.ts +162 -512
  70. package/src/parsers/package-json-parser.ts +45 -3
  71. package/src/parsers/service-binding-parser-helpers.ts +86 -15
  72. package/src/parsers/service-binding-parser.ts +147 -597
  73. package/src/parsers/symbol-parser.ts +513 -352
  74. package/src/trace/002-trace-diagnostics.ts +36 -1
  75. package/src/trace/007-implementation-start-diagnostic.ts +1 -0
  76. package/src/trace/011-event-subscriber-traversal.ts +100 -8
  77. package/src/trace/013-trace-root-scopes.ts +2 -3
  78. package/src/trace/014-compact-contract.ts +6 -0
  79. package/src/trace/015-trace-edge-recorder.ts +61 -4
  80. package/src/trace/016-compact-projector.ts +15 -17
  81. package/src/trace/019-trace-edge-semantics.ts +6 -10
  82. package/src/trace/020-compact-field-projection.ts +122 -38
  83. package/src/trace/021-compact-decision-normalization.ts +171 -0
  84. package/src/trace/022-trace-fact-preflight.ts +21 -0
  85. package/src/trace/023-nested-event-scopes.ts +23 -0
  86. package/src/trace/024-compact-observation-decision.ts +81 -0
  87. package/src/trace/025-trace-implementation-scope.ts +123 -0
  88. package/src/trace/026-trace-start-scope.ts +336 -0
  89. package/src/trace/027-trace-scope-execution.ts +566 -0
  90. package/src/trace/028-trace-operation-execution.ts +336 -0
  91. package/src/trace/029-trace-start-implementation.ts +172 -0
  92. package/src/trace/030-event-runtime-resolution.ts +151 -0
  93. package/src/trace/031-local-call-expansion.ts +37 -0
  94. package/src/trace/implementation-hints.ts +9 -6
  95. package/src/trace/selectors.ts +1 -0
  96. package/src/trace/trace-engine.ts +122 -624
  97. package/src/types.ts +57 -0
  98. package/src/utils/001-placeholders.ts +188 -10
  99. package/src/version.ts +1 -1
  100. package/dist/chunk-ZQABU7MR.js +0 -12151
  101. package/dist/chunk-ZQABU7MR.js.map +0 -1
@@ -7,7 +7,7 @@ export function loadTraceDiagnostics(
7
7
  workspaceId?: number,
8
8
  ): Array<Record<string, unknown>> {
9
9
  if (repoId === undefined && !includeWorkspaceDiagnostics) return [];
10
- return db.prepare(`SELECT d.repo_id repoId,d.severity,d.code,d.message,
10
+ const diagnostics = db.prepare(`SELECT d.repo_id repoId,d.severity,d.code,d.message,
11
11
  d.source_file sourceFile,d.source_line sourceLine
12
12
  FROM diagnostics d LEFT JOIN repositories r ON r.id=d.repo_id
13
13
  WHERE (? IS NULL OR d.repo_id=?)
@@ -18,6 +18,41 @@ export function loadTraceDiagnostics(
18
18
  ) as Array<
19
19
  Record<string, unknown>
20
20
  >;
21
+ const pending = packagePendingCount(
22
+ db, repoId, includeWorkspaceDiagnostics, workspaceId,
23
+ );
24
+ if (pending > 0) diagnostics.unshift(packagePendingDiagnostic(pending));
25
+ return diagnostics;
26
+ }
27
+
28
+ function packagePendingCount(
29
+ db: Db,
30
+ repoId: number | undefined,
31
+ includeWorkspace: boolean,
32
+ workspaceId: number | undefined,
33
+ ): number {
34
+ const row = db.prepare(`SELECT COUNT(*) count FROM symbol_calls sc
35
+ JOIN repositories r ON r.id=sc.repo_id
36
+ WHERE sc.status='unresolved' AND sc.callee_symbol_id IS NULL
37
+ AND sc.unresolved_reason='package_resolution_pending'
38
+ AND (?=1 OR sc.repo_id=?)
39
+ AND (? IS NULL OR r.workspace_id=?)`).get(
40
+ includeWorkspace ? 1 : 0, repoId, workspaceId, workspaceId,
41
+ );
42
+ return Number(row?.count ?? 0);
43
+ }
44
+
45
+ function packagePendingDiagnostic(count: number): Record<string, unknown> {
46
+ return {
47
+ severity: 'warning',
48
+ code: 'package_import_resolution_pending',
49
+ message: 'Package-import facts await workspace linking.',
50
+ packageResolutionState: 'pre_link_pending',
51
+ pendingPackageImportCount: count,
52
+ graphState: 'stale',
53
+ requiredAction: 'relink',
54
+ remediation: 'Run service-flow link --force for this workspace.',
55
+ };
21
56
  }
22
57
 
23
58
  export function prependTraceDiagnostic(
@@ -15,6 +15,7 @@ export function implementationStartDiagnostic(
15
15
  message: `Indexed operation matched but implementation edge is ${String(
16
16
  edge.status ?? 'unresolved',
17
17
  )}`,
18
+ selectorKind: 'operation',
18
19
  resolutionStage: 'implementation',
19
20
  resolutionStatus: edge.status === 'ambiguous'
20
21
  ? 'ambiguous_implementation'
@@ -1,5 +1,10 @@
1
1
  import type { Db } from '../db/connection.js';
2
2
  import {
3
+ matchRuntimeTemplate,
4
+ substituteVariables,
5
+ } from '../linker/dynamic-edge-resolver.js';
6
+ import {
7
+ compareBinary,
3
8
  type TraversalScopeScheduler,
4
9
  type TraversalScopeState,
5
10
  } from './010-traversal-scope.js';
@@ -53,6 +58,8 @@ export interface EventSubscriberTransition {
53
58
  candidateCount: number;
54
59
  symbolCallUnresolvedReason?: string;
55
60
  omittedSymbolCallUnresolvedReasonCharacterCount?: number;
61
+ matchStrategy?: string;
62
+ dispatchCertainty?: string;
56
63
  handler?: EventSubscriberSymbolTarget;
57
64
  }
58
65
 
@@ -60,6 +67,7 @@ export interface EventSubscriberTransitionQuery {
60
67
  workspaceId: number;
61
68
  graphGeneration: number;
62
69
  eventName: string;
70
+ vars?: Record<string, string>;
63
71
  }
64
72
 
65
73
  export type EventBodyExpansion =
@@ -156,8 +164,8 @@ export function eventTransitionEvidence(
156
164
  subscribeCallId: transition.subscribeCallId,
157
165
  symbolCallId: transition.symbolCallId,
158
166
  eventName: transition.eventName,
159
- matchStrategy: 'workspace_exact_event_name',
160
- dispatchCertainty: 'static_name_only',
167
+ matchStrategy: transition.matchStrategy ?? 'workspace_exact_event_name',
168
+ dispatchCertainty: transition.dispatchCertainty ?? 'static_name_only',
161
169
  associationBasis: transition.associationBasis,
162
170
  dispatchScope: transition.dispatchScope,
163
171
  roleSiteMatchCount: transition.roleSiteMatchCount,
@@ -194,7 +202,8 @@ export function loadEventSubscriberTransitions(
194
202
  query: EventSubscriberTransitionQuery,
195
203
  ): EventSubscriberTransition[] {
196
204
  const rows = db.prepare(`SELECT ge.id graphEdgeId,ge.generation graphGeneration,
197
- ge.from_id eventName,ge.status,ge.to_kind targetKind,ge.to_id targetId,
205
+ ge.from_kind fromKind,ge.from_id eventName,ge.status,
206
+ ge.to_kind targetKind,ge.to_id targetId,
198
207
  ge.confidence,ge.unresolved_reason unresolvedReason,ge.evidence_json evidenceJson,
199
208
  subscribe.id subscribeCallId,subscribe.repo_id subscriptionRepoId,
200
209
  subscribe.source_file sourceFile,subscribe.source_line sourceLine,
@@ -218,19 +227,100 @@ export function loadEventSubscriberTransitions(
218
227
  LEFT JOIN repositories handler_repo
219
228
  ON handler_repo.id=handler.repo_id AND handler_repo.workspace_id=ge.workspace_id
220
229
  WHERE ge.workspace_id=? AND ge.generation=?
221
- AND ge.edge_type='EVENT_SUBSCRIPTION_HANDLED_BY' AND ge.from_kind='event'
222
- AND ge.from_id COLLATE BINARY=? COLLATE BINARY
230
+ AND ge.edge_type='EVENT_SUBSCRIPTION_HANDLED_BY'
231
+ AND ge.from_kind IN ('event','event_candidate')
223
232
  ORDER BY COALESCE(subscription_repo.name,'') COLLATE BINARY,
224
233
  COALESCE(subscription_repo.id,0),COALESCE(subscribe.source_file,'') COLLATE BINARY,
225
234
  subscribe.call_site_start_offset,subscribe.call_site_end_offset,ge.id`).all(
226
- query.workspaceId, query.graphGeneration, query.eventName,
235
+ query.workspaceId, query.graphGeneration,
227
236
  );
228
- return rows.flatMap((row) => {
229
- const transition = transitionFromRow(row);
237
+ return rows.flatMap((raw) => {
238
+ const row = eventRowForQuery(raw, query);
239
+ const transition = row ? transitionFromRow(row) : undefined;
230
240
  return transition ? [transition] : [];
231
241
  });
232
242
  }
233
243
 
244
+ function eventRowForQuery(
245
+ row: Record<string, unknown>,
246
+ query: EventSubscriberTransitionQuery,
247
+ ): Record<string, unknown> | undefined {
248
+ const variables = query.vars;
249
+ if (variables === undefined) return exactPersistedEventRow(row, query);
250
+ const evidence = parseEvidence(row.evidenceJson);
251
+ const resolution = isRecord(evidence.eventTemplateResolution)
252
+ ? evidence.eventTemplateResolution : {};
253
+ if (typeof resolution.original !== 'string')
254
+ return exactPersistedEventRow(row, query);
255
+ return runtimeMatchedEventRow(
256
+ row, query, evidence, resolution.original, variables,
257
+ );
258
+ }
259
+
260
+ function exactPersistedEventRow(
261
+ row: Record<string, unknown>,
262
+ query: EventSubscriberTransitionQuery,
263
+ ): Record<string, unknown> | undefined {
264
+ return row.fromKind === 'event' && row.eventName === query.eventName
265
+ ? row : undefined;
266
+ }
267
+
268
+ function runtimeMatchedEventRow(
269
+ row: Record<string, unknown>,
270
+ query: EventSubscriberTransitionQuery,
271
+ evidence: Record<string, unknown>,
272
+ template: string,
273
+ variables: Record<string, string>,
274
+ ): Record<string, unknown> | undefined {
275
+ const substitution = substituteVariables(template, variables);
276
+ if (substitution.missing.length > 0
277
+ || substitution.effective !== query.eventName) return undefined;
278
+ const resolvedAssociation = evidence.associationStatus === 'resolved';
279
+ return {
280
+ ...row,
281
+ eventName: query.eventName,
282
+ status: typeof evidence.associationStatus === 'string'
283
+ ? evidence.associationStatus : row.status,
284
+ unresolvedReason: resolvedAssociation ? null : row.unresolvedReason,
285
+ evidenceJson: JSON.stringify({
286
+ ...evidence,
287
+ eventSubscriptionRuntimeSubstitution: substitution,
288
+ matchStrategy: 'workspace_exact_event_name_after_runtime_substitution',
289
+ dispatchCertainty: 'runtime_variables_exact',
290
+ }),
291
+ };
292
+ }
293
+
294
+ export function eventSubscriberMissingVariables(
295
+ db: Db,
296
+ query: EventSubscriberTransitionQuery,
297
+ ): string[] {
298
+ if (query.vars === undefined) return [];
299
+ const variables = query.vars;
300
+ const rows = db.prepare(`SELECT ge.from_id eventName,
301
+ ge.evidence_json evidenceJson FROM graph_edges ge
302
+ WHERE ge.workspace_id=? AND ge.generation=?
303
+ AND ge.edge_type='EVENT_SUBSCRIPTION_HANDLED_BY'
304
+ AND ge.from_kind='event_candidate'`).all(
305
+ query.workspaceId, query.graphGeneration,
306
+ );
307
+ const missing = rows.flatMap((row) => {
308
+ const evidence = parseEvidence(row.evidenceJson);
309
+ const resolution = isRecord(evidence.eventTemplateResolution)
310
+ ? evidence.eventTemplateResolution : {};
311
+ const template = typeof resolution.original === 'string'
312
+ ? resolution.original : undefined;
313
+ if (!template) return [];
314
+ const substitution = substituteVariables(template, variables);
315
+ if (substitution.missing.length === 0
316
+ || matchRuntimeTemplate(
317
+ substitution.effective, query.eventName,
318
+ ) === undefined) return [];
319
+ return substitution.missing;
320
+ });
321
+ return [...new Set(missing)].sort(compareBinary);
322
+ }
323
+
234
324
  function transitionFromRow(
235
325
  row: Record<string, unknown>,
236
326
  ): EventSubscriberTransition | undefined {
@@ -290,6 +380,8 @@ function associationEvidence(
290
380
  omittedSymbolCallUnresolvedReasonCharacterCount: symbolCallUnresolvedReason
291
381
  ? nonNegativeCount(evidence.omittedSymbolCallUnresolvedReasonCharacterCount)
292
382
  : undefined,
383
+ matchStrategy: stringValue(evidence.matchStrategy),
384
+ dispatchCertainty: stringValue(evidence.dispatchCertainty),
293
385
  };
294
386
  }
295
387
 
@@ -253,12 +253,11 @@ function hasExactDispatch(db: Db, calls: RootCallRow[]): boolean {
253
253
  AND subscriber.generation=emitted.generation
254
254
  AND subscriber.edge_type='EVENT_SUBSCRIPTION_HANDLED_BY'
255
255
  AND subscriber.from_kind='event'
256
- AND subscriber.from_id COLLATE BINARY=? COLLATE BINARY)
256
+ AND subscriber.from_id COLLATE BINARY=emitted.to_id COLLATE BINARY)
257
257
  LIMIT 1`);
258
258
  return calls.some((call) => call.callType === 'async_emit'
259
- && typeof call.eventName === 'string'
260
259
  && Boolean(match.get(call.workspaceId, call.graphGeneration,
261
- String(call.id), call.eventName)));
260
+ String(call.id))));
262
261
  }
263
262
 
264
263
  function workspaceAmbiguityDiagnostic(db: Db): Record<string, unknown> {
@@ -82,6 +82,7 @@ export interface CompactDecisionInput {
82
82
  implementationStrategy?: string;
83
83
  implementationGuided?: boolean;
84
84
  implementationContextual?: boolean;
85
+ tiedCandidateRepos?: CompactReferenceGroupV1;
85
86
  reasonCode?: string;
86
87
  eventMatchStrategy?: string;
87
88
  dispatchCertainty?: string;
@@ -225,6 +226,7 @@ export interface CompactDecisionV1 {
225
226
  implementationStrategy?: string;
226
227
  implementationGuided?: boolean;
227
228
  implementationContextual?: boolean;
229
+ tiedCandidateRepos?: CompactReferenceGroupV1;
228
230
  eventMatchStrategy?: string;
229
231
  dispatchCertainty?: string;
230
232
  eventSubscriptionCount?: number;
@@ -247,6 +249,10 @@ export interface CompactEdgeDetailsV1 {
247
249
 
248
250
  export interface CompactDiagnosticDetailsV1 {
249
251
  reasonCode?: string;
252
+ tiedCandidateRepos?: CompactReferenceGroupV1;
253
+ selectorKind?: string;
254
+ selectorSuggestions?: CompactReferenceGroupV1;
255
+ invalidFactCategories?: CompactReferenceGroupV1;
250
256
  missingVariableNames?: string[];
251
257
  missingVariableCount?: number;
252
258
  shownMissingVariableCount?: number;
@@ -8,6 +8,12 @@ import type {
8
8
  CompactStatus,
9
9
  CompactTraceObserver,
10
10
  } from './014-compact-contract.js';
11
+ import { implementationHintSuggestionProjection } from
12
+ './implementation-hints.js';
13
+ import {
14
+ isSafeCompactReferenceName,
15
+ projectCompactReferenceGroup,
16
+ } from './021-compact-decision-normalization.js';
11
17
 
12
18
  export interface TraceEdgeSemantics {
13
19
  source: CompactSemanticEndpoint;
@@ -111,8 +117,7 @@ export function semanticGraphTarget(
111
117
  ): CompactSemanticEndpoint {
112
118
  if (row.to_kind === 'event') return {
113
119
  kind: 'event', workspaceId,
114
- eventName: typeof call.event_name_expr === 'string'
115
- ? call.event_name_expr : row.to_id,
120
+ eventName: row.to_id,
116
121
  };
117
122
  const id = positiveNumber(row.to_id);
118
123
  if (row.to_kind === 'operation')
@@ -198,7 +203,7 @@ export function compactDecisionFromEvidence(
198
203
  const authoritativeMissingCount = finiteNumber(
199
204
  dynamic.missingVariableCount ?? evidence.missingVariableCount,
200
205
  );
201
- return {
206
+ const decision: CompactDecisionInput = {
202
207
  effectiveResolutionStatus: stringValue(effective.status),
203
208
  effectiveTarget: targetSummary(effective),
204
209
  persistedResolutionStatus: stringValue(persisted.status),
@@ -221,7 +226,7 @@ export function compactDecisionFromEvidence(
221
226
  implementationGuided: booleanValue(implementation.guided),
222
227
  implementationContextual: booleanValue(
223
228
  evidence.contextualImplementationSelected),
224
- reasonCode: stringValue(evidence.reasonCode ?? evidence.cycleReason),
229
+ reasonCode: compactEvidenceReasonCode(evidence),
225
230
  eventMatchStrategy: stringValue(evidence.matchStrategy),
226
231
  dispatchCertainty: stringValue(evidence.dispatchCertainty),
227
232
  associationStatus: stringValue(evidence.associationStatus),
@@ -233,6 +238,7 @@ export function compactDecisionFromEvidence(
233
238
  bodyExpansion: stringValue(evidence.bodyExpansion),
234
239
  ...overrides,
235
240
  };
241
+ return decisionWithTiedCandidates(evidence, decision);
236
242
  }
237
243
 
238
244
  export function compactRefs(
@@ -334,3 +340,54 @@ function firstNumber(...values: unknown[]): number | undefined {
334
340
  }
335
341
  return undefined;
336
342
  }
343
+
344
+ export function compactEvidenceReasonCode(
345
+ evidence: Record<string, unknown>,
346
+ ): string | undefined {
347
+ return stringValue(evidence.reasonCode ?? evidence.cycleReason)
348
+ ?? parserWarningReason(evidence.parserWarning);
349
+ }
350
+
351
+ function parserWarningReason(value: unknown): string | undefined {
352
+ const warning = recordValue(value);
353
+ const code = stringValue(warning.code);
354
+ if (code && code !== 'parser_warning') return code;
355
+ return stringValue(warning.message);
356
+ }
357
+
358
+ function tiedCandidateRepositoryGroup(
359
+ evidence: Record<string, unknown>,
360
+ decision: CompactDecisionInput,
361
+ ): CompactDecisionInput['tiedCandidateRepos'] {
362
+ if (decision.effectiveResolutionStatus !== 'ambiguous'
363
+ || (decision.candidateCount ?? 0) <= 1) return undefined;
364
+ const persisted = Array.isArray(evidence.implementationHintSuggestions)
365
+ ? evidence.implementationHintSuggestions.filter(
366
+ (value): value is Record<string, unknown> =>
367
+ Boolean(value && typeof value === 'object' && !Array.isArray(value)),
368
+ )
369
+ : [];
370
+ const projection = persisted.length > 0 ? {
371
+ suggestions: persisted,
372
+ suggestionCount: finiteNumber(
373
+ evidence.implementationHintSuggestionCount,
374
+ ) ?? persisted.length,
375
+ } : implementationHintSuggestionProjection(
376
+ evidence, Number.MAX_SAFE_INTEGER,
377
+ );
378
+ const repos = projection.suggestions.flatMap((suggestion) =>
379
+ typeof suggestion.implementationRepo === 'string'
380
+ ? [suggestion.implementationRepo] : []);
381
+ const group = projectCompactReferenceGroup(
382
+ repos, projection.suggestionCount, isSafeCompactReferenceName,
383
+ );
384
+ return group && group.total > 1 ? group : undefined;
385
+ }
386
+
387
+ function decisionWithTiedCandidates(
388
+ evidence: Record<string, unknown>,
389
+ decision: CompactDecisionInput,
390
+ ): CompactDecisionInput {
391
+ const tiedCandidateRepos = tiedCandidateRepositoryGroup(evidence, decision);
392
+ return tiedCandidateRepos ? { ...decision, tiedCandidateRepos } : decision;
393
+ }
@@ -20,10 +20,13 @@ import {
20
20
  } from './014-compact-contract.js';
21
21
  import {
22
22
  compactCompleteness, compactSafeCode, compactStatusCounts, compactStatusTotal,
23
- projectCompactDecision, projectCompactDecisionTarget, projectCompactDiagnostics,
23
+ projectCompactDecisionTarget, projectCompactDiagnostics,
24
24
  projectCompactQuery, projectCompactStart,
25
- removeEquivalentCompactPersistedDecision,
26
25
  } from './020-compact-field-projection.js';
26
+ import {
27
+ projectObservationDecision,
28
+ type CompactDecisionNode,
29
+ } from './024-compact-observation-decision.js';
27
30
 
28
31
  const REFERENCE_LIMIT = 5;
29
32
 
@@ -92,29 +95,24 @@ function observationDecision(
92
95
  input: CompactEdgeObservation,
93
96
  target: ResolvedNode,
94
97
  ): CompactDecisionV1 {
95
- const decision = projectCompactDecision(input.decision);
96
- if (decision.effectiveResolutionStatus && target.decisionTarget)
97
- decision.effectiveTarget = target.decisionTarget;
98
- if (decision.persistedResolutionStatus && input.decision?.persistedTarget) {
99
- const persisted = persistedDecisionTarget(db, input.decision.persistedTarget);
100
- if (persisted) decision.persistedTarget = persisted;
101
- }
102
- removeEquivalentCompactPersistedDecision(decision);
103
- return decision;
98
+ return projectObservationDecision(
99
+ input, target, (value) => decisionInputNode(db, value),
100
+ );
104
101
  }
105
102
 
106
- function persistedDecisionTarget(
103
+ function decisionInputNode(
107
104
  db: Db,
108
105
  target: CompactDecisionTargetInput,
109
- ): string | undefined {
106
+ ): CompactDecisionNode {
110
107
  const numeric = numericId(target.id);
111
108
  if (target.kind === 'operation' && numeric !== undefined)
112
- return operationNode(db, numeric)?.decisionTarget;
109
+ return operationNode(db, numeric) ?? {};
113
110
  if (target.kind === 'symbol' && numeric !== undefined)
114
- return symbolNode(db, numeric)?.decisionTarget;
111
+ return symbolNode(db, numeric) ?? {};
115
112
  if (target.kind === 'handler_method' && numeric !== undefined)
116
- return handlerNode(db, numeric)?.decisionTarget;
117
- return projectCompactDecisionTarget(target.kind, target.id);
113
+ return handlerNode(db, numeric) ?? {};
114
+ const decisionTarget = projectCompactDecisionTarget(target.kind, target.id);
115
+ return { key: decisionTarget, decisionTarget, projectedIdentity: true };
118
116
  }
119
117
 
120
118
  function resolveEndpoint(
@@ -2,6 +2,7 @@ import type { TraceEdge } from '../types.js';
2
2
  import type { CompactSemanticEndpoint, CompactStatus } from './014-compact-contract.js';
3
3
  import {
4
4
  compactDecisionFromEvidence,
5
+ compactEvidenceReasonCode,
5
6
  compactEventStatus,
6
7
  compactGraphStatus,
7
8
  compactRefs,
@@ -157,11 +158,11 @@ export function recordOutboundObservation(
157
158
  source, target,
158
159
  status: compactGraphStatus(input.row, input.evidence,
159
160
  input.unresolvedReason, input.dynamicMode),
160
- decision: compactDecisionFromEvidence(input.evidence, {
161
- reasonCode: input.unresolvedReason
162
- ? safeReasonCode(input.evidence.reasonCode, 'outbound_target_unresolved')
163
- : undefined,
164
- }),
161
+ decision: compactDecisionFromEvidence(input.evidence,
162
+ input.unresolvedReason ? {
163
+ reasonCode: compactEvidenceReasonCode(input.evidence)
164
+ ?? 'outbound_target_unresolved',
165
+ } : {}),
165
166
  refs: compactRefs({
166
167
  graphEdgeId: positiveId(input.evidence.persistedGraphEdgeId),
167
168
  outboundCallId: input.call.id,
@@ -295,11 +296,6 @@ function eventSite(
295
296
  endOffset: plan.transition.callSiteEndOffset });
296
297
  }
297
298
 
298
- function safeReasonCode(value: unknown, fallback: string): string {
299
- return typeof value === 'string' && /^[a-z][a-z0-9_.-]{0,79}$/.test(value)
300
- ? value : fallback;
301
- }
302
-
303
299
  function positiveId(value: unknown): number | string | undefined {
304
300
  if (typeof value === 'number') return Number.isFinite(value) && value > 0
305
301
  ? value : undefined;