@saptools/service-flow 0.1.6 → 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/dist/cli.js CHANGED
@@ -10,12 +10,12 @@ import {
10
10
  parsePackageJson,
11
11
  parseServiceBindings,
12
12
  trace
13
- } from "./chunk-6C5HZ6IR.js";
13
+ } from "./chunk-NBAO2R2H.js";
14
14
 
15
15
  // src/cli.ts
16
16
  import { Command } from "commander";
17
- import path6 from "path";
18
- import fs6 from "fs/promises";
17
+ import path5 from "path";
18
+ import fs5 from "fs/promises";
19
19
  import pc from "picocolors";
20
20
 
21
21
  // src/config/defaults.ts
@@ -90,7 +90,7 @@ import path2 from "path";
90
90
  // src/db/schema.ts
91
91
  var schemaSql = `
92
92
  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);
93
- 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, UNIQUE(workspace_id, absolute_path), FOREIGN KEY(workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE);
93
+ 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, UNIQUE(workspace_id, absolute_path), FOREIGN KEY(workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE);
94
94
  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);
95
95
  CREATE TABLE IF NOT EXISTS cds_requires (id INTEGER PRIMARY KEY, repo_id INTEGER NOT NULL, alias TEXT NOT NULL, kind TEXT, model TEXT, destination TEXT, service_path TEXT, request_timeout INTEGER, raw_json TEXT NOT NULL, FOREIGN KEY(repo_id) REFERENCES repositories(id) ON DELETE CASCADE);
96
96
  CREATE TABLE IF NOT EXISTS cds_services (id INTEGER PRIMARY KEY, repo_id INTEGER NOT NULL, namespace TEXT, service_name TEXT NOT NULL, qualified_name TEXT NOT NULL, service_path TEXT NOT NULL, is_extend INTEGER NOT NULL, source_file TEXT NOT NULL, source_line INTEGER NOT NULL, FOREIGN KEY(repo_id) REFERENCES repositories(id) ON DELETE CASCADE);
@@ -98,11 +98,11 @@ CREATE TABLE IF NOT EXISTS cds_operations (id INTEGER PRIMARY KEY, service_id IN
98
98
  CREATE TABLE IF NOT EXISTS symbols (id INTEGER PRIMARY KEY, repo_id INTEGER NOT NULL, file_id INTEGER, kind TEXT NOT NULL, name TEXT NOT NULL, qualified_name TEXT NOT NULL, exported INTEGER NOT NULL, start_line INTEGER NOT NULL, end_line INTEGER NOT NULL, FOREIGN KEY(repo_id) REFERENCES repositories(id) ON DELETE CASCADE, FOREIGN KEY(file_id) REFERENCES files(id) ON DELETE CASCADE);
99
99
  CREATE TABLE IF NOT EXISTS handler_classes (id INTEGER PRIMARY KEY, repo_id INTEGER NOT NULL, symbol_id INTEGER, class_name TEXT NOT NULL, source_file TEXT NOT NULL, source_line INTEGER NOT NULL, FOREIGN KEY(repo_id) REFERENCES repositories(id) ON DELETE CASCADE, FOREIGN KEY(symbol_id) REFERENCES symbols(id) ON DELETE SET NULL);
100
100
  CREATE TABLE IF NOT EXISTS handler_methods (id INTEGER PRIMARY KEY, handler_class_id INTEGER NOT NULL, method_name TEXT NOT NULL, decorator_kind TEXT NOT NULL, decorator_value TEXT, decorator_raw_expression TEXT NOT NULL, source_file TEXT NOT NULL, source_line INTEGER NOT NULL, FOREIGN KEY(handler_class_id) REFERENCES handler_classes(id) ON DELETE CASCADE);
101
- CREATE TABLE IF NOT EXISTS handler_registrations (id INTEGER PRIMARY KEY, repo_id INTEGER NOT NULL, handler_class_id INTEGER, registration_file TEXT NOT NULL, registration_line INTEGER NOT NULL, registration_kind TEXT NOT NULL, confidence REAL NOT NULL, FOREIGN KEY(repo_id) REFERENCES repositories(id) ON DELETE CASCADE, FOREIGN KEY(handler_class_id) REFERENCES handler_classes(id) ON DELETE SET NULL);
101
+ CREATE TABLE IF NOT EXISTS handler_registrations (id INTEGER PRIMARY KEY, repo_id INTEGER NOT NULL, handler_class_id INTEGER, class_name TEXT, import_source TEXT, registration_file TEXT NOT NULL, registration_line INTEGER NOT NULL, registration_kind TEXT NOT NULL, confidence REAL NOT NULL, FOREIGN KEY(repo_id) REFERENCES repositories(id) ON DELETE CASCADE, FOREIGN KEY(handler_class_id) REFERENCES handler_classes(id) ON DELETE SET NULL);
102
102
  CREATE TABLE IF NOT EXISTS service_bindings (id INTEGER PRIMARY KEY, repo_id INTEGER NOT NULL, symbol_id INTEGER, variable_name TEXT NOT NULL, alias TEXT, alias_expr TEXT, destination_expr TEXT, service_path_expr TEXT, is_dynamic INTEGER NOT NULL, placeholders_json TEXT NOT NULL, source_file TEXT NOT NULL, source_line INTEGER NOT NULL, helper_chain_json TEXT, FOREIGN KEY(repo_id) REFERENCES repositories(id) ON DELETE CASCADE, FOREIGN KEY(symbol_id) REFERENCES symbols(id) ON DELETE SET NULL);
103
103
  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, 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);
104
- 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, FOREIGN KEY(workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE);
105
- 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, FOREIGN KEY(workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE);
104
+ 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);
105
+ 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);
106
106
  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);
107
107
  CREATE INDEX IF NOT EXISTS idx_repo_name ON repositories(name);
108
108
  CREATE INDEX IF NOT EXISTS idx_service_path ON cds_services(service_path);
@@ -112,7 +112,31 @@ CREATE VIRTUAL TABLE IF NOT EXISTS search_index USING fts5(kind, name, path, rep
112
112
  `;
