@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.
Files changed (56) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +11 -5
  3. package/TECHNICAL-NOTE.md +9 -0
  4. package/dist/{chunk-AEM4JY22.js → chunk-3N3B5KHV.js} +6983 -5500
  5. package/dist/chunk-3N3B5KHV.js.map +1 -0
  6. package/dist/cli.js +317 -105
  7. package/dist/cli.js.map +1 -1
  8. package/dist/index.d.ts +7 -1
  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.ts +151 -87
  13. package/src/db/000-call-fact-repository.ts +45 -19
  14. package/src/db/003-current-fact-semantics.ts +1 -0
  15. package/src/db/004-package-target-invalidation.ts +10 -0
  16. package/src/db/006-relative-symbol-resolution.ts +28 -7
  17. package/src/db/008-relative-fact-semantics.ts +6 -3
  18. package/src/db/009-binding-fact-semantics.ts +5 -0
  19. package/src/db/012-binding-reference-proof.ts +4 -0
  20. package/src/db/013-index-publication-failure.ts +91 -0
  21. package/src/db/014-binding-helper-provenance.ts +17 -0
  22. package/src/db/repositories.ts +22 -5
  23. package/src/indexer/cds-extension-resolver.ts +27 -3
  24. package/src/indexer/repository-indexer.ts +66 -5
  25. package/src/indexer/workspace-indexer.ts +141 -29
  26. package/src/linker/004-event-subscription-handler-linker.ts +32 -11
  27. package/src/linker/006-event-template-link.ts +72 -0
  28. package/src/linker/007-call-edge-insertion.ts +568 -0
  29. package/src/linker/cross-repo-linker.ts +2 -165
  30. package/src/parsers/000-direct-query-execution.ts +11 -0
  31. package/src/parsers/006-binding-identity.ts +6 -1
  32. package/src/parsers/014-service-binding-helper-flow.ts +70 -4
  33. package/src/parsers/021-binding-visibility.ts +17 -1
  34. package/src/parsers/022-outbound-expression-analysis.ts +700 -0
  35. package/src/parsers/023-outbound-call-classifier.ts +692 -0
  36. package/src/parsers/outbound-call-parser.ts +146 -509
  37. package/src/parsers/symbol-parser.ts +37 -5
  38. package/src/trace/007-implementation-start-diagnostic.ts +1 -0
  39. package/src/trace/011-event-subscriber-traversal.ts +100 -8
  40. package/src/trace/013-trace-root-scopes.ts +2 -3
  41. package/src/trace/014-compact-contract.ts +6 -0
  42. package/src/trace/015-trace-edge-recorder.ts +61 -4
  43. package/src/trace/016-compact-projector.ts +2 -3
  44. package/src/trace/019-trace-edge-semantics.ts +6 -10
  45. package/src/trace/020-compact-field-projection.ts +74 -1
  46. package/src/trace/021-compact-decision-normalization.ts +75 -9
  47. package/src/trace/024-compact-observation-decision.ts +7 -2
  48. package/src/trace/026-trace-start-scope.ts +1 -0
  49. package/src/trace/027-trace-scope-execution.ts +33 -33
  50. package/src/trace/030-event-runtime-resolution.ts +151 -0
  51. package/src/trace/031-local-call-expansion.ts +37 -0
  52. package/src/trace/implementation-hints.ts +9 -6
  53. package/src/trace/selectors.ts +1 -0
  54. package/src/types.ts +2 -1
  55. package/src/version.ts +1 -1
  56. package/dist/chunk-AEM4JY22.js.map +0 -1
