@saptools/service-flow 0.1.67 → 0.1.68

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 (80) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +20 -8
  3. package/TECHNICAL-NOTE.md +27 -0
  4. package/dist/{chunk-ZQABU7MR.js → chunk-AEM4JY22.js} +12676 -6714
  5. package/dist/chunk-AEM4JY22.js.map +1 -0
  6. package/dist/cli.js +2325 -413
  7. package/dist/cli.js.map +1 -1
  8. package/dist/index.d.ts +61 -2
  9. package/dist/index.js +1 -1
  10. package/package.json +1 -1
  11. package/src/cli/003-doctor-package-resolution.ts +68 -0
  12. package/src/cli/doctor.ts +25 -14
  13. package/src/db/000-call-fact-repository.ts +475 -342
  14. package/src/db/001-fact-lifecycle.ts +60 -30
  15. package/src/db/002-fact-json-inventory.ts +169 -0
  16. package/src/db/003-current-fact-semantics.ts +698 -0
  17. package/src/db/004-package-target-invalidation.ts +173 -0
  18. package/src/db/005-schema-structure.ts +201 -0
  19. package/src/db/006-relative-symbol-resolution.ts +443 -0
  20. package/src/db/007-package-fact-semantics.ts +573 -0
  21. package/src/db/008-relative-fact-semantics.ts +207 -0
  22. package/src/db/009-binding-fact-semantics.ts +347 -0
  23. package/src/db/010-package-symbol-surface-semantics.ts +320 -0
  24. package/src/db/011-symbol-call-semantics.ts +144 -0
  25. package/src/db/012-binding-reference-proof.ts +264 -0
  26. package/src/db/migrations.ts +16 -3
  27. package/src/db/repositories.ts +113 -6
  28. package/src/db/schema.ts +4 -2
  29. package/src/index.ts +12 -0
  30. package/src/indexer/repository-indexer.ts +72 -11
  31. package/src/indexer/workspace-indexer.ts +123 -32
  32. package/src/linker/003-package-import-symbol-resolver.ts +363 -131
  33. package/src/linker/004-event-subscription-handler-linker.ts +2 -0
  34. package/src/linker/005-odata-path-structure.ts +371 -0
  35. package/src/linker/cross-repo-linker.ts +2 -1
  36. package/src/linker/odata-path-normalizer.ts +273 -180
  37. package/src/linker/service-resolver.ts +197 -77
  38. package/src/parsers/002-symbol-import-bindings.ts +516 -0
  39. package/src/parsers/003-package-public-surface.ts +661 -0
  40. package/src/parsers/004-fact-identity.ts +108 -0
  41. package/src/parsers/005-event-subscription-facts.ts +281 -0
  42. package/src/parsers/006-binding-identity.ts +343 -0
  43. package/src/parsers/007-source-fact-reconciliation.ts +105 -0
  44. package/src/parsers/008-package-surface-publication.ts +82 -0
  45. package/src/parsers/009-symbol-call-facts.ts +528 -0
  46. package/src/parsers/010-package-public-surface-analysis.ts +352 -0
  47. package/src/parsers/011-binding-lexical-scope.ts +583 -0
  48. package/src/parsers/012-package-fact-contract.ts +306 -0
  49. package/src/parsers/013-executable-body-eligibility.ts +35 -0
  50. package/src/parsers/014-service-binding-helper-flow.ts +240 -0
  51. package/src/parsers/015-service-binding-collector.ts +693 -0
  52. package/src/parsers/016-local-symbol-reference.ts +261 -0
  53. package/src/parsers/017-symbol-derived-contexts.ts +268 -0
  54. package/src/parsers/018-package-commonjs-syntax.ts +142 -0
  55. package/src/parsers/019-binding-assignment-targets.ts +76 -0
  56. package/src/parsers/020-stable-local-value.ts +217 -0
  57. package/src/parsers/021-binding-visibility.ts +152 -0
  58. package/src/parsers/operation-path-analysis.ts +6 -1
  59. package/src/parsers/outbound-call-parser.ts +19 -6
  60. package/src/parsers/package-json-parser.ts +45 -3
  61. package/src/parsers/service-binding-parser-helpers.ts +86 -15
  62. package/src/parsers/service-binding-parser.ts +147 -597
  63. package/src/parsers/symbol-parser.ts +482 -353
  64. package/src/trace/002-trace-diagnostics.ts +36 -1
  65. package/src/trace/016-compact-projector.ts +16 -17
  66. package/src/trace/020-compact-field-projection.ts +48 -37
  67. package/src/trace/021-compact-decision-normalization.ts +105 -0
  68. package/src/trace/022-trace-fact-preflight.ts +21 -0
  69. package/src/trace/023-nested-event-scopes.ts +23 -0
  70. package/src/trace/024-compact-observation-decision.ts +76 -0
  71. package/src/trace/025-trace-implementation-scope.ts +123 -0
  72. package/src/trace/026-trace-start-scope.ts +335 -0
  73. package/src/trace/027-trace-scope-execution.ts +566 -0
  74. package/src/trace/028-trace-operation-execution.ts +336 -0
  75. package/src/trace/029-trace-start-implementation.ts +172 -0
  76. package/src/trace/trace-engine.ts +122 -624
  77. package/src/types.ts +56 -0
  78. package/src/utils/001-placeholders.ts +188 -10
  79. package/src/version.ts +1 -1
  80. 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(
@@ -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,25 @@ 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
+ return {
115
+ decisionTarget: projectCompactDecisionTarget(target.kind, target.id),
116
+ };
118
117
  }
