@saptools/service-flow 0.1.68 → 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 +10 -0
- package/README.md +11 -5
- package/TECHNICAL-NOTE.md +9 -0
- package/dist/{chunk-AEM4JY22.js → chunk-3N3B5KHV.js} +6983 -5500
- package/dist/chunk-3N3B5KHV.js.map +1 -0
- package/dist/cli.js +317 -105
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/src/cli/001-index-summary.ts +22 -0
- package/src/cli.ts +151 -87
- package/src/db/000-call-fact-repository.ts +45 -19
- package/src/db/003-current-fact-semantics.ts +1 -0
- package/src/db/004-package-target-invalidation.ts +10 -0
- package/src/db/006-relative-symbol-resolution.ts +28 -7
- package/src/db/008-relative-fact-semantics.ts +6 -3
- package/src/db/009-binding-fact-semantics.ts +5 -0
- package/src/db/012-binding-reference-proof.ts +4 -0
- package/src/db/013-index-publication-failure.ts +91 -0
- package/src/db/014-binding-helper-provenance.ts +17 -0
- package/src/db/repositories.ts +22 -5
- package/src/indexer/cds-extension-resolver.ts +27 -3
- package/src/indexer/repository-indexer.ts +66 -5
- package/src/indexer/workspace-indexer.ts +141 -29
- package/src/linker/004-event-subscription-handler-linker.ts +32 -11
- 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 +2 -165
- package/src/parsers/000-direct-query-execution.ts +11 -0
- package/src/parsers/006-binding-identity.ts +6 -1
- package/src/parsers/014-service-binding-helper-flow.ts +70 -4
- package/src/parsers/021-binding-visibility.ts +17 -1
- package/src/parsers/022-outbound-expression-analysis.ts +700 -0
- package/src/parsers/023-outbound-call-classifier.ts +692 -0
- package/src/parsers/outbound-call-parser.ts +146 -509
- package/src/parsers/symbol-parser.ts +37 -5
- 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 +2 -3
- package/src/trace/019-trace-edge-semantics.ts +6 -10
- package/src/trace/020-compact-field-projection.ts +74 -1
- package/src/trace/021-compact-decision-normalization.ts +75 -9
- package/src/trace/024-compact-observation-decision.ts +7 -2
- package/src/trace/026-trace-start-scope.ts +1 -0
- package/src/trace/027-trace-scope-execution.ts +33 -33
- 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/types.ts +2 -1
- package/src/version.ts +1 -1
- package/dist/chunk-AEM4JY22.js.map +0 -1
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { posix } from 'node:path';
|
|
2
2
|
import type { SymbolCallFact } from '../types.js';
|
|
3
|
+
import { packageModuleRequest } from
|
|
4
|
+
'../parsers/002-symbol-import-bindings.js';
|
|
3
5
|
import type { Db } from './connection.js';
|
|
4
6
|
|
|
5
7
|
export interface RelativeSymbolCallResolution {
|
|
@@ -33,6 +35,7 @@ interface MappingTarget {
|
|
|
33
35
|
interface MappedExecutableRows {
|
|
34
36
|
rows: TargetRow[];
|
|
35
37
|
ambiguous: boolean;
|
|
38
|
+
packageTargetUnsupported: boolean;
|
|
36
39
|
}
|
|
37
40
|
|
|
38
41
|
const stripExtension = (value: string): string =>
|
|
@@ -138,8 +141,12 @@ function handlerMemberPubliclyCarried(
|
|
|
138
141
|
): boolean {
|
|
139
142
|
if (row.exported) return true;
|
|
140
143
|
const evidence = parsedRecord(row.evidenceJson);
|
|
141
|
-
|
|
142
|
-
|
|
144
|
+
if (evidence?.source !== 'exported_class_instance_member') return false;
|
|
145
|
+
const exportKind = evidence.exportedClassExportKind;
|
|
146
|
+
if (exportKind !== 'default' && exportKind !== 'named') return false;
|
|
147
|
+
return binding.bindingKind === 'esm_default'
|
|
148
|
+
? exportKind === 'default'
|
|
149
|
+
: evidence.exportedClass === binding.importedName;
|
|
143
150
|
}
|
|
144
151
|
|
|
145
152
|
function requiresPublicClassMember(
|
|
@@ -287,9 +294,12 @@ function mappingTargetScope(
|
|
|
287
294
|
): RelativeModuleScope | undefined {
|
|
288
295
|
const target = mappingTarget(row);
|
|
289
296
|
if (!target) return undefined;
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
297
|
+
if (!target.specifier)
|
|
298
|
+
return {
|
|
299
|
+
paths: new Set([stripExtension(row.sourceFile)]), ambiguous: false,
|
|
300
|
+
};
|
|
301
|
+
if (packageModuleRequest(target.specifier)) return undefined;
|
|
302
|
+
return relativeModuleScope(db, repoId, row.sourceFile, target.specifier);
|
|
293
303
|
}
|
|
294
304
|
|
|
295
305
|
function mappedExecutableRows(
|
|
@@ -298,18 +308,24 @@ function mappedExecutableRows(
|
|
|
298
308
|
rows: readonly TargetRow[],
|
|
299
309
|
aliases: readonly TargetRow[],
|
|
300
310
|
): MappedExecutableRows {
|
|
311
|
+
const packageTargetUnsupported = aliases.some((alias) => {
|
|
312
|
+
const target = mappingTarget(alias);
|
|
313
|
+
return Boolean(target?.specifier && packageModuleRequest(target.specifier));
|
|
314
|
+
});
|
|
301
315
|
const scopes = aliases.flatMap((alias) => {
|
|
302
316
|
const scope = mappingTargetScope(db, repoId, alias);
|
|
303
317
|
return scope ? [scope] : [];
|
|
304
318
|
});
|
|
305
319
|
if (scopes.some((scope) => scope.ambiguous))
|
|
306
|
-
return { rows: [], ambiguous: true };
|
|
320
|
+
return { rows: [], ambiguous: true, packageTargetUnsupported };
|
|
307
321
|
const mapped = scopes.flatMap((scope) => {
|
|
308
322
|
return rows.filter((row) =>
|
|
309
323
|
row.kind !== 'object_alias' && bodyEligible(row)
|
|
310
324
|
&& scope.paths.has(stripExtension(row.sourceFile)));
|
|
311
325
|
});
|
|
312
|
-
return {
|
|
326
|
+
return {
|
|
327
|
+
rows: uniqueRows(mapped), ambiguous: false, packageTargetUnsupported,
|
|
328
|
+
};
|
|
313
329
|
}
|
|
314
330
|
|
|
315
331
|
function proxyResolution(
|
|
@@ -326,6 +342,11 @@ function proxyResolution(
|
|
|
326
342
|
'relative_import_module_resolution_ambiguous',
|
|
327
343
|
rows.length, 0, false,
|
|
328
344
|
);
|
|
345
|
+
if (mapped.packageTargetUnsupported) return unresolved(
|
|
346
|
+
'proxy_member_exported_object_map',
|
|
347
|
+
'relative_import_proxy_alias_targets_package_unsupported',
|
|
348
|
+
rows.length,
|
|
349
|
+
);
|
|
329
350
|
const direct = scoped.filter((row) =>
|
|
330
351
|
row.kind !== 'object_alias' && row.exported && bodyEligible(row));
|
|
331
352
|
const eligible = uniqueRows([
|
|
@@ -81,9 +81,12 @@ function derivedRelativeProvenance(
|
|
|
81
81
|
targetName: string,
|
|
82
82
|
): boolean {
|
|
83
83
|
if (!memberReference(binding)) return false;
|
|
84
|
-
if (evidence.relation === 'class_instance_method')
|
|
85
|
-
|
|
86
|
-
|
|
84
|
+
if (evidence.relation === 'class_instance_method') {
|
|
85
|
+
if (evidence.methodName !== binding.referencedMemberName) return false;
|
|
86
|
+
return binding.bindingKind === 'esm_default'
|
|
87
|
+
? targetName === `${binding.localName}.${binding.referencedMemberName}`
|
|
88
|
+
: targetName === binding.requestedPublicName;
|
|
89
|
+
}
|
|
87
90
|
if (evidence.relation !== 'relative_import_proxy_member') return false;
|
|
88
91
|
return targetName === binding.referencedMemberName
|
|
89
92
|
&& typeof evidence.proxyVariableName === 'string';
|
|
@@ -9,6 +9,9 @@ import {
|
|
|
9
9
|
validBindingLexicalScope,
|
|
10
10
|
type BindingProofTarget,
|
|
11
11
|
} from './012-binding-reference-proof.js';
|
|
12
|
+
import {
|
|
13
|
+
hasSingleHopHelperReturn,
|
|
14
|
+
} from './014-binding-helper-provenance.js';
|
|
12
15
|
|
|
13
16
|
export interface BindingFactCategoryCount {
|
|
14
17
|
category: string;
|
|
@@ -114,6 +117,7 @@ function bindingTargets(
|
|
|
114
117
|
binding.binding_site_start_offset startOffset,
|
|
115
118
|
binding.binding_site_end_offset endOffset,
|
|
116
119
|
binding.owner_resolution ownerResolution,
|
|
120
|
+
binding.helper_chain_json helperChainJson,
|
|
117
121
|
owner.start_offset ownerStartOffset,owner.end_offset ownerEndOffset
|
|
118
122
|
FROM service_bindings binding
|
|
119
123
|
JOIN repositories repo ON repo.id=binding.repo_id
|
|
@@ -140,6 +144,7 @@ function bindingTarget(
|
|
|
140
144
|
ownerResolution: row.ownerResolution,
|
|
141
145
|
ownerStartOffset: row.ownerStartOffset,
|
|
142
146
|
ownerEndOffset: row.ownerEndOffset,
|
|
147
|
+
singleHopHelperReturn: hasSingleHopHelperReturn(row.helperChainJson),
|
|
143
148
|
}];
|
|
144
149
|
}
|
|
145
150
|
|
|
@@ -23,6 +23,7 @@ export interface BindingProofTarget {
|
|
|
23
23
|
ownerResolution: string;
|
|
24
24
|
ownerStartOffset: number | null;
|
|
25
25
|
ownerEndOffset: number | null;
|
|
26
|
+
singleHopHelperReturn: boolean;
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
interface ResolvedBindingProof {
|
|
@@ -45,6 +46,7 @@ const strategies = new Set([
|
|
|
45
46
|
'lexical_declaration',
|
|
46
47
|
'lexical_alias_declaration',
|
|
47
48
|
'deterministic_reaching_assignment',
|
|
49
|
+
'single_hop_helper_return',
|
|
48
50
|
]);
|
|
49
51
|
|
|
50
52
|
function record(value: unknown): Record<string, unknown> | undefined {
|
|
@@ -242,6 +244,8 @@ function targetMatches(
|
|
|
242
244
|
target: BindingProofTarget | undefined,
|
|
243
245
|
): boolean {
|
|
244
246
|
if (!target || call.bindingId !== target.id) return false;
|
|
247
|
+
if (reference.resolutionStrategy === 'single_hop_helper_return'
|
|
248
|
+
&& !target.singleHopHelperReturn) return false;
|
|
245
249
|
return all([
|
|
246
250
|
target.repoId === call.repoId,
|
|
247
251
|
target.sourceFile === call.sourceFile,
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { Db } from './connection.js';
|
|
2
|
+
|
|
3
|
+
export type PreparedRepositoryFactKind =
|
|
4
|
+
| 'outbound_call'
|
|
5
|
+
| 'service_binding'
|
|
6
|
+
| 'symbol_call';
|
|
7
|
+
|
|
8
|
+
export type PreparedSnapshotFailureCode =
|
|
9
|
+
| 'binding_lexical_proof_invalid'
|
|
10
|
+
| 'binding_owner_mismatch'
|
|
11
|
+
| 'binding_reference_mismatch'
|
|
12
|
+
| 'binding_reference_missing'
|
|
13
|
+
| 'binding_site_missing'
|
|
14
|
+
| 'duplicate_service_binding_site'
|
|
15
|
+
| 'outbound_owner_mismatch'
|
|
16
|
+
| 'package_import_provenance_missing'
|
|
17
|
+
| 'symbol_call_owner_mismatch';
|
|
18
|
+
|
|
19
|
+
export interface PreparedSnapshotFailureSite {
|
|
20
|
+
factKind: PreparedRepositoryFactKind;
|
|
21
|
+
sourceFile?: string;
|
|
22
|
+
sourceLine?: number;
|
|
23
|
+
callSiteStartOffset?: number;
|
|
24
|
+
callSiteEndOffset?: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export class PreparedRepositorySnapshotError extends Error {
|
|
28
|
+
readonly failureCode: PreparedSnapshotFailureCode;
|
|
29
|
+
readonly site: PreparedSnapshotFailureSite;
|
|
30
|
+
|
|
31
|
+
constructor(
|
|
32
|
+
failureCode: PreparedSnapshotFailureCode,
|
|
33
|
+
site: PreparedSnapshotFailureSite,
|
|
34
|
+
) {
|
|
35
|
+
super(`invalid_prepared_repository_snapshot:${failureCode}`);
|
|
36
|
+
this.name = 'PreparedRepositorySnapshotError';
|
|
37
|
+
this.failureCode = failureCode;
|
|
38
|
+
this.site = site;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface PreparedCallSite {
|
|
43
|
+
sourceFile: string;
|
|
44
|
+
sourceLine: number;
|
|
45
|
+
callSiteStartOffset?: number;
|
|
46
|
+
callSiteEndOffset?: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function preparedCallSnapshotError(
|
|
50
|
+
failureCode: PreparedSnapshotFailureCode,
|
|
51
|
+
factKind: 'outbound_call' | 'symbol_call',
|
|
52
|
+
call: PreparedCallSite,
|
|
53
|
+
): PreparedRepositorySnapshotError {
|
|
54
|
+
return new PreparedRepositorySnapshotError(failureCode, {
|
|
55
|
+
factKind,
|
|
56
|
+
sourceFile: call.sourceFile,
|
|
57
|
+
sourceLine: call.sourceLine,
|
|
58
|
+
callSiteStartOffset: call.callSiteStartOffset,
|
|
59
|
+
callSiteEndOffset: call.callSiteEndOffset,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function isPreparedRepositorySnapshotError(
|
|
64
|
+
error: unknown,
|
|
65
|
+
): error is PreparedRepositorySnapshotError {
|
|
66
|
+
return error instanceof PreparedRepositorySnapshotError;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function recordPreparedSnapshotFailure(
|
|
70
|
+
db: Db,
|
|
71
|
+
repoId: number,
|
|
72
|
+
error: PreparedRepositorySnapshotError,
|
|
73
|
+
): void {
|
|
74
|
+
db.prepare(`UPDATE repositories SET index_status='failed',
|
|
75
|
+
error_count=1 WHERE id=?`).run(repoId);
|
|
76
|
+
db.prepare(`DELETE FROM diagnostics WHERE repo_id=? AND (
|
|
77
|
+
code IN ('index_failed_snapshot_preserved','source_read_failed')
|
|
78
|
+
OR code GLOB 'invalid_prepared_repository_snapshot:*'
|
|
79
|
+
)`).run(repoId);
|
|
80
|
+
db.prepare(`INSERT INTO diagnostics(
|
|
81
|
+
repo_id,severity,code,message,source_file,source_line
|
|
82
|
+
) VALUES(?,?,?,?,?,?)`).run(
|
|
83
|
+
repoId,
|
|
84
|
+
'error',
|
|
85
|
+
error.message,
|
|
86
|
+
'Index publication failed before commit for this repository; previous facts and fingerprint were preserved. '
|
|
87
|
+
+ `factKind=${error.site.factKind}`,
|
|
88
|
+
error.site.sourceFile,
|
|
89
|
+
error.site.sourceLine,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
function record(value: unknown): value is Record<string, unknown> {
|
|
2
|
+
return Boolean(value && typeof value === 'object' && !Array.isArray(value));
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
function parsed(value: string): unknown {
|
|
6
|
+
try {
|
|
7
|
+
return JSON.parse(value) as unknown;
|
|
8
|
+
} catch {
|
|
9
|
+
return undefined;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function hasSingleHopHelperReturn(value: unknown): boolean {
|
|
14
|
+
const chain = typeof value === 'string' ? parsed(value) : value;
|
|
15
|
+
return Array.isArray(chain) && chain.some((step) =>
|
|
16
|
+
record(step) && step.bindingOrigin === 'single_hop_helper_return');
|
|
17
|
+
}
|
package/src/db/repositories.ts
CHANGED
|
@@ -12,6 +12,10 @@ import {
|
|
|
12
12
|
selectCallOwner,
|
|
13
13
|
type OwnerCandidate,
|
|
14
14
|
} from '../parsers/004-fact-identity.js';
|
|
15
|
+
import {
|
|
16
|
+
PreparedRepositorySnapshotError,
|
|
17
|
+
type PreparedSnapshotFailureCode,
|
|
18
|
+
} from './013-index-publication-failure.js';
|
|
15
19
|
export interface RepoRow {
|
|
16
20
|
id: number;
|
|
17
21
|
name: string;
|
|
@@ -414,6 +418,19 @@ interface PersistedOwnerCandidate extends OwnerCandidate {
|
|
|
414
418
|
id: number;
|
|
415
419
|
}
|
|
416
420
|
|
|
421
|
+
function bindingSnapshotError(
|
|
422
|
+
failureCode: PreparedSnapshotFailureCode,
|
|
423
|
+
fact: ServiceBindingFact,
|
|
424
|
+
): PreparedRepositorySnapshotError {
|
|
425
|
+
return new PreparedRepositorySnapshotError(failureCode, {
|
|
426
|
+
factKind: 'service_binding',
|
|
427
|
+
sourceFile: fact.sourceFile,
|
|
428
|
+
sourceLine: fact.sourceLine,
|
|
429
|
+
callSiteStartOffset: fact.bindingSiteStartOffset,
|
|
430
|
+
callSiteEndOffset: fact.bindingSiteEndOffset,
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
|
|
417
434
|
function persistedOwnerCandidates(
|
|
418
435
|
db: Db,
|
|
419
436
|
repoId: number,
|
|
@@ -454,16 +471,16 @@ function bindingOwnerId(
|
|
|
454
471
|
);
|
|
455
472
|
if (fact.ownerResolution === 'ownerless_file_scope') {
|
|
456
473
|
if (selected.status !== 'none')
|
|
457
|
-
throw
|
|
474
|
+
throw bindingSnapshotError('binding_owner_mismatch', fact);
|
|
458
475
|
return null;
|
|
459
476
|
}
|
|
460
477
|
if (fact.ownerResolution !== 'owned_exact'
|
|
461
478
|
|| selected.status !== 'resolved'
|
|
462
479
|
|| selected.owner?.qualifiedName !== fact.sourceSymbolQualifiedName)
|
|
463
|
-
throw
|
|
480
|
+
throw bindingSnapshotError('binding_owner_mismatch', fact);
|
|
464
481
|
const owner = selected.owner;
|
|
465
482
|
if (!owner)
|
|
466
|
-
throw
|
|
483
|
+
throw bindingSnapshotError('binding_owner_mismatch', fact);
|
|
467
484
|
return owner.id;
|
|
468
485
|
}
|
|
469
486
|
|
|
@@ -474,13 +491,13 @@ function assertUniquePreparedBindingSites(
|
|
|
474
491
|
for (const row of rows) {
|
|
475
492
|
if (row.bindingSiteStartOffset === undefined
|
|
476
493
|
|| row.bindingSiteEndOffset === undefined)
|
|
477
|
-
throw
|
|
494
|
+
throw bindingSnapshotError('binding_site_missing', row);
|
|
478
495
|
const key = [
|
|
479
496
|
row.sourceFile, row.variableName,
|
|
480
497
|
row.bindingSiteStartOffset, row.bindingSiteEndOffset,
|
|
481
498
|
].join('\u0000');
|
|
482
499
|
if (seen.has(key))
|
|
483
|
-
throw
|
|
500
|
+
throw bindingSnapshotError('duplicate_service_binding_site', row);
|
|
484
501
|
seen.add(key);
|
|
485
502
|
}
|
|
486
503
|
}
|
|
@@ -15,9 +15,12 @@ interface BaseRow { id: number; repoId: number }
|
|
|
15
15
|
interface DesiredOperation { id: number; operationType: string; operationName: string; operationPath: string; paramsJson: string; returnType: string | null; sourceFile: string; sourceLine: number }
|
|
16
16
|
interface ExistingOperation extends DesiredOperation { baseOperationId: number | null }
|
|
17
17
|
|
|
18
|
-
export function materializeCdsExtensionOperations(
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
export function materializeCdsExtensionOperations(
|
|
19
|
+
db: Db,
|
|
20
|
+
workspaceId: number,
|
|
21
|
+
excludedRepoIds: ReadonlySet<number> = new Set(),
|
|
22
|
+
): void {
|
|
23
|
+
const extensions = extensionRows(db, workspaceId, excludedRepoIds);
|
|
21
24
|
db.transaction(() => {
|
|
22
25
|
const changedRepos = new Set<number>();
|
|
23
26
|
for (const extension of extensions) {
|
|
@@ -27,6 +30,27 @@ export function materializeCdsExtensionOperations(db: Db, workspaceId: number):
|
|
|
27
30
|
});
|
|
28
31
|
}
|
|
29
32
|
|
|
33
|
+
function extensionRows(
|
|
34
|
+
db: Db,
|
|
35
|
+
workspaceId: number,
|
|
36
|
+
excludedRepoIds: ReadonlySet<number>,
|
|
37
|
+
): ExtensionRow[] {
|
|
38
|
+
const excluded = [...excludedRepoIds].sort((left, right) => left - right);
|
|
39
|
+
const exclusion = excluded.length > 0
|
|
40
|
+
? ` AND r.id NOT IN (${excluded.map(() => '?').join(',')})`
|
|
41
|
+
: '';
|
|
42
|
+
return db.prepare(`SELECT s.id,r.id repoId,s.service_name serviceName,
|
|
43
|
+
s.qualified_name qualifiedName,s.source_file sourceFile,
|
|
44
|
+
s.extension_module_specifier moduleSpecifier,
|
|
45
|
+
s.extension_imported_symbol importedSymbol,
|
|
46
|
+
s.extension_import_kind importKind
|
|
47
|
+
FROM cds_services s JOIN repositories r ON r.id=s.repo_id
|
|
48
|
+
WHERE r.workspace_id=? AND s.is_extend=1${exclusion}
|
|
49
|
+
ORDER BY r.id,s.id`).all(
|
|
50
|
+
workspaceId, ...excluded,
|
|
51
|
+
) as unknown as ExtensionRow[];
|
|
52
|
+
}
|
|
53
|
+
|
|
30
54
|
function reconcileExtension(db: Db, workspaceId: number, extension: ExtensionRow): boolean {
|
|
31
55
|
const bases = resolveBase(db, workspaceId, extension);
|
|
32
56
|
const status = bases.length === 1 ? 'resolved' : bases.length > 1 ? 'ambiguous' : 'unresolved';
|
|
@@ -45,6 +45,10 @@ import {
|
|
|
45
45
|
invalidatePackageTargetFacts,
|
|
46
46
|
type PackageInvalidationBatch,
|
|
47
47
|
} from '../db/004-package-target-invalidation.js';
|
|
48
|
+
import {
|
|
49
|
+
isPreparedRepositorySnapshotError,
|
|
50
|
+
recordPreparedSnapshotFailure,
|
|
51
|
+
} from '../db/013-index-publication-failure.js';
|
|
48
52
|
import {
|
|
49
53
|
loadRepositorySourceContext,
|
|
50
54
|
type RepositorySourceContext,
|
|
@@ -82,12 +86,21 @@ export async function indexRepository(
|
|
|
82
86
|
): Promise<IndexRepoResult> {
|
|
83
87
|
try {
|
|
84
88
|
const prepared = await prepareRepositoryIndex(repo, force, instrumentation);
|
|
85
|
-
if (
|
|
89
|
+
if (prepared.skipped)
|
|
90
|
+
return { fileCount: 0, diagnosticCount: 0, skipped: true };
|
|
91
|
+
const outcome = db.transaction(() => {
|
|
86
92
|
const batch = createPackageInvalidationBatch([prepared.repo.id]);
|
|
87
|
-
|
|
88
|
-
finalizePackageTargetInvalidations(db, batch);
|
|
93
|
+
const published = publishOneRepository(db, prepared, batch);
|
|
94
|
+
if (published.ok) finalizePackageTargetInvalidations(db, batch);
|
|
95
|
+
return published;
|
|
89
96
|
});
|
|
90
|
-
return
|
|
97
|
+
return outcome.ok
|
|
98
|
+
? {
|
|
99
|
+
fileCount: prepared.fileCount,
|
|
100
|
+
diagnosticCount: prepared.diagnosticCount,
|
|
101
|
+
skipped: false,
|
|
102
|
+
}
|
|
103
|
+
: { fileCount: 0, diagnosticCount: 1, skipped: false };
|
|
91
104
|
} catch (error) {
|
|
92
105
|
recordIndexFailure(db, repo.id, error);
|
|
93
106
|
return { fileCount: 0, diagnosticCount: 1, skipped: false };
|
|
@@ -172,10 +185,58 @@ export function publishPreparedRepositoryIndex(
|
|
|
172
185
|
insertCalls(db, repoId, prepared.parsed.calls);
|
|
173
186
|
db.prepare("UPDATE repositories SET last_indexed_at=?, index_status='indexed', error_count=0, fingerprint=?, fact_generation=COALESCE(fact_generation,0)+1, graph_stale_reason='facts_changed', graph_stale_at=?, fact_analyzer_version=? WHERE id=?").run(now, prepared.fingerprint, now, ANALYZER_VERSION, repoId);
|
|
174
187
|
}
|
|
188
|
+
|
|
189
|
+
export type RepositoryPublicationOutcome =
|
|
190
|
+
| { ok: true }
|
|
191
|
+
| { ok: false; error: unknown };
|
|
192
|
+
|
|
193
|
+
export function publishOneRepository(
|
|
194
|
+
db: Db,
|
|
195
|
+
prepared: PreparedRepositoryIndex,
|
|
196
|
+
invalidations: PackageInvalidationBatch,
|
|
197
|
+
): RepositoryPublicationOutcome {
|
|
198
|
+
try {
|
|
199
|
+
db.transaction(() => withPublicationSavepoint(
|
|
200
|
+
db,
|
|
201
|
+
prepared.repo.id,
|
|
202
|
+
() => publishPreparedRepositoryIndex(db, prepared, invalidations),
|
|
203
|
+
));
|
|
204
|
+
return { ok: true };
|
|
205
|
+
} catch (error) {
|
|
206
|
+
recordIndexFailure(db, prepared.repo.id, error);
|
|
207
|
+
return { ok: false, error };
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function withPublicationSavepoint<T>(
|
|
212
|
+
db: Db,
|
|
213
|
+
repoId: number,
|
|
214
|
+
publish: () => T,
|
|
215
|
+
): T {
|
|
216
|
+
const name = `service_flow_repository_${repoId}`;
|
|
217
|
+
db.exec(`SAVEPOINT ${name}`);
|
|
218
|
+
try {
|
|
219
|
+
const result = publish();
|
|
220
|
+
db.exec(`RELEASE SAVEPOINT ${name}`);
|
|
221
|
+
return result;
|
|
222
|
+
} catch (error) {
|
|
223
|
+
db.exec(`ROLLBACK TO SAVEPOINT ${name}`);
|
|
224
|
+
db.exec(`RELEASE SAVEPOINT ${name}`);
|
|
225
|
+
throw error;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
175
229
|
export function recordIndexFailure(db: Db, repoId: number, error: unknown): void {
|
|
230
|
+
if (isPreparedRepositorySnapshotError(error)) {
|
|
231
|
+
recordPreparedSnapshotFailure(db, repoId, error);
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
176
234
|
const message = errorMessage(error);
|
|
177
235
|
db.prepare("UPDATE repositories SET index_status='failed', error_count=1 WHERE id=?").run(repoId);
|
|
178
|
-
db.prepare(
|
|
236
|
+
db.prepare(`DELETE FROM diagnostics WHERE repo_id=? AND (
|
|
237
|
+
code IN ('index_failed_snapshot_preserved','source_read_failed')
|
|
238
|
+
OR code GLOB 'invalid_prepared_repository_snapshot:*'
|
|
239
|
+
)`).run(repoId);
|
|
179
240
|
db.prepare('INSERT INTO diagnostics(repo_id,severity,code,message) VALUES(?,?,?,?)').run(repoId, 'error', 'source_read_failed', `Index failed before publication; previous facts and fingerprint were preserved. ${message}`);
|
|
180
241
|
}
|
|
181
242
|
async function parseAllSourceFacts(
|