@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
@@ -17,8 +17,15 @@ import type {
17
17
  CompactStartV1,
18
18
  CompactStatusCountsV1,
19
19
  } from './014-compact-contract.js';
20
+ import {
21
+ compactMissingRemediation,
22
+ isSafeCompactReferenceName,
23
+ isSafeCompactSelectorSuggestion,
24
+ projectCompactReferenceGroup,
25
+ projectCompactMissingNames,
26
+ type CompactMissingNameProjection,
27
+ } from './021-compact-decision-normalization.js';
20
28
 
21
- const compactNameLimit = 8;
22
29
  const compactDiagnosticMessages: Readonly<Record<string, string>> = {
23
30
  schema_upgrade_required: 'The database schema must be upgraded before tracing.',
24
31
  reindex_required: 'Current analyzer facts are required before tracing.',
@@ -27,8 +34,19 @@ const compactDiagnosticMessages: Readonly<Record<string, string>> = {
27
34
  implementation_hint_mismatch: 'The implementation hint did not select one implementation.',
28
35
  selected_handler_provenance_mismatch: 'Selected handler provenance did not match its graph target.',
29
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.',
30
39
  trace_start_implementation_unresolved: 'The trace start implementation is unresolved.',
31
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
+ ]);
32
50
 
33
51
  export function projectCompactDecision(
34
52
  input: CompactDecisionInput | undefined,
@@ -56,14 +74,10 @@ function resolutionDecision(input: CompactDecisionInput): CompactDecisionV1 {
56
74
  }
57
75
 
58
76
  function addNameDecision(out: CompactDecisionV1, input: CompactDecisionInput): void {
59
- const allNames = safeVariableNames(input.missingVariableNames);
60
- const names = allNames.slice(0, compactNameLimit);
61
- const total = Math.max(compactCount(input.missingVariableCount), allNames.length);
62
- if (names.length > 0) out.missingVariableNames = names;
63
- if (total === 0) return;
64
- out.missingVariableCount = total;
65
- out.shownMissingVariableCount = names.length;
66
- out.omittedMissingVariableCount = Math.max(0, total - names.length);
77
+ const projection = projectCompactMissingNames(
78
+ input.missingVariableNames, input.missingVariableCount,
79
+ );
80
+ applyMissingProjection(out, projection);
67
81
  }
68
82
 
69
83
  function addDynamicDecision(out: CompactDecisionV1, input: CompactDecisionInput): void {
@@ -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 {
@@ -117,14 +133,24 @@ function addRemediationDecision(
117
133
  out: CompactDecisionV1,
118
134
  input: CompactDecisionInput,
119
135
  ): void {
120
- const hint = input.remediationCode
121
- ? compactRemediationHint(input.remediationCode) : undefined;
136
+ const hint = decisionRemediation(input);
122
137
  if (!hint) return;
123
138
  out.remediationHint = hint;
124
139
  const total = Math.max(1, compactCount(input.remediationHintCount));
125
140
  out.omittedRemediationHintCount = Math.max(0, total - 1);
126
141
  }
127
142
 
143
+ function decisionRemediation(input: CompactDecisionInput): string | undefined {
144
+ if (input.remediationCode === 'provide_runtime_variables') {
145
+ const projection = projectCompactMissingNames(
146
+ input.missingVariableNames, input.missingVariableCount,
147
+ );
148
+ return compactMissingRemediation(projection, 'detailed trace edge');
149
+ }
150
+ return input.remediationCode
151
+ ? compactRemediationHint(input.remediationCode) : undefined;
152
+ }
153
+
128
154
  export function projectCompactDiagnostics(
129
155
  values: Array<Record<string, unknown>>,
130
156
  ): CompactProjectedDiagnostic[] {
@@ -200,15 +226,6 @@ export function compactStatusTotal(counts: CompactStatusCountsV1): number {
200
226
  + counts.ambiguous + counts.unresolved + counts.cycle;
201
227
  }
202
228
 
203
- export function removeEquivalentCompactPersistedDecision(
204
- decision: CompactDecisionV1,
205
- ): void {
206
- if (decision.persistedResolutionStatus !== decision.effectiveResolutionStatus) return;
207
- if (!decision.persistedTarget || decision.persistedTarget !== decision.effectiveTarget) return;
208
- delete decision.persistedResolutionStatus;
209
- delete decision.persistedTarget;
210
- }
211
-
212
229
  function compactBlockingDiagnostic(item: CompactDiagnosticRowV1): boolean {
213
230
  if (item[1] === 'error') return true;
214
231
  return item[2] === 'schema_upgrade_required'
@@ -242,9 +259,13 @@ function compactDiagnosticDetails(
242
259
  const out: CompactDiagnosticDetailsV1 = {};
243
260
  const reasonCode = compactSafeCode(value.reasonCode);
244
261
  if (reasonCode) out.reasonCode = reasonCode;
245
- addDiagnosticNames(out, value);
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);
266
+ if (code === 'trace_runtime_variables_missing') addDiagnosticNames(out, value);
246
267
  addDiagnosticCounts(out, value);
247
- const hint = compactDiagnosticRemediation(code);
268
+ const hint = compactDiagnosticRemediation(code, out);
248
269
  if (hint) {
249
270
  out.remediationHint = hint;
250
271
  out.omittedHintCount = Math.max(
@@ -254,18 +275,70 @@ function compactDiagnosticDetails(
254
275
  return out;
255
276
  }
256
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
+
257
323
  function addDiagnosticNames(
258
324
  out: CompactDiagnosticDetailsV1,
259
325
  value: Record<string, unknown>,
260
326
  ): void {
261
- const allNames = safeVariableNames(compactStringArray(value.missingVariables));
262
- const names = allNames.slice(0, compactNameLimit);
263
- const total = Math.max(allNames.length, compactCount(value.missingVariableCount));
264
- if (names.length > 0) out.missingVariableNames = names;
265
- if (total === 0) return;
266
- out.missingVariableCount = total;
267
- out.shownMissingVariableCount = names.length;
268
- out.omittedMissingVariableCount = Math.max(0, total - names.length);
327
+ const projection = projectCompactMissingNames(
328
+ compactStringArray(value.missingVariables), value.missingVariableCount,
329
+ );
330
+ applyMissingProjection(out, projection);
331
+ }
332
+
333
+ function applyMissingProjection(
334
+ out: CompactDecisionV1 | CompactDiagnosticDetailsV1,
335
+ projection: CompactMissingNameProjection,
336
+ ): void {
337
+ if (projection.names.length > 0) out.missingVariableNames = projection.names;
338
+ if (projection.total === 0) return;
339
+ out.missingVariableCount = projection.total;
340
+ out.shownMissingVariableCount = projection.shown;
341
+ out.omittedMissingVariableCount = projection.omitted;
269
342
  }
270
343
 
271
344
  function addDiagnosticCounts(
@@ -303,11 +376,19 @@ function compactDiagnosticSeverity(value: unknown): 'error' | 'warning' | 'info'
303
376
  return value === 'error' || value === 'warning' ? value : 'info';
304
377
  }
305
378
 
306
- function compactDiagnosticRemediation(code: string): string | undefined {
379
+ function compactDiagnosticRemediation(
380
+ code: string,
381
+ details: CompactDiagnosticDetailsV1,
382
+ ): string | undefined {
307
383
  if (code === 'schema_upgrade_required' || code === 'reindex_required')
308
384
  return compactRemediationHint('reindex_and_link');
309
385
  if (code === 'trace_runtime_variables_missing')
310
- return compactRemediationHint('provide_runtime_variables');
386
+ return compactMissingRemediation({
387
+ names: details.missingVariableNames ?? [],
388
+ total: details.missingVariableCount ?? 0,
389
+ shown: details.shownMissingVariableCount ?? 0,
390
+ omitted: details.omittedMissingVariableCount ?? 0,
391
+ }, 'detailed diagnostic');
311
392
  if (code === 'implementation_hint_mismatch')
312
393
  return compactRemediationHint('select_implementation');
313
394
  return undefined;
@@ -329,7 +410,8 @@ function compactDiagnosticHintCount(
329
410
 
330
411
  function compactRemediationHint(code: string): string | undefined {
331
412
  if (code === 'provide_runtime_variables') return 'Provide the missing variable names listed in details.';
332
- 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.';
333
415
  if (code === 'reindex_and_link') return 'Force reindex, then force relink the workspace.';
334
416
  if (code === 'inspect_detailed_edge') return 'Inspect the correlated detailed trace edge.';
335
417
  return undefined;
@@ -358,11 +440,6 @@ function compactSafeSourceFile(value: unknown): string | undefined {
358
440
  && !/^[a-z]+:\/\//i.test(value) && !/[\r\n]/.test(value) ? value : undefined;
359
441
  }
360
442
 
361
- function safeVariableNames(values: string[] | undefined): string[] {
362
- return compactSortedUnique((values ?? [])
363
- .filter((value) => /^[A-Za-z_$][\w$]*$/.test(value)));
364
- }
365
-
366
443
  function compactCount(value: unknown): number {
367
444
  return typeof value === 'number' && Number.isFinite(value)
368
445
  ? Math.max(0, Math.floor(value)) : 0;
@@ -378,6 +455,13 @@ function compactStringArray(value: unknown): string[] {
378
455
  ? value.filter((item): item is string => typeof item === 'string') : [];
379
456
  }
380
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
+
381
465
  function compactArrayLength(value: unknown): number {
382
466
  return Array.isArray(value) ? value.length : 0;
383
467
  }
@@ -0,0 +1,171 @@
1
+ import { compareBinary } from './010-traversal-scope.js';
2
+ import type {
3
+ CompactDecisionV1,
4
+ CompactReferenceGroupV1,
5
+ CompactStatus,
6
+ } from './014-compact-contract.js';
7
+
8
+ export const COMPACT_MISSING_NAME_LIMIT = 8;
9
+ export const COMPACT_MISSING_NAME_MAX_LENGTH = 160;
10
+ export const COMPACT_REFERENCE_VALUE_LIMIT = 5;
11
+ const COMPACT_REFERENCE_VALUE_MAX_LENGTH = 240;
12
+
13
+ export interface CompactMissingNameProjection {
14
+ readonly names: string[];
15
+ readonly total: number;
16
+ readonly shown: number;
17
+ readonly omitted: number;
18
+ }
19
+
20
+ const identifier = '[A-Za-z_$][A-Za-z0-9_$]*';
21
+ const member = `(?:(?:\\.|\\?\\.)${identifier}|(?:\\[|\\?\\.\\[)(?:0|[1-9][0-9]*)\\])`;
22
+ const transform = '(?:(?:\\.|\\?\\.)(?:toLowerCase|toUpperCase|trim)\\(\\))?';
23
+ const safeMissingName = new RegExp(`^${identifier}(?:${member})*${transform}$`);
24
+
25
+ export function projectCompactMissingNames(
26
+ values: readonly string[] | undefined,
27
+ authoritativeCount: unknown,
28
+ ): CompactMissingNameProjection {
29
+ const candidates = new Map<string, boolean>();
30
+ for (const value of values ?? []) {
31
+ const normalized = value.trim();
32
+ if (!normalized) continue;
33
+ candidates.set(
34
+ normalized,
35
+ (candidates.get(normalized) ?? true) && isSafeCompactMissingName(value),
36
+ );
37
+ }
38
+ const raw = [...candidates.keys()].sort(compareBinary);
39
+ const safe = raw.filter((name) => candidates.get(name) === true);
40
+ const names = safe.slice(0, COMPACT_MISSING_NAME_LIMIT);
41
+ const total = Math.max(compactCount(authoritativeCount), raw.length);
42
+ return {
43
+ names, total, shown: names.length,
44
+ omitted: Math.max(0, total - names.length),
45
+ };
46
+ }
47
+
48
+ export function isSafeCompactMissingName(value: string): boolean {
49
+ if (hasControlCharacter(value)) return false;
50
+ const normalized = value.trim();
51
+ return normalized.length > 0
52
+ && normalized.length <= COMPACT_MISSING_NAME_MAX_LENGTH
53
+ && safeMissingName.test(normalized);
54
+ }
55
+
56
+ export function projectCompactReferenceGroup(
57
+ values: readonly string[] | undefined,
58
+ authoritativeCount: unknown,
59
+ isSafe: (value: string) => boolean,
60
+ ): CompactReferenceGroupV1 | undefined {
61
+ const candidates = normalizedReferenceValues(values, isSafe);
62
+ const unique = [...candidates.keys()].sort(compareBinary);
63
+ const safe = unique.filter((value) => candidates.get(value) === true);
64
+ const shown = safe.slice(0, COMPACT_REFERENCE_VALUE_LIMIT);
65
+ const total = Math.max(compactCount(authoritativeCount), unique.length);
66
+ if (total === 0) return undefined;
67
+ return {
68
+ values: shown,
69
+ total,
70
+ shown: shown.length,
71
+ omitted: Math.max(0, total - shown.length),
72
+ };
73
+ }
74
+
75
+ export function isSafeCompactReferenceName(value: string): boolean {
76
+ if (!safeReferenceText(value, 160)) return false;
77
+ return /^[@A-Za-z0-9_$][@A-Za-z0-9_$./:-]*$/.test(value.trim());
78
+ }
79
+
80
+ export function isSafeCompactSelectorSuggestion(value: string): boolean {
81
+ if (!safeReferenceText(value, COMPACT_REFERENCE_VALUE_MAX_LENGTH))
82
+ return false;
83
+ return /^(?:--(?:repo|service|operation|path|handler) [@A-Za-z0-9_$./:-]+)(?: --(?:repo|service|operation|path|handler) [@A-Za-z0-9_$./:-]+)*$/
84
+ .test(value.trim());
85
+ }
86
+
87
+ export function normalizeCompactDecisionEquivalence(
88
+ decision: CompactDecisionV1,
89
+ status: CompactStatus,
90
+ canonicalTarget: string | undefined,
91
+ targetIdentityEqual = false,
92
+ persistedIdentityEqual = false,
93
+ rawPersistedStatus?: string,
94
+ ): void {
95
+ if (decision.effectiveResolutionStatus === status)
96
+ delete decision.effectiveResolutionStatus;
97
+ removeEquivalentPersistedDecision(
98
+ decision, status, canonicalTarget, persistedIdentityEqual,
99
+ rawPersistedStatus,
100
+ );
101
+ if (targetIdentityEqual && canonicalTarget !== undefined
102
+ && decision.effectiveTarget === canonicalTarget)
103
+ delete decision.effectiveTarget;
104
+ }
105
+
106
+ export function compactMissingRemediation(
107
+ projection: CompactMissingNameProjection,
108
+ artifact: 'detailed diagnostic' | 'detailed trace edge',
109
+ ): string {
110
+ if (projection.total > 0 && projection.shown === projection.total)
111
+ return 'Provide the missing variable names listed in details.';
112
+ if (projection.shown > 0)
113
+ return `Provide the listed names; inspect the correlated ${artifact} for omitted names.`;
114
+ return `Inspect the correlated ${artifact} for missing variable names.`;
115
+ }
116
+
117
+ function removeEquivalentPersistedDecision(
118
+ decision: CompactDecisionV1,
119
+ status: CompactStatus,
120
+ canonicalTarget: string | undefined,
121
+ identityEqual: boolean,
122
+ rawPersistedStatus: string | undefined,
123
+ ): void {
124
+ if (!identityEqual
125
+ || rawPersistedStatus !== status
126
+ || (decision.effectiveResolutionStatus !== undefined
127
+ && decision.persistedResolutionStatus
128
+ !== decision.effectiveResolutionStatus))
129
+ return;
130
+ if (!decision.persistedTarget || canonicalTarget === undefined
131
+ || decision.persistedTarget !== canonicalTarget)
132
+ return;
133
+ delete decision.persistedResolutionStatus;
134
+ delete decision.persistedTarget;
135
+ }
136
+
137
+ function compactCount(value: unknown): number {
138
+ return typeof value === 'number' && Number.isFinite(value)
139
+ ? Math.max(0, Math.floor(value)) : 0;
140
+ }
141
+
142
+ function normalizedReferenceValues(
143
+ values: readonly string[] | undefined,
144
+ isSafe: (value: string) => boolean,
145
+ ): Map<string, boolean> {
146
+ const unique = new Map<string, boolean>();
147
+ for (const raw of values ?? []) {
148
+ const value = raw.trim();
149
+ if (!value) continue;
150
+ unique.set(value, (unique.get(value) ?? true) && isSafe(raw));
151
+ }
152
+ return unique;
153
+ }
154
+
155
+ function safeReferenceText(value: string, maxLength: number): boolean {
156
+ if (hasControlCharacter(value)) return false;
157
+ const normalized = value.trim();
158
+ return normalized.length > 0
159
+ && normalized.length <= maxLength
160
+ && !/^[a-z]+:\/\//i.test(normalized)
161
+ && !/(?:^|[^A-Za-z0-9])(?:authorization|bearer|credential|destination|password|secret|token)(?:$|[^A-Za-z0-9])/i
162
+ .test(normalized);
163
+ }
164
+
165
+ function hasControlCharacter(value: string): boolean {
166
+ for (let index = 0; index < value.length; index += 1) {
167
+ const code = value.charCodeAt(index);
168
+ if (code <= 31 || code === 127) return true;
169
+ }
170
+ return false;
171
+ }
@@ -0,0 +1,21 @@
1
+ import type { Db } from '../db/connection.js';
2
+ import { currentFactLifecycleDiagnostic } from '../db/001-fact-lifecycle.js';
3
+ import { resolveTraversalWorkspaceId } from './010-traversal-scope.js';
4
+
5
+ function workspaceScopeIsAmbiguous(db: Db): boolean {
6
+ const rows = db.prepare(`SELECT DISTINCT workspace_id workspaceId
7
+ FROM repositories ORDER BY workspace_id LIMIT 2`).all();
8
+ return rows.length > 1;
9
+ }
10
+
11
+ export function shouldDeferTraceStartSelection(
12
+ db: Db,
13
+ requestedWorkspaceId: number | undefined,
14
+ repoId: number | undefined,
15
+ ): boolean {
16
+ const workspaceId = resolveTraversalWorkspaceId(
17
+ db, requestedWorkspaceId, repoId,
18
+ );
19
+ if (workspaceId === undefined) return workspaceScopeIsAmbiguous(db);
20
+ return currentFactLifecycleDiagnostic(db, workspaceId) !== undefined;
21
+ }
@@ -0,0 +1,23 @@
1
+ import type { Db } from '../db/connection.js';
2
+
3
+ export function outboundScopeSymbolIds(
4
+ db: Db,
5
+ symbolIds: ReadonlySet<number> | undefined,
6
+ includeAsync: boolean,
7
+ ): ReadonlySet<number> | undefined {
8
+ if (!includeAsync || !symbolIds || symbolIds.size === 0) return symbolIds;
9
+ const ids = [...symbolIds];
10
+ const rows = db.prepare(`SELECT DISTINCT child.id FROM symbols parent
11
+ JOIN symbols child ON child.repo_id=parent.repo_id
12
+ AND child.source_file=parent.source_file
13
+ AND child.kind='event_registration'
14
+ AND parent.start_offset<=child.start_offset
15
+ AND parent.end_offset>=child.end_offset
16
+ WHERE parent.id IN (${ids.map(() => '?').join(',')})
17
+ ORDER BY child.id`).all(...ids);
18
+ return new Set([
19
+ ...ids,
20
+ ...rows.flatMap((row) =>
21
+ typeof row.id === 'number' ? [row.id] : []),
22
+ ]);
23
+ }
@@ -0,0 +1,81 @@
1
+ import type {
2
+ CompactDecisionTargetInput,
3
+ CompactDecisionV1,
4
+ CompactEdgeObservation,
5
+ } from './014-compact-contract.js';
6
+ import { projectCompactDecision } from './020-compact-field-projection.js';
7
+ import { normalizeCompactDecisionEquivalence } from
8
+ './021-compact-decision-normalization.js';
9
+
10
+ export interface CompactDecisionNode {
11
+ key?: string;
12
+ decisionTarget?: string;
13
+ projectedIdentity?: boolean;
14
+ }
15
+
16
+ function resolvedNode(
17
+ value: CompactDecisionTargetInput | undefined,
18
+ resolve: (input: CompactDecisionTargetInput) => CompactDecisionNode,
19
+ ): CompactDecisionNode | undefined {
20
+ return value ? resolve(value) : undefined;
21
+ }
22
+
23
+ function applyEffectiveTarget(
24
+ decision: CompactDecisionV1,
25
+ effective: CompactDecisionNode | undefined,
26
+ ): void {
27
+ if (effective?.decisionTarget)
28
+ decision.effectiveTarget = effective.decisionTarget;
29
+ }
30
+
31
+ function applyPersistedTarget(
32
+ decision: CompactDecisionV1,
33
+ persisted: CompactDecisionNode | undefined,
34
+ status: string | undefined,
35
+ ): void {
36
+ if (persisted?.decisionTarget && status)
37
+ decision.persistedTarget = persisted.decisionTarget;
38
+ }
39
+
40
+ function sameCanonicalNode(
41
+ left: CompactDecisionNode | undefined,
42
+ right: CompactDecisionNode,
43
+ ): boolean {
44
+ if (left?.key === undefined) return false;
45
+ return left.projectedIdentity === true
46
+ ? left.key === right.decisionTarget
47
+ : left.key === right.key;
48
+ }
49
+
50
+ function persistedCanonical(
51
+ persisted: CompactDecisionNode | undefined,
52
+ effective: CompactDecisionNode | undefined,
53
+ target: CompactDecisionNode,
54
+ ): boolean {
55
+ if (!sameCanonicalNode(persisted, target)) return false;
56
+ return effective?.key === undefined || sameCanonicalNode(effective, target);
57
+ }
58
+
59
+ export function projectObservationDecision(
60
+ input: CompactEdgeObservation,
61
+ target: CompactDecisionNode,
62
+ resolve: (value: CompactDecisionTargetInput) => CompactDecisionNode,
63
+ ): CompactDecisionV1 {
64
+ const decision = projectCompactDecision(input.decision);
65
+ const source = input.decision;
66
+ const effective = resolvedNode(source?.effectiveTarget, resolve);
67
+ const persisted = resolvedNode(source?.persistedTarget, resolve);
68
+ applyEffectiveTarget(decision, effective);
69
+ applyPersistedTarget(
70
+ decision, persisted, source?.persistedResolutionStatus,
71
+ );
72
+ normalizeCompactDecisionEquivalence(
73
+ decision,
74
+ input.status,
75
+ target.decisionTarget,
76
+ sameCanonicalNode(effective, target),
77
+ persistedCanonical(persisted, effective, target),
78
+ source?.persistedResolutionStatus,
79
+ );
80
+ return decision;
81
+ }
@@ -0,0 +1,123 @@
1
+ import type { Db } from '../db/connection.js';
2
+ import type { ImplementationHint } from '../types.js';
3
+ import type { TraceGraphEdgeRow } from './012-trace-graph-lookups.js';
4
+
5
+ export interface ImplementationHintOptions {
6
+ implementationRepo?: string;
7
+ implementationHints?: ImplementationHint[];
8
+ }
9
+
10
+ export interface ImplementationScope {
11
+ repoId?: number;
12
+ files: Set<string>;
13
+ symbolId?: number;
14
+ edge?: TraceGraphEdgeRow;
15
+ }
16
+
17
+ interface HandlerScopeRow {
18
+ repoId?: number;
19
+ sourceFile?: string;
20
+ symbolId?: number;
21
+ }
22
+
23
+ function normalizeOperation(value: string | undefined): string | undefined {
24
+ if (!value) return undefined;
25
+ return value.startsWith('/') ? value.slice(1) : value;
26
+ }
27
+
28
+ function implementationEdge(
29
+ db: Db,
30
+ operationId: string,
31
+ ): TraceGraphEdgeRow | undefined {
32
+ return db.prepare(`SELECT * FROM graph_edges
33
+ WHERE edge_type='OPERATION_IMPLEMENTED_BY_HANDLER'
34
+ AND from_kind='operation' AND from_id=?
35
+ ORDER BY CASE status WHEN 'resolved' THEN 0
36
+ WHEN 'ambiguous' THEN 1 ELSE 2 END,id LIMIT 1`)
37
+ .get(operationId) as TraceGraphEdgeRow | undefined;
38
+ }
39
+
40
+ function handlerScopeRow(db: Db, methodId: string): HandlerScopeRow | undefined {
41
+ return db.prepare(`SELECT hc.repo_id repoId,hc.source_file sourceFile,
42
+ s.id symbolId
43
+ FROM handler_methods hm
44
+ JOIN handler_classes hc ON hc.id=hm.handler_class_id
45
+ LEFT JOIN symbols s ON s.repo_id=hc.repo_id
46
+ AND s.source_file=hc.source_file
47
+ AND s.qualified_name=hc.class_name || '.' || hm.method_name
48
+ AND s.start_line=hm.source_line
49
+ WHERE hm.id=?`).get(methodId) as HandlerScopeRow | undefined;
50
+ }
51
+
52
+ export function implementationScope(
53
+ db: Db,
54
+ operationId: string,
55
+ ): ImplementationScope {
56
+ const edge = implementationEdge(db, operationId);
57
+ if (!edge || edge.status !== 'resolved') return { files: new Set(), edge };
58
+ const row = handlerScopeRow(db, edge.to_id);
59
+ if (!row || typeof row.symbolId !== 'number')
60
+ return { repoId: row?.repoId, files: new Set(), edge };
61
+ return {
62
+ repoId: row.repoId,
63
+ files: new Set(row.sourceFile ? [row.sourceFile] : []),
64
+ symbolId: row.symbolId,
65
+ edge,
66
+ };
67
+ }
68
+
69
+ export function handlerScope(
70
+ db: Db,
71
+ methodId: string,
72
+ ): Omit<ImplementationScope, 'edge'> | undefined {
73
+ const row = handlerScopeRow(db, methodId);
74
+ if (!row || typeof row.symbolId !== 'number') return undefined;
75
+ return {
76
+ repoId: row.repoId,
77
+ files: new Set(row.sourceFile ? [row.sourceFile] : []),
78
+ symbolId: row.symbolId,
79
+ };
80
+ }
81
+
82
+ export function handlerFilesForOperation(
83
+ db: Db,
84
+ operationId: string,
85
+ ): Set<string> {
86
+ const op = db.prepare(`SELECT o.operation_name operationName,
87
+ o.operation_path operationPath,s.repo_id repoId
88
+ FROM cds_operations o JOIN cds_services s ON s.id=o.service_id
89
+ WHERE o.id=?`).get(operationId) as {
90
+ operationName?: string;
91
+ operationPath?: string;
92
+ repoId?: number;
93
+ } | undefined;
94
+ if (!op) return new Set();
95
+ const operation = normalizeOperation(op.operationPath ?? op.operationName);
96
+ const rows = handlerFileRows(db, op.repoId, operation, op.operationName);
97
+ return new Set(rows.flatMap((row) =>
98
+ typeof row.sourceFile === 'string' ? [row.sourceFile] : []));
99
+ }
100
+
101
+ function handlerFileRows(
102
+ db: Db,
103
+ repoId: number | undefined,
104
+ operation: string | undefined,
105
+ operationName: string | undefined,
106
+ ): Array<{ sourceFile?: string }> {
107
+ return db.prepare(`SELECT DISTINCT hc.source_file sourceFile
108
+ FROM handler_classes hc
109
+ JOIN handler_methods hm ON hm.handler_class_id=hc.id
110
+ WHERE hc.repo_id=?
111
+ AND COALESCE(json_extract(hm.decorator_resolution_json,'$.handlerKind'),
112
+ CASE WHEN hm.decorator_kind='Event' THEN 'event'
113
+ WHEN hm.decorator_kind IN ('Action','Func','On') THEN 'operation'
114
+ ELSE 'unsupported' END)='operation'
115
+ AND COALESCE(json_extract(hm.decorator_resolution_json,'$.executable'),
116
+ CASE WHEN hm.decorator_kind IN ('Action','Func','On')
117
+ THEN 1 ELSE 0 END)=1
118
+ AND (hm.decorator_value=? OR hm.method_name=?
119
+ OR hm.decorator_value=?)`)
120
+ .all(repoId, operation, operation, operationName) as Array<{
121
+ sourceFile?: string;
122
+ }>;
123
+ }