@saptools/service-flow 0.1.67 → 0.1.69
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/README.md +27 -9
- package/TECHNICAL-NOTE.md +36 -0
- package/dist/chunk-3N3B5KHV.js +19596 -0
- package/dist/chunk-3N3B5KHV.js.map +1 -0
- package/dist/cli.js +2645 -521
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +67 -2
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/src/cli/001-index-summary.ts +22 -0
- package/src/cli/003-doctor-package-resolution.ts +68 -0
- package/src/cli/doctor.ts +25 -14
- package/src/cli.ts +151 -87
- package/src/db/000-call-fact-repository.ts +499 -340
- package/src/db/001-fact-lifecycle.ts +60 -30
- package/src/db/002-fact-json-inventory.ts +169 -0
- package/src/db/003-current-fact-semantics.ts +699 -0
- package/src/db/004-package-target-invalidation.ts +183 -0
- package/src/db/005-schema-structure.ts +201 -0
- package/src/db/006-relative-symbol-resolution.ts +464 -0
- package/src/db/007-package-fact-semantics.ts +573 -0
- package/src/db/008-relative-fact-semantics.ts +210 -0
- package/src/db/009-binding-fact-semantics.ts +352 -0
- package/src/db/010-package-symbol-surface-semantics.ts +320 -0
- package/src/db/011-symbol-call-semantics.ts +144 -0
- package/src/db/012-binding-reference-proof.ts +268 -0
- package/src/db/013-index-publication-failure.ts +91 -0
- package/src/db/014-binding-helper-provenance.ts +17 -0
- package/src/db/migrations.ts +16 -3
- package/src/db/repositories.ts +130 -6
- package/src/db/schema.ts +4 -2
- package/src/index.ts +12 -0
- package/src/indexer/cds-extension-resolver.ts +27 -3
- package/src/indexer/repository-indexer.ts +135 -13
- package/src/indexer/workspace-indexer.ts +237 -34
- package/src/linker/003-package-import-symbol-resolver.ts +363 -131
- package/src/linker/004-event-subscription-handler-linker.ts +34 -11
- package/src/linker/005-odata-path-structure.ts +371 -0
- package/src/linker/006-event-template-link.ts +72 -0
- package/src/linker/007-call-edge-insertion.ts +568 -0
- package/src/linker/cross-repo-linker.ts +4 -166
- package/src/linker/odata-path-normalizer.ts +273 -180
- package/src/linker/service-resolver.ts +197 -77
- package/src/parsers/000-direct-query-execution.ts +11 -0
- package/src/parsers/002-symbol-import-bindings.ts +516 -0
- package/src/parsers/003-package-public-surface.ts +661 -0
- package/src/parsers/004-fact-identity.ts +108 -0
- package/src/parsers/005-event-subscription-facts.ts +281 -0
- package/src/parsers/006-binding-identity.ts +348 -0
- package/src/parsers/007-source-fact-reconciliation.ts +105 -0
- package/src/parsers/008-package-surface-publication.ts +82 -0
- package/src/parsers/009-symbol-call-facts.ts +528 -0
- package/src/parsers/010-package-public-surface-analysis.ts +352 -0
- package/src/parsers/011-binding-lexical-scope.ts +583 -0
- package/src/parsers/012-package-fact-contract.ts +306 -0
- package/src/parsers/013-executable-body-eligibility.ts +35 -0
- package/src/parsers/014-service-binding-helper-flow.ts +306 -0
- package/src/parsers/015-service-binding-collector.ts +693 -0
- package/src/parsers/016-local-symbol-reference.ts +261 -0
- package/src/parsers/017-symbol-derived-contexts.ts +268 -0
- package/src/parsers/018-package-commonjs-syntax.ts +142 -0
- package/src/parsers/019-binding-assignment-targets.ts +76 -0
- package/src/parsers/020-stable-local-value.ts +217 -0
- package/src/parsers/021-binding-visibility.ts +168 -0
- package/src/parsers/022-outbound-expression-analysis.ts +700 -0
- package/src/parsers/023-outbound-call-classifier.ts +692 -0
- package/src/parsers/operation-path-analysis.ts +6 -1
- package/src/parsers/outbound-call-parser.ts +162 -512
- package/src/parsers/package-json-parser.ts +45 -3
- package/src/parsers/service-binding-parser-helpers.ts +86 -15
- package/src/parsers/service-binding-parser.ts +147 -597
- package/src/parsers/symbol-parser.ts +513 -352
- package/src/trace/002-trace-diagnostics.ts +36 -1
- package/src/trace/007-implementation-start-diagnostic.ts +1 -0
- package/src/trace/011-event-subscriber-traversal.ts +100 -8
- package/src/trace/013-trace-root-scopes.ts +2 -3
- package/src/trace/014-compact-contract.ts +6 -0
- package/src/trace/015-trace-edge-recorder.ts +61 -4
- package/src/trace/016-compact-projector.ts +15 -17
- package/src/trace/019-trace-edge-semantics.ts +6 -10
- package/src/trace/020-compact-field-projection.ts +122 -38
- package/src/trace/021-compact-decision-normalization.ts +171 -0
- package/src/trace/022-trace-fact-preflight.ts +21 -0
- package/src/trace/023-nested-event-scopes.ts +23 -0
- package/src/trace/024-compact-observation-decision.ts +81 -0
- package/src/trace/025-trace-implementation-scope.ts +123 -0
- package/src/trace/026-trace-start-scope.ts +336 -0
- package/src/trace/027-trace-scope-execution.ts +566 -0
- package/src/trace/028-trace-operation-execution.ts +336 -0
- package/src/trace/029-trace-start-implementation.ts +172 -0
- package/src/trace/030-event-runtime-resolution.ts +151 -0
- package/src/trace/031-local-call-expansion.ts +37 -0
- package/src/trace/implementation-hints.ts +9 -6
- package/src/trace/selectors.ts +1 -0
- package/src/trace/trace-engine.ts +122 -624
- package/src/types.ts +57 -0
- package/src/utils/001-placeholders.ts +188 -10
- package/src/version.ts +1 -1
- package/dist/chunk-ZQABU7MR.js +0 -12151
- package/dist/chunk-ZQABU7MR.js.map +0 -1
|
@@ -17,8 +17,15 @@ import type {
|
|
|
17
17
|
CompactStartV1,
|
|
18
18
|
CompactStatusCountsV1,
|
|
19
19
|
} from './014-compact-contract.js';
|
|
20
|
+
import {
|
|
21
|
+
compactMissingRemediation,
|
|
22
|
+
isSafeCompactReferenceName,
|
|
23
|
+
isSafeCompactSelectorSuggestion,
|
|
24
|
+
projectCompactReferenceGroup,
|
|
25
|
+
projectCompactMissingNames,
|
|
26
|
+
type CompactMissingNameProjection,
|
|
27
|
+
} from './021-compact-decision-normalization.js';
|
|
20
28
|
|
|
21
|
-
const compactNameLimit = 8;
|
|
22
29
|
const compactDiagnosticMessages: Readonly<Record<string, string>> = {
|
|
23
30
|
schema_upgrade_required: 'The database schema must be upgraded before tracing.',
|
|
24
31
|
reindex_required: 'Current analyzer facts are required before tracing.',
|
|
@@ -27,8 +34,19 @@ const compactDiagnosticMessages: Readonly<Record<string, string>> = {
|
|
|
27
34
|
implementation_hint_mismatch: 'The implementation hint did not select one implementation.',
|
|
28
35
|
selected_handler_provenance_mismatch: 'Selected handler provenance did not match its graph target.',
|
|
29
36
|
selected_handler_target_not_found: 'The selected handler target is not indexed.',
|
|
37
|
+
trace_start_ambiguous: 'The trace start selector is ambiguous.',
|
|
38
|
+
trace_start_not_found: 'The trace start selector did not match an indexed start.',
|
|
30
39
|
trace_start_implementation_unresolved: 'The trace start implementation is unresolved.',
|
|
31
40
|
};
|
|
41
|
+
const selectorDiagnosticCodes = new Set([
|
|
42
|
+
'handler_decorators_not_indexed',
|
|
43
|
+
'handler_methods_not_indexed',
|
|
44
|
+
'selector_repo_ambiguous',
|
|
45
|
+
'selector_repo_not_found',
|
|
46
|
+
'trace_start_ambiguous',
|
|
47
|
+
'trace_start_implementation_unresolved',
|
|
48
|
+
'trace_start_not_found',
|
|
49
|
+
]);
|
|
32
50
|
|
|
33
51
|
export function projectCompactDecision(
|
|
34
52
|
input: CompactDecisionInput | undefined,
|
|
@@ -56,14 +74,10 @@ function resolutionDecision(input: CompactDecisionInput): CompactDecisionV1 {
|
|
|
56
74
|
}
|
|
57
75
|
|
|
58
76
|
function addNameDecision(out: CompactDecisionV1, input: CompactDecisionInput): void {
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
if (total === 0) return;
|
|
64
|
-
out.missingVariableCount = total;
|
|
65
|
-
out.shownMissingVariableCount = names.length;
|
|
66
|
-
out.omittedMissingVariableCount = Math.max(0, total - names.length);
|
|
77
|
+
const projection = projectCompactMissingNames(
|
|
78
|
+
input.missingVariableNames, input.missingVariableCount,
|
|
79
|
+
);
|
|
80
|
+
applyMissingProjection(out, projection);
|
|
67
81
|
}
|
|
68
82
|
|
|
69
83
|
function addDynamicDecision(out: CompactDecisionV1, input: CompactDecisionInput): void {
|
|
@@ -87,6 +101,8 @@ function addImplementationDecision(
|
|
|
87
101
|
out.implementationGuided = input.implementationGuided;
|
|
88
102
|
if (input.implementationContextual !== undefined)
|
|
89
103
|
out.implementationContextual = input.implementationContextual;
|
|
104
|
+
if (input.tiedCandidateRepos)
|
|
105
|
+
out.tiedCandidateRepos = input.tiedCandidateRepos;
|
|
90
106
|
}
|
|
91
107
|
|
|
92
108
|
function addEventDecision(out: CompactDecisionV1, input: CompactDecisionInput): void {
|
|
@@ -117,14 +133,24 @@ function addRemediationDecision(
|
|
|
117
133
|
out: CompactDecisionV1,
|
|
118
134
|
input: CompactDecisionInput,
|
|
119
135
|
): void {
|
|
120
|
-
const hint = input
|
|
121
|
-
? compactRemediationHint(input.remediationCode) : undefined;
|
|
136
|
+
const hint = decisionRemediation(input);
|
|
122
137
|
if (!hint) return;
|
|
123
138
|
out.remediationHint = hint;
|
|
124
139
|
const total = Math.max(1, compactCount(input.remediationHintCount));
|
|
125
140
|
out.omittedRemediationHintCount = Math.max(0, total - 1);
|
|
126
141
|
}
|
|
127
142
|
|
|
143
|
+
function decisionRemediation(input: CompactDecisionInput): string | undefined {
|
|
144
|
+
if (input.remediationCode === 'provide_runtime_variables') {
|
|
145
|
+
const projection = projectCompactMissingNames(
|
|
146
|
+
input.missingVariableNames, input.missingVariableCount,
|
|
147
|
+
);
|
|
148
|
+
return compactMissingRemediation(projection, 'detailed trace edge');
|
|
149
|
+
}
|
|
150
|
+
return input.remediationCode
|
|
151
|
+
? compactRemediationHint(input.remediationCode) : undefined;
|
|
152
|
+
}
|
|
153
|
+
|
|
128
154
|
export function projectCompactDiagnostics(
|
|
129
155
|
values: Array<Record<string, unknown>>,
|
|
130
156
|
): CompactProjectedDiagnostic[] {
|
|
@@ -200,15 +226,6 @@ export function compactStatusTotal(counts: CompactStatusCountsV1): number {
|
|
|
200
226
|
+ counts.ambiguous + counts.unresolved + counts.cycle;
|
|
201
227
|
}
|
|
202
228
|
|
|
203
|
-
export function removeEquivalentCompactPersistedDecision(
|
|
204
|
-
decision: CompactDecisionV1,
|
|
205
|
-
): void {
|
|
206
|
-
if (decision.persistedResolutionStatus !== decision.effectiveResolutionStatus) return;
|
|
207
|
-
if (!decision.persistedTarget || decision.persistedTarget !== decision.effectiveTarget) return;
|
|
208
|
-
delete decision.persistedResolutionStatus;
|
|
209
|
-
delete decision.persistedTarget;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
229
|
function compactBlockingDiagnostic(item: CompactDiagnosticRowV1): boolean {
|
|
213
230
|
if (item[1] === 'error') return true;
|
|
214
231
|
return item[2] === 'schema_upgrade_required'
|
|
@@ -242,9 +259,13 @@ function compactDiagnosticDetails(
|
|
|
242
259
|
const out: CompactDiagnosticDetailsV1 = {};
|
|
243
260
|
const reasonCode = compactSafeCode(value.reasonCode);
|
|
244
261
|
if (reasonCode) out.reasonCode = reasonCode;
|
|
245
|
-
|
|
262
|
+
if (selectorDiagnosticCodes.has(code)) addDiagnosticSelector(out, value);
|
|
263
|
+
if (code === 'reindex_required') addInvalidFactCategories(out, value);
|
|
264
|
+
if (code === 'implementation_hint_mismatch')
|
|
265
|
+
addImplementationHintCandidates(out, value);
|
|
266
|
+
if (code === 'trace_runtime_variables_missing') addDiagnosticNames(out, value);
|
|
246
267
|
addDiagnosticCounts(out, value);
|
|
247
|
-
const hint = compactDiagnosticRemediation(code);
|
|
268
|
+
const hint = compactDiagnosticRemediation(code, out);
|
|
248
269
|
if (hint) {
|
|
249
270
|
out.remediationHint = hint;
|
|
250
271
|
out.omittedHintCount = Math.max(
|
|
@@ -254,18 +275,70 @@ function compactDiagnosticDetails(
|
|
|
254
275
|
return out;
|
|
255
276
|
}
|
|
256
277
|
|
|
278
|
+
function addDiagnosticSelector(
|
|
279
|
+
out: CompactDiagnosticDetailsV1,
|
|
280
|
+
value: Record<string, unknown>,
|
|
281
|
+
): void {
|
|
282
|
+
const selectorKind = compactSafeCode(value.selectorKind);
|
|
283
|
+
if (selectorKind) out.selectorKind = selectorKind;
|
|
284
|
+
const suggestions = projectCompactReferenceGroup(
|
|
285
|
+
compactStringArray(value.selectorSuggestions),
|
|
286
|
+
value.selectorSuggestionCount,
|
|
287
|
+
isSafeCompactSelectorSuggestion,
|
|
288
|
+
);
|
|
289
|
+
if (suggestions) out.selectorSuggestions = suggestions;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function addInvalidFactCategories(
|
|
293
|
+
out: CompactDiagnosticDetailsV1,
|
|
294
|
+
value: Record<string, unknown>,
|
|
295
|
+
): void {
|
|
296
|
+
const categories = compactRecordArray(value.invalidFactCategories)
|
|
297
|
+
.flatMap((item) =>
|
|
298
|
+
typeof item.category === 'string' ? [item.category] : []);
|
|
299
|
+
const projection = projectCompactReferenceGroup(
|
|
300
|
+
categories, value.invalidFactCategoryCount,
|
|
301
|
+
(item) => compactSafeCode(item) === item,
|
|
302
|
+
);
|
|
303
|
+
if (projection) out.invalidFactCategories = projection;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function addImplementationHintCandidates(
|
|
307
|
+
out: CompactDiagnosticDetailsV1,
|
|
308
|
+
value: Record<string, unknown>,
|
|
309
|
+
): void {
|
|
310
|
+
const repos = compactRecordArray(value.implementationHintSuggestions)
|
|
311
|
+
.flatMap((item) =>
|
|
312
|
+
typeof item.implementationRepo === 'string'
|
|
313
|
+
? [item.implementationRepo] : []);
|
|
314
|
+
const uniqueCount = new Set(repos.map((repo) => repo.trim())).size;
|
|
315
|
+
if (uniqueCount < 2) return;
|
|
316
|
+
const projection = projectCompactReferenceGroup(
|
|
317
|
+
repos, value.implementationHintSuggestionCount,
|
|
318
|
+
isSafeCompactReferenceName,
|
|
319
|
+
);
|
|
320
|
+
if (projection) out.tiedCandidateRepos = projection;
|
|
321
|
+
}
|
|
322
|
+
|
|
257
323
|
function addDiagnosticNames(
|
|
258
324
|
out: CompactDiagnosticDetailsV1,
|
|
259
325
|
value: Record<string, unknown>,
|
|
260
326
|
): void {
|
|
261
|
-
const
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
out
|
|
327
|
+
const projection = projectCompactMissingNames(
|
|
328
|
+
compactStringArray(value.missingVariables), value.missingVariableCount,
|
|
329
|
+
);
|
|
330
|
+
applyMissingProjection(out, projection);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function applyMissingProjection(
|
|
334
|
+
out: CompactDecisionV1 | CompactDiagnosticDetailsV1,
|
|
335
|
+
projection: CompactMissingNameProjection,
|
|
336
|
+
): void {
|
|
337
|
+
if (projection.names.length > 0) out.missingVariableNames = projection.names;
|
|
338
|
+
if (projection.total === 0) return;
|
|
339
|
+
out.missingVariableCount = projection.total;
|
|
340
|
+
out.shownMissingVariableCount = projection.shown;
|
|
341
|
+
out.omittedMissingVariableCount = projection.omitted;
|
|
269
342
|
}
|
|
270
343
|
|
|
271
344
|
function addDiagnosticCounts(
|
|
@@ -303,11 +376,19 @@ function compactDiagnosticSeverity(value: unknown): 'error' | 'warning' | 'info'
|
|
|
303
376
|
return value === 'error' || value === 'warning' ? value : 'info';
|
|
304
377
|
}
|
|
305
378
|
|
|
306
|
-
function compactDiagnosticRemediation(
|
|
379
|
+
function compactDiagnosticRemediation(
|
|
380
|
+
code: string,
|
|
381
|
+
details: CompactDiagnosticDetailsV1,
|
|
382
|
+
): string | undefined {
|
|
307
383
|
if (code === 'schema_upgrade_required' || code === 'reindex_required')
|
|
308
384
|
return compactRemediationHint('reindex_and_link');
|
|
309
385
|
if (code === 'trace_runtime_variables_missing')
|
|
310
|
-
return
|
|
386
|
+
return compactMissingRemediation({
|
|
387
|
+
names: details.missingVariableNames ?? [],
|
|
388
|
+
total: details.missingVariableCount ?? 0,
|
|
389
|
+
shown: details.shownMissingVariableCount ?? 0,
|
|
390
|
+
omitted: details.omittedMissingVariableCount ?? 0,
|
|
391
|
+
}, 'detailed diagnostic');
|
|
311
392
|
if (code === 'implementation_hint_mismatch')
|
|
312
393
|
return compactRemediationHint('select_implementation');
|
|
313
394
|
return undefined;
|
|
@@ -329,7 +410,8 @@ function compactDiagnosticHintCount(
|
|
|
329
410
|
|
|
330
411
|
function compactRemediationHint(code: string): string | undefined {
|
|
331
412
|
if (code === 'provide_runtime_variables') return 'Provide the missing variable names listed in details.';
|
|
332
|
-
if (code === 'select_implementation')
|
|
413
|
+
if (code === 'select_implementation')
|
|
414
|
+
return 'Use --implementation-hint with service, operation, package, repository, family, and repo keys; repo is required.';
|
|
333
415
|
if (code === 'reindex_and_link') return 'Force reindex, then force relink the workspace.';
|
|
334
416
|
if (code === 'inspect_detailed_edge') return 'Inspect the correlated detailed trace edge.';
|
|
335
417
|
return undefined;
|
|
@@ -358,11 +440,6 @@ function compactSafeSourceFile(value: unknown): string | undefined {
|
|
|
358
440
|
&& !/^[a-z]+:\/\//i.test(value) && !/[\r\n]/.test(value) ? value : undefined;
|
|
359
441
|
}
|
|
360
442
|
|
|
361
|
-
function safeVariableNames(values: string[] | undefined): string[] {
|
|
362
|
-
return compactSortedUnique((values ?? [])
|
|
363
|
-
.filter((value) => /^[A-Za-z_$][\w$]*$/.test(value)));
|
|
364
|
-
}
|
|
365
|
-
|
|
366
443
|
function compactCount(value: unknown): number {
|
|
367
444
|
return typeof value === 'number' && Number.isFinite(value)
|
|
368
445
|
? Math.max(0, Math.floor(value)) : 0;
|
|
@@ -378,6 +455,13 @@ function compactStringArray(value: unknown): string[] {
|
|
|
378
455
|
? value.filter((item): item is string => typeof item === 'string') : [];
|
|
379
456
|
}
|
|
380
457
|
|
|
458
|
+
function compactRecordArray(value: unknown): Array<Record<string, unknown>> {
|
|
459
|
+
return Array.isArray(value)
|
|
460
|
+
? value.filter((item): item is Record<string, unknown> =>
|
|
461
|
+
Boolean(item && typeof item === 'object' && !Array.isArray(item)))
|
|
462
|
+
: [];
|
|
463
|
+
}
|
|
464
|
+
|
|
381
465
|
function compactArrayLength(value: unknown): number {
|
|
382
466
|
return Array.isArray(value) ? value.length : 0;
|
|
383
467
|
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { compareBinary } from './010-traversal-scope.js';
|
|
2
|
+
import type {
|
|
3
|
+
CompactDecisionV1,
|
|
4
|
+
CompactReferenceGroupV1,
|
|
5
|
+
CompactStatus,
|
|
6
|
+
} from './014-compact-contract.js';
|
|
7
|
+
|
|
8
|
+
export const COMPACT_MISSING_NAME_LIMIT = 8;
|
|
9
|
+
export const COMPACT_MISSING_NAME_MAX_LENGTH = 160;
|
|
10
|
+
export const COMPACT_REFERENCE_VALUE_LIMIT = 5;
|
|
11
|
+
const COMPACT_REFERENCE_VALUE_MAX_LENGTH = 240;
|
|
12
|
+
|
|
13
|
+
export interface CompactMissingNameProjection {
|
|
14
|
+
readonly names: string[];
|
|
15
|
+
readonly total: number;
|
|
16
|
+
readonly shown: number;
|
|
17
|
+
readonly omitted: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const identifier = '[A-Za-z_$][A-Za-z0-9_$]*';
|
|
21
|
+
const member = `(?:(?:\\.|\\?\\.)${identifier}|(?:\\[|\\?\\.\\[)(?:0|[1-9][0-9]*)\\])`;
|
|
22
|
+
const transform = '(?:(?:\\.|\\?\\.)(?:toLowerCase|toUpperCase|trim)\\(\\))?';
|
|
23
|
+
const safeMissingName = new RegExp(`^${identifier}(?:${member})*${transform}$`);
|
|
24
|
+
|
|
25
|
+
export function projectCompactMissingNames(
|
|
26
|
+
values: readonly string[] | undefined,
|
|
27
|
+
authoritativeCount: unknown,
|
|
28
|
+
): CompactMissingNameProjection {
|
|
29
|
+
const candidates = new Map<string, boolean>();
|
|
30
|
+
for (const value of values ?? []) {
|
|
31
|
+
const normalized = value.trim();
|
|
32
|
+
if (!normalized) continue;
|
|
33
|
+
candidates.set(
|
|
34
|
+
normalized,
|
|
35
|
+
(candidates.get(normalized) ?? true) && isSafeCompactMissingName(value),
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
const raw = [...candidates.keys()].sort(compareBinary);
|
|
39
|
+
const safe = raw.filter((name) => candidates.get(name) === true);
|
|
40
|
+
const names = safe.slice(0, COMPACT_MISSING_NAME_LIMIT);
|
|
41
|
+
const total = Math.max(compactCount(authoritativeCount), raw.length);
|
|
42
|
+
return {
|
|
43
|
+
names, total, shown: names.length,
|
|
44
|
+
omitted: Math.max(0, total - names.length),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function isSafeCompactMissingName(value: string): boolean {
|
|
49
|
+
if (hasControlCharacter(value)) return false;
|
|
50
|
+
const normalized = value.trim();
|
|
51
|
+
return normalized.length > 0
|
|
52
|
+
&& normalized.length <= COMPACT_MISSING_NAME_MAX_LENGTH
|
|
53
|
+
&& safeMissingName.test(normalized);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function projectCompactReferenceGroup(
|
|
57
|
+
values: readonly string[] | undefined,
|
|
58
|
+
authoritativeCount: unknown,
|
|
59
|
+
isSafe: (value: string) => boolean,
|
|
60
|
+
): CompactReferenceGroupV1 | undefined {
|
|
61
|
+
const candidates = normalizedReferenceValues(values, isSafe);
|
|
62
|
+
const unique = [...candidates.keys()].sort(compareBinary);
|
|
63
|
+
const safe = unique.filter((value) => candidates.get(value) === true);
|
|
64
|
+
const shown = safe.slice(0, COMPACT_REFERENCE_VALUE_LIMIT);
|
|
65
|
+
const total = Math.max(compactCount(authoritativeCount), unique.length);
|
|
66
|
+
if (total === 0) return undefined;
|
|
67
|
+
return {
|
|
68
|
+
values: shown,
|
|
69
|
+
total,
|
|
70
|
+
shown: shown.length,
|
|
71
|
+
omitted: Math.max(0, total - shown.length),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function isSafeCompactReferenceName(value: string): boolean {
|
|
76
|
+
if (!safeReferenceText(value, 160)) return false;
|
|
77
|
+
return /^[@A-Za-z0-9_$][@A-Za-z0-9_$./:-]*$/.test(value.trim());
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function isSafeCompactSelectorSuggestion(value: string): boolean {
|
|
81
|
+
if (!safeReferenceText(value, COMPACT_REFERENCE_VALUE_MAX_LENGTH))
|
|
82
|
+
return false;
|
|
83
|
+
return /^(?:--(?:repo|service|operation|path|handler) [@A-Za-z0-9_$./:-]+)(?: --(?:repo|service|operation|path|handler) [@A-Za-z0-9_$./:-]+)*$/
|
|
84
|
+
.test(value.trim());
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function normalizeCompactDecisionEquivalence(
|
|
88
|
+
decision: CompactDecisionV1,
|
|
89
|
+
status: CompactStatus,
|
|
90
|
+
canonicalTarget: string | undefined,
|
|
91
|
+
targetIdentityEqual = false,
|
|
92
|
+
persistedIdentityEqual = false,
|
|
93
|
+
rawPersistedStatus?: string,
|
|
94
|
+
): void {
|
|
95
|
+
if (decision.effectiveResolutionStatus === status)
|
|
96
|
+
delete decision.effectiveResolutionStatus;
|
|
97
|
+
removeEquivalentPersistedDecision(
|
|
98
|
+
decision, status, canonicalTarget, persistedIdentityEqual,
|
|
99
|
+
rawPersistedStatus,
|
|
100
|
+
);
|
|
101
|
+
if (targetIdentityEqual && canonicalTarget !== undefined
|
|
102
|
+
&& decision.effectiveTarget === canonicalTarget)
|
|
103
|
+
delete decision.effectiveTarget;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function compactMissingRemediation(
|
|
107
|
+
projection: CompactMissingNameProjection,
|
|
108
|
+
artifact: 'detailed diagnostic' | 'detailed trace edge',
|
|
109
|
+
): string {
|
|
110
|
+
if (projection.total > 0 && projection.shown === projection.total)
|
|
111
|
+
return 'Provide the missing variable names listed in details.';
|
|
112
|
+
if (projection.shown > 0)
|
|
113
|
+
return `Provide the listed names; inspect the correlated ${artifact} for omitted names.`;
|
|
114
|
+
return `Inspect the correlated ${artifact} for missing variable names.`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function removeEquivalentPersistedDecision(
|
|
118
|
+
decision: CompactDecisionV1,
|
|
119
|
+
status: CompactStatus,
|
|
120
|
+
canonicalTarget: string | undefined,
|
|
121
|
+
identityEqual: boolean,
|
|
122
|
+
rawPersistedStatus: string | undefined,
|
|
123
|
+
): void {
|
|
124
|
+
if (!identityEqual
|
|
125
|
+
|| rawPersistedStatus !== status
|
|
126
|
+
|| (decision.effectiveResolutionStatus !== undefined
|
|
127
|
+
&& decision.persistedResolutionStatus
|
|
128
|
+
!== decision.effectiveResolutionStatus))
|
|
129
|
+
return;
|
|
130
|
+
if (!decision.persistedTarget || canonicalTarget === undefined
|
|
131
|
+
|| decision.persistedTarget !== canonicalTarget)
|
|
132
|
+
return;
|
|
133
|
+
delete decision.persistedResolutionStatus;
|
|
134
|
+
delete decision.persistedTarget;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function compactCount(value: unknown): number {
|
|
138
|
+
return typeof value === 'number' && Number.isFinite(value)
|
|
139
|
+
? Math.max(0, Math.floor(value)) : 0;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function normalizedReferenceValues(
|
|
143
|
+
values: readonly string[] | undefined,
|
|
144
|
+
isSafe: (value: string) => boolean,
|
|
145
|
+
): Map<string, boolean> {
|
|
146
|
+
const unique = new Map<string, boolean>();
|
|
147
|
+
for (const raw of values ?? []) {
|
|
148
|
+
const value = raw.trim();
|
|
149
|
+
if (!value) continue;
|
|
150
|
+
unique.set(value, (unique.get(value) ?? true) && isSafe(raw));
|
|
151
|
+
}
|
|
152
|
+
return unique;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function safeReferenceText(value: string, maxLength: number): boolean {
|
|
156
|
+
if (hasControlCharacter(value)) return false;
|
|
157
|
+
const normalized = value.trim();
|
|
158
|
+
return normalized.length > 0
|
|
159
|
+
&& normalized.length <= maxLength
|
|
160
|
+
&& !/^[a-z]+:\/\//i.test(normalized)
|
|
161
|
+
&& !/(?:^|[^A-Za-z0-9])(?:authorization|bearer|credential|destination|password|secret|token)(?:$|[^A-Za-z0-9])/i
|
|
162
|
+
.test(normalized);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function hasControlCharacter(value: string): boolean {
|
|
166
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
167
|
+
const code = value.charCodeAt(index);
|
|
168
|
+
if (code <= 31 || code === 127) return true;
|
|
169
|
+
}
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Db } from '../db/connection.js';
|
|
2
|
+
import { currentFactLifecycleDiagnostic } from '../db/001-fact-lifecycle.js';
|
|
3
|
+
import { resolveTraversalWorkspaceId } from './010-traversal-scope.js';
|
|
4
|
+
|
|
5
|
+
function workspaceScopeIsAmbiguous(db: Db): boolean {
|
|
6
|
+
const rows = db.prepare(`SELECT DISTINCT workspace_id workspaceId
|
|
7
|
+
FROM repositories ORDER BY workspace_id LIMIT 2`).all();
|
|
8
|
+
return rows.length > 1;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function shouldDeferTraceStartSelection(
|
|
12
|
+
db: Db,
|
|
13
|
+
requestedWorkspaceId: number | undefined,
|
|
14
|
+
repoId: number | undefined,
|
|
15
|
+
): boolean {
|
|
16
|
+
const workspaceId = resolveTraversalWorkspaceId(
|
|
17
|
+
db, requestedWorkspaceId, repoId,
|
|
18
|
+
);
|
|
19
|
+
if (workspaceId === undefined) return workspaceScopeIsAmbiguous(db);
|
|
20
|
+
return currentFactLifecycleDiagnostic(db, workspaceId) !== undefined;
|
|
21
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Db } from '../db/connection.js';
|
|
2
|
+
|
|
3
|
+
export function outboundScopeSymbolIds(
|
|
4
|
+
db: Db,
|
|
5
|
+
symbolIds: ReadonlySet<number> | undefined,
|
|
6
|
+
includeAsync: boolean,
|
|
7
|
+
): ReadonlySet<number> | undefined {
|
|
8
|
+
if (!includeAsync || !symbolIds || symbolIds.size === 0) return symbolIds;
|
|
9
|
+
const ids = [...symbolIds];
|
|
10
|
+
const rows = db.prepare(`SELECT DISTINCT child.id FROM symbols parent
|
|
11
|
+
JOIN symbols child ON child.repo_id=parent.repo_id
|
|
12
|
+
AND child.source_file=parent.source_file
|
|
13
|
+
AND child.kind='event_registration'
|
|
14
|
+
AND parent.start_offset<=child.start_offset
|
|
15
|
+
AND parent.end_offset>=child.end_offset
|
|
16
|
+
WHERE parent.id IN (${ids.map(() => '?').join(',')})
|
|
17
|
+
ORDER BY child.id`).all(...ids);
|
|
18
|
+
return new Set([
|
|
19
|
+
...ids,
|
|
20
|
+
...rows.flatMap((row) =>
|
|
21
|
+
typeof row.id === 'number' ? [row.id] : []),
|
|
22
|
+
]);
|
|
23
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CompactDecisionTargetInput,
|
|
3
|
+
CompactDecisionV1,
|
|
4
|
+
CompactEdgeObservation,
|
|
5
|
+
} from './014-compact-contract.js';
|
|
6
|
+
import { projectCompactDecision } from './020-compact-field-projection.js';
|
|
7
|
+
import { normalizeCompactDecisionEquivalence } from
|
|
8
|
+
'./021-compact-decision-normalization.js';
|
|
9
|
+
|
|
10
|
+
export interface CompactDecisionNode {
|
|
11
|
+
key?: string;
|
|
12
|
+
decisionTarget?: string;
|
|
13
|
+
projectedIdentity?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function resolvedNode(
|
|
17
|
+
value: CompactDecisionTargetInput | undefined,
|
|
18
|
+
resolve: (input: CompactDecisionTargetInput) => CompactDecisionNode,
|
|
19
|
+
): CompactDecisionNode | undefined {
|
|
20
|
+
return value ? resolve(value) : undefined;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function applyEffectiveTarget(
|
|
24
|
+
decision: CompactDecisionV1,
|
|
25
|
+
effective: CompactDecisionNode | undefined,
|
|
26
|
+
): void {
|
|
27
|
+
if (effective?.decisionTarget)
|
|
28
|
+
decision.effectiveTarget = effective.decisionTarget;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function applyPersistedTarget(
|
|
32
|
+
decision: CompactDecisionV1,
|
|
33
|
+
persisted: CompactDecisionNode | undefined,
|
|
34
|
+
status: string | undefined,
|
|
35
|
+
): void {
|
|
36
|
+
if (persisted?.decisionTarget && status)
|
|
37
|
+
decision.persistedTarget = persisted.decisionTarget;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function sameCanonicalNode(
|
|
41
|
+
left: CompactDecisionNode | undefined,
|
|
42
|
+
right: CompactDecisionNode,
|
|
43
|
+
): boolean {
|
|
44
|
+
if (left?.key === undefined) return false;
|
|
45
|
+
return left.projectedIdentity === true
|
|
46
|
+
? left.key === right.decisionTarget
|
|
47
|
+
: left.key === right.key;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function persistedCanonical(
|
|
51
|
+
persisted: CompactDecisionNode | undefined,
|
|
52
|
+
effective: CompactDecisionNode | undefined,
|
|
53
|
+
target: CompactDecisionNode,
|
|
54
|
+
): boolean {
|
|
55
|
+
if (!sameCanonicalNode(persisted, target)) return false;
|
|
56
|
+
return effective?.key === undefined || sameCanonicalNode(effective, target);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function projectObservationDecision(
|
|
60
|
+
input: CompactEdgeObservation,
|
|
61
|
+
target: CompactDecisionNode,
|
|
62
|
+
resolve: (value: CompactDecisionTargetInput) => CompactDecisionNode,
|
|
63
|
+
): CompactDecisionV1 {
|
|
64
|
+
const decision = projectCompactDecision(input.decision);
|
|
65
|
+
const source = input.decision;
|
|
66
|
+
const effective = resolvedNode(source?.effectiveTarget, resolve);
|
|
67
|
+
const persisted = resolvedNode(source?.persistedTarget, resolve);
|
|
68
|
+
applyEffectiveTarget(decision, effective);
|
|
69
|
+
applyPersistedTarget(
|
|
70
|
+
decision, persisted, source?.persistedResolutionStatus,
|
|
71
|
+
);
|
|
72
|
+
normalizeCompactDecisionEquivalence(
|
|
73
|
+
decision,
|
|
74
|
+
input.status,
|
|
75
|
+
target.decisionTarget,
|
|
76
|
+
sameCanonicalNode(effective, target),
|
|
77
|
+
persistedCanonical(persisted, effective, target),
|
|
78
|
+
source?.persistedResolutionStatus,
|
|
79
|
+
);
|
|
80
|
+
return decision;
|
|
81
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import type { Db } from '../db/connection.js';
|
|
2
|
+
import type { ImplementationHint } from '../types.js';
|
|
3
|
+
import type { TraceGraphEdgeRow } from './012-trace-graph-lookups.js';
|
|
4
|
+
|
|
5
|
+
export interface ImplementationHintOptions {
|
|
6
|
+
implementationRepo?: string;
|
|
7
|
+
implementationHints?: ImplementationHint[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface ImplementationScope {
|
|
11
|
+
repoId?: number;
|
|
12
|
+
files: Set<string>;
|
|
13
|
+
symbolId?: number;
|
|
14
|
+
edge?: TraceGraphEdgeRow;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface HandlerScopeRow {
|
|
18
|
+
repoId?: number;
|
|
19
|
+
sourceFile?: string;
|
|
20
|
+
symbolId?: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function normalizeOperation(value: string | undefined): string | undefined {
|
|
24
|
+
if (!value) return undefined;
|
|
25
|
+
return value.startsWith('/') ? value.slice(1) : value;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function implementationEdge(
|
|
29
|
+
db: Db,
|
|
30
|
+
operationId: string,
|
|
31
|
+
): TraceGraphEdgeRow | undefined {
|
|
32
|
+
return db.prepare(`SELECT * FROM graph_edges
|
|
33
|
+
WHERE edge_type='OPERATION_IMPLEMENTED_BY_HANDLER'
|
|
34
|
+
AND from_kind='operation' AND from_id=?
|
|
35
|
+
ORDER BY CASE status WHEN 'resolved' THEN 0
|
|
36
|
+
WHEN 'ambiguous' THEN 1 ELSE 2 END,id LIMIT 1`)
|
|
37
|
+
.get(operationId) as TraceGraphEdgeRow | undefined;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function handlerScopeRow(db: Db, methodId: string): HandlerScopeRow | undefined {
|
|
41
|
+
return db.prepare(`SELECT hc.repo_id repoId,hc.source_file sourceFile,
|
|
42
|
+
s.id symbolId
|
|
43
|
+
FROM handler_methods hm
|
|
44
|
+
JOIN handler_classes hc ON hc.id=hm.handler_class_id
|
|
45
|
+
LEFT JOIN symbols s ON s.repo_id=hc.repo_id
|
|
46
|
+
AND s.source_file=hc.source_file
|
|
47
|
+
AND s.qualified_name=hc.class_name || '.' || hm.method_name
|
|
48
|
+
AND s.start_line=hm.source_line
|
|
49
|
+
WHERE hm.id=?`).get(methodId) as HandlerScopeRow | undefined;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function implementationScope(
|
|
53
|
+
db: Db,
|
|
54
|
+
operationId: string,
|
|
55
|
+
): ImplementationScope {
|
|
56
|
+
const edge = implementationEdge(db, operationId);
|
|
57
|
+
if (!edge || edge.status !== 'resolved') return { files: new Set(), edge };
|
|
58
|
+
const row = handlerScopeRow(db, edge.to_id);
|
|
59
|
+
if (!row || typeof row.symbolId !== 'number')
|
|
60
|
+
return { repoId: row?.repoId, files: new Set(), edge };
|
|
61
|
+
return {
|
|
62
|
+
repoId: row.repoId,
|
|
63
|
+
files: new Set(row.sourceFile ? [row.sourceFile] : []),
|
|
64
|
+
symbolId: row.symbolId,
|
|
65
|
+
edge,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function handlerScope(
|
|
70
|
+
db: Db,
|
|
71
|
+
methodId: string,
|
|
72
|
+
): Omit<ImplementationScope, 'edge'> | undefined {
|
|
73
|
+
const row = handlerScopeRow(db, methodId);
|
|
74
|
+
if (!row || typeof row.symbolId !== 'number') return undefined;
|
|
75
|
+
return {
|
|
76
|
+
repoId: row.repoId,
|
|
77
|
+
files: new Set(row.sourceFile ? [row.sourceFile] : []),
|
|
78
|
+
symbolId: row.symbolId,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function handlerFilesForOperation(
|
|
83
|
+
db: Db,
|
|
84
|
+
operationId: string,
|
|
85
|
+
): Set<string> {
|
|
86
|
+
const op = db.prepare(`SELECT o.operation_name operationName,
|
|
87
|
+
o.operation_path operationPath,s.repo_id repoId
|
|
88
|
+
FROM cds_operations o JOIN cds_services s ON s.id=o.service_id
|
|
89
|
+
WHERE o.id=?`).get(operationId) as {
|
|
90
|
+
operationName?: string;
|
|
91
|
+
operationPath?: string;
|
|
92
|
+
repoId?: number;
|
|
93
|
+
} | undefined;
|
|
94
|
+
if (!op) return new Set();
|
|
95
|
+
const operation = normalizeOperation(op.operationPath ?? op.operationName);
|
|
96
|
+
const rows = handlerFileRows(db, op.repoId, operation, op.operationName);
|
|
97
|
+
return new Set(rows.flatMap((row) =>
|
|
98
|
+
typeof row.sourceFile === 'string' ? [row.sourceFile] : []));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function handlerFileRows(
|
|
102
|
+
db: Db,
|
|
103
|
+
repoId: number | undefined,
|
|
104
|
+
operation: string | undefined,
|
|
105
|
+
operationName: string | undefined,
|
|
106
|
+
): Array<{ sourceFile?: string }> {
|
|
107
|
+
return db.prepare(`SELECT DISTINCT hc.source_file sourceFile
|
|
108
|
+
FROM handler_classes hc
|
|
109
|
+
JOIN handler_methods hm ON hm.handler_class_id=hc.id
|
|
110
|
+
WHERE hc.repo_id=?
|
|
111
|
+
AND COALESCE(json_extract(hm.decorator_resolution_json,'$.handlerKind'),
|
|
112
|
+
CASE WHEN hm.decorator_kind='Event' THEN 'event'
|
|
113
|
+
WHEN hm.decorator_kind IN ('Action','Func','On') THEN 'operation'
|
|
114
|
+
ELSE 'unsupported' END)='operation'
|
|
115
|
+
AND COALESCE(json_extract(hm.decorator_resolution_json,'$.executable'),
|
|
116
|
+
CASE WHEN hm.decorator_kind IN ('Action','Func','On')
|
|
117
|
+
THEN 1 ELSE 0 END)=1
|
|
118
|
+
AND (hm.decorator_value=? OR hm.method_name=?
|
|
119
|
+
OR hm.decorator_value=?)`)
|
|
120
|
+
.all(repoId, operation, operation, operationName) as Array<{
|
|
121
|
+
sourceFile?: string;
|
|
122
|
+
}>;
|
|
123
|
+
}
|