@saptools/service-flow 0.1.7 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,14 +1,15 @@
1
1
  # Changelog
2
2
 
3
- ## 0.1.7
4
-
5
- - Added atomic last-good repository publication so failed source reads preserve the previous complete snapshot and fingerprint.
6
- - Added explicit graph freshness metadata and stale diagnostics after successful fact changes until relink.
7
- - Persisted handler registration class/import evidence and linked registered cross-package implementation handlers through application dependency evidence.
8
- - Made service-only trace selectors return a typed narrowing diagnostic instead of workspace-wide traversal.
9
- - Expanded link summaries with dependency and implementation categories whose totals reconcile with persisted graph edges.
10
- - Preserved terminal parser warning evidence separately from routing status.
11
-
3
+ ## 0.1.8
4
+
5
+ - Replaced regex-only handler registration extraction with TypeScript AST evidence for direct handler arrays, identifier arrays, spreads, imported arrays, default exports, aliases, and safe relative re-exports.
6
+ - Persisted class-level `handler_registrations` rows with registration file/line and import evidence so registered handlers can be resolved across same-repository and cross-package layouts.
7
+ - Relaxed implementation linking to support same-repository registrations, handler-package-owned registrations, and application registrations while preserving ambiguous candidate evidence.
8
+ - Continued trace traversal from static and runtime-resolved operations into registered implementation handlers via `OPERATION_IMPLEMENTED_BY_HANDLER` edges.
9
+ - Moved source discovery, reads, and fingerprinting into the protected repository indexing flow so failed reads preserve last-good facts/fingerprints and produce doctor-visible diagnostics.
10
+ - Added a legacy-schema doctor warning when migrated stores lack fresh foreign-key metadata for key tables.
11
+ - Suppressed the known `node:sqlite` experimental warning for normal supported-runtime database commands without suppressing application errors.
12
+ - Changed `index_run_abandoned` doctor policy to report only running index runs older than the documented 60-minute threshold, including run id and start time.
12
13
 
13
14
  ## 0.1.6
14
15
 