package/dist/index.d.ts CHANGED
@@ -130,7 +130,7 @@ interface ServiceBindingReference {
130
130
  bindingSourceFile?: string;
131
131
  bindingSiteStartOffset?: number;
132
132
  bindingSiteEndOffset?: number;
133
- resolutionStrategy?: 'lexical_declaration' | 'lexical_alias_declaration' | 'deterministic_reaching_assignment';
133
+ resolutionStrategy?: 'lexical_declaration' | 'lexical_alias_declaration' | 'deterministic_reaching_assignment' | 'single_hop_helper_return';
134
134
  lexicalScopeChain?: LexicalScopeFact[];
135
135
  bindingScopeIndex?: number;
136
136
  scopeChainTotal: number;
@@ -274,6 +274,7 @@ interface ClassifiedOutboundCall {
274
274
  fact: OutboundCallFact;
275
275
  node: ts.CallExpression;
276
276
  }
277
+
277
278
  declare function parseOutboundCalls(repoPath: string, filePath: string, context?: RepositorySourceContext, classified?: readonly ClassifiedOutboundCall[], preparedBindings?: readonly ServiceBindingFact[]): Promise<OutboundCallFact[]>;
278
279
 
279
280
  declare function parseGeneratedConstants(repoPath: string, filePath: string): Promise<GeneratedConstantFact[]>;
@@ -393,6 +394,7 @@ interface CompactDecisionV1 {
393
394
  implementationStrategy?: string;
394
395
  implementationGuided?: boolean;
395
396
  implementationContextual?: boolean;
397
+ tiedCandidateRepos?: CompactReferenceGroupV1;
396
398
  eventMatchStrategy?: string;
397
399
  dispatchCertainty?: string;
398
400
  eventSubscriptionCount?: number;
@@ -413,6 +415,10 @@ interface CompactEdgeDetailsV1 {
413
415
  }
414
416
  interface CompactDiagnosticDetailsV1 {
415
417
  reasonCode?: string;
418
+ tiedCandidateRepos?: CompactReferenceGroupV1;
419
+ selectorKind?: string;
420
+ selectorSuggestions?: CompactReferenceGroupV1;
421
+ invalidFactCategories?: CompactReferenceGroupV1;
416
422
  missingVariableNames?: string[];
417
423
  missingVariableCount?: number;
418
424
  shownMissingVariableCount?: number;
package/dist/index.js CHANGED
@@ -19,7 +19,7 @@ import {
19
19
  substituteVariables,
20
20
  trace,
21
21
  traceAndCompact
22
- } from "./chunk-AEM4JY22.js";
22
+ } from "./chunk-3N3B5KHV.js";
23
23
 
24
24
  // src/parsers/generated-constants-parser.ts
25
25
  import fs from "fs/promises";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saptools/service-flow",
3
- "version": "0.1.68",
3
+ "version": "0.1.69",
4
4
  "description": "Trace SAP CAP service-to-service flows across multi-repository workspaces with runtime-aware graph resolution",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -0,0 +1,22 @@
1
+ import type {
2
+ IndexWorkspaceSummary,
3
+ } from '../indexer/workspace-indexer.js';
4
+
5
+ export interface IndexCommandOutcome {
6
+ stdout: string;
7
+ exitCode: 0 | 1;
8
+ }
9
+
10
+ export function indexCommandOutcome(
11
+ summary: IndexWorkspaceSummary,
12
+ ): IndexCommandOutcome {
13
+ const failed = summary.failedCount > 0
14
+ ? `, failed ${summary.failedCount} (${
15
+ summary.failedRepos.map(({ name, code }) => `${name}: ${code}`).join(', ')
16
+ })`
17
+ : '';
18
+ return {
19
+ stdout: `Indexed ${summary.indexedCount} repositories, skipped ${summary.skippedCount}${failed}, ${summary.fileCount} files, ${summary.diagnosticCount} diagnostics\n`,
20
+ exitCode: summary.failedCount > 0 ? 1 : 0,
21
+ };
22
+ }
package/src/cli.ts CHANGED
@@ -41,6 +41,7 @@ import type {
41
41
  TraceStart,
42
42
  } from './types.js';
43
43
  import { cleanWorkspaceState } from './cli/000-clean.js';
44
+ import { indexCommandOutcome } from './cli/001-index-summary.js';
44
45
 
45
46
  const stdout = createStdoutWriter(process.stdout, fail);
46
47
  const TRACE_FORMATS = ['table', 'json', 'mermaid', 'compact-json'] as const;
@@ -245,14 +246,16 @@ function runGraphCommand(opts: GraphCommandOptions): Promise<void> {
245
246
  });
246
247
  }
247
248
 
