@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
package/dist/cli.js CHANGED
@@ -1,27 +1,30 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  ANALYZER_VERSION,
4
+ PreparedRepositorySnapshotError,
4
5
  VERSION,
6
+ analyzePackagePublicSurface,
7
+ binaryCompare,
8
+ bindingSite,
5
9
  classifyODataPathIntent,
6
10
  classifyOutboundCallsInSource,
7
- clearRepoFacts,
11
+ collectSymbolImportBindings,
8
12
  compactTrace,
9
13
  containsSupportedOutboundCall,
14
+ createBindingLexicalIndex,
15
+ derivedMemberImportReference,
10
16
  discoverRepositories,
17
+ executableSymbolCandidates,
11
18
  factLifecycleDiagnostic,
12
- getWorkspace,
13
- handlerMethodIsExecutable,
19
+ identifierMatchesDeclaration,
14
20
  implementationHintSuggestions,
15
- insertBindings,
16
21
  insertCalls,
17
- insertExecutableSymbols,
18
- insertHandler,
19
- insertRegistrations,
20
- insertRequires,
21
- insertService,
22
22
  insertSymbolCalls,
23
+ isPreparedRepositorySnapshotError,
24
+ lexicalIdentifierDeclaration,
25
+ lexicalIdentifierDeclarations,
26
+ lexicalScopeChain,
23
27
  linkWorkspace,
24
- listRepositories,
25
28
  loadPackageJsonSnapshot,
26
29
  loadRepositorySourceContext,
27
30
  migrate,
@@ -32,17 +35,20 @@ import {
32
35
  parseHandlerRegistrations,
33
36
  parseImplementationHint,
34
37
  parseOutboundCalls,
38
+ parsePackageImportReference,
35
39
  parsePackageJson,
36
40
  parseServiceBindings,
37
41
  parseVars,
38
42
  projectBoundedInOrder,
43
+ recordPreparedSnapshotFailure,
39
44
  redactValue,
40
- reposByName,
45
+ selectCallOwner,
46
+ selectVisibleBinding,
41
47
  selectorRepoAmbiguousDiagnostic,
42
- trace,
43
- upsertRepository,
44
- upsertWorkspace
45
- } from "./chunk-ZQABU7MR.js";
48
+ stableLocalValueReference,
49
+ symbolImportReference,
50
+ trace
51
+ } from "./chunk-3N3B5KHV.js";
46
52
 
47
53
  // src/cli.ts
48
54
  import { Command, Option } from "commander";
@@ -209,6 +215,354 @@ function openReadOnlyDatabase(dbPath) {
209
215
  return openDatabase(dbPath, { readonly: true, migrate: false });
210
216
  }
211
217
 
218
+ // src/db/repositories.ts
219
+ function initialPackagePublicSurface(packageName2) {
220
+ return JSON.stringify({
221
+ schema: "service-flow/package-public-surface@1",
222
+ status: packageName2 ? "incomplete" : "not_applicable",
223
+ reason: packageName2 ? "package_surface_pending_index" : null,
224
+ recordCap: 256,
225
+ total: 0,
226
+ shown: 0,
227
+ omitted: 0,
228
+ packageName: packageName2 ?? null,
229
+ exportsPresent: false,
230
+ exportsAuthoritative: false,
231
+ main: null,
232
+ module: null,
233
+ entries: [],
234
+ scopes: []
235
+ });
236
+ }
237
+ function upsertWorkspace(db, rootPath, dbPath) {
238
+ const now = (/* @__PURE__ */ new Date()).toISOString();
239
+ db.prepare(
240
+ "INSERT INTO workspaces(root_path,db_path,created_at,updated_at) VALUES(?,?,?,?) ON CONFLICT(root_path) DO UPDATE SET db_path=excluded.db_path,updated_at=excluded.updated_at"
241
+ ).run(rootPath, dbPath, now, now);
242
+ return Number(
243
+ db.prepare("SELECT id FROM workspaces WHERE root_path=?").get(rootPath)?.id
244
+ );
245
+ }
246
+ function getWorkspace(db, rootPath) {
247
+ return db.prepare("SELECT * FROM workspaces WHERE root_path=?").get(rootPath);
248
+ }
249
+ function upsertRepository(db, workspaceId, r) {
250
+ db.prepare(
251
+ `INSERT INTO repositories(workspace_id,name,absolute_path,relative_path,
252
+ package_name,package_version,dependencies_json,
253
+ package_public_surface_json,kind,is_git_repo)
254
+ VALUES(?,?,?,?,?,?,?,?,?,?)
255
+ ON CONFLICT(workspace_id,absolute_path) DO UPDATE SET
256
+ name=excluded.name,relative_path=excluded.relative_path,
257
+ package_name=excluded.package_name,
258
+ package_version=excluded.package_version,
259
+ dependencies_json=excluded.dependencies_json,kind=excluded.kind`
260
+ ).run(
261
+ workspaceId,
262
+ r.name,
263
+ r.absolutePath,
264
+ r.relativePath,
265
+ r.packageName,
266
+ r.packageVersion,
267
+ JSON.stringify(r.dependencies ?? {}),
268
+ initialPackagePublicSurface(r.packageName),
269
+ r.kind ?? "unknown",
270
+ r.isGitRepo ? 1 : 0
271
+ );
272
+ return Number(
273
+ db.prepare(
274
+ "SELECT id FROM repositories WHERE workspace_id=? AND absolute_path=?"
275
+ ).get(workspaceId, r.absolutePath)?.id
276
+ );
277
+ }
278
+ function listRepositories(db, workspaceId) {
279
+ return db.prepare("SELECT * FROM repositories WHERE (? IS NULL OR workspace_id=?) ORDER BY name,absolute_path,id").all(workspaceId, workspaceId);
280
+ }
281
+ function reposByName(db, name, workspaceId) {
282
+ return db.prepare(`SELECT * FROM repositories
283
+ WHERE (? IS NULL OR workspace_id=?) AND (name=? OR package_name=?)
284
+ ORDER BY name,absolute_path,id`).all(workspaceId, workspaceId, name, name);
285
+ }
286
+ function clearRepoFacts(db, repoId) {
287
+ for (const t of [
288
+ "cds_requires",
289
+ "cds_services",
290
+ "handler_classes",
291
+ "outbound_calls",
292
+ "symbol_calls",
293
+ "handler_registrations",
294
+ "service_bindings",
295
+ "symbols",
296
+ "diagnostics",
297
+ "files"
298
+ ])
299
+ db.prepare(`DELETE FROM ${t} WHERE repo_id=?`).run(repoId);
300
+ db.prepare("DELETE FROM search_index WHERE repo=?").run(String(repoId));
301
+ }
302
+ function insertRequires(db, repoId, rows) {
303
+ const stmt = db.prepare(
304
+ "INSERT INTO cds_requires(repo_id,alias,kind,model,destination,service_path,request_timeout,raw_json) VALUES(?,?,?,?,?,?,?,?)"
305
+ );
306
+ for (const r of rows)
307
+ stmt.run(
308
+ repoId,
309
+ r.alias,
310
+ r.kind,
311
+ r.model,
312
+ r.destination,
313
+ r.servicePath,
314
+ r.requestTimeout,
315
+ r.rawJson
316
+ );
317
+ }
318
+ function insertService(db, repoId, s) {
319
+ const id = Number(
320
+ db.prepare(
321
+ "INSERT INTO cds_services(repo_id,namespace,service_name,qualified_name,service_path,is_extend,source_file,source_line,extension_local_ref,extension_imported_symbol,extension_local_alias,extension_module_specifier,extension_import_kind) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?) RETURNING id"
322
+ ).get(
323
+ repoId,
324
+ s.namespace,
325
+ s.serviceName,
326
+ s.qualifiedName,
327
+ s.servicePath,
328
+ s.isExtend ? 1 : 0,
329
+ s.sourceFile,
330
+ s.sourceLine,
331
+ s.extension?.localReference,
332
+ s.extension?.importedSymbol,
333
+ s.extension?.localAlias,
334
+ s.extension?.moduleSpecifier,
335
+ s.extension?.importKind
336
+ )?.id
337
+ );
338
+ const stmt = db.prepare(
339
+ "INSERT INTO cds_operations(service_id,operation_type,operation_name,operation_path,params_json,return_type,source_file,source_line,provenance,base_operation_id) VALUES(?,?,?,?,?,?,?,?,?,?)"
340
+ );
341
+ db.prepare(
342
+ "INSERT INTO search_index(kind,name,path,repo) VALUES(?,?,?,?)"
343
+ ).run("service", s.qualifiedName, s.servicePath, String(repoId));
344
+ for (const o of s.operations)
345
+ stmt.run(
346
+ id,
347
+ o.operationType,
348
+ o.operationName,
349
+ o.operationPath,
350
+ o.paramsJson,
351
+ o.returnType,
352
+ o.sourceFile,
353
+ o.sourceLine,
354
+ o.provenance ?? "direct",
355
+ o.baseOperationId ?? null
356
+ );
357
+ const search = db.prepare(
358
+ "INSERT INTO search_index(kind,name,path,repo) VALUES(?,?,?,?)"
359
+ );
360
+ for (const o of s.operations)
361
+ search.run("operation", o.operationName, o.operationPath, String(repoId));
362
+ return id;
363
+ }
364
+ function insertHandler(db, repoId, h) {
365
+ const sid = insertHandlerClassSymbol(db, repoId, h);
366
+ const hid = Number(
367
+ db.prepare(
368
+ "INSERT INTO handler_classes(repo_id,symbol_id,class_name,source_file,source_line) VALUES(?,?,?,?,?) RETURNING id"
369
+ ).get(repoId, sid, h.className, h.sourceFile, h.sourceLine)?.id
370
+ );
371
+ const stmt = db.prepare(
372
+ "INSERT INTO handler_methods(handler_class_id,method_name,decorator_kind,decorator_value,decorator_raw_expression,decorator_resolution_json,source_file,source_line) VALUES(?,?,?,?,?,?,?,?)"
373
+ );
374
+ for (const m of h.methods)
375
+ stmt.run(
376
+ hid,
377
+ m.methodName,
378
+ m.decoratorKind,
379
+ m.decoratorValue,
380
+ m.decoratorRawExpression,
381
+ JSON.stringify(canonicalHandlerMethodResolution(m)),
382
+ m.sourceFile,
383
+ m.sourceLine
384
+ );
385
+ insertHandlerIndexDiagnostic(db, repoId, h);
386
+ return hid;
387
+ }
388
+ function insertHandlerClassSymbol(db, repoId, h) {
389
+ const classEvidence = {
390
+ hasHandlerDecorator: h.hasHandlerDecorator ?? false,
391
+ classDecoratorNames: h.classDecoratorNames ?? [],
392
+ observedDecoratorNames: h.observedDecoratorNames ?? [],
393
+ unsupportedDecoratorNames: h.unsupportedDecoratorNames ?? [],
394
+ unsupportedMethods: h.methods.filter((method) => !handlerMethodIsExecutable(method)).map((method) => ({
395
+ methodName: method.methodName,
396
+ decoratorKind: method.decoratorKind,
397
+ sourceFile: method.sourceFile,
398
+ sourceLine: method.sourceLine,
399
+ reason: method.decoratorResolution.unresolvedReason
400
+ }))
401
+ };
402
+ return Number(
403
+ db.prepare(
404
+ "INSERT INTO symbols(repo_id,kind,name,qualified_name,exported,start_line,end_line,source_file,evidence_json) VALUES(?,?,?,?,?,?,?,?,?) RETURNING id"
405
+ ).get(
406
+ repoId,
407
+ "class",
408
+ h.className,
409
+ h.className,
410
+ 1,
411
+ h.sourceLine,
412
+ h.sourceLine,
413
+ h.sourceFile,
414
+ JSON.stringify(classEvidence)
415
+ )?.id
416
+ );
417
+ }
418
+ function insertHandlerIndexDiagnostic(db, repoId, h) {
419
+ if (!h.hasHandlerDecorator) return;
420
+ const hasExecutable = h.methods.some(handlerMethodIsExecutable);
421
+ const unsupported = h.methods.filter((method) => !handlerMethodIsExecutable(method));
422
+ if (hasExecutable && unsupported.length === 0) return;
423
+ const code = hasExecutable ? "handler_decorators_not_indexed" : "handler_methods_not_indexed";
424
+ const names = unsupported.map((method) => method.decoratorKind).sort();
425
+ const detail = names.length > 0 ? ` Unsupported decorators: ${[...new Set(names)].join(", ")}.` : "";
426
+ db.prepare(
427
+ "INSERT INTO diagnostics(repo_id,severity,code,message,source_file,source_line) VALUES(?,?,?,?,?,?)"
428
+ ).run(
429
+ repoId,
430
+ "warning",
431
+ code,
432
+ hasExecutable ? `Handler class ${h.className} contains methods that were not indexed.${detail}` : `Handler class ${h.className} has no indexed executable methods; use a supported CAP handler decorator and re-index.${detail}`,
433
+ h.sourceFile,
434
+ h.sourceLine
435
+ );
436
+ }
437
+ function canonicalHandlerMethodResolution(method) {
438
+ const handlerKind = method.handlerKind ?? method.decoratorResolution.handlerKind ?? legacyHandlerKind(method.decoratorKind);
439
+ const executable = method.executable ?? method.decoratorResolution.executable ?? (handlerKind === "operation" || handlerKind === "event" || handlerKind === "entity_lifecycle");
440
+ return {
441
+ ...method.decoratorResolution,
442
+ handlerKind,
443
+ executable,
444
+ lifecyclePhase: method.lifecyclePhase ?? method.decoratorResolution.lifecyclePhase,
445
+ lifecycleEvent: method.lifecycleEvent ?? method.decoratorResolution.lifecycleEvent
446
+ };
447
+ }
448
+ function handlerMethodIsExecutable(method) {
449
+ return canonicalHandlerMethodResolution(method).executable === true;
450
+ }
451
+ function legacyHandlerKind(kind) {
452
+ if (kind === "Event") return "event";
453
+ if (["Action", "Func", "On"].includes(kind)) return "operation";
454
+ return "unsupported_decorator";
455
+ }
456
+ function insertRegistrations(db, repoId, rows) {
457
+ const stmt = db.prepare(
458
+ "INSERT INTO handler_registrations(repo_id,handler_class_id,class_name,import_source,registration_file,registration_line,registration_kind,confidence) VALUES(?,?,?,?,?,?,?,?)"
459
+ );
460
+ for (const r of rows) {
461
+ const handlerClass = r.className ? db.prepare(
462
+ "SELECT id FROM handler_classes WHERE repo_id=? AND class_name=? ORDER BY id"
463
+ ).all(repoId, r.className) : [];
464
+ stmt.run(
465
+ repoId,
466
+ handlerClass.length === 1 ? handlerClass[0]?.id : null,
467
+ r.className,
468
+ r.importSource,
469
+ r.registrationFile,
470
+ r.registrationLine,
471
+ r.registrationKind,
472
+ r.confidence
473
+ );
474
+ }
475
+ }
476
+ function insertBindings(db, repoId, rows) {
477
+ assertUniquePreparedBindingSites(rows);
478
+ const stmt = db.prepare(
479
+ "INSERT INTO service_bindings(repo_id,symbol_id,variable_name,alias,alias_expr,destination_expr,service_path_expr,is_dynamic,placeholders_json,source_file,source_line,binding_site_start_offset,binding_site_end_offset,owner_resolution,helper_chain_json) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)"
480
+ );
481
+ for (const r of rows)
482
+ stmt.run(
483
+ repoId,
484
+ bindingOwnerId(db, repoId, r),
485
+ r.variableName,
486
+ r.alias,
487
+ r.aliasExpr,
488
+ r.destinationExpr,
489
+ r.servicePathExpr,
490
+ r.isDynamic ? 1 : 0,
491
+ JSON.stringify(r.placeholders),
492
+ r.sourceFile,
493
+ r.sourceLine,
494
+ r.bindingSiteStartOffset,
495
+ r.bindingSiteEndOffset,
496
+ r.ownerResolution,
497
+ r.helperChain ? JSON.stringify(r.helperChain) : null
498
+ );
499
+ }
500
+ function bindingSnapshotError(failureCode, fact) {
501
+ return new PreparedRepositorySnapshotError(failureCode, {
502
+ factKind: "service_binding",
503
+ sourceFile: fact.sourceFile,
504
+ sourceLine: fact.sourceLine,
505
+ callSiteStartOffset: fact.bindingSiteStartOffset,
506
+ callSiteEndOffset: fact.bindingSiteEndOffset
507
+ });
508
+ }
509
+ function persistedOwnerCandidates(db, repoId, fact) {
510
+ const rows = db.prepare(`SELECT id,kind,qualified_name qualifiedName,
511
+ start_offset startOffset,end_offset endOffset FROM symbols
512
+ WHERE repo_id=? AND source_file=? AND start_offset<=? AND end_offset>=?`).all(
513
+ repoId,
514
+ fact.sourceFile,
515
+ fact.bindingSiteStartOffset,
516
+ fact.bindingSiteEndOffset
517
+ );
518
+ return rows.flatMap((row) => typeof row.id === "number" && typeof row.kind === "string" && typeof row.qualifiedName === "string" && typeof row.startOffset === "number" && typeof row.endOffset === "number" ? [{
519
+ id: row.id,
520
+ kind: row.kind,
521
+ qualifiedName: row.qualifiedName,
522
+ startOffset: row.startOffset,
523
+ endOffset: row.endOffset
524
+ }] : []);
525
+ }
526
+ function bindingOwnerId(db, repoId, fact) {
527
+ const candidates = persistedOwnerCandidates(db, repoId, fact);
528
+ const selected = selectCallOwner(
529
+ candidates,
530
+ fact.bindingSiteStartOffset ?? -1,
531
+ fact.bindingSiteEndOffset ?? -1
532
+ );
533
+ if (fact.ownerResolution === "ownerless_file_scope") {
534
+ if (selected.status !== "none")
535
+ throw bindingSnapshotError("binding_owner_mismatch", fact);
536
+ return null;
537
+ }
538
+ if (fact.ownerResolution !== "owned_exact" || selected.status !== "resolved" || selected.owner?.qualifiedName !== fact.sourceSymbolQualifiedName)
539
+ throw bindingSnapshotError("binding_owner_mismatch", fact);
540
+ const owner = selected.owner;
541
+ if (!owner)
542
+ throw bindingSnapshotError("binding_owner_mismatch", fact);
543
+ return owner.id;
544
+ }
545
+ function assertUniquePreparedBindingSites(rows) {
546
+ const seen = /* @__PURE__ */ new Set();
547
+ for (const row of rows) {
548
+ if (row.bindingSiteStartOffset === void 0 || row.bindingSiteEndOffset === void 0)
549
+ throw bindingSnapshotError("binding_site_missing", row);
550
+ const key = [
551
+ row.sourceFile,
552
+ row.variableName,
553
+ row.bindingSiteStartOffset,
554
+ row.bindingSiteEndOffset
555
+ ].join("\0");
556
+ if (seen.has(key))
557
+ throw bindingSnapshotError("duplicate_service_binding_site", row);
558
+ seen.add(key);
559
+ }
560
+ }
561
+ function insertExecutableSymbols(db, repoId, rows) {
562
+ const stmt = db.prepare("INSERT INTO symbols(repo_id,file_id,kind,name,qualified_name,exported,start_line,end_line,start_offset,end_offset,source_file,exported_name,evidence_json) VALUES(?,(SELECT id FROM files WHERE repo_id=? AND relative_path=?),?,?,?,?,?,?,?,?,?,?,?)");
563
+ for (const r of rows) stmt.run(repoId, repoId, r.sourceFile, r.kind, r.localName, r.qualifiedName, r.exported ? 1 : 0, r.startLine, r.endLine, r.startOffset, r.endOffset, r.sourceFile, r.exportedName, r.importExportEvidence ? JSON.stringify(r.importExportEvidence) : null);
564
+ }
565
+
212
566
  // src/discovery/classify-repository.ts
213
567
  import fs3 from "fs/promises";
214
568
  import path3 from "path";
@@ -258,91 +612,1042 @@ import path5 from "path";
258
612
  // src/parsers/symbol-parser.ts
259
613
  import fs4 from "fs/promises";
260
614
  import path4 from "path";
615
+ import ts8 from "typescript";
616
+
617
+ // src/parsers/005-event-subscription-facts.ts
618
+ import ts2 from "typescript";
619
+
620
+ // src/parsers/016-local-symbol-reference.ts
261
621
  import ts from "typescript";