package/README.md CHANGED
@@ -47,19 +47,26 @@ npm install @saptools/service-flow
47
47
  ```
48
48
 
49
49
  > [!NOTE]
50
- > Requires **Node.js ≥ 24.0.0** for the bundled `node:sqlite` runtime. Version 0.1.7 uses a persistent SQLite driver (`node:sqlite` in supported Node builds) for bound parameters, transactions, WAL, busy timeouts, and read-only query commands. The analyzer is static: it reads files and package metadata, but it does not start CAP services, connect to SAP BTP, or execute application code.
50
+ > Requires **Node.js ≥ 24.0.0** for the bundled `node:sqlite` runtime. Version 0.1.8 uses a persistent SQLite driver (`node:sqlite` in supported Node builds) for bound parameters, transactions, WAL, busy timeouts, and read-only query commands. The analyzer is static: it reads files and package metadata, but it does not start CAP services, connect to SAP BTP, or execute application code.
51
51
 
52
52
  ---
53
53
 
54
54
 
55
- ### Correctness notes for 0.1.7
55
+ ### Correctness notes for 0.1.8
56
56
 
57
57
  - Runtime `--var` values are considered only for dynamic, ambiguous, or unresolved **remote** graph edges whose alias, destination, service path, or operation path expressions contain supplied placeholders. Local database, external HTTP, event, and already resolved static edges keep their persisted status, target, reason, and confidence. Partial substitutions remain dynamic and report the missing placeholder names.
58
58
  - `trace` and `graph` both accept repeatable `--var key=value` options. Effective substitutions are rendered in trace evidence without mutating the persisted graph. Confidence values are bounded to `[0, 1]`.
59
59
  - Repository selectors on list, trace, graph, and inspect commands narrow scope. Unknown selectors return empty machine-readable diagnostics instead of falling back to the whole workspace.
60
60
  - Helper-package dependency edges prefer exact indexed package names. Duplicate package-name candidates are persisted as ambiguous evidence rather than silently selecting one repository.
61
+ - Handler registration parsing is AST-based for common `createCombinedHandler({ handler: ... })` forms: direct arrays, arrays assembled with spreads, non-`handlers` array names, aliased class imports, default-imported arrays, named exported arrays, and safe relative re-exports. Class-level rows keep registration file/line and import evidence.
62
+ - Implementation edges require both operation compatibility and registration evidence. Same-repository registrations do not need a self-dependency edge; cross-package matches use registration or handler-package dependencies on the model package. Duplicate strong candidates are stored as ambiguous implementation edges.
63
+ - Traces prefer persisted `OPERATION_IMPLEMENTED_BY_HANDLER` edges after static or runtime remote operation resolution, then fall back to same-repository decorator matching for simple projects.
61
64
  - Repository fingerprints include source content, package name/version, dependencies and devDependencies, scripts, normalized `cds.requires` (including nested credentials), package file content, and the analyzer version. Metadata-only changes therefore trigger reindexing.
62
65
  - Index publication is designed around the last-good snapshot: failed parse or persistence attempts are recorded as diagnostics and must not be mixed with older graph facts. After indexing changes, relink before relying on graph/trace output; doctor reports stale or inconsistent stores where detectable.
66
+ - Source discovery, file reads, hashing, parsing, and publication are all inside the repository-level protected indexing flow. A failed read keeps the previous fingerprint and facts, marks the repository failed, and records a `source_read_failed` diagnostic; a later successful index clears superseded read-failure diagnostics during fact publication.
67
+ - Normal successful database commands on supported Node 24 runtimes suppress the known `node:sqlite` experimental warning so JSON stdout remains parseable and stderr-clean. Real service-flow errors still use stderr and non-zero exit codes.
68
+ - Doctor treats a `running` index run as abandoned only after 60 minutes and includes the run id/start time. Active short-lived concurrent runs are not default warnings.
69
+ - Fresh databases include foreign keys for key graph, run, and diagnostic tables. Migrated legacy stores that still lack that metadata are reported by doctor with `legacy_schema_weaker_foreign_keys`; rebuild into a fresh database if strict structural parity is required.
63
70
  - Parser warnings describe analysis completeness, while routing status describes graph behavior. A terminal DB edge can remain terminal while still exposing parser warning evidence about an unknown entity.
64
71
 
65
72
  ## 🚀 Quick Start
package/TECHNICAL-NOTE.md CHANGED
@@ -1,4 +1,4 @@
1
- # Service Flow 0.1.7 Resolution Notes
1
+ # Service Flow 0.1.8 Resolution Notes
2
2
 
3
3
  - Imported helper bindings: TypeScript imports are resolved for relative modules. When a caller assigns `const client = await connectToService()`, the analyzer follows the imported symbol to an exported helper that returns `cds.connect.to(...)` and persists caller-variable evidence plus the helper source/export chain.
4
4
  - Candidate ranking: operation-path matches start as weak candidates. A resolved operation edge requires a strong signal such as exact service path, CDS alias/destination context, or explicit dynamic variable overrides. Otherwise candidates are preserved in edge evidence as ambiguous or unresolved.
@@ -21,7 +21,22 @@
21
21
  - Repository-level fingerprints include source paths/hashes, package dependencies, and analyzer schema version. Unchanged repositories are skipped unless `--force` is used.
22
22
 
23
23
 
24
- ## 0.1.7 correctness additions
24
+ ## 0.1.8 implementation resolver model
25
+
26
+ - Facts: indexing records CDS operations, decorated handler classes/methods, package dependencies, service bindings, outbound calls, and class-level handler registration facts. Registration facts include class name, import source where known, registration file, registration line, kind, and confidence.
27
+ - Dependency evidence: helper-package linking creates repository dependency edges from package metadata. Implementation linking treats same-repository registration/handler evidence as strong without requiring a self-dependency edge, and treats registration-package or handler-package dependencies on the model package as strong cross-package evidence.
28
+ - Registration evidence: the registration parser uses the TypeScript AST for `createCombinedHandler({ handler: ... })` and resolves direct arrays, identifier arrays, spreads, imported aggregate arrays, default exports, named exports, aliases, and safe relative re-exports. Decorator-only matches are not resolved as strong implementation edges.
29
+ - Implementation edges: an operation-to-handler edge requires an exact operation path/name or method-name match plus registration evidence. One strong candidate becomes a resolved `OPERATION_IMPLEMENTED_BY_HANDLER` edge; multiple strong candidates become an ambiguous edge with candidate evidence.
30
+ - Trace traversal: trace follows remote-call graph edges to operations, applies runtime `--var` substitutions only to eligible dynamic/ambiguous/unresolved remote edges, and then prefers persisted implementation edges to enqueue the registered handler scope. Depth limits and visited-scope keys continue to bound recursion and report cycles.
31
+
32
+ ## 0.1.8 operational notes
33
+
34
+ - The known Node 24 `node:sqlite` experimental warning is filtered before loading the database driver for normal service-flow database commands. This filter targets only that runtime warning and does not convert application errors into success.
35
+ - Repository indexing protects discovery, reads, hashing, parsing, and publication in one repository-level failure boundary. Failed attempts keep last-good facts and fingerprints and produce `source_read_failed` diagnostics visible in doctor.
36
+ - Doctor reports `index_run_abandoned` only for running rows older than 60 minutes, including run id and start time.
37
+ - Fresh schema version 3 stores define foreign keys for `graph_edges`, `index_runs`, and `diagnostics`. If a migrated legacy store is structurally weaker, doctor reports `legacy_schema_weaker_foreign_keys` rather than implying parity.
38
+
39
+ ## 0.1.8 correctness additions
25
40
 
26
41
  - Runtime resolution now has an explicit eligibility gate: only remote dynamic/ambiguous/unresolved graph edges with affected placeholders are re-resolved in memory. Terminal and resolved static edges are copied through unchanged, and substitutions keep original expressions, effective values, supplied variables, and missing variables separate.
27
42
  - Operation candidate scores are clamped into `[0, 1]` before graph or trace rendering.
@@ -328,45 +328,176 @@ async function parseDecorators(repoPath, filePath) {
328
328
  }
329
329
 
330
330
  // src/parsers/handler-registration-parser.ts
331
+ import fsSync from "fs";
331
332
  import fs5 from "fs/promises";
332
333
  import path6 from "path";
333
- function lineOf2(text, idx) {
334
- return text.slice(0, idx).split("\n").length;
334
+ import ts3 from "typescript";
335
+ var MAX_EXPORT_DEPTH = 5;
336
+ function lineOf2(sourceFile, node) {
337
+ return sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1;
338
+ }
339
+ function isRelative(source) {
340
+ return source.startsWith("./") || source.startsWith("../");
341
+ }
342
+ function sourceText(node, sourceFile) {
343
+ if (ts3.isIdentifier(node) || ts3.isStringLiteral(node) || ts3.isNumericLiteral(node)) return node.text;
344
+ return node.getText(sourceFile);
345
+ }
346
+ function importSourceFor(identifier, imports) {
347
+ const evidence = imports.get(identifier);
348
+ return evidence ? `${evidence.source}#${evidence.importedName}` : void 0;
335
349
  }
