@saptools/service-flow 0.1.51 → 0.1.53

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 +13 -0
  2. package/README.md +16 -14
  3. package/TECHNICAL-NOTE.md +18 -6
  4. package/dist/{chunk-YZJKE5UX.js → chunk-LFH7C46B.js} +4290 -1261
  5. package/dist/chunk-LFH7C46B.js.map +1 -0
  6. package/dist/cli.js +435 -515
  7. package/dist/cli.js.map +1 -1
  8. package/dist/index.d.ts +45 -7
  9. package/dist/index.js +1 -1
  10. package/package.json +1 -1
  11. package/src/cli/000-clean.ts +82 -0
  12. package/src/cli/001-doctor-projection.ts +140 -0
  13. package/src/cli/doctor.ts +20 -3
  14. package/src/cli.ts +75 -57
  15. package/src/db/connection.ts +1 -1
  16. package/src/db/migrations.ts +5 -3
  17. package/src/db/repositories.ts +130 -24
  18. package/src/db/schema.ts +7 -2
  19. package/src/indexer/repository-indexer.ts +57 -29
  20. package/src/indexer/workspace-indexer.ts +84 -6
  21. package/src/linker/000-implementation-candidates.ts +641 -0
  22. package/src/linker/001-implementation-evidence-projection.ts +119 -0
  23. package/src/linker/002-call-evidence.ts +226 -0
  24. package/src/linker/cross-repo-linker.ts +27 -441
  25. package/src/linker/dynamic-edge-resolver.ts +35 -0
  26. package/src/linker/external-http-target.ts +24 -3
  27. package/src/linker/helper-package-linker.ts +18 -2
  28. package/src/linker/service-resolver.ts +45 -2
  29. package/src/output/doctor-output.ts +13 -4
  30. package/src/output/table-output.ts +33 -5
  31. package/src/parsers/cds-parser.ts +8 -2
  32. package/src/parsers/decorator-parser.ts +382 -48
  33. package/src/parsers/handler-registration-parser.ts +38 -21
  34. package/src/parsers/imported-wrapper-parser.ts +18 -5
  35. package/src/parsers/outbound-call-parser.ts +25 -8
  36. package/src/parsers/package-json-parser.ts +36 -11
  37. package/src/parsers/service-binding-parser-helpers.ts +8 -1
  38. package/src/parsers/service-binding-parser.ts +6 -3
  39. package/src/parsers/symbol-parser.ts +13 -3
  40. package/src/parsers/ts-project.ts +54 -0
  41. package/src/trace/000-dynamic-target-types.ts +96 -0
  42. package/src/trace/001-dynamic-identity.ts +308 -0
  43. package/src/trace/002-trace-diagnostics.ts +54 -0
  44. package/src/trace/003-dynamic-references.ts +283 -0
  45. package/src/trace/004-dynamic-candidate-sources.ts +155 -0
  46. package/src/trace/005-implementation-selection.ts +187 -0
  47. package/src/trace/006-contextual-projection.ts +30 -0
  48. package/src/trace/007-implementation-start-diagnostic.ts +61 -0
  49. package/src/trace/dynamic-targets.ts +582 -306
  50. package/src/trace/evidence.ts +331 -46
  51. package/src/trace/implementation-hints.ts +148 -8
  52. package/src/trace/selectors.ts +551 -3
  53. package/src/trace/trace-engine.ts +129 -135
  54. package/src/types.ts +27 -1
  55. package/src/utils/000-bounded-projection.ts +161 -0
  56. package/dist/chunk-YZJKE5UX.js.map +0 -1
