@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.
Files changed (101) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +27 -9
  3. package/TECHNICAL-NOTE.md +36 -0
  4. package/dist/chunk-3N3B5KHV.js +19596 -0
  5. package/dist/chunk-3N3B5KHV.js.map +1 -0
  6. package/dist/cli.js +2645 -521
  7. package/dist/cli.js.map +1 -1
  8. package/dist/index.d.ts +67 -2
  9. package/dist/index.js +1 -1
  10. package/package.json +1 -1
  11. package/src/cli/001-index-summary.ts +22 -0
  12. package/src/cli/003-doctor-package-resolution.ts +68 -0
  13. package/src/cli/doctor.ts +25 -14
  14. package/src/cli.ts +151 -87
  15. package/src/db/000-call-fact-repository.ts +499 -340
  16. package/src/db/001-fact-lifecycle.ts +60 -30
  17. package/src/db/002-fact-json-inventory.ts +169 -0
  18. package/src/db/003-current-fact-semantics.ts +699 -0
  19. package/src/db/004-package-target-invalidation.ts +183 -0
  20. package/src/db/005-schema-structure.ts +201 -0
  21. package/src/db/006-relative-symbol-resolution.ts +464 -0
  22. package/src/db/007-package-fact-semantics.ts +573 -0
  23. package/src/db/008-relative-fact-semantics.ts +210 -0
  24. package/src/db/009-binding-fact-semantics.ts +352 -0
  25. package/src/db/010-package-symbol-surface-semantics.ts +320 -0
  26. package/src/db/011-symbol-call-semantics.ts +144 -0
  27. package/src/db/012-binding-reference-proof.ts +268 -0
  28. package/src/db/013-index-publication-failure.ts +91 -0
  29. package/src/db/014-binding-helper-provenance.ts +17 -0
  30. package/src/db/migrations.ts +16 -3
  31. package/src/db/repositories.ts +130 -6
  32. package/src/db/schema.ts +4 -2
  33. package/src/index.ts +12 -0
  34. package/src/indexer/cds-extension-resolver.ts +27 -3
  35. package/src/indexer/repository-indexer.ts +135 -13
  36. package/src/indexer/workspace-indexer.ts +237 -34
  37. package/src/linker/003-package-import-symbol-resolver.ts +363 -131
  38. package/src/linker/004-event-subscription-handler-linker.ts +34 -11
  39. package/src/linker/005-odata-path-structure.ts +371 -0
  40. package/src/linker/006-event-template-link.ts +72 -0
  41. package/src/linker/007-call-edge-insertion.ts +568 -0
  42. package/src/linker/cross-repo-linker.ts +4 -166
  43. package/src/linker/odata-path-normalizer.ts +273 -180
  44. package/src/linker/service-resolver.ts +197 -77
  45. package/src/parsers/000-direct-query-execution.ts +11 -0
  46. package/src/parsers/002-symbol-import-bindings.ts +516 -0
  47. package/src/parsers/003-package-public-surface.ts +661 -0
  48. package/src/parsers/004-fact-identity.ts +108 -0
  49. package/src/parsers/005-event-subscription-facts.ts +281 -0
  50. package/src/parsers/006-binding-identity.ts +348 -0
  51. package/src/parsers/007-source-fact-reconciliation.ts +105 -0
  52. package/src/parsers/008-package-surface-publication.ts +82 -0
  53. package/src/parsers/009-symbol-call-facts.ts +528 -0
  54. package/src/parsers/010-package-public-surface-analysis.ts +352 -0
  55. package/src/parsers/011-binding-lexical-scope.ts +583 -0
  56. package/src/parsers/012-package-fact-contract.ts +306 -0
  57. package/src/parsers/013-executable-body-eligibility.ts +35 -0
  58. package/src/parsers/014-service-binding-helper-flow.ts +306 -0
  59. package/src/parsers/015-service-binding-collector.ts +693 -0
  60. package/src/parsers/016-local-symbol-reference.ts +261 -0
  61. package/src/parsers/017-symbol-derived-contexts.ts +268 -0
  62. package/src/parsers/018-package-commonjs-syntax.ts +142 -0
  63. package/src/parsers/019-binding-assignment-targets.ts +76 -0
  64. package/src/parsers/020-stable-local-value.ts +217 -0
  65. package/src/parsers/021-binding-visibility.ts +168 -0
  66. package/src/parsers/022-outbound-expression-analysis.ts +700 -0
  67. package/src/parsers/023-outbound-call-classifier.ts +692 -0
  68. package/src/parsers/operation-path-analysis.ts +6 -1
  69. package/src/parsers/outbound-call-parser.ts +162 -512
  70. package/src/parsers/package-json-parser.ts +45 -3
  71. package/src/parsers/service-binding-parser-helpers.ts +86 -15
  72. package/src/parsers/service-binding-parser.ts +147 -597
  73. package/src/parsers/symbol-parser.ts +513 -352
  74. package/src/trace/002-trace-diagnostics.ts +36 -1
  75. package/src/trace/007-implementation-start-diagnostic.ts +1 -0
  76. package/src/trace/011-event-subscriber-traversal.ts +100 -8
  77. package/src/trace/013-trace-root-scopes.ts +2 -3
  78. package/src/trace/014-compact-contract.ts +6 -0
  79. package/src/trace/015-trace-edge-recorder.ts +61 -4
  80. package/src/trace/016-compact-projector.ts +15 -17
  81. package/src/trace/019-trace-edge-semantics.ts +6 -10
  82. package/src/trace/020-compact-field-projection.ts +122 -38
  83. package/src/trace/021-compact-decision-normalization.ts +171 -0
  84. package/src/trace/022-trace-fact-preflight.ts +21 -0
  85. package/src/trace/023-nested-event-scopes.ts +23 -0
  86. package/src/trace/024-compact-observation-decision.ts +81 -0
  87. package/src/trace/025-trace-implementation-scope.ts +123 -0
  88. package/src/trace/026-trace-start-scope.ts +336 -0
  89. package/src/trace/027-trace-scope-execution.ts +566 -0
  90. package/src/trace/028-trace-operation-execution.ts +336 -0
  91. package/src/trace/029-trace-start-implementation.ts +172 -0
  92. package/src/trace/030-event-runtime-resolution.ts +151 -0
  93. package/src/trace/031-local-call-expansion.ts +37 -0
  94. package/src/trace/implementation-hints.ts +9 -6
  95. package/src/trace/selectors.ts +1 -0
  96. package/src/trace/trace-engine.ts +122 -624
  97. package/src/types.ts +57 -0
  98. package/src/utils/001-placeholders.ts +188 -10
  99. package/src/version.ts +1 -1
  100. package/dist/chunk-ZQABU7MR.js +0 -12151
  101. 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(db: Db, workspaceId: number): void {
19
- const extensions = db.prepare(`SELECT s.id,r.id repoId,s.service_name serviceName,s.qualified_name qualifiedName,s.source_file sourceFile,s.extension_module_specifier moduleSpecifier,s.extension_imported_symbol importedSymbol,s.extension_import_kind importKind
20
- FROM cds_services s JOIN repositories r ON r.id=s.repo_id WHERE r.workspace_id=? AND s.is_extend=1`).all(workspaceId) as unknown as ExtensionRow[];
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 { parseOutboundCalls } from '../parsers/outbound-call-parser.js';
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 (!prepared.skipped) db.transaction(() => publishPreparedRepositoryIndex(db, prepared));
68
- return { fileCount: prepared.fileCount, diagnosticCount: prepared.diagnosticCount, skipped: prepared.skipped };
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 parsed = await parseAllSourceFacts(repo.absolute_path, sources);
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(db: Db, prepared: PreparedRepositoryIndex): void {
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 || !prepared.kind) throw new Error('Prepared repository index is missing publication facts');
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
- db.prepare('UPDATE repositories SET package_name=?, package_version=?, dependencies_json=?, kind=?, index_status=? WHERE id=?').run(prepared.packageFacts.packageName, prepared.packageFacts.packageVersion, JSON.stringify(prepared.packageFacts.dependencies), prepared.kind, 'indexing', repoId);
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("DELETE FROM diagnostics WHERE repo_id=? AND code IN ('index_failed_snapshot_preserved','source_read_failed')").run(repoId);
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
- facts.bindings.push(...(await parseServiceBindings(root, file, sources)));
145
- const symbolFacts = await parseExecutableSymbols(root, file, sources);
146
- facts.symbols.push(...symbolFacts.symbols);
147
- facts.symbolCalls.push(...symbolFacts.calls);
148
- facts.calls.push(...(await parseOutboundCalls(root, file, sources)));
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 { prepareRepositoryIndex, publishPreparedRepositoryIndex, recordIndexFailure, type PreparedRepositoryIndex } from './repository-indexer.js';
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<{ repoCount: number; indexedCount: number; skippedCount: number; fileCount: number; diagnosticCount: number }> {
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
- let fileCount = 0;
93
- let diagnosticCount = 0;
94
- let skippedCount = 0;
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
- for (const repo of repos) {
99
- activeRepoId = repo.id;
100
- const result = await prepareRepositoryIndex(repo, options.force);
101
- preparedRows.push(result);
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.prepare("UPDATE index_runs SET finished_at=?, status='failed', file_count=?, diagnostic_count=?, error_message=? WHERE id=?").run(new Date().toISOString(), fileCount, diagnosticCount + 1, errorMessage(error), runId);
118
- if (activeRepoId && preparedRows.length < repos.length) recordIndexFailure(db, activeRepoId, error);
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
+ }