113
113
 
114
114
  // src/db/migrations.ts
115
- var CURRENT_SCHEMA_VERSION = 2;
115
+ var CURRENT_SCHEMA_VERSION = 3;
116
+ var columns = {
117
+ service_bindings: [
118
+ { name: "helper_chain_json", ddl: "ALTER TABLE service_bindings ADD COLUMN helper_chain_json TEXT" },
119
+ { name: "alias_expr", ddl: "ALTER TABLE service_bindings ADD COLUMN alias_expr TEXT" }
120
+ ],
121
+ repositories: [
122
+ { name: "fingerprint", ddl: "ALTER TABLE repositories ADD COLUMN fingerprint TEXT" },
123
+ { name: "fact_generation", ddl: "ALTER TABLE repositories ADD COLUMN fact_generation INTEGER NOT NULL DEFAULT 0" },
124
+ { name: "graph_generation", ddl: "ALTER TABLE repositories ADD COLUMN graph_generation INTEGER NOT NULL DEFAULT 0" },
125
+ { name: "graph_stale_reason", ddl: "ALTER TABLE repositories ADD COLUMN graph_stale_reason TEXT" },
126
+ { name: "graph_stale_at", ddl: "ALTER TABLE repositories ADD COLUMN graph_stale_at TEXT" }
127
+ ],
128
+ graph_edges: [
129
+ { name: "status", ddl: "ALTER TABLE graph_edges ADD COLUMN status TEXT NOT NULL DEFAULT 'unresolved'" },
130
+ { name: "generation", ddl: "ALTER TABLE graph_edges ADD COLUMN generation INTEGER NOT NULL DEFAULT 0" }
131
+ ],
132
+ handler_registrations: [
133
+ { name: "class_name", ddl: "ALTER TABLE handler_registrations ADD COLUMN class_name TEXT" },
134
+ { name: "import_source", ddl: "ALTER TABLE handler_registrations ADD COLUMN import_source TEXT" }
135
+ ],
136
+ index_runs: [
137
+ { name: "error_message", ddl: "ALTER TABLE index_runs ADD COLUMN error_message TEXT" }
138
+ ]
139
+ };
116
140
  function hasColumn(db, table, column) {
117
141
  return db.prepare(`PRAGMA table_info(${table})`).all().some((row) => row.name === column);
118
142
  }
@@ -120,21 +144,45 @@ function userVersion(db) {
120
144
  const row = db.pragma("user_version")[0];
121
145
  return Number(row?.user_version ?? 0);
122
146
  }