119
118
 
120
119
  function resolveEndpoint(
@@ -17,8 +17,12 @@ import type {
17
17
  CompactStartV1,
18
18
  CompactStatusCountsV1,
19
19
  } from './014-compact-contract.js';
20
+ import {
21
+ compactMissingRemediation,
22
+ projectCompactMissingNames,
23
+ type CompactMissingNameProjection,
24
+ } from './021-compact-decision-normalization.js';
20
25
 
21
- const compactNameLimit = 8;
22
26
  const compactDiagnosticMessages: Readonly<Record<string, string>> = {
23
27
  schema_upgrade_required: 'The database schema must be upgraded before tracing.',
24
28
  reindex_required: 'Current analyzer facts are required before tracing.',
@@ -56,14 +60,10 @@ function resolutionDecision(input: CompactDecisionInput): CompactDecisionV1 {
56
60
  }
57
61
 
58
62
  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);
63
+ const projection = projectCompactMissingNames(
64
+ input.missingVariableNames, input.missingVariableCount,
65
+ );
66
+ applyMissingProjection(out, projection);
67
67
  }
68
68
 
69
69
  function addDynamicDecision(out: CompactDecisionV1, input: CompactDecisionInput): void {
@@ -117,14 +117,24 @@ function addRemediationDecision(
117
117
  out: CompactDecisionV1,
118
118
  input: CompactDecisionInput,
119
119
  ): void {
120
- const hint = input.remediationCode
121
- ? compactRemediationHint(input.remediationCode) : undefined;
120
+ const hint = decisionRemediation(input);
122
121
  if (!hint) return;
123
122
  out.remediationHint = hint;
124
123
  const total = Math.max(1, compactCount(input.remediationHintCount));
125
124
  out.omittedRemediationHintCount = Math.max(0, total - 1);
126
125
  }
127
126
 
127
+ function decisionRemediation(input: CompactDecisionInput): string | undefined {
128
+ if (input.remediationCode === 'provide_runtime_variables') {
129
+ const projection = projectCompactMissingNames(
130
+ input.missingVariableNames, input.missingVariableCount,
131
+ );
132
+ return compactMissingRemediation(projection, 'detailed trace edge');
133
+ }
134
+ return input.remediationCode
135
+ ? compactRemediationHint(input.remediationCode) : undefined;
136
+ }
137
+
128
138
  export function projectCompactDiagnostics(
129
139
  values: Array<Record<string, unknown>>,
130
140
  ): CompactProjectedDiagnostic[] {
@@ -200,15 +210,6 @@ export function compactStatusTotal(counts: CompactStatusCountsV1): number {
200
210
  + counts.ambiguous + counts.unresolved + counts.cycle;
201
211
  }
202
212
 
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
213
  function compactBlockingDiagnostic(item: CompactDiagnosticRowV1): boolean {
213
214
  if (item[1] === 'error') return true;
214
215
  return item[2] === 'schema_upgrade_required'
@@ -242,9 +243,9 @@ function compactDiagnosticDetails(
242
243
  const out: CompactDiagnosticDetailsV1 = {};
243
244
  const reasonCode = compactSafeCode(value.reasonCode);
244
245
  if (reasonCode) out.reasonCode = reasonCode;
245
- addDiagnosticNames(out, value);
246
+ if (code === 'trace_runtime_variables_missing') addDiagnosticNames(out, value);
246
247
  addDiagnosticCounts(out, value);
247
- const hint = compactDiagnosticRemediation(code);
248
+ const hint = compactDiagnosticRemediation(code, out);
248
249
  if (hint) {
249
250
  out.remediationHint = hint;
250
251
  out.omittedHintCount = Math.max(
@@ -258,14 +259,21 @@ function addDiagnosticNames(
258
259
  out: CompactDiagnosticDetailsV1,
259
260
  value: Record<string, unknown>,
260
261
  ): 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);
262
+ const projection = projectCompactMissingNames(
263
+ compactStringArray(value.missingVariables), value.missingVariableCount,
264
+ );
265
+ applyMissingProjection(out, projection);
266
+ }
267
+
268
+ function applyMissingProjection(
269
+ out: CompactDecisionV1 | CompactDiagnosticDetailsV1,
270
+ projection: CompactMissingNameProjection,
271
+ ): void {
272
+ if (projection.names.length > 0) out.missingVariableNames = projection.names;
273
+ if (projection.total === 0) return;
274
+ out.missingVariableCount = projection.total;
275
+ out.shownMissingVariableCount = projection.shown;
276
+ out.omittedMissingVariableCount = projection.omitted;
269
277
  }
270
278
 
271
279
  function addDiagnosticCounts(
@@ -303,11 +311,19 @@ function compactDiagnosticSeverity(value: unknown): 'error' | 'warning' | 'info'
303
311
  return value === 'error' || value === 'warning' ? value : 'info';
304
312
  }
305
313
 
306
- function compactDiagnosticRemediation(code: string): string | undefined {
314
+ function compactDiagnosticRemediation(
315
+ code: string,
316
+ details: CompactDiagnosticDetailsV1,
317
+ ): string | undefined {
307
318
  if (code === 'schema_upgrade_required' || code === 'reindex_required')
308
319
  return compactRemediationHint('reindex_and_link');
309
320
  if (code === 'trace_runtime_variables_missing')
310
- return compactRemediationHint('provide_runtime_variables');
321
+ return compactMissingRemediation({
322
+ names: details.missingVariableNames ?? [],
323
+ total: details.missingVariableCount ?? 0,
324
+ shown: details.shownMissingVariableCount ?? 0,
325
+ omitted: details.omittedMissingVariableCount ?? 0,
326
+ }, 'detailed diagnostic');
311
327
  if (code === 'implementation_hint_mismatch')
312
328
  return compactRemediationHint('select_implementation');
313
329
  return undefined;
@@ -358,11 +374,6 @@ function compactSafeSourceFile(value: unknown): string | undefined {
358
374
  && !/^[a-z]+:\/\//i.test(value) && !/[\r\n]/.test(value) ? value : undefined;
359
375
  }
360
376
 
361
- function safeVariableNames(values: string[] | undefined): string[] {
362
- return compactSortedUnique((values ?? [])
363
- .filter((value) => /^[A-Za-z_$][\w$]*$/.test(value)));
364
- }
365
-
366
377
  function compactCount(value: unknown): number {
367
378
  return typeof value === 'number' && Number.isFinite(value)
368
379
  ? Math.max(0, Math.floor(value)) : 0;
@@ -0,0 +1,105 @@
1
+ import { compareBinary } from './010-traversal-scope.js';
2
+ import type {
3
+ CompactDecisionV1,
4
+ CompactStatus,
5
+ } from './014-compact-contract.js';
6
+
7
+ export const COMPACT_MISSING_NAME_LIMIT = 8;
8
+ export const COMPACT_MISSING_NAME_MAX_LENGTH = 160;
9
+
10
+ export interface CompactMissingNameProjection {
11
+ readonly names: string[];
12
+ readonly total: number;
13
+ readonly shown: number;
14
+ readonly omitted: number;
15
+ }
16
+
17
+ const identifier = '[A-Za-z_$][A-Za-z0-9_$]*';
18
+ const member = `(?:(?:\\.|\\?\\.)${identifier}|(?:\\[|\\?\\.\\[)(?:0|[1-9][0-9]*)\\])`;
19
+ const transform = '(?:(?:\\.|\\?\\.)(?:toLowerCase|toUpperCase|trim)\\(\\))?';
20
+ const safeMissingName = new RegExp(`^${identifier}(?:${member})*${transform}$`);
21
+
22
+ export function projectCompactMissingNames(
23
+ values: readonly string[] | undefined,
24
+ authoritativeCount: unknown,
25
+ ): CompactMissingNameProjection {
26
+ const raw = [...new Set((values ?? [])
27
+ .map((value) => value.trim())
28
+ .filter(Boolean))]
29
+ .sort(compareBinary);
30
+ const safe = raw.filter(isSafeCompactMissingName);
31
+ const names = safe.slice(0, COMPACT_MISSING_NAME_LIMIT);
32
+ const total = Math.max(compactCount(authoritativeCount), raw.length);
33
+ return {
34
+ names, total, shown: names.length,
35
+ omitted: Math.max(0, total - names.length),
36
+ };
37
+ }
38
+
39
+ export function isSafeCompactMissingName(value: string): boolean {
40
+ const normalized = value.trim();
41
+ return normalized.length > 0
42
+ && normalized.length <= COMPACT_MISSING_NAME_MAX_LENGTH
43
+ && !hasControlCharacter(normalized)
44
+ && safeMissingName.test(normalized);
45
+ }
46
+
47
+ export function normalizeCompactDecisionEquivalence(
48
+ decision: CompactDecisionV1,
49
+ status: CompactStatus,
50
+ canonicalTarget: string | undefined,
51
+ targetIdentityEqual = false,
52
+ persistedIdentityEqual = false,
53
+ ): void {
54
+ removeEquivalentPersistedDecision(
55
+ decision, status, canonicalTarget, persistedIdentityEqual,
56
+ );
57
+ if (decision.effectiveResolutionStatus === status)
58
+ delete decision.effectiveResolutionStatus;
59
+ if (targetIdentityEqual && canonicalTarget !== undefined
60
+ && decision.effectiveTarget === canonicalTarget)
61
+ delete decision.effectiveTarget;
62
+ }
63
+
64
+ export function compactMissingRemediation(
65
+ projection: CompactMissingNameProjection,
66
+ artifact: 'detailed diagnostic' | 'detailed trace edge',
67
+ ): string {
68
+ if (projection.total > 0 && projection.shown === projection.total)
69
+ return 'Provide the missing variable names listed in details.';
70
+ if (projection.shown > 0)
71
+ return `Provide the listed names; inspect the correlated ${artifact} for omitted names.`;
72
+ return `Inspect the correlated ${artifact} for missing variable names.`;
73
+ }
74
+
75
+ function removeEquivalentPersistedDecision(
76
+ decision: CompactDecisionV1,
77
+ status: CompactStatus,
78
+ canonicalTarget: string | undefined,
79
+ identityEqual: boolean,
80
+ ): void {
81
+ if (!identityEqual
82
+ || decision.persistedResolutionStatus !== status
83
+ || (decision.effectiveResolutionStatus !== undefined
84
+ && decision.persistedResolutionStatus
85
+ !== decision.effectiveResolutionStatus))
86
+ return;
87
+ if (!decision.persistedTarget || canonicalTarget === undefined
88
+ || decision.persistedTarget !== canonicalTarget)
89
+ return;
90
+ delete decision.persistedResolutionStatus;
91
+ delete decision.persistedTarget;
92
+ }
93
+
94
+ function compactCount(value: unknown): number {
95
+ return typeof value === 'number' && Number.isFinite(value)
96
+ ? Math.max(0, Math.floor(value)) : 0;
97
+ }
98
+
99
+ function hasControlCharacter(value: string): boolean {
100
+ for (let index = 0; index < value.length; index += 1) {
101
+ const code = value.charCodeAt(index);
102
+ if (code <= 31 || code === 127) return true;
103
+ }
104
+ return false;
105
+ }
@@ -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,76 @@
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
+ }
14
+
15
+ function resolvedNode(
16
+ value: CompactDecisionTargetInput | undefined,
17
+ resolve: (input: CompactDecisionTargetInput) => CompactDecisionNode,
18
+ ): CompactDecisionNode | undefined {
19
+ return value ? resolve(value) : undefined;
20
+ }
21
+
22
+ function applyEffectiveTarget(
23
+ decision: CompactDecisionV1,
24
+ effective: CompactDecisionNode | undefined,
25
+ ): void {
26
+ if (effective?.decisionTarget)
27
+ decision.effectiveTarget = effective.decisionTarget;
28
+ }
29
+
30
+ function applyPersistedTarget(
31
+ decision: CompactDecisionV1,
32
+ persisted: CompactDecisionNode | undefined,
33
+ status: string | undefined,
34
+ ): void {
35
+ if (persisted?.decisionTarget && status)
36
+ decision.persistedTarget = persisted.decisionTarget;
37
+ }
38
+
39
+ function sameCanonicalNode(
40
+ left: CompactDecisionNode | undefined,
41
+ right: CompactDecisionNode,
42
+ ): boolean {
43
+ return left?.key !== undefined && left.key === right.key;
44
+ }
45
+
46
+ function persistedCanonical(
47
+ persisted: CompactDecisionNode | undefined,
48
+ effective: CompactDecisionNode | undefined,
49
+ target: CompactDecisionNode,
50
+ ): boolean {
51
+ if (!sameCanonicalNode(persisted, target)) return false;
52
+ return effective?.key === undefined || effective.key === target.key;
53
+ }
54
+
55
+ export function projectObservationDecision(
56
+ input: CompactEdgeObservation,
57
+ target: CompactDecisionNode,
58
+ resolve: (value: CompactDecisionTargetInput) => CompactDecisionNode,
59
+ ): CompactDecisionV1 {
60
+ const decision = projectCompactDecision(input.decision);
61
+ const source = input.decision;
62
+ const effective = resolvedNode(source?.effectiveTarget, resolve);
63
+ const persisted = resolvedNode(source?.persistedTarget, resolve);
64
+ applyEffectiveTarget(decision, effective);
65
+ applyPersistedTarget(
66
+ decision, persisted, source?.persistedResolutionStatus,
67
+ );
68
+ normalizeCompactDecisionEquivalence(
69
+ decision,
70
+ input.status,
71
+ target.decisionTarget,
72
+ sameCanonicalNode(effective, target),
73
+ persistedCanonical(persisted, effective, target),
74
+ );
75
+ return decision;
76
+ }
@@ -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
+ }