248
- export function createProgram(): Command {
249
- const program = new Command();
250
- program
249
+ function configuredProgram(): Command {
250
+ return new Command()
251
251
  .name('service-flow')
252
252
  .description(
253
253
  'Trace SAP CAP service-to-service flows across multi-repository workspaces',
254
254
  )
255
255
  .version(VERSION);
256
+ }
257
+
258
+ function registerInitCommand(program: Command): void {
256
259
  program
257
260
  .command('init')
258
261
  .argument('<workspace>')
@@ -262,6 +265,9 @@ export function createProgram(): Command {
262
265
  (workspace: string, opts: { db?: string; ignore?: string[] }) =>
263
266
  void init(workspace, opts).catch(fail),
264
267
  );
268
+ }
269
+
270
+ function registerIndexCommand(program: Command): void {
265
271
  program
266
272
  .command('index')
267
273
  .option('--workspace <path>')
@@ -274,11 +280,14 @@ export function createProgram(): Command {
274
280
  repo: opts.repo,
275
281
  force: Boolean(opts.force),
276
282
  });
277
- writeStdout(
278
- `Indexed ${r.indexedCount} repositories, skipped ${r.skippedCount}, ${r.fileCount} files, ${r.diagnosticCount} diagnostics\n`,
279
- );
283
+ const outcome = indexCommandOutcome(r);
284
+ writeStdout(outcome.stdout);
285
+ if (outcome.exitCode !== 0) process.exitCode = outcome.exitCode;
280
286
  }).catch(fail),
281
287
  );
288
+ }
289
+
290
+ function registerLinkCommand(program: Command): void {
282
291
  program
283
292
  .command('link')
284
293
  .option('--workspace <path>')
@@ -293,6 +302,9 @@ export function createProgram(): Command {
293
302
  );
294
303
  }).catch(fail),
295
304
  );