@@ -0,0 +1,641 @@
1
+ import type { Db } from '../db/connection.js';
2
+ import { implementationHintSuggestionProjection } from '../trace/implementation-hints.js';
3
+ import { normalizeDecoratorOperationSignal, normalizedOperationName } from './operation-decorator-normalizer.js';
4
+ import {
5
+ boundedImplementationEvidence,
6
+ boundedImplementationTargetIds,
7
+ } from './001-implementation-evidence-projection.js';
8
+
9
+ interface ImplementationCandidate extends Record<string, unknown> {
10
+ methodId: number;
11
+ registrations?: Array<Record<string, unknown>>;
12
+ score: number;
13
+ accepted: boolean;
14
+ acceptedReasons: string[];
15
+ rejectedReasons: string[];
16
+ }
17
+
18
+ interface ImplementationDecision {
19
+ candidates: ImplementationCandidate[];
20
+ accepted: ImplementationCandidate[];
21
+ selected: ImplementationCandidate[];
22
+ unique?: ImplementationCandidate;
23
+ evidence: Record<string, unknown>;
24
+ evidenceWithHints: Record<string, unknown>;
25
+ }
26
+
27
+ export function linkImplementations(
28
+ db: Db,
29
+ workspaceId: number,
30
+ generation: number,
31
+ ): { edgeCount: number; resolvedCount: number; ambiguousCount: number; unresolvedCount: number } {
32
+ const operations = workspaceOperations(db, workspaceId);
33
+ let edgeCount = 0;
34
+ let resolvedCount = 0;
35
+ let ambiguousCount = 0;
36
+ let unresolvedCount = 0;
37
+ for (const operation of operations) {
38
+ const decision = implementationDecision(db, workspaceId, operation, true);
39
+ if (decision.candidates.length === 0) continue;
40
+ const status = decision.unique ? 'resolved' : decision.accepted.length > 0
41
+ ? 'ambiguous'
42
+ : 'unresolved';
43
+ insertImplementationEdge(db, workspaceId, generation, operation, decision, status);
44
+ edgeCount += 1;
45
+ if (status === 'resolved') resolvedCount += 1;
46
+ else if (status === 'ambiguous') ambiguousCount += 1;
47
+ else unresolvedCount += 1;
48
+ }
49
+ return { edgeCount, resolvedCount, ambiguousCount, unresolvedCount };
50
+ }
51
+
52
+ export function canonicalImplementationEvidence(
53
+ db: Db,
54
+ operationId: string | number,
55
+ ): Record<string, unknown> | undefined {
56
+ const operation = operationById(db, operationId);
57
+ const workspaceId = numberValue(operation?.workspaceId);
58
+ if (!operation || workspaceId === undefined) return undefined;
59
+ return implementationDecision(db, workspaceId, operation, false).evidenceWithHints;
60
+ }
61
+
62
+ function workspaceOperations(db: Db, workspaceId: number): Array<Record<string, unknown>> {
63
+ const rows = db.prepare(`SELECT o.id operationId,o.operation_path operationPath,
64
+ o.operation_name operationName,o.provenance provenance,o.base_operation_id baseOperationId,
65
+ s.service_path servicePath,s.repo_id modelRepoId,r.name modelRepo,
66
+ r.package_name modelPackage,r.kind modelKind
67
+ FROM cds_operations o JOIN cds_services s ON s.id=o.service_id
68
+ JOIN repositories r ON r.id=s.repo_id WHERE r.workspace_id=?`).all(workspaceId);
69
+ return rows;
70
+ }
71
+
72
+ function operationById(
73
+ db: Db,
74
+ operationId: string | number,
75
+ ): Record<string, unknown> | undefined {
76
+ const row = db.prepare(`SELECT o.id operationId,o.operation_path operationPath,
77
+ o.operation_name operationName,o.provenance provenance,o.base_operation_id baseOperationId,
78
+ s.service_path servicePath,s.repo_id modelRepoId,r.name modelRepo,
79
+ r.package_name modelPackage,r.kind modelKind,r.workspace_id workspaceId
80
+ FROM cds_operations o JOIN cds_services s ON s.id=o.service_id
81
+ JOIN repositories r ON r.id=s.repo_id WHERE o.id=?`).get(operationId);
82
+ return row;
83
+ }
84
+
85
+ function implementationDecision(
86
+ db: Db,
87
+ workspaceId: number,
88
+ operation: Record<string, unknown>,
89
+ recordDiagnostics: boolean,
90
+ ): ImplementationDecision {
91
+ const implementationContext = implementationContextForOperation(db, operation);
92
+ const candidates = rankedImplementationCandidates(
93
+ db, workspaceId, implementationContext, recordDiagnostics,
94
+ );
95
+ const accepted = candidates.filter((candidate) => candidate.accepted);
96
+ const topScore = accepted[0]?.score ?? 0;
97
+ const winners = accepted.filter((candidate) => candidate.score === topScore);
98
+ const duplicateFamilies = duplicatePackageFamilies(accepted);
99
+ const duplicatePackageAmbiguous = duplicateFamilies.length > 0
100
+ && !accepted.some(hasDirectOwnershipEvidence);
101
+ const selected = duplicatePackageAmbiguous ? accepted : winners;
102
+ const unique = !duplicatePackageAmbiguous && winners.length === 1
103
+ ? winners[0]
104
+ : undefined;
105
+ const ambiguityReasons = duplicatePackageAmbiguous
106
+ ? ['duplicate_package_name_candidates']
107
+ : winners.length > 1 ? ['multiple_equal_score_implementation_candidates'] : [];
108
+ const evidence = implementationEvidence(
109
+ operation, implementationContext, candidates, duplicateFamilies, ambiguityReasons,
110
+ );
111
+ const hintProjection = implementationHintSuggestionProjection(evidence);
112
+ return {
113
+ candidates,
114
+ accepted,
115
+ selected,
116
+ unique,
117
+ evidence,
118
+ evidenceWithHints: unique
119
+ ? evidence
120
+ : {
121
+ ...evidence,
122
+ implementationHintSuggestions: hintProjection.suggestions,
123
+ implementationHintSuggestionCount: hintProjection.suggestionCount,
124
+ shownImplementationHintSuggestionCount: hintProjection.shownSuggestionCount,
125
+ omittedImplementationHintSuggestionCount: hintProjection.omittedSuggestionCount,
126
+ },
127
+ };
128
+ }
129
+
130
+ function insertImplementationEdge(
131
+ db: Db,
132
+ workspaceId: number,
133
+ generation: number,
134
+ operation: Record<string, unknown>,
135
+ decision: ImplementationDecision,
136
+ status: 'resolved' | 'ambiguous' | 'unresolved',
137
+ ): void {
138
+ const targetCandidates = status === 'unresolved'
139
+ ? decision.candidates
140
+ : decision.selected;
141
+ const targetProjection = boundedImplementationTargetIds(targetCandidates);
142
+ const targetIds = targetProjection.items;
143
+ const toId = status === 'resolved'
144
+ ? graphId(decision.unique?.methodId)
145
+ : targetIds.join(',');
146
+ const reason = status === 'unresolved'
147
+ ? 'No implementation candidate passed policy'
148
+ : status === 'ambiguous'
149
+ ? 'Ambiguous registered handler implementation candidates'
150
+ : null;
151
+ db.prepare(`INSERT INTO graph_edges(workspace_id,edge_type,status,from_kind,from_id,
152
+ to_kind,to_id,confidence,evidence_json,is_dynamic,unresolved_reason,generation)
153
+ VALUES(?,?,?,?,?,?,?,?,?,?,?,?)`).run(
154
+ workspaceId,
155
+ 'OPERATION_IMPLEMENTED_BY_HANDLER',
156
+ status,
157
+ 'operation',
158
+ graphId(operation.operationId),
159
+ status === 'resolved' ? 'handler_method' : 'handler_method_candidates',
160
+ toId,
161
+ status === 'resolved' ? 0.95 : status === 'ambiguous' ? 0.5 : 0,
162
+ JSON.stringify(boundedImplementationEvidence(
163
+ decision.evidenceWithHints, targetProjection.totalCount,
164
+ )),
165
+ 0,
166
+ reason,
167
+ generation,
168
+ );
169
+ }
170
+
171
+ function implementationEvidence(
172
+ operation: Record<string, unknown>,
173
+ context: Record<string, unknown>,
174
+ candidates: ImplementationCandidate[],
175
+ duplicateFamilies: Array<Record<string, unknown>>,
176
+ ambiguityReasons: string[],
177
+ ): Record<string, unknown> {
178
+ return {
179
+ servicePath: operation.servicePath,
180
+ operationPath: operation.operationPath,
181
+ operationName: operation.operationName,
182
+ modelPackage: {
183
+ id: operation.modelRepoId,
184
+ name: operation.modelRepo,
185
+ packageName: operation.modelPackage,
186
+ },
187
+ implementationSource: context.operationId === operation.operationId
188
+ ? 'direct_or_concrete_override'
189
+ : 'inherited_from_base_operation',
190
+ baseOperationId: operation.baseOperationId,
191
+ implementationOperationId: context.operationId,
192
+ ambiguityReasons,
193
+ candidateFamilies: duplicateFamilies,
194
+ candidates: candidates.map((candidate, index) => candidateEvidence(candidate, index + 1)),
195
+ };
196
+ }
197
+
198
+
199
+ function implementationContextForOperation(
200
+ db: Db,
201
+ operation: Record<string, unknown>,
202
+ ): Record<string, unknown> {
203
+ if (operation.provenance !== 'inherited' || !operation.baseOperationId) return operation;
204
+ const base = db.prepare(`SELECT o.id operationId,o.operation_path operationPath,
205
+ o.operation_name operationName,o.provenance provenance,o.base_operation_id baseOperationId,
206
+ s.service_path servicePath,s.repo_id modelRepoId,r.name modelRepo,
207
+ r.package_name modelPackage,r.kind modelKind
208
+ FROM cds_operations o JOIN cds_services s ON s.id=o.service_id
209
+ JOIN repositories r ON r.id=s.repo_id WHERE o.id=?`).get(operation.baseOperationId);
210
+ return base ? {
211
+ ...base,
212
+ effectiveOperationId: operation.operationId,
213
+ effectiveServicePath: operation.servicePath,
214
+ effectiveOperationPath: operation.operationPath,
215
+ } : operation;
216
+ }
217
+
218
+ function rankedImplementationCandidates(
219
+ db: Db,
220
+ workspaceId: number,
221
+ operation: Record<string, unknown>,
222
+ recordDiagnostics: boolean,
223
+ ): ImplementationCandidate[] {
224
+ const rows = implementationCandidates(db, workspaceId, operation);
225
+ if (recordDiagnostics) recordRegistrationInvariantDiagnostics(
226
+ db, rows.filter((row) => !validRegistrationPair(row)),
227
+ );
228
+ return deduplicateCandidates(rows.filter(validRegistrationPair)
229
+ .map((row) => scoreImplementationCandidate(row, operation)))
230
+ .sort((left, right) => right.score - left.score
231
+ || String(left.className).localeCompare(String(right.className))
232
+ || left.methodId - right.methodId);
233
+ }
234
+
235
+ function validRegistrationPair(row: Record<string, unknown>): boolean {
236
+ if (row.registrationHandlerClassId === null || row.registrationHandlerClassId === undefined)
237
+ return registrationPairingStrategy(row) !== 'unproven';
238
+ return Number(row.registrationHandlerClassId) === Number(row.classId);
239
+ }
240
+
241
+ function registrationPairingStrategy(row: Record<string, unknown>): string {
242
+ if (row.registrationHandlerClassId !== null && row.registrationHandlerClassId !== undefined)
243
+ return 'exact_handler_class_id';
244
+ if (Number(row.applicationRepoId) === Number(row.handlerRepoId))
245
+ return 'same_repository_class_name_fallback';
246
+ const source = stringValue(row.importSource);
247
+ const separator = source?.lastIndexOf('#') ?? -1;
248
+ if (!source || separator <= 0) return 'unproven';
249
+ const moduleName = source.slice(0, separator);
250
+ const importedName = source.slice(separator + 1);
251
+ const matchesClass = importedName === row.className
252
+ || (importedName === 'default' && row.registrationClassName === row.className);
253
+ return moduleName === row.handlerPackage && matchesClass
254
+ ? 'explicit_package_import'
255
+ : 'unproven';
256
+ }
257
+
258
+ function recordRegistrationInvariantDiagnostics(
259
+ db: Db,
260
+ rows: Array<Record<string, unknown>>,
261
+ ): void {
262
+ const insert = db.prepare(`INSERT INTO diagnostics(repo_id,severity,code,message,
263
+ source_file,source_line)
264
+ SELECT ?,'error','handler_registration_class_mismatch',
265
+ 'Implementation candidate registration did not match its persisted handler class id',?,?
266
+ WHERE NOT EXISTS (SELECT 1 FROM diagnostics WHERE repo_id=?
267
+ AND code='handler_registration_class_mismatch' AND source_file=? AND source_line=?)`);
268
+ for (const row of rows) insert.run(
269
+ row.applicationRepoId, row.registrationFile, row.registrationLine,
270
+ row.applicationRepoId, row.registrationFile, row.registrationLine,
271
+ );
272
+ }
273
+
274
+ function deduplicateCandidates(rows: ImplementationCandidate[]): ImplementationCandidate[] {
275
+ const merged = new Map<string, ImplementationCandidate>();
276
+ for (const row of rows) {
277
+ const key = [row.methodId, row.classId, row.handlerRepoId].join(':');
278
+ const registration = registrationEvidence(row);
279
+ const existing = merged.get(key);
280
+ if (!existing) {
281
+ merged.set(key, { ...row, registrations: [registration] });
282
+ continue;
283
+ }
284
+ existing.registrations = uniqueRegistrations([
285
+ ...(existing.registrations ?? []), registration,
286
+ ]);
287
+ existing.score = Math.max(existing.score, row.score);
288
+ existing.accepted = existing.accepted || row.accepted;
289
+ existing.acceptedReasons = [...new Set([...existing.acceptedReasons, ...row.acceptedReasons])];
290
+ existing.rejectedReasons = [...new Set([...existing.rejectedReasons, ...row.rejectedReasons])];
291
+ }
292
+ return [...merged.values()];
293
+ }
294
+
295
+ function registrationEvidence(row: Record<string, unknown>): Record<string, unknown> {
296
+ return {
297
+ id: row.registrationId,
298
+ handlerClassId: row.registrationHandlerClassId,
299
+ file: row.registrationFile,
300
+ line: row.registrationLine,
301
+ kind: row.registrationKind,
302
+ importSource: row.importSource,
303
+ pairingStrategy: registrationPairingStrategy(row),
304
+ };
305
+ }
306
+
307
+ function uniqueRegistrations(rows: Array<Record<string, unknown>>): Array<Record<string, unknown>> {
308
+ const seen = new Set<string>();
309
+ return rows.filter((row) => {
310
+ const key = JSON.stringify(row);
311
+ if (seen.has(key)) return false;
312
+ seen.add(key);
313
+ return true;
314
+ });
315
+ }
316
+
317
+ function duplicatePackageFamilies(candidates: ImplementationCandidate[]): Array<Record<string, unknown>> {
318
+ const byPackage = new Map<string, ImplementationCandidate[]>();
319
+ for (const candidate of candidates) {
320
+ const packageName = stringValue(candidate.handlerPackage);
321
+ if (packageName) byPackage.set(packageName, [
322
+ ...(byPackage.get(packageName) ?? []), candidate,
323
+ ]);
324
+ }
325
+ return [...byPackage.entries()].filter(([, rows]) =>
326
+ new Set(rows.map((row) => Number(row.handlerRepoId))).size > 1)
327
+ .map(([packageName, rows]) => ({
328
+ reason: 'duplicate_package_name_candidates',
329
+ packageName,
330
+ count: rows.length,
331
+ repositories: rows.map((row) => row.handlerRepo).sort(),
332
+ }));
333
+ }
334
+
335
+ function hasDirectOwnershipEvidence(candidate: ImplementationCandidate): boolean {
336
+ return candidate.acceptedReasons.some((reason) => [
337
+ 'model package equals registration package',
338
+ 'model package equals handler package',
339
+ 'registration package contains exact local service path',
340
+ ].includes(reason));
341
+ }
342
+
343
+ function implementationCandidates(
344
+ db: Db,
345
+ workspaceId: number,
346
+ operation: Record<string, unknown>,
347
+ ): Array<Record<string, unknown>> {
348
+ const modelRepoGraphId = graphId(operation.modelRepoId);
349
+ return db.prepare(`SELECT DISTINCT
350
+ hm.id methodId,hm.method_name methodName,hm.decorator_value decoratorValue,
351
+ hm.decorator_raw_expression decoratorRawExpression,
352
+ hm.decorator_resolution_json decoratorResolutionJson,hc.id classId,
353
+ hc.class_name className,hc.source_file sourceFile,hc.source_line sourceLine,
354
+ hr.id registrationId,hr.handler_class_id registrationHandlerClassId,
355
+ hr.class_name registrationClassName,hr.repo_id applicationRepoId,
356
+ hr.registration_file registrationFile,hr.registration_line registrationLine,
357
+ hr.registration_kind registrationKind,hr.import_source importSource,
358
+ handlerRepo.id handlerRepoId,handlerRepo.name handlerRepo,
359
+ handlerRepo.package_name handlerPackage,appRepo.name applicationRepo,
360
+ appRepo.package_name applicationPackage,? modelRepoId,? modelRepo,? modelPackage,
361
+ ? modelKind,? servicePath,? operationPath,? operationName,
362
+ CASE WHEN appRepo.id=? THEN 1 ELSE 0 END modelIsApplicationRepo,
363
+ CASE WHEN handlerRepo.id=? THEN 1 ELSE 0 END modelIsHandlerRepo,
364
+ CASE WHEN appRepo.id=handlerRepo.id THEN 1 ELSE 0 END sameRepoRegistration,
365
+ CASE WHEN EXISTS (SELECT 1 FROM cds_services localService
366
+ WHERE localService.repo_id=appRepo.id AND localService.service_path=?)
367
+ THEN 1 ELSE 0 END localServicePathMatch,
368
+ CASE WHEN EXISTS (SELECT 1 FROM cds_services localService
369
+ WHERE localService.repo_id=appRepo.id) THEN 1 ELSE 0 END applicationHasLocalServices,
370
+ CASE WHEN EXISTS (SELECT 1 FROM handler_registrations localReg
371
+ JOIN handler_classes localClass ON ((localReg.handler_class_id IS NOT NULL
372
+ AND localClass.id=localReg.handler_class_id) OR (localReg.handler_class_id IS NULL
373
+ AND localClass.class_name=localReg.class_name AND localClass.repo_id=localReg.repo_id))
374
+ JOIN handler_methods localMethod ON localMethod.handler_class_id=localClass.id
375
+ WHERE localReg.repo_id=appRepo.id
376
+ AND COALESCE(json_extract(localMethod.decorator_resolution_json,'$.handlerKind'),
377
+ CASE WHEN localMethod.decorator_kind='Event' THEN 'event'
378
+ WHEN localMethod.decorator_kind IN ('Action','Func','On') THEN 'operation'
379
+ ELSE 'unsupported' END)='operation'
380
+ AND COALESCE(json_extract(localMethod.decorator_resolution_json,'$.executable'),
381
+ CASE WHEN localMethod.decorator_kind IN ('Action','Func','On') THEN 1 ELSE 0 END)=1
382
+ AND (localMethod.decorator_value=? OR localMethod.decorator_value=?
383
+ OR localMethod.method_name=? OR localMethod.decorator_raw_expression LIKE ?))
384
+ THEN 1 ELSE 0 END applicationHasLocalRegistrationForOperation,
385
+ CASE WHEN EXISTS (SELECT 1 FROM graph_edges dep
386
+ WHERE dep.edge_type='REPO_IMPORTS_HELPER_PACKAGE' AND dep.status='resolved'
387
+ AND dep.from_kind='repo' AND dep.from_id=CAST(appRepo.id AS TEXT)
388
+ AND dep.to_id=?) THEN 1 ELSE 0 END appDependsOnModel,
389
+ CASE WHEN EXISTS (SELECT 1 FROM graph_edges dep
390
+ WHERE dep.edge_type='REPO_IMPORTS_HELPER_PACKAGE' AND dep.status='resolved'
391
+ AND dep.from_kind='repo' AND dep.from_id=CAST(appRepo.id AS TEXT)
392
+ AND dep.to_id=CAST(handlerRepo.id AS TEXT)) THEN 1 ELSE 0 END appDependsOnHandler,
393
+ CASE WHEN EXISTS (SELECT 1 FROM graph_edges dep
394
+ WHERE dep.edge_type='REPO_IMPORTS_HELPER_PACKAGE' AND dep.status='resolved'
395
+ AND dep.from_kind='repo' AND dep.from_id=CAST(handlerRepo.id AS TEXT)
396
+ AND dep.to_id=?) THEN 1 ELSE 0 END handlerDependsOnModel
397
+ FROM handler_methods hm JOIN handler_classes hc ON hc.id=hm.handler_class_id
398
+ JOIN repositories handlerRepo ON handlerRepo.id=hc.repo_id
399
+ JOIN handler_registrations hr ON ((hr.handler_class_id IS NOT NULL
400
+ AND hr.handler_class_id=hc.id) OR (hr.handler_class_id IS NULL AND
401
+ ((hr.class_name=hc.class_name AND hr.repo_id=hc.repo_id) OR
402
+ (instr(hr.import_source,'#')>1 AND substr(hr.import_source,1,
403
+ instr(hr.import_source,'#')-1)=handlerRepo.package_name AND
404
+ (substr(hr.import_source,instr(hr.import_source,'#')+1)=hc.class_name OR
405
+ (substr(hr.import_source,instr(hr.import_source,'#')+1)='default'
406
+ AND hr.class_name=hc.class_name))))))
407
+ JOIN repositories appRepo ON appRepo.id=hr.repo_id
408
+ WHERE appRepo.workspace_id=?
409
+ AND COALESCE(json_extract(hm.decorator_resolution_json,'$.handlerKind'),
410
+ CASE WHEN hm.decorator_kind='Event' THEN 'event'
411
+ WHEN hm.decorator_kind IN ('Action','Func','On') THEN 'operation'
412
+ ELSE 'unsupported' END)='operation'
413
+ AND COALESCE(json_extract(hm.decorator_resolution_json,'$.executable'),
414
+ CASE WHEN hm.decorator_kind IN ('Action','Func','On') THEN 1 ELSE 0 END)=1
415
+ AND (hm.decorator_value=? OR hm.decorator_value=? OR hm.method_name=?
416
+ OR hm.decorator_raw_expression LIKE ?)`).all(
417
+ operation.modelRepoId, operation.modelRepo, operation.modelPackage, operation.modelKind,
418
+ operation.servicePath, operation.operationPath, operation.operationName,
419
+ operation.modelRepoId, operation.modelRepoId, operation.servicePath,
420
+ normalizedOperation(String(operation.operationPath ?? '')),
421
+ operation.operationName, operation.operationName,
422
+ `%${upperFirst(normalizedOperation(String(operation.operationPath
423
+ ?? operation.operationName ?? '')))}%`,
424
+ modelRepoGraphId, modelRepoGraphId, workspaceId,
425
+ normalizedOperation(String(operation.operationPath ?? '')),
426
+ operation.operationName, operation.operationName,
427
+ `%${upperFirst(normalizedOperation(String(operation.operationPath
428
+ ?? operation.operationName ?? '')))}%`,
429
+ );
430
+ }
431
+
432
+ function scoreImplementationCandidate(
433
+ row: Record<string, unknown>,
434
+ operation: Record<string, unknown>,
435
+ ): ImplementationCandidate {
436
+ const acceptedReasons: string[] = [];
437
+ const rejectedReasons: string[] = [];
438
+ const methodSignal = implementationMethodSignal(row, operation);
439
+ acceptedReasons.push(...methodSignal.acceptedReasons);
440
+ rejectedReasons.push(...methodSignal.rejectedReasons);
441
+ const signals = implementationOwnershipSignals(row, methodSignal.matches);
442
+ acceptedReasons.push(...signals.acceptedReasons);
443
+ rejectedReasons.push(...signals.rejectedReasons);
444
+ const accepted = methodSignal.matches && !methodSignal.contradicted
445
+ && !signals.contradicted && signals.hasOwnership;
446
+ if (!accepted && rejectedReasons.length === 0)
447
+ rejectedReasons.push('candidate did not meet implementation ownership policy');
448
+ return {
449
+ ...row,
450
+ methodId: Number(row.methodId),
451
+ score: signals.score,
452
+ accepted,
453
+ acceptedReasons,
454
+ rejectedReasons,
455
+ };
456
+ }
457
+
458
+ function implementationOwnershipSignals(
459
+ row: Record<string, unknown>,
460
+ methodMatches: boolean,
461
+ ): { score: number; hasOwnership: boolean; contradicted: boolean; acceptedReasons: string[]; rejectedReasons: string[] } {
462
+ const acceptedReasons: string[] = [];
463
+ const rejectedReasons: string[] = [];
464
+ const modelIsApplicationRepo = flag(row.modelIsApplicationRepo);
465
+ const modelIsHandlerRepo = flag(row.modelIsHandlerRepo);
466
+ const localServicePathMatch = flag(row.localServicePathMatch);
467
+ const applicationHasLocalServices = flag(row.applicationHasLocalServices);
468
+ const appDependsOnModel = flag(row.appDependsOnModel);
469
+ const appDependsOnHandler = flag(row.appDependsOnHandler);
470
+ const handlerDependsOnModel = flag(row.handlerDependsOnModel);
471
+ const importSource = Boolean(stringValue(row.importSource));
472
+ const sameRepoRegistration = flag(row.sameRepoRegistration);
473
+ const modelOriented = row.modelKind === 'cap-db-model'
474
+ || !flag(row.applicationHasLocalRegistrationForOperation);
475
+ const helperOwned = modelOriented && methodMatches && sameRepoRegistration && importSource
476
+ && !applicationHasLocalServices && !modelIsApplicationRepo && !modelIsHandlerRepo
477
+ && !localServicePathMatch && !appDependsOnModel && !appDependsOnHandler && !handlerDependsOnModel;
478
+ const score = ownershipScore({
479
+ modelIsApplicationRepo, modelIsHandlerRepo, localServicePathMatch,
480
+ appDependsOnModel, appDependsOnHandler, handlerDependsOnModel, helperOwned,
481
+ importSource,
482
+ }, acceptedReasons);
483
+ const hasOwnership = modelIsApplicationRepo || modelIsHandlerRepo;
484
+ const hasCrossPackage = appDependsOnModel
485
+ && (modelIsHandlerRepo || appDependsOnHandler || !importSource);
486
+ const contradicted = applicationHasLocalServices && !localServicePathMatch
487
+ && !appDependsOnModel && !hasOwnership;
488
+ if (applicationHasLocalServices && !localServicePathMatch && !appDependsOnModel
489
+ && !modelIsApplicationRepo)
490
+ rejectedReasons.push(`registration package has local services but none match ${String(row.servicePath ?? '')}`);
491
+ if (!hasOwnership && !localServicePathMatch && !hasCrossPackage && !helperOwned)
492
+ rejectedReasons.push('missing direct ownership, exact local service path, or validated cross-package dependency evidence');
493
+ return {
494
+ score,
495
+ hasOwnership: hasOwnership || localServicePathMatch || hasCrossPackage
496
+ || handlerDependsOnModel || helperOwned,
497
+ contradicted,
498
+ acceptedReasons,
499
+ rejectedReasons,
500
+ };
501
+ }
502
+
503
+ function ownershipScore(
504
+ signals: Record<string, boolean>,
505
+ acceptedReasons: string[],
506
+ ): number {
507
+ let score = 0;
508
+ const values: Array<[keyof typeof signals, number, string]> = [
509
+ ['modelIsApplicationRepo', 100, 'model package equals registration package'],
510
+ ['modelIsHandlerRepo', 100, 'model package equals handler package'],
511
+ ['localServicePathMatch', 80, 'registration package contains exact local service path'],
512
+ ['appDependsOnModel', 70, 'registration package depends on model package'],
513
+ ['appDependsOnHandler', 30, 'registration package depends on handler package'],
514
+ ['handlerDependsOnModel', 20, 'handler package depends on model package'],
515
+ ['helperOwned', 60, 'unique registered helper implementation for model-only operation'],
516
+ ['importSource', 10, 'registration imports handler class'],
517
+ ];
518
+ for (const [key, amount, reason] of values) {
519
+ if (!signals[key]) continue;
520
+ score += amount;
521
+ acceptedReasons.push(reason);
522
+ }
523
+ return score;
524
+ }
525
+
526
+ function candidateEvidence(candidate: ImplementationCandidate, rank: number): Record<string, unknown> {
527
+ return {
528
+ rank,
529
+ score: candidate.score,
530
+ accepted: candidate.accepted,
531
+ acceptedReasons: candidate.acceptedReasons,
532
+ rejectedReasons: candidate.rejectedReasons,
533
+ methodId: candidate.methodId,
534
+ classId: candidate.classId,
535
+ className: candidate.className,
536
+ sourceFile: candidate.sourceFile,
537
+ sourceLine: candidate.sourceLine,
538
+ decoratorResolution: objectJson(candidate.decoratorResolutionJson),
539
+ registration: registrationEvidence(candidate),
540
+ registrations: candidate.registrations ?? [],
541
+ registrationPairing: {
542
+ strategy: registrationPairingStrategy(candidate),
543
+ registrationId: candidate.registrationId,
544
+ registrationHandlerClassId: candidate.registrationHandlerClassId,
545
+ candidateHandlerClassId: candidate.classId,
546
+ invariantStatus: validRegistrationPair(candidate) ? 'valid' : 'invalid',
547
+ },
548
+ applicationPackage: {
549
+ id: candidate.applicationRepoId,
550
+ name: candidate.applicationRepo,
551
+ packageName: candidate.applicationPackage,
552
+ },
553
+ handlerPackage: {
554
+ id: candidate.handlerRepoId,
555
+ name: candidate.handlerRepo,
556
+ packageName: candidate.handlerPackage,
557
+ },
558
+ modelPackage: {
559
+ id: candidate.modelRepoId,
560
+ name: candidate.modelRepo,
561
+ packageName: candidate.modelPackage,
562
+ },
563
+ servicePath: candidate.servicePath,
564
+ operationPath: candidate.operationPath,
565
+ operationName: candidate.operationName,
566
+ signals: {
567
+ directOwnership: {
568
+ modelIsApplicationRepo: flag(candidate.modelIsApplicationRepo),
569
+ modelIsHandlerRepo: flag(candidate.modelIsHandlerRepo),
570
+ },
571
+ localServicePathMatch: flag(candidate.localServicePathMatch),
572
+ applicationHasLocalServices: flag(candidate.applicationHasLocalServices),
573
+ applicationHasLocalRegistrationForOperation:
574
+ flag(candidate.applicationHasLocalRegistrationForOperation),
575
+ appDependsOnModel: flag(candidate.appDependsOnModel),
576
+ appDependsOnHandler: flag(candidate.appDependsOnHandler),
577
+ handlerDependsOnModel: flag(candidate.handlerDependsOnModel),
578
+ sameRepoRegistration: flag(candidate.sameRepoRegistration),
579
+ },
580
+ };
581
+ }
582
+
583
+ function implementationMethodSignal(
584
+ row: Record<string, unknown>,
585
+ operation: Record<string, unknown>,
586
+ ): { matches: boolean; contradicted: boolean; acceptedReasons: string[]; rejectedReasons: string[] } {
587
+ const resolution = objectJson(row.decoratorResolutionJson) ?? {};
588
+ if (resolution.handlerKind && resolution.handlerKind !== 'operation')
589
+ return { matches: false, contradicted: true, acceptedReasons: [], rejectedReasons: ['non_operation_handler_kind'] };
590
+ if (resolution.executable === false)
591
+ return { matches: false, contradicted: true, acceptedReasons: [], rejectedReasons: ['handler_method_not_executable'] };
592
+ const operationName = normalizedOperationName(String(
593
+ operation.operationPath ?? operation.operationName ?? '',
594
+ ));
595
+ const decorator = normalizeDecoratorOperationSignal(
596
+ stringValue(row.decoratorValue), stringValue(row.decoratorRawExpression), operationName,
597
+ );
598
+ if (decorator.status === 'resolved' && decorator.operationName === operationName)
599
+ return { matches: true, contradicted: false, acceptedReasons: ['decorator targets operation'], rejectedReasons: [] };
600
+ if (decorator.status === 'resolved')
601
+ return { matches: false, contradicted: true, acceptedReasons: [], rejectedReasons: ['method_name_matches_but_decorator_targets_different_operation'] };
602
+ return String(row.methodName ?? '') === operationName
603
+ ? { matches: true, contradicted: false, acceptedReasons: ['method name fallback matched operation'], rejectedReasons: [] }
604
+ : { matches: false, contradicted: false, acceptedReasons: [], rejectedReasons: ['method name does not match operation'] };
605
+ }
606
+
607
+ function objectJson(value: unknown): Record<string, unknown> | undefined {
608
+ if (typeof value !== 'string' || value.length === 0) return undefined;
609
+ try {
610
+ const parsed = JSON.parse(value) as unknown;
611
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
612
+ ? parsed as Record<string, unknown>
613
+ : undefined;
614
+ } catch {
615
+ return undefined;
616
+ }
617
+ }
618
+
619
+ function upperFirst(value: string): string {
620
+ return value ? `${value[0]?.toUpperCase() ?? ''}${value.slice(1)}` : value;
621
+ }
622
+
623
+ function flag(value: unknown): boolean {
624
+ return Boolean(Number(value ?? 0));
625
+ }
626
+
627
+ function graphId(value: unknown): string {
628
+ return String(value ?? '');
629
+ }
630
+
631
+ function normalizedOperation(value: string): string {
632
+ return value.startsWith('/') ? value.slice(1) : value;
633
+ }
634
+
635
+ function stringValue(value: unknown): string | undefined {
636
+ return typeof value === 'string' ? value : undefined;
637
+ }
638
+
639
+ function numberValue(value: unknown): number | undefined {
640
+ return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
641
+ }