147
+ function addMissingColumns(db) {
148
+ for (const [table, tableColumns] of Object.entries(columns)) {
149
+ for (const column of tableColumns) {
150
+ if (!hasColumn(db, table, column.name)) db.prepare(column.ddl).run();
151
+ }
152
+ }
153
+ }
154
+ function normalizeLegacyStatus(db) {
155
+ db.prepare("UPDATE graph_edges SET status=CASE WHEN edge_type='REMOTE_CALL_RESOLVES_TO_OPERATION' THEN 'resolved' WHEN edge_type IN ('HANDLER_RUNS_DB_QUERY','HANDLER_CALLS_EXTERNAL_HTTP','HANDLER_EMITS_EVENT','EVENT_CONSUMED_BY_HANDLER') THEN 'terminal' WHEN edge_type='DYNAMIC_EDGE_CANDIDATE' THEN 'dynamic' WHEN status='ambiguous' THEN 'ambiguous' ELSE status END").run();
156
+ db.prepare("UPDATE repositories SET graph_stale_reason='schema_migration_requires_relink', graph_stale_at=COALESCE(graph_stale_at, datetime('now')) WHERE EXISTS (SELECT 1 FROM graph_edges WHERE graph_edges.workspace_id=repositories.workspace_id) AND graph_generation=0").run();
157
+ }
123
158
  function migrate(db) {
124
159
  db.transaction(() => {
160
+ const version = userVersion(db);
161
+ if (version > CURRENT_SCHEMA_VERSION) throw new Error(`Unsupported future service-flow schema version ${version}`);
125
162
  db.exec(schemaSql);
126
- if (!hasColumn(db, "service_bindings", "helper_chain_json")) db.prepare("ALTER TABLE service_bindings ADD COLUMN helper_chain_json TEXT").run();
127
- if (!hasColumn(db, "service_bindings", "alias_expr")) db.prepare("ALTER TABLE service_bindings ADD COLUMN alias_expr TEXT").run();
128
- if (!hasColumn(db, "repositories", "fingerprint")) db.prepare("ALTER TABLE repositories ADD COLUMN fingerprint TEXT").run();
129
- if (!hasColumn(db, "graph_edges", "status")) db.prepare("ALTER TABLE graph_edges ADD COLUMN status TEXT NOT NULL DEFAULT 'unresolved'").run();
130
- db.prepare("UPDATE graph_edges SET status=CASE WHEN edge_type='REMOTE_CALL_RESOLVES_TO_OPERATION' THEN 'resolved' WHEN edge_type IN ('HANDLER_RUNS_DB_QUERY','HANDLER_CALLS_EXTERNAL_HTTP','HANDLER_EMITS_EVENT','EVENT_CONSUMED_BY_HANDLER') THEN 'terminal' WHEN edge_type='DYNAMIC_EDGE_CANDIDATE' THEN 'dynamic' ELSE status END").run();
131
- db.pragma(`user_version = ${Math.max(userVersion(db), CURRENT_SCHEMA_VERSION)}`);
163
+ addMissingColumns(db);
164
+ normalizeLegacyStatus(db);
165
+ const violations = db.pragma("foreign_key_check");
166
+ if (violations.length > 0) throw new Error("SQLite foreign_key_check failed during migration");
167
+ db.pragma(`user_version = ${CURRENT_SCHEMA_VERSION}`);
132
168
  });
133
169
  }
134
170
 
135
171
  // src/db/connection.ts