262
- function lineOf(source, pos) {
263
- return source.getLineAndCharacterOfPosition(pos).line + 1;
622
+ function functionTargetNode(declaration) {
623
+ const parent = declaration.parent;
624
+ if (ts.isFunctionDeclaration(parent)) return parent;
625
+ if (!ts.isVariableDeclaration(parent)) return void 0;
626
+ const initializer = parent.initializer;
627
+ return initializer && (ts.isArrowFunction(initializer) || ts.isFunctionExpression(initializer)) ? initializer : void 0;
628
+ }
629
+ function declarationKey(identifier) {
630
+ return `${identifier.getStart(identifier.getSourceFile())}:${identifier.getEnd()}`;
631
+ }
632
+ function unwrapAssignmentTarget(expression) {
633
+ if (ts.isParenthesizedExpression(expression) || ts.isAsExpression(expression) || ts.isSatisfiesExpression(expression) || ts.isTypeAssertionExpression(expression) || ts.isNonNullExpression(expression))
634
+ return unwrapAssignmentTarget(expression.expression);
635
+ return expression;
636
+ }
637
+ function objectAssignmentTargets(expression) {
638
+ return expression.properties.flatMap((property) => {
639
+ if (ts.isShorthandPropertyAssignment(property)) return [property.name];
640
+ if (ts.isPropertyAssignment(property))
641
+ return assignmentTargets(property.initializer);
642
+ return ts.isSpreadAssignment(property) ? assignmentTargets(property.expression) : [];
643
+ });
264
644
  }
265
- function nameOf(node) {
266
- if (!node) return void 0;
267
- if (ts.isIdentifier(node) || ts.isStringLiteral(node) || ts.isNumericLiteral(node)) return node.text;
268
- return void 0;
645
+ function assignmentTargets(expression) {
646
+ const target = unwrapAssignmentTarget(expression);
647
+ if (ts.isIdentifier(target)) return [target];
648
+ if (ts.isSpreadElement(target))
649
+ return assignmentTargets(target.expression);
650
+ if (ts.isBinaryExpression(target) && target.operatorToken.kind === ts.SyntaxKind.EqualsToken)
651
+ return assignmentTargets(target.left);
652
+ if (ts.isArrayLiteralExpression(target))
653
+ return target.elements.flatMap((item) => ts.isExpression(item) ? assignmentTargets(item) : []);
654
+ return ts.isObjectLiteralExpression(target) ? objectAssignmentTargets(target) : [];
655
+ }
656
+ function assignmentOperator(kind) {
657
+ return kind >= ts.SyntaxKind.FirstAssignment && kind <= ts.SyntaxKind.LastAssignment;
658
+ }
659
+ function mutationUnary(node) {
660
+ return node.operator === ts.SyntaxKind.PlusPlusToken || node.operator === ts.SyntaxKind.MinusMinusToken;
661
+ }
662
+ function binaryWritesDeclaration(node, matches) {
663
+ if (!ts.isBinaryExpression(node) || !assignmentOperator(node.operatorToken.kind)) return false;
664
+ return assignmentTargets(node.left).some(matches);
665
+ }
666
+ function unaryWritesDeclaration(node, matches) {
667
+ if (!ts.isPrefixUnaryExpression(node) && !ts.isPostfixUnaryExpression(node)) return false;
668
+ return mutationUnary(node) && ts.isIdentifier(node.operand) && matches(node.operand);
669
+ }
670
+ function loopWritesDeclaration(node, matches) {
671
+ if (!ts.isForInStatement(node) && !ts.isForOfStatement(node)) return false;
672
+ return !ts.isVariableDeclarationList(node.initializer) && assignmentTargets(node.initializer).some(matches);
673
+ }
674
+ function declarationWritten(source, declarations) {
675
+ const keys = new Set(declarations.map(declarationKey));
676
+ let written = false;
677
+ const matches = (identifier) => lexicalIdentifierDeclarations(identifier).some((item) => keys.has(declarationKey(item)));
678
+ const visit = (node) => {
679
+ if (written) return;
680
+ written = binaryWritesDeclaration(node, matches) || unaryWritesDeclaration(node, matches) || loopWritesDeclaration(node, matches);
681
+ if (!written) ts.forEachChild(node, visit);
682
+ };
683
+ visit(source);
684
+ return written;
269
685
  }
270
- function isFunctionLike(node) {
271
- return ts.isFunctionDeclaration(node) || ts.isMethodDeclaration(node) || ts.isFunctionExpression(node) || ts.isArrowFunction(node);
686
+ function singleImmutableDeclaration(declaration) {
687
+ const parent = declaration?.parent;
688
+ if (parent && ts.isClassDeclaration(parent)) return true;
689
+ return Boolean(parent && ts.isVariableDeclaration(parent) && ts.isVariableDeclarationList(parent.parent) && (parent.parent.flags & ts.NodeFlags.Const) !== 0);
272
690
  }
273
- function exported(node) {
274
- return Boolean(ts.getCombinedModifierFlags(node) & ts.ModifierFlags.Export);
691
+ function immutableDeclarationSet(declarations, source) {
692
+ if (declarations.length === 0 || declarationWritten(source, declarations)) return false;
693
+ if (declarations.every((item) => ts.isFunctionDeclaration(item.parent)))
694
+ return true;
695
+ if (declarations.length !== 1) return false;
696
+ return singleImmutableDeclaration(declarations[0]);
275
697
  }
276
- function isPublicClassMethod(node) {
277
- const flags = ts.getCombinedModifierFlags(node);
278
- return (flags & ts.ModifierFlags.Private) === 0 && (flags & ts.ModifierFlags.Protected) === 0;
698
+ function propertyContainer(declaration) {
699
+ const parent = declaration.parent;
700
+ if (ts.isClassDeclaration(parent)) return parent;
701
+ if (!ts.isVariableDeclaration(parent)) return void 0;
702
+ const initializer = parent.initializer;
703
+ return initializer && (ts.isObjectLiteralExpression(initializer) || ts.isClassExpression(initializer)) ? initializer : void 0;
279
704
  }
280
- function exportDeclarations(source) {
281
- const exports = /* @__PURE__ */ new Map();
282
- const visit = (node) => {
283
- if (ts.isExportDeclaration(node) && node.exportClause && ts.isNamedExports(node.exportClause)) {
284
- for (const el of node.exportClause.elements) exports.set((el.propertyName ?? el.name).text, el.name.text);
285
- }
286
- ts.forEachChild(node, visit);
705
+ function identity(symbol) {
706
+ return {
707
+ sourceFile: symbol.sourceFile,
708
+ qualifiedName: symbol.qualifiedName,
709
+ startOffset: symbol.startOffset,
710
+ endOffset: symbol.endOffset
287
711
  };
288
- visit(source);
289
- return exports;
290
712
  }
291
- function isRelativeImport(value) {
292
- return Boolean(value?.startsWith("."));
713
+ function executableBody(symbol) {
714
+ const value = symbol.importExportEvidence?.executableBodyEligibility;
715
+ return Boolean(value && typeof value === "object" && !Array.isArray(value) && "eligible" in value && value.eligible === true);
716
+ }
717
+ function exactFunctionTarget(declarations, symbols, source) {
718
+ if (!immutableDeclarationSet(declarations, source)) return void 0;
719
+ const spans = declarations.flatMap((declaration) => {
720
+ const target = functionTargetNode(declaration);
721
+ return target ? [{ start: target.getStart(source), end: target.getEnd() }] : [];
722
+ });
723
+ const matches = symbols.filter((symbol) => localFunctionMatches(symbol, spans, source.fileName));
724
+ return matches.length === 1 && matches[0] ? identity(matches[0]) : void 0;
725
+ }
726
+ function localFunctionMatches(symbol, spans, sourceFile2) {
727
+ if (symbol.sourceFile !== sourceFile2 || !executableBody(symbol))
728
+ return false;
729
+ return spans.some((span) => symbol.startOffset === span.start && symbol.endOffset === span.end);
293
730
  }
294
- function directHandlerReferenceTarget(expression, source, imports, namespaceImports) {
731
+ function exactPropertyTarget(expression, declaration, symbols, source) {
732
+ if (!immutableDeclarationSet([declaration], source) || !stableLocalValueReference(source, declaration))
733
+ return void 0;
734
+ const container = propertyContainer(declaration);
735
+ if (!container) return void 0;
736
+ const qualifiedName = `${declaration.text}.${expression.name.text}`;
737
+ const start = container.getStart(source);
738
+ const end = container.getEnd();
739
+ const matches = symbols.filter((symbol) => symbol.sourceFile === source.fileName && symbol.qualifiedName === qualifiedName && symbol.startOffset >= start && symbol.endOffset <= end && executableBody(symbol));
740
+ return matches.length === 1 && matches[0] ? identity(matches[0]) : void 0;
741
+ }
742
+ function localSymbolTarget(expression, source, symbols) {
295
743
  if (ts.isIdentifier(expression)) {
296
- const importSource2 = imports.get(expression.text);
744
+ return exactFunctionTarget(
745
+ lexicalIdentifierDeclarations(expression),
746
+ symbols,
747
+ source
748
+ );
749
+ }
750
+ if (!ts.isPropertyAccessExpression(expression) || expression.questionDotToken || !ts.isIdentifier(expression.expression)) return void 0;
751
+ const declaration = lexicalIdentifierDeclaration(expression.expression);
752
+ return declaration ? exactPropertyTarget(expression, declaration, symbols, source) : void 0;
753
+ }
754
+
755
+ // src/parsers/005-event-subscription-facts.ts
756
+ function lineOf(source, node) {
757
+ return source.getLineAndCharacterOfPosition(node.getStart(source)).line + 1;
758
+ }
759
+ function lineAt(source, position) {
760
+ return source.getLineAndCharacterOfPosition(position).line + 1;
761
+ }
762
+ function importRelation(binding) {
763
+ if (binding.moduleKind === "package") return "package_import";
764
+ return binding.referenceShape === "namespace_member" ? "relative_import_namespace_member" : "relative_import";
765
+ }
766
+ function directTarget(expression, source, imports, symbols) {
767
+ const imported = symbolImportReference(expression, imports);
768
+ if (imported) return importedTarget(expression, source, imported);
769
+ if (ts2.isIdentifier(expression)) {
770
+ const exact = localSymbolTarget(expression, source, symbols);
297
771
  return {
298
772
  calleeExpression: expression.text,
299
773
  calleeLocalName: expression.text,
300
- importSource: importSource2,
301
- relation: importSource2 ? isRelativeImport(importSource2) ? "relative_import" : "package_import" : "indexed_local_symbol"
774
+ relation: exact ? "indexed_local_symbol" : "indexed_local_symbol_unproven",
775
+ referenceShape: "identifier",
776
+ localTargetIdentity: exact
302
777
  };
303
778
  }
304
- if (!ts.isPropertyAccessExpression(expression) || expression.questionDotToken || !ts.isIdentifier(expression.expression) || !ts.isIdentifier(expression.name))
305
- return void 0;
306
- const objectName = expression.expression.text;
307
- const memberName = expression.name.text;
308
- const importSource = imports.get(objectName);
309
- if (namespaceImports.has(objectName)) return {
779
+ return propertyTarget(expression, source, symbols);
780
+ }
781
+ function importedTarget(expression, source, binding) {
782
+ return {
310
783
  calleeExpression: expression.getText(source),
311
- calleeLocalName: memberName,
312
- importSource,
313
- relation: "relative_import_namespace_member"
784
+ calleeLocalName: binding.requestedPublicName,
785
+ importSource: binding.rawModuleSpecifier,
786
+ relation: importRelation(binding),
787
+ referenceShape: binding.referenceShape,
788
+ importBinding: binding
314
789
  };
315
- const qualifiedName = `${objectName}.${memberName}`;
790
+ }
791
+ function propertyTarget(expression, source, symbols) {
792
+ if (!ts2.isPropertyAccessExpression(expression) || expression.questionDotToken || !ts2.isIdentifier(expression.expression)) return void 0;
793
+ const exact = localSymbolTarget(expression, source, symbols);
316
794
  return {
317
- calleeExpression: qualifiedName,
318
- calleeLocalName: qualifiedName,
319
- importSource,
320
- relation: importSource ? isRelativeImport(importSource) ? "relative_import" : "package_import" : "indexed_local_symbol"
795
+ calleeExpression: expression.getText(source),
796
+ calleeLocalName: `${expression.expression.text}.${expression.name.text}`,
797
+ relation: exact ? "indexed_local_symbol" : "indexed_local_symbol_unproven",
798
+ referenceShape: "static_member",
799
+ localTargetIdentity: exact
321
800
  };
322
801
  }
323
- function handlerReferenceTarget(expression, source, imports, namespaceImports) {
324
- const direct = directHandlerReferenceTarget(
325
- expression,
802
+ function unsupportedClassification(status, reason, referenceShape) {
803
+ return { status, reason, referenceShape };
804
+ }
805
+ function classifyHandlerReference(expression, source, imports = collectSymbolImportBindings(source), symbols = []) {
806
+ if (!expression)
807
+ return unsupportedClassification(
808
+ "missing_argument",
809
+ "handler_argument_missing",
810
+ "missing"
811
+ );
812
+ if (ts2.isArrowFunction(expression) || ts2.isFunctionExpression(expression))
813
+ return unsupportedClassification(
814
+ "unsupported_inline",
815
+ "inline_handler_body_not_indexed",
816
+ "inline_callback"
817
+ );
818
+ const direct = directTarget(expression, source, imports, symbols);
819
+ if (direct)
820
+ return { status: "role_required", referenceShape: direct.referenceShape, target: direct };
821
+ if (!ts2.isCallExpression(expression))
822
+ return unsupportedClassification(
823
+ "unsupported_reference_shape",
824
+ "handler_reference_shape_unsupported",
825
+ "unsupported_expression"
826
+ );
827
+ return classifyWrapperReference(expression, source, imports, symbols);
828
+ }
829
+ function classifyWrapperReference(expression, source, imports, symbols) {
830
+ if (expression.questionDotToken || expression.arguments.length !== 1)
831
+ return unsupportedClassification(
832
+ "unsupported_wrapper",
833
+ "wrapper_requires_one_reference",
834
+ "wrapper_call"
835
+ );
836
+ const inner = expression.arguments[0];
837
+ const target = inner ? directTarget(inner, source, imports, symbols) : void 0;
838
+ if (!target)
839
+ return unsupportedClassification(
840
+ "unsupported_wrapper",
841
+ "wrapper_reference_shape_unsupported",
842
+ "wrapper_call"
843
+ );
844
+ return {
845
+ status: "role_required",
846
+ referenceShape: `wrapped_${target.referenceShape}`,
847
+ target: { ...target, wrapperFunction: expression.expression.getText(source) }
848
+ };
849
+ }
850
+ function eventSymbol(source, classified) {
851
+ const node = classified.node;
852
+ const eventName = classified.fact.eventNameExpr ?? "";
853
+ const line = lineOf(source, node);
854
+ const safeName = eventName.replace(/[^A-Za-z0-9_$-]/g, "_");
855
+ const name = `event:${safeName}:${line}`;
856
+ const receiver = classified.fact.evidence?.receiver;
857
+ return {
858
+ kind: "event_registration",
859
+ localName: name,
860
+ qualifiedName: `module:${classified.fact.sourceFile}#${name}`,
861
+ sourceFile: classified.fact.sourceFile,
862
+ startLine: line,
863
+ endLine: lineAt(source, node.getEnd() - 1),
864
+ startOffset: node.getStart(source),
865
+ endOffset: node.getEnd(),
866
+ exported: false,
867
+ importExportEvidence: {
868
+ source: "synthetic_event_registration",
869
+ eventName,
870
+ registrationLine: line,
871
+ receiver: typeof receiver === "string" ? receiver : void 0
872
+ }
873
+ };
874
+ }
875
+ function eventCall(classified, owner, classification) {
876
+ const target = classification.target;
877
+ if (!target) return void 0;
878
+ return {
879
+ callerQualifiedName: owner.qualifiedName,
880
+ calleeExpression: target.calleeExpression,
881
+ calleeLocalName: target.calleeLocalName,
882
+ importSource: target.importSource,
883
+ sourceFile: classified.fact.sourceFile,
884
+ sourceLine: classified.fact.sourceLine,
885
+ callSiteStartOffset: classified.fact.callSiteStartOffset,
886
+ callSiteEndOffset: classified.fact.callSiteEndOffset,
887
+ callRole: "event_subscribe_handler",
888
+ evidence: {
889
+ relation: target.relation,
890
+ caller: owner.qualifiedName,
891
+ targetName: target.calleeLocalName,
892
+ referenceShape: classification.referenceShape,
893
+ ...target.importBinding ? { importBinding: target.importBinding } : {},
894
+ ...target.localTargetIdentity ? { localTargetIdentity: target.localTargetIdentity } : {},
895
+ ...target.wrapperFunction ? { wrapperFunction: target.wrapperFunction } : {},
896
+ factOrigin: "event_subscribe_handler_reference"
897
+ }
898
+ };
899
+ }
900
+ function enrichSubscription(source, classified, symbols) {
901
+ const classification = classifyHandlerReference(
902
+ classified.node.arguments[1],
326
903
  source,
327
- imports,
328
- namespaceImports
904
+ void 0,
905
+ symbols
906
+ );
907
+ const evidence = {
908
+ ...classified.fact.evidence ?? {},
909
+ handlerReferenceStatus: classification.status,
910
+ ...classification.reason ? { handlerReferenceReason: classification.reason } : {},
911
+ handlerReferenceShape: classification.referenceShape
912
+ };
913
+ return {
914
+ classified: { ...classified, fact: { ...classified.fact, evidence } },
915
+ classification
916
+ };
917
+ }
918
+ function reconcileEventSubscriptions(source, classifications, symbols, calls) {
919
+ const subscriptions = classifications.filter(
920
+ (item) => item.fact.callType === "async_subscribe"
921
+ ).map((item) => enrichSubscription(source, item, symbols));
922
+ const eventSymbols = subscriptions.map((item) => eventSymbol(source, item.classified));
923
+ const eventCalls = subscriptions.flatMap((item, index) => {
924
+ const owner = eventSymbols[index];
925
+ const call = owner ? eventCall(item.classified, owner, item.classification) : void 0;
926
+ return call ? [call] : [];
927
+ });
928
+ return {
929
+ classifications: classifications.map((item) => subscriptions.find((candidate) => candidate.classified.node === item.node)?.classified ?? item),
930
+ symbols: [...symbols.filter((item) => item.kind !== "event_registration"), ...eventSymbols],
931
+ calls: [
932
+ ...calls.filter((item) => item.callRole !== "event_subscribe_handler"),
933
+ ...eventCalls
934
+ ]
935
+ };
936
+ }
937
+
938
+ // src/parsers/007-source-fact-reconciliation.ts
939
+ import "typescript";
940
+
941
+ // src/parsers/006-binding-identity.ts
942
+ import ts3 from "typescript";
943
+ var scopeChainCap = 16;
944
+ function matchFactsToSites(index, facts) {
945
+ return facts.map((fact) => {
946
+ if (fact.bindingSiteStartOffset === void 0 || fact.bindingSiteEndOffset === void 0)
947
+ throw new Error("invalid_prepared_repository_snapshot:binding_site_missing");
948
+ const site = bindingSite(
949
+ index,
950
+ fact.variableName,
951
+ fact.bindingSiteStartOffset,
952
+ fact.bindingSiteEndOffset
953
+ );
954
+ if (!site)
955
+ throw new Error("invalid_prepared_repository_snapshot:binding_site_invalid");
956
+ return { fact, site };
957
+ });
958
+ }
959
+ function exactOwner(fact, site, symbols) {
960
+ const candidates = executableSymbolCandidates(symbols, fact.sourceFile);
961
+ const owner = selectCallOwner(
962
+ candidates,
963
+ site.startOffset,
964
+ site.endOffset
329
965
  );
330
- if (direct) return direct;
331
- if (!ts.isCallExpression(expression) || expression.questionDotToken || expression.arguments.length !== 1) return void 0;
332
- const inner = directHandlerReferenceTarget(
333
- expression.arguments[0],
966
+ if (owner.status === "ambiguous")
967
+ throw new Error("invalid_prepared_repository_snapshot:binding_owner_ambiguous");
968
+ return {
969
+ ...fact,
970
+ bindingSiteStartOffset: site.startOffset,
971
+ bindingSiteEndOffset: site.endOffset,
972
+ sourceSymbolQualifiedName: owner.owner?.qualifiedName,
973
+ ownerResolution: owner.status === "resolved" ? "owned_exact" : "ownerless_file_scope"
974
+ };
975
+ }
976
+ function prepareBindings(index, facts, symbols) {
977
+ return matchFactsToSites(index, facts).map(({ fact, site }) => ({
978
+ fact: exactOwner(fact, site, symbols),
979
+ site,
980
+ aliasProven: site.aliasSource === void 0
981
+ }));
982
+ }
983
+ function aliasStep(binding, source) {
984
+ const aliasKind = binding.site.aliasKind ?? (binding.site.flow === "assignment" ? "identity-assignment" : "identity");
985
+ return {
986
+ callerVariable: binding.fact.variableName,
987
+ aliasOf: source.fact.variableName,
988
+ aliasKind,
989
+ scopeRule: "exact_lexical_scope",
990
+ ...aliasKind === "transaction" ? { transactionAliasSource: source.fact.variableName } : {},
991
+ ...binding.site.aliasArrayIndex === void 0 ? {} : {
992
+ sourceVariable: source.fact.variableName,
993
+ arrayIndex: binding.site.aliasArrayIndex,
994
+ promiseAll: binding.site.aliasPromiseAll ?? false,
995
+ arrayContainer: binding.site.aliasPromiseAll ? "Promise.all" : "array_literal"
996
+ }
997
+ };
998
+ }
999
+ function copyAliasProvenance(binding, source) {
1000
+ binding.fact = {
1001
+ ...binding.fact,
1002
+ alias: source.fact.alias,
1003
+ aliasExpr: source.fact.aliasExpr,
1004
+ destinationExpr: source.fact.destinationExpr,
1005
+ servicePathExpr: source.fact.servicePathExpr,
1006
+ isDynamic: source.fact.isDynamic,
1007
+ placeholders: source.fact.placeholders,
1008
+ helperChain: [
1009
+ ...source.fact.helperChain ?? [],
1010
+ aliasStep(binding, source)
1011
+ ]
1012
+ };
1013
+ binding.aliasProven = true;
1014
+ }
1015
+ function bindingCandidates(bindings) {
1016
+ return bindings.map((binding) => ({
1017
+ variableName: binding.fact.variableName,
1018
+ bindingSiteStartOffset: binding.site.startOffset,
1019
+ bindingSiteEndOffset: binding.site.endOffset,
1020
+ value: binding
1021
+ }));
1022
+ }
1023
+ function resolvedAliasSource(selected) {
1024
+ if (selected.status !== "resolved") return void 0;
1025
+ if (!selected.site?.deterministic) return void 0;
1026
+ if (selected.declarationSite?.declarationKind === "var") return void 0;
1027
+ const source = selected.candidate?.value;
1028
+ return source?.aliasProven ? source : void 0;
1029
+ }
1030
+ function reconcileAliases(index, bindings) {
1031
+ const candidates = bindingCandidates(bindings);
1032
+ for (const binding of bindings) {
1033
+ const sourceName = binding.site.aliasSource;
1034
+ if (!sourceName) continue;
1035
+ const selected = selectVisibleBinding(
1036
+ index,
1037
+ candidates,
1038
+ sourceName,
1039
+ binding.site.node
1040
+ );
1041
+ const source = resolvedAliasSource(selected);
1042
+ if (source) copyAliasProvenance(binding, source);
1043
+ }
1044
+ }
1045
+ function assertUniqueSites(bindings) {
1046
+ const seen = /* @__PURE__ */ new Set();
1047
+ for (const binding of bindings) {
1048
+ const key = [
1049
+ binding.fact.sourceFile,
1050
+ binding.fact.variableName,
1051
+ binding.site.startOffset,
1052
+ binding.site.endOffset
1053
+ ].join("\0");
1054
+ if (seen.has(key))
1055
+ throw new Error("invalid_prepared_repository_snapshot:duplicate_service_binding_site");
1056
+ seen.add(key);
1057
+ }
1058
+ }
1059
+ function callNodeMap(source) {
1060
+ const calls = /* @__PURE__ */ new Map();
1061
+ const visit = (node) => {
1062
+ if (ts3.isCallExpression(node))
1063
+ calls.set(`${node.getStart(source)}:${node.getEnd()}`, node);
1064
+ ts3.forEachChild(node, visit);
1065
+ };
1066
+ visit(source);
1067
+ return calls;
1068
+ }
1069
+ function emptyReference(call, reason) {
1070
+ return {
1071
+ status: call.serviceVariableName ? "unresolved" : "not_applicable",
1072
+ variableName: call.serviceVariableName,
1073
+ scopeChainTotal: 0,
1074
+ scopeChainShown: 0,
1075
+ scopeChainOmitted: 0,
1076
+ reason
1077
+ };
1078
+ }
1079
+ function rejectedReference(call, chain, reason, status = "unresolved") {
1080
+ return {
1081
+ status,
1082
+ variableName: call.serviceVariableName,
1083
+ scopeChainTotal: chain.length,
1084
+ scopeChainShown: Math.min(chain.length, scopeChainCap),
1085
+ scopeChainOmitted: Math.max(0, chain.length - scopeChainCap),
1086
+ reason
1087
+ };
1088
+ }
1089
+ function resolvedReference(call, selected, chain, scopeIndex) {
1090
+ const helperReturn = selected.fact.helperChain?.some(
1091
+ (step) => step.bindingOrigin === "single_hop_helper_return"
1092
+ ) ?? false;
1093
+ return {
1094
+ status: "resolved_exact",
1095
+ variableName: call.serviceVariableName,
1096
+ bindingSourceFile: selected.fact.sourceFile,
1097
+ bindingSiteStartOffset: selected.site.startOffset,
1098
+ bindingSiteEndOffset: selected.site.endOffset,
1099
+ resolutionStrategy: selected.site.flow === "assignment" ? "deterministic_reaching_assignment" : selected.site.aliasSource ? "lexical_alias_declaration" : helperReturn ? "single_hop_helper_return" : "lexical_declaration",
1100
+ lexicalScopeChain: chain,
1101
+ bindingScopeIndex: scopeIndex,
1102
+ scopeChainTotal: chain.length,
1103
+ scopeChainShown: chain.length,
1104
+ scopeChainOmitted: 0
1105
+ };
1106
+ }
1107
+ function selectedBindingReference(call, chain, chosen) {
1108
+ const selected = chosen.candidate?.value;
1109
+ if (!selected) {
1110
+ return rejectedReference(
1111
+ call,
1112
+ chain,
1113
+ chosen.reason ?? "binding_not_found"
1114
+ );
1115
+ }
1116
+ if (chosen.declarationSite?.declarationKind === "var")
1117
+ return rejectedReference(call, chain, "unsupported_var_binding");
1118
+ if (!selected.site.deterministic || !selected.aliasProven)
1119
+ return rejectedReference(call, chain, "unsupported_reaching_assignment");
1120
+ return resolvedReference(call, selected, chain, chosen.scopeIndex ?? 0);
1121
+ }
1122
+ function bindingReference(call, node, index, bindings) {
1123
+ if (!call.serviceVariableName) return emptyReference(call);
1124
+ if (!node) return emptyReference(call, "binding_flow_unsupported");
1125
+ const chain = lexicalScopeChain(node, index.source);
1126
+ if (chain.length > scopeChainCap)
1127
+ return rejectedReference(call, chain, "scope_chain_limit_exceeded");
1128
+ const chosen = selectVisibleBinding(
1129
+ index,
1130
+ bindingCandidates(bindings),
1131
+ call.serviceVariableName,
1132
+ node
1133
+ );
1134
+ if (chosen.status === "ambiguous")
1135
+ return rejectedReference(
1136
+ call,
1137
+ chain,
1138
+ "binding_scope_ambiguous",
1139
+ "ambiguous"
1140
+ );
1141
+ return selectedBindingReference(call, chain, chosen);
1142
+ }
1143
+ function exactCallOwner(call, symbols) {
1144
+ const start = call.callSiteStartOffset;
1145
+ const end = call.callSiteEndOffset;
1146
+ if (start === void 0 || end === void 0)
1147
+ return { resolution: "ownerless_file_scope" };
1148
+ const selected = selectCallOwner(
1149
+ executableSymbolCandidates(symbols, call.sourceFile),
1150
+ start,
1151
+ end,
1152
+ call.callType === "async_subscribe"
1153
+ );
1154
+ if (selected.status === "ambiguous")
1155
+ throw new Error("invalid_prepared_repository_snapshot:outbound_owner_ambiguous");
1156
+ if (call.callType === "async_subscribe" && selected.status !== "resolved")
1157
+ throw new Error("invalid_prepared_repository_snapshot:subscription_owner_missing");
1158
+ return selected.owner ? { qualifiedName: selected.owner.qualifiedName, resolution: "owned_exact" } : { resolution: "ownerless_file_scope" };
1159
+ }
1160
+ function reconcileBindingAndCallIdentity(source, bindingFacts, callFacts, symbols) {
1161
+ const lexicalIndex = createBindingLexicalIndex(source);
1162
+ const bindings = prepareBindings(lexicalIndex, bindingFacts, symbols);
1163
+ reconcileAliases(lexicalIndex, bindings);
1164
+ assertUniqueSites(bindings);
1165
+ const nodes = callNodeMap(source);
1166
+ const calls = callFacts.map((call) => {
1167
+ const key = `${call.callSiteStartOffset}:${call.callSiteEndOffset}`;
1168
+ const reference = bindingReference(
1169
+ call,
1170
+ nodes.get(key),
1171
+ lexicalIndex,
1172
+ bindings
1173
+ );
1174
+ const owner = exactCallOwner(call, symbols);
1175
+ return {
1176
+ ...call,
1177
+ sourceSymbolQualifiedName: owner.qualifiedName,
1178
+ serviceBindingReference: reference,
1179
+ evidence: {
1180
+ ...call.evidence ?? {},
1181
+ sourceOwnerResolution: owner.resolution,
1182
+ serviceBindingReference: reference
1183
+ }
1184
+ };
1185
+ });
1186
+ return { bindings: bindings.map((binding) => binding.fact), calls };
1187
+ }
1188
+
1189
+ // src/parsers/007-source-fact-reconciliation.ts
1190
+ function callKey(call) {
1191
+ return `${call.callType}:${call.callSiteStartOffset}:${call.callSiteEndOffset}`;
1192
+ }
1193
+ function mergedOutboundCalls(existing, classifications) {
1194
+ const classifiedKeys = new Set(
1195
+ classifications.map((item) => callKey(item.fact))
1196
+ );
1197
+ return [
1198
+ ...classifications.map((item) => item.fact),
1199
+ ...existing.filter((call) => !classifiedKeys.has(callKey(call)))
1200
+ ];
1201
+ }
1202
+ function exactSymbolCallOwner(call, symbols) {
1203
+ const start = call.callSiteStartOffset;
1204
+ const end = call.callSiteEndOffset;
1205
+ if (start === void 0 || end === void 0) return void 0;
1206
+ const selected = selectCallOwner(
1207
+ executableSymbolCandidates(symbols, call.sourceFile),
1208
+ start,
1209
+ end,
1210
+ call.callRole === "event_subscribe_handler"
1211
+ );
1212
+ if (selected.status === "ambiguous")
1213
+ throw new Error("invalid_prepared_repository_snapshot:symbol_call_owner_ambiguous");
1214
+ if (call.callRole === "event_subscribe_handler" && !selected.owner)
1215
+ throw new Error("invalid_prepared_repository_snapshot:handler_owner_missing");
1216
+ return selected.owner;
1217
+ }
1218
+ function reconcileSymbolCallOwners(calls, symbols) {
1219
+ return calls.flatMap((call) => {
1220
+ const owner = exactSymbolCallOwner(call, symbols);
1221
+ if (!owner) return [];
1222
+ return [{
1223
+ ...call,
1224
+ callerQualifiedName: owner.qualifiedName,
1225
+ evidence: {
1226
+ ...call.evidence,
1227
+ caller: owner.qualifiedName,
1228
+ callerResolution: "full_span_containment"
1229
+ }
1230
+ }];
1231
+ });
1232
+ }
1233
+ function reconcileSourceFacts(source, classifications, bindings, outboundCalls, symbols, symbolCalls) {
1234
+ const events = reconcileEventSubscriptions(
1235
+ source,
1236
+ classifications,
1237
+ symbols,
1238
+ symbolCalls
1239
+ );
1240
+ const allCalls = mergedOutboundCalls(outboundCalls, events.classifications);
1241
+ const identities = reconcileBindingAndCallIdentity(
334
1242
  source,
335
- imports,
336
- namespaceImports
1243
+ bindings,
1244
+ allCalls,
1245
+ events.symbols
337
1246
  );
338
- return inner ? { ...inner, wrapperFunction: expression.expression.getText(source) } : void 0;
1247
+ return {
1248
+ bindings: identities.bindings,
1249
+ outboundCalls: identities.calls,
1250
+ symbols: events.symbols,
1251
+ symbolCalls: reconcileSymbolCallOwners(events.calls, events.symbols),
1252
+ classifications: events.classifications
1253
+ };
339
1254
  }
340
- function isObjectFunction(node) {
341
- return ts.isFunctionExpression(node) || ts.isArrowFunction(node) || ts.isMethodDeclaration(node);
1255
+
1256
+ // src/parsers/009-symbol-call-facts.ts
1257
+ import ts5 from "typescript";
1258
+ var commonTerminalMembers = /* @__PURE__ */ new Set([
1259
+ "push",
1260
+ "includes",
1261
+ "find",
1262
+ "findIndex",
1263
+ "map",
1264
+ "filter",
1265
+ "reduce",
1266
+ "forEach",
1267
+ "some",
1268
+ "every",
1269
+ "toUpperCase",
1270
+ "toLowerCase",
1271
+ "trim",
1272
+ "split",
1273
+ "join",
1274
+ "get",
1275
+ "set",
1276
+ "has"
1277
+ ]);
1278
+ var loggerMembers = /* @__PURE__ */ new Set([
1279
+ "trace",
1280
+ "debug",
1281
+ "info",
1282
+ "warn",
1283
+ "error",
1284
+ "fatal",
1285
+ "log"
1286
+ ]);
1287
+ var globalObjects = /* @__PURE__ */ new Set([
1288
+ "JSON",
1289
+ "Object",
1290
+ "Array",
1291
+ "String",
1292
+ "Number",
1293
+ "Boolean",
1294
+ "Math",
1295
+ "Date",
1296
+ "Promise",
1297
+ "Reflect"
1298
+ ]);
1299
+ var capDslRoots = /* @__PURE__ */ new Set([
1300
+ "SELECT",
1301
+ "INSERT",
1302
+ "UPSERT",
1303
+ "UPDATE",
1304
+ "DELETE"
1305
+ ]);
1306
+ var requestHelpers = /* @__PURE__ */ new Set([
1307
+ "reject",
1308
+ "error",
1309
+ "info",
1310
+ "warn",
1311
+ "notify"
1312
+ ]);
1313
+ var transportMembers = /* @__PURE__ */ new Set([
1314
+ "emit",
1315
+ "publish",
1316
+ "send",
1317
+ "on"
1318
+ ]);
1319
+ var cdsFrameworkPrefixes = [
1320
+ "cds.connect.",
1321
+ "cds.services.",
1322
+ "cds.parse."
1323
+ ];
1324
+ function symbolCallName(expr) {
1325
+ if (ts5.isIdentifier(expr)) return { expression: expr.text, local: expr.text };
1326
+ if (!ts5.isPropertyAccessExpression(expr))
1327
+ return { expression: expr.getText() };
1328
+ const left = expr.expression.getText();
1329
+ return {
1330
+ expression: expr.getText(),
1331
+ local: left === "this" ? void 0 : left.split(".")[0],
1332
+ member: expr.name.text,
1333
+ receiver: left
1334
+ };
1335
+ }
1336
+ function hasSetMember(value, values) {
1337
+ return value ? values.has(value) : false;
1338
+ }
1339
+ function cdsFrameworkCall(callee) {
1340
+ return callee.expression === "cds.run" || cdsFrameworkPrefixes.some((prefix) => callee.expression.startsWith(prefix));
1341
+ }
1342
+ function requestHelperCall(callee) {
1343
+ return callee.local === "req" && hasSetMember(callee.member, requestHelpers);
1344
+ }
1345
+ function ignoredFrameworkCall(callee) {
1346
+ const checks = [
1347
+ hasSetMember(callee.local, capDslRoots),
1348
+ cdsFrameworkCall(callee),
1349
+ requestHelperCall(callee),
1350
+ hasSetMember(callee.member, transportMembers),
1351
+ hasSetMember(callee.local, globalObjects),
1352
+ callee.expression.startsWith("new Date().")
1353
+ ];
1354
+ return checks.some(Boolean);
1355
+ }
1356
+ function argumentEvidence(args) {
1357
+ return args.map((arg) => {
1358
+ if (ts5.isIdentifier(arg)) return { kind: "identifier", name: arg.text };
1359
+ if (ts5.isArrayLiteralExpression(arg)) return {
1360
+ kind: "array_literal",
1361
+ elements: arg.elements.flatMap((item, index) => ts5.isIdentifier(item) ? [{ index, kind: "identifier", name: item.text }] : [])
1362
+ };
1363
+ if (ts5.isObjectLiteralExpression(arg))
1364
+ return objectArgumentEvidence(arg);
1365
+ return { kind: "unsupported", expression: arg.getText() };
1366
+ });
1367
+ }
1368
+ function objectArgumentEvidence(argument) {
1369
+ const properties = argument.properties.flatMap((property) => {
1370
+ if (ts5.isShorthandPropertyAssignment(property))
1371
+ return [{
1372
+ kind: "shorthand",
1373
+ property: property.name.text,
1374
+ argument: property.name.text
1375
+ }];
1376
+ if (!ts5.isPropertyAssignment(property) || !ts5.isIdentifier(property.initializer)) return [];
1377
+ const name = propertyName(property.name);
1378
+ return name ? [{
1379
+ kind: "property_assignment",
1380
+ property: name,
1381
+ argument: property.initializer.text
1382
+ }] : [];
1383
+ });
1384
+ return { kind: "object_literal", properties };
1385
+ }
1386
+ function propertyName(name) {
1387
+ return ts5.isIdentifier(name) || ts5.isStringLiteralLike(name) || ts5.isNumericLiteral(name) ? name.text : void 0;
1388
+ }
1389
+ function exactCaller(collection, node) {
1390
+ const selected = selectCallOwner(
1391
+ executableSymbolCandidates(collection.symbols, collection.sourceFile),
1392
+ node.getStart(collection.source),
1393
+ node.getEnd()
1394
+ );
1395
+ if (selected.status === "ambiguous")
1396
+ throw new Error("invalid_prepared_repository_snapshot:symbol_call_owner_ambiguous");
1397
+ return selected.owner;
1398
+ }
1399
+ function relation(reference, instance, proxy, localTarget, provenThisMethod) {
1400
+ const derived = derivedRelation(instance, proxy);
1401
+ if (derived) return derived;
1402
+ if (reference?.moduleKind === "package") return "package_import";
1403
+ if (reference?.referenceShape === "namespace_member")
1404
+ return "relative_import_namespace_member";
1405
+ if (reference) return "relative_import";
1406
+ if (localTarget) return "indexed_local_symbol";
1407
+ return provenThisMethod ? "indexed_this_method" : "indexed_local_symbol";
1408
+ }
1409
+ function derivedRelation(instance, proxy) {
1410
+ if (instance?.importBinding?.moduleKind === "package")
1411
+ return "package_import_derived_member";
1412
+ if (instance) return "class_instance_method";
1413
+ if (proxy?.importBinding.moduleKind === "package")
1414
+ return "package_import_derived_member";
1415
+ return proxy ? "relative_import_proxy_member" : void 0;
1416
+ }
1417
+ function targetName(callee, reference, instance, proxy) {
1418
+ if (instance && callee.member)
1419
+ return `${instance.className}.${callee.member}`;
1420
+ if (proxy && callee.member) return callee.member;
1421
+ if (callee.receiver === "this") return callee.member;
1422
+ if (reference) return reference.requestedPublicName;
1423
+ return callee.member && callee.local ? `${callee.local}.${callee.member}` : callee.local;
1424
+ }
1425
+ function callFact(collection, node) {
1426
+ const caller = exactCaller(collection, node);
1427
+ if (!caller) return void 0;
1428
+ const callee = symbolCallName(node.expression);
1429
+ const proxy = callProxy(collection, node);
1430
+ const instance = callInstance(collection, node, callee);
1431
+ const reference = callImportReference(
1432
+ collection,
1433
+ node.expression,
1434
+ callee,
1435
+ instance,
1436
+ proxy
1437
+ );
1438
+ const localTarget = reference || instance || proxy ? void 0 : localSymbolTarget(node.expression, collection.source, collection.symbols);
1439
+ return retainedCallFact(collection, node, caller, callee, {
1440
+ proxy,
1441
+ instance,
1442
+ reference,
1443
+ localTarget
1444
+ });
1445
+ }
1446
+ function expressionRoot(expression) {
1447
+ let current = expression;
1448
+ while (ts5.isPropertyAccessExpression(current))
1449
+ current = current.expression;
1450
+ return ts5.isIdentifier(current) ? current : void 0;
1451
+ }
1452
+ function exactDeclaredContext(identifier, candidates) {
1453
+ const matches = candidates.filter((candidate) => identifierMatchesDeclaration(
1454
+ identifier,
1455
+ candidate.declarationStartOffset,
1456
+ candidate.declarationEndOffset
1457
+ ));
1458
+ return matches.length === 1 ? matches[0] : void 0;
1459
+ }
1460
+ function callProxy(collection, node) {
1461
+ const root = expressionRoot(node.expression);
1462
+ if (!root) return void 0;
1463
+ return exactDeclaredContext(
1464
+ root,
1465
+ collection.proxies.get(root.text) ?? []
1466
+ );
1467
+ }
1468
+ function callInstance(collection, node, callee) {
1469
+ const root = expressionRoot(node.expression);
1470
+ if (root && root.text !== "this") {
1471
+ const exact = exactDeclaredContext(
1472
+ root,
1473
+ collection.instances.get(root.text) ?? []
1474
+ );
1475
+ if (exact) return exact;
1476
+ }
1477
+ return callee.receiver ? thisPropertyInstance(collection, node, callee.receiver) : void 0;
1478
+ }
1479
+ function enclosingClass(node) {
1480
+ let current = node.parent;
1481
+ while (current && !ts5.isSourceFile(current)) {
1482
+ if (ts5.isClassLike(current)) return current;
1483
+ current = current.parent;
1484
+ }
1485
+ return void 0;
1486
+ }
1487
+ function thisPropertyInstance(collection, node, receiver) {
1488
+ if (!receiver.startsWith("this.")) return void 0;
1489
+ const container = enclosingClass(node);
1490
+ if (!container) return void 0;
1491
+ const start = container.getStart(collection.source);
1492
+ const end = container.getEnd();
1493
+ const matches = (collection.instances.get(receiver) ?? []).filter(
1494
+ (candidate) => candidate.containerStartOffset === start && candidate.containerEndOffset === end
1495
+ );
1496
+ return matches.length === 1 ? matches[0] : void 0;
1497
+ }
1498
+ function callImportReference(collection, expression, callee, instance, proxy) {
1499
+ const direct = symbolImportReference(expression, collection.importBindings);
1500
+ if (direct || !callee.member) return direct;
1501
+ const inherited = instance?.importBinding ?? proxy?.importBinding;
1502
+ return inherited ? derivedMemberImportReference(inherited, callee.member) : void 0;
1503
+ }
1504
+ function retainedCallFact(collection, node, caller, callee, context) {
1505
+ const target = targetName(
1506
+ callee,
1507
+ context.reference,
1508
+ context.instance,
1509
+ context.proxy
1510
+ );
1511
+ const className = caller.qualifiedName.includes(".") ? caller.qualifiedName.split(".")[0] : void 0;
1512
+ const thisTarget = callee.receiver === "this" && className && callee.member ? `${className}.${callee.member}` : void 0;
1513
+ const resolvedTarget = thisTarget ?? target;
1514
+ if (!resolvedTarget || !shouldRetainCall(
1515
+ collection,
1516
+ callee,
1517
+ resolvedTarget,
1518
+ context,
1519
+ Boolean(thisTarget)
1520
+ ))
1521
+ return void 0;
1522
+ return createCallFact(
1523
+ collection,
1524
+ node,
1525
+ caller,
1526
+ callee,
1527
+ resolvedTarget,
1528
+ thisTarget,
1529
+ context
1530
+ );
1531
+ }
1532
+ function shouldRetainCall(collection, callee, target, context, provenThisMethod) {
1533
+ if (!target || ignoredCall(callee)) return false;
1534
+ const callables = new Set(collection.symbols.flatMap((symbol) => [symbol.localName, symbol.qualifiedName]));
1535
+ return provenThisMethod || Boolean(context.localTarget) || callables.has(target) && Boolean(context.instance) || hasImportedContext(context);
1536
+ }
1537
+ function ignoredCall(callee) {
1538
+ return loggerCall(callee) || terminalCall(callee) || ignoredFrameworkCall(callee);
1539
+ }
1540
+ function loggerCall(callee) {
1541
+ if (callee.local === "logger") return true;
1542
+ if (callee.receiver?.endsWith(".logger")) return true;
1543
+ return callee.expression.startsWith("this.logger.") && hasSetMember(callee.member, loggerMembers);
1544
+ }
1545
+ function terminalCall(callee) {
1546
+ return hasSetMember(callee.member, commonTerminalMembers) || hasSetMember(callee.member, loggerMembers);
1547
+ }
1548
+ function hasImportedContext(context) {
1549
+ return Boolean(context.reference ?? context.proxy ?? context.instance);
1550
+ }
1551
+ function createCallFact(collection, node, caller, callee, target, thisTarget, context) {
1552
+ const importSource = context.instance?.importSource ?? context.proxy?.importSource ?? context.reference?.rawModuleSpecifier;
1553
+ return {
1554
+ callerQualifiedName: caller.qualifiedName,
1555
+ calleeExpression: callee.expression,
1556
+ calleeLocalName: target,
1557
+ receiverLocalName: callee.member ? callee.local ?? callee.receiver : void 0,
1558
+ importSource,
1559
+ sourceFile: collection.sourceFile,
1560
+ sourceLine: collection.source.getLineAndCharacterOfPosition(
1561
+ node.getStart(collection.source)
1562
+ ).line + 1,
1563
+ callSiteStartOffset: node.getStart(collection.source),
1564
+ callSiteEndOffset: node.getEnd(),
1565
+ callRole: "ordinary_call",
1566
+ evidence: callEvidence(caller, callee, target, thisTarget, context, node)
1567
+ };
1568
+ }
1569
+ function callEvidence(caller, callee, target, thisTarget, context, node) {
1570
+ return {
1571
+ relation: relation(
1572
+ context.reference,
1573
+ context.instance,
1574
+ context.proxy,
1575
+ context.localTarget,
1576
+ Boolean(thisTarget)
1577
+ ),
1578
+ caller: caller.qualifiedName,
1579
+ targetName: target,
1580
+ ...importReferenceEvidence(context),
1581
+ ...instanceEvidence(context.instance, callee),
1582
+ callArguments: argumentEvidence(node.arguments),
1583
+ ...proxyEvidence(context.proxy),
1584
+ ...context.localTarget ? { localTargetIdentity: context.localTarget } : {},
1585
+ candidateStrategy: parserCandidateStrategy(context)
1586
+ };
1587
+ }
1588
+ function importReferenceEvidence(context) {
1589
+ if (!context.reference) return {};
1590
+ const derived = context.instance ?? context.proxy;
1591
+ return derived && context.reference.moduleKind === "package" ? { derivedImportBinding: context.reference } : { importBinding: context.reference };
1592
+ }
1593
+ function instanceEvidence(instance, callee) {
1594
+ if (!instance) return {};
1595
+ return {
1596
+ instanceVariable: instance.propertyName ?? callee.local,
1597
+ className: instance.className,
1598
+ methodName: callee.member,
1599
+ classImportSource: instance.importSource,
1600
+ classImportBinding: instance.importBinding
1601
+ };
1602
+ }
1603
+ function proxyEvidence(proxy) {
1604
+ if (!proxy) return {};
1605
+ return {
1606
+ proxyVariableName: proxy.variableName,
1607
+ factory: proxy.factory,
1608
+ factoryExpression: proxy.factory,
1609
+ factoryImportSource: proxy.importSource,
1610
+ factoryImportBinding: proxy.importBinding
1611
+ };
1612
+ }
1613
+ function parserCandidateStrategy(context) {
1614
+ if (context.instance?.importSource)
1615
+ return "relative_import_class_instance_method";
1616
+ if (context.instance) return "same_file_class_instance_method";
1617
+ return context.proxy ? "proxy_member_exact_export_or_unique_member" : void 0;
342
1618
  }
343
- var commonTerminalMembers = /* @__PURE__ */ new Set(["push", "includes", "find", "findIndex", "map", "filter", "reduce", "forEach", "some", "every", "toUpperCase", "toLowerCase", "trim", "split", "join", "get", "set", "has"]);
344
- var loggerMembers = /* @__PURE__ */ new Set(["trace", "debug", "info", "warn", "error", "fatal", "log"]);
345
- var globalObjects = /* @__PURE__ */ new Set(["JSON", "Object", "Array", "String", "Number", "Boolean", "Math", "Date", "Promise", "Reflect"]);
1619
+ function collectSymbolCallFacts(collection) {
1620
+ const calls = [];
1621
+ const visit = (node) => {
1622
+ if (ts5.isCallExpression(node)) {
1623
+ const call = callFact(collection, node);
1624
+ if (call) calls.push(call);
1625
+ }
1626
+ ts5.forEachChild(node, visit);
1627
+ };
1628
+ visit(collection.source);
1629
+ return calls;
1630
+ }
1631
+
1632
+ // src/parsers/013-executable-body-eligibility.ts
1633
+ import ts6 from "typescript";
1634
+ function hasModifier(node, kind) {
1635
+ return ts6.canHaveModifiers(node) && Boolean(ts6.getModifiers(node)?.some((item) => item.kind === kind));
1636
+ }
1637
+ function executableBodyEligibility(node, source) {
1638
+ if (node.body) return { eligible: true, reason: "body_present" };
1639
+ if (hasModifier(node, ts6.SyntaxKind.AbstractKeyword))
1640
+ return { eligible: false, reason: "abstract_bodyless" };
1641
+ if (hasModifier(node, ts6.SyntaxKind.DeclareKeyword))
1642
+ return { eligible: false, reason: "ambient_declaration" };
1643
+ return {
1644
+ eligible: false,
1645
+ reason: source.isDeclarationFile ? "declaration_only" : "overload_signature"
1646
+ };
1647
+ }
1648
+
1649
+ // src/parsers/017-symbol-derived-contexts.ts
1650
+ import ts7 from "typescript";
346
1651
  var builtInConstructors = /* @__PURE__ */ new Set([
347
1652
  "Set",
348
1653
  "Map",
@@ -377,340 +1682,688 @@ var builtInConstructors = /* @__PURE__ */ new Set([
377
1682
  "Promise",
378
1683
  "AbortController"
379
1684
  ]);
380
- var capDslRoots = /* @__PURE__ */ new Set(["SELECT", "INSERT", "UPSERT", "UPDATE", "DELETE"]);
381
- var requestHelpers = /* @__PURE__ */ new Set(["reject", "error", "info", "warn", "notify"]);
382
- var transportMembers = /* @__PURE__ */ new Set(["emit", "publish", "send", "on"]);
383
- function callName(expr) {
384
- if (ts.isIdentifier(expr)) return { expression: expr.text, local: expr.text };
385
- if (ts.isPropertyAccessExpression(expr)) {
386
- const left = expr.expression.getText();
387
- const root = left.split(".")[0];
388
- return { expression: expr.getText(), local: left === "this" ? void 0 : root, member: expr.name.text, receiver: left };
389
- }
390
- return { expression: expr.getText() };
1685
+ function appendNamed(values, name, value) {
1686
+ const matches = values.get(name) ?? [];
1687
+ matches.push(value);
1688
+ values.set(name, matches);
1689
+ }
1690
+ function assignmentOperator2(kind) {
1691
+ return kind >= ts7.SyntaxKind.FirstAssignment && kind <= ts7.SyntaxKind.LastAssignment;
1692
+ }
1693
+ function mutationUnary2(node) {
1694
+ return node.operator === ts7.SyntaxKind.PlusPlusToken || node.operator === ts7.SyntaxKind.MinusMinusToken;
1695
+ }
1696
+ function receiverIdentifier(expression) {
1697
+ let current = expression;
1698
+ while (ts7.isPropertyAccessExpression(current) || ts7.isElementAccessExpression(current))
1699
+ current = current.expression;
1700
+ return ts7.isIdentifier(current) ? current : void 0;
1701
+ }
1702
+ function nodeWritesMember(node, matches) {
1703
+ if (ts7.isBinaryExpression(node))
1704
+ return assignmentOperator2(node.operatorToken.kind) && matches(node.left);
1705
+ if (ts7.isPrefixUnaryExpression(node) || ts7.isPostfixUnaryExpression(node))
1706
+ return mutationUnary2(node) && matches(node.operand);
1707
+ return ts7.isDeleteExpression(node) && matches(node.expression);
1708
+ }
1709
+ function memberWrite(source, declaration) {
1710
+ const start = declaration.getStart(source);
1711
+ const end = declaration.getEnd();
1712
+ const matches = (expression) => {
1713
+ const receiver = receiverIdentifier(expression);
1714
+ return Boolean(receiver && receiver !== expression && identifierMatchesDeclaration(receiver, start, end));
1715
+ };
1716
+ let written = false;
1717
+ const visit = (node) => {
1718
+ if (written) return;
1719
+ written = nodeWritesMember(node, matches);
1720
+ if (!written) ts7.forEachChild(node, visit);
1721
+ };
1722
+ visit(source);
1723
+ return written;
391
1724
  }
392
- function requireSource(expr) {
393
- if (!ts.isCallExpression(expr) || !ts.isIdentifier(expr.expression) || expr.expression.text !== "require") return void 0;
394
- const first = expr.arguments[0];
395
- return first && ts.isStringLiteral(first) ? first.text : void 0;
1725
+ function stableVariable(collection, node) {
1726
+ return ts7.isIdentifier(node.name) && ts7.isVariableDeclarationList(node.parent) && (node.parent.flags & ts7.NodeFlags.Const) !== 0 && !memberWrite(collection.source, node.name);
396
1727
  }
397
- function ignoredFrameworkCall(callee) {
398
- if (callee.local && capDslRoots.has(callee.local)) return true;
399
- if (callee.expression === "cds.run" || callee.expression.startsWith("cds.connect.") || callee.expression.startsWith("cds.services.") || callee.expression.startsWith("cds.parse.")) return true;
400
- if (callee.local === "req" && callee.member && requestHelpers.has(callee.member)) return true;
401
- if (callee.member && transportMembers.has(callee.member)) return true;
402
- if (callee.local && globalObjects.has(callee.local)) return true;
403
- if (callee.expression.startsWith("new Date().")) return true;
404
- return false;
405
- }
406
- function nearest(symbols, line) {
407
- return symbols.filter((s) => s.startLine <= line && s.endLine >= line).sort((a, b) => a.endLine - a.startLine - (b.endLine - b.startLine))[0];
408
- }
409
- function argumentEvidence(args, source) {
410
- return args.map((arg) => {
411
- if (ts.isIdentifier(arg)) return { kind: "identifier", name: arg.text };
412
- if (ts.isObjectLiteralExpression(arg)) {
413
- const properties = [];
414
- for (const prop of arg.properties) {
415
- if (ts.isShorthandPropertyAssignment(prop)) properties.push({ kind: "shorthand", property: prop.name.text, argument: prop.name.text });
416
- if (ts.isPropertyAssignment(prop)) {
417
- const propName = nameOf(prop.name);
418
- if (propName && ts.isIdentifier(prop.initializer)) properties.push({ kind: "property_assignment", property: propName, argument: prop.initializer.text });
419
- }
420
- }
421
- return { kind: "object_literal", properties };
422
- }
423
- if (ts.isArrayLiteralExpression(arg)) {
424
- const elements = arg.elements.flatMap((element, index) => ts.isIdentifier(element) ? [{ index, kind: "identifier", name: element.text }] : []);
425
- return { kind: "array_literal", elements };
426
- }
427
- return { kind: "unsupported", expression: arg.getText(source) };
1728
+ function collectProxy(collection, node) {
1729
+ if (!ts7.isVariableDeclaration(node) || !stableVariable(collection, node) || !node.initializer || !ts7.isCallExpression(node.initializer) || !ts7.isPropertyAccessExpression(node.initializer.expression)) return;
1730
+ const callee = symbolCallName(node.initializer.expression);
1731
+ const binding = symbolImportReference(
1732
+ node.initializer.expression,
1733
+ collection.importBindings
1734
+ );
1735
+ if (!callee.member || !binding) return;
1736
+ appendNamed(collection.proxies, node.name.text, {
1737
+ importSource: binding.rawModuleSpecifier,
1738
+ importBinding: binding,
1739
+ factory: callee.expression,
1740
+ variableName: node.name.text,
1741
+ declarationStartOffset: node.name.getStart(collection.source),
1742
+ declarationEndOffset: node.name.getEnd()
428
1743
  });
429
1744
  }
1745
+ function propertyContainer2(declaration, propertyName3) {
1746
+ const property = propertyName3 && ts7.isPropertyDeclaration(declaration.parent) ? declaration.parent : void 0;
1747
+ return property && ts7.isClassLike(property.parent) ? property.parent : void 0;
1748
+ }
1749
+ function eligibleClassName(collection, className, imported) {
1750
+ if (builtInConstructors.has(className)) return false;
1751
+ return Boolean(imported || collection.declaredClasses.has(className));
1752
+ }
1753
+ function resolvedClassName(className, imported) {
1754
+ const importedName = imported?.importedName;
1755
+ return importedName && importedName !== "default" ? importedName : className;
1756
+ }
1757
+ function rememberInstance(collection, declaration, classExpression, propertyName3) {
1758
+ const className = classExpression.text;
1759
+ const imported = symbolImportReference(
1760
+ classExpression,
1761
+ collection.importBindings
1762
+ );
1763
+ if (!eligibleClassName(collection, className, imported)) return;
1764
+ const container = propertyContainer2(declaration, propertyName3);
1765
+ const variableName = propertyName3 ? `this.${propertyName3}` : declaration.getText();
1766
+ appendNamed(collection.instances, variableName, {
1767
+ className: resolvedClassName(className, imported),
1768
+ importSource: imported?.rawModuleSpecifier,
1769
+ importBinding: imported,
1770
+ propertyName: propertyName3,
1771
+ declarationStartOffset: declaration.getStart(collection.source),
1772
+ declarationEndOffset: declaration.getEnd(),
1773
+ containerStartOffset: container?.getStart(collection.source),
1774
+ containerEndOffset: container?.getEnd()
1775
+ });
1776
+ }
1777
+ function collectVariableInstance(collection, node) {
1778
+ if (!ts7.isVariableDeclaration(node) || !stableVariable(collection, node))
1779
+ return;
1780
+ const initializer = node.initializer;
1781
+ if (initializer && ts7.isNewExpression(initializer) && ts7.isIdentifier(initializer.expression))
1782
+ rememberInstance(collection, node.name, initializer.expression);
1783
+ }
1784
+ function propertyName2(name) {
1785
+ return ts7.isIdentifier(name) || ts7.isStringLiteralLike(name) || ts7.isNumericLiteral(name) ? name.text : void 0;
1786
+ }
1787
+ function thisPropertyWrite(expression, name) {
1788
+ if (ts7.isPropertyAccessExpression(expression))
1789
+ return expression.expression.kind === ts7.SyntaxKind.ThisKeyword && expression.name.text === name;
1790
+ return ts7.isElementAccessExpression(expression) && expression.expression.kind === ts7.SyntaxKind.ThisKeyword && Boolean(expression.argumentExpression && ts7.isStringLiteralLike(expression.argumentExpression) && expression.argumentExpression.text === name);
1791
+ }
1792
+ function nodeWritesThisProperty(node, name) {
1793
+ if (ts7.isBinaryExpression(node))
1794
+ return assignmentOperator2(node.operatorToken.kind) && thisPropertyWrite(node.left, name);
1795
+ if (ts7.isPrefixUnaryExpression(node) || ts7.isPostfixUnaryExpression(node))
1796
+ return mutationUnary2(node) && thisPropertyWrite(node.operand, name);
1797
+ return ts7.isDeleteExpression(node) && thisPropertyWrite(node.expression, name);
1798
+ }
1799
+ function stableProperty(node, name) {
1800
+ const flags = ts7.getCombinedModifierFlags(node);
1801
+ if ((flags & (ts7.ModifierFlags.Private | ts7.ModifierFlags.Readonly)) === 0 || !ts7.isClassLike(node.parent)) return false;
1802
+ let written = false;
1803
+ const visit = (child) => {
1804
+ if (written || child === node) return;
1805
+ written = nodeWritesThisProperty(child, name);
1806
+ if (!written) ts7.forEachChild(child, visit);
1807
+ };
1808
+ visit(node.parent);
1809
+ return !written;
1810
+ }
1811
+ function collectPropertyInstance(collection, node) {
1812
+ if (!ts7.isPropertyDeclaration(node)) return;
1813
+ const initializer = node.initializer;
1814
+ if (!initializer || !ts7.isNewExpression(initializer) || !ts7.isIdentifier(initializer.expression)) return;
1815
+ const name = propertyName2(node.name);
1816
+ if (name && stableProperty(node, name)) rememberInstance(
1817
+ collection,
1818
+ node.name,
1819
+ initializer.expression,
1820
+ name
1821
+ );
1822
+ }
1823
+ function collectDerivedSymbolContexts(collection) {
1824
+ const visit = (node) => {
1825
+ collectProxy(collection, node);
1826
+ collectVariableInstance(collection, node);
1827
+ collectPropertyInstance(collection, node);
1828
+ ts7.forEachChild(node, visit);
1829
+ };
1830
+ visit(collection.source);
1831
+ }
1832
+
1833
+ // src/parsers/symbol-parser.ts
1834
+ function lineOf2(source, pos) {
1835
+ return source.getLineAndCharacterOfPosition(pos).line + 1;
1836
+ }
1837
+ function nameOf(node) {
1838
+ if (!node) return void 0;
1839
+ if (ts8.isIdentifier(node) || ts8.isStringLiteral(node) || ts8.isNumericLiteral(node)) return node.text;
1840
+ return void 0;
1841
+ }
1842
+ function isFunctionLike(node) {
1843
+ return ts8.isFunctionDeclaration(node) || ts8.isMethodDeclaration(node) || ts8.isFunctionExpression(node) || ts8.isArrowFunction(node);
1844
+ }
1845
+ function exported(node) {
1846
+ return Boolean(ts8.getCombinedModifierFlags(node) & ts8.ModifierFlags.Export);
1847
+ }
1848
+ function defaultExported(node) {
1849
+ return Boolean(
1850
+ ts8.getCombinedModifierFlags(node) & ts8.ModifierFlags.Default
1851
+ );
1852
+ }
1853
+ function isPublicClassMethod(node) {
1854
+ const flags = ts8.getCombinedModifierFlags(node);
1855
+ return (flags & ts8.ModifierFlags.Private) === 0 && (flags & ts8.ModifierFlags.Protected) === 0;
1856
+ }
1857
+ function exportDeclarations(source) {
1858
+ const exports = /* @__PURE__ */ new Map();
1859
+ const visit = (node) => {
1860
+ if (ts8.isExportDeclaration(node) && node.exportClause && ts8.isNamedExports(node.exportClause)) {
1861
+ for (const el of node.exportClause.elements) exports.set((el.propertyName ?? el.name).text, el.name.text);
1862
+ }
1863
+ ts8.forEachChild(node, visit);
1864
+ };
1865
+ visit(source);
1866
+ return exports;
1867
+ }
1868
+ function isObjectFunction(node) {
1869
+ return ts8.isFunctionExpression(node) || ts8.isArrowFunction(node) || ts8.isMethodDeclaration(node);
1870
+ }
1871
+ function requireSource(expr) {
1872
+ if (!ts8.isCallExpression(expr) || !ts8.isIdentifier(expr.expression) || expr.expression.text !== "require") return void 0;
1873
+ const first = expr.arguments[0];
1874
+ return first && ts8.isStringLiteral(first) ? first.text : void 0;
1875
+ }
430
1876
  function bindingLocalName(name, initializer) {
431
- if (ts.isIdentifier(name)) return name.text;
432
- if (initializer && ts.isIdentifier(initializer)) return initializer.text;
1877
+ if (ts8.isIdentifier(name)) return name.text;
1878
+ if (initializer && ts8.isIdentifier(initializer)) return initializer.text;
433
1879
  return void 0;
434
1880
  }
435
1881
  function objectPatternAliases(pattern, parameter, source, lineNode) {
436
1882
  return pattern.elements.flatMap((element) => {
437
- if (element.dotDotDotToken || ts.isObjectBindingPattern(element.name) || ts.isArrayBindingPattern(element.name)) return [];
1883
+ if (element.dotDotDotToken || ts8.isObjectBindingPattern(element.name) || ts8.isArrayBindingPattern(element.name)) return [];
438
1884
  const property = element.propertyName ? nameOf(element.propertyName) : nameOf(element.name);
439
1885
  if (!property) return [];
440
1886
  const local = bindingLocalName(element.name, element.initializer);
441
- return local ? [{ parameter, property, local, kind: "object_parameter_destructure", line: lineOf(source, lineNode.getStart(source)) }] : [];
1887
+ return local ? [{ parameter, property, local, kind: "object_parameter_destructure", line: lineOf2(source, lineNode.getStart(source)) }] : [];
1888
+ });
1889
+ }
1890
+ function parameterPropertyAliases(fn, source) {
1891
+ const parameterNames = new Set(fn.parameters.flatMap((param) => ts8.isIdentifier(param.name) ? [param.name.text] : []));
1892
+ if (!fn.body || parameterNames.size === 0) return [];
1893
+ const aliases = [];
1894
+ const addFromAssignment = (left, right, node) => {
1895
+ if (!ts8.isObjectLiteralExpression(left) || !ts8.isIdentifier(right) || !parameterNames.has(right.text)) return;
1896
+ for (const prop of left.properties) {
1897
+ if (!ts8.isPropertyAssignment(prop)) continue;
1898
+ const property = nameOf(prop.name);
1899
+ if (property && ts8.isIdentifier(prop.initializer)) aliases.push({ parameter: right.text, property, local: prop.initializer.text, kind: "object_parameter_destructure", line: lineOf2(source, node.getStart(source)) });
1900
+ }
1901
+ };
1902
+ const visit = (node) => {
1903
+ if (ts8.isVariableDeclaration(node) && ts8.isObjectBindingPattern(node.name) && node.initializer && ts8.isIdentifier(node.initializer) && parameterNames.has(node.initializer.text)) aliases.push(...objectPatternAliases(node.name, node.initializer.text, source, node));
1904
+ if (ts8.isBinaryExpression(node) && node.operatorToken.kind === ts8.SyntaxKind.EqualsToken) addFromAssignment(ts8.isParenthesizedExpression(node.left) ? node.left.expression : node.left, node.right, node);
1905
+ ts8.forEachChild(node, visit);
1906
+ };
1907
+ visit(fn.body);
1908
+ const seen = /* @__PURE__ */ new Set();
1909
+ return aliases.filter((alias) => {
1910
+ const key = `${alias.parameter}.${alias.property}:${alias.local}`;
1911
+ if (seen.has(key)) return false;
1912
+ seen.add(key);
1913
+ return true;
1914
+ });
1915
+ }
1916
+ function parameterBindings(params) {
1917
+ return params.flatMap((param, index) => {
1918
+ if (ts8.isIdentifier(param.name)) return [{ index, kind: "identifier", name: param.name.text }];
1919
+ if (ts8.isObjectBindingPattern(param.name)) {
1920
+ const properties = param.name.elements.flatMap((element) => {
1921
+ if (element.dotDotDotToken || ts8.isObjectBindingPattern(element.name) || ts8.isArrayBindingPattern(element.name)) return [];
1922
+ const property = element.propertyName ? nameOf(element.propertyName) : nameOf(element.name);
1923
+ if (!property) return [];
1924
+ const local = bindingLocalName(element.name, element.initializer);
1925
+ return local ? [{ property, local }] : [];
1926
+ });
1927
+ return properties.length > 0 ? [{ index, kind: "object_pattern", properties }] : [];
1928
+ }
1929
+ if (ts8.isArrayBindingPattern(param.name)) {
1930
+ const elements = param.name.elements.flatMap((element, elementIndex) => ts8.isBindingElement(element) && !element.dotDotDotToken && ts8.isIdentifier(element.name) ? [{ index: elementIndex, local: element.name.text }] : []);
1931
+ return elements.length > 0 ? [{ index, kind: "array_pattern", elements }] : [];
1932
+ }
1933
+ return [];
1934
+ });
1935
+ }
1936
+ function symbolSourceEvidence(collection, node, options) {
1937
+ if (options.evidence) return options.evidence;
1938
+ if (options.classMemberExported) return {
1939
+ source: "exported_class_member",
1940
+ exportedClass: options.parentRoot,
1941
+ memberKind: ts8.getCombinedModifierFlags(node) & ts8.ModifierFlags.Static ? "static_method" : "class_method"
1942
+ };
1943
+ if (options.classContainerExported && ts8.isMethodDeclaration(node) && isPublicClassMethod(node)) return {
1944
+ source: "exported_class_instance_member",
1945
+ exportedClass: options.parentRoot,
1946
+ exportedClassExportKind: options.classContainerDefaultExported ? "default" : "named",
1947
+ memberKind: "class_method"
1948
+ };
1949
+ if (options.declaredExportName) return {
1950
+ exportedName: options.declaredExportName,
1951
+ source: "export_declaration"
1952
+ };
1953
+ return options.objectExported ? { exportedName: options.qualifiedName, source: "exported_object_literal" } : {};
1954
+ }
1955
+ function executableEvidence(node, source) {
1956
+ if (!isFunctionLike(node)) return {};
1957
+ const bindings = parameterBindings(node.parameters);
1958
+ const parameters = bindings.flatMap((binding) => binding.kind === "identifier" ? [binding.name] : []);
1959
+ const aliases = parameterPropertyAliases(node, source);
1960
+ return {
1961
+ executableBodyEligibility: executableBodyEligibility(node, source),
1962
+ ...bindings.length > 0 ? { parameters, parameterBindings: bindings } : {},
1963
+ ...aliases.length > 0 ? { parameterPropertyAliases: aliases } : {}
1964
+ };
1965
+ }
1966
+ function exportedClassMember(collection, kind, parentName, parentRoot, node) {
1967
+ if (kind !== "method" || !parentName || !collection.exportedClasses.has(parentRoot) || !ts8.isMethodDeclaration(node)) return false;
1968
+ return Boolean(ts8.getCombinedModifierFlags(node) & ts8.ModifierFlags.Static) && isPublicClassMethod(node);
1969
+ }
1970
+ function classExportState(collection, parentName, parentRoot) {
1971
+ return {
1972
+ classContainerExported: Boolean(
1973
+ parentName && collection.exportedClasses.has(parentRoot)
1974
+ ),
1975
+ classContainerDefaultExported: Boolean(
1976
+ parentName && collection.defaultExportedClasses.has(parentRoot)
1977
+ )
1978
+ };
1979
+ }
1980
+ function symbolNames(collection, kind, localName, node, parentName, exportedName) {
1981
+ const parentRoot = parentName?.split(".")[0] ?? "";
1982
+ const declaredExportName = exportedName ?? collection.exportNames.get(
1983
+ parentName ? parentRoot : localName
1984
+ );
1985
+ const qualifiedName = parentName ? `${parentName}.${localName}` : localName;
1986
+ const objectExported = Boolean(
1987
+ parentName && collection.objectExports.has(parentRoot)
1988
+ );
1989
+ const classMemberExported = exportedClassMember(
1990
+ collection,
1991
+ kind,
1992
+ parentName,
1993
+ parentRoot,
1994
+ node
1995
+ );
1996
+ const classState = classExportState(collection, parentName, parentRoot);
1997
+ return {
1998
+ parentRoot,
1999
+ declaredExportName,
2000
+ qualifiedName,
2001
+ objectExported,
2002
+ ...classState,
2003
+ classMemberExported,
2004
+ effectiveName: classMemberExported || objectExported ? qualifiedName : declaredExportName
2005
+ };
2006
+ }
2007
+ function addExecutableSymbol(collection, kind, localName, node, parentName, exportedName, evidence) {
2008
+ const names = symbolNames(
2009
+ collection,
2010
+ kind,
2011
+ localName,
2012
+ node,
2013
+ parentName,
2014
+ exportedName
2015
+ );
2016
+ const sourceEvidence = symbolSourceEvidence(collection, node, {
2017
+ parentRoot: names.parentRoot,
2018
+ qualifiedName: names.qualifiedName,
2019
+ declaredExportName: names.declaredExportName,
2020
+ classContainerExported: names.classContainerExported,
2021
+ classContainerDefaultExported: names.classContainerDefaultExported,
2022
+ classMemberExported: names.classMemberExported,
2023
+ objectExported: names.objectExported,
2024
+ evidence
2025
+ });
2026
+ collection.symbols.push({
2027
+ kind,
2028
+ localName: kind === "object_method" ? names.qualifiedName : localName,
2029
+ exportedName: names.effectiveName,
2030
+ qualifiedName: names.qualifiedName,
2031
+ sourceFile: collection.sourceFile,
2032
+ startLine: lineOf2(collection.source, node.getStart(collection.source)),
2033
+ endLine: lineOf2(collection.source, node.getEnd()),
2034
+ startOffset: node.getStart(collection.source),
2035
+ endOffset: node.getEnd(),
2036
+ exported: exported(node) || Boolean(names.effectiveName),
2037
+ importExportEvidence: {
2038
+ ...sourceEvidence,
2039
+ ...executableEvidence(node, collection.source)
2040
+ }
2041
+ });
2042
+ }
2043
+ function addAliasSymbol(collection, objectName, propertyName3, node) {
2044
+ collection.symbols.push({
2045
+ kind: "object_alias",
2046
+ localName: propertyName3,
2047
+ exportedName: propertyName3,
2048
+ qualifiedName: `${objectName}.${propertyName3}`,
2049
+ sourceFile: collection.sourceFile,
2050
+ startLine: lineOf2(collection.source, node.getStart(collection.source)),
2051
+ endLine: lineOf2(collection.source, node.getEnd()),
2052
+ startOffset: node.getStart(collection.source),
2053
+ endOffset: node.getEnd(),
2054
+ exported: true,
2055
+ importExportEvidence: {
2056
+ source: "exported_object_shorthand",
2057
+ objectName,
2058
+ propertyName: propertyName3,
2059
+ targetImportSource: collection.imports.get(propertyName3)
2060
+ }
442
2061
  });
443
2062
  }
444
- function parameterPropertyAliases(fn, source) {
445
- const parameterNames = new Set(fn.parameters.flatMap((param) => ts.isIdentifier(param.name) ? [param.name.text] : []));
446
- if (!fn.body || parameterNames.size === 0) return [];
447
- const aliases = [];
448
- const addFromAssignment = (left, right, node) => {
449
- if (!ts.isObjectLiteralExpression(left) || !ts.isIdentifier(right) || !parameterNames.has(right.text)) return;
450
- for (const prop of left.properties) {
451
- if (!ts.isPropertyAssignment(prop)) continue;
452
- const property = nameOf(prop.name);
453
- if (property && ts.isIdentifier(prop.initializer)) aliases.push({ parameter: right.text, property, local: prop.initializer.text, kind: "object_parameter_destructure", line: lineOf(source, node.getStart(source)) });
454
- }
455
- };
2063
+ function collectImportSources(collection) {
456
2064
  const visit = (node) => {
457
- if (ts.isVariableDeclaration(node) && ts.isObjectBindingPattern(node.name) && node.initializer && ts.isIdentifier(node.initializer) && parameterNames.has(node.initializer.text)) aliases.push(...objectPatternAliases(node.name, node.initializer.text, source, node));
458
- if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.EqualsToken) addFromAssignment(ts.isParenthesizedExpression(node.left) ? node.left.expression : node.left, node.right, node);
459
- ts.forEachChild(node, visit);
2065
+ if (ts8.isImportDeclaration(node) && ts8.isStringLiteral(node.moduleSpecifier))
2066
+ collectEsmImportSources(collection.imports, node);
2067
+ if (ts8.isVariableStatement(node))
2068
+ collectCjsImportSources(collection.imports, node);
2069
+ ts8.forEachChild(node, visit);
460
2070
  };
461
- visit(fn.body);
462
- const seen = /* @__PURE__ */ new Set();
463
- return aliases.filter((alias) => {
464
- const key = `${alias.parameter}.${alias.property}:${alias.local}`;
465
- if (seen.has(key)) return false;
466
- seen.add(key);
467
- return true;
468
- });
2071
+ visit(collection.source);
2072
+ }
2073
+ function collectEsmImportSources(imports, node) {
2074
+ if (!ts8.isStringLiteral(node.moduleSpecifier)) return;
2075
+ const source = node.moduleSpecifier.text;
2076
+ const clause = node.importClause;
2077
+ if (clause?.name) imports.set(clause.name.text, source);
2078
+ const named = clause?.namedBindings;
2079
+ if (named && ts8.isNamedImports(named))
2080
+ for (const item of named.elements) imports.set(item.name.text, source);
2081
+ if (named && ts8.isNamespaceImport(named))
2082
+ imports.set(named.name.text, source);
2083
+ }
2084
+ function collectCjsImportSources(imports, node) {
2085
+ for (const declaration of node.declarationList.declarations) {
2086
+ const source = declaration.initializer ? requireSource(declaration.initializer) : void 0;
2087
+ if (!source) continue;
2088
+ if (ts8.isIdentifier(declaration.name))
2089
+ imports.set(declaration.name.text, source);
2090
+ if (ts8.isObjectBindingPattern(declaration.name)) {
2091
+ for (const item of declaration.name.elements)
2092
+ if (ts8.isIdentifier(item.name)) imports.set(item.name.text, source);
2093
+ }
2094
+ }
469
2095
  }
470
- function parameterBindings(params) {
471
- return params.flatMap((param, index) => {
472
- if (ts.isIdentifier(param.name)) return [{ index, kind: "identifier", name: param.name.text }];
473
- if (ts.isObjectBindingPattern(param.name)) {
474
- const properties = param.name.elements.flatMap((element) => {
475
- if (element.dotDotDotToken || ts.isObjectBindingPattern(element.name) || ts.isArrayBindingPattern(element.name)) return [];
476
- const property = element.propertyName ? nameOf(element.propertyName) : nameOf(element.name);
477
- if (!property) return [];
478
- const local = bindingLocalName(element.name, element.initializer);
479
- return local ? [{ property, local }] : [];
2096
+ function classPropertySymbol(collection, node, parentClass) {
2097
+ const initializer = node.initializer;
2098
+ const localName = nameOf(node.name);
2099
+ if (!localName || !initializer || !ts8.isArrowFunction(initializer) && !ts8.isFunctionExpression(initializer)) return;
2100
+ const staticPublic = publicStaticProperty(collection, node, parentClass);
2101
+ const memberKind = propertyMemberKind(initializer, staticPublic);
2102
+ addExecutableSymbol(
2103
+ collection,
2104
+ "method",
2105
+ localName,
2106
+ initializer,
2107
+ parentClass,
2108
+ staticPublic ? `${parentClass}.${localName}` : void 0,
2109
+ staticPublic ? { source: "exported_class_member", exportedClass: parentClass, memberKind } : { source: "class_property_function", memberKind }
2110
+ );
2111
+ }
2112
+ function publicStaticProperty(collection, node, parentClass) {
2113
+ const flags = ts8.getCombinedModifierFlags(node);
2114
+ return collection.exportedClasses.has(parentClass) && Boolean(flags & ts8.ModifierFlags.Static) && (flags & ts8.ModifierFlags.Private) === 0 && (flags & ts8.ModifierFlags.Protected) === 0;
2115
+ }
2116
+ function propertyMemberKind(initializer, staticPublic) {
2117
+ if (ts8.isArrowFunction(initializer))
2118
+ return staticPublic ? "static_arrow_function" : "arrow_function_property";
2119
+ return staticPublic ? "static_function_expression" : "function_expression_property";
2120
+ }
2121
+ function objectCallable(property) {
2122
+ if (ts8.isMethodDeclaration(property)) return property;
2123
+ return ts8.isPropertyAssignment(property) && isObjectFunction(property.initializer) ? property.initializer : void 0;
2124
+ }
2125
+ function objectLiteralSymbols(collection, objectName, object, objectIsExported) {
2126
+ if (objectIsExported) collection.objectExports.add(objectName);
2127
+ for (const property of object.properties) {
2128
+ if (objectIsExported && ts8.isShorthandPropertyAssignment(property))
2129
+ addAliasSymbol(collection, objectName, property.name.text, property.name);
2130
+ const callable = objectCallable(property);
2131
+ const propertyName3 = callable ? nameOf(property.name) : void 0;
2132
+ if (callable && propertyName3)
2133
+ addExecutableSymbol(
2134
+ collection,
2135
+ "object_method",
2136
+ propertyName3,
2137
+ callable,
2138
+ objectName
2139
+ );
2140
+ }
2141
+ }
2142
+ function variableSymbols(collection, node) {
2143
+ for (const declaration of node.declarationList.declarations) {
2144
+ const localName = nameOf(declaration.name);
2145
+ const initializer = declaration.initializer;
2146
+ if (!localName || !initializer) continue;
2147
+ if (isFunctionLike(initializer)) addExecutableSymbol(
2148
+ collection,
2149
+ "function",
2150
+ localName,
2151
+ initializer,
2152
+ void 0,
2153
+ exported(node) ? localName : collection.exportNames.get(localName)
2154
+ );
2155
+ if (ts8.isObjectLiteralExpression(initializer))
2156
+ objectLiteralSymbols(
2157
+ collection,
2158
+ localName,
2159
+ initializer,
2160
+ exported(node) || collection.exportNames.has(localName)
2161
+ );
2162
+ }
2163
+ }
2164
+ function collectClassDeclaration(collection, node) {
2165
+ if (!ts8.isClassDeclaration(node) || !node.name) return false;
2166
+ collection.declaredClasses.add(node.name.text);
2167
+ if (exported(node) || collection.exportNames.has(node.name.text))
2168
+ collection.exportedClasses.add(node.name.text);
2169
+ if (defaultExported(node))
2170
+ collection.defaultExportedClasses.add(node.name.text);
2171
+ for (const member of node.members)
2172
+ visitDeclaredSymbol(collection, member, node.name.text);
2173
+ return true;
2174
+ }
2175
+ function collectMethodDeclaration(collection, node, parentClass) {
2176
+ if (!ts8.isMethodDeclaration(node)) return false;
2177
+ const localName = nameOf(node.name);
2178
+ if (localName)
2179
+ addExecutableSymbol(collection, "method", localName, node, parentClass);
2180
+ return true;
2181
+ }
2182
+ function visitDeclaredSymbol(collection, node, parentClass) {
2183
+ if (collectClassDeclaration(collection, node)) return;
2184
+ if (collectMethodDeclaration(collection, node, parentClass)) return;
2185
+ if (ts8.isPropertyDeclaration(node)) {
2186
+ if (parentClass) classPropertySymbol(collection, node, parentClass);
2187
+ return;
2188
+ }
2189
+ if (ts8.isFunctionDeclaration(node) && node.name) {
2190
+ addExecutableSymbol(
2191
+ collection,
2192
+ "function",
2193
+ node.name.text,
2194
+ node,
2195
+ void 0,
2196
+ exported(node) ? node.name.text : void 0
2197
+ );
2198
+ return;
2199
+ }
2200
+ if (ts8.isVariableStatement(node)) {
2201
+ variableSymbols(collection, node);
2202
+ return;
2203
+ }
2204
+ ts8.forEachChild(node, (child) => visitDeclaredSymbol(collection, child, parentClass));
2205
+ }
2206
+ function collectDeclaredSymbols(collection) {
2207
+ visitDeclaredSymbol(collection, collection.source);
2208
+ }
2209
+ function isTopLevelCallback(node) {
2210
+ if (!ts8.isArrowFunction(node) && !ts8.isFunctionExpression(node) || !ts8.isCallExpression(node.parent)) return false;
2211
+ const callee = symbolCallName(node.parent.expression);
2212
+ const member = callee.member ?? callee.local;
2213
+ return Boolean(member && [
2214
+ "bootstrap",
2215
+ "served",
2216
+ "connect",
2217
+ "on",
2218
+ "once",
2219
+ "use",
2220
+ "get",
2221
+ "post",
2222
+ "put",
2223
+ "patch",
2224
+ "delete",
2225
+ "subscribe"
2226
+ ].includes(member));
2227
+ }
2228
+ function collectCallbackSymbols(collection) {
2229
+ const visit = (node) => {
2230
+ if (isTopLevelCallback(node) && containsSupportedOutboundCall(node, collection.classifiedCalls)) {
2231
+ const startLine = lineOf2(
2232
+ collection.source,
2233
+ node.getStart(collection.source)
2234
+ );
2235
+ const name = `callback:${startLine}`;
2236
+ collection.symbols.push({
2237
+ kind: "callback",
2238
+ localName: name,
2239
+ qualifiedName: `module:${collection.sourceFile}#${name}`,
2240
+ sourceFile: collection.sourceFile,
2241
+ startLine,
2242
+ endLine: lineOf2(collection.source, node.getEnd()),
2243
+ startOffset: node.getStart(collection.source),
2244
+ endOffset: node.getEnd(),
2245
+ exported: false,
2246
+ importExportEvidence: {
2247
+ source: "synthetic_outbound_callback",
2248
+ callbackLine: startLine
2249
+ }
480
2250
  });
481
- return properties.length > 0 ? [{ index, kind: "object_pattern", properties }] : [];
482
- }
483
- if (ts.isArrayBindingPattern(param.name)) {
484
- const elements = param.name.elements.flatMap((element, elementIndex) => ts.isBindingElement(element) && !element.dotDotDotToken && ts.isIdentifier(element.name) ? [{ index: elementIndex, local: element.name.text }] : []);
485
- return elements.length > 0 ? [{ index, kind: "array_pattern", elements }] : [];
486
2251
  }
487
- return [];
488
- });
2252
+ ts8.forEachChild(node, visit);
2253
+ };
2254
+ visit(collection.source);
2255
+ }
2256
+ function createCollection(source, sourceFile2, classifiedCalls) {
2257
+ return {
2258
+ source,
2259
+ sourceFile: sourceFile2,
2260
+ classifiedCalls,
2261
+ symbols: [],
2262
+ imports: /* @__PURE__ */ new Map(),
2263
+ importBindings: collectSymbolImportBindings(source),
2264
+ exportNames: exportDeclarations(source),
2265
+ objectExports: /* @__PURE__ */ new Set(),
2266
+ exportedClasses: /* @__PURE__ */ new Set(),
2267
+ defaultExportedClasses: /* @__PURE__ */ new Set(),
2268
+ declaredClasses: /* @__PURE__ */ new Set(),
2269
+ proxies: /* @__PURE__ */ new Map(),
2270
+ instances: /* @__PURE__ */ new Map()
2271
+ };
2272
+ }
2273
+ function populateCollection(collection) {
2274
+ collectImportSources(collection);
2275
+ collectDeclaredSymbols(collection);
2276
+ collectCallbackSymbols(collection);
2277
+ collectDerivedSymbolContexts(collection);
489
2278
  }
490
- async function parseExecutableSymbols(repoPath, filePath, context) {
2279
+ async function sourceFile(repoPath, filePath, context) {
491
2280
  const snapshot = context?.get(filePath);
492
2281
  const text = snapshot?.text ?? await fs4.readFile(path4.join(repoPath, filePath), "utf8");
493
- const source = snapshot?.sourceFile() ?? ts.createSourceFile(
2282
+ return snapshot?.sourceFile() ?? ts8.createSourceFile(
494
2283
  filePath,
495
2284
  text,
496
- ts.ScriptTarget.Latest,
2285
+ ts8.ScriptTarget.Latest,
497
2286
  true,
498
- filePath.endsWith(".ts") ? ts.ScriptKind.TS : ts.ScriptKind.JS
2287
+ filePath.endsWith(".ts") ? ts8.ScriptKind.TS : ts8.ScriptKind.JS
499
2288
  );
500
- const sourceFile = normalizePath(filePath);
501
- const symbols = [];
502
- const calls = [];
503
- const imports = /* @__PURE__ */ new Map();
504
- const namespaceImports = /* @__PURE__ */ new Set();
505
- const eventSubscriptionOffsets = new Set(
506
- classifyOutboundCallsInSource(source, sourceFile).filter((call) => call.fact.callType === "async_subscribe").map((call) => `${call.node.getStart(source)}:${call.node.getEnd()}`)
507
- );
508
- const exportNames = exportDeclarations(source);
509
- const objectExports = /* @__PURE__ */ new Set();
510
- const exportedClasses = /* @__PURE__ */ new Set();
511
- const declaredClasses = /* @__PURE__ */ new Set();
512
- const proxyVariables = /* @__PURE__ */ new Map();
513
- const classInstances = /* @__PURE__ */ new Map();
514
- const addSymbol = (kind, localName, node, parentName, exportedName, evidence) => {
515
- const parentRoot = parentName?.split(".")[0] ?? "";
516
- const declaredExportName = exportedName ?? exportNames.get(parentName ? parentRoot : localName);
517
- const qualifiedName = parentName ? `${parentName}.${localName}` : localName;
518
- const objectExported = parentName ? objectExports.has(parentRoot) : false;
519
- const classMemberExported = kind === "method" && parentName ? exportedClasses.has(parentRoot) && ts.isMethodDeclaration(node) && (ts.getCombinedModifierFlags(node) & ts.ModifierFlags.Static) !== 0 && isPublicClassMethod(node) : false;
520
- const effectiveExportedName = classMemberExported || objectExported ? qualifiedName : declaredExportName;
521
- const bindings = isFunctionLike(node) ? parameterBindings(node.parameters) : void 0;
522
- const params = bindings?.flatMap((binding) => binding.kind === "identifier" ? [binding.name] : []);
523
- const sourceEvidence = evidence ?? (classMemberExported ? { source: "exported_class_member", exportedClass: parentRoot, memberKind: (ts.getCombinedModifierFlags(node) & ts.ModifierFlags.Static) !== 0 ? "static_method" : "class_method", parameters: params } : declaredExportName ? { exportedName: declaredExportName, source: "export_declaration" } : objectExported ? { exportedName: qualifiedName, source: "exported_object_literal" } : void 0);
524
- const aliases = isFunctionLike(node) ? parameterPropertyAliases(node, source) : [];
525
- const parameterEvidence = { ...bindings && bindings.length > 0 ? { parameters: params, parameterBindings: bindings } : {}, ...aliases.length > 0 ? { parameterPropertyAliases: aliases } : {} };
526
- symbols.push({ kind, localName: kind === "object_method" ? qualifiedName : localName, exportedName: effectiveExportedName, qualifiedName, sourceFile, startLine: lineOf(source, node.getStart(source)), endLine: lineOf(source, node.getEnd()), startOffset: node.getStart(source), endOffset: node.getEnd(), exported: exported(node) || Boolean(effectiveExportedName), importExportEvidence: sourceEvidence ? { ...sourceEvidence, ...parameterEvidence } : bindings && bindings.length > 0 ? parameterEvidence : void 0 });
527
- };
528
- const addAliasSymbol = (objectName, propertyName, node, targetImportSource) => {
529
- symbols.push({ kind: "object_alias", localName: propertyName, exportedName: propertyName, qualifiedName: `${objectName}.${propertyName}`, sourceFile, startLine: lineOf(source, node.getStart(source)), endLine: lineOf(source, node.getEnd()), startOffset: node.getStart(source), endOffset: node.getEnd(), exported: true, importExportEvidence: { source: "exported_object_shorthand", objectName, propertyName, targetImportSource } });
530
- };
531
- const visitImports = (node) => {
532
- if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) {
533
- const sourceText = node.moduleSpecifier.text;
534
- const clause = node.importClause;
535
- if (clause?.name) imports.set(clause.name.text, sourceText);
536
- const named = clause?.namedBindings;
537
- if (named && ts.isNamedImports(named)) for (const el of named.elements) imports.set(el.name.text, sourceText);
538
- if (named && ts.isNamespaceImport(named)) {
539
- imports.set(named.name.text, sourceText);
540
- namespaceImports.add(named.name.text);
541
- }
542
- }
543
- if (ts.isVariableStatement(node)) {
544
- for (const declaration of node.declarationList.declarations) {
545
- const requiredSource = declaration.initializer ? requireSource(declaration.initializer) : void 0;
546
- if (ts.isIdentifier(declaration.name) && requiredSource) imports.set(declaration.name.text, requiredSource);
547
- if (ts.isObjectBindingPattern(declaration.name) && requiredSource) {
548
- for (const element of declaration.name.elements) if (ts.isIdentifier(element.name)) imports.set(element.name.text, requiredSource);
549
- }
550
- }
551
- }
552
- ts.forEachChild(node, visitImports);
553
- };
554
- visitImports(source);
555
- const visitSymbols = (node, parentClass) => {
556
- if (ts.isClassDeclaration(node) && node.name) {
557
- declaredClasses.add(node.name.text);
558
- if (exported(node) || exportNames.has(node.name.text)) exportedClasses.add(node.name.text);
559
- for (const member of node.members) visitSymbols(member, node.name.text);
560
- return;
561
- }
562
- if (ts.isMethodDeclaration(node)) {
563
- const localName = nameOf(node.name);
564
- if (localName) addSymbol("method", localName, node, parentClass);
565
- } else if (ts.isPropertyDeclaration(node) && parentClass && node.initializer && (ts.isArrowFunction(node.initializer) || ts.isFunctionExpression(node.initializer))) {
566
- const localName = nameOf(node.name);
567
- if (localName) {
568
- const flags = ts.getCombinedModifierFlags(node);
569
- const staticPublicExported = exportedClasses.has(parentClass) && (flags & ts.ModifierFlags.Static) !== 0 && (flags & ts.ModifierFlags.Private) === 0 && (flags & ts.ModifierFlags.Protected) === 0;
570
- const isArrow = ts.isArrowFunction(node.initializer);
571
- const memberKind = isArrow ? staticPublicExported ? "static_arrow_function" : "arrow_function_property" : staticPublicExported ? "static_function_expression" : "function_expression_property";
572
- addSymbol("method", localName, node.initializer, parentClass, staticPublicExported ? `${parentClass}.${localName}` : void 0, staticPublicExported ? { source: "exported_class_member", exportedClass: parentClass, memberKind } : { source: "class_property_function", memberKind });
573
- }
574
- } else if (ts.isFunctionDeclaration(node) && node.name) addSymbol("function", node.name.text, node, void 0, exported(node) ? node.name.text : void 0);
575
- else if (ts.isVariableStatement(node)) {
576
- for (const d of node.declarationList.declarations) {
577
- const localName = nameOf(d.name);
578
- if (!localName || !d.initializer) continue;
579
- if (isFunctionLike(d.initializer)) addSymbol("function", localName, d.initializer, void 0, exported(node) ? localName : exportNames.get(localName));
580
- if (ts.isObjectLiteralExpression(d.initializer)) {
581
- const objectIsExported = exported(node) || exportNames.has(localName);
582
- if (objectIsExported) objectExports.add(localName);
583
- for (const prop of d.initializer.properties) {
584
- if (objectIsExported && ts.isShorthandPropertyAssignment(prop)) addAliasSymbol(localName, prop.name.text, prop.name, imports.get(prop.name.text));
585
- if (ts.isPropertyAssignment(prop) && isObjectFunction(prop.initializer)) {
586
- const propName = nameOf(prop.name);
587
- if (propName) addSymbol("object_method", propName, prop.initializer, localName);
588
- } else if (ts.isMethodDeclaration(prop)) {
589
- const propName = nameOf(prop.name);
590
- if (propName) addSymbol("object_method", propName, prop, localName);
591
- }
592
- }
593
- }
594
- }
595
- } else ts.forEachChild(node, (child) => visitSymbols(child, parentClass));
596
- };
597
- visitSymbols(source);
598
- const isTopLevelCallback = (node) => {
599
- if (!ts.isArrowFunction(node) && !ts.isFunctionExpression(node)) return false;
600
- if (!ts.isCallExpression(node.parent)) return false;
601
- const callee = callName(node.parent.expression);
602
- const member = callee.member ?? callee.local;
603
- return Boolean(member && ["bootstrap", "served", "connect", "on", "once", "use", "get", "post", "put", "patch", "delete", "subscribe"].includes(member));
604
- };
605
- const visitCallbackSymbols = (node) => {
606
- if (isTopLevelCallback(node) && containsSupportedOutboundCall(node)) {
607
- const startLine = lineOf(source, node.getStart(source));
608
- const name = `callback:${startLine}`;
609
- symbols.push({ kind: "callback", localName: name, qualifiedName: `module:${sourceFile}#${name}`, sourceFile, startLine, endLine: lineOf(source, node.getEnd()), startOffset: node.getStart(source), endOffset: node.getEnd(), exported: false, importExportEvidence: { source: "synthetic_outbound_callback", callbackLine: startLine } });
610
- }
611
- ts.forEachChild(node, visitCallbackSymbols);
612
- };
613
- visitCallbackSymbols(source);
614
- const visitEventRegistrationSymbols = (node) => {
615
- if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === "on") {
616
- const receiver = node.expression.expression.getText(source);
617
- const eventArg = node.arguments[0];
618
- if ((receiver === "cds" || /^(srv|service|serviceClient|messaging|messageClient|eventClient|.*Client)$/.test(receiver)) && eventArg && (ts.isStringLiteral(eventArg) || ts.isNoSubstitutionTemplateLiteral(eventArg))) {
619
- const startLine = lineOf(source, node.getStart(source));
620
- const eventName = eventArg.text.replace(/[^A-Za-z0-9_$-]/g, "_");
621
- const name = `event:${eventName}:${startLine}`;
622
- symbols.push({ kind: "event_registration", localName: name, qualifiedName: `module:${sourceFile}#${name}`, sourceFile, startLine, endLine: lineOf(source, node.getEnd()), startOffset: node.getStart(source), endOffset: node.getEnd(), exported: false, importExportEvidence: { source: "synthetic_event_registration", eventName: eventArg.text, registrationLine: startLine, receiver } });
623
- }
624
- if (eventSubscriptionOffsets.has(`${node.getStart(source)}:${node.getEnd()}`)) {
625
- const startLine = lineOf(source, node.getStart(source));
626
- const handlerArgument = node.arguments[1];
627
- const target = handlerArgument ? handlerReferenceTarget(
628
- handlerArgument,
629
- source,
630
- imports,
631
- namespaceImports
632
- ) : void 0;
633
- const anchor = nearest(symbols, startLine);
634
- if (target && anchor) calls.push({
635
- callerQualifiedName: anchor.qualifiedName,
636
- calleeExpression: target.calleeExpression,
637
- calleeLocalName: target.calleeLocalName,
638
- importSource: target.importSource,
639
- sourceFile,
640
- sourceLine: startLine,
641
- callSiteStartOffset: node.getStart(source),
642
- callSiteEndOffset: node.getEnd(),
643
- callRole: "event_subscribe_handler",
644
- evidence: {
645
- relation: target.relation,
646
- caller: anchor.qualifiedName,
647
- targetName: target.calleeLocalName,
648
- ...target.wrapperFunction ? { wrapperFunction: target.wrapperFunction } : {},
649
- factOrigin: "event_subscribe_handler_reference"
650
- }
651
- });
652
- }
653
- }
654
- ts.forEachChild(node, visitEventRegistrationSymbols);
655
- };
656
- visitEventRegistrationSymbols(source);
657
- const visitProxyVariables = (node) => {
658
- if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && ts.isCallExpression(node.initializer) && ts.isPropertyAccessExpression(node.initializer.expression)) {
659
- const callee = callName(node.initializer.expression);
660
- const importSource = callee.local ? imports.get(callee.local) : void 0;
661
- if (callee.member && importSource && isRelativeImport(importSource)) proxyVariables.set(node.name.text, { importSource, factory: callee.expression, variableName: node.name.text });
662
- }
663
- ts.forEachChild(node, visitProxyVariables);
664
- };
665
- visitProxyVariables(source);
666
- const rememberClassInstance = (variableName, className, propertyName) => {
667
- const importSource = imports.get(className);
668
- if (!builtInConstructors.has(className) && (importSource && isRelativeImport(importSource) || declaredClasses.has(className))) classInstances.set(variableName, { className, importSource, propertyName });
2289
+ }
2290
+ async function parseExecutableSymbols(repoPath, filePath, context, preparedOutboundCalls) {
2291
+ const source = await sourceFile(repoPath, filePath, context);
2292
+ const normalizedFile = normalizePath(filePath);
2293
+ const classified = preparedOutboundCalls ?? classifyOutboundCallsInSource(source, normalizedFile);
2294
+ const collection = createCollection(source, normalizedFile, classified);
2295
+ populateCollection(collection);
2296
+ const calls = collectSymbolCallFacts({
2297
+ source,
2298
+ sourceFile: normalizedFile,
2299
+ symbols: collection.symbols,
2300
+ imports: collection.imports,
2301
+ importBindings: collection.importBindings,
2302
+ proxies: collection.proxies,
2303
+ instances: collection.instances
2304
+ });
2305
+ const events = reconcileEventSubscriptions(
2306
+ source,
2307
+ classified,
2308
+ collection.symbols,
2309
+ calls
2310
+ );
2311
+ return {
2312
+ symbols: events.symbols,
2313
+ calls: reconcileSymbolCallOwners(events.calls, events.symbols)
669
2314
  };
670
- const visitClassInstances = (node) => {
671
- if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && ts.isNewExpression(node.initializer) && ts.isIdentifier(node.initializer.expression)) {
672
- rememberClassInstance(node.name.text, node.initializer.expression.text);
673
- }
674
- if (ts.isPropertyDeclaration(node) && node.initializer && ts.isNewExpression(node.initializer) && ts.isIdentifier(node.initializer.expression)) {
675
- const propertyName = nameOf(node.name);
676
- if (propertyName) rememberClassInstance(`this.${propertyName}`, node.initializer.expression.text, propertyName);
677
- }
678
- ts.forEachChild(node, visitClassInstances);
679
- };
680
- visitClassInstances(source);
681
- const localCallables = new Set(symbols.flatMap((sym) => [sym.localName, sym.qualifiedName]));
682
- const visitCalls = (node) => {
683
- if (ts.isCallExpression(node)) {
684
- const line = lineOf(source, node.getStart(source));
685
- const caller = nearest(symbols, line);
686
- if (caller) {
687
- const callee = callName(node.expression);
688
- const proxy = callee.local ? proxyVariables.get(callee.local) : void 0;
689
- const instance = (callee.local ? classInstances.get(callee.local) : void 0) ?? (callee.receiver ? classInstances.get(callee.receiver) : void 0);
690
- const importSource = instance?.importSource ?? proxy?.importSource ?? (callee.local ? imports.get(callee.local) : void 0) ?? (callee.member && callee.local ? imports.get(callee.local) : void 0);
691
- const directThisMethod = callee.receiver === "this";
692
- const namespaceMember = Boolean(callee.member && callee.local && namespaceImports.has(callee.local));
693
- const packageImport = Boolean(importSource && !isRelativeImport(importSource));
694
- const targetName = instance && callee.member ? `${instance.className}.${callee.member}` : proxy && callee.member ? callee.member : directThisMethod ? callee.member : namespaceMember ? callee.member : packageImport ? callee.member ?? callee.local : callee.member && callee.local ? `${callee.local}.${callee.member}` : callee.local;
695
- const className = caller.qualifiedName.includes(".") ? caller.qualifiedName.split(".")[0] : void 0;
696
- const thisTarget = directThisMethod && className && callee.member ? `${className}.${callee.member}` : void 0;
697
- const loggerLike = callee.receiver?.endsWith(".logger") || callee.local === "logger" || (callee.expression.startsWith("this.logger.") && callee.member ? loggerMembers.has(callee.member) : false);
698
- const terminalMember = callee.member ? commonTerminalMembers.has(callee.member) || loggerMembers.has(callee.member) : false;
699
- const provenLocal = Boolean(targetName) && localCallables.has(String(targetName));
700
- const provenThisMethod = Boolean(thisTarget && localCallables.has(thisTarget));
701
- const provenRelativeImport = Boolean(isRelativeImport(importSource) && targetName);
702
- const provenClassInstance = Boolean(instance && callee.member && targetName);
703
- const provenPackageImport = Boolean(packageImport && targetName);
704
- const ignored = loggerLike || terminalMember || ignoredFrameworkCall(callee);
705
- const resolvedTarget = provenThisMethod ? thisTarget : targetName;
706
- const keep = Boolean(resolvedTarget) && !ignored && (provenLocal || provenThisMethod || provenRelativeImport || provenClassInstance || provenPackageImport);
707
- if (keep) calls.push({ callerQualifiedName: caller.qualifiedName, calleeExpression: callee.expression, calleeLocalName: resolvedTarget, receiverLocalName: callee.member ? callee.local ?? callee.receiver : void 0, importSource, sourceFile, sourceLine: line, callSiteStartOffset: node.getStart(source), callSiteEndOffset: node.getEnd(), callRole: "ordinary_call", evidence: { relation: instance ? "class_instance_method" : proxy ? "relative_import_proxy_member" : packageImport ? "package_import" : namespaceMember ? "relative_import_namespace_member" : importSource ? "relative_import" : provenThisMethod ? "indexed_this_method" : "indexed_local_symbol", caller: caller.qualifiedName, targetName: resolvedTarget, instanceVariable: instance ? instance.propertyName ?? callee.local : void 0, className: instance?.className, methodName: instance ? callee.member : void 0, classImportSource: instance?.importSource, callArguments: argumentEvidence(node.arguments, source), proxyVariableName: proxy?.variableName, factory: proxy?.factory, factoryExpression: proxy?.factory, factoryImportSource: proxy?.importSource, candidateStrategy: instance ? instance.importSource ? "relative_import_class_instance_method" : "same_file_class_instance_method" : proxy ? "proxy_member_exact_export_or_unique_member" : void 0 } });
2315
+ }
2316
+
2317
+ // src/parsers/008-package-surface-publication.ts
2318
+ function symbolIdentity(symbol) {
2319
+ return [
2320
+ symbol.sourceFile,
2321
+ symbol.kind,
2322
+ symbol.qualifiedName,
2323
+ symbol.startOffset,
2324
+ symbol.endOffset
2325
+ ].join("\0");
2326
+ }
2327
+ function exposureBySymbol(analysis) {
2328
+ return new Map(analysis.symbols.map((item) => [
2329
+ symbolIdentity({
2330
+ sourceFile: item.target.sourceFile,
2331
+ kind: item.target.kind,
2332
+ qualifiedName: item.target.qualifiedName,
2333
+ startOffset: item.target.startOffset,
2334
+ endOffset: item.target.endOffset
2335
+ }),
2336
+ item
2337
+ ]));
2338
+ }
2339
+ function analyzeRepositoryPackageSurface(facts, manifest, sources) {
2340
+ const modules = sources.entries().filter((snapshot) => /\.[jt]s$/.test(snapshot.filePath)).map((snapshot) => ({
2341
+ sourceFile: normalizePath(snapshot.filePath),
2342
+ source: snapshot.sourceFile()
2343
+ }));
2344
+ return analyzePackagePublicSurface(facts.packageName, manifest, modules);
2345
+ }
2346
+ function mergePackageSymbolEvidence(symbols, analysis) {
2347
+ const exposures = exposureBySymbol(analysis);
2348
+ return symbols.map((symbol) => {
2349
+ const exposure = exposures.get(symbolIdentity(symbol));
2350
+ if (!exposure) return symbol;
2351
+ return {
2352
+ ...symbol,
2353
+ importExportEvidence: {
2354
+ ...symbol.importExportEvidence ?? {},
2355
+ packagePublicSurface: {
2356
+ schema: analysis.surface.schema,
2357
+ recordCap: analysis.surface.recordCap,
2358
+ bodyEligibility: exposure.target.bodyEligibility,
2359
+ exposures: exposure.exposures,
2360
+ exposureTotal: exposure.exposureTotal,
2361
+ shownExposureCount: exposure.shownExposureCount,
2362
+ omittedExposureCount: exposure.omittedExposureCount
2363
+ }
708
2364
  }
709
- }
710
- ts.forEachChild(node, visitCalls);
711
- };
712
- visitCalls(source);
713
- return { symbols, calls };
2365
+ };
2366
+ });
714
2367
  }
715
2368
 
716
2369
  // src/utils/hashing.ts
@@ -720,6 +2373,136 @@ function sha256Text(text) {
720
2373
  return createHash("sha256").update(text).digest("hex");
721
2374
  }
722
2375
 
2376
+ // src/db/004-package-target-invalidation.ts
2377
+ var resolverKeys = /* @__PURE__ */ new Set([
2378
+ "candidateStrategy",
2379
+ "candidateCount",
2380
+ "eligibleCandidateCount",
2381
+ "selectedCandidateCount",
2382
+ "candidateSetComplete",
2383
+ "resolvedModulePath",
2384
+ "resolvedTargetRepositoryId",
2385
+ "unresolvedReason",
2386
+ "targetRepositoryCandidateCount",
2387
+ "targetRepositoryCandidates",
2388
+ "shownTargetRepositoryCandidateCount",
2389
+ "omittedTargetRepositoryCandidateCount",
2390
+ "publicSurface"
2391
+ ]);
2392
+ function record(value) {
2393
+ return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
2394
+ }
2395
+ function parsedEvidence(value) {
2396
+ if (typeof value !== "string") return void 0;
2397
+ try {
2398
+ return record(JSON.parse(value));
2399
+ } catch {
2400
+ return void 0;
2401
+ }
2402
+ }
2403
+ function packageName(evidence) {
2404
+ return parsePackageImportReference(evidence.importBinding)?.requestedPackageName ?? void 0;
2405
+ }
2406
+ function packageCallEvidenceValid(evidence) {
2407
+ const binding = record(evidence.importBinding);
2408
+ const classified = evidence.relation === "package_import" || binding?.moduleKind === "package";
2409
+ return !classified || packageName(evidence) !== void 0;
2410
+ }
2411
+ function currentCalls(db, workspaceId, targetRepoId) {
2412
+ const rows = db.prepare(`SELECT sc.id,sc.repo_id repoId,
2413
+ sc.evidence_json evidenceJson FROM symbol_calls sc
2414
+ JOIN repositories r ON r.id=sc.repo_id
2415
+ WHERE r.workspace_id=? AND r.id<>? AND r.fact_analyzer_version=?
2416
+ ORDER BY sc.id`).all(workspaceId, targetRepoId, ANALYZER_VERSION);
2417
+ return rows.flatMap((row) => {
2418
+ const evidence = parsedEvidence(row.evidenceJson);
2419
+ if (!evidence || typeof row.id !== "number" || typeof row.repoId !== "number")
2420
+ throw new Error("invalid_current_package_import_evidence");
2421
+ if (!packageCallEvidenceValid(evidence))
2422
+ throw new Error("invalid_current_package_import_evidence");
2423
+ return evidence.relation === "package_import" || record(evidence.importBinding)?.moduleKind === "package" ? [{ id: row.id, repoId: row.repoId, evidence }] : [];
2424
+ });
2425
+ }
2426
+ function pendingEvidence(evidence) {
2427
+ const parser = Object.fromEntries(Object.entries(evidence).filter(
2428
+ ([key]) => !resolverKeys.has(key)
2429
+ ));
2430
+ if (!packageName(parser))
2431
+ throw new Error("invalid_current_package_import_evidence");
2432
+ return JSON.stringify({
2433
+ ...parser,
2434
+ candidateStrategy: "package_import_pending",
2435
+ candidateCount: 0,
2436
+ eligibleCandidateCount: 0,
2437
+ selectedCandidateCount: 0,
2438
+ candidateSetComplete: false,
2439
+ unresolvedReason: "package_resolution_pending"
2440
+ });
2441
+ }
2442
+ function targetWorkspace(db, repoId) {
2443
+ const row = db.prepare(`SELECT workspace_id workspaceId,
2444
+ package_name packageName FROM repositories WHERE id=?`).get(repoId);
2445
+ if (typeof row?.workspaceId !== "number")
2446
+ throw new Error("Repository target is missing its workspace");
2447
+ return {
2448
+ workspaceId: row.workspaceId,
2449
+ packageName: typeof row.packageName === "string" || row.packageName === null ? row.packageName : void 0
2450
+ };
2451
+ }
2452
+ function packageIdentityChanged(previous, next) {
2453
+ const previousName = typeof previous === "string" ? previous : null;
2454
+ const nextName = typeof next === "string" ? next : null;
2455
+ return previousName !== nextName;
2456
+ }
2457
+ function invalidatePackageTargetFacts(db, targetRepoId, newPackageName, batch) {
2458
+ const target = targetWorkspace(db, targetRepoId);
2459
+ const names = new Set(
2460
+ [target.packageName, newPackageName].filter(
2461
+ (value) => typeof value === "string" && value.length > 0
2462
+ )
2463
+ );
2464
+ if (names.size === 0) return;
2465
+ const update = db.prepare(`UPDATE symbol_calls SET callee_symbol_id=NULL,
2466
+ status='unresolved',unresolved_reason='package_resolution_pending',
2467
+ evidence_json=? WHERE id=?`);
2468
+ let matched = false;
2469
+ for (const call of currentCalls(db, target.workspaceId, targetRepoId)) {
2470
+ const requested = packageName(call.evidence);
2471
+ if (!requested || !names.has(requested)) continue;
2472
+ update.run(pendingEvidence(call.evidence), call.id);
2473
+ batch.affectedCallerRepoIds.add(call.repoId);
2474
+ matched = true;
2475
+ }
2476
+ if (matched || packageIdentityChanged(
2477
+ target.packageName,
2478
+ newPackageName
2479
+ )) batch.affectedWorkspaceIds.add(target.workspaceId);
2480
+ }
2481
+ function createPackageInvalidationBatch(publishingRepoIds) {
2482
+ return {
2483
+ publishingRepoIds: new Set(publishingRepoIds),
2484
+ affectedCallerRepoIds: /* @__PURE__ */ new Set(),
2485
+ affectedWorkspaceIds: /* @__PURE__ */ new Set()
2486
+ };
2487
+ }
2488
+ function mergePackageInvalidationEffects(target, source) {
2489
+ for (const repoId of source.affectedCallerRepoIds)
2490
+ target.affectedCallerRepoIds.add(repoId);
2491
+ for (const workspaceId of source.affectedWorkspaceIds)
2492
+ target.affectedWorkspaceIds.add(workspaceId);
2493
+ }
2494
+ function finalizePackageTargetInvalidations(db, batch) {
2495
+ const increment = db.prepare(`UPDATE repositories
2496
+ SET fact_generation=fact_generation+1 WHERE id=?`);
2497
+ for (const repoId of batch.affectedCallerRepoIds)
2498
+ if (!batch.publishingRepoIds.has(repoId)) increment.run(repoId);
2499
+ const stale = db.prepare(`UPDATE repositories
2500
+ SET graph_stale_reason='package_target_facts_changed',
2501
+ graph_stale_at=datetime('now') WHERE workspace_id=?`);
2502
+ for (const workspaceId of batch.affectedWorkspaceIds)
2503
+ stale.run(workspaceId);
2504
+ }
2505
+
723
2506
  // src/indexer/repository-indexer.ts
724
2507
  async function prepareRepositoryIndex(repo, force, instrumentation) {
725
2508
  const sourceFiles = await findSourceFiles(repo.absolute_path);
@@ -739,24 +2522,51 @@ async function prepareRepositoryIndex(repo, force, instrumentation) {
739
2522
  packageSnapshot.rawText
740
2523
  );
741
2524
  if (!force && repo.fingerprint === fingerprint) return { repo, fileCount: 0, diagnosticCount: 0, skipped: true };
742
- const parsed = await parseAllSourceFacts(repo.absolute_path, sources);
2525
+ const parsedFacts = await parseAllSourceFacts(repo.absolute_path, sources);
2526
+ const packageSurface = analyzeRepositoryPackageSurface(
2527
+ packageFacts,
2528
+ packageSnapshot.manifest,
2529
+ sources
2530
+ );
2531
+ const parsed = {
2532
+ ...parsedFacts,
2533
+ symbols: mergePackageSymbolEvidence(parsedFacts.symbols, packageSurface)
2534
+ };
743
2535
  return {
744
2536
  repo,
745
2537
  packageFacts,
746
2538
  fingerprint,
747
2539
  kind: await classifyRepository(repo.absolute_path, packageFacts),
748
2540
  parsed,
2541
+ packagePublicSurface: packageSurface.surface,
749
2542
  fileCount: sourceFiles.length,
750
2543
  diagnosticCount: parsed.handlers.filter((handler) => handler.hasHandlerDecorator && (handler.methods.length === 0 || handler.methods.some((method) => !handlerMethodIsExecutable(method)))).length,
751
2544
  skipped: false
752
2545
  };
753
2546
  }
754
- function publishPreparedRepositoryIndex(db, prepared) {
2547
+ function publishPreparedRepositoryIndex(db, prepared, invalidations) {
755
2548
  if (prepared.skipped) return;
756
- if (!prepared.packageFacts || !prepared.parsed || !prepared.fingerprint || !prepared.kind) throw new Error("Prepared repository index is missing publication facts");
2549
+ if (!prepared.packageFacts || !prepared.parsed || !prepared.fingerprint || !prepared.kind || !prepared.packagePublicSurface)
2550
+ throw new Error("Prepared repository index is missing publication facts");
757
2551
  const now = (/* @__PURE__ */ new Date()).toISOString();
758
2552
  const repoId = prepared.repo.id;
759
- 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);
2553
+ invalidatePackageTargetFacts(
2554
+ db,
2555
+ repoId,
2556
+ prepared.packageFacts.packageName,
2557
+ invalidations
2558
+ );
2559
+ db.prepare(`UPDATE repositories SET package_name=?, package_version=?,
2560
+ dependencies_json=?,package_public_surface_json=?,kind=?,index_status=?
2561
+ WHERE id=?`).run(
2562
+ prepared.packageFacts.packageName,
2563
+ prepared.packageFacts.packageVersion,
2564
+ JSON.stringify(prepared.packageFacts.dependencies),
2565
+ JSON.stringify(prepared.packagePublicSurface),
2566
+ prepared.kind,
2567
+ "indexing",
2568
+ repoId
2569
+ );
760
2570
  clearRepoFacts(db, repoId);
761
2571
  insertRequires(db, repoId, prepared.packageFacts.cdsRequires);
762
2572
  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");
@@ -770,10 +2580,43 @@ function publishPreparedRepositoryIndex(db, prepared) {
770
2580
  insertCalls(db, repoId, prepared.parsed.calls);
771
2581
  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);
772
2582
  }
2583
+ function publishOneRepository(db, prepared, invalidations) {
2584
+ try {
2585
+ db.transaction(() => withPublicationSavepoint(
2586
+ db,
2587
+ prepared.repo.id,
2588
+ () => publishPreparedRepositoryIndex(db, prepared, invalidations)
2589
+ ));
2590
+ return { ok: true };
2591
+ } catch (error) {
2592
+ recordIndexFailure(db, prepared.repo.id, error);
2593
+ return { ok: false, error };
2594
+ }
2595
+ }
2596
+ function withPublicationSavepoint(db, repoId, publish) {
2597
+ const name = `service_flow_repository_${repoId}`;
2598
+ db.exec(`SAVEPOINT ${name}`);
2599
+ try {
2600
+ const result = publish();
2601
+ db.exec(`RELEASE SAVEPOINT ${name}`);
2602
+ return result;
2603
+ } catch (error) {
2604
+ db.exec(`ROLLBACK TO SAVEPOINT ${name}`);
2605
+ db.exec(`RELEASE SAVEPOINT ${name}`);
2606
+ throw error;
2607
+ }
2608
+ }
773
2609
  function recordIndexFailure(db, repoId, error) {
2610
+ if (isPreparedRepositorySnapshotError(error)) {
2611
+ recordPreparedSnapshotFailure(db, repoId, error);
2612
+ return;
2613
+ }
774
2614
  const message = errorMessage(error);
775
2615
  db.prepare("UPDATE repositories SET index_status='failed', error_count=1 WHERE id=?").run(repoId);
776
- db.prepare("DELETE FROM diagnostics WHERE repo_id=? AND code IN ('index_failed_snapshot_preserved','source_read_failed')").run(repoId);
2616
+ db.prepare(`DELETE FROM diagnostics WHERE repo_id=? AND (
2617
+ code IN ('index_failed_snapshot_preserved','source_read_failed')
2618
+ OR code GLOB 'invalid_prepared_repository_snapshot:*'
2619
+ )`).run(repoId);
777
2620
  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}`);
778
2621
  }
779
2622
  async function parseAllSourceFacts(root, sources) {
@@ -783,13 +2626,36 @@ async function parseAllSourceFacts(root, sources) {
783
2626
  facts.fileRecords.push({ relativePath: normalizePath(file), extension: path5.extname(file), sha256: sha256Text(snapshot.text), sizeBytes: snapshot.sizeBytes });
784
2627
  if (file.endsWith(".cds")) facts.services.push(...await parseCdsFile(root, file, sources));
785
2628
  if (/\.[jt]s$/.test(file)) {
2629
+ const source = snapshot.sourceFile();
2630
+ const classified = classifyOutboundCallsInSource(source, file);
786
2631
  facts.handlers.push(...await parseDecorators(root, file, sources));
787
2632
  facts.registrations.push(...await parseHandlerRegistrations(root, file, sources));
788
- facts.bindings.push(...await parseServiceBindings(root, file, sources));
789
- const symbolFacts = await parseExecutableSymbols(root, file, sources);
790
- facts.symbols.push(...symbolFacts.symbols);
791
- facts.symbolCalls.push(...symbolFacts.calls);
792
- facts.calls.push(...await parseOutboundCalls(root, file, sources));
2633
+ const bindings = await parseServiceBindings(root, file, sources);
2634
+ const symbolFacts = await parseExecutableSymbols(
2635
+ root,
2636
+ file,
2637
+ sources,
2638
+ classified
2639
+ );
2640
+ const outboundCalls = await parseOutboundCalls(
2641
+ root,
2642
+ file,
2643
+ sources,
2644
+ classified,
2645
+ bindings
2646
+ );
2647
+ const reconciled = reconcileSourceFacts(
2648
+ source,
2649
+ classified,
2650
+ bindings,
2651
+ outboundCalls,
2652
+ symbolFacts.symbols,
2653
+ symbolFacts.calls
2654
+ );
2655
+ facts.bindings.push(...reconciled.bindings);
2656
+ facts.symbols.push(...reconciled.symbols);
2657
+ facts.symbolCalls.push(...reconciled.symbolCalls);
2658
+ facts.calls.push(...reconciled.outboundCalls);
793
2659
  }
794
2660
  }
795
2661
  return facts;
@@ -831,9 +2697,8 @@ function repositoryFingerprint(sources, facts, packageJsonText) {
831
2697
  }
832
2698
 
833
2699
  // src/indexer/cds-extension-resolver.ts
834
- function materializeCdsExtensionOperations(db, workspaceId) {
835
- 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
836
- FROM cds_services s JOIN repositories r ON r.id=s.repo_id WHERE r.workspace_id=? AND s.is_extend=1`).all(workspaceId);
2700
+ function materializeCdsExtensionOperations(db, workspaceId, excludedRepoIds = /* @__PURE__ */ new Set()) {
2701
+ const extensions = extensionRows(db, workspaceId, excludedRepoIds);
837
2702
  db.transaction(() => {
838
2703
  const changedRepos = /* @__PURE__ */ new Set();
839
2704
  for (const extension of extensions) {
@@ -842,6 +2707,21 @@ function materializeCdsExtensionOperations(db, workspaceId) {
842
2707
  for (const repoId of changedRepos) markRepositoryDerivedFactsChanged(db, repoId);
843
2708
  });
844
2709
  }
2710
+ function extensionRows(db, workspaceId, excludedRepoIds) {
2711
+ const excluded = [...excludedRepoIds].sort((left, right) => left - right);
2712
+ const exclusion = excluded.length > 0 ? ` AND r.id NOT IN (${excluded.map(() => "?").join(",")})` : "";
2713
+ return db.prepare(`SELECT s.id,r.id repoId,s.service_name serviceName,
2714
+ s.qualified_name qualifiedName,s.source_file sourceFile,
2715
+ s.extension_module_specifier moduleSpecifier,
2716
+ s.extension_imported_symbol importedSymbol,
2717
+ s.extension_import_kind importKind
2718
+ FROM cds_services s JOIN repositories r ON r.id=s.repo_id
2719
+ WHERE r.workspace_id=? AND s.is_extend=1${exclusion}
2720
+ ORDER BY r.id,s.id`).all(
2721
+ workspaceId,
2722
+ ...excluded
2723
+ );
2724
+ }
845
2725
  function reconcileExtension(db, workspaceId, extension) {
846
2726
  const bases = resolveBase(db, workspaceId, extension);
847
2727
  const status = bases.length === 1 ? "resolved" : bases.length > 1 ? "ambiguous" : "unresolved";
@@ -906,15 +2786,15 @@ function relativeBase(db, extension, symbol) {
906
2786
  return db.prepare(`SELECT s.id,s.repo_id repoId FROM cds_services s WHERE s.repo_id=? AND s.is_extend=0 AND (s.qualified_name=? OR s.service_name=?) AND (s.source_file=? OR s.source_file=?) ORDER BY s.id`).all(extension.repoId, symbol, symbol, modulePath, `${modulePath}.cds`);
907
2787
  }
908
2788
  function packageBase(db, workspaceId, specifier, symbol) {
909
- const packageName = packageNameFromSpecifier(specifier);
910
- const moduleSuffix = specifier.slice(packageName.length).replace(/^\//, "");
911
- return db.prepare(`SELECT s.id,s.repo_id repoId FROM cds_services s JOIN repositories r ON r.id=s.repo_id WHERE r.workspace_id=? AND s.is_extend=0 AND r.package_name=? AND (s.qualified_name=? OR s.service_name=?) AND (?='' OR s.source_file=? OR s.source_file=?) ORDER BY s.id`).all(workspaceId, packageName, symbol, symbol, moduleSuffix, moduleSuffix, `${moduleSuffix}.cds`);
2789
+ const packageName2 = packageNameFromSpecifier(specifier);
2790
+ const moduleSuffix = specifier.slice(packageName2.length).replace(/^\//, "");
2791
+ return db.prepare(`SELECT s.id,s.repo_id repoId FROM cds_services s JOIN repositories r ON r.id=s.repo_id WHERE r.workspace_id=? AND s.is_extend=0 AND r.package_name=? AND (s.qualified_name=? OR s.service_name=?) AND (?='' OR s.source_file=? OR s.source_file=?) ORDER BY s.id`).all(workspaceId, packageName2, symbol, symbol, moduleSuffix, moduleSuffix, `${moduleSuffix}.cds`);
912
2792
  }
913
2793
  function sameRepoBase(db, extension, symbol) {
914
2794
  return db.prepare("SELECT id,repo_id repoId FROM cds_services WHERE repo_id=? AND is_extend=0 AND (qualified_name=? OR service_name=?) ORDER BY id").all(extension.repoId, symbol, symbol);
915
2795
  }
916
- function normalizeModulePath(sourceFile, specifier) {
917
- const base = sourceFile.split("/").slice(0, -1).join("/");
2796
+ function normalizeModulePath(sourceFile2, specifier) {
2797
+ const base = sourceFile2.split("/").slice(0, -1).join("/");
918
2798
  const parts = `${base}/${specifier}`.split("/");
919
2799
  const out = [];
920
2800
  for (const part of parts) {
@@ -985,42 +2865,157 @@ function claimIndexRun(db, workspaceId, repoCount) {
985
2865
  }
986
2866
  }
987
2867
  async function indexWorkspace(db, workspaceId, options) {
988
- const repos = options.repo ? reposByName(db, options.repo, workspaceId) : listRepositories(db, workspaceId);
989
- if (options.repo && repos.length === 0)
990
- throw new Error(`selector_repo_not_found: no indexed repository matched ${options.repo}.`);
991
- if (options.repo && repos.length > 1)
992
- throw new Error(`selector_repo_ambiguous: repository selector ${options.repo} matched ${repos.length} repositories; use a unique repository name.`);
2868
+ const repos = selectedRepositories(db, workspaceId, options.repo);
993
2869
  const runId = claimIndexRun(db, workspaceId, repos.length);
994
- let fileCount = 0;
995
- let diagnosticCount = 0;
996
- let skippedCount = 0;
997
- const preparedRows = [];
998
- let activeRepoId;
2870
+ const state = {
2871
+ fileCount: 0,
2872
+ diagnosticCount: 0,
2873
+ skippedCount: 0,
2874
+ rows: []
2875
+ };
999
2876
  try {
1000
- for (const repo of repos) {
1001
- activeRepoId = repo.id;
1002
- const result = await prepareRepositoryIndex(repo, options.force);
1003
- preparedRows.push(result);
1004
- fileCount += result.fileCount;
1005
- diagnosticCount += result.diagnosticCount;
1006
- skippedCount += result.skipped ? 1 : 0;
1007
- }
1008
- db.transaction(() => {
1009
- for (const row of preparedRows) {
1010
- activeRepoId = row.repo.id;
1011
- publishPreparedRepositoryIndex(db, row);
1012
- }
1013
- if (options.injectDerivedMaterializationFailure) throw new Error("Injected derived materialization failure");
1014
- materializeCdsExtensionOperations(db, workspaceId);
1015
- db.prepare("UPDATE index_runs SET finished_at=?, status='success', file_count=?, diagnostic_count=? WHERE id=?").run((/* @__PURE__ */ new Date()).toISOString(), fileCount, diagnosticCount, runId);
1016
- });
1017
- return { repoCount: repos.length, indexedCount: repos.length - skippedCount, skippedCount, fileCount, diagnosticCount };
2877
+ await prepareRepositories(repos, options.force, state);
2878
+ return publishPreparedWorkspaceRows(
2879
+ db,
2880
+ workspaceId,
2881
+ runId,
2882
+ state.rows,
2883
+ options
2884
+ );
1018
2885
  } catch (error) {
1019
- db.prepare("UPDATE index_runs SET finished_at=?, status='failed', file_count=?, diagnostic_count=?, error_message=? WHERE id=?").run((/* @__PURE__ */ new Date()).toISOString(), fileCount, diagnosticCount + 1, errorMessage(error), runId);
1020
- if (activeRepoId && preparedRows.length < repos.length) recordIndexFailure(db, activeRepoId, error);
2886
+ finishFailedRun(db, runId, state, error);
2887
+ if (state.activeRepoId && state.rows.length < repos.length)
2888
+ recordIndexFailure(db, state.activeRepoId, error);
1021
2889
  throw error;
1022
2890
  }
1023
2891
  }
2892
+ function selectedRepositories(db, workspaceId, repoName) {
2893
+ const repos = repoName ? reposByName(db, repoName, workspaceId) : listRepositories(db, workspaceId);
2894
+ if (repoName && repos.length === 0)
2895
+ throw new Error(
2896
+ `selector_repo_not_found: no indexed repository matched ${repoName}.`
2897
+ );
2898
+ if (repoName && repos.length > 1)
2899
+ throw new Error(
2900
+ `selector_repo_ambiguous: repository selector ${repoName} matched ${repos.length} repositories; use a unique repository name.`
2901
+ );
2902
+ return repos;
2903
+ }
2904
+ async function prepareRepositories(repos, force, state) {
2905
+ for (const repo of repos) {
2906
+ state.activeRepoId = repo.id;
2907
+ const result = await prepareRepositoryIndex(repo, force);
2908
+ state.rows.push(result);
2909
+ state.fileCount += result.fileCount;
2910
+ state.diagnosticCount += result.diagnosticCount;
2911
+ state.skippedCount += result.skipped ? 1 : 0;
2912
+ }
2913
+ }
2914
+ function publishPreparedWorkspaceRows(db, workspaceId, runId, rows, options = {}) {
2915
+ const state = publicationState(rows);
2916
+ db.transaction(() => {
2917
+ const effects = createPackageInvalidationBatch([]);
2918
+ const publishedRepoIds = [];
2919
+ for (const row of state.rows) {
2920
+ state.activeRepoId = row.repo.id;
2921
+ if (row.skipped) continue;
2922
+ const result = publishPreparedRow(db, row);
2923
+ if (result.status === "failed") {
2924
+ recordPublicationFailure(db, state, row, result.error);
2925
+ continue;
2926
+ }
2927
+ state.indexedCount += 1;
2928
+ publishedRepoIds.push(row.repo.id);
2929
+ mergePackageInvalidationEffects(effects, result.effects);
2930
+ }
2931
+ if (options.injectDerivedMaterializationFailure)
2932
+ throw new Error("Injected derived materialization failure");
2933
+ materializeCdsExtensionOperations(
2934
+ db,
2935
+ workspaceId,
2936
+ state.failedRepoIds
2937
+ );
2938
+ const invalidations = createPackageInvalidationBatch(publishedRepoIds);
2939
+ mergePackageInvalidationEffects(invalidations, effects);
2940
+ finalizePackageTargetInvalidations(db, invalidations);
2941
+ finishCompletedRun(db, runId, state);
2942
+ });
2943
+ return indexSummary(rows.length, state);
2944
+ }
2945
+ function publicationState(rows) {
2946
+ return {
2947
+ rows,
2948
+ fileCount: rows.reduce((total, row) => total + row.fileCount, 0),
2949
+ diagnosticCount: rows.reduce(
2950
+ (total, row) => total + row.diagnosticCount,
2951
+ 0
2952
+ ),
2953
+ skippedCount: rows.filter((row) => row.skipped).length,
2954
+ indexedCount: 0,
2955
+ publicationFailureCount: 0,
2956
+ failedRepoIds: /* @__PURE__ */ new Set(),
2957
+ failedRepos: []
2958
+ };
2959
+ }
2960
+ function publishPreparedRow(db, row) {
2961
+ const effects = createPackageInvalidationBatch([row.repo.id]);
2962
+ const outcome = publishOneRepository(db, row, effects);
2963
+ if (!outcome.ok && !isPreparedRepositorySnapshotError(outcome.error))
2964
+ throw outcome.error;
2965
+ return outcome.ok ? { status: "published", effects } : { status: "failed", error: outcome.error };
2966
+ }
2967
+ function recordPublicationFailure(db, state, row, error) {
2968
+ state.failedRepoIds.add(row.repo.id);
2969
+ state.failedRepos.push({
2970
+ name: row.repo.name,
2971
+ code: isPreparedRepositorySnapshotError(error) ? error.message : "source_read_failed"
2972
+ });
2973
+ state.publicationFailureCount += 1;
2974
+ }
2975
+ function finishCompletedRun(db, runId, state) {
2976
+ const status = completedRunStatus(
2977
+ state.rows.length,
2978
+ state.publicationFailureCount
2979
+ );
2980
+ const error = status === "success" ? null : `${state.publicationFailureCount} repositories failed index publication.`;
2981
+ db.prepare(`UPDATE index_runs SET finished_at=?,status=?,
2982
+ file_count=?,diagnostic_count=?,error_message=? WHERE id=?`).run(
2983
+ (/* @__PURE__ */ new Date()).toISOString(),
2984
+ status,
2985
+ state.fileCount,
2986
+ completedDiagnosticCount(state),
2987
+ error,
2988
+ runId
2989
+ );
2990
+ }
2991
+ function completedRunStatus(repoCount, failedCount) {
2992
+ if (failedCount === 0) return "success";
2993
+ return failedCount === repoCount ? "failed" : "partial_failure";
2994
+ }
2995
+ function completedDiagnosticCount(state) {
2996
+ return state.diagnosticCount + state.publicationFailureCount;
2997
+ }
2998
+ function finishFailedRun(db, runId, state, error) {
2999
+ db.prepare(`UPDATE index_runs SET finished_at=?,status='failed',
3000
+ file_count=?,diagnostic_count=?,error_message=? WHERE id=?`).run(
3001
+ (/* @__PURE__ */ new Date()).toISOString(),
3002
+ state.fileCount,
3003
+ state.diagnosticCount + 1,
3004
+ errorMessage(error),
3005
+ runId
3006
+ );
3007
+ }
3008
+ function indexSummary(repoCount, state) {
3009
+ return {
3010
+ repoCount,
3011
+ indexedCount: state.indexedCount,
3012
+ skippedCount: state.skippedCount,
3013
+ failedCount: state.publicationFailureCount,
3014
+ failedRepos: [...state.failedRepos].sort((left, right) => binaryCompare(left.name, right.name) || binaryCompare(left.code, right.code)),
3015
+ fileCount: state.fileCount,
3016
+ diagnosticCount: completedDiagnosticCount(state)
3017
+ };
3018
+ }
1024
3019
 
1025
3020
  // src/cli/001-doctor-projection.ts
1026
3021
  var boundedDoctorArrayKeys = /* @__PURE__ */ new Set([
@@ -1191,15 +3186,88 @@ function analyzerVersionDiagnostics(db, strict, workspaceId) {
1191
3186
  return [{ severity: "warning", code: "reindex_required_after_analyzer_upgrade", message: "Repository facts were produced by an older or unknown analyzer; run service-flow index --force before relink to apply current parser semantics.", scope: "workspace", affectedRepositoryCount: rows.length, currentAnalyzerVersion: ANALYZER_VERSION, repositories: rows, remediation: "service-flow index --force && service-flow link" }];
1192
3187
  }
1193
3188
 
3189
+ // src/cli/003-doctor-package-resolution.ts
3190
+ function pendingPredicate(alias) {
3191
+ return `${alias}.status='unresolved'
3192
+ AND ${alias}.callee_symbol_id IS NULL
3193
+ AND ${alias}.unresolved_reason='package_resolution_pending'
3194
+ AND json_extract(${alias}.evidence_json,'$.relation')='package_import'
3195
+ AND json_extract(${alias}.evidence_json,
3196
+ '$.importBinding.moduleKind')='package'
3197
+ AND json_extract(${alias}.evidence_json,
3198
+ '$.candidateStrategy')='package_import_pending'
3199
+ AND json_extract(${alias}.evidence_json,'$.candidateCount')=0
3200
+ AND json_extract(${alias}.evidence_json,'$.eligibleCandidateCount')=0
3201
+ AND json_extract(${alias}.evidence_json,'$.selectedCandidateCount')=0
3202
+ AND json_extract(${alias}.evidence_json,'$.candidateSetComplete')=0`;
3203
+ }
3204
+ function count(db, sql) {
3205
+ return Number(db.prepare(sql).get()?.count ?? 0);
3206
+ }
3207
+ function packagePendingDiagnostics(db) {
3208
+ const pending = count(db, `SELECT COUNT(*) count FROM symbol_calls sc
3209
+ WHERE ${pendingPredicate("sc")}`);
3210
+ if (pending === 0) return [];
3211
+ const stale = count(db, `SELECT COUNT(*) count FROM repositories
3212
+ WHERE graph_stale_reason IS NOT NULL`);
3213
+ return [{
3214
+ severity: "warning",
3215
+ code: "package_import_resolution_pending",
3216
+ message: "Package-import facts await workspace linking; terminal package-resolution quality is deferred.",
3217
+ packageResolutionState: "pre_link_pending",
3218
+ pendingPackageImportCount: pending,
3219
+ graphState: "stale",
3220
+ staleRepositoryCount: stale,
3221
+ requiredAction: "relink",
3222
+ remediation: "service-flow link --workspace /workspace --force"
3223
+ }];
3224
+ }
3225
+ function symbolCallQuality(db) {
3226
+ const terminal = `NOT (${pendingPredicate("sc")})`;
3227
+ const row = db.prepare(`SELECT COUNT(*) total,
3228
+ SUM(CASE WHEN sc.status='resolved' THEN 1 ELSE 0 END) resolved,
3229
+ SUM(CASE WHEN sc.status='unresolved' THEN 1 ELSE 0 END) unresolved
3230
+ FROM symbol_calls sc WHERE ${terminal}`).get();
3231
+ const top = db.prepare(`SELECT sc.callee_expression calleeExpression,
3232
+ COUNT(*) count FROM symbol_calls sc
3233
+ WHERE sc.status='unresolved' AND ${terminal}
3234
+ GROUP BY sc.callee_expression
3235
+ ORDER BY count DESC,sc.callee_expression COLLATE BINARY LIMIT 5`).all();
3236
+ const total = Number(row?.total ?? 0);
3237
+ const unresolved = Number(row?.unresolved ?? 0);
3238
+ const ratio = total === 0 ? 0 : Number((unresolved / total).toFixed(4));
3239
+ return {
3240
+ severity: ratio > 0.05 ? "warning" : "info",
3241
+ code: "strict_symbol_call_quality",
3242
+ message: "Terminal symbol-call quality aggregate",
3243
+ total,
3244
+ resolved: Number(row?.resolved ?? 0),
3245
+ unresolved,
3246
+ unresolvedRatio: ratio,
3247
+ unresolvedRatioThreshold: 0.05,
3248
+ topUnresolvedCallees: top
3249
+ };
3250
+ }
3251
+
1194
3252
  // src/cli/doctor.ts
1195
3253
  function doctorDiagnostics(db, strict, options = {}) {
1196
3254
  const lifecycle = factLifecycleDiagnostic(db, options.workspaceId);
1197
- if (lifecycle?.code === "schema_upgrade_required" || lifecycle?.code === "unsupported_future_schema")
1198
- return boundDoctorDiagnostics([lifecycle]);
3255
+ if (lifecycle) return boundDoctorDiagnostics([lifecycle]);
3256
+ const globalLifecycle = options.workspaceId === void 0 ? void 0 : factLifecycleDiagnostic(db);
3257
+ if (globalLifecycle) return boundDoctorDiagnostics([
3258
+ ...schemaDriftDiagnostics(db, strict, options.workspaceId),
3259
+ ...analyzerVersionDiagnostics(db, strict, options.workspaceId),
3260
+ {
3261
+ severity: "warning",
3262
+ code: "workspace_detail_checks_deferred",
3263
+ message: "Selected-workspace lifecycle is valid, but JSON-dependent global detail checks were deferred because another workspace requires reindexing.",
3264
+ remediation: "Run doctor for the affected workspace after force index and link."
3265
+ }
3266
+ ]);
1199
3267
  const diagnostics = db.prepare("SELECT severity,code,message,source_file sourceFile,source_line sourceLine FROM diagnostics ORDER BY id").all();
1200
3268
  return boundDoctorDiagnostics([
1201
- ...lifecycle ? [lifecycle] : [],
1202
3269
  ...diagnostics,
3270
+ ...packagePendingDiagnostics(db),
1203
3271
  ...healthDiagnostics(db, strict),
1204
3272
  ...remoteTargetWithoutImplementationDiagnostics(db, strict, Boolean(options.detail)),
1205
3273
  ...localServiceDiagnostics(db, strict),
@@ -1338,14 +3406,6 @@ function jsonEvidenceQuality(db) {
1338
3406
  { severity: Number(graph.nonObject ?? 0) > 0 || Number(graph.withOutboundEvidence ?? 0) < Number(graph.total ?? 0) ? "warning" : "info", code: "strict_graph_evidence_quality", message: "Call-derived graph evidence and parser-evidence propagation aggregate", total: Number(graph.total ?? 0), nonObject: Number(graph.nonObject ?? 0), withOutboundEvidence: Number(graph.withOutboundEvidence ?? 0), examples: graphExamples }
1339
3407
  ];
1340
3408
  }
1341
- function symbolCallQuality(db) {
1342
- const row = db.prepare("SELECT COUNT(*) total, SUM(CASE WHEN status='resolved' THEN 1 ELSE 0 END) resolved, SUM(CASE WHEN status='unresolved' THEN 1 ELSE 0 END) unresolved FROM symbol_calls").get();
1343
- const top = db.prepare("SELECT callee_expression calleeExpression,COUNT(*) count FROM symbol_calls WHERE status='unresolved' GROUP BY callee_expression ORDER BY count DESC,callee_expression LIMIT 5").all();
1344
- const total = Number(row.total ?? 0);
1345
- const unresolved = Number(row.unresolved ?? 0);
1346
- const ratio = total === 0 ? 0 : Number((unresolved / total).toFixed(4));
1347
- return { severity: ratio > 0.05 ? "warning" : "info", code: "strict_symbol_call_quality", message: "Symbol-call quality aggregate", total, resolved: Number(row.resolved ?? 0), unresolved, unresolvedRatio: ratio, unresolvedRatioThreshold: 0.05, topUnresolvedCallees: top };
1348
- }
1349
3409
  function dbQueryQuality(db) {
1350
3410
  const row = db.prepare("SELECT COUNT(*) total, SUM(CASE WHEN query_entity IS NOT NULL THEN 1 ELSE 0 END) known, SUM(CASE WHEN query_entity IS NULL THEN 1 ELSE 0 END) unknown FROM outbound_calls WHERE call_type='local_db_query'").get();
1351
3411
  const total = Number(row.total ?? 0);
@@ -1561,8 +3621,12 @@ function serviceBindingQuality(db, detail) {
1561
3621
  function bindingCategory(row) {
1562
3622
  const evidence = parseObject(row.evidenceJson);
1563
3623
  const resolution = parseObject(evidence.serviceBindingResolution);
3624
+ const reference = parseObject(evidence.serviceBindingReference);
1564
3625
  if (resolution.status === "rejected_future_binding") return "direct_binding_missing";
1565
- if (resolution.status === "ambiguous") return "ambiguous_binding_candidates";
3626
+ if (reference.reason === "binding_declared_after_call")
3627
+ return "direct_binding_missing";
3628
+ if (resolution.status === "ambiguous" || reference.status === "ambiguous" || reference.reason === "unsupported_reaching_assignment")
3629
+ return "ambiguous_binding_candidates";
1566
3630
  const receiver = evidence.receiver;
1567
3631
  const symbolEvidence = parseObject(row.symbolEvidenceJson);
1568
3632
  if (symbolHasReceiverParameter(symbolEvidence, receiver))
@@ -1588,7 +3652,8 @@ function bindingExample(row) {
1588
3652
  sourceLine: row.sourceLine,
1589
3653
  receiver: evidence.receiver,
1590
3654
  unresolvedReason: row.unresolvedReason,
1591
- bindingResolution: evidence.serviceBindingResolution
3655
+ bindingResolution: evidence.serviceBindingResolution,
3656
+ bindingReference: evidence.serviceBindingReference
1592
3657
  };
1593
3658
  }
1594
3659
  function bindingCategoryAction(category) {
@@ -1841,8 +3906,8 @@ function hintLines(evidence) {
1841
3906
  }
1842
3907
  function dynamicHintLines(evidence) {
1843
3908
  const exploration = isRecord(evidence.dynamicTargetExploration) ? evidence.dynamicTargetExploration : evidence;
1844
- const count = numberValue(exploration.candidateCount);
1845
- if (count === 0) return [];
3909
+ const count2 = numberValue(exploration.candidateCount);
3910
+ if (count2 === 0) return [];
1846
3911
  const shown = numberValue(exploration.shownCandidateCount);
1847
3912
  const omitted = numberValue(exploration.omittedCandidateCount);
1848
3913
  const rejected = numberValue(exploration.rejectedCandidateCount);
@@ -1850,7 +3915,7 @@ function dynamicHintLines(evidence) {
1850
3915
  `viable candidates: ${shown} shown, ${omitted} omitted; rejected: ${rejected}`
1851
3916
  ];
1852
3917
  lines.push(...varSetHints(exploration.suggestedVarSets));
1853
- if (omitted > 0 || rejected > 0 || shown < count)
3918
+ if (omitted > 0 || rejected > 0 || shown < count2)
1854
3919
  lines.push("use --dynamic-mode candidates --max-dynamic-candidates 20 to explore candidate branches");
1855
3920
  return lines;
1856
3921
  }
@@ -1920,9 +3985,9 @@ function diagnosticLocation(diagnostic) {
1920
3985
  }
1921
3986
  function compactMessage(diagnostic) {
1922
3987
  const message = String(diagnostic.message ?? "");
1923
- const count = typeof diagnostic.count === "number" ? ` count=${diagnostic.count}` : "";
3988
+ const count2 = typeof diagnostic.count === "number" ? ` count=${diagnostic.count}` : "";
1924
3989
  const total = typeof diagnostic.total === "number" ? ` total=${diagnostic.total}` : "";
1925
- return `${message}${count}${total}`.trim();
3990
+ return `${message}${count2}${total}`.trim();
1926
3991
  }
1927
3992
  function suggestedHintLines(diagnostic) {
1928
3993
  const direct = cliHints(diagnostic.suggestedHints);
@@ -2096,6 +4161,16 @@ async function markCleanClaimFailed(dbPath, runId, error) {
2096
4161
  }
2097
4162
  }
2098
4163
 
4164
+ // src/cli/001-index-summary.ts
4165
+ function indexCommandOutcome(summary) {
4166
+ const failed = summary.failedCount > 0 ? `, failed ${summary.failedCount} (${summary.failedRepos.map(({ name, code }) => `${name}: ${code}`).join(", ")})` : "";
4167
+ return {
4168
+ stdout: `Indexed ${summary.indexedCount} repositories, skipped ${summary.skippedCount}${failed}, ${summary.fileCount} files, ${summary.diagnosticCount} diagnostics
4169
+ `,
4170
+ exitCode: summary.failedCount > 0 ? 1 : 0
4171
+ };
4172
+ }
4173
+
2099
4174
  // src/cli.ts
2100
4175
  var stdout = createStdoutWriter(process.stdout, fail);
2101
4176
  var TRACE_FORMATS = ["table", "json", "mermaid", "compact-json"];
@@ -2243,26 +4318,30 @@ function runGraphCommand(opts) {
2243
4318
  writeTraceOutput(db, start, options, opts.format);
2244
4319
  });
2245
4320
  }
2246
- function createProgram() {
2247
- const program = new Command();
2248
- program.name("service-flow").description(
4321
+ function configuredProgram() {
4322
+ return new Command().name("service-flow").description(
2249
4323
  "Trace SAP CAP service-to-service flows across multi-repository workspaces"
2250
4324
  ).version(VERSION);
4325
+ }
4326
+ function registerInitCommand(program) {
2251
4327
  program.command("init").argument("<workspace>").option("--db <path>").option("--ignore <pattern...>").action(
2252
4328
  (workspace, opts) => void init(workspace, opts).catch(fail)
2253
4329
  );
4330
+ }
4331
+ function registerIndexCommand(program) {
2254
4332
  program.command("index").option("--workspace <path>").option("--repo <name>").option("--force").action(
2255
4333
  (opts) => void withWorkspace(opts.workspace, async (db, workspaceId) => {
2256
4334
  const r = await indexWorkspace(db, workspaceId, {
2257
4335
  repo: opts.repo,
2258
4336
  force: Boolean(opts.force)
2259
4337
  });
2260
- writeStdout(
2261
- `Indexed ${r.indexedCount} repositories, skipped ${r.skippedCount}, ${r.fileCount} files, ${r.diagnosticCount} diagnostics
2262
- `
2263
- );
4338
+ const outcome = indexCommandOutcome(r);
4339
+ writeStdout(outcome.stdout);
4340
+ if (outcome.exitCode !== 0) process.exitCode = outcome.exitCode;
2264
4341
  }).catch(fail)
2265
4342
  );
4343
+ }
4344
+ function registerLinkCommand(program) {
2266
4345
  program.command("link").option("--workspace <path>").option("--force").action(
2267
4346
  (opts) => void withWorkspace(opts.workspace, (db, workspaceId) => {
2268
4347
  const r = linkWorkspace(db, workspaceId);
@@ -2274,90 +4353,121 @@ function createProgram() {
2274
4353
  );
2275
4354
  }).catch(fail)
2276
4355
  );
4356
+ }
4357
+ function registerTraceCommand(program) {
2277
4358
  program.command("trace").option("--workspace <path>").option("--repo <name>").option("--operation <name>").option("--service <path>").option("--path <operationPath>").option("--handler <name>").option("--depth <n>", "trace depth", "25").addOption(traceFormatOption()).option("--include-external").option("--include-db").option("--include-async").option("--implementation-repo <name>").option("--implementation-hint <scope>", "scoped implementation hint", collect, []).option("--var <key=value>", "dynamic variable", collect, []).option("--dynamic-mode <mode>", "strict|candidates|infer", "strict").option("--max-dynamic-candidates <n>", "maximum dynamic candidates to show", "5").action((opts) => void runTraceCommand(opts).catch(fail));
4359
+ }
4360
+ function listRepositoriesCommand(opts) {
4361
+ return withReadOnlyWorkspace(opts.workspace, (db, workspaceId) => writeStdout(
4362
+ renderJson(
4363
+ listRepositories(db, workspaceId).map((repo) => ({
4364
+ name: repo.name,
4365
+ kind: repo.kind,
4366
+ packageName: repo.package_name
4367
+ }))
4368
+ )
4369
+ ));
4370
+ }
4371
+ function listServicesCommand(opts) {
4372
+ return withReadOnlyWorkspace(opts.workspace, (db, workspaceId) => {
4373
+ const selection = opts.repo ? selectRepository(db, opts.repo, workspaceId) : {};
4374
+ if (selection.diagnostic) {
4375
+ writeStdout(renderJson([selection.diagnostic]));
4376
+ return;
4377
+ }
4378
+ const repo = selection.repo;
4379
+ const rows = db.prepare(
4380
+ "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"
4381
+ ).all(workspaceId, repo?.id, repo?.id);
4382
+ writeStdout(renderJson(rows));
4383
+ });
4384
+ }
4385
+ function listOperationsCommand(opts) {
4386
+ return withReadOnlyWorkspace(opts.workspace, (db, workspaceId) => {
4387
+ const selection = opts.repo ? selectRepository(db, opts.repo, workspaceId) : {};
4388
+ if (selection.diagnostic) {
4389
+ writeStdout(renderJson([selection.diagnostic]));
4390
+ return;
4391
+ }
4392
+ const repo = selection.repo;
4393
+ const rows = db.prepare(
4394
+ "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=?)"
4395
+ ).all(
4396
+ workspaceId,
4397
+ repo?.id,
4398
+ repo?.id,
4399
+ opts.service,
4400
+ opts.service
4401
+ );
4402
+ writeStdout(renderJson(rows));
4403
+ });
4404
+ }
4405
+ function listCallsCommand(opts) {
4406
+ return withReadOnlyWorkspace(opts.workspace, (db, workspaceId) => {
4407
+ const selection = opts.repo ? selectRepository(db, opts.repo, workspaceId) : {};
4408
+ if (selection.diagnostic) {
4409
+ writeStdout(renderJson([selection.diagnostic]));
4410
+ return;
4411
+ }
4412
+ const repo = selection.repo;
4413
+ const rows = db.prepare(
4414
+ "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 ?)"
4415
+ ).all(
4416
+ workspaceId,
4417
+ repo?.id,
4418
+ repo?.id,
4419
+ opts.operation,
4420
+ opts.operation,
4421
+ opts.operation ? `/${opts.operation}` : void 0,
4422
+ opts.operation ? `%${opts.operation}%` : void 0
4423
+ );
4424
+ writeStdout(renderJson(rows));
4425
+ });
4426
+ }
4427
+ function registerListCommands(program) {
2278
4428
  const list = program.command("list");
2279
4429
  list.command("repos").option("--workspace <path>").action(
2280
- (opts) => void withReadOnlyWorkspace(
2281
- opts.workspace,
2282
- (db, workspaceId) => writeStdout(
2283
- renderJson(
2284
- listRepositories(db, workspaceId).map((r) => ({
2285
- name: r.name,
2286
- kind: r.kind,
2287
- packageName: r.package_name
2288
- }))
2289
- )
2290
- )
2291
- ).catch(fail)
4430
+ (opts) => void listRepositoriesCommand(opts).catch(fail)
2292
4431
  );
2293
4432
  list.command("services").option("--workspace <path>").option("--repo <name>").action(
2294
- (opts) => void withReadOnlyWorkspace(opts.workspace, (db, workspaceId) => {
2295
- const selection = opts.repo ? selectRepository(db, opts.repo, workspaceId) : {};
2296
- if (selection.diagnostic) {
2297
- writeStdout(renderJson([selection.diagnostic]));
2298
- return;
2299
- }
2300
- const repo = selection.repo;
2301
- const rows = db.prepare(
2302
- "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"
2303
- ).all(workspaceId, repo?.id, repo?.id);
2304
- writeStdout(renderJson(rows));
2305
- }).catch(fail)
4433
+ (opts) => void listServicesCommand(opts).catch(fail)
2306
4434
  );
2307
4435
  list.command("operations").option("--workspace <path>").option("--repo <name>").option("--service <path>").action(
2308
- (opts) => void withReadOnlyWorkspace(opts.workspace, (db, workspaceId) => {
2309
- const selection = opts.repo ? selectRepository(db, opts.repo, workspaceId) : {};
2310
- if (selection.diagnostic) {
2311
- writeStdout(renderJson([selection.diagnostic]));
2312
- return;
2313
- }
2314
- const repo = selection.repo;
2315
- const rows = db.prepare(
2316
- "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=?)"
2317
- ).all(workspaceId, repo?.id, repo?.id, opts.service, opts.service);
2318
- writeStdout(renderJson(rows));
2319
- }).catch(fail)
4436
+ (opts) => void listOperationsCommand(opts).catch(fail)
2320
4437
  );
2321
4438
  list.command("calls").option("--workspace <path>").option("--repo <name>").option("--operation <name>").action(
2322
- (opts) => void withReadOnlyWorkspace(opts.workspace, (db, workspaceId) => {
2323
- const selection = opts.repo ? selectRepository(db, opts.repo, workspaceId) : {};
2324
- if (selection.diagnostic) {
2325
- writeStdout(renderJson([selection.diagnostic]));
2326
- return;
2327
- }
2328
- const repo = selection.repo;
2329
- const rows = db.prepare(
2330
- "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 ?)"
2331
- ).all(
2332
- workspaceId,
2333
- repo?.id,
2334
- repo?.id,
2335
- opts.operation,
2336
- opts.operation,
2337
- opts.operation ? `/${opts.operation}` : void 0,
2338
- opts.operation ? `%${opts.operation}%` : void 0
2339
- );
2340
- writeStdout(renderJson(rows));
2341
- }).catch(fail)
4439
+ (opts) => void listCallsCommand(opts).catch(fail)
2342
4440
  );
4441
+ }
4442
+ function registerGraphCommand(program) {
2343
4443
  program.command("graph").option("--workspace <path>").option("--repo <name>").option("--operation <name>").option("--service <path>").option("--path <operationPath>").addOption(graphFormatOption()).option("--implementation-repo <name>").option("--implementation-hint <scope>", "scoped implementation hint", collect, []).option("--var <key=value>", "dynamic variable", collect, []).option("--dynamic-mode <mode>", "strict|candidates|infer", "strict").option("--max-dynamic-candidates <n>", "maximum dynamic candidates to show", "5").action((opts) => void runGraphCommand(opts).catch(fail));
4444
+ }
4445
+ function inspectRepositoryCommand(name, opts) {
4446
+ return withReadOnlyWorkspace(opts.workspace, (db, workspaceId) => {
4447
+ const selection = selectRepository(db, name, workspaceId);
4448
+ writeStdout(renderJson(
4449
+ selection.repo ?? selection.diagnostic ?? { error: "repo not found" }
4450
+ ));
4451
+ });
4452
+ }
4453
+ function inspectOperationCommand(selector, opts) {
4454
+ return withReadOnlyWorkspace(opts.workspace, (db, workspaceId) => {
4455
+ const rows = db.prepare(
4456
+ "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=?)"
4457
+ ).all(workspaceId, selector, selector);
4458
+ writeStdout(renderJson(rows));
4459
+ });
4460
+ }
4461
+ function registerInspectCommands(program) {
2344
4462
  const inspect = program.command("inspect");
2345
4463
  inspect.command("repo").argument("<name>").option("--workspace <path>").action(
2346
- (name, opts) => void withReadOnlyWorkspace(opts.workspace, (db, workspaceId) => {
2347
- const selection = selectRepository(db, name, workspaceId);
2348
- writeStdout(renderJson(
2349
- selection.repo ?? selection.diagnostic ?? { error: "repo not found" }
2350
- ));
2351
- }).catch(fail)
4464
+ (name, opts) => void inspectRepositoryCommand(name, opts).catch(fail)
2352
4465
  );
2353
4466
  inspect.command("operation").argument("<selector>").option("--workspace <path>").action(
2354
- (selector, opts) => void withReadOnlyWorkspace(opts.workspace, (db, workspaceId) => {
2355
- const rows = db.prepare(
2356
- "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=?)"
2357
- ).all(workspaceId, selector, selector);
2358
- writeStdout(renderJson(rows));
2359
- }).catch(fail)
4467
+ (selector, opts) => void inspectOperationCommand(selector, opts).catch(fail)
2360
4468
  );
4469
+ }
4470
+ function registerDoctorCommand(program) {
2361
4471
  program.command("doctor").option("--workspace <path>").option("--strict").option("--detail").option("--format <format>", "json|table").action(
2362
4472
  (opts) => void withReadOnlyWorkspace(opts.workspace, (db, workspaceId) => {
2363
4473
  const allDiagnostics = doctorDiagnostics(db, Boolean(opts.strict), {
@@ -2367,6 +4477,8 @@ function createProgram() {
2367
4477
  writeStdout(renderDoctorDiagnostics(allDiagnostics, opts.format));
2368
4478
  }).catch(fail)
2369
4479
  );
4480
+ }
4481
+ function registerCleanCommand(program) {
2370
4482
  program.command("clean").option("--workspace <path>").option("--db-only").action(
2371
4483
  (opts) => void (async () => {
2372
4484
  const config = await loadWorkspaceConfig(opts.workspace);
@@ -2374,6 +4486,18 @@ function createProgram() {
2374
4486
  writeStdout("Cleaned service-flow state\n");
2375
4487
  })().catch(fail)
2376
4488
  );
4489
+ }
4490
+ function createProgram() {
4491
+ const program = configuredProgram();
4492
+ registerInitCommand(program);
4493
+ registerIndexCommand(program);
4494
+ registerLinkCommand(program);
4495
+ registerTraceCommand(program);
4496
+ registerListCommands(program);
4497
+ registerGraphCommand(program);
4498
+ registerInspectCommands(program);
4499
+ registerDoctorCommand(program);
4500
+ registerCleanCommand(program);
2377
4501
  return program;
2378
4502
  }
2379
4503
  function collect(value, previous) {