@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
@@ -42,6 +42,12 @@ function isFunctionLike(node: ts.Node): node is ts.FunctionLikeDeclaration {
42
42
  function exported(node: ts.Node): boolean {
43
43
  return Boolean(ts.getCombinedModifierFlags(node as ts.Declaration) & ts.ModifierFlags.Export);
44
44
  }
45
+ function defaultExported(node: ts.Node): boolean {
46
+ return Boolean(
47
+ ts.getCombinedModifierFlags(node as ts.Declaration)
48
+ & ts.ModifierFlags.Default,
49
+ );
50
+ }
45
51
  function isPublicClassMethod(node: ts.MethodDeclaration): boolean {
46
52
  const flags = ts.getCombinedModifierFlags(node);
47
53
  return (flags & ts.ModifierFlags.Private) === 0 && (flags & ts.ModifierFlags.Protected) === 0;
@@ -141,6 +147,7 @@ interface SymbolCollection {
141
147
  exportNames: Map<string, string>;
142
148
  objectExports: Set<string>;
143
149
  exportedClasses: Set<string>;
150
+ defaultExportedClasses: Set<string>;
144
151
  declaredClasses: Set<string>;
145
152
  proxies: Map<string, SymbolCallProxy[]>;
146
153
  instances: Map<string, SymbolClassInstance[]>;
@@ -153,7 +160,8 @@ function symbolSourceEvidence(
153
160
  parentRoot: string;
154
161
  qualifiedName: string;
155
162
  declaredExportName?: string;
156
- classContainerExported: boolean; classMemberExported: boolean;
163
+ classContainerExported: boolean; classContainerDefaultExported: boolean;
164
+ classMemberExported: boolean;
157
165
  objectExported: boolean;
158
166
  evidence?: Record<string, unknown>;
159
167
  },
@@ -168,6 +176,8 @@ function symbolSourceEvidence(
168
176
  if (options.classContainerExported && ts.isMethodDeclaration(node)
169
177
  && isPublicClassMethod(node)) return {
170
178
  source: 'exported_class_instance_member', exportedClass: options.parentRoot,
179
+ exportedClassExportKind: options.classContainerDefaultExported
180
+ ? 'default' : 'named',
171
181
  memberKind: 'class_method',
172
182
  };
173
183
  if (options.declaredExportName) return {
@@ -201,6 +211,7 @@ interface SymbolNames {
201
211
  declaredExportName?: string;
202
212
  objectExported: boolean;
203
213
  classContainerExported: boolean;
214
+ classContainerDefaultExported: boolean;
204
215
  classMemberExported: boolean;
205
216
  effectiveName?: string;
206
217
  }
@@ -219,6 +230,24 @@ function exportedClassMember(
219
230
  && isPublicClassMethod(node);
220
231
  }
221
232
 
233
+ function classExportState(
234
+ collection: SymbolCollection,
235
+ parentName: string | undefined,
236
+ parentRoot: string,
237
+ ): {
238
+ classContainerExported: boolean;
239
+ classContainerDefaultExported: boolean;
240
+ } {
241
+ return {
242
+ classContainerExported: Boolean(
243
+ parentName && collection.exportedClasses.has(parentRoot),
244
+ ),
245
+ classContainerDefaultExported: Boolean(
246
+ parentName && collection.defaultExportedClasses.has(parentRoot),
247
+ ),
248
+ };
249
+ }
250
+
222
251
  function symbolNames(
223
252
  collection: SymbolCollection,
224
253
  kind: string,
@@ -238,12 +267,11 @@ function symbolNames(
238
267
  const classMemberExported = exportedClassMember(
239
268
  collection, kind, parentName, parentRoot, node,
240
269
  );
241
- const classContainerExported = Boolean(
242
- parentName && collection.exportedClasses.has(parentRoot),
243
- );
270
+ const classState = classExportState(collection, parentName, parentRoot);
244
271
  return {
245
272
  parentRoot, declaredExportName, qualifiedName,
246
- objectExported, classContainerExported, classMemberExported,
273
+ objectExported, ...classState,
274
+ classMemberExported,
247
275
  effectiveName: classMemberExported || objectExported
248
276
  ? qualifiedName : declaredExportName,
249
277
  };
@@ -266,6 +294,7 @@ function addExecutableSymbol(
266
294
  qualifiedName: names.qualifiedName,
267
295
  declaredExportName: names.declaredExportName,
268
296
  classContainerExported: names.classContainerExported,
297
+ classContainerDefaultExported: names.classContainerDefaultExported,
269
298
  classMemberExported: names.classMemberExported,
270
299
  objectExported: names.objectExported,
271
300
  evidence,
@@ -457,6 +486,8 @@ function collectClassDeclaration(
457
486
  collection.declaredClasses.add(node.name.text);
458
487
  if (exported(node) || collection.exportNames.has(node.name.text))
459
488
  collection.exportedClasses.add(node.name.text);
489
+ if (defaultExported(node))
490
+ collection.defaultExportedClasses.add(node.name.text);
460
491
  for (const member of node.members)
461
492
  visitDeclaredSymbol(collection, member, node.name.text);
462
493
  return true;
@@ -553,6 +584,7 @@ function createCollection(
553
584
  importBindings: collectSymbolImportBindings(source),
554
585
  exportNames: exportDeclarations(source),
555
586
  objectExports: new Set(), exportedClasses: new Set(),
587
+ defaultExportedClasses: new Set(),
556
588
  declaredClasses: new Set(), proxies: new Map(), instances: new Map(),
557
589
  };
558
590
  }
@@ -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
+ }
@@ -111,9 +111,8 @@ function decisionInputNode(
111
111
  return symbolNode(db, numeric) ?? {};
112
112
  if (target.kind === 'handler_method' && numeric !== undefined)
113
113
  return handlerNode(db, numeric) ?? {};
114
- return {
115
- decisionTarget: projectCompactDecisionTarget(target.kind, target.id),
116
- };
114
+ const decisionTarget = projectCompactDecisionTarget(target.kind, target.id);
115
+ return { key: decisionTarget, decisionTarget, projectedIdentity: true };
117
116
  }
118
117
 
119
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;
@@ -19,6 +19,9 @@ import type {
19
19
  } from './014-compact-contract.js';
20
20
  import {
21
21
  compactMissingRemediation,
22
+ isSafeCompactReferenceName,
23
+ isSafeCompactSelectorSuggestion,
24
+ projectCompactReferenceGroup,
22
25
  projectCompactMissingNames,
23
26
  type CompactMissingNameProjection,
24
27
  } from './021-compact-decision-normalization.js';
@@ -31,8 +34,19 @@ const compactDiagnosticMessages: Readonly<Record<string, string>> = {
31
34
  implementation_hint_mismatch: 'The implementation hint did not select one implementation.',
32
35
  selected_handler_provenance_mismatch: 'Selected handler provenance did not match its graph target.',
33
36
  selected_handler_target_not_found: 'The selected handler target is not indexed.',
37
+ trace_start_ambiguous: 'The trace start selector is ambiguous.',
38
+ trace_start_not_found: 'The trace start selector did not match an indexed start.',
34
39
  trace_start_implementation_unresolved: 'The trace start implementation is unresolved.',
35
40
  };
41
+ const selectorDiagnosticCodes = new Set([
42
+ 'handler_decorators_not_indexed',
43
+ 'handler_methods_not_indexed',
44
+ 'selector_repo_ambiguous',
45
+ 'selector_repo_not_found',
46
+ 'trace_start_ambiguous',
47
+ 'trace_start_implementation_unresolved',
48
+ 'trace_start_not_found',
49
+ ]);
36
50
 
37
51
  export function projectCompactDecision(
38
52
  input: CompactDecisionInput | undefined,
@@ -87,6 +101,8 @@ function addImplementationDecision(
87
101
  out.implementationGuided = input.implementationGuided;
88
102
  if (input.implementationContextual !== undefined)
89
103
  out.implementationContextual = input.implementationContextual;
104
+ if (input.tiedCandidateRepos)
105
+ out.tiedCandidateRepos = input.tiedCandidateRepos;
90
106
  }
91
107
 
92
108
  function addEventDecision(out: CompactDecisionV1, input: CompactDecisionInput): void {
@@ -243,6 +259,10 @@ function compactDiagnosticDetails(
243
259
  const out: CompactDiagnosticDetailsV1 = {};
244
260
  const reasonCode = compactSafeCode(value.reasonCode);
245
261
  if (reasonCode) out.reasonCode = reasonCode;
262
+ if (selectorDiagnosticCodes.has(code)) addDiagnosticSelector(out, value);
263
+ if (code === 'reindex_required') addInvalidFactCategories(out, value);
264
+ if (code === 'implementation_hint_mismatch')
265
+ addImplementationHintCandidates(out, value);
246
266
  if (code === 'trace_runtime_variables_missing') addDiagnosticNames(out, value);
247
267
  addDiagnosticCounts(out, value);
248
268
  const hint = compactDiagnosticRemediation(code, out);
@@ -255,6 +275,51 @@ function compactDiagnosticDetails(
255
275
  return out;
256
276
  }
257
277
 
278
+ function addDiagnosticSelector(
279
+ out: CompactDiagnosticDetailsV1,
280
+ value: Record<string, unknown>,
281
+ ): void {
282
+ const selectorKind = compactSafeCode(value.selectorKind);
283
+ if (selectorKind) out.selectorKind = selectorKind;
284
+ const suggestions = projectCompactReferenceGroup(
285
+ compactStringArray(value.selectorSuggestions),
286
+ value.selectorSuggestionCount,
287
+ isSafeCompactSelectorSuggestion,
288
+ );
289
+ if (suggestions) out.selectorSuggestions = suggestions;
290
+ }
291
+
292
+ function addInvalidFactCategories(
293
+ out: CompactDiagnosticDetailsV1,
294
+ value: Record<string, unknown>,
295
+ ): void {
296
+ const categories = compactRecordArray(value.invalidFactCategories)
297
+ .flatMap((item) =>
298
+ typeof item.category === 'string' ? [item.category] : []);
299
+ const projection = projectCompactReferenceGroup(
300
+ categories, value.invalidFactCategoryCount,
301
+ (item) => compactSafeCode(item) === item,
302
+ );
303
+ if (projection) out.invalidFactCategories = projection;
304
+ }
305
+
306
+ function addImplementationHintCandidates(
307
+ out: CompactDiagnosticDetailsV1,
308
+ value: Record<string, unknown>,
309
+ ): void {
310
+ const repos = compactRecordArray(value.implementationHintSuggestions)
311
+ .flatMap((item) =>
312
+ typeof item.implementationRepo === 'string'
313
+ ? [item.implementationRepo] : []);
314
+ const uniqueCount = new Set(repos.map((repo) => repo.trim())).size;
315
+ if (uniqueCount < 2) return;
316
+ const projection = projectCompactReferenceGroup(
317
+ repos, value.implementationHintSuggestionCount,
318
+ isSafeCompactReferenceName,
319
+ );
320
+ if (projection) out.tiedCandidateRepos = projection;
321
+ }
322
+
258
323
  function addDiagnosticNames(
259
324
  out: CompactDiagnosticDetailsV1,
260
325
  value: Record<string, unknown>,
@@ -345,7 +410,8 @@ function compactDiagnosticHintCount(
345
410
 
346
411
  function compactRemediationHint(code: string): string | undefined {
347
412
  if (code === 'provide_runtime_variables') return 'Provide the missing variable names listed in details.';
348
- if (code === 'select_implementation') return 'Select one implementation with a scoped implementation hint.';
413
+ if (code === 'select_implementation')
414
+ return 'Use --implementation-hint with service, operation, package, repository, family, and repo keys; repo is required.';
349
415
  if (code === 'reindex_and_link') return 'Force reindex, then force relink the workspace.';
350
416
  if (code === 'inspect_detailed_edge') return 'Inspect the correlated detailed trace edge.';
351
417
  return undefined;
@@ -389,6 +455,13 @@ function compactStringArray(value: unknown): string[] {
389
455
  ? value.filter((item): item is string => typeof item === 'string') : [];
390
456
  }
391
457
 
458
+ function compactRecordArray(value: unknown): Array<Record<string, unknown>> {
459
+ return Array.isArray(value)
460
+ ? value.filter((item): item is Record<string, unknown> =>
461
+ Boolean(item && typeof item === 'object' && !Array.isArray(item)))
462
+ : [];
463
+ }
464
+
392
465
  function compactArrayLength(value: unknown): number {
393
466
  return Array.isArray(value) ? value.length : 0;
394
467
  }