@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
|
@@ -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';
|
|
@@ -18,16 +18,37 @@ import { classifyRepository } from '../discovery/classify-repository.js';
|
|
|
18
18
|
import { parseCdsFile } from '../parsers/cds-parser.js';
|
|
19
19
|
import { parseDecorators } from '../parsers/decorator-parser.js';
|
|
20
20
|
import { parseHandlerRegistrations } from '../parsers/handler-registration-parser.js';
|
|
21
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
classifyOutboundCallsInSource,
|
|
23
|
+
parseOutboundCalls,
|
|
24
|
+
} from '../parsers/outbound-call-parser.js';
|
|
22
25
|
import { parseExecutableSymbols } from '../parsers/symbol-parser.js';
|
|
23
26
|
import {
|
|
24
27
|
loadPackageJsonSnapshot,
|
|
25
28
|
} from '../parsers/package-json-parser.js';
|
|
26
29
|
import { parseServiceBindings } from '../parsers/service-binding-parser.js';
|
|
30
|
+
import { reconcileSourceFacts } from '../parsers/007-source-fact-reconciliation.js';
|
|
31
|
+
import {
|
|
32
|
+
analyzeRepositoryPackageSurface,
|
|
33
|
+
mergePackageSymbolEvidence,
|
|
34
|
+
} from '../parsers/008-package-surface-publication.js';
|
|
35
|
+
import type {
|
|
36
|
+
PackagePublicSurfaceFact,
|
|
37
|
+
} from '../parsers/003-package-public-surface.js';
|
|
27
38
|
import { normalizePath } from '../utils/path-utils.js';
|
|
28
39
|
import { errorMessage } from '../utils/diagnostics.js';
|
|
29
40
|
import { sha256Text } from '../utils/hashing.js';
|
|
30
41
|
import { ANALYZER_VERSION } from '../version.js';
|
|
42
|
+
import {
|
|
43
|
+
createPackageInvalidationBatch,
|
|
44
|
+
finalizePackageTargetInvalidations,
|
|
45
|
+
invalidatePackageTargetFacts,
|
|
46
|
+
type PackageInvalidationBatch,
|
|
47
|
+
} from '../db/004-package-target-invalidation.js';
|
|
48
|
+
import {
|
|
49
|
+
isPreparedRepositorySnapshotError,
|
|
50
|
+
recordPreparedSnapshotFailure,
|
|
51
|
+
} from '../db/013-index-publication-failure.js';
|
|
31
52
|
import {
|
|
32
53
|
loadRepositorySourceContext,
|
|
33
54
|
type RepositorySourceContext,
|
|
@@ -55,6 +76,7 @@ export interface PreparedRepositoryIndex extends IndexRepoResult {
|
|
|
55
76
|
fingerprint?: string;
|
|
56
77
|
kind?: string;
|
|
57
78
|
parsed?: ParsedFacts;
|
|
79
|
+
packagePublicSurface?: PackagePublicSurfaceFact;
|
|
58
80
|
}
|
|
59
81
|
export async function indexRepository(
|
|
60
82
|
db: Db,
|
|
@@ -64,8 +86,21 @@ export async function indexRepository(
|
|
|
64
86
|
): Promise<IndexRepoResult> {
|
|
65
87
|
try {
|
|
66
88
|
const prepared = await prepareRepositoryIndex(repo, force, instrumentation);
|
|
67
|
-
if (
|
|
68
|
-
|
|
89
|
+
if (prepared.skipped)
|
|
90
|
+
return { fileCount: 0, diagnosticCount: 0, skipped: true };
|
|
91
|
+
const outcome = db.transaction(() => {
|
|
92
|
+
const batch = createPackageInvalidationBatch([prepared.repo.id]);
|
|
93
|
+
const published = publishOneRepository(db, prepared, batch);
|
|
94
|
+
if (published.ok) finalizePackageTargetInvalidations(db, batch);
|
|
95
|
+
return published;
|
|
96
|
+
});
|
|
97
|
+
return outcome.ok
|
|
98
|
+
? {
|
|
99
|
+
fileCount: prepared.fileCount,
|
|
100
|
+
diagnosticCount: prepared.diagnosticCount,
|
|
101
|
+
skipped: false,
|
|
102
|
+
}
|
|
103
|
+
: { fileCount: 0, diagnosticCount: 1, skipped: false };
|
|
69
104
|
} catch (error) {
|
|
70
105
|
recordIndexFailure(db, repo.id, error);
|
|
71
106
|
return { fileCount: 0, diagnosticCount: 1, skipped: false };
|
|
@@ -89,13 +124,21 @@ export async function prepareRepositoryIndex(
|
|
|
89
124
|
sources, packageFacts, packageSnapshot.rawText,
|
|
90
125
|
);
|
|
91
126
|
if (!force && repo.fingerprint === fingerprint) return { repo, fileCount: 0, diagnosticCount: 0, skipped: true };
|
|
92
|
-
const
|
|
127
|
+
const parsedFacts = await parseAllSourceFacts(repo.absolute_path, sources);
|
|
128
|
+
const packageSurface = analyzeRepositoryPackageSurface(
|
|
129
|
+
packageFacts, packageSnapshot.manifest, sources,
|
|
130
|
+
);
|
|
131
|
+
const parsed = {
|
|
132
|
+
...parsedFacts,
|
|
133
|
+
symbols: mergePackageSymbolEvidence(parsedFacts.symbols, packageSurface),
|
|
134
|
+
};
|
|
93
135
|
return {
|
|
94
136
|
repo,
|
|
95
137
|
packageFacts,
|
|
96
138
|
fingerprint,
|
|
97
139
|
kind: await classifyRepository(repo.absolute_path, packageFacts),
|
|
98
140
|
parsed,
|
|
141
|
+
packagePublicSurface: packageSurface.surface,
|
|
99
142
|
fileCount: sourceFiles.length,
|
|
100
143
|
diagnosticCount: parsed.handlers.filter((handler) =>
|
|
101
144
|
handler.hasHandlerDecorator
|
|
@@ -104,12 +147,31 @@ export async function prepareRepositoryIndex(
|
|
|
104
147
|
skipped: false,
|
|
105
148
|
};
|
|
106
149
|
}
|
|
107
|
-
export function publishPreparedRepositoryIndex(
|
|
150
|
+
export function publishPreparedRepositoryIndex(
|
|
151
|
+
db: Db,
|
|
152
|
+
prepared: PreparedRepositoryIndex,
|
|
153
|
+
invalidations: PackageInvalidationBatch,
|
|
154
|
+
): void {
|
|
108
155
|
if (prepared.skipped) return;
|
|
109
|
-
if (!prepared.packageFacts || !prepared.parsed || !prepared.fingerprint
|
|
156
|
+
if (!prepared.packageFacts || !prepared.parsed || !prepared.fingerprint
|
|
157
|
+
|| !prepared.kind || !prepared.packagePublicSurface)
|
|
158
|
+
throw new Error('Prepared repository index is missing publication facts');
|
|
110
159
|
const now = new Date().toISOString();
|
|
111
160
|
const repoId = prepared.repo.id;
|
|
112
|
-
|
|
161
|
+
invalidatePackageTargetFacts(
|
|
162
|
+
db, repoId, prepared.packageFacts.packageName, invalidations,
|
|
163
|
+
);
|
|
164
|
+
db.prepare(`UPDATE repositories SET package_name=?, package_version=?,
|
|
165
|
+
dependencies_json=?,package_public_surface_json=?,kind=?,index_status=?
|
|
166
|
+
WHERE id=?`).run(
|
|
167
|
+
prepared.packageFacts.packageName,
|
|
168
|
+
prepared.packageFacts.packageVersion,
|
|
169
|
+
JSON.stringify(prepared.packageFacts.dependencies),
|
|
170
|
+
JSON.stringify(prepared.packagePublicSurface),
|
|
171
|
+
prepared.kind,
|
|
172
|
+
'indexing',
|
|
173
|
+
repoId,
|
|
174
|
+
);
|
|
113
175
|
clearRepoFacts(db, repoId);
|
|
114
176
|
insertRequires(db, repoId, prepared.packageFacts.cdsRequires);
|
|
115
177
|
const fileStmt = db.prepare('INSERT INTO files(repo_id,relative_path,extension,sha256,size_bytes,last_indexed_at) VALUES(?,?,?,?,?,?) ON CONFLICT(repo_id,relative_path) DO UPDATE SET sha256=excluded.sha256,size_bytes=excluded.size_bytes,last_indexed_at=excluded.last_indexed_at');
|
|
@@ -123,10 +185,58 @@ export function publishPreparedRepositoryIndex(db: Db, prepared: PreparedReposit
|
|
|
123
185
|
insertCalls(db, repoId, prepared.parsed.calls);
|
|
124
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);
|
|
125
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
|
+
|
|
126
229
|
export function recordIndexFailure(db: Db, repoId: number, error: unknown): void {
|
|
230
|
+
if (isPreparedRepositorySnapshotError(error)) {
|
|
231
|
+
recordPreparedSnapshotFailure(db, repoId, error);
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
127
234
|
const message = errorMessage(error);
|
|
128
235
|
db.prepare("UPDATE repositories SET index_status='failed', error_count=1 WHERE id=?").run(repoId);
|
|
129
|
-
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);
|
|
130
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}`);
|
|
131
241
|
}
|
|
132
242
|
async function parseAllSourceFacts(
|
|
@@ -139,13 +249,25 @@ async function parseAllSourceFacts(
|
|
|
139
249
|
facts.fileRecords.push({ relativePath: normalizePath(file), extension: path.extname(file), sha256: sha256Text(snapshot.text), sizeBytes: snapshot.sizeBytes });
|
|
140
250
|
if (file.endsWith('.cds')) facts.services.push(...(await parseCdsFile(root, file, sources)));
|
|
141
251
|
if (/\.[jt]s$/.test(file)) {
|
|
252
|
+
const source = snapshot.sourceFile();
|
|
253
|
+
const classified = classifyOutboundCallsInSource(source, file);
|
|
142
254
|
facts.handlers.push(...(await parseDecorators(root, file, sources)));
|
|
143
255
|
facts.registrations.push(...(await parseHandlerRegistrations(root, file, sources)));
|
|
144
|
-
|
|
145
|
-
const symbolFacts = await parseExecutableSymbols(
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
256
|
+
const bindings = await parseServiceBindings(root, file, sources);
|
|
257
|
+
const symbolFacts = await parseExecutableSymbols(
|
|
258
|
+
root, file, sources, classified,
|
|
259
|
+
);
|
|
260
|
+
const outboundCalls = await parseOutboundCalls(
|
|
261
|
+
root, file, sources, classified, bindings,
|
|
262
|
+
);
|
|
263
|
+
const reconciled = reconcileSourceFacts(
|
|
264
|
+
source, classified, bindings, outboundCalls,
|
|
265
|
+
symbolFacts.symbols, symbolFacts.calls,
|
|
266
|
+
);
|
|
267
|
+
facts.bindings.push(...reconciled.bindings);
|
|
268
|
+
facts.symbols.push(...reconciled.symbols);
|
|
269
|
+
facts.symbolCalls.push(...reconciled.symbolCalls);
|
|
270
|
+
facts.calls.push(...reconciled.outboundCalls);
|
|
149
271
|
}
|
|
150
272
|
}
|
|
151
273
|
return facts;
|
|
@@ -1,8 +1,23 @@
|
|
|
1
1
|
import type { Db } from '../db/connection.js';
|
|
2
2
|
import { listRepositories, reposByName } from '../db/repositories.js';
|
|
3
3
|
import { errorMessage } from '../utils/diagnostics.js';
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
prepareRepositoryIndex,
|
|
6
|
+
publishOneRepository,
|
|
7
|
+
recordIndexFailure,
|
|
8
|
+
type PreparedRepositoryIndex,
|
|
9
|
+
} from './repository-indexer.js';
|
|
5
10
|
import { materializeCdsExtensionOperations } from './cds-extension-resolver.js';
|
|
11
|
+
import {
|
|
12
|
+
createPackageInvalidationBatch,
|
|
13
|
+
finalizePackageTargetInvalidations,
|
|
14
|
+
mergePackageInvalidationEffects,
|
|
15
|
+
type PackageInvalidationBatch,
|
|
16
|
+
} from '../db/004-package-target-invalidation.js';
|
|
17
|
+
import {
|
|
18
|
+
isPreparedRepositorySnapshotError,
|
|
19
|
+
} from '../db/013-index-publication-failure.js';
|
|
20
|
+
import { binaryCompare } from '../parsers/004-fact-identity.js';
|
|
6
21
|
// Ownerless rows predate PID coordination; this matches doctor's stale-run threshold without taking over a recent legacy writer.
|
|
7
22
|
const LEGACY_OWNER_RECOVERY_MS = 60 * 60 * 1_000;
|
|
8
23
|
type RunningIndexRow = Record<string, unknown>;
|
|
@@ -76,46 +91,234 @@ export function claimIndexRun(
|
|
|
76
91
|
throw error;
|
|
77
92
|
}
|
|
78
93
|
}
|
|
94
|
+
export interface IndexWorkspaceSummary {
|
|
95
|
+
repoCount: number;
|
|
96
|
+
indexedCount: number;
|
|
97
|
+
skippedCount: number;
|
|
98
|
+
failedCount: number;
|
|
99
|
+
failedRepos: Array<{ name: string; code: string }>;
|
|
100
|
+
fileCount: number;
|
|
101
|
+
diagnosticCount: number;
|
|
102
|
+
}
|
|
103
|
+
|
|
79
104
|
export async function indexWorkspace(
|
|
80
105
|
db: Db,
|
|
81
106
|
workspaceId: number,
|
|
82
107
|
options: { repo?: string; force: boolean; injectDerivedMaterializationFailure?: boolean },
|
|
83
|
-
): Promise<
|
|
84
|
-
const repos = options.repo
|
|
85
|
-
? reposByName(db, options.repo, workspaceId)
|
|
86
|
-
: listRepositories(db, workspaceId);
|
|
87
|
-
if (options.repo && repos.length === 0)
|
|
88
|
-
throw new Error(`selector_repo_not_found: no indexed repository matched ${options.repo}.`);
|
|
89
|
-
if (options.repo && repos.length > 1)
|
|
90
|
-
throw new Error(`selector_repo_ambiguous: repository selector ${options.repo} matched ${repos.length} repositories; use a unique repository name.`);
|
|
108
|
+
): Promise<IndexWorkspaceSummary> {
|
|
109
|
+
const repos = selectedRepositories(db, workspaceId, options.repo);
|
|
91
110
|
const runId = claimIndexRun(db, workspaceId, repos.length);
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
const preparedRows: PreparedRepositoryIndex[] = [];
|
|
96
|
-
let activeRepoId: number | undefined;
|
|
111
|
+
const state: PreparationState = {
|
|
112
|
+
fileCount: 0, diagnosticCount: 0, skippedCount: 0, rows: [],
|
|
113
|
+
};
|
|
97
114
|
try {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
fileCount += result.fileCount;
|
|
103
|
-
diagnosticCount += result.diagnosticCount;
|
|
104
|
-
skippedCount += result.skipped ? 1 : 0;
|
|
105
|
-
}
|
|
106
|
-
db.transaction(() => {
|
|
107
|
-
for (const row of preparedRows) {
|
|
108
|
-
activeRepoId = row.repo.id;
|
|
109
|
-
publishPreparedRepositoryIndex(db, row);
|
|
110
|
-
}
|
|
111
|
-
if (options.injectDerivedMaterializationFailure) throw new Error('Injected derived materialization failure');
|
|
112
|
-
materializeCdsExtensionOperations(db, workspaceId);
|
|
113
|
-
db.prepare("UPDATE index_runs SET finished_at=?, status='success', file_count=?, diagnostic_count=? WHERE id=?").run(new Date().toISOString(), fileCount, diagnosticCount, runId);
|
|
114
|
-
});
|
|
115
|
-
return { repoCount: repos.length, indexedCount: repos.length - skippedCount, skippedCount, fileCount, diagnosticCount };
|
|
115
|
+
await prepareRepositories(repos, options.force, state);
|
|
116
|
+
return publishPreparedWorkspaceRows(
|
|
117
|
+
db, workspaceId, runId, state.rows, options,
|
|
118
|
+
);
|
|
116
119
|
} catch (error) {
|
|
117
|
-
db
|
|
118
|
-
if (activeRepoId &&
|
|
120
|
+
finishFailedRun(db, runId, state, error);
|
|
121
|
+
if (state.activeRepoId && state.rows.length < repos.length)
|
|
122
|
+
recordIndexFailure(db, state.activeRepoId, error);
|
|
119
123
|
throw error;
|
|
120
124
|
}
|
|
121
125
|
}
|
|
126
|
+
|
|
127
|
+
type IndexRepository = ReturnType<typeof listRepositories>[number];
|
|
128
|
+
interface PreparationState {
|
|
129
|
+
fileCount: number;
|
|
130
|
+
diagnosticCount: number;
|
|
131
|
+
skippedCount: number;
|
|
132
|
+
rows: PreparedRepositoryIndex[];
|
|
133
|
+
activeRepoId?: number;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
interface PublicationState {
|
|
137
|
+
fileCount: number;
|
|
138
|
+
diagnosticCount: number;
|
|
139
|
+
skippedCount: number;
|
|
140
|
+
indexedCount: number;
|
|
141
|
+
publicationFailureCount: number;
|
|
142
|
+
failedRepoIds: Set<number>;
|
|
143
|
+
failedRepos: Array<{ name: string; code: string }>;
|
|
144
|
+
rows: readonly PreparedRepositoryIndex[];
|
|
145
|
+
activeRepoId?: number;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function selectedRepositories(
|
|
149
|
+
db: Db,
|
|
150
|
+
workspaceId: number,
|
|
151
|
+
repoName: string | undefined,
|
|
152
|
+
): IndexRepository[] {
|
|
153
|
+
const repos = repoName
|
|
154
|
+
? reposByName(db, repoName, workspaceId)
|
|
155
|
+
: listRepositories(db, workspaceId);
|
|
156
|
+
if (repoName && repos.length === 0)
|
|
157
|
+
throw new Error(
|
|
158
|
+
`selector_repo_not_found: no indexed repository matched ${repoName}.`,
|
|
159
|
+
);
|
|
160
|
+
if (repoName && repos.length > 1)
|
|
161
|
+
throw new Error(
|
|
162
|
+
`selector_repo_ambiguous: repository selector ${repoName} matched ${repos.length} repositories; use a unique repository name.`,
|
|
163
|
+
);
|
|
164
|
+
return repos;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async function prepareRepositories(
|
|
168
|
+
repos: readonly IndexRepository[],
|
|
169
|
+
force: boolean,
|
|
170
|
+
state: PreparationState,
|
|
171
|
+
): Promise<void> {
|
|
172
|
+
for (const repo of repos) {
|
|
173
|
+
state.activeRepoId = repo.id;
|
|
174
|
+
const result = await prepareRepositoryIndex(repo, force);
|
|
175
|
+
state.rows.push(result);
|
|
176
|
+
state.fileCount += result.fileCount;
|
|
177
|
+
state.diagnosticCount += result.diagnosticCount;
|
|
178
|
+
state.skippedCount += result.skipped ? 1 : 0;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function publishPreparedWorkspaceRows(
|
|
183
|
+
db: Db,
|
|
184
|
+
workspaceId: number,
|
|
185
|
+
runId: number,
|
|
186
|
+
rows: readonly PreparedRepositoryIndex[],
|
|
187
|
+
options: { injectDerivedMaterializationFailure?: boolean } = {},
|
|
188
|
+
): IndexWorkspaceSummary {
|
|
189
|
+
const state = publicationState(rows);
|
|
190
|
+
db.transaction(() => {
|
|
191
|
+
const effects = createPackageInvalidationBatch([]);
|
|
192
|
+
const publishedRepoIds: number[] = [];
|
|
193
|
+
for (const row of state.rows) {
|
|
194
|
+
state.activeRepoId = row.repo.id;
|
|
195
|
+
if (row.skipped) continue;
|
|
196
|
+
const result = publishPreparedRow(db, row);
|
|
197
|
+
if (result.status === 'failed') {
|
|
198
|
+
recordPublicationFailure(db, state, row, result.error);
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
state.indexedCount += 1;
|
|
202
|
+
publishedRepoIds.push(row.repo.id);
|
|
203
|
+
mergePackageInvalidationEffects(effects, result.effects);
|
|
204
|
+
}
|
|
205
|
+
if (options.injectDerivedMaterializationFailure)
|
|
206
|
+
throw new Error('Injected derived materialization failure');
|
|
207
|
+
materializeCdsExtensionOperations(
|
|
208
|
+
db, workspaceId, state.failedRepoIds,
|
|
209
|
+
);
|
|
210
|
+
const invalidations = createPackageInvalidationBatch(publishedRepoIds);
|
|
211
|
+
mergePackageInvalidationEffects(invalidations, effects);
|
|
212
|
+
finalizePackageTargetInvalidations(db, invalidations);
|
|
213
|
+
finishCompletedRun(db, runId, state);
|
|
214
|
+
});
|
|
215
|
+
return indexSummary(rows.length, state);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function publicationState(
|
|
219
|
+
rows: readonly PreparedRepositoryIndex[],
|
|
220
|
+
): PublicationState {
|
|
221
|
+
return {
|
|
222
|
+
rows,
|
|
223
|
+
fileCount: rows.reduce((total, row) => total + row.fileCount, 0),
|
|
224
|
+
diagnosticCount: rows.reduce(
|
|
225
|
+
(total, row) => total + row.diagnosticCount, 0,
|
|
226
|
+
),
|
|
227
|
+
skippedCount: rows.filter((row) => row.skipped).length,
|
|
228
|
+
indexedCount: 0,
|
|
229
|
+
publicationFailureCount: 0,
|
|
230
|
+
failedRepoIds: new Set(),
|
|
231
|
+
failedRepos: [],
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
type PublicationResult =
|
|
236
|
+
| { status: 'published'; effects: PackageInvalidationBatch }
|
|
237
|
+
| { status: 'failed'; error: unknown };
|
|
238
|
+
|
|
239
|
+
function publishPreparedRow(
|
|
240
|
+
db: Db,
|
|
241
|
+
row: PreparedRepositoryIndex,
|
|
242
|
+
): PublicationResult {
|
|
243
|
+
const effects = createPackageInvalidationBatch([row.repo.id]);
|
|
244
|
+
const outcome = publishOneRepository(db, row, effects);
|
|
245
|
+
if (!outcome.ok && !isPreparedRepositorySnapshotError(outcome.error))
|
|
246
|
+
throw outcome.error;
|
|
247
|
+
return outcome.ok
|
|
248
|
+
? { status: 'published', effects }
|
|
249
|
+
: { status: 'failed', error: outcome.error };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function recordPublicationFailure(
|
|
253
|
+
db: Db,
|
|
254
|
+
state: PublicationState,
|
|
255
|
+
row: PreparedRepositoryIndex,
|
|
256
|
+
error: unknown,
|
|
257
|
+
): void {
|
|
258
|
+
state.failedRepoIds.add(row.repo.id);
|
|
259
|
+
state.failedRepos.push({
|
|
260
|
+
name: row.repo.name,
|
|
261
|
+
code: isPreparedRepositorySnapshotError(error)
|
|
262
|
+
? error.message : 'source_read_failed',
|
|
263
|
+
});
|
|
264
|
+
state.publicationFailureCount += 1;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function finishCompletedRun(
|
|
268
|
+
db: Db,
|
|
269
|
+
runId: number,
|
|
270
|
+
state: PublicationState,
|
|
271
|
+
): void {
|
|
272
|
+
const status = completedRunStatus(
|
|
273
|
+
state.rows.length, state.publicationFailureCount,
|
|
274
|
+
);
|
|
275
|
+
const error = status === 'success' ? null
|
|
276
|
+
: `${state.publicationFailureCount} repositories failed index publication.`;
|
|
277
|
+
db.prepare(`UPDATE index_runs SET finished_at=?,status=?,
|
|
278
|
+
file_count=?,diagnostic_count=?,error_message=? WHERE id=?`).run(
|
|
279
|
+
new Date().toISOString(), status, state.fileCount,
|
|
280
|
+
completedDiagnosticCount(state), error, runId,
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function completedRunStatus(
|
|
285
|
+
repoCount: number,
|
|
286
|
+
failedCount: number,
|
|
287
|
+
): 'success' | 'partial_failure' | 'failed' {
|
|
288
|
+
if (failedCount === 0) return 'success';
|
|
289
|
+
return failedCount === repoCount ? 'failed' : 'partial_failure';
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function completedDiagnosticCount(state: PublicationState): number {
|
|
293
|
+
return state.diagnosticCount + state.publicationFailureCount;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function finishFailedRun(
|
|
297
|
+
db: Db,
|
|
298
|
+
runId: number,
|
|
299
|
+
state: PreparationState,
|
|
300
|
+
error: unknown,
|
|
301
|
+
): void {
|
|
302
|
+
db.prepare(`UPDATE index_runs SET finished_at=?,status='failed',
|
|
303
|
+
file_count=?,diagnostic_count=?,error_message=? WHERE id=?`).run(
|
|
304
|
+
new Date().toISOString(), state.fileCount, state.diagnosticCount + 1,
|
|
305
|
+
errorMessage(error), runId,
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function indexSummary(
|
|
310
|
+
repoCount: number,
|
|
311
|
+
state: PublicationState,
|
|
312
|
+
): IndexWorkspaceSummary {
|
|
313
|
+
return {
|
|
314
|
+
repoCount,
|
|
315
|
+
indexedCount: state.indexedCount,
|
|
316
|
+
skippedCount: state.skippedCount,
|
|
317
|
+
failedCount: state.publicationFailureCount,
|
|
318
|
+
failedRepos: [...state.failedRepos].sort((left, right) =>
|
|
319
|
+
binaryCompare(left.name, right.name)
|
|
320
|
+
|| binaryCompare(left.code, right.code)),
|
|
321
|
+
fileCount: state.fileCount,
|
|
322
|
+
diagnosticCount: completedDiagnosticCount(state),
|
|
323
|
+
};
|
|
324
|
+
}
|