172
+ var sqliteWarningFilterInstalled = false;
173
+ function installSqliteWarningFilter() {
174
+ if (sqliteWarningFilterInstalled) return;
175
+ sqliteWarningFilterInstalled = true;
176
+ const original = process.emitWarning.bind(process);
177
+ process.emitWarning = ((warning, ...args) => {
178
+ const text = warning instanceof Error ? warning.message : String(warning);
179
+ if (text.includes("SQLite is an experimental feature")) return;
180
+ Reflect.apply(original, process, [warning, ...args]);
181
+ });
182
+ }
136
183
  function loadSqlite() {
137
184
  try {
185
+ installSqliteWarningFilter();
138
186
  const moduleValue = process.getBuiltinModule("node:sqlite");
139
187
  if (!moduleValue || typeof moduleValue !== "object" || !("DatabaseSync" in moduleValue))
140
188
  throw new Error("node:sqlite DatabaseSync is unavailable");
@@ -144,7 +192,7 @@ function loadSqlite() {
144
192
  return sqlite;
145
193
  } catch (error) {
146
194
  throw new Error(
147
- "service-flow 0.1.6 requires Node.js >=24 with node:sqlite DatabaseSync support. Upgrade Node.js or install a service-flow build with a compatible SQLite driver.",
195
+ "service-flow 0.1.8 requires Node.js >=24 with node:sqlite DatabaseSync support. Upgrade Node.js or install a service-flow build with a compatible SQLite driver.",
148
196
  { cause: error }
149
197
  );
150
198
  }
@@ -360,17 +408,23 @@ function insertHandler(db, repoId, h) {
360
408
  }
361
409
  function insertRegistrations(db, repoId, rows) {
362
410
  const stmt = db.prepare(
363
- "INSERT INTO handler_registrations(repo_id,handler_class_id,registration_file,registration_line,registration_kind,confidence) VALUES(?,?,?,?,?,?)"
411
+ "INSERT INTO handler_registrations(repo_id,handler_class_id,class_name,import_source,registration_file,registration_line,registration_kind,confidence) VALUES(?,?,?,?,?,?,?,?)"
364
412
  );
365
- for (const r of rows)
413
+ for (const r of rows) {
414
+ const handlerClass = r.className ? db.prepare(
415
+ "SELECT id FROM handler_classes WHERE repo_id=? AND class_name=? ORDER BY id"
416
+ ).all(repoId, r.className) : [];
366
417
  stmt.run(
367
418
  repoId,
368
- null,
419
+ handlerClass.length === 1 ? handlerClass[0]?.id : null,
420
+ r.className,
421
+ r.importSource,
369
422
  r.registrationFile,
370
423
  r.registrationLine,
371
424
  r.registrationKind,
372
425
  r.confidence
373
426
  );
427
+ }
374
428
  }
375
429
  function insertBindings(db, repoId, rows) {
376
430
  const stmt = db.prepare(
@@ -453,11 +507,12 @@ async function findFiles(root, suffix) {
453
507
  return out;
454
508
  }
455
509
 
456
- // src/indexer/repository-indexer.ts
457
- import fs5 from "fs/promises";
458
- import path5 from "path";
510
+ // src/utils/diagnostics.ts
511
+ function errorMessage(error) {
512
+ return error instanceof Error ? error.message : String(error);
513
+ }
459
514
 
460
- // src/indexer/incremental-index.ts
515
+ // src/indexer/repository-indexer.ts
461
516
  import fs4 from "fs/promises";
462
517
  import path4 from "path";
463
518
 
@@ -471,32 +526,10 @@ function sha256Text(text) {
471
526
  return createHash("sha256").update(text).digest("hex");
472
527
  }
473
528
 
474
- // src/indexer/incremental-index.ts
475
- async function recordFile(db, repoId, repoPath, relativeFile) {
476
- const abs = path4.join(repoPath, relativeFile);
477
- const stat = await fs4.stat(abs);
478
- const hash = await sha256File(abs);
479
- db.prepare(
480
- "INSERT INTO files(repo_id,relative_path,extension,sha256,size_bytes,last_indexed_at) VALUES(?,?,?,?,?,?) ON CONFLICT(repo_id,relative_path) DO UPDATE SET sha256=excluded.sha256,size_bytes=excluded.size_bytes,last_indexed_at=excluded.last_indexed_at"
481
- ).run(
482
- repoId,
483
- normalizePath(relativeFile),
484
- path4.extname(relativeFile),
485
- hash,
486
- stat.size,
487
- (/* @__PURE__ */ new Date()).toISOString()
488
- );
489
- }
490
-
491
- // src/utils/diagnostics.ts
492
- function errorMessage(error) {
493
- return error instanceof Error ? error.message : String(error);
494
- }
495
-
496
529
  // package.json
497
530
  var package_default = {
498
531
  name: "@saptools/service-flow",
499
- version: "0.1.6",
532
+ version: "0.1.8",
500
533
  description: "Trace SAP CAP service-to-service flows across multi-repository workspaces with runtime-aware graph resolution",
501
534
  type: "module",
502
535
  publishConfig: {
@@ -572,80 +605,60 @@ var ANALYZER_VERSION = package_default.version;
572
605
 
573
606
  // src/indexer/repository-indexer.ts
574
607
  async function indexRepository(db, repo, force) {
575
- let diagnostics = 0;
576
- let files = 0;
577
- const sourceFiles = await findSourceFiles(repo.absolute_path);
578
- const facts = await parsePackageJson(repo.absolute_path);
579
- const fingerprint = await repositoryFingerprint(repo.absolute_path, sourceFiles, facts, Boolean(force));
580
- if (!force && repo.fingerprint === fingerprint) return { fileCount: 0, diagnosticCount: 0, skipped: true };
581
- const kind = await classifyRepository(repo.absolute_path, facts);
582
- db.prepare(
583
- "UPDATE repositories SET package_name=?, package_version=?, dependencies_json=?, kind=?, index_status=? WHERE id=?"
584
- ).run(
585
- facts.packageName,
586
- facts.packageVersion,
587
- JSON.stringify(facts.dependencies),
588
- kind,
589
- "indexing",
590
- repo.id
591
- );
592
- clearRepoFacts(db, repo.id);
593
- insertRequires(db, repo.id, facts.cdsRequires);
594
- for (const file of sourceFiles) {
595
- try {
596
- await recordFile(db, repo.id, repo.absolute_path, file);
597
- files += 1;
598
- if (file.endsWith(".cds"))
599
- for (const s of await parseCdsFile(repo.absolute_path, file))
600
- insertService(db, repo.id, s);
601
- if (/\.[jt]s$/.test(file)) {
602
- for (const h of await parseDecorators(repo.absolute_path, file))
603
- insertHandler(db, repo.id, h);
604
- insertRegistrations(
605
- db,
606
- repo.id,
607
- await parseHandlerRegistrations(repo.absolute_path, file)
608
- );
609
- insertBindings(
610
- db,
611
- repo.id,
612
- await parseServiceBindings(repo.absolute_path, file)
613
- );
614
- insertCalls(
615
- db,
616
- repo.id,
617
- await parseOutboundCalls(repo.absolute_path, file)
618
- );
619
- }
620
- } catch (error) {
621
- diagnostics += 1;
622
- db.prepare(
623
- "INSERT INTO diagnostics(repo_id,severity,code,message,source_file) VALUES(?,?,?,?,?)"
624
- ).run(repo.id, "warning", "parse_failed", errorMessage(error), file);
608
+ try {
609
+ const sourceFiles = await findSourceFiles(repo.absolute_path);
610
+ const packageFacts = await parsePackageJson(repo.absolute_path);
611
+ const fingerprint = await repositoryFingerprint(repo.absolute_path, sourceFiles, packageFacts);
612
+ if (!force && repo.fingerprint === fingerprint) return { fileCount: 0, diagnosticCount: 0, skipped: true };
613
+ const kind = await classifyRepository(repo.absolute_path, packageFacts);
614
+ const parsed = await parseAllSourceFacts(repo.absolute_path, sourceFiles);
615
+ db.transaction(() => {
616
+ db.prepare("UPDATE repositories SET package_name=?, package_version=?, dependencies_json=?, kind=?, index_status=? WHERE id=?").run(packageFacts.packageName, packageFacts.packageVersion, JSON.stringify(packageFacts.dependencies), kind, "indexing", repo.id);
617
+ clearRepoFacts(db, repo.id);
618
+ insertRequires(db, repo.id, packageFacts.cdsRequires);
619
+ const fileStmt = db.prepare("INSERT INTO files(repo_id,relative_path,extension,sha256,size_bytes,last_indexed_at) VALUES(?,?,?,?,?,?) ON CONFLICT(repo_id,relative_path) DO UPDATE SET sha256=excluded.sha256,size_bytes=excluded.size_bytes,last_indexed_at=excluded.last_indexed_at");
620
+ for (const file of parsed.fileRecords) fileStmt.run(repo.id, file.relativePath, file.extension, file.sha256, file.sizeBytes, (/* @__PURE__ */ new Date()).toISOString());
621
+ for (const s of parsed.services) insertService(db, repo.id, s);
622
+ for (const h of parsed.handlers) insertHandler(db, repo.id, h);
623
+ insertRegistrations(db, repo.id, parsed.registrations);
624
+ insertBindings(db, repo.id, parsed.bindings);
625
+ insertCalls(db, repo.id, parsed.calls);
626
+ db.prepare("UPDATE repositories SET last_indexed_at=?, index_status='indexed', error_count=0, fingerprint=?, fact_generation=COALESCE(fact_generation,0)+1, graph_stale_reason='facts_changed', graph_stale_at=? WHERE id=?").run((/* @__PURE__ */ new Date()).toISOString(), fingerprint, (/* @__PURE__ */ new Date()).toISOString(), repo.id);
627
+ });
628
+ return { fileCount: sourceFiles.length, diagnosticCount: 0, skipped: false };
629
+ } catch (error) {
630
+ const message = errorMessage(error);
631
+ db.prepare("UPDATE repositories SET index_status='failed', error_count=1 WHERE id=?").run(repo.id);
632
+ db.prepare("DELETE FROM diagnostics WHERE repo_id=? AND code IN ('index_failed_snapshot_preserved','source_read_failed')").run(repo.id);
633
+ db.prepare("INSERT INTO diagnostics(repo_id,severity,code,message) VALUES(?,?,?,?)").run(repo.id, "error", "source_read_failed", `Index failed before publication; previous facts and fingerprint were preserved. ${message}`);
634
+ return { fileCount: 0, diagnosticCount: 1, skipped: false };
635
+ }
636
+ }
637
+ async function parseAllSourceFacts(root, files) {
638
+ const facts = { services: [], handlers: [], registrations: [], bindings: [], calls: [], fileRecords: [] };
639
+ for (const file of files) {
640
+ const abs = path4.join(root, file);
641
+ const stat = await fs4.stat(abs);
642
+ facts.fileRecords.push({ relativePath: normalizePath(file), extension: path4.extname(file), sha256: await sha256File(abs), sizeBytes: stat.size });
643
+ if (file.endsWith(".cds")) facts.services.push(...await parseCdsFile(root, file));
644
+ if (/\.[jt]s$/.test(file)) {
645
+ facts.handlers.push(...await parseDecorators(root, file));
646
+ facts.registrations.push(...await parseHandlerRegistrations(root, file));
647
+ facts.bindings.push(...await parseServiceBindings(root, file));
648
+ facts.calls.push(...await parseOutboundCalls(root, file));
625
649
  }
626
650
  }
627
- db.prepare(
628
- "UPDATE repositories SET last_indexed_at=?, index_status=?, error_count=?, fingerprint=? WHERE id=?"
629
- ).run(
630
- (/* @__PURE__ */ new Date()).toISOString(),
631
- diagnostics ? "partial" : "indexed",
632
- diagnostics,
633
- fingerprint,
634
- repo.id
635
- );
636
- return { fileCount: files, diagnosticCount: diagnostics, skipped: false };
651
+ return facts;
637
652
  }
638
653
  async function findSourceFiles(root) {
639
654
  const out = [];
640
655
  async function walk(dir, prefix = "") {
641
- const entries = await fs5.readdir(dir, { withFileTypes: true }).catch(() => []);
656
+ const entries = await fs4.readdir(dir, { withFileTypes: true }).catch(() => []);
642
657
  for (const e of entries) {
643
658
  const rel = prefix ? `${prefix}/${e.name}` : e.name;
644
659
  if (e.isDirectory()) {
645
- if (!["node_modules", "dist", "gen", "coverage", ".git"].includes(e.name))
646
- await walk(path5.join(dir, e.name), rel);
647
- } else if (/\.(cds|ts|js)$/.test(e.name) && !isDefaultTestFile(rel))
648
- out.push(rel);
660
+ if (!["node_modules", "dist", "gen", "coverage", ".git"].includes(e.name)) await walk(path4.join(dir, e.name), rel);
661
+ } else if (/\.(cds|ts|js)$/.test(e.name) && !isDefaultTestFile(rel)) out.push(rel);
649
662
  }
650
663
  }
651
664
  await walk(root);
@@ -653,13 +666,11 @@ async function findSourceFiles(root) {
653
666
  }
654
667
  function isDefaultTestFile(relativeFile) {
655
668
  const parts = relativeFile.split("/");
656
- if (parts.some((part) => ["test", "tests", "__tests__"].includes(part)))
657
- return true;
669
+ if (parts.some((part) => ["test", "tests", "__tests__"].includes(part))) return true;
658
670
  return /\.(test|spec)\.[jt]s$/.test(parts.at(-1) ?? "");
659
671
  }
660
- async function repositoryFingerprint(root, files, facts, force) {
661
- void force;
662
- const packageJson = await fs5.readFile(path5.join(root, "package.json"), "utf8").catch(() => "");
672
+ async function repositoryFingerprint(root, files, facts) {
673
+ const packageJson = await fs4.readFile(path4.join(root, "package.json"), "utf8").catch(() => "");
663
674
  const normalizedFacts = {
664
675
  analyzerVersion: ANALYZER_VERSION,
665
676
  packageName: facts.packageName,
@@ -672,7 +683,7 @@ async function repositoryFingerprint(root, files, facts, force) {
672
683
  };
673
684
  const entries = [`facts:${JSON.stringify(normalizedFacts)}`];
674
685
  for (const file of files) {
675
- const content = await fs5.readFile(path5.join(root, file), "utf8").catch(() => "");
686
+ const content = await fs4.readFile(path4.join(root, file), "utf8");
676
687
  entries.push(`${file}:${sha256Text(content)}`);
677
688
  }
678
689
  return sha256Text(entries.join("\n"));
@@ -682,30 +693,23 @@ async function repositoryFingerprint(root, files, facts, force) {
682
693
  async function indexWorkspace(db, workspaceId, options) {
683
694
  const started = (/* @__PURE__ */ new Date()).toISOString();
684
695
  const repos = options.repo ? [repoByName(db, options.repo)].filter((r) => r !== void 0) : listRepositories(db);
685
- const runId = Number(
686
- db.prepare(
687
- "INSERT INTO index_runs(workspace_id,started_at,status,repo_count,file_count,diagnostic_count) VALUES(?,?,?,?,?,?) RETURNING id"
688
- ).get(workspaceId, started, "running", repos.length, 0, 0)?.id
689
- );
696
+ 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);
690
697
  let fileCount = 0;
691
698
  let diagnosticCount = 0;
692
699
  let skippedCount = 0;
693
- for (const repo of repos) {
694
- const result = await indexRepository(db, repo, options.force);
695
- fileCount += result.fileCount;
696
- diagnosticCount += result.diagnosticCount;
697
- skippedCount += result.skipped ? 1 : 0;
700
+ try {
701
+ for (const repo of repos) {
702
+ const result = await indexRepository(db, repo, options.force);
703
+ fileCount += result.fileCount;
704
+ diagnosticCount += result.diagnosticCount;
705
+ skippedCount += result.skipped ? 1 : 0;
706
+ }
707
+ 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);
708
+ return { repoCount: repos.length, indexedCount: repos.length - skippedCount, skippedCount, fileCount, diagnosticCount };
709
+ } catch (error) {
710
+ 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);
711
+ throw error;
698
712
  }
699
- db.prepare(
700
- "UPDATE index_runs SET finished_at=?, status=?, file_count=?, diagnostic_count=? WHERE id=?"
701
- ).run(
702
- (/* @__PURE__ */ new Date()).toISOString(),
703
- diagnosticCount ? "partial" : "success",
704
- fileCount,
705
- diagnosticCount,
706
- runId
707
- );
708
- return { repoCount: repos.length, indexedCount: repos.length - skippedCount, skippedCount, fileCount, diagnosticCount };
709
713
  }
710
714
 
711
715
  // src/trace/selectors.ts
@@ -841,7 +845,7 @@ function createProgram() {
841
845
  (opts) => void withWorkspace(opts.workspace, (db, workspaceId) => {
842
846
  const r = linkWorkspace(db, workspaceId);
843
847
  process.stdout.write(
844
- `Linked ${r.edgeCount} edges: ${r.resolvedCount} remote resolved, ${r.unresolvedCount} remote unresolved, ${r.ambiguousCount} ambiguous, ${r.dynamicCount} dynamic, ${r.terminalCount} terminal
848
+ `Linked ${r.edgeCount} edges: ${r.resolvedCount} remote resolved, ${r.unresolvedCount} remote unresolved, ${r.ambiguousCount} remote ambiguous, ${r.dynamicCount} dynamic, ${r.terminalCount} terminal, ${r.dependencyResolvedCount} dependency resolved, ${r.dependencyAmbiguousCount} dependency ambiguous, ${r.implementationResolvedCount} implementation resolved, ${r.implementationAmbiguousCount} implementation ambiguous
845
849
  `
846
850
  );
847
851
  }).catch(fail)
@@ -991,7 +995,16 @@ function createProgram() {
991
995
  WHERE NOT EXISTS (SELECT 1 FROM search_index)
992
996
  UNION ALL
993
997
  SELECT 'error','foreign_key_violation','SQLite foreign_key_check reported integrity failures',NULL,NULL
994
- WHERE EXISTS (SELECT 1 FROM pragma_foreign_key_check)`
998
+ WHERE EXISTS (SELECT 1 FROM pragma_foreign_key_check)
999
+ UNION ALL
1000
+ SELECT 'warning','legacy_schema_weaker_foreign_keys','Legacy table lacks fresh-schema foreign-key metadata; rebuild the database or re-run init/index in a new database',NULL,NULL
1001
+ WHERE (SELECT COUNT(*) FROM pragma_foreign_key_list('graph_edges'))=0 OR (SELECT COUNT(*) FROM pragma_foreign_key_list('index_runs'))=0 OR (SELECT COUNT(*) FROM pragma_foreign_key_list('diagnostics'))=0
1002
+ UNION ALL
1003
+ SELECT 'warning','graph_stale','Graph is stale after repository fact changes; run service-flow link',NULL,NULL
1004
+ WHERE EXISTS (SELECT 1 FROM repositories WHERE graph_stale_reason IS NOT NULL)
1005
+ UNION ALL
1006
+ SELECT 'warning','index_run_abandoned','Index run ' || id || ' started at ' || started_at || ' is still running after the 60 minute abandonment threshold',NULL,NULL
1007
+ FROM index_runs WHERE status='running' AND datetime(started_at) < datetime('now','-60 minutes')`
995
1008
  ).all(Boolean(opts.strict), Boolean(opts.strict));
996
1009
  const allDiagnostics = [...diagnostics, ...health];
997
1010
  process.stdout.write(
@@ -1003,20 +1016,20 @@ function createProgram() {
1003
1016
  program.command("clean").option("--workspace <path>").option("--db-only").action(
1004
1017
  (opts) => void (async () => {
1005
1018
  const config = await loadWorkspaceConfig(opts.workspace);
1006
- const dbDir = path6.resolve(path6.dirname(config.dbPath));
1007
- const workspaceRoot = path6.resolve(config.rootPath);
1008
- await fs6.rm(config.dbPath, { force: true });
1019
+ const dbDir = path5.resolve(path5.dirname(config.dbPath));
1020
+ const workspaceRoot = path5.resolve(config.rootPath);
1021
+ await fs5.rm(config.dbPath, { force: true });
1009
1022
  if (!opts.dbOnly) {
1010
- const marker = path6.join(dbDir, ".service-flow-state");
1023
+ const marker = path5.join(dbDir, ".service-flow-state");
1011
1024
  const dangerous = /* @__PURE__ */ new Set([
1012
- path6.parse(dbDir).root,
1025
+ path5.parse(dbDir).root,
1013
1026
  "/tmp",
1014
- process.env.HOME ? path6.resolve(process.env.HOME) : "",
1027
+ process.env.HOME ? path5.resolve(process.env.HOME) : "",
1015
1028
  workspaceRoot
1016
1029
  ]);
1017
1030
  let ownsState;
1018
1031
  try {
1019
- ownsState = (await fs6.stat(marker)).isFile();
1032
+ ownsState = (await fs5.stat(marker)).isFile();
1020
1033
  } catch {
1021
1034
  ownsState = false;
1022
1035
  }
@@ -1024,7 +1037,7 @@ function createProgram() {
1024
1037
  throw new Error(
1025
1038
  `Refusing to recursively delete unowned or dangerous state directory: ${dbDir}. Use --db-only to remove only the database file.`
1026
1039
  );
1027
- await fs6.rm(dbDir, { recursive: true, force: true });
1040
+ await fs5.rm(dbDir, { recursive: true, force: true });
1028
1041
  }
1029
1042
  process.stdout.write("Cleaned service-flow state\n");
1030
1043
  })().catch(fail)