336
350
  async function parseHandlerRegistrations(repoPath, filePath) {
337
- const text = await fs5.readFile(path6.join(repoPath, filePath), "utf8");
351
+ const absolutePath = path6.join(repoPath, filePath);
352
+ const text = await fs5.readFile(absolutePath, "utf8");
353
+ const sourceFile = ts3.createSourceFile(filePath, text, ts3.ScriptTarget.Latest, true, ts3.ScriptKind.TS);
354
+ const imports = collectImports(sourceFile);
355
+ const localArrays = collectLocalArrays(sourceFile, imports, /* @__PURE__ */ new Map(), repoPath, filePath);
338
356
  const out = [];
339
- const imports = /* @__PURE__ */ new Map();
340
- for (const m of text.matchAll(/import\s+\{?\s*([A-Za-z0-9_,\s]+)\s*\}?\s+from\s+['"]([^'"]+)['"]/g)) {
341
- const source = m[2];
342
- for (const name of (m[1] ?? "").split(",")) {
343
- const symbol = name.trim().split(/\s+as\s+/).pop()?.trim();
344
- if (symbol) imports.set(symbol, source);
357
+ function emitFromExpression(expression, call) {
358
+ const classes = resolveArrayExpression(expression, localArrays, imports, repoPath, filePath, /* @__PURE__ */ new Set());
359
+ for (const cls of classes) {
360
+ out.push({
361
+ className: cls.className,
362
+ importSource: cls.importSource,
363
+ registrationFile: normalizePath(filePath),
364
+ registrationLine: lineOf2(sourceFile, call),
365
+ registrationKind: "combined-handler-class",
366
+ confidence: 0.95
367
+ });
345
368
  }
346
- }
347
- for (const m of text.matchAll(
348
- /createCombinedHandler\s*\(|srv\.prepend\s*\(|cds\.serve\s*\(/g
349
- ))
350
- out.push({
351
- registrationFile: normalizePath(filePath),
352
- registrationLine: lineOf2(text, m.index ?? 0),
353
- registrationKind: m[0].startsWith("cds") ? "cds.serve" : "combined-handler",
354
- confidence: 0.8
355
- });
356
- for (const m of text.matchAll(
357
- /(?:const|export\s+const)\s+handlers\s*=\s*\[([\s\S]*?)\]/g
358
- ))
359
- for (const c of (m[1] ?? "").matchAll(/\b(\w+Handler)\b/g))
369
+ if (classes.length === 0) {
360
370
  out.push({
361
- className: c[1],
362
- importSource: imports.get(c[1]),
363
371
  registrationFile: normalizePath(filePath),
364
- registrationLine: lineOf2(text, (m.index ?? 0) + (c.index ?? 0)),
365
- registrationKind: "handler-array",
366
- confidence: 0.9
372
+ registrationLine: lineOf2(sourceFile, call),
373
+ registrationKind: "combined-handler",
374
+ confidence: 0.75
367
375
  });
376
+ }
377
+ }
378
+ function visit(node) {
379
+ if (ts3.isCallExpression(node) && isRegistrationCall(node)) {
380
+ const handlerExpr = handlerExpression(node, sourceFile);
381
+ if (handlerExpr) emitFromExpression(handlerExpr, node);
382
+ else out.push({ registrationFile: normalizePath(filePath), registrationLine: lineOf2(sourceFile, node), registrationKind: "combined-handler", confidence: 0.75 });
383
+ }
384
+ ts3.forEachChild(node, visit);
385
+ }
386
+ visit(sourceFile);
387
+ return out;
388
+ }
389
+ function isRegistrationCall(call) {
390
+ const text = call.expression.getText();
391
+ return text.endsWith("createCombinedHandler") || text.endsWith("srv.prepend") || text.endsWith("cds.serve");
392
+ }
393
+ function handlerExpression(call, sourceFile) {
394
+ for (const arg of call.arguments) {
395
+ if (!ts3.isObjectLiteralExpression(arg)) continue;
396
+ for (const prop of arg.properties) {
397
+ if (!ts3.isPropertyAssignment(prop)) continue;
398
+ if (sourceText(prop.name, sourceFile) === "handler") return prop.initializer;
399
+ }
400
+ }
401
+ return void 0;
402
+ }
403
+ function collectImports(sourceFile) {
404
+ const imports = /* @__PURE__ */ new Map();
405
+ for (const statement of sourceFile.statements) {
406
+ if (!ts3.isImportDeclaration(statement) || !ts3.isStringLiteral(statement.moduleSpecifier)) continue;
407
+ const source = statement.moduleSpecifier.text;
408
+ const clause = statement.importClause;
409
+ if (!clause) continue;
410
+ if (clause.name) imports.set(clause.name.text, { importedName: "default", source });
411
+ const named = clause.namedBindings;
412
+ if (named && ts3.isNamedImports(named)) {
413
+ for (const element of named.elements) imports.set(element.name.text, { importedName: element.propertyName?.text ?? element.name.text, source });
414
+ }
415
+ if (named && ts3.isNamespaceImport(named)) imports.set(named.name.text, { importedName: "*", source });
416
+ }
417
+ return imports;
418
+ }
419
+ function collectLocalArrays(sourceFile, imports, seed, repoPath = "", fromFile = "") {
420
+ const arrays = new Map(seed);
421
+ for (const statement of sourceFile.statements) {
422
+ if (ts3.isVariableStatement(statement)) {
423
+ for (const decl of statement.declarationList.declarations) {
424
+ if (ts3.isIdentifier(decl.name) && decl.initializer && ts3.isArrayLiteralExpression(decl.initializer)) {
425
+ arrays.set(decl.name.text, resolveArrayLiteral(decl.initializer, arrays, imports, repoPath, fromFile, /* @__PURE__ */ new Set()));
426
+ }
427
+ }
428
+ }
429
+ }
430
+ return arrays;
431
+ }
432
+ function resolveArrayExpression(expr, arrays, imports, repoPath, fromFile, seen) {
433
+ if (ts3.isArrayLiteralExpression(expr)) return resolveArrayLiteral(expr, arrays, imports, repoPath, fromFile, seen);
434
+ if (ts3.isIdentifier(expr)) {
435
+ const local = arrays.get(expr.text);
436
+ if (local) return local;
437
+ const evidence = imports.get(expr.text);
438
+ if (evidence && isRelative(evidence.source)) return resolveImportedArray(repoPath, fromFile, evidence, seen);
439
+ if (evidence) return [{ className: evidence.importedName === "default" ? expr.text : evidence.importedName, importSource: `${evidence.source}#${evidence.importedName}` }];
440
+ }
441
+ return [];
442
+ }
443
+ function resolveArrayLiteral(array, arrays, imports, repoPath, fromFile, seen) {
444
+ const out = [];
445
+ for (const element of array.elements) {
446
+ if (ts3.isSpreadElement(element)) out.push(...resolveArrayExpression(element.expression, arrays, imports, repoPath, fromFile, seen));
447
+ else if (ts3.isIdentifier(element)) out.push({ className: element.text, importSource: importSourceFor(element.text, imports) });
448
+ }
368
449
  return out;
369
450
  }
451
+ function resolveImportedArray(repoPath, fromFile, evidence, seen) {
452
+ const moduleFile = resolveRelativeModule(repoPath, fromFile, evidence.source);
453
+ if (!moduleFile) return [];
454
+ const key = `${moduleFile}:${evidence.importedName}`;
455
+ if (seen.has(key) || seen.size > MAX_EXPORT_DEPTH) return [];
456
+ seen.add(key);
457
+ const exports = readExports(repoPath, moduleFile, seen);
458
+ if (evidence.importedName === "default") return exports.defaultArray ?? [];
459
+ return exports.arrays.get(evidence.importedName) ?? exports.arrays.get(exports.aliases.get(evidence.importedName) ?? evidence.importedName) ?? [];
460
+ }
461
+ function resolveRelativeModule(repoPath, fromFile, specifier) {
462
+ const base = path6.resolve(repoPath, path6.dirname(fromFile), specifier);
463
+ for (const candidate of [base, `${base}.ts`, `${base}.js`, path6.join(base, "index.ts"), path6.join(base, "index.js")]) {
464
+ try {
465
+ const stat = fsSync.statSync(candidate);
466
+ if (stat.isFile()) return normalizePath(path6.relative(repoPath, candidate));
467
+ } catch {
468
+ }
469
+ }
470
+ return void 0;
471
+ }
472
+ function readExports(repoPath, filePath, seen) {
473
+ const absolute = path6.join(repoPath, filePath);
474
+ let text;
475
+ try {
476
+ text = fsSync.readFileSync(absolute, "utf8");
477
+ } catch {
478
+ return { arrays: /* @__PURE__ */ new Map(), aliases: /* @__PURE__ */ new Map() };
479
+ }
480
+ const sourceFile = ts3.createSourceFile(filePath, text, ts3.ScriptTarget.Latest, true, ts3.ScriptKind.TS);
481
+ const imports = collectImports(sourceFile);
482
+ const arrays = collectLocalArrays(sourceFile, imports, /* @__PURE__ */ new Map(), repoPath, filePath);
483
+ const aliases = /* @__PURE__ */ new Map();
484
+ let defaultArray;
485
+ for (const statement of sourceFile.statements) {
486
+ if (ts3.isExportAssignment(statement) && ts3.isIdentifier(statement.expression)) defaultArray = arrays.get(statement.expression.text);
487
+ if (ts3.isExportDeclaration(statement) && statement.exportClause && ts3.isNamedExports(statement.exportClause)) {
488
+ const module = statement.moduleSpecifier && ts3.isStringLiteral(statement.moduleSpecifier) ? statement.moduleSpecifier.text : void 0;
489
+ for (const element of statement.exportClause.elements) {
490
+ const local = element.propertyName?.text ?? element.name.text;
491
+ aliases.set(element.name.text, local);
492
+ if (module && isRelative(module)) {
493
+ const imported = resolveImportedArray(repoPath, filePath, { source: module, importedName: local }, seen);
494
+ if (imported.length > 0) arrays.set(element.name.text, imported);
495
+ }
496
+ }
497
+ }
498
+ }
499
+ return { arrays, defaultArray, aliases };
500
+ }
370
501
 
371
502
  // src/utils/redaction.ts
372
503
  var SENSITIVE = /authorization|cookie|token|secret|password|key|credential/i;
@@ -513,15 +644,15 @@ async function parseOutboundCalls(repoPath, filePath) {
513
644
  // src/parsers/service-binding-parser.ts
514
645
  import fs7 from "fs/promises";
515
646
  import path8 from "path";
516
- import ts3 from "typescript";
647
+ import ts4 from "typescript";
517
648
  function lineOf4(sf, node) {
518
649
  return sf.getLineAndCharacterOfPosition(node.getStart(sf)).line + 1;
519
650
  }
520
651
  function stringValue(node) {
521
652
  if (!node) return void 0;
522
- if (ts3.isStringLiteralLike(node) || ts3.isNoSubstitutionTemplateLiteral(node))
653
+ if (ts4.isStringLiteralLike(node) || ts4.isNoSubstitutionTemplateLiteral(node))
523
654
  return node.text;
524
- if (ts3.isTemplateExpression(node))
655
+ if (ts4.isTemplateExpression(node))
525
656
  return node.getText().replace(/^`|`$/g, "");
526
657
  return node.getText();
527
658
  }
@@ -530,22 +661,22 @@ function placeholders(value) {
530
661
  }
531
662
  function connectFactFromCall(call) {
532
663
  const expr = call.expression;
533
- if (!ts3.isPropertyAccessExpression(expr) || expr.name.text !== "to")
664
+ if (!ts4.isPropertyAccessExpression(expr) || expr.name.text !== "to")
534
665
  return void 0;
535
666
  const inner = expr.expression;
536
- if (!ts3.isPropertyAccessExpression(inner) || inner.name.text !== "connect" || inner.expression.getText() !== "cds")
667
+ if (!ts4.isPropertyAccessExpression(inner) || inner.name.text !== "connect" || inner.expression.getText() !== "cds")
537
668
  return void 0;
538
669
  const first = call.arguments[0];
539
670
  if (!first) return void 0;
540
671
  const second = call.arguments[1];
541
- const objectArg = ts3.isObjectLiteralExpression(first) ? first : second && ts3.isObjectLiteralExpression(second) ? second : void 0;
672
+ const objectArg = ts4.isObjectLiteralExpression(first) ? first : second && ts4.isObjectLiteralExpression(second) ? second : void 0;
542
673
  let alias;
543
674
  let aliasExpr;
544
- if (ts3.isStringLiteralLike(first) || ts3.isNoSubstitutionTemplateLiteral(first))
675
+ if (ts4.isStringLiteralLike(first) || ts4.isNoSubstitutionTemplateLiteral(first))
545
676
  alias = first.text;
546
- else if (!ts3.isObjectLiteralExpression(first))
677
+ else if (!ts4.isObjectLiteralExpression(first))
547
678
  aliasExpr = stringValue(first);
548
- if ((ts3.isStringLiteralLike(first) || ts3.isNoSubstitutionTemplateLiteral(first)) && !objectArg)
679
+ if ((ts4.isStringLiteralLike(first) || ts4.isNoSubstitutionTemplateLiteral(first)) && !objectArg)
549
680
  return { alias: first.text, isDynamic: false, placeholders: [] };
550
681
  if (!objectArg && aliasExpr)
551
682
  return {
@@ -557,13 +688,13 @@ function connectFactFromCall(call) {
557
688
  let servicePathExpr;
558
689
  function visitObject(obj) {
559
690
  for (const prop of obj.properties) {
560
- if (!ts3.isPropertyAssignment(prop)) continue;
561
- const name = ts3.isIdentifier(prop.name) || ts3.isStringLiteralLike(prop.name) ? prop.name.text : void 0;
691
+ if (!ts4.isPropertyAssignment(prop)) continue;
692
+ const name = ts4.isIdentifier(prop.name) || ts4.isStringLiteralLike(prop.name) ? prop.name.text : void 0;
562
693
  if (name === "destination")
563
694
  destinationExpr = stringValue(prop.initializer);
564
695
  if (name === "path" || name === "servicePath")
565
696
  servicePathExpr = stringValue(prop.initializer);
566
- if (ts3.isObjectLiteralExpression(prop.initializer))
697
+ if (ts4.isObjectLiteralExpression(prop.initializer))
567
698
  visitObject(prop.initializer);
568
699
  }
569
700
  }
@@ -583,8 +714,8 @@ function connectFactFromCall(call) {
583
714
  };
584
715
  }
585
716
  function unwrapCall(expr) {
586
- if (ts3.isAwaitExpression(expr)) return unwrapCall(expr.expression);
587
- if (ts3.isCallExpression(expr)) return expr;
717
+ if (ts4.isAwaitExpression(expr)) return unwrapCall(expr.expression);
718
+ if (ts4.isCallExpression(expr)) return expr;
588
719
  return void 0;
589
720
  }
590
721
  function findConnectInExpression(expr) {
@@ -596,8 +727,8 @@ function findConnectInExpression(expr) {
596
727
  let found;
597
728
  function visit(node) {
598
729
  if (found) return;
599
- if (ts3.isCallExpression(node)) found = connectFactFromCall(node);
600
- if (!found) ts3.forEachChild(node, visit);
730
+ if (ts4.isCallExpression(node)) found = connectFactFromCall(node);
731
+ if (!found) ts4.forEachChild(node, visit);
601
732
  }
602
733
  visit(expr);
603
734
  return found;
@@ -605,12 +736,12 @@ function findConnectInExpression(expr) {
605
736
  async function readSource(abs) {
606
737
  try {
607
738
  const text = await fs7.readFile(abs, "utf8");
608
- return ts3.createSourceFile(
739
+ return ts4.createSourceFile(
609
740
  abs,
610
741
  text,
611
- ts3.ScriptTarget.Latest,
742
+ ts4.ScriptTarget.Latest,
612
743
  true,
613
- ts3.ScriptKind.TS
744
+ ts4.ScriptKind.TS
614
745
  );
615
746
  } catch {
616
747
  return void 0;
@@ -639,7 +770,7 @@ async function resolveImport(repoPath, fromFile, spec) {
639
770
  async function importsFor(repoPath, filePath, sf) {
640
771
  const imports = [];
641
772
  for (const stmt of sf.statements) {
642
- if (!ts3.isImportDeclaration(stmt) || !ts3.isStringLiteralLike(stmt.moduleSpecifier))
773
+ if (!ts4.isImportDeclaration(stmt) || !ts4.isStringLiteralLike(stmt.moduleSpecifier))
643
774
  continue;
644
775
  const sourceFile = await resolveImport(
645
776
  repoPath,
@@ -655,7 +786,7 @@ async function importsFor(repoPath, filePath, sf) {
655
786
  sourceFile
656
787
  });
657
788
  const bindings = clause.namedBindings;
658
- if (bindings && ts3.isNamedImports(bindings))
789
+ if (bindings && ts4.isNamedImports(bindings))
659
790
  for (const el of bindings.elements)
660
791
  imports.push({
661
792
  localName: el.name.text,
@@ -668,17 +799,17 @@ async function importsFor(repoPath, filePath, sf) {
668
799
  function exportedLocalNames(sf) {
669
800
  const exports = /* @__PURE__ */ new Map();
670
801
  for (const stmt of sf.statements) {
671
- const direct = ts3.canHaveModifiers(stmt) ? ts3.getModifiers(stmt)?.some(
672
- (m) => m.kind === ts3.SyntaxKind.ExportKeyword
802
+ const direct = ts4.canHaveModifiers(stmt) ? ts4.getModifiers(stmt)?.some(
803
+ (m) => m.kind === ts4.SyntaxKind.ExportKeyword
673
804
  ) ?? false : false;
674
- if (direct && ts3.isFunctionDeclaration(stmt) && stmt.name)
805
+ if (direct && ts4.isFunctionDeclaration(stmt) && stmt.name)
675
806
  exports.set(stmt.name.text, stmt.name.text);
676
- if (direct && ts3.isVariableStatement(stmt)) {
807
+ if (direct && ts4.isVariableStatement(stmt)) {
677
808
  for (const decl of stmt.declarationList.declarations)
678
- if (ts3.isIdentifier(decl.name)) exports.set(decl.name.text, decl.name.text);
809
+ if (ts4.isIdentifier(decl.name)) exports.set(decl.name.text, decl.name.text);
679
810
  }
680
- if (!ts3.isExportDeclaration(stmt) || !stmt.exportClause) continue;
681
- if (!ts3.isNamedExports(stmt.exportClause)) continue;
811
+ if (!ts4.isExportDeclaration(stmt) || !stmt.exportClause) continue;
812
+ if (!ts4.isNamedExports(stmt.exportClause)) continue;
682
813
  for (const el of stmt.exportClause.elements)
683
814
  exports.set(el.name.text, el.propertyName?.text ?? el.name.text);
684
815
  }
@@ -692,18 +823,18 @@ async function helperBindings(repoPath, filePath) {
692
823
  const exportedLocals = exportedLocalNames(sf);
693
824
  const factsByLocal = /* @__PURE__ */ new Map();
694
825
  for (const stmt of sf.statements) {
695
- if (ts3.isFunctionDeclaration(stmt) && stmt.name) {
826
+ if (ts4.isFunctionDeclaration(stmt) && stmt.name) {
696
827
  let fact;
697
828
  stmt.forEachChild(function visit(node) {
698
- if (!fact && ts3.isReturnStatement(node) && node.expression)
829
+ if (!fact && ts4.isReturnStatement(node) && node.expression)
699
830
  fact = findConnectInExpression(node.expression);
700
- if (!fact) ts3.forEachChild(node, visit);
831
+ if (!fact) ts4.forEachChild(node, visit);
701
832
  });
702
833
  if (fact) factsByLocal.set(stmt.name.text, { ...fact, sourceLine: lineOf4(sf, stmt) });
703
834
  }
704
- if (ts3.isVariableStatement(stmt))
835
+ if (ts4.isVariableStatement(stmt))
705
836
  for (const decl of stmt.declarationList.declarations) {
706
- if (!ts3.isIdentifier(decl.name) || !decl.initializer) continue;
837
+ if (!ts4.isIdentifier(decl.name) || !decl.initializer) continue;
707
838
  const fact = findConnectInExpression(decl.initializer);
708
839
  if (fact)
709
840
  factsByLocal.set(decl.name.text, {
@@ -744,7 +875,7 @@ async function parseServiceBindings(repoPath, filePath) {
744
875
  return helper ? { imp, helper } : void 0;
745
876
  }
746
877
  async function recordVariable(decl) {
747
- if (!ts3.isIdentifier(decl.name) || !decl.initializer) return;
878
+ if (!ts4.isIdentifier(decl.name) || !decl.initializer) return;
748
879
  const call = unwrapCall(decl.initializer);
749
880
  if (!call) return;
750
881
  const direct = connectFactFromCall(call);
@@ -755,7 +886,7 @@ async function parseServiceBindings(repoPath, filePath) {
755
886
  sourceFile: normalizePath(filePath),
756
887
  sourceLine: lineOf4(sourceFileAst, decl)
757
888
  });
758
- else if (ts3.isIdentifier(call.expression)) {
889
+ else if (ts4.isIdentifier(call.expression)) {
759
890
  const resolved = await importedHelper(call.expression.text);
760
891
  if (resolved)
761
892
  out.push({
@@ -782,14 +913,14 @@ async function parseServiceBindings(repoPath, filePath) {
782
913
  }
783
914
  }
784
915
  function recordDestructuredClassHelper(decl) {
785
- if (!ts3.isObjectBindingPattern(decl.name) || !decl.initializer) return;
916
+ if (!ts4.isObjectBindingPattern(decl.name) || !decl.initializer) return;
786
917
  const call = unwrapCall(decl.initializer);
787
- if (!call || !ts3.isPropertyAccessExpression(call.expression)) return;
918
+ if (!call || !ts4.isPropertyAccessExpression(call.expression)) return;
788
919
  const target = call.expression;
789
- if (target.expression.kind !== ts3.SyntaxKind.ThisKeyword) return;
920
+ if (target.expression.kind !== ts4.SyntaxKind.ThisKeyword) return;
790
921
  for (const el of decl.name.elements) {
791
- if (!ts3.isIdentifier(el.name)) continue;
792
- const propertyName = el.propertyName && ts3.isIdentifier(el.propertyName) ? el.propertyName.text : el.name.text;
922
+ if (!ts4.isIdentifier(el.name)) continue;
923
+ const propertyName = el.propertyName && ts4.isIdentifier(el.propertyName) ? el.propertyName.text : el.name.text;
793
924
  const helper = classHelpers.find(
794
925
  (h) => h.helperName === target.name.text && h.propertyName === propertyName
795
926
  );
@@ -815,8 +946,8 @@ async function parseServiceBindings(repoPath, filePath) {
815
946
  }
816
947
  const declarations = [];
817
948
  function collectDeclarations(node) {
818
- if (ts3.isVariableDeclaration(node)) declarations.push(node);
819
- ts3.forEachChild(node, collectDeclarations);
949
+ if (ts4.isVariableDeclaration(node)) declarations.push(node);
950
+ ts4.forEachChild(node, collectDeclarations);
820
951
  }
821
952
  collectDeclarations(sourceFileAst);
822
953
  for (const decl of declarations) {
@@ -828,16 +959,16 @@ async function parseServiceBindings(repoPath, filePath) {
828
959
  function collectClassHelpers(sf) {
829
960
  const helpers = [];
830
961
  for (const stmt of sf.statements) {
831
- if (!ts3.isClassDeclaration(stmt) || !stmt.name) continue;
962
+ if (!ts4.isClassDeclaration(stmt) || !stmt.name) continue;
832
963
  for (const member of stmt.members) {
833
964
  let visit2 = function(node) {
834
- if (ts3.isVariableDeclaration(node) && ts3.isIdentifier(node.name) && node.initializer) {
965
+ if (ts4.isVariableDeclaration(node) && ts4.isIdentifier(node.name) && node.initializer) {
835
966
  const fact = findConnectInExpression(node.initializer);
836
967
  if (fact) bindings.set(node.name.text, fact);
837
968
  }
838
- if (ts3.isReturnStatement(node) && node.expression && ts3.isObjectLiteralExpression(node.expression)) {
969
+ if (ts4.isReturnStatement(node) && node.expression && ts4.isObjectLiteralExpression(node.expression)) {
839
970
  for (const prop of node.expression.properties) {
840
- if (ts3.isShorthandPropertyAssignment(prop)) {
971
+ if (ts4.isShorthandPropertyAssignment(prop)) {
841
972
  const fact = bindings.get(prop.name.text);
842
973
  if (fact)
843
974
  helpers.push({
@@ -849,8 +980,8 @@ function collectClassHelpers(sf) {
849
980
  sourceLine: lineOf4(sf, prop)
850
981
  });
851
982
  }
852
- if (ts3.isPropertyAssignment(prop) && ts3.isIdentifier(prop.initializer)) {
853
- const propertyName = ts3.isIdentifier(prop.name) || ts3.isStringLiteralLike(prop.name) ? prop.name.text : void 0;
983
+ if (ts4.isPropertyAssignment(prop) && ts4.isIdentifier(prop.initializer)) {
984
+ const propertyName = ts4.isIdentifier(prop.name) || ts4.isStringLiteralLike(prop.name) ? prop.name.text : void 0;
854
985
  const fact = propertyName ? bindings.get(prop.initializer.text) : void 0;
855
986
  if (propertyName && fact)
856
987
  helpers.push({
@@ -864,15 +995,15 @@ function collectClassHelpers(sf) {
864
995
  }
865
996
  }
866
997
  }
867
- ts3.forEachChild(node, visit2);
998
+ ts4.forEachChild(node, visit2);
868
999
  };
869
1000
  var visit = visit2;
870
- if (!ts3.isPropertyDeclaration(member) || !member.initializer) continue;
871
- if (!ts3.isIdentifier(member.name)) continue;
1001
+ if (!ts4.isPropertyDeclaration(member) || !member.initializer) continue;
1002
+ if (!ts4.isIdentifier(member.name)) continue;
872
1003
  const className = stmt.name.text;
873
1004
  const helperName = member.name.text;
874
1005
  const initializer = member.initializer;
875
- if (!ts3.isArrowFunction(initializer) && !ts3.isFunctionExpression(initializer))
1006
+ if (!ts4.isArrowFunction(initializer) && !ts4.isFunctionExpression(initializer))
876
1007
  continue;
877
1008
  const bindings = /* @__PURE__ */ new Map();
878
1009
  visit2(initializer);
@@ -1110,7 +1241,30 @@ function linkImplementations(db, workspaceId, generation) {
1110
1241
  return { edgeCount, resolvedCount, ambiguousCount };
1111
1242
  }
1112
1243
  function implementationCandidates(db, workspaceId, operation) {
1113
- return db.prepare(`SELECT DISTINCT hm.id methodId,hc.id classId,hc.class_name className,hc.source_file sourceFile,hc.source_line sourceLine,hr.repo_id applicationRepoId,handlerRepo.name handlerRepo,appRepo.name applicationRepo FROM handler_methods hm JOIN handler_classes hc ON hc.id=hm.handler_class_id JOIN repositories handlerRepo ON handlerRepo.id=hc.repo_id JOIN handler_registrations hr ON hr.class_name=hc.class_name JOIN repositories appRepo ON appRepo.id=hr.repo_id JOIN graph_edges modelDep ON modelDep.edge_type='REPO_IMPORTS_HELPER_PACKAGE' AND modelDep.status='resolved' AND modelDep.from_kind='repo' AND modelDep.from_id=CAST(appRepo.id AS TEXT) AND modelDep.to_id=CAST(? AS TEXT) JOIN graph_edges handlerDep ON handlerDep.edge_type='REPO_IMPORTS_HELPER_PACKAGE' AND handlerDep.status='resolved' AND handlerDep.from_kind='repo' AND handlerDep.from_id=CAST(appRepo.id AS TEXT) AND handlerDep.to_id=CAST(handlerRepo.id AS TEXT) WHERE appRepo.workspace_id=? AND (hm.decorator_value=? OR hm.decorator_value=? OR hm.method_name=?)`).all(operation.modelRepoId, workspaceId, normalizedOperation(String(operation.operationPath ?? "")), operation.operationName, operation.operationName);
1244
+ const rows2 = db.prepare(`SELECT DISTINCT
1245
+ hm.id methodId,
1246
+ hc.id classId,
1247
+ hc.class_name className,
1248
+ hc.source_file sourceFile,
1249
+ hc.source_line sourceLine,
1250
+ hr.repo_id applicationRepoId,
1251
+ hr.registration_file registrationFile,
1252
+ hr.registration_line registrationLine,
1253
+ hr.import_source importSource,
1254
+ handlerRepo.id handlerRepoId,
1255
+ handlerRepo.name handlerRepo,
1256
+ appRepo.name applicationRepo,
1257
+ CASE WHEN appRepo.id=handlerRepo.id THEN 1 ELSE 0 END sameRepoRegistration,
1258
+ CASE WHEN EXISTS (SELECT 1 FROM graph_edges dep WHERE dep.edge_type='REPO_IMPORTS_HELPER_PACKAGE' AND dep.status='resolved' AND dep.from_kind='repo' AND dep.from_id=CAST(appRepo.id AS TEXT) AND dep.to_id=CAST(? AS TEXT)) THEN 1 ELSE 0 END appDependsOnModel,
1259
+ CASE WHEN EXISTS (SELECT 1 FROM graph_edges dep WHERE dep.edge_type='REPO_IMPORTS_HELPER_PACKAGE' AND dep.status='resolved' AND dep.from_kind='repo' AND dep.from_id=CAST(handlerRepo.id AS TEXT) AND dep.to_id=CAST(? AS TEXT)) THEN 1 ELSE 0 END handlerDependsOnModel
1260
+ FROM handler_methods hm
1261
+ JOIN handler_classes hc ON hc.id=hm.handler_class_id
1262
+ JOIN repositories handlerRepo ON handlerRepo.id=hc.repo_id
1263
+ JOIN handler_registrations hr ON (hr.handler_class_id=hc.id OR (hr.class_name=hc.class_name AND (hr.repo_id=hc.repo_id OR hr.import_source IS NOT NULL)))
1264
+ JOIN repositories appRepo ON appRepo.id=hr.repo_id
1265
+ WHERE appRepo.workspace_id=?
1266
+ AND (hm.decorator_value=? OR hm.decorator_value=? OR hm.method_name=?)`).all(operation.modelRepoId, operation.modelRepoId, workspaceId, normalizedOperation(String(operation.operationPath ?? "")), operation.operationName, operation.operationName);
1267
+ return rows2.filter((row) => Boolean(Number(row.sameRepoRegistration)) || Boolean(Number(row.appDependsOnModel)) || Boolean(Number(row.handlerDependsOnModel)));
1114
1268
  }
1115
1269
  function normalizedOperation(value) {
1116
1270
  return value.startsWith("/") ? value.slice(1) : value;
@@ -1394,4 +1548,4 @@ export {
1394
1548
  linkWorkspace,
1395
1549
  trace
1396
1550
  };
1397
- //# sourceMappingURL=chunk-YFT57U54.js.map
1551
+ //# sourceMappingURL=chunk-NBAO2R2H.js.map