@saptools/service-flow 0.1.51 → 0.1.53

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +16 -14
  3. package/TECHNICAL-NOTE.md +18 -6
  4. package/dist/{chunk-YZJKE5UX.js → chunk-LFH7C46B.js} +4290 -1261
  5. package/dist/chunk-LFH7C46B.js.map +1 -0
  6. package/dist/cli.js +435 -515
  7. package/dist/cli.js.map +1 -1
  8. package/dist/index.d.ts +45 -7
  9. package/dist/index.js +1 -1
  10. package/package.json +1 -1
  11. package/src/cli/000-clean.ts +82 -0
  12. package/src/cli/001-doctor-projection.ts +140 -0
  13. package/src/cli/doctor.ts +20 -3
  14. package/src/cli.ts +75 -57
  15. package/src/db/connection.ts +1 -1
  16. package/src/db/migrations.ts +5 -3
  17. package/src/db/repositories.ts +130 -24
  18. package/src/db/schema.ts +7 -2
  19. package/src/indexer/repository-indexer.ts +57 -29
  20. package/src/indexer/workspace-indexer.ts +84 -6
  21. package/src/linker/000-implementation-candidates.ts +641 -0
  22. package/src/linker/001-implementation-evidence-projection.ts +119 -0
  23. package/src/linker/002-call-evidence.ts +226 -0
  24. package/src/linker/cross-repo-linker.ts +27 -441
  25. package/src/linker/dynamic-edge-resolver.ts +35 -0
  26. package/src/linker/external-http-target.ts +24 -3
  27. package/src/linker/helper-package-linker.ts +18 -2
  28. package/src/linker/service-resolver.ts +45 -2
  29. package/src/output/doctor-output.ts +13 -4
  30. package/src/output/table-output.ts +33 -5
  31. package/src/parsers/cds-parser.ts +8 -2
  32. package/src/parsers/decorator-parser.ts +382 -48
  33. package/src/parsers/handler-registration-parser.ts +38 -21
  34. package/src/parsers/imported-wrapper-parser.ts +18 -5
  35. package/src/parsers/outbound-call-parser.ts +25 -8
  36. package/src/parsers/package-json-parser.ts +36 -11
  37. package/src/parsers/service-binding-parser-helpers.ts +8 -1
  38. package/src/parsers/service-binding-parser.ts +6 -3
  39. package/src/parsers/symbol-parser.ts +13 -3
  40. package/src/parsers/ts-project.ts +54 -0
  41. package/src/trace/000-dynamic-target-types.ts +96 -0
  42. package/src/trace/001-dynamic-identity.ts +308 -0
  43. package/src/trace/002-trace-diagnostics.ts +54 -0
  44. package/src/trace/003-dynamic-references.ts +283 -0
  45. package/src/trace/004-dynamic-candidate-sources.ts +155 -0
  46. package/src/trace/005-implementation-selection.ts +187 -0
  47. package/src/trace/006-contextual-projection.ts +30 -0
  48. package/src/trace/007-implementation-start-diagnostic.ts +61 -0
  49. package/src/trace/dynamic-targets.ts +582 -306
  50. package/src/trace/evidence.ts +331 -46
  51. package/src/trace/implementation-hints.ts +148 -8
  52. package/src/trace/selectors.ts +551 -3
  53. package/src/trace/trace-engine.ts +129 -135
  54. package/src/types.ts +27 -1
  55. package/src/utils/000-bounded-projection.ts +161 -0
  56. package/dist/chunk-YZJKE5UX.js.map +0 -1
package/dist/cli.js CHANGED
@@ -1,10 +1,24 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  classifyODataPathIntent,
4
+ clearRepoFacts,
4
5
  containsSupportedOutboundCall,
5
6
  discoverRepositories,
7
+ getWorkspace,
8
+ handlerMethodIsExecutable,
6
9
  implementationHintSuggestions,
10
+ insertBindings,
11
+ insertCalls,
12
+ insertExecutableSymbols,
13
+ insertHandler,
14
+ insertRegistrations,
15
+ insertRequires,
16
+ insertService,
17
+ insertSymbolCalls,
7
18
  linkWorkspace,
19
+ listRepositories,
20
+ loadPackageJsonSnapshot,
21
+ loadRepositorySourceContext,
8
22
  normalizeODataOperationInvocationPath,
9
23
  normalizePath,
10
24
  parseCdsFile,
@@ -15,13 +29,17 @@ import {
15
29
  parsePackageJson,
16
30
  parseServiceBindings,
17
31
  parseVars,
18
- trace
19
- } from "./chunk-YZJKE5UX.js";
32
+ projectBounded,
33
+ reposByName,
34
+ selectorRepoAmbiguousDiagnostic,
35
+ stableProjectionValue,
36
+ trace,
37
+ upsertRepository,
38
+ upsertWorkspace
39
+ } from "./chunk-LFH7C46B.js";
20
40
 
21
41
  // src/cli.ts
22
42
  import { Command } from "commander";
23
- import path6 from "path";
24
- import fs6 from "fs/promises";
25
43
 
26
44
  // src/config/defaults.ts