305
+ }
306
+
307
+ function registerTraceCommand(program: Command): void {
296
308
  program
297
309
  .command('trace')
298
310
  .option('--workspace <path>')
@@ -312,23 +324,91 @@ export function createProgram(): Command {
312
324
  .option('--dynamic-mode <mode>', 'strict|candidates|infer', 'strict')
313
325
  .option('--max-dynamic-candidates <n>', 'maximum dynamic candidates to show', '5')
314
326
  .action((opts: TraceCommandOptions) => void runTraceCommand(opts).catch(fail));
327
+ }
328
+
329
+ function listRepositoriesCommand(
330
+ opts: { workspace?: string },
331
+ ): Promise<void> {
332
+ return withReadOnlyWorkspace(opts.workspace, (db, workspaceId) =>
333
+ writeStdout(
334
+ renderJson(
335
+ listRepositories(db, workspaceId).map((repo) => ({
336
+ name: repo.name,
337
+ kind: repo.kind,
338
+ packageName: repo.package_name,
339
+ })),
340
+ ),
341
+ ));
342
+ }
343
+
344
+ function listServicesCommand(
345
+ opts: { workspace?: string; repo?: string },
346
+ ): Promise<void> {
347
+ return withReadOnlyWorkspace(opts.workspace, (db, workspaceId) => {
348
+ const selection = opts.repo
349
+ ? selectRepository(db, opts.repo, workspaceId) : {};
350
+ if (selection.diagnostic) {
351
+ writeStdout(renderJson([selection.diagnostic]));
352
+ return;
353
+ }
354
+ const repo = selection.repo;
355
+ const rows = db.prepare(
356
+ 'SELECT r.name repo,s.service_path servicePath,s.qualified_name qualifiedName FROM cds_services s JOIN repositories r ON r.id=s.repo_id WHERE r.workspace_id=? AND (? IS NULL OR s.repo_id=?) ORDER BY r.name,s.service_path',
357
+ ).all(workspaceId, repo?.id, repo?.id);
358
+ writeStdout(renderJson(rows));
359
+ });
360
+ }
361
+
362
+ function listOperationsCommand(
363
+ opts: { workspace?: string; repo?: string; service?: string },
364
+ ): Promise<void> {
365
+ return withReadOnlyWorkspace(opts.workspace, (db, workspaceId) => {
366
+ const selection = opts.repo
367
+ ? selectRepository(db, opts.repo, workspaceId) : {};
368
+ if (selection.diagnostic) {
369
+ writeStdout(renderJson([selection.diagnostic]));
370
+ return;
371
+ }
372
+ const repo = selection.repo;
373
+ const rows = db.prepare(
374
+ 'SELECT r.name repo,s.service_path servicePath,o.operation_name operation,o.operation_path path FROM cds_operations o JOIN cds_services s ON s.id=o.service_id JOIN repositories r ON r.id=s.repo_id WHERE r.workspace_id=? AND (? IS NULL OR s.repo_id=?) AND (? IS NULL OR s.service_path=?)',
375
+ ).all(
376
+ workspaceId, repo?.id, repo?.id, opts.service, opts.service,
377
+ );
378
+ writeStdout(renderJson(rows));
379
+ });
380
+ }
381
+
382
+ function listCallsCommand(
383
+ opts: { workspace?: string; repo?: string; operation?: string },
384
+ ): Promise<void> {
385
+ return withReadOnlyWorkspace(opts.workspace, (db, workspaceId) => {
386
+ const selection = opts.repo
387
+ ? selectRepository(db, opts.repo, workspaceId) : {};
388
+ if (selection.diagnostic) {
389
+ writeStdout(renderJson([selection.diagnostic]));
390
+ return;
391
+ }
392
+ const repo = selection.repo;
393
+ const rows = db.prepare(
394
+ 'SELECT r.name repo,c.call_type type,c.operation_path_expr path,c.source_file file,c.source_line line FROM outbound_calls c JOIN repositories r ON r.id=c.repo_id WHERE r.workspace_id=? AND (? IS NULL OR c.repo_id=?) AND (? IS NULL OR c.operation_path_expr=? OR c.operation_path_expr=? OR c.payload_summary LIKE ?)',
395
+ ).all(
396
+ workspaceId, repo?.id, repo?.id, opts.operation, opts.operation,
397
+ opts.operation ? `/${opts.operation}` : undefined,
398
+ opts.operation ? `%${opts.operation}%` : undefined,
399
+ );
400
+ writeStdout(renderJson(rows));
401
+ });
402
+ }
403
+
404
+ function registerListCommands(program: Command): void {
315
405
  const list = program.command('list');
316
406
  list
317
407
  .command('repos')
318
408
  .option('--workspace <path>')
319
409
  .action(
320
410
  (opts: { workspace?: string }) =>
321
- void withReadOnlyWorkspace(opts.workspace, (db, workspaceId) =>
322
- writeStdout(
323
- renderJson(
324
- listRepositories(db, workspaceId).map((r) => ({
325
- name: r.name,
326
- kind: r.kind,
327
- packageName: r.package_name,
328
- })),
329
- ),
330
- ),
331
- ).catch(fail),
411
+ void listRepositoriesCommand(opts).catch(fail),
332
412
  );
333
413
  list
334
414
  .command('services')
@@ -336,22 +416,7 @@ export function createProgram(): Command {
336
416
  .option('--repo <name>')
337
417
  .action(
338
418
  (opts: { workspace?: string; repo?: string }) =>
339
- void withReadOnlyWorkspace(opts.workspace, (db, workspaceId) => {
340
- const selection = opts.repo
341
- ? selectRepository(db, opts.repo, workspaceId)
342
- : {};
343
- if (selection.diagnostic) {
344
- writeStdout(renderJson([selection.diagnostic]));
345
- return;
346
- }
347
- const repo = selection.repo;
348
- const rows = db
349
- .prepare(
350
- 'SELECT r.name repo,s.service_path servicePath,s.qualified_name qualifiedName FROM cds_services s JOIN repositories r ON r.id=s.repo_id WHERE r.workspace_id=? AND (? IS NULL OR s.repo_id=?) ORDER BY r.name,s.service_path',
351
- )
352
- .all(workspaceId, repo?.id, repo?.id);
353
- writeStdout(renderJson(rows));
354
- }).catch(fail),
419
+ void listServicesCommand(opts).catch(fail),
355
420
  );
356
421
  list
357
422
  .command('operations')
@@ -360,22 +425,7 @@ export function createProgram(): Command {
360
425
  .option('--service <path>')
361
426
  .action(
362
427
  (opts: { workspace?: string; repo?: string; service?: string }) =>
363
- void withReadOnlyWorkspace(opts.workspace, (db, workspaceId) => {
364
- const selection = opts.repo
365
- ? selectRepository(db, opts.repo, workspaceId)
366
- : {};
367
- if (selection.diagnostic) {
368
- writeStdout(renderJson([selection.diagnostic]));
369
- return;
370
- }
371
- const repo = selection.repo;
372
- const rows = db
373
- .prepare(
374
- 'SELECT r.name repo,s.service_path servicePath,o.operation_name operation,o.operation_path path FROM cds_operations o JOIN cds_services s ON s.id=o.service_id JOIN repositories r ON r.id=s.repo_id WHERE r.workspace_id=? AND (? IS NULL OR s.repo_id=?) AND (? IS NULL OR s.service_path=?)',
375
- )
376
- .all(workspaceId, repo?.id, repo?.id, opts.service, opts.service);
377
- writeStdout(renderJson(rows));
378
- }).catch(fail),
428
+ void listOperationsCommand(opts).catch(fail),
379
429
  );
380
430
  list
381
431
  .command('calls')
@@ -384,31 +434,11 @@ export function createProgram(): Command {
384
434
  .option('--operation <name>')
385
435
  .action(
386
436
  (opts: { workspace?: string; repo?: string; operation?: string }) =>
387
- void withReadOnlyWorkspace(opts.workspace, (db, workspaceId) => {
388
- const selection = opts.repo
389
- ? selectRepository(db, opts.repo, workspaceId)
390
- : {};
391
- if (selection.diagnostic) {
392
- writeStdout(renderJson([selection.diagnostic]));
393
- return;
394
- }
395
- const repo = selection.repo;
396
- const rows = db
397
- .prepare(
398
- 'SELECT r.name repo,c.call_type type,c.operation_path_expr path,c.source_file file,c.source_line line FROM outbound_calls c JOIN repositories r ON r.id=c.repo_id WHERE r.workspace_id=? AND (? IS NULL OR c.repo_id=?) AND (? IS NULL OR c.operation_path_expr=? OR c.operation_path_expr=? OR c.payload_summary LIKE ?)',
399
- )
400
- .all(
401
- workspaceId,
402
- repo?.id,
403
- repo?.id,
404
- opts.operation,
405
- opts.operation,
406
- opts.operation ? `/${opts.operation}` : undefined,
407
- opts.operation ? `%${opts.operation}%` : undefined,
408
- );
409
- writeStdout(renderJson(rows));
410
- }).catch(fail),
437
+ void listCallsCommand(opts).catch(fail),
411
438
  );
439
+ }
440
+
441
+ function registerGraphCommand(program: Command): void {
412
442
  program
413
443
  .command('graph')
414
444
  .option('--workspace <path>')
@@ -423,6 +453,33 @@ export function createProgram(): Command {
423
453
  .option('--dynamic-mode <mode>', 'strict|candidates|infer', 'strict')
424
454
  .option('--max-dynamic-candidates <n>', 'maximum dynamic candidates to show', '5')
425
455
  .action((opts: GraphCommandOptions) => void runGraphCommand(opts).catch(fail));
456
+ }
457
+
458
+ function inspectRepositoryCommand(
459
+ name: string,
460
+ opts: { workspace?: string },
461
+ ): Promise<void> {
462
+ return withReadOnlyWorkspace(opts.workspace, (db, workspaceId) => {
463
+ const selection = selectRepository(db, name, workspaceId);
464
+ writeStdout(renderJson(
465
+ selection.repo ?? selection.diagnostic ?? { error: 'repo not found' },
466
+ ));
467
+ });
468
+ }
469
+
470
+ function inspectOperationCommand(
471
+ selector: string,
472
+ opts: { workspace?: string },
473
+ ): Promise<void> {
474
+ return withReadOnlyWorkspace(opts.workspace, (db, workspaceId) => {
475
+ const rows = db.prepare(
476
+ 'SELECT o.* FROM cds_operations o JOIN cds_services s ON s.id=o.service_id JOIN repositories r ON r.id=s.repo_id WHERE r.workspace_id=? AND (o.operation_name=? OR o.operation_path=?)',
477
+ ).all(workspaceId, selector, selector);
478
+ writeStdout(renderJson(rows));
479
+ });
480
+ }
481
+
482
+ function registerInspectCommands(program: Command): void {
426
483
  const inspect = program.command('inspect');
427
484
  inspect
428
485
  .command('repo')
@@ -430,12 +487,7 @@ export function createProgram(): Command {
430
487
  .option('--workspace <path>')
431
488
  .action(
432
489
  (name: string, opts: { workspace?: string }) =>
433
- void withReadOnlyWorkspace(opts.workspace, (db, workspaceId) => {
434
- const selection = selectRepository(db, name, workspaceId);
435
- writeStdout(renderJson(
436
- selection.repo ?? selection.diagnostic ?? { error: 'repo not found' },
437
- ));
438
- }).catch(fail),
490
+ void inspectRepositoryCommand(name, opts).catch(fail),
439
491
  );
440
492
  inspect
441
493
  .command('operation')
@@ -443,15 +495,11 @@ export function createProgram(): Command {
443
495
  .option('--workspace <path>')
444
496
  .action(
445
497
  (selector: string, opts: { workspace?: string }) =>
446
- void withReadOnlyWorkspace(opts.workspace, (db, workspaceId) => {
447
- const rows = db
448
- .prepare(
449
- 'SELECT o.* FROM cds_operations o JOIN cds_services s ON s.id=o.service_id JOIN repositories r ON r.id=s.repo_id WHERE r.workspace_id=? AND (o.operation_name=? OR o.operation_path=?)',
450
- )
451
- .all(workspaceId, selector, selector);
452
- writeStdout(renderJson(rows));
453
- }).catch(fail),
498
+ void inspectOperationCommand(selector, opts).catch(fail),
454
499
  );
500
+ }
501
+
502
+ function registerDoctorCommand(program: Command): void {
455
503
  program
456
504
  .command('doctor')
457
505
  .option('--workspace <path>')
@@ -467,6 +515,9 @@ export function createProgram(): Command {
467
515
  writeStdout(renderDoctorDiagnostics(allDiagnostics, opts.format));
468
516
  }).catch(fail),
469
517
  );
518
+ }
519
+
520
+ function registerCleanCommand(program: Command): void {
470
521
  program
471
522
  .command('clean')
472
523
  .option('--workspace <path>')
@@ -479,6 +530,19 @@ export function createProgram(): Command {
479
530
  writeStdout('Cleaned service-flow state\n');
480
531
  })().catch(fail),
481
532
  );
533
+ }
534
+
535
+ export function createProgram(): Command {
536
+ const program = configuredProgram();
537
+ registerInitCommand(program);
538
+ registerIndexCommand(program);
539
+ registerLinkCommand(program);
540
+ registerTraceCommand(program);
541
+ registerListCommands(program);
542
+ registerGraphCommand(program);
543
+ registerInspectCommands(program);
544
+ registerDoctorCommand(program);
545
+ registerCleanCommand(program);
482
546
  return program;
483
547
  }
484
548
  function collect(value: string, previous: string[]): string[] {
@@ -16,6 +16,12 @@ import {
16
16
  type BindingProofCall,
17
17
  type BindingProofTarget,
18
18
  } from './012-binding-reference-proof.js';
19
+ import {
20
+ preparedCallSnapshotError,
21
+ } from './013-index-publication-failure.js';
22
+ import {
23
+ hasSingleHopHelperReturn,
24
+ } from './014-binding-helper-provenance.js';
19
25
 
20
26
  export function insertSymbolCalls(db: Db, repoId: number, rows: SymbolCallFact[]): void {
21
27
  const insertStmt = db.prepare('INSERT INTO symbol_calls(repo_id,caller_symbol_id,callee_symbol_id,callee_expression,import_source,source_file,source_line,call_site_start_offset,call_site_end_offset,call_role,status,confidence,evidence_json,unresolved_reason) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?)');
@@ -58,8 +64,8 @@ function assertImportProvenance(call: SymbolCallFact): void {
58
64
  const direct = directPackageProvenanceValid(call);
59
65
  const derived = derivedPackageProvenanceValid(call);
60
66
  if (!direct && !derived)
61
- throw new Error(
62
- 'invalid_prepared_repository_snapshot:package_import_provenance_missing',
67
+ throw preparedCallSnapshotError(
68
+ 'package_import_provenance_missing', 'symbol_call', call,
63
69
  );
64
70
  }
65
71
 
@@ -156,7 +162,9 @@ function requiredSymbolCallOwnerId(
156
162
  );
157
163
  if (selected.status !== 'resolved'
158
164
  || selected.owner?.qualifiedName !== call.callerQualifiedName)
159
- throw new Error('invalid_prepared_repository_snapshot:symbol_call_owner_mismatch');
165
+ throw preparedCallSnapshotError(
166
+ 'symbol_call_owner_mismatch', 'symbol_call', call,
167
+ );
160
168
  return selected.owner.id;
161
169
  }
162
170
 
@@ -265,9 +273,11 @@ function exportedSymbolRows(db: Db, repoId: number, r: SymbolCallFact): SymbolTa
265
273
  ));
266
274
  }
267
275
 
268
- function isRelativeImportedSymbolCall(r: SymbolCallFact): boolean {
269
- return Boolean(r.importSource?.startsWith('.'));
270
- }
276
+ const sameFileEligibleRelations = new Set([
277
+ 'indexed_local_symbol',
278
+ 'indexed_local_symbol_unproven',
279
+ 'indexed_this_method',
280
+ ]);
271
281
 
272
282
  function eligibleSymbolResolution(
273
283
  rows: SymbolTargetRow[],
@@ -292,8 +302,7 @@ function sameFileResolution(
292
302
  r: SymbolCallFact,
293
303
  relation: unknown,
294
304
  ): SymbolCallResolution | undefined {
295
- if (isRelativeImportedSymbolCall(r) || relation === 'package_import')
296
- return undefined;
305
+ if (!sameFileEligibleRelations.has(String(relation))) return undefined;
297
306
  if (relation === 'indexed_local_symbol_unproven')
298
307
  return unresolvedSymbol(
299
308
  'exact_symbol_match', 'no_local_symbol_target', 0,
@@ -452,6 +461,7 @@ interface PersistedBinding {
452
461
  ownerResolution: string;
453
462
  ownerStartOffset: number | null;
454
463
  ownerEndOffset: number | null;
464
+ singleHopHelperReturn: boolean;
455
465
  }
456
466
 
457
467
  function outboundOwnerId(
@@ -471,13 +481,18 @@ function outboundOwnerId(
471
481
  );
472
482
  const resolution = call.evidence?.sourceOwnerResolution;
473
483
  if (resolution === 'ownerless_file_scope')
474
- return ownerlessOutboundOwner(selected.status);
484
+ return ownerlessOutboundOwner(call, selected.status);
475
485
  return ownedOutboundOwner(call, resolution, selected);
476
486
  }
477
487
 
478
- function ownerlessOutboundOwner(status: string): null {
488
+ function ownerlessOutboundOwner(
489
+ call: OutboundCallFact,
490
+ status: string,
491
+ ): null {
479
492
  if (status !== 'none')
480
- throw new Error('invalid_prepared_repository_snapshot:outbound_owner_mismatch');
493
+ throw preparedCallSnapshotError(
494
+ 'outbound_owner_mismatch', 'outbound_call', call,
495
+ );
481
496
  return null;
482
497
  }
483
498
 
@@ -488,10 +503,14 @@ function ownedOutboundOwner(
488
503
  ): number {
489
504
  if (resolution !== 'owned_exact' || selected.status !== 'resolved'
490
505
  || selected.owner?.qualifiedName !== call.sourceSymbolQualifiedName)
491
- throw new Error('invalid_prepared_repository_snapshot:outbound_owner_mismatch');
506
+ throw preparedCallSnapshotError(
507
+ 'outbound_owner_mismatch', 'outbound_call', call,
508
+ );
492
509
  const owner = selected.owner;
493
510
  if (!owner)
494
- throw new Error('invalid_prepared_repository_snapshot:outbound_owner_mismatch');
511
+ throw preparedCallSnapshotError(
512
+ 'outbound_owner_mismatch', 'outbound_call', call,
513
+ );
495
514
  return owner.id;
496
515
  }
497
516
 
@@ -506,14 +525,18 @@ function resolvePersistedBinding(
506
525
  } {
507
526
  const reference = call.serviceBindingReference;
508
527
  if (!reference)
509
- throw new Error('invalid_prepared_repository_snapshot:binding_reference_missing');
528
+ throw preparedCallSnapshotError(
529
+ 'binding_reference_missing', 'outbound_call', call,
530
+ );
510
531
  if (reference.status !== 'resolved_exact')
511
532
  return unresolvedBinding(reference);
512
533
  const candidates = exactBindingRows(db, repoId, reference);
513
534
  const selected = candidates[0];
514
535
  if (candidates.length !== 1 || !selected
515
536
  || selected.variableName !== call.serviceVariableName)
516
- throw new Error('invalid_prepared_repository_snapshot:binding_reference_mismatch');
537
+ throw preparedCallSnapshotError(
538
+ 'binding_reference_mismatch', 'outbound_call', call,
539
+ );
517
540
  assertResolvedBindingProof(repoId, call, selected, reference);
518
541
  return {
519
542
  bindingId: selected.id,
@@ -533,8 +556,8 @@ function bindingProofCall(
533
556
  ): BindingProofCall {
534
557
  if (call.callSiteStartOffset === undefined
535
558
  || call.callSiteEndOffset === undefined)
536
- throw new Error(
537
- 'invalid_prepared_repository_snapshot:binding_lexical_proof_invalid',
559
+ throw preparedCallSnapshotError(
560
+ 'binding_lexical_proof_invalid', 'outbound_call', call,
538
561
  );
539
562
  return {
540
563
  repoId,
@@ -561,6 +584,7 @@ function bindingProofTarget(
561
584
  ownerResolution: binding.ownerResolution,
562
585
  ownerStartOffset: binding.ownerStartOffset,
563
586
  ownerEndOffset: binding.ownerEndOffset,
587
+ singleHopHelperReturn: binding.singleHopHelperReturn,
564
588
  };
565
589
  }
566
590
 
@@ -576,8 +600,8 @@ function assertResolvedBindingProof(
576
600
  bindingProofTarget(repoId, binding),
577
601
  );
578
602
  if (!valid)
579
- throw new Error(
580
- 'invalid_prepared_repository_snapshot:binding_lexical_proof_invalid',
603
+ throw preparedCallSnapshotError(
604
+ 'binding_lexical_proof_invalid', 'outbound_call', call,
581
605
  );
582
606
  }
583
607
 
@@ -608,6 +632,7 @@ function exactBindingRows(
608
632
  binding.binding_site_start_offset siteStart,
609
633
  binding.binding_site_end_offset siteEnd,
610
634
  binding.owner_resolution ownerResolution,
635
+ binding.helper_chain_json helperChainJson,
611
636
  owner.start_offset ownerStartOffset,owner.end_offset ownerEndOffset
612
637
  FROM service_bindings binding
613
638
  LEFT JOIN symbols owner ON owner.id=binding.symbol_id
@@ -642,6 +667,7 @@ function persistedBindingRow(
642
667
  ownerResolution: row.ownerResolution,
643
668
  ownerStartOffset,
644
669
  ownerEndOffset,
670
+ singleHopHelperReturn: hasSingleHopHelperReturn(row.helperChainJson),
645
671
  }];
646
672
  }
647
673
 
@@ -71,6 +71,7 @@ const symbolReasons = [
71
71
  'relative_import_type_only',
72
72
  'relative_import_module_resolution_ambiguous',
73
73
  'relative_import_requested_module_has_no_executable_body',
74
+ 'relative_import_proxy_alias_targets_package_unsupported',
74
75
  'relative_import_requested_module_has_no_target',
75
76
  'multiple_relative_class_targets_in_requested_module',
76
77
  'multiple_namespace_targets_in_requested_module',
@@ -157,6 +157,16 @@ export function createPackageInvalidationBatch(
157
157
  };
158
158
  }
159
159
 
160
+ export function mergePackageInvalidationEffects(
161
+ target: PackageInvalidationBatch,
162
+ source: PackageInvalidationBatch,
163
+ ): void {
164
+ for (const repoId of source.affectedCallerRepoIds)
165
+ target.affectedCallerRepoIds.add(repoId);
166
+ for (const workspaceId of source.affectedWorkspaceIds)
167
+ target.affectedWorkspaceIds.add(workspaceId);
168
+ }
169
+
160
170
  export function finalizePackageTargetInvalidations(
161
171
  db: Db,
162
172
  batch: PackageInvalidationBatch,