@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.
- package/CHANGELOG.md +13 -0
- package/README.md +16 -14
- package/TECHNICAL-NOTE.md +18 -6
- package/dist/{chunk-YZJKE5UX.js → chunk-LFH7C46B.js} +4290 -1261
- package/dist/chunk-LFH7C46B.js.map +1 -0
- package/dist/cli.js +435 -515
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +45 -7
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/src/cli/000-clean.ts +82 -0
- package/src/cli/001-doctor-projection.ts +140 -0
- package/src/cli/doctor.ts +20 -3
- package/src/cli.ts +75 -57
- package/src/db/connection.ts +1 -1
- package/src/db/migrations.ts +5 -3
- package/src/db/repositories.ts +130 -24
- package/src/db/schema.ts +7 -2
- package/src/indexer/repository-indexer.ts +57 -29
- package/src/indexer/workspace-indexer.ts +84 -6
- package/src/linker/000-implementation-candidates.ts +641 -0
- package/src/linker/001-implementation-evidence-projection.ts +119 -0
- package/src/linker/002-call-evidence.ts +226 -0
- package/src/linker/cross-repo-linker.ts +27 -441
- package/src/linker/dynamic-edge-resolver.ts +35 -0
- package/src/linker/external-http-target.ts +24 -3
- package/src/linker/helper-package-linker.ts +18 -2
- package/src/linker/service-resolver.ts +45 -2
- package/src/output/doctor-output.ts +13 -4
- package/src/output/table-output.ts +33 -5
- package/src/parsers/cds-parser.ts +8 -2
- package/src/parsers/decorator-parser.ts +382 -48
- package/src/parsers/handler-registration-parser.ts +38 -21
- package/src/parsers/imported-wrapper-parser.ts +18 -5
- package/src/parsers/outbound-call-parser.ts +25 -8
- package/src/parsers/package-json-parser.ts +36 -11
- package/src/parsers/service-binding-parser-helpers.ts +8 -1
- package/src/parsers/service-binding-parser.ts +6 -3
- package/src/parsers/symbol-parser.ts +13 -3
- package/src/parsers/ts-project.ts +54 -0
- package/src/trace/000-dynamic-target-types.ts +96 -0
- package/src/trace/001-dynamic-identity.ts +308 -0
- package/src/trace/002-trace-diagnostics.ts +54 -0
- package/src/trace/003-dynamic-references.ts +283 -0
- package/src/trace/004-dynamic-candidate-sources.ts +155 -0
- package/src/trace/005-implementation-selection.ts +187 -0
- package/src/trace/006-contextual-projection.ts +30 -0
- package/src/trace/007-implementation-start-diagnostic.ts +61 -0
- package/src/trace/dynamic-targets.ts +582 -306
- package/src/trace/evidence.ts +331 -46
- package/src/trace/implementation-hints.ts +148 -8
- package/src/trace/selectors.ts +551 -3
- package/src/trace/trace-engine.ts +129 -135
- package/src/types.ts +27 -1
- package/src/utils/000-bounded-projection.ts +161 -0
- 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
|
+
}
|