27
45
  var DEFAULT_IGNORES = [
@@ -93,7 +111,7 @@ import fs2 from "fs";
93
111
  import path2 from "path";
94
112
 
95
113
  // src/db/schema.ts
96
- var schemaSql = `
114
+ var schemaTablesSql = `
97
115
  CREATE TABLE IF NOT EXISTS workspaces (id INTEGER PRIMARY KEY, root_path TEXT UNIQUE NOT NULL, db_path TEXT NOT NULL, created_at TEXT NOT NULL, updated_at TEXT NOT NULL);
98
116
  CREATE TABLE IF NOT EXISTS repositories (id INTEGER PRIMARY KEY, workspace_id INTEGER NOT NULL, name TEXT NOT NULL, absolute_path TEXT NOT NULL, relative_path TEXT NOT NULL, package_name TEXT, package_version TEXT, dependencies_json TEXT DEFAULT '{}', kind TEXT NOT NULL, is_git_repo INTEGER NOT NULL, last_indexed_at TEXT, index_status TEXT DEFAULT 'pending', error_count INTEGER DEFAULT 0, fingerprint TEXT, fact_generation INTEGER NOT NULL DEFAULT 0, graph_generation INTEGER NOT NULL DEFAULT 0, graph_stale_reason TEXT, graph_stale_at TEXT, fact_analyzer_version TEXT, UNIQUE(workspace_id, absolute_path), FOREIGN KEY(workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE);
99
117
  CREATE TABLE IF NOT EXISTS files (id INTEGER PRIMARY KEY, repo_id INTEGER NOT NULL, relative_path TEXT NOT NULL, extension TEXT NOT NULL, sha256 TEXT NOT NULL, size_bytes INTEGER NOT NULL, last_indexed_at TEXT NOT NULL, UNIQUE(repo_id, relative_path), FOREIGN KEY(repo_id) REFERENCES repositories(id) ON DELETE CASCADE);
@@ -108,8 +126,10 @@ CREATE TABLE IF NOT EXISTS service_bindings (id INTEGER PRIMARY KEY, repo_id INT
108
126
  CREATE TABLE IF NOT EXISTS outbound_calls (id INTEGER PRIMARY KEY, repo_id INTEGER NOT NULL, source_symbol_id INTEGER, call_type TEXT NOT NULL, service_binding_id INTEGER, method TEXT, operation_path_expr TEXT, query_entity TEXT, event_name_expr TEXT, payload_summary TEXT, source_file TEXT NOT NULL, source_line INTEGER NOT NULL, confidence REAL NOT NULL, unresolved_reason TEXT, local_service_name TEXT, local_service_lookup TEXT, alias_chain_json TEXT, evidence_json TEXT, external_target_kind TEXT, external_target_id TEXT, external_target_label TEXT, external_target_dynamic INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(repo_id) REFERENCES repositories(id) ON DELETE CASCADE, FOREIGN KEY(source_symbol_id) REFERENCES symbols(id) ON DELETE SET NULL, FOREIGN KEY(service_binding_id) REFERENCES service_bindings(id) ON DELETE SET NULL);
109
127
  CREATE TABLE IF NOT EXISTS symbol_calls (id INTEGER PRIMARY KEY, repo_id INTEGER NOT NULL, caller_symbol_id INTEGER NOT NULL, callee_symbol_id INTEGER, callee_expression TEXT NOT NULL, import_source TEXT, source_file TEXT NOT NULL, source_line INTEGER NOT NULL, status TEXT NOT NULL, confidence REAL NOT NULL, evidence_json TEXT NOT NULL, unresolved_reason TEXT, FOREIGN KEY(repo_id) REFERENCES repositories(id) ON DELETE CASCADE, FOREIGN KEY(caller_symbol_id) REFERENCES symbols(id) ON DELETE CASCADE, FOREIGN KEY(callee_symbol_id) REFERENCES symbols(id) ON DELETE SET NULL);
110
128
  CREATE TABLE IF NOT EXISTS graph_edges (id INTEGER PRIMARY KEY, workspace_id INTEGER NOT NULL, edge_type TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'unresolved', from_kind TEXT NOT NULL, from_id TEXT NOT NULL, to_kind TEXT NOT NULL, to_id TEXT NOT NULL, confidence REAL NOT NULL, evidence_json TEXT NOT NULL, is_dynamic INTEGER NOT NULL, unresolved_reason TEXT, generation INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE);
111
- CREATE TABLE IF NOT EXISTS index_runs (id INTEGER PRIMARY KEY, workspace_id INTEGER NOT NULL, started_at TEXT NOT NULL, finished_at TEXT, status TEXT NOT NULL, repo_count INTEGER NOT NULL, file_count INTEGER NOT NULL, diagnostic_count INTEGER NOT NULL, error_message TEXT, FOREIGN KEY(workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE);
129
+ CREATE TABLE IF NOT EXISTS index_runs (id INTEGER PRIMARY KEY, workspace_id INTEGER NOT NULL, started_at TEXT NOT NULL, finished_at TEXT, status TEXT NOT NULL, repo_count INTEGER NOT NULL, file_count INTEGER NOT NULL, diagnostic_count INTEGER NOT NULL, error_message TEXT, owner_pid INTEGER, FOREIGN KEY(workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE);
112
130
  CREATE TABLE IF NOT EXISTS diagnostics (id INTEGER PRIMARY KEY, repo_id INTEGER, file_id INTEGER, severity TEXT NOT NULL, code TEXT NOT NULL, message TEXT NOT NULL, source_file TEXT, source_line INTEGER, FOREIGN KEY(repo_id) REFERENCES repositories(id) ON DELETE CASCADE, FOREIGN KEY(file_id) REFERENCES files(id) ON DELETE SET NULL);
131
+ `;
132
+ var schemaIndexesSql = `
113
133
  CREATE INDEX IF NOT EXISTS idx_repo_name ON repositories(name);
114
134
  CREATE INDEX IF NOT EXISTS idx_service_path ON cds_services(service_path);
115
135
  CREATE INDEX IF NOT EXISTS idx_extension_import ON cds_services(extension_module_specifier, extension_imported_symbol, is_extend);
@@ -119,9 +139,11 @@ CREATE INDEX IF NOT EXISTS idx_calls_repo ON outbound_calls(repo_id, call_type);
119
139
  CREATE INDEX IF NOT EXISTS idx_symbol_calls_caller ON symbol_calls(repo_id, caller_symbol_id);
120
140
  CREATE VIRTUAL TABLE IF NOT EXISTS search_index USING fts5(kind, name, path, repo);
121
141
  `;
142
+ var schemaSql = `${schemaTablesSql}
143
+ ${schemaIndexesSql}`;
122
144
 
123
145
  // src/db/migrations.ts
124
- var CURRENT_SCHEMA_VERSION = 10;
146
+ var CURRENT_SCHEMA_VERSION = 11;
125
147
  var columns = {
126
148
  handler_methods: [
127
149
  { name: "decorator_resolution_json", ddl: "ALTER TABLE handler_methods ADD COLUMN decorator_resolution_json TEXT NOT NULL DEFAULT '{}'" }
@@ -177,7 +199,8 @@ var columns = {
177
199
  { name: "external_target_dynamic", ddl: "ALTER TABLE outbound_calls ADD COLUMN external_target_dynamic INTEGER NOT NULL DEFAULT 0" }
178
200
  ],
179
201
  index_runs: [
180
- { name: "error_message", ddl: "ALTER TABLE index_runs ADD COLUMN error_message TEXT" }
202
+ { name: "error_message", ddl: "ALTER TABLE index_runs ADD COLUMN error_message TEXT" },
203
+ { name: "owner_pid", ddl: "ALTER TABLE index_runs ADD COLUMN owner_pid INTEGER" }
181
204
  ]
182
205
  };
183
206
  function hasColumn(db, table, column) {
@@ -202,8 +225,9 @@ function migrate(db) {
202
225
  db.transaction(() => {
203
226
  const version = userVersion(db);
204
227
  if (version > CURRENT_SCHEMA_VERSION) throw new Error(`Unsupported future service-flow schema version ${version}`);
205
- db.exec(schemaSql);
228
+ db.exec(schemaTablesSql);
206
229
  addMissingColumns(db);
230
+ db.exec(schemaIndexesSql);
207
231
  normalizeLegacyStatus(db);
208
232
  const violations = db.pragma("foreign_key_check");
209
233
  if (violations.length > 0) throw new Error("SQLite foreign_key_check failed during migration");
@@ -214,7 +238,7 @@ function migrate(db) {
214
238
  // package.json
215
239
  var package_default = {
216
240
  name: "@saptools/service-flow",
217
- version: "0.1.51",
241
+ version: "0.1.53",
218
242
  description: "Trace SAP CAP service-to-service flows across multi-repository workspaces with runtime-aware graph resolution",
219
243
  type: "module",
220
244
  publishConfig: {
@@ -356,8 +380,8 @@ function openDatabase(dbPath, options = {}) {
356
380
  },
357
381
  transaction(fn) {
358
382
  if (inTransaction) return fn();
359
- inTransaction = true;
360
383
  native.exec("BEGIN IMMEDIATE");
384
+ inTransaction = true;
361
385
  try {
362
386
  const result = fn();
363
387
  native.exec("COMMIT");
@@ -385,410 +409,6 @@ function openReadOnlyDatabase(dbPath) {
385
409
  return openDatabase(dbPath, { readonly: true, migrate: false });
386
410
  }
387
411
 
388
- // src/db/repositories.ts
389
- function upsertWorkspace(db, rootPath, dbPath) {
390
- const now = (/* @__PURE__ */ new Date()).toISOString();
391
- db.prepare(
392
- "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"
393
- ).run(rootPath, dbPath, now, now);
394
- return Number(
395
- db.prepare("SELECT id FROM workspaces WHERE root_path=?").get(rootPath)?.id
396
- );
397
- }
398
- function getWorkspace(db, rootPath) {
399
- return db.prepare("SELECT * FROM workspaces WHERE root_path=?").get(rootPath);
400
- }
401
- function upsertRepository(db, workspaceId, r) {
402
- db.prepare(
403
- `INSERT INTO repositories(workspace_id,name,absolute_path,relative_path,package_name,package_version,dependencies_json,kind,is_git_repo) VALUES(?,?,?,?,?,?,?,?,?) ON CONFLICT(workspace_id,absolute_path) DO UPDATE SET name=excluded.name,relative_path=excluded.relative_path,package_name=excluded.package_name,package_version=excluded.package_version,dependencies_json=excluded.dependencies_json,kind=excluded.kind`
404
- ).run(
405
- workspaceId,
406
- r.name,
407
- r.absolutePath,
408
- r.relativePath,
409
- r.packageName,
410
- r.packageVersion,
411
- JSON.stringify(r.dependencies ?? {}),
412
- r.kind ?? "unknown",
413
- r.isGitRepo ? 1 : 0
414
- );
415
- return Number(
416
- db.prepare(
417
- "SELECT id FROM repositories WHERE workspace_id=? AND absolute_path=?"
418
- ).get(workspaceId, r.absolutePath)?.id
419
- );
420
- }
421
- function listRepositories(db) {
422
- return db.prepare("SELECT * FROM repositories ORDER BY name").all();
423
- }
424
- function repoByName(db, name) {
425
- return db.prepare("SELECT * FROM repositories WHERE name=? OR package_name=?").get(name, name);
426
- }
427
- function clearRepoFacts(db, repoId) {
428
- for (const t of [
429
- "cds_requires",
430
- "cds_services",
431
- "handler_classes",
432
- "outbound_calls",
433
- "symbol_calls",
434
- "handler_registrations",
435
- "service_bindings",
436
- "symbols",
437
- "diagnostics",
438
- "files"
439
- ])
440
- db.prepare(`DELETE FROM ${t} WHERE repo_id=?`).run(repoId);
441
- db.prepare("DELETE FROM search_index WHERE repo=?").run(String(repoId));
442
- }
443
- function insertRequires(db, repoId, rows) {
444
- const stmt = db.prepare(
445
- "INSERT INTO cds_requires(repo_id,alias,kind,model,destination,service_path,request_timeout,raw_json) VALUES(?,?,?,?,?,?,?,?)"
446
- );
447
- for (const r of rows)
448
- stmt.run(
449
- repoId,
450
- r.alias,
451
- r.kind,
452
- r.model,
453
- r.destination,
454
- r.servicePath,
455
- r.requestTimeout,
456
- r.rawJson
457
- );
458
- }
459
- function insertService(db, repoId, s) {
460
- const id = Number(
461
- db.prepare(
462
- "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"
463
- ).get(
464
- repoId,
465
- s.namespace,
466
- s.serviceName,
467
- s.qualifiedName,
468
- s.servicePath,
469
- s.isExtend ? 1 : 0,
470
- s.sourceFile,
471
- s.sourceLine,
472
- s.extension?.localReference,
473
- s.extension?.importedSymbol,
474
- s.extension?.localAlias,
475
- s.extension?.moduleSpecifier,
476
- s.extension?.importKind
477
- )?.id
478
- );
479
- const stmt = db.prepare(
480
- "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(?,?,?,?,?,?,?,?,?,?)"
481
- );
482
- db.prepare(
483
- "INSERT INTO search_index(kind,name,path,repo) VALUES(?,?,?,?)"
484
- ).run("service", s.qualifiedName, s.servicePath, String(repoId));
485
- for (const o of s.operations)
486
- stmt.run(
487
- id,
488
- o.operationType,
489
- o.operationName,
490
- o.operationPath,
491
- o.paramsJson,
492
- o.returnType,
493
- o.sourceFile,
494
- o.sourceLine,
495
- o.provenance ?? "direct",
496
- o.baseOperationId ?? null
497
- );
498
- const search = db.prepare(
499
- "INSERT INTO search_index(kind,name,path,repo) VALUES(?,?,?,?)"
500
- );
501
- for (const o of s.operations)
502
- search.run("operation", o.operationName, o.operationPath, String(repoId));
503
- return id;
504
- }
505
- function insertHandler(db, repoId, h) {
506
- const sid = Number(
507
- db.prepare(
508
- "INSERT INTO symbols(repo_id,kind,name,qualified_name,exported,start_line,end_line) VALUES(?,?,?,?,?,?,?) RETURNING id"
509
- ).get(
510
- repoId,
511
- "class",
512
- h.className,
513
- h.className,
514
- 1,
515
- h.sourceLine,
516
- h.sourceLine
517
- )?.id
518
- );
519
- const hid = Number(
520
- db.prepare(
521
- "INSERT INTO handler_classes(repo_id,symbol_id,class_name,source_file,source_line) VALUES(?,?,?,?,?) RETURNING id"
522
- ).get(repoId, sid, h.className, h.sourceFile, h.sourceLine)?.id
523
- );
524
- const stmt = db.prepare(
525
- "INSERT INTO handler_methods(handler_class_id,method_name,decorator_kind,decorator_value,decorator_raw_expression,decorator_resolution_json,source_file,source_line) VALUES(?,?,?,?,?,?,?,?)"
526
- );
527
- for (const m of h.methods)
528
- stmt.run(
529
- hid,
530
- m.methodName,
531
- m.decoratorKind,
532
- m.decoratorValue,
533
- m.decoratorRawExpression,
534
- JSON.stringify(m.decoratorResolution),
535
- m.sourceFile,
536
- m.sourceLine
537
- );
538
- return hid;
539
- }
540
- function insertRegistrations(db, repoId, rows) {
541
- const stmt = db.prepare(
542
- "INSERT INTO handler_registrations(repo_id,handler_class_id,class_name,import_source,registration_file,registration_line,registration_kind,confidence) VALUES(?,?,?,?,?,?,?,?)"
543
- );
544
- for (const r of rows) {
545
- const handlerClass = r.className ? db.prepare(
546
- "SELECT id FROM handler_classes WHERE repo_id=? AND class_name=? ORDER BY id"
547
- ).all(repoId, r.className) : [];
548
- stmt.run(
549
- repoId,
550
- handlerClass.length === 1 ? handlerClass[0]?.id : null,
551
- r.className,
552
- r.importSource,
553
- r.registrationFile,
554
- r.registrationLine,
555
- r.registrationKind,
556
- r.confidence
557
- );
558
- }
559
- }
560
- function insertBindings(db, repoId, rows) {
561
- const stmt = db.prepare(
562
- "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,helper_chain_json) VALUES(?,(SELECT id FROM symbols WHERE repo_id=? AND source_file=? AND start_line<=? AND end_line>=? ORDER BY (end_line-start_line),id LIMIT 1),?,?,?,?,?,?,?,?,?,?)"
563
- );
564
- for (const r of rows)
565
- stmt.run(
566
- repoId,
567
- repoId,
568
- r.sourceFile,
569
- r.sourceLine,
570
- r.sourceLine,
571
- r.variableName,
572
- r.alias,
573
- r.aliasExpr,
574
- r.destinationExpr,
575
- r.servicePathExpr,
576
- r.isDynamic ? 1 : 0,
577
- JSON.stringify(r.placeholders),
578
- r.sourceFile,
579
- r.sourceLine,
580
- r.helperChain ? JSON.stringify(r.helperChain) : null
581
- );
582
- }
583
- function insertExecutableSymbols(db, repoId, rows) {
584
- 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=?),?,?,?,?,?,?,?,?,?,?,?)");
585
- 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);
586
- }
587
- function insertSymbolCalls(db, repoId, rows) {
588
- const callerStmt = db.prepare("SELECT id FROM symbols WHERE repo_id=? AND source_file=? AND qualified_name=? ORDER BY id LIMIT 1");
589
- const insertStmt = db.prepare("INSERT INTO symbol_calls(repo_id,caller_symbol_id,callee_symbol_id,callee_expression,import_source,source_file,source_line,status,confidence,evidence_json,unresolved_reason) VALUES(?,?,?,?,?,?,?,?,?,?,?)");
590
- for (const r of rows) {
591
- const caller = callerStmt.get(repoId, r.sourceFile, r.callerQualifiedName);
592
- const target = resolveSymbolCallTarget(db, repoId, r);
593
- insertStmt.run(repoId, caller?.id, target.id, r.calleeExpression, r.importSource, r.sourceFile, r.sourceLine, target.status, 0.8, JSON.stringify({ ...r.evidence, candidateStrategy: target.strategy, candidateCount: target.candidateCount }), target.reason);
594
- }
595
- }
596
- function isRelativeImportedSymbolCall(r) {
597
- return Boolean(r.importSource?.startsWith("."));
598
- }
599
- function resolveSymbolCallTarget(db, repoId, r) {
600
- const evidence = r.evidence;
601
- const localRows = db.prepare("SELECT id FROM symbols WHERE repo_id=? AND source_file=? AND (name=? OR qualified_name=?) ORDER BY id").all(repoId, r.sourceFile, r.calleeLocalName, r.calleeLocalName);
602
- if (localRows.length === 1) return { id: localRows[0]?.id ?? null, status: "resolved", reason: null, strategy: "same_file_exact", candidateCount: 1 };
603
- if (localRows.length > 1) return { id: null, status: "ambiguous", reason: "Multiple same-file symbol targets matched exactly", strategy: "same_file_exact", candidateCount: localRows.length };
604
- if (evidence.relation === "class_instance_method" && isRelativeImportedSymbolCall(r)) {
605
- const classRows = db.prepare("SELECT id FROM symbols WHERE repo_id=? AND source_file<>? AND qualified_name=? ORDER BY id").all(repoId, r.sourceFile, r.calleeLocalName);
606
- if (classRows.length === 1) return { id: classRows[0]?.id ?? null, status: "resolved", reason: null, strategy: "relative_import_class_instance_method", candidateCount: 1 };
607
- if (classRows.length > 1) return { id: null, status: "ambiguous", reason: "Multiple relative class instance method targets matched exactly", strategy: "relative_import_class_instance_method", candidateCount: classRows.length };
608
- }
609
- const rows = db.prepare("SELECT id,kind,evidence_json evidenceJson FROM symbols WHERE repo_id=? AND source_file<>? AND exported=1 AND (exported_name=? OR name=? OR qualified_name=?) ORDER BY id").all(repoId, r.sourceFile, r.calleeLocalName, r.calleeLocalName, r.calleeLocalName);
610
- if (evidence.relation === "relative_import_proxy_member" && rows.length > 1) {
611
- const objectMapRows = rows.filter((row) => String(row.evidenceJson ?? "").includes("exported_object_shorthand") || String(row.evidenceJson ?? "").includes("exported_object_literal"));
612
- if (objectMapRows.length > 0) {
613
- const concrete = rows.find((row) => row.kind !== "object_alias") ?? objectMapRows[0];
614
- return { id: concrete?.id ?? null, status: "resolved", reason: null, strategy: "proxy_member_exported_object_map", candidateCount: rows.length };
615
- }
616
- return { id: null, status: "ambiguous", reason: "Proxy member target requires explicit factory/module/type evidence; global member name is ambiguous", strategy: "proxy_member_no_global_name_fallback", candidateCount: rows.length };
617
- }
618
- if (rows.length === 1) return { id: rows[0]?.id ?? null, status: "resolved", reason: null, strategy: evidence.relation === "relative_import_proxy_member" ? "proxy_member_unique_exported_candidate" : "relative_import_exported_exact", candidateCount: 1 };
619
- if (rows.length > 1) return { id: null, status: "ambiguous", reason: "Multiple exported symbol targets matched exactly", strategy: "exported_exact", candidateCount: rows.length };
620
- return { id: null, status: "unresolved", reason: "No local symbol target matched exactly", strategy: evidence.relation === "relative_import_proxy_member" ? "proxy_member_no_global_name_fallback" : "exact_symbol_match", candidateCount: 0 };
621
- }
622
- function insertCalls(db, repoId, rows) {
623
- const stmt = db.prepare(
624
- "INSERT INTO outbound_calls(repo_id,source_symbol_id,call_type,method,operation_path_expr,query_entity,event_name_expr,payload_summary,source_file,source_line,confidence,unresolved_reason,local_service_name,local_service_lookup,alias_chain_json,evidence_json,external_target_kind,external_target_id,external_target_label,external_target_dynamic,service_binding_id) VALUES(?,COALESCE((SELECT id FROM symbols WHERE repo_id=? AND source_file=? AND qualified_name=? ORDER BY id LIMIT 1),(SELECT id FROM symbols WHERE repo_id=? AND source_file=? AND start_line<=? AND end_line>=? ORDER BY (end_line-start_line),id LIMIT 1)),?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)"
625
- );
626
- for (const r of rows) {
627
- const binding = resolvePersistedBinding(db, repoId, r);
628
- const evidence = {
629
- ...r.evidence ?? {},
630
- serviceBindingResolution: binding.evidence
631
- };
632
- stmt.run(
633
- repoId,
634
- repoId,
635
- r.sourceFile,
636
- r.sourceSymbolQualifiedName,
637
- repoId,
638
- r.sourceFile,
639
- r.sourceLine,
640
- r.sourceLine,
641
- r.callType,
642
- r.method,
643
- r.operationPathExpr,
644
- r.queryEntity,
645
- r.eventNameExpr,
646
- r.payloadSummary,
647
- r.sourceFile,
648
- r.sourceLine,
649
- r.confidence,
650
- r.unresolvedReason ?? binding.unresolvedReason,
651
- r.localServiceName,
652
- r.localServiceLookup,
653
- r.aliasChain ? JSON.stringify(r.aliasChain) : null,
654
- JSON.stringify(evidence),
655
- r.externalTarget?.kind ?? null,
656
- r.externalTarget?.stableId ?? null,
657
- r.externalTarget?.label ?? null,
658
- r.externalTarget?.dynamic ? 1 : 0,
659
- binding.bindingId
660
- );
661
- }
662
- }
663
- function resolvePersistedBinding(db, repoId, call) {
664
- if (!call.serviceVariableName)
665
- return { bindingId: null, evidence: { status: "not_applicable", candidateCount: 0 } };
666
- const candidates = bindingCandidates(db, repoId, call);
667
- const prior = candidates.filter((candidate) => candidate.sourceLine <= call.sourceLine);
668
- const families = new Set(prior.map(bindingSignature));
669
- if (prior.length > 0 && families.size === 1) {
670
- const selected = prior.at(-1);
671
- return {
672
- bindingId: selected?.id ?? null,
673
- evidence: bindingEvidence("selected", prior, selected)
674
- };
675
- }
676
- if (prior.length > 1) {
677
- return {
678
- bindingId: null,
679
- unresolvedReason: "ambiguous_service_binding_candidates",
680
- evidence: bindingEvidence("ambiguous", prior)
681
- };
682
- }
683
- if (candidates.length > 0) {
684
- return {
685
- bindingId: null,
686
- unresolvedReason: "service_binding_declared_after_call",
687
- evidence: bindingEvidence("rejected_future_binding", candidates)
688
- };
689
- }
690
- return {
691
- bindingId: null,
692
- evidence: bindingEvidence("unrecoverable", [])
693
- };
694
- }
695
- function bindingCandidates(db, repoId, call) {
696
- const ownerId = callSymbolId(db, repoId, call);
697
- const rows = db.prepare(`
698
- SELECT id,symbol_id symbolId,variable_name variableName,alias,alias_expr aliasExpr,
699
- destination_expr destinationExpr,service_path_expr servicePathExpr,
700
- source_file sourceFile,source_line sourceLine,helper_chain_json helperChainJson
701
- FROM service_bindings
702
- WHERE repo_id=? AND variable_name=? AND source_file=?
703
- AND (? IS NULL OR symbol_id IS NULL OR symbol_id=?)
704
- ORDER BY source_line,id
705
- `).all(
706
- repoId,
707
- call.serviceVariableName,
708
- call.sourceFile,
709
- ownerId,
710
- ownerId
711
- );
712
- return rows.flatMap((row) => {
713
- if (typeof row.id !== "number" || typeof row.variableName !== "string" || typeof row.sourceFile !== "string" || typeof row.sourceLine !== "number")
714
- return [];
715
- return [{
716
- id: row.id,
717
- symbolId: nullableNumber(row.symbolId),
718
- variableName: row.variableName,
719
- alias: nullableString(row.alias),
720
- aliasExpr: nullableString(row.aliasExpr),
721
- destinationExpr: nullableString(row.destinationExpr),
722
- servicePathExpr: nullableString(row.servicePathExpr),
723
- sourceFile: row.sourceFile,
724
- sourceLine: row.sourceLine,
725
- helperChainJson: nullableString(row.helperChainJson)
726
- }];
727
- });
728
- }
729
- function nullableString(value) {
730
- return value === null || typeof value === "string" ? value : void 0;
731
- }
732
- function nullableNumber(value) {
733
- return value === null || typeof value === "number" ? value : void 0;
734
- }
735
- function callSymbolId(db, repoId, call) {
736
- const row = db.prepare(`
737
- SELECT id FROM symbols
738
- WHERE repo_id=? AND source_file=?
739
- AND ((? IS NOT NULL AND qualified_name=?)
740
- OR (start_line<=? AND end_line>=?))
741
- ORDER BY CASE WHEN qualified_name=? THEN 0 ELSE 1 END,
742
- (end_line-start_line),id
743
- LIMIT 1
744
- `).get(
745
- repoId,
746
- call.sourceFile,
747
- call.sourceSymbolQualifiedName,
748
- call.sourceSymbolQualifiedName,
749
- call.sourceLine,
750
- call.sourceLine,
751
- call.sourceSymbolQualifiedName
752
- );
753
- return typeof row?.id === "number" ? row.id : void 0;
754
- }
755
- function bindingEvidence(status, candidates, selected) {
756
- return {
757
- status,
758
- candidateCount: candidates.length,
759
- selectedBindingId: selected?.id,
760
- sourceOrderRule: "binding_source_line_must_not_follow_call",
761
- candidates: candidates.map((candidate) => ({
762
- bindingId: candidate.id,
763
- symbolId: candidate.symbolId,
764
- variableName: candidate.variableName,
765
- alias: candidate.alias,
766
- aliasExpr: candidate.aliasExpr,
767
- destinationExpr: candidate.destinationExpr,
768
- servicePathExpr: candidate.servicePathExpr,
769
- sourceFile: candidate.sourceFile,
770
- sourceLine: candidate.sourceLine,
771
- helperChain: parseBindingChain(candidate.helperChainJson)
772
- }))
773
- };
774
- }
775
- function bindingSignature(candidate) {
776
- return JSON.stringify([
777
- candidate.alias,
778
- candidate.aliasExpr,
779
- candidate.destinationExpr,
780
- candidate.servicePathExpr
781
- ]);
782
- }
783
- function parseBindingChain(value) {
784
- if (!value) return void 0;
785
- try {
786
- return JSON.parse(value);
787
- } catch {
788
- return void 0;
789
- }
790
- }
791
-
792
412
  // src/discovery/classify-repository.ts
793
413
  import fs3 from "fs/promises";
794
414
  import path3 from "path";
@@ -1021,9 +641,16 @@ function parameterBindings(params) {
1021
641
  return [];
1022
642
  });
1023
643
  }
1024
- async function parseExecutableSymbols(repoPath, filePath) {
1025
- const text = await fs4.readFile(path4.join(repoPath, filePath), "utf8");
1026
- const source = ts.createSourceFile(filePath, text, ts.ScriptTarget.Latest, true, filePath.endsWith(".ts") ? ts.ScriptKind.TS : ts.ScriptKind.JS);
644
+ async function parseExecutableSymbols(repoPath, filePath, context) {
645
+ const snapshot = context?.get(filePath);
646
+ const text = snapshot?.text ?? await fs4.readFile(path4.join(repoPath, filePath), "utf8");
647
+ const source = snapshot?.sourceFile() ?? ts.createSourceFile(
648
+ filePath,
649
+ text,
650
+ ts.ScriptTarget.Latest,
651
+ true,
652
+ filePath.endsWith(".ts") ? ts.ScriptKind.TS : ts.ScriptKind.JS
653
+ );
1027
654
  const sourceFile = normalizePath(filePath);
1028
655
  const symbols = [];
1029
656
  const calls = [];
@@ -1199,27 +826,38 @@ async function parseExecutableSymbols(repoPath, filePath) {
1199
826
  // src/utils/hashing.ts
1200
827
  import { createHash } from "crypto";
1201
828
  import { readFile } from "fs/promises";
1202
- async function sha256File(filePath) {
1203
- return createHash("sha256").update(await readFile(filePath)).digest("hex");
1204
- }
1205
829
  function sha256Text(text) {
1206
830
  return createHash("sha256").update(text).digest("hex");
1207
831
  }
1208
832
 
1209
833
  // src/indexer/repository-indexer.ts
1210
- async function prepareRepositoryIndex(repo, force) {
834
+ async function prepareRepositoryIndex(repo, force, instrumentation) {
1211
835
  const sourceFiles = await findSourceFiles(repo.absolute_path);
1212
- const packageFacts = await parsePackageJson(repo.absolute_path);
1213
- const fingerprint = await repositoryFingerprint(repo.absolute_path, sourceFiles, packageFacts);
836
+ const packageSnapshot = await loadPackageJsonSnapshot(repo.absolute_path, {
837
+ strict: true,
838
+ allowMissing: repo.package_name === null
839
+ });
840
+ const packageFacts = packageSnapshot.facts;
841
+ const sources = await loadRepositorySourceContext(
842
+ repo.absolute_path,
843
+ sourceFiles,
844
+ instrumentation
845
+ );
846
+ const fingerprint = repositoryFingerprint(
847
+ sources,
848
+ packageFacts,
849
+ packageSnapshot.rawText
850
+ );
1214
851
  if (!force && repo.fingerprint === fingerprint) return { repo, fileCount: 0, diagnosticCount: 0, skipped: true };
852
+ const parsed = await parseAllSourceFacts(repo.absolute_path, sources);
1215
853
  return {
1216
854
  repo,
1217
855
  packageFacts,
1218
856
  fingerprint,
1219
857
  kind: await classifyRepository(repo.absolute_path, packageFacts),
1220
- parsed: await parseAllSourceFacts(repo.absolute_path, sourceFiles),
858
+ parsed,
1221
859
  fileCount: sourceFiles.length,
1222
- diagnosticCount: 0,
860
+ diagnosticCount: parsed.handlers.filter((handler) => handler.hasHandlerDecorator && (handler.methods.length === 0 || handler.methods.some((method) => !handlerMethodIsExecutable(method)))).length,
1223
861
  skipped: false
1224
862
  };
1225
863
  }
@@ -1248,21 +886,20 @@ function recordIndexFailure(db, repoId, error) {
1248
886
  db.prepare("DELETE FROM diagnostics WHERE repo_id=? AND code IN ('index_failed_snapshot_preserved','source_read_failed')").run(repoId);
1249
887
  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}`);
1250
888
  }
1251
- async function parseAllSourceFacts(root, files) {
889
+ async function parseAllSourceFacts(root, sources) {
1252
890
  const facts = { services: [], handlers: [], registrations: [], bindings: [], calls: [], symbols: [], symbolCalls: [], fileRecords: [] };
1253
- for (const file of files) {
1254
- const abs = path5.join(root, file);
1255
- const stat = await fs5.stat(abs);
1256
- facts.fileRecords.push({ relativePath: normalizePath(file), extension: path5.extname(file), sha256: await sha256File(abs), sizeBytes: stat.size });
1257
- if (file.endsWith(".cds")) facts.services.push(...await parseCdsFile(root, file));
891
+ for (const snapshot of sources.entries()) {
892
+ const file = snapshot.filePath;
893
+ facts.fileRecords.push({ relativePath: normalizePath(file), extension: path5.extname(file), sha256: sha256Text(snapshot.text), sizeBytes: snapshot.sizeBytes });
894
+ if (file.endsWith(".cds")) facts.services.push(...await parseCdsFile(root, file, sources));
1258
895
  if (/\.[jt]s$/.test(file)) {
1259
- facts.handlers.push(...await parseDecorators(root, file));
1260
- facts.registrations.push(...await parseHandlerRegistrations(root, file));
1261
- facts.bindings.push(...await parseServiceBindings(root, file));
1262
- const symbolFacts = await parseExecutableSymbols(root, file);
896
+ facts.handlers.push(...await parseDecorators(root, file, sources));
897
+ facts.registrations.push(...await parseHandlerRegistrations(root, file, sources));
898
+ facts.bindings.push(...await parseServiceBindings(root, file, sources));
899
+ const symbolFacts = await parseExecutableSymbols(root, file, sources);
1263
900
  facts.symbols.push(...symbolFacts.symbols);
1264
901
  facts.symbolCalls.push(...symbolFacts.calls);
1265
- facts.calls.push(...await parseOutboundCalls(root, file));
902
+ facts.calls.push(...await parseOutboundCalls(root, file, sources));
1266
903
  }
1267
904
  }
1268
905
  return facts;
@@ -1270,7 +907,7 @@ async function parseAllSourceFacts(root, files) {
1270
907
  async function findSourceFiles(root) {
1271
908
  const out = [];
1272
909
  async function walk(dir, prefix = "") {
1273
- const entries = await fs5.readdir(dir, { withFileTypes: true }).catch(() => []);
910
+ const entries = await fs5.readdir(dir, { withFileTypes: true });
1274
911
  for (const e of entries) {
1275
912
  const rel = prefix ? `${prefix}/${e.name}` : e.name;
1276
913
  if (e.isDirectory()) {
@@ -1286,8 +923,7 @@ function isDefaultTestFile(relativeFile) {
1286
923
  if (parts.some((part) => ["test", "tests", "__tests__"].includes(part))) return true;
1287
924
  return /\.(test|spec)\.[jt]s$/.test(parts.at(-1) ?? "");
1288
925
  }
1289
- async function repositoryFingerprint(root, files, facts) {
1290
- const packageJson = await fs5.readFile(path5.join(root, "package.json"), "utf8").catch(() => "");
926
+ function repositoryFingerprint(sources, facts, packageJsonText) {
1291
927
  const normalizedFacts = {
1292
928
  analyzerVersion: ANALYZER_VERSION,
1293
929
  packageName: facts.packageName,
@@ -1296,13 +932,11 @@ async function repositoryFingerprint(root, files, facts) {
1296
932
  cdsRequires: [...facts.cdsRequires].sort((a, b) => a.alias.localeCompare(b.alias)),
1297
933
  scripts: Object.fromEntries(Object.entries(facts.scripts).sort()),
1298
934
  includeTests: false,
1299
- packageJsonHash: sha256Text(packageJson)
935
+ packageJsonHash: sha256Text(packageJsonText)
1300
936
  };
1301
937
  const entries = [`facts:${JSON.stringify(normalizedFacts)}`];
1302
- for (const file of files) {
1303
- const content = await fs5.readFile(path5.join(root, file), "utf8");
1304
- entries.push(`${file}:${sha256Text(content)}`);
1305
- }
938
+ for (const snapshot of sources.entries())
939
+ entries.push(`${snapshot.filePath}:${sha256Text(snapshot.text)}`);
1306
940
  return sha256Text(entries.join("\n"));
1307
941
  }
1308
942
 
@@ -1406,10 +1040,67 @@ function packageNameFromSpecifier(specifier) {
1406
1040
  }
1407
1041
 
1408
1042
  // src/indexer/workspace-indexer.ts
1043
+ var LEGACY_OWNER_RECOVERY_MS = 60 * 60 * 1e3;
1044
+ function ownerPid(value) {
1045
+ return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : void 0;
1046
+ }
1047
+ function processIsAlive(pid) {
1048
+ try {
1049
+ process.kill(pid, 0);
1050
+ return true;
1051
+ } catch (error) {
1052
+ const ownerIsMissing = typeof error === "object" && error !== null && "code" in error && error.code === "ESRCH";
1053
+ return !ownerIsMissing;
1054
+ }
1055
+ }
1056
+ function isRecoverableRun(row, now) {
1057
+ const pid = ownerPid(row.ownerPid);
1058
+ if (pid !== void 0) return !processIsAlive(pid);
1059
+ if (typeof row.startedAt !== "string") return false;
1060
+ const startedAt = Date.parse(row.startedAt);
1061
+ return Number.isFinite(startedAt) && now - startedAt >= LEGACY_OWNER_RECOVERY_MS;
1062
+ }
1063
+ function recoveredOwnerMessage(row) {
1064
+ const pid = ownerPid(row.ownerPid);
1065
+ return pid === void 0 ? "Recovered stale legacy index writer without owner process metadata." : `Recovered stale index writer because owner process ${pid} is no longer running.`;
1066
+ }
1067
+ function claimIndexRun(db, workspaceId, repoCount) {
1068
+ try {
1069
+ return db.transaction(() => {
1070
+ const now = Date.now();
1071
+ const rows = db.prepare("SELECT id,workspace_id workspaceId,owner_pid ownerPid,started_at startedAt FROM index_runs WHERE status='running' ORDER BY id").all();
1072
+ const active = rows.find((row) => !isRecoverableRun(row, now));
1073
+ if (active) {
1074
+ const pid = ownerPid(active.ownerPid);
1075
+ const owner = pid === void 0 ? "an unknown owner" : `process ${pid}`;
1076
+ throw new Error(`index_writer_active: this database is already being indexed for workspace ${String(active.workspaceId ?? "unknown")} by ${owner}; wait for that writer to finish.`);
1077
+ }
1078
+ const finish = db.prepare(
1079
+ "UPDATE index_runs SET finished_at=?,status='failed',error_message=? WHERE id=?"
1080
+ );
1081
+ for (const row of rows)
1082
+ finish.run(new Date(now).toISOString(), recoveredOwnerMessage(row), row.id);
1083
+ const inserted = db.prepare("INSERT INTO index_runs(workspace_id,started_at,status,repo_count,file_count,diagnostic_count,owner_pid) VALUES(?,?,?,?,?,?,?) RETURNING id").get(workspaceId, new Date(now).toISOString(), "running", repoCount, 0, 0, process.pid);
1084
+ const runId = Number(inserted?.id);
1085
+ if (!Number.isSafeInteger(runId)) throw new Error("index_writer_claim_failed: SQLite did not return an index run identifier.");
1086
+ return runId;
1087
+ });
1088
+ } catch (error) {
1089
+ if (/\b(?:locked|busy)\b/i.test(errorMessage(error)))
1090
+ throw new Error(
1091
+ "index_writer_coordination_failed: SQLite remained busy beyond the bounded writer-claim interval; wait for the active publication to finish.",
1092
+ { cause: error }
1093
+ );
1094
+ throw error;
1095
+ }
1096
+ }
1409
1097
  async function indexWorkspace(db, workspaceId, options) {
1410
- const started = (/* @__PURE__ */ new Date()).toISOString();
1411
- const repos = options.repo ? [repoByName(db, options.repo)].filter((r) => r !== void 0) : listRepositories(db);
1412
- const runId = Number(db.prepare("INSERT INTO index_runs(workspace_id,started_at,status,repo_count,file_count,diagnostic_count) VALUES(?,?,?,?,?,?) RETURNING id").get(workspaceId, started, "running", repos.length, 0, 0)?.id);
1098
+ const repos = options.repo ? reposByName(db, options.repo, workspaceId) : listRepositories(db, workspaceId);
1099
+ if (options.repo && repos.length === 0)
1100
+ throw new Error(`selector_repo_not_found: no indexed repository matched ${options.repo}.`);
1101
+ if (options.repo && repos.length > 1)
1102
+ throw new Error(`selector_repo_ambiguous: repository selector ${options.repo} matched ${repos.length} repositories; use a unique repository name.`);
1103
+ const runId = claimIndexRun(db, workspaceId, repos.length);
1413
1104
  let fileCount = 0;
1414
1105
  let diagnosticCount = 0;
1415
1106
  let skippedCount = 0;
@@ -1431,23 +1122,141 @@ async function indexWorkspace(db, workspaceId, options) {
1431
1122
  }
1432
1123
  if (options.injectDerivedMaterializationFailure) throw new Error("Injected derived materialization failure");
1433
1124
  materializeCdsExtensionOperations(db, workspaceId);
1434
- db.prepare("UPDATE index_runs SET finished_at=?, status=?, file_count=?, diagnostic_count=? WHERE id=?").run((/* @__PURE__ */ new Date()).toISOString(), diagnosticCount ? "failed" : "success", fileCount, diagnosticCount, runId);
1125
+ db.prepare("UPDATE index_runs SET finished_at=?, status='success', file_count=?, diagnostic_count=? WHERE id=?").run((/* @__PURE__ */ new Date()).toISOString(), fileCount, diagnosticCount, runId);
1435
1126
  });
1436
1127
  return { repoCount: repos.length, indexedCount: repos.length - skippedCount, skippedCount, fileCount, diagnosticCount };
1437
1128
  } catch (error) {
1438
- if (activeRepoId && preparedRows.length < repos.length) recordIndexFailure(db, activeRepoId, error);
1439
1129
  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);
1130
+ if (activeRepoId && preparedRows.length < repos.length) recordIndexFailure(db, activeRepoId, error);
1440
1131
  throw error;
1441
1132
  }
1442
1133
  }
1443
1134
 
1135
+ // src/cli/001-doctor-projection.ts
1136
+ var boundedDoctorArrayKeys = /* @__PURE__ */ new Set([
1137
+ "candidates",
1138
+ "candidateScores",
1139
+ "candidateFamilies",
1140
+ "candidateEvidence",
1141
+ "candidatePaths",
1142
+ "candidateRawPaths",
1143
+ "candidateNormalizedOperationPaths",
1144
+ "normalizedCandidateOperations",
1145
+ "candidateLiterals",
1146
+ "bindingCandidates",
1147
+ "bindingAlternatives",
1148
+ "registrations",
1149
+ "implementationHintSuggestions",
1150
+ "selectableImplementationRepositories",
1151
+ "matchedHints",
1152
+ "candidateSuggestions",
1153
+ "dynamicTargetCandidates",
1154
+ "dynamicTargetCandidateSuggestions",
1155
+ "rejectedCandidates",
1156
+ "suggestedVarSets",
1157
+ "copyableExamples",
1158
+ "examples",
1159
+ "expandedExamples",
1160
+ "selectorSuggestions",
1161
+ "serviceSuggestions",
1162
+ "repositories"
1163
+ ]);
1164
+ function boundDoctorDiagnostics(diagnostics) {
1165
+ return diagnostics.map(boundDoctorDiagnostic);
1166
+ }
1167
+ function boundDoctorDiagnostic(diagnostic) {
1168
+ const bounded = boundDoctorValue(diagnostic);
1169
+ return isDiagnostic(bounded) ? bounded : {};
1170
+ }
1171
+ function boundDoctorValue(value) {
1172
+ if (Array.isArray(value)) return value.map(boundDoctorValue);
1173
+ if (!isDiagnostic(value)) return value;
1174
+ const input = value;
1175
+ const output = {};
1176
+ for (const [key, child] of Object.entries(input)) {
1177
+ if (!Array.isArray(child) || !boundedDoctorArrayKeys.has(key)) {
1178
+ output[key] = boundDoctorValue(child);
1179
+ continue;
1180
+ }
1181
+ const projection = projectBounded(child.map(boundDoctorValue), compareDiagnostic);
1182
+ output[key] = projection.items;
1183
+ addProjectionMetadata(output, input, key, projection);
1184
+ }
1185
+ return output;
1186
+ }
1187
+ function isDiagnostic(value) {
1188
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
1189
+ }
1190
+ function addProjectionMetadata(output, input, key, projection) {
1191
+ const names = projectionNames(key);
1192
+ const total = Math.max(
1193
+ numericValue(input[names.total]),
1194
+ projection.totalCount,
1195
+ siblingCollectionCount(input, key)
1196
+ );
1197
+ output[names.total] = total;
1198
+ output[names.shown] = projection.shownCount;
1199
+ output[names.omitted] = Math.max(0, total - projection.shownCount);
1200
+ }
1201
+ function siblingCollectionCount(input, key) {
1202
+ if (key !== "examples" || !Array.isArray(input.expandedExamples)) return 0;
1203
+ return input.expandedExamples.length;
1204
+ }
1205
+ function projectionNames(key) {
1206
+ const stem = projectionStem(key);
1207
+ return {
1208
+ total: `${stem}Count`,
1209
+ shown: `shown${upperFirst(stem)}Count`,
1210
+ omitted: `omitted${upperFirst(stem)}Count`
1211
+ };
1212
+ }
1213
+ function projectionStem(key) {
1214
+ const stems = {
1215
+ candidates: "candidate",
1216
+ candidateScores: "candidateScore",
1217
+ candidateFamilies: "candidateFamily",
1218
+ candidateEvidence: "candidateEvidence",
1219
+ candidatePaths: "candidatePath",
1220
+ candidateRawPaths: "candidateRawPath",
1221
+ candidateNormalizedOperationPaths: "candidateNormalizedOperationPath",
1222
+ normalizedCandidateOperations: "normalizedCandidateOperation",
1223
+ candidateLiterals: "candidateLiteral",
1224
+ bindingCandidates: "bindingCandidate",
1225
+ bindingAlternatives: "bindingAlternative",
1226
+ registrations: "registration",
1227
+ implementationHintSuggestions: "implementationHintSuggestion",
1228
+ selectableImplementationRepositories: "selectableImplementationRepository",
1229
+ matchedHints: "matchedHint",
1230
+ candidateSuggestions: "candidateSuggestion",
1231
+ dynamicTargetCandidates: "dynamicTargetCandidate",
1232
+ dynamicTargetCandidateSuggestions: "dynamicTargetCandidateSuggestion",
1233
+ rejectedCandidates: "rejectedCandidate",
1234
+ suggestedVarSets: "suggestedVarSet",
1235
+ copyableExamples: "copyableExample",
1236
+ examples: "example",
1237
+ expandedExamples: "expandedExample",
1238
+ selectorSuggestions: "selectorSuggestion",
1239
+ serviceSuggestions: "serviceSuggestion"
1240
+ };
1241
+ return stems[key] ?? "repository";
1242
+ }
1243
+ function compareDiagnostic(left, right) {
1244
+ return stableProjectionValue(left).localeCompare(stableProjectionValue(right));
1245
+ }
1246
+ function numericValue(value) {
1247
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
1248
+ }
1249
+ function upperFirst(value) {
1250
+ return value ? `${value[0]?.toUpperCase() ?? ""}${value.slice(1)}` : value;
1251
+ }
1252
+
1444
1253
  // src/cli/doctor.ts
1445
1254
  function linkUpgradeWarnings(db) {
1446
1255
  return [...schemaDriftDiagnostics(db, true), ...analyzerVersionDiagnostics(db, true)].filter((item) => ["schema_legacy_columns_present", "external_target_columns_missing_data", "reindex_required_after_upgrade", "reindex_required_after_analyzer_upgrade"].includes(String(item.code)));
1447
1256
  }
1448
1257
  function doctorDiagnostics(db, strict, options = {}) {
1449
1258
  const diagnostics = db.prepare("SELECT severity,code,message,source_file sourceFile,source_line sourceLine FROM diagnostics ORDER BY id").all();
1450
- return [
1259
+ return boundDoctorDiagnostics([
1451
1260
  ...diagnostics,
1452
1261
  ...healthDiagnostics(db, strict),
1453
1262
  ...remoteTargetWithoutImplementationDiagnostics(db, strict, Boolean(options.detail)),
@@ -1455,7 +1264,7 @@ function doctorDiagnostics(db, strict, options = {}) {
1455
1264
  ...schemaDriftDiagnostics(db, strict),
1456
1265
  ...analyzerVersionDiagnostics(db, strict),
1457
1266
  ...parserQualityDiagnostics(db, strict, options)
1458
- ];
1267
+ ]);
1459
1268
  }
1460
1269
  function healthDiagnostics(db, strict) {
1461
1270
  return db.prepare(
@@ -1667,6 +1476,10 @@ function addImplementationCategory(grouped, row) {
1667
1476
  const current = grouped.get(key) ?? { category, baseOperation, reason, candidateFamily: family, count: 0, servicePaths: [], examples: [] };
1668
1477
  const hintSuggestions = implementationSuggestions(evidence);
1669
1478
  const candidates = asRecords(evidence.candidates);
1479
+ const candidateCount2 = Math.max(
1480
+ numericValue2(evidence.candidateCount),
1481
+ candidates.length
1482
+ );
1670
1483
  current.count += 1;
1671
1484
  current.servicePaths.push(String(row.servicePath ?? evidence.servicePath ?? ""));
1672
1485
  current.examples.push({
@@ -1674,7 +1487,15 @@ function addImplementationCategory(grouped, row) {
1674
1487
  operation: row.operationName,
1675
1488
  status: row.status,
1676
1489
  reason: row.unresolvedReason,
1677
- candidateCount: candidates.length,
1490
+ candidateCount: candidateCount2,
1491
+ shownCandidateCount: Math.min(
1492
+ candidateCount2,
1493
+ numericValue2(evidence.shownCandidateCount) || candidates.length
1494
+ ),
1495
+ omittedCandidateCount: Math.max(
1496
+ 0,
1497
+ candidateCount2 - (numericValue2(evidence.shownCandidateCount) || candidates.length)
1498
+ ),
1678
1499
  candidateEvidence: candidates.slice(0, 3),
1679
1500
  implementationHintSuggestions: hintSuggestions
1680
1501
  });
@@ -2012,6 +1833,9 @@ function remoteActionNoBindingQuality(db) {
2012
1833
  function candidateCount(value) {
2013
1834
  return Number(parseObject(value).candidateCount ?? 0);
2014
1835
  }
1836
+ function numericValue2(value) {
1837
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
1838
+ }
2015
1839
  function parseObject(value) {
2016
1840
  if (value && typeof value === "object" && !Array.isArray(value)) return value;
2017
1841
  try {
@@ -2050,7 +1874,25 @@ function renderTraceTable(result) {
2050
1874
  }
2051
1875
  function diagnosticLines(diagnostic) {
2052
1876
  const first = `${String(diagnostic.severity ?? "info")} ${String(diagnostic.code ?? "diagnostic")} ${String(diagnostic.message ?? "")}`;
2053
- return [first, ...hintLines(diagnostic).map((hint) => ` ${hint}`)];
1877
+ const details = diagnosticDetailLines(diagnostic);
1878
+ return [first, ...[...details, ...hintLines(diagnostic)].map((hint) => ` ${hint}`)];
1879
+ }
1880
+ function diagnosticDetailLines(diagnostic) {
1881
+ const lines = [];
1882
+ if (diagnostic.sourceFile || diagnostic.sourceLine)
1883
+ lines.push(`at ${String(diagnostic.sourceFile ?? "")}:${String(diagnostic.sourceLine ?? "")}`);
1884
+ const unsupported = stringList(diagnostic.unsupportedDecoratorNames);
1885
+ const observed = stringList(diagnostic.observedDecoratorNames);
1886
+ if (unsupported.length > 0)
1887
+ lines.push(`unsupported decorators: ${unsupported.join(", ")}`);
1888
+ else if (observed.length > 0)
1889
+ lines.push(`observed decorators: ${observed.join(", ")}`);
1890
+ if (typeof diagnostic.remediation === "string")
1891
+ lines.push(`hint: ${diagnostic.remediation}`);
1892
+ return lines;
1893
+ }
1894
+ function stringList(value) {
1895
+ return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
2054
1896
  }
2055
1897
  function hintLines(evidence) {
2056
1898
  const dynamicLines = dynamicHintLines(evidence);
@@ -2059,8 +1901,10 @@ function hintLines(evidence) {
2059
1901
  const hints = suggestions.flatMap((item) => isRecord(item) && typeof item.cli === "string" ? [item.cli] : []);
2060
1902
  const unique = [...new Set(hints)];
2061
1903
  const shown = unique.slice(0, 3).map((hint) => `try ${hint}`);
2062
- if (unique.length > shown.length)
2063
- shown.push(`... ${unique.length - shown.length} more hint(s) available in JSON`);
1904
+ const omitted = numberValue(evidence.omittedImplementationHintSuggestionCount);
1905
+ const remaining = Math.max(0, unique.length - shown.length) + omitted;
1906
+ if (remaining > 0)
1907
+ shown.push(`... ${remaining} additional hint(s) omitted; use a scoped --implementation-hint`);
2064
1908
  return [...dynamicLines, ...shown];
2065
1909
  }
2066
1910
  function dynamicHintLines(evidence) {
@@ -2069,9 +1913,12 @@ function dynamicHintLines(evidence) {
2069
1913
  if (count === 0) return [];
2070
1914
  const shown = numberValue(exploration.shownCandidateCount);
2071
1915
  const omitted = numberValue(exploration.omittedCandidateCount);
2072
- const lines = [`candidates: ${shown} shown, ${omitted} omitted`];
1916
+ const rejected = numberValue(exploration.rejectedCandidateCount);
1917
+ const lines = [
1918
+ `viable candidates: ${shown} shown, ${omitted} omitted; rejected: ${rejected}`
1919
+ ];
2073
1920
  lines.push(...varSetHints(exploration.suggestedVarSets));
2074
- if (omitted > 0 || shown < count)
1921
+ if (omitted > 0 || rejected > 0 || shown < count)
2075
1922
  lines.push("use --dynamic-mode candidates --max-dynamic-candidates 20 to explore candidate branches");
2076
1923
  return lines;
2077
1924
  }
@@ -2147,8 +1994,12 @@ function compactMessage(diagnostic) {
2147
1994
  }
2148
1995
  function suggestedHintLines(diagnostic) {
2149
1996
  const direct = cliHints(diagnostic.suggestedHints);
2150
- if (direct.length > 0) return cappedHints(direct);
2151
- return cappedHints(cliHintsFromSuggestions(diagnostic.implementationHintSuggestions));
1997
+ const omitted = numericValue3(diagnostic.omittedImplementationHintSuggestionCount);
1998
+ if (direct.length > 0) return cappedHints(direct, omitted);
1999
+ return cappedHints(
2000
+ cliHintsFromSuggestions(diagnostic.implementationHintSuggestions),
2001
+ omitted
2002
+ );
2152
2003
  }
2153
2004
  function cliHints(value) {
2154
2005
  return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
@@ -2163,12 +2014,17 @@ function cliHintsFromSuggestions(value) {
2163
2014
  function isRecord2(value) {
2164
2015
  return Boolean(value && typeof value === "object" && !Array.isArray(value));
2165
2016
  }
2166
- function cappedHints(hints) {
2017
+ function cappedHints(hints, omitted) {
2167
2018
  const unique = [...new Set(hints)];
2168
2019
  const shown = unique.slice(0, 3);
2169
- if (unique.length > shown.length) shown.push(`... ${unique.length - shown.length} more hint(s) available in --format json`);
2020
+ const remaining = Math.max(0, unique.length - shown.length) + omitted;
2021
+ if (remaining > 0)
2022
+ shown.push(`... ${remaining} additional hint(s) omitted; use a scoped --implementation-hint`);
2170
2023
  return shown;
2171
2024
  }
2025
+ function numericValue3(value) {
2026
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
2027
+ }
2172
2028
  function cleanDoctorMessage() {
2173
2029
  return `${pc.green("No diagnostics recorded")}
2174
2030
  `;
@@ -2200,6 +2056,65 @@ function renderMermaid(trace2) {
2200
2056
  `;
2201
2057
  }
2202
2058
 
2059
+ // src/cli/000-clean.ts
2060
+ import fs6 from "fs/promises";
2061
+ import path6 from "path";
2062
+ async function cleanWorkspaceState(config, dbOnly) {
2063
+ const dbDir = path6.resolve(path6.dirname(config.dbPath));
2064
+ if (!dbOnly) await assertOwnedStateDirectory(dbDir, config.rootPath);
2065
+ const runId = claimCleanWriter(config);
2066
+ try {
2067
+ if (dbOnly) await removeDatabaseFiles(config.dbPath);
2068
+ else await fs6.rm(dbDir, { recursive: true, force: true });
2069
+ } catch (error) {
2070
+ await markCleanClaimFailed(config.dbPath, runId, error);
2071
+ throw error;
2072
+ }
2073
+ }
2074
+ function claimCleanWriter(config) {
2075
+ const db = openDatabase(config.dbPath);
2076
+ try {
2077
+ const workspaceId = getWorkspace(db, config.rootPath)?.id ?? upsertWorkspace(db, config.rootPath, config.dbPath);
2078
+ return claimIndexRun(db, workspaceId, 0);
2079
+ } finally {
2080
+ db.close();
2081
+ }
2082
+ }
2083
+ async function assertOwnedStateDirectory(dbDir, rootPath) {
2084
+ const marker = path6.join(dbDir, ".service-flow-state");
2085
+ const dangerous = /* @__PURE__ */ new Set([
2086
+ path6.parse(dbDir).root,
2087
+ "/tmp",
2088
+ process.env.HOME ? path6.resolve(process.env.HOME) : "",
2089
+ path6.resolve(rootPath)
2090
+ ]);
2091
+ const ownsState = await fs6.stat(marker).then((stat) => stat.isFile()).catch(() => false);
2092
+ if (!ownsState || dangerous.has(dbDir))
2093
+ throw new Error(
2094
+ `Refusing to recursively delete unowned or dangerous state directory: ${dbDir}. Use --db-only to remove only the database file.`
2095
+ );
2096
+ }
2097
+ async function removeDatabaseFiles(dbPath) {
2098
+ for (const suffix of ["-wal", "-shm", "-journal"])
2099
+ await fs6.rm(`${dbPath}${suffix}`, { force: true });
2100
+ await fs6.rm(dbPath, { force: true });
2101
+ }
2102
+ async function markCleanClaimFailed(dbPath, runId, error) {
2103
+ const exists = await fs6.stat(dbPath).then(() => true).catch(() => false);
2104
+ if (!exists) return;
2105
+ const db = openDatabase(dbPath);
2106
+ try {
2107
+ db.prepare(`UPDATE index_runs SET finished_at=?,status='failed',
2108
+ error_message=? WHERE id=? AND status='running'`).run(
2109
+ (/* @__PURE__ */ new Date()).toISOString(),
2110
+ `Clean failed after writer claim: ${errorMessage(error)}`,
2111
+ runId
2112
+ );
2113
+ } finally {
2114
+ db.close();
2115
+ }
2116
+ }
2117
+
2203
2118
  // src/cli.ts
2204
2119
  async function init(workspace, options) {
2205
2120
  const config = createWorkspaceConfig(
@@ -2254,6 +2169,27 @@ async function withReadOnlyWorkspace(workspace, fn) {
2254
2169
  db.close();
2255
2170
  }
2256
2171
  }
2172
+ function selectRepository(db, selector, workspaceId) {
2173
+ const candidates = reposByName(db, selector, workspaceId);
2174
+ if (candidates.length === 1) return { repo: candidates[0] };
2175
+ if (candidates.length === 0) return {
2176
+ diagnostic: {
2177
+ severity: "warning",
2178
+ code: "selector_repo_not_found",
2179
+ message: `Repository selector not found: ${selector}`
2180
+ }
2181
+ };
2182
+ return {
2183
+ diagnostic: selectorRepoAmbiguousDiagnostic(
2184
+ selector,
2185
+ candidates.map((repo) => ({
2186
+ id: repo.id,
2187
+ name: repo.name,
2188
+ packageName: repo.package_name ?? void 0
2189
+ }))
2190
+ )
2191
+ };
2192
+ }
2257
2193
  function createProgram() {
2258
2194
  const program = new Command();
2259
2195
  program.name("service-flow").description(
@@ -2286,7 +2222,7 @@ function createProgram() {
2286
2222
  }).catch(fail)
2287
2223
  );
2288
2224
  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").option("--format <format>", "table|json|mermaid", "table").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(
2289
- (opts) => void withReadOnlyWorkspace(opts.workspace, (db) => {
2225
+ (opts) => void withReadOnlyWorkspace(opts.workspace, (db, workspaceId) => {
2290
2226
  const result = trace(
2291
2227
  db,
2292
2228
  {
@@ -2298,6 +2234,7 @@ function createProgram() {
2298
2234
  },
2299
2235
  {
2300
2236
  depth: Number(opts.depth),
2237
+ workspaceId,
2301
2238
  vars: parseVars(opts.var),
2302
2239
  includeExternal: Boolean(opts.includeExternal),
2303
2240
  includeDb: Boolean(opts.includeDb),
@@ -2317,9 +2254,9 @@ function createProgram() {
2317
2254
  list.command("repos").option("--workspace <path>").action(
2318
2255
  (opts) => void withReadOnlyWorkspace(
2319
2256
  opts.workspace,
2320
- (db) => process.stdout.write(
2257
+ (db, workspaceId) => process.stdout.write(
2321
2258
  renderJson(
2322
- listRepositories(db).map((r) => ({
2259
+ listRepositories(db, workspaceId).map((r) => ({
2323
2260
  name: r.name,
2324
2261
  kind: r.kind,
2325
2262
  packageName: r.package_name
@@ -2329,41 +2266,45 @@ function createProgram() {
2329
2266
  ).catch(fail)
2330
2267
  );
2331
2268
  list.command("services").option("--workspace <path>").option("--repo <name>").action(
2332
- (opts) => void withReadOnlyWorkspace(opts.workspace, (db) => {
2333
- const repo = opts.repo ? repoByName(db, opts.repo) : void 0;
2334
- if (opts.repo && !repo) {
2335
- process.stdout.write(renderJson([{ severity: "warning", code: "selector_repo_not_found", message: `Repository selector not found: ${opts.repo}` }]));
2269
+ (opts) => void withReadOnlyWorkspace(opts.workspace, (db, workspaceId) => {
2270
+ const selection = opts.repo ? selectRepository(db, opts.repo, workspaceId) : {};
2271
+ if (selection.diagnostic) {
2272
+ process.stdout.write(renderJson([selection.diagnostic]));
2336
2273
  return;
2337
2274
  }
2275
+ const repo = selection.repo;
2338
2276
  const rows = db.prepare(
2339
- "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 (? IS NULL OR s.repo_id=?) ORDER BY r.name,s.service_path"
2340
- ).all(repo?.id, repo?.id);
2277
+ "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"
2278
+ ).all(workspaceId, repo?.id, repo?.id);
2341
2279
  process.stdout.write(renderJson(rows));
2342
2280
  }).catch(fail)
2343
2281
  );
2344
2282
  list.command("operations").option("--workspace <path>").option("--repo <name>").option("--service <path>").action(
2345
- (opts) => void withReadOnlyWorkspace(opts.workspace, (db) => {
2346
- const repo = opts.repo ? repoByName(db, opts.repo) : void 0;
2347
- if (opts.repo && !repo) {
2348
- process.stdout.write(renderJson([{ severity: "warning", code: "selector_repo_not_found", message: `Repository selector not found: ${opts.repo}` }]));
2283
+ (opts) => void withReadOnlyWorkspace(opts.workspace, (db, workspaceId) => {
2284
+ const selection = opts.repo ? selectRepository(db, opts.repo, workspaceId) : {};
2285
+ if (selection.diagnostic) {
2286
+ process.stdout.write(renderJson([selection.diagnostic]));
2349
2287
  return;
2350
2288
  }
2289
+ const repo = selection.repo;
2351
2290
  const rows = db.prepare(
2352
- "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 (? IS NULL OR s.repo_id=?) AND (? IS NULL OR s.service_path=?)"
2353
- ).all(repo?.id, repo?.id, opts.service, opts.service);
2291
+ "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=?)"
2292
+ ).all(workspaceId, repo?.id, repo?.id, opts.service, opts.service);
2354
2293
  process.stdout.write(renderJson(rows));
2355
2294
  }).catch(fail)
2356
2295
  );
2357
2296
  list.command("calls").option("--workspace <path>").option("--repo <name>").option("--operation <name>").action(
2358
- (opts) => void withReadOnlyWorkspace(opts.workspace, (db) => {
2359
- const repo = opts.repo ? repoByName(db, opts.repo) : void 0;
2360
- if (opts.repo && !repo) {
2361
- process.stdout.write(renderJson([{ severity: "warning", code: "selector_repo_not_found", message: `Repository selector not found: ${opts.repo}` }]));
2297
+ (opts) => void withReadOnlyWorkspace(opts.workspace, (db, workspaceId) => {
2298
+ const selection = opts.repo ? selectRepository(db, opts.repo, workspaceId) : {};
2299
+ if (selection.diagnostic) {
2300
+ process.stdout.write(renderJson([selection.diagnostic]));
2362
2301
  return;
2363
2302
  }
2303
+ const repo = selection.repo;
2364
2304
  const rows = db.prepare(
2365
- "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 (? IS NULL OR c.repo_id=?) AND (? IS NULL OR c.operation_path_expr=? OR c.operation_path_expr=? OR c.payload_summary LIKE ?)"
2305
+ "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 ?)"
2366
2306
  ).all(
2307
+ workspaceId,
2367
2308
  repo?.id,
2368
2309
  repo?.id,
2369
2310
  opts.operation,
@@ -2375,7 +2316,7 @@ function createProgram() {
2375
2316
  }).catch(fail)
2376
2317
  );
2377
2318
  program.command("graph").option("--workspace <path>").option("--repo <name>").option("--operation <name>").option("--service <path>").option("--path <operationPath>").option("--format <format>", "mermaid|json", "mermaid").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(
2378
- (opts) => void withReadOnlyWorkspace(opts.workspace, (db) => {
2319
+ (opts) => void withReadOnlyWorkspace(opts.workspace, (db, workspaceId) => {
2379
2320
  const result = trace(
2380
2321
  db,
2381
2322
  {
@@ -2386,6 +2327,7 @@ function createProgram() {
2386
2327
  },
2387
2328
  {
2388
2329
  depth: 100,
2330
+ workspaceId,
2389
2331
  includeAsync: true,
2390
2332
  includeDb: true,
2391
2333
  includeExternal: true,
@@ -2403,18 +2345,18 @@ function createProgram() {
2403
2345
  );
2404
2346
  const inspect = program.command("inspect");
2405
2347
  inspect.command("repo").argument("<name>").option("--workspace <path>").action(
2406
- (name, opts) => void withReadOnlyWorkspace(
2407
- opts.workspace,
2408
- (db) => process.stdout.write(
2409
- renderJson(repoByName(db, name) ?? { error: "repo not found" })
2410
- )
2411
- ).catch(fail)
2348
+ (name, opts) => void withReadOnlyWorkspace(opts.workspace, (db, workspaceId) => {
2349
+ const selection = selectRepository(db, name, workspaceId);
2350
+ process.stdout.write(renderJson(
2351
+ selection.repo ?? selection.diagnostic ?? { error: "repo not found" }
2352
+ ));
2353
+ }).catch(fail)
2412
2354
  );
2413
2355
  inspect.command("operation").argument("<selector>").option("--workspace <path>").action(
2414
- (selector, opts) => void withReadOnlyWorkspace(opts.workspace, (db) => {
2356
+ (selector, opts) => void withReadOnlyWorkspace(opts.workspace, (db, workspaceId) => {
2415
2357
  const rows = db.prepare(
2416
- "SELECT * FROM cds_operations WHERE operation_name=? OR operation_path=?"
2417
- ).all(selector, selector);
2358
+ "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=?)"
2359
+ ).all(workspaceId, selector, selector);
2418
2360
  process.stdout.write(renderJson(rows));
2419
2361
  }).catch(fail)
2420
2362
  );
@@ -2427,29 +2369,7 @@ function createProgram() {
2427
2369
  program.command("clean").option("--workspace <path>").option("--db-only").action(
2428
2370
  (opts) => void (async () => {
2429
2371
  const config = await loadWorkspaceConfig(opts.workspace);
2430
- const dbDir = path6.resolve(path6.dirname(config.dbPath));
2431
- const workspaceRoot = path6.resolve(config.rootPath);
2432
- await fs6.rm(config.dbPath, { force: true });
2433
- if (!opts.dbOnly) {
2434
- const marker = path6.join(dbDir, ".service-flow-state");
2435
- const dangerous = /* @__PURE__ */ new Set([
2436
- path6.parse(dbDir).root,
2437
- "/tmp",
2438
- process.env.HOME ? path6.resolve(process.env.HOME) : "",
2439
- workspaceRoot
2440
- ]);
2441
- let ownsState;
2442
- try {
2443
- ownsState = (await fs6.stat(marker)).isFile();
2444
- } catch {
2445
- ownsState = false;
2446
- }
2447
- if (!ownsState || dangerous.has(dbDir))
2448
- throw new Error(
2449
- `Refusing to recursively delete unowned or dangerous state directory: ${dbDir}. Use --db-only to remove only the database file.`
2450
- );
2451
- await fs6.rm(dbDir, { recursive: true, force: true });
2452
- }
2372
+ await cleanWorkspaceState(config, Boolean(opts.dbOnly));
2453
2373
  process.stdout.write("Cleaned service-flow state\n");
2454
2374
  })().catch(fail)
2455
2375
  );