@saptools/service-flow 0.1.50 → 0.1.52
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/README.md +16 -3
- package/TECHNICAL-NOTE.md +10 -1
- package/dist/{chunk-52OUS3MO.js → chunk-PTLDSHRC.js} +2663 -363
- package/dist/chunk-PTLDSHRC.js.map +1 -0
- package/dist/cli.js +318 -510
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +59 -17
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/src/cli/000-clean.ts +82 -0
- package/src/cli.ts +97 -57
- package/src/db/connection.ts +1 -1
- package/src/db/migrations.ts +5 -3
- package/src/db/repositories.ts +120 -22
- package/src/db/schema.ts +7 -2
- package/src/index.ts +1 -1
- package/src/indexer/repository-indexer.ts +57 -29
- package/src/indexer/workspace-indexer.ts +84 -6
- package/src/linker/cross-repo-linker.ts +22 -2
- package/src/linker/service-resolver.ts +15 -4
- package/src/output/table-output.ts +56 -3
- package/src/parsers/cds-parser.ts +8 -2
- package/src/parsers/decorator-parser.ts +382 -48
- package/src/parsers/handler-registration-parser.ts +38 -21
- package/src/parsers/imported-wrapper-parser.ts +18 -5
- package/src/parsers/outbound-call-parser.ts +25 -8
- package/src/parsers/package-json-parser.ts +36 -11
- package/src/parsers/service-binding-parser-helpers.ts +8 -1
- package/src/parsers/service-binding-parser.ts +6 -3
- package/src/parsers/symbol-parser.ts +13 -3
- package/src/parsers/ts-project.ts +54 -0
- package/src/trace/000-dynamic-target-types.ts +84 -0
- package/src/trace/001-dynamic-identity.ts +280 -0
- package/src/trace/002-trace-diagnostics.ts +54 -0
- package/src/trace/003-dynamic-references.ts +82 -0
- package/src/trace/dynamic-branches.ts +45 -0
- package/src/trace/dynamic-targets.ts +654 -0
- package/src/trace/evidence.ts +391 -22
- package/src/trace/selectors.ts +483 -0
- package/src/trace/trace-engine.ts +101 -94
- package/src/types.ts +39 -1
- package/dist/chunk-52OUS3MO.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,15 @@ import {
|
|
|
15
29
|
parsePackageJson,
|
|
16
30
|
parseServiceBindings,
|
|
17
31
|
parseVars,
|
|
18
|
-
|
|
19
|
-
|
|
32
|
+
reposByName,
|
|
33
|
+
selectorRepoAmbiguousDiagnostic,
|
|
34
|
+
trace,
|
|
35
|
+
upsertRepository,
|
|
36
|
+
upsertWorkspace
|
|
37
|
+
} from "./chunk-PTLDSHRC.js";
|
|
20
38
|
|
|
21
39
|
// src/cli.ts
|
|
22
40
|
import { Command } from "commander";
|
|
23
|
-
import path6 from "path";
|
|
24
|
-
import fs6 from "fs/promises";
|
|
25
41
|
|
|
26
42
|
// src/config/defaults.ts
|
|
27
43
|
var DEFAULT_IGNORES = [
|
|
@@ -93,7 +109,7 @@ import fs2 from "fs";
|
|
|
93
109
|
import path2 from "path";
|
|
94
110
|
|
|
95
111
|
// src/db/schema.ts
|
|
96
|
-
var
|
|
112
|
+
var schemaTablesSql = `
|
|
97
113
|
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
114
|
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
115
|
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 +124,10 @@ CREATE TABLE IF NOT EXISTS service_bindings (id INTEGER PRIMARY KEY, repo_id INT
|
|
|
108
124
|
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
125
|
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
126
|
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);
|
|
127
|
+
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
128
|
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);
|
|
129
|
+
`;
|
|
130
|
+
var schemaIndexesSql = `
|
|
113
131
|
CREATE INDEX IF NOT EXISTS idx_repo_name ON repositories(name);
|
|
114
132
|
CREATE INDEX IF NOT EXISTS idx_service_path ON cds_services(service_path);
|
|
115
133
|
CREATE INDEX IF NOT EXISTS idx_extension_import ON cds_services(extension_module_specifier, extension_imported_symbol, is_extend);
|
|
@@ -119,9 +137,11 @@ CREATE INDEX IF NOT EXISTS idx_calls_repo ON outbound_calls(repo_id, call_type);
|
|
|
119
137
|
CREATE INDEX IF NOT EXISTS idx_symbol_calls_caller ON symbol_calls(repo_id, caller_symbol_id);
|
|
120
138
|
CREATE VIRTUAL TABLE IF NOT EXISTS search_index USING fts5(kind, name, path, repo);
|
|
121
139
|
`;
|
|
140
|
+
var schemaSql = `${schemaTablesSql}
|
|
141
|
+
${schemaIndexesSql}`;
|
|
122
142
|
|
|
123
143
|
// src/db/migrations.ts
|
|
124
|
-
var CURRENT_SCHEMA_VERSION =
|
|
144
|
+
var CURRENT_SCHEMA_VERSION = 11;
|
|
125
145
|
var columns = {
|
|
126
146
|
handler_methods: [
|
|
127
147
|
{ name: "decorator_resolution_json", ddl: "ALTER TABLE handler_methods ADD COLUMN decorator_resolution_json TEXT NOT NULL DEFAULT '{}'" }
|
|
@@ -177,7 +197,8 @@ var columns = {
|
|
|
177
197
|
{ name: "external_target_dynamic", ddl: "ALTER TABLE outbound_calls ADD COLUMN external_target_dynamic INTEGER NOT NULL DEFAULT 0" }
|
|
178
198
|
],
|
|
179
199
|
index_runs: [
|
|
180
|
-
{ name: "error_message", ddl: "ALTER TABLE index_runs ADD COLUMN error_message TEXT" }
|
|
200
|
+
{ name: "error_message", ddl: "ALTER TABLE index_runs ADD COLUMN error_message TEXT" },
|
|
201
|
+
{ name: "owner_pid", ddl: "ALTER TABLE index_runs ADD COLUMN owner_pid INTEGER" }
|
|
181
202
|
]
|
|
182
203
|
};
|
|
183
204
|
function hasColumn(db, table, column) {
|
|
@@ -202,8 +223,9 @@ function migrate(db) {
|
|
|
202
223
|
db.transaction(() => {
|
|
203
224
|
const version = userVersion(db);
|
|
204
225
|
if (version > CURRENT_SCHEMA_VERSION) throw new Error(`Unsupported future service-flow schema version ${version}`);
|
|
205
|
-
db.exec(
|
|
226
|
+
db.exec(schemaTablesSql);
|
|
206
227
|
addMissingColumns(db);
|
|
228
|
+
db.exec(schemaIndexesSql);
|
|
207
229
|
normalizeLegacyStatus(db);
|
|
208
230
|
const violations = db.pragma("foreign_key_check");
|
|
209
231
|
if (violations.length > 0) throw new Error("SQLite foreign_key_check failed during migration");
|
|
@@ -214,7 +236,7 @@ function migrate(db) {
|
|
|
214
236
|
// package.json
|
|
215
237
|
var package_default = {
|
|
216
238
|
name: "@saptools/service-flow",
|
|
217
|
-
version: "0.1.
|
|
239
|
+
version: "0.1.52",
|
|
218
240
|
description: "Trace SAP CAP service-to-service flows across multi-repository workspaces with runtime-aware graph resolution",
|
|
219
241
|
type: "module",
|
|
220
242
|
publishConfig: {
|
|
@@ -356,8 +378,8 @@ function openDatabase(dbPath, options = {}) {
|
|
|
356
378
|
},
|
|
357
379
|
transaction(fn) {
|
|
358
380
|
if (inTransaction) return fn();
|
|
359
|
-
inTransaction = true;
|
|
360
381
|
native.exec("BEGIN IMMEDIATE");
|
|
382
|
+
inTransaction = true;
|
|
361
383
|
try {
|
|
362
384
|
const result = fn();
|
|
363
385
|
native.exec("COMMIT");
|
|
@@ -385,410 +407,6 @@ function openReadOnlyDatabase(dbPath) {
|
|
|
385
407
|
return openDatabase(dbPath, { readonly: true, migrate: false });
|
|
386
408
|
}
|
|
387
409
|
|
|
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
410
|
// src/discovery/classify-repository.ts
|
|
793
411
|
import fs3 from "fs/promises";
|
|
794
412
|
import path3 from "path";
|
|
@@ -1021,9 +639,16 @@ function parameterBindings(params) {
|
|
|
1021
639
|
return [];
|
|
1022
640
|
});
|
|
1023
641
|
}
|
|
1024
|
-
async function parseExecutableSymbols(repoPath, filePath) {
|
|
1025
|
-
const
|
|
1026
|
-
const
|
|
642
|
+
async function parseExecutableSymbols(repoPath, filePath, context) {
|
|
643
|
+
const snapshot = context?.get(filePath);
|
|
644
|
+
const text = snapshot?.text ?? await fs4.readFile(path4.join(repoPath, filePath), "utf8");
|
|
645
|
+
const source = snapshot?.sourceFile() ?? ts.createSourceFile(
|
|
646
|
+
filePath,
|
|
647
|
+
text,
|
|
648
|
+
ts.ScriptTarget.Latest,
|
|
649
|
+
true,
|
|
650
|
+
filePath.endsWith(".ts") ? ts.ScriptKind.TS : ts.ScriptKind.JS
|
|
651
|
+
);
|
|
1027
652
|
const sourceFile = normalizePath(filePath);
|
|
1028
653
|
const symbols = [];
|
|
1029
654
|
const calls = [];
|
|
@@ -1199,27 +824,38 @@ async function parseExecutableSymbols(repoPath, filePath) {
|
|
|
1199
824
|
// src/utils/hashing.ts
|
|
1200
825
|
import { createHash } from "crypto";
|
|
1201
826
|
import { readFile } from "fs/promises";
|
|
1202
|
-
async function sha256File(filePath) {
|
|
1203
|
-
return createHash("sha256").update(await readFile(filePath)).digest("hex");
|
|
1204
|
-
}
|
|
1205
827
|
function sha256Text(text) {
|
|
1206
828
|
return createHash("sha256").update(text).digest("hex");
|
|
1207
829
|
}
|
|
1208
830
|
|
|
1209
831
|
// src/indexer/repository-indexer.ts
|
|
1210
|
-
async function prepareRepositoryIndex(repo, force) {
|
|
832
|
+
async function prepareRepositoryIndex(repo, force, instrumentation) {
|
|
1211
833
|
const sourceFiles = await findSourceFiles(repo.absolute_path);
|
|
1212
|
-
const
|
|
1213
|
-
|
|
834
|
+
const packageSnapshot = await loadPackageJsonSnapshot(repo.absolute_path, {
|
|
835
|
+
strict: true,
|
|
836
|
+
allowMissing: repo.package_name === null
|
|
837
|
+
});
|
|
838
|
+
const packageFacts = packageSnapshot.facts;
|
|
839
|
+
const sources = await loadRepositorySourceContext(
|
|
840
|
+
repo.absolute_path,
|
|
841
|
+
sourceFiles,
|
|
842
|
+
instrumentation
|
|
843
|
+
);
|
|
844
|
+
const fingerprint = repositoryFingerprint(
|
|
845
|
+
sources,
|
|
846
|
+
packageFacts,
|
|
847
|
+
packageSnapshot.rawText
|
|
848
|
+
);
|
|
1214
849
|
if (!force && repo.fingerprint === fingerprint) return { repo, fileCount: 0, diagnosticCount: 0, skipped: true };
|
|
850
|
+
const parsed = await parseAllSourceFacts(repo.absolute_path, sources);
|
|
1215
851
|
return {
|
|
1216
852
|
repo,
|
|
1217
853
|
packageFacts,
|
|
1218
854
|
fingerprint,
|
|
1219
855
|
kind: await classifyRepository(repo.absolute_path, packageFacts),
|
|
1220
|
-
parsed
|
|
856
|
+
parsed,
|
|
1221
857
|
fileCount: sourceFiles.length,
|
|
1222
|
-
diagnosticCount: 0,
|
|
858
|
+
diagnosticCount: parsed.handlers.filter((handler) => handler.hasHandlerDecorator && (handler.methods.length === 0 || handler.methods.some((method) => !handlerMethodIsExecutable(method)))).length,
|
|
1223
859
|
skipped: false
|
|
1224
860
|
};
|
|
1225
861
|
}
|
|
@@ -1248,21 +884,20 @@ function recordIndexFailure(db, repoId, error) {
|
|
|
1248
884
|
db.prepare("DELETE FROM diagnostics WHERE repo_id=? AND code IN ('index_failed_snapshot_preserved','source_read_failed')").run(repoId);
|
|
1249
885
|
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
886
|
}
|
|
1251
|
-
async function parseAllSourceFacts(root,
|
|
887
|
+
async function parseAllSourceFacts(root, sources) {
|
|
1252
888
|
const facts = { services: [], handlers: [], registrations: [], bindings: [], calls: [], symbols: [], symbolCalls: [], fileRecords: [] };
|
|
1253
|
-
for (const
|
|
1254
|
-
const
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
if (file.endsWith(".cds")) facts.services.push(...await parseCdsFile(root, file));
|
|
889
|
+
for (const snapshot of sources.entries()) {
|
|
890
|
+
const file = snapshot.filePath;
|
|
891
|
+
facts.fileRecords.push({ relativePath: normalizePath(file), extension: path5.extname(file), sha256: sha256Text(snapshot.text), sizeBytes: snapshot.sizeBytes });
|
|
892
|
+
if (file.endsWith(".cds")) facts.services.push(...await parseCdsFile(root, file, sources));
|
|
1258
893
|
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);
|
|
894
|
+
facts.handlers.push(...await parseDecorators(root, file, sources));
|
|
895
|
+
facts.registrations.push(...await parseHandlerRegistrations(root, file, sources));
|
|
896
|
+
facts.bindings.push(...await parseServiceBindings(root, file, sources));
|
|
897
|
+
const symbolFacts = await parseExecutableSymbols(root, file, sources);
|
|
1263
898
|
facts.symbols.push(...symbolFacts.symbols);
|
|
1264
899
|
facts.symbolCalls.push(...symbolFacts.calls);
|
|
1265
|
-
facts.calls.push(...await parseOutboundCalls(root, file));
|
|
900
|
+
facts.calls.push(...await parseOutboundCalls(root, file, sources));
|
|
1266
901
|
}
|
|
1267
902
|
}
|
|
1268
903
|
return facts;
|
|
@@ -1270,7 +905,7 @@ async function parseAllSourceFacts(root, files) {
|
|
|
1270
905
|
async function findSourceFiles(root) {
|
|
1271
906
|
const out = [];
|
|
1272
907
|
async function walk(dir, prefix = "") {
|
|
1273
|
-
const entries = await fs5.readdir(dir, { withFileTypes: true })
|
|
908
|
+
const entries = await fs5.readdir(dir, { withFileTypes: true });
|
|
1274
909
|
for (const e of entries) {
|
|
1275
910
|
const rel = prefix ? `${prefix}/${e.name}` : e.name;
|
|
1276
911
|
if (e.isDirectory()) {
|
|
@@ -1286,8 +921,7 @@ function isDefaultTestFile(relativeFile) {
|
|
|
1286
921
|
if (parts.some((part) => ["test", "tests", "__tests__"].includes(part))) return true;
|
|
1287
922
|
return /\.(test|spec)\.[jt]s$/.test(parts.at(-1) ?? "");
|
|
1288
923
|
}
|
|
1289
|
-
|
|
1290
|
-
const packageJson = await fs5.readFile(path5.join(root, "package.json"), "utf8").catch(() => "");
|
|
924
|
+
function repositoryFingerprint(sources, facts, packageJsonText) {
|
|
1291
925
|
const normalizedFacts = {
|
|
1292
926
|
analyzerVersion: ANALYZER_VERSION,
|
|
1293
927
|
packageName: facts.packageName,
|
|
@@ -1296,13 +930,11 @@ async function repositoryFingerprint(root, files, facts) {
|
|
|
1296
930
|
cdsRequires: [...facts.cdsRequires].sort((a, b) => a.alias.localeCompare(b.alias)),
|
|
1297
931
|
scripts: Object.fromEntries(Object.entries(facts.scripts).sort()),
|
|
1298
932
|
includeTests: false,
|
|
1299
|
-
packageJsonHash: sha256Text(
|
|
933
|
+
packageJsonHash: sha256Text(packageJsonText)
|
|
1300
934
|
};
|
|
1301
935
|
const entries = [`facts:${JSON.stringify(normalizedFacts)}`];
|
|
1302
|
-
for (const
|
|
1303
|
-
|
|
1304
|
-
entries.push(`${file}:${sha256Text(content)}`);
|
|
1305
|
-
}
|
|
936
|
+
for (const snapshot of sources.entries())
|
|
937
|
+
entries.push(`${snapshot.filePath}:${sha256Text(snapshot.text)}`);
|
|
1306
938
|
return sha256Text(entries.join("\n"));
|
|
1307
939
|
}
|
|
1308
940
|
|
|
@@ -1406,10 +1038,67 @@ function packageNameFromSpecifier(specifier) {
|
|
|
1406
1038
|
}
|
|
1407
1039
|
|
|
1408
1040
|
// src/indexer/workspace-indexer.ts
|
|
1041
|
+
var LEGACY_OWNER_RECOVERY_MS = 60 * 60 * 1e3;
|
|
1042
|
+
function ownerPid(value) {
|
|
1043
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : void 0;
|
|
1044
|
+
}
|
|
1045
|
+
function processIsAlive(pid) {
|
|
1046
|
+
try {
|
|
1047
|
+
process.kill(pid, 0);
|
|
1048
|
+
return true;
|
|
1049
|
+
} catch (error) {
|
|
1050
|
+
const ownerIsMissing = typeof error === "object" && error !== null && "code" in error && error.code === "ESRCH";
|
|
1051
|
+
return !ownerIsMissing;
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
function isRecoverableRun(row, now) {
|
|
1055
|
+
const pid = ownerPid(row.ownerPid);
|
|
1056
|
+
if (pid !== void 0) return !processIsAlive(pid);
|
|
1057
|
+
if (typeof row.startedAt !== "string") return false;
|
|
1058
|
+
const startedAt = Date.parse(row.startedAt);
|
|
1059
|
+
return Number.isFinite(startedAt) && now - startedAt >= LEGACY_OWNER_RECOVERY_MS;
|
|
1060
|
+
}
|
|
1061
|
+
function recoveredOwnerMessage(row) {
|
|
1062
|
+
const pid = ownerPid(row.ownerPid);
|
|
1063
|
+
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.`;
|
|
1064
|
+
}
|
|
1065
|
+
function claimIndexRun(db, workspaceId, repoCount) {
|
|
1066
|
+
try {
|
|
1067
|
+
return db.transaction(() => {
|
|
1068
|
+
const now = Date.now();
|
|
1069
|
+
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();
|
|
1070
|
+
const active = rows.find((row) => !isRecoverableRun(row, now));
|
|
1071
|
+
if (active) {
|
|
1072
|
+
const pid = ownerPid(active.ownerPid);
|
|
1073
|
+
const owner = pid === void 0 ? "an unknown owner" : `process ${pid}`;
|
|
1074
|
+
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.`);
|
|
1075
|
+
}
|
|
1076
|
+
const finish = db.prepare(
|
|
1077
|
+
"UPDATE index_runs SET finished_at=?,status='failed',error_message=? WHERE id=?"
|
|
1078
|
+
);
|
|
1079
|
+
for (const row of rows)
|
|
1080
|
+
finish.run(new Date(now).toISOString(), recoveredOwnerMessage(row), row.id);
|
|
1081
|
+
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);
|
|
1082
|
+
const runId = Number(inserted?.id);
|
|
1083
|
+
if (!Number.isSafeInteger(runId)) throw new Error("index_writer_claim_failed: SQLite did not return an index run identifier.");
|
|
1084
|
+
return runId;
|
|
1085
|
+
});
|
|
1086
|
+
} catch (error) {
|
|
1087
|
+
if (/\b(?:locked|busy)\b/i.test(errorMessage(error)))
|
|
1088
|
+
throw new Error(
|
|
1089
|
+
"index_writer_coordination_failed: SQLite remained busy beyond the bounded writer-claim interval; wait for the active publication to finish.",
|
|
1090
|
+
{ cause: error }
|
|
1091
|
+
);
|
|
1092
|
+
throw error;
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1409
1095
|
async function indexWorkspace(db, workspaceId, options) {
|
|
1410
|
-
const
|
|
1411
|
-
|
|
1412
|
-
|
|
1096
|
+
const repos = options.repo ? reposByName(db, options.repo, workspaceId) : listRepositories(db, workspaceId);
|
|
1097
|
+
if (options.repo && repos.length === 0)
|
|
1098
|
+
throw new Error(`selector_repo_not_found: no indexed repository matched ${options.repo}.`);
|
|
1099
|
+
if (options.repo && repos.length > 1)
|
|
1100
|
+
throw new Error(`selector_repo_ambiguous: repository selector ${options.repo} matched ${repos.length} repositories; use a unique repository name.`);
|
|
1101
|
+
const runId = claimIndexRun(db, workspaceId, repos.length);
|
|
1413
1102
|
let fileCount = 0;
|
|
1414
1103
|
let diagnosticCount = 0;
|
|
1415
1104
|
let skippedCount = 0;
|
|
@@ -1431,12 +1120,12 @@ async function indexWorkspace(db, workspaceId, options) {
|
|
|
1431
1120
|
}
|
|
1432
1121
|
if (options.injectDerivedMaterializationFailure) throw new Error("Injected derived materialization failure");
|
|
1433
1122
|
materializeCdsExtensionOperations(db, workspaceId);
|
|
1434
|
-
db.prepare("UPDATE index_runs SET finished_at=?, status
|
|
1123
|
+
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
1124
|
});
|
|
1436
1125
|
return { repoCount: repos.length, indexedCount: repos.length - skippedCount, skippedCount, fileCount, diagnosticCount };
|
|
1437
1126
|
} catch (error) {
|
|
1438
|
-
if (activeRepoId && preparedRows.length < repos.length) recordIndexFailure(db, activeRepoId, error);
|
|
1439
1127
|
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);
|
|
1128
|
+
if (activeRepoId && preparedRows.length < repos.length) recordIndexFailure(db, activeRepoId, error);
|
|
1440
1129
|
throw error;
|
|
1441
1130
|
}
|
|
1442
1131
|
}
|
|
@@ -2050,21 +1739,63 @@ function renderTraceTable(result) {
|
|
|
2050
1739
|
}
|
|
2051
1740
|
function diagnosticLines(diagnostic) {
|
|
2052
1741
|
const first = `${String(diagnostic.severity ?? "info")} ${String(diagnostic.code ?? "diagnostic")} ${String(diagnostic.message ?? "")}`;
|
|
2053
|
-
|
|
1742
|
+
const details = diagnosticDetailLines(diagnostic);
|
|
1743
|
+
return [first, ...[...details, ...hintLines(diagnostic)].map((hint) => ` ${hint}`)];
|
|
1744
|
+
}
|
|
1745
|
+
function diagnosticDetailLines(diagnostic) {
|
|
1746
|
+
const lines = [];
|
|
1747
|
+
if (diagnostic.sourceFile || diagnostic.sourceLine)
|
|
1748
|
+
lines.push(`at ${String(diagnostic.sourceFile ?? "")}:${String(diagnostic.sourceLine ?? "")}`);
|
|
1749
|
+
const unsupported = stringList(diagnostic.unsupportedDecoratorNames);
|
|
1750
|
+
const observed = stringList(diagnostic.observedDecoratorNames);
|
|
1751
|
+
if (unsupported.length > 0)
|
|
1752
|
+
lines.push(`unsupported decorators: ${unsupported.join(", ")}`);
|
|
1753
|
+
else if (observed.length > 0)
|
|
1754
|
+
lines.push(`observed decorators: ${observed.join(", ")}`);
|
|
1755
|
+
if (typeof diagnostic.remediation === "string")
|
|
1756
|
+
lines.push(`hint: ${diagnostic.remediation}`);
|
|
1757
|
+
return lines;
|
|
1758
|
+
}
|
|
1759
|
+
function stringList(value) {
|
|
1760
|
+
return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
|
|
2054
1761
|
}
|
|
2055
1762
|
function hintLines(evidence) {
|
|
1763
|
+
const dynamicLines = dynamicHintLines(evidence);
|
|
2056
1764
|
const suggestions = evidence.implementationHintSuggestions;
|
|
2057
|
-
if (!Array.isArray(suggestions)) return
|
|
1765
|
+
if (!Array.isArray(suggestions)) return dynamicLines;
|
|
2058
1766
|
const hints = suggestions.flatMap((item) => isRecord(item) && typeof item.cli === "string" ? [item.cli] : []);
|
|
2059
1767
|
const unique = [...new Set(hints)];
|
|
2060
1768
|
const shown = unique.slice(0, 3).map((hint) => `try ${hint}`);
|
|
2061
1769
|
if (unique.length > shown.length)
|
|
2062
1770
|
shown.push(`... ${unique.length - shown.length} more hint(s) available in JSON`);
|
|
2063
|
-
return shown;
|
|
1771
|
+
return [...dynamicLines, ...shown];
|
|
1772
|
+
}
|
|
1773
|
+
function dynamicHintLines(evidence) {
|
|
1774
|
+
const exploration = isRecord(evidence.dynamicTargetExploration) ? evidence.dynamicTargetExploration : evidence;
|
|
1775
|
+
const count = numberValue(exploration.candidateCount);
|
|
1776
|
+
if (count === 0) return [];
|
|
1777
|
+
const shown = numberValue(exploration.shownCandidateCount);
|
|
1778
|
+
const omitted = numberValue(exploration.omittedCandidateCount);
|
|
1779
|
+
const rejected = numberValue(exploration.rejectedCandidateCount);
|
|
1780
|
+
const lines = [
|
|
1781
|
+
`viable candidates: ${shown} shown, ${omitted} omitted; rejected: ${rejected}`
|
|
1782
|
+
];
|
|
1783
|
+
lines.push(...varSetHints(exploration.suggestedVarSets));
|
|
1784
|
+
if (omitted > 0 || rejected > 0 || shown < count)
|
|
1785
|
+
lines.push("use --dynamic-mode candidates --max-dynamic-candidates 20 to explore candidate branches");
|
|
1786
|
+
return lines;
|
|
1787
|
+
}
|
|
1788
|
+
function varSetHints(value) {
|
|
1789
|
+
if (!Array.isArray(value)) return [];
|
|
1790
|
+
const hints = value.flatMap((item) => isRecord(item) && typeof item.cli === "string" ? [`try ${item.cli}`] : []);
|
|
1791
|
+
return [...new Set(hints)].slice(0, 3);
|
|
2064
1792
|
}
|
|
2065
1793
|
function isRecord(value) {
|
|
2066
1794
|
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
2067
1795
|
}
|
|
1796
|
+
function numberValue(value) {
|
|
1797
|
+
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
1798
|
+
}
|
|
2068
1799
|
|
|
2069
1800
|
// src/output/json-output.ts
|
|
2070
1801
|
function renderJson(value) {
|
|
@@ -2179,6 +1910,65 @@ function renderMermaid(trace2) {
|
|
|
2179
1910
|
`;
|
|
2180
1911
|
}
|
|
2181
1912
|
|
|
1913
|
+
// src/cli/000-clean.ts
|
|
1914
|
+
import fs6 from "fs/promises";
|
|
1915
|
+
import path6 from "path";
|
|
1916
|
+
async function cleanWorkspaceState(config, dbOnly) {
|
|
1917
|
+
const dbDir = path6.resolve(path6.dirname(config.dbPath));
|
|
1918
|
+
if (!dbOnly) await assertOwnedStateDirectory(dbDir, config.rootPath);
|
|
1919
|
+
const runId = claimCleanWriter(config);
|
|
1920
|
+
try {
|
|
1921
|
+
if (dbOnly) await removeDatabaseFiles(config.dbPath);
|
|
1922
|
+
else await fs6.rm(dbDir, { recursive: true, force: true });
|
|
1923
|
+
} catch (error) {
|
|
1924
|
+
await markCleanClaimFailed(config.dbPath, runId, error);
|
|
1925
|
+
throw error;
|
|
1926
|
+
}
|
|
1927
|
+
}
|
|
1928
|
+
function claimCleanWriter(config) {
|
|
1929
|
+
const db = openDatabase(config.dbPath);
|
|
1930
|
+
try {
|
|
1931
|
+
const workspaceId = getWorkspace(db, config.rootPath)?.id ?? upsertWorkspace(db, config.rootPath, config.dbPath);
|
|
1932
|
+
return claimIndexRun(db, workspaceId, 0);
|
|
1933
|
+
} finally {
|
|
1934
|
+
db.close();
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1937
|
+
async function assertOwnedStateDirectory(dbDir, rootPath) {
|
|
1938
|
+
const marker = path6.join(dbDir, ".service-flow-state");
|
|
1939
|
+
const dangerous = /* @__PURE__ */ new Set([
|
|
1940
|
+
path6.parse(dbDir).root,
|
|
1941
|
+
"/tmp",
|
|
1942
|
+
process.env.HOME ? path6.resolve(process.env.HOME) : "",
|
|
1943
|
+
path6.resolve(rootPath)
|
|
1944
|
+
]);
|
|
1945
|
+
const ownsState = await fs6.stat(marker).then((stat) => stat.isFile()).catch(() => false);
|
|
1946
|
+
if (!ownsState || dangerous.has(dbDir))
|
|
1947
|
+
throw new Error(
|
|
1948
|
+
`Refusing to recursively delete unowned or dangerous state directory: ${dbDir}. Use --db-only to remove only the database file.`
|
|
1949
|
+
);
|
|
1950
|
+
}
|
|
1951
|
+
async function removeDatabaseFiles(dbPath) {
|
|
1952
|
+
for (const suffix of ["-wal", "-shm", "-journal"])
|
|
1953
|
+
await fs6.rm(`${dbPath}${suffix}`, { force: true });
|
|
1954
|
+
await fs6.rm(dbPath, { force: true });
|
|
1955
|
+
}
|
|
1956
|
+
async function markCleanClaimFailed(dbPath, runId, error) {
|
|
1957
|
+
const exists = await fs6.stat(dbPath).then(() => true).catch(() => false);
|
|
1958
|
+
if (!exists) return;
|
|
1959
|
+
const db = openDatabase(dbPath);
|
|
1960
|
+
try {
|
|
1961
|
+
db.prepare(`UPDATE index_runs SET finished_at=?,status='failed',
|
|
1962
|
+
error_message=? WHERE id=? AND status='running'`).run(
|
|
1963
|
+
(/* @__PURE__ */ new Date()).toISOString(),
|
|
1964
|
+
`Clean failed after writer claim: ${errorMessage(error)}`,
|
|
1965
|
+
runId
|
|
1966
|
+
);
|
|
1967
|
+
} finally {
|
|
1968
|
+
db.close();
|
|
1969
|
+
}
|
|
1970
|
+
}
|
|
1971
|
+
|
|
2182
1972
|
// src/cli.ts
|
|
2183
1973
|
async function init(workspace, options) {
|
|
2184
1974
|
const config = createWorkspaceConfig(
|
|
@@ -2233,6 +2023,27 @@ async function withReadOnlyWorkspace(workspace, fn) {
|
|
|
2233
2023
|
db.close();
|
|
2234
2024
|
}
|
|
2235
2025
|
}
|
|
2026
|
+
function selectRepository(db, selector, workspaceId) {
|
|
2027
|
+
const candidates = reposByName(db, selector, workspaceId);
|
|
2028
|
+
if (candidates.length === 1) return { repo: candidates[0] };
|
|
2029
|
+
if (candidates.length === 0) return {
|
|
2030
|
+
diagnostic: {
|
|
2031
|
+
severity: "warning",
|
|
2032
|
+
code: "selector_repo_not_found",
|
|
2033
|
+
message: `Repository selector not found: ${selector}`
|
|
2034
|
+
}
|
|
2035
|
+
};
|
|
2036
|
+
return {
|
|
2037
|
+
diagnostic: selectorRepoAmbiguousDiagnostic(
|
|
2038
|
+
selector,
|
|
2039
|
+
candidates.map((repo) => ({
|
|
2040
|
+
id: repo.id,
|
|
2041
|
+
name: repo.name,
|
|
2042
|
+
packageName: repo.package_name ?? void 0
|
|
2043
|
+
}))
|
|
2044
|
+
)
|
|
2045
|
+
};
|
|
2046
|
+
}
|
|
2236
2047
|
function createProgram() {
|
|
2237
2048
|
const program = new Command();
|
|
2238
2049
|
program.name("service-flow").description(
|
|
@@ -2264,8 +2075,8 @@ function createProgram() {
|
|
|
2264
2075
|
);
|
|
2265
2076
|
}).catch(fail)
|
|
2266
2077
|
);
|
|
2267
|
-
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, []).action(
|
|
2268
|
-
(opts) => void withReadOnlyWorkspace(opts.workspace, (db) => {
|
|
2078
|
+
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(
|
|
2079
|
+
(opts) => void withReadOnlyWorkspace(opts.workspace, (db, workspaceId) => {
|
|
2269
2080
|
const result = trace(
|
|
2270
2081
|
db,
|
|
2271
2082
|
{
|
|
@@ -2277,12 +2088,15 @@ function createProgram() {
|
|
|
2277
2088
|
},
|
|
2278
2089
|
{
|
|
2279
2090
|
depth: Number(opts.depth),
|
|
2091
|
+
workspaceId,
|
|
2280
2092
|
vars: parseVars(opts.var),
|
|
2281
2093
|
includeExternal: Boolean(opts.includeExternal),
|
|
2282
2094
|
includeDb: Boolean(opts.includeDb),
|
|
2283
2095
|
includeAsync: Boolean(opts.includeAsync),
|
|
2284
2096
|
implementationRepo: opts.implementationRepo,
|
|
2285
|
-
implementationHints: opts.implementationHint.map(parseImplementationHint)
|
|
2097
|
+
implementationHints: opts.implementationHint.map(parseImplementationHint),
|
|
2098
|
+
dynamicMode: parseDynamicMode(opts.dynamicMode),
|
|
2099
|
+
maxDynamicCandidates: parsePositiveInteger(opts.maxDynamicCandidates, 5)
|
|
2286
2100
|
}
|
|
2287
2101
|
);
|
|
2288
2102
|
process.stdout.write(
|
|
@@ -2294,9 +2108,9 @@ function createProgram() {
|
|
|
2294
2108
|
list.command("repos").option("--workspace <path>").action(
|
|
2295
2109
|
(opts) => void withReadOnlyWorkspace(
|
|
2296
2110
|
opts.workspace,
|
|
2297
|
-
(db) => process.stdout.write(
|
|
2111
|
+
(db, workspaceId) => process.stdout.write(
|
|
2298
2112
|
renderJson(
|
|
2299
|
-
listRepositories(db).map((r) => ({
|
|
2113
|
+
listRepositories(db, workspaceId).map((r) => ({
|
|
2300
2114
|
name: r.name,
|
|
2301
2115
|
kind: r.kind,
|
|
2302
2116
|
packageName: r.package_name
|
|
@@ -2306,41 +2120,45 @@ function createProgram() {
|
|
|
2306
2120
|
).catch(fail)
|
|
2307
2121
|
);
|
|
2308
2122
|
list.command("services").option("--workspace <path>").option("--repo <name>").action(
|
|
2309
|
-
(opts) => void withReadOnlyWorkspace(opts.workspace, (db) => {
|
|
2310
|
-
const
|
|
2311
|
-
if (
|
|
2312
|
-
process.stdout.write(renderJson([
|
|
2123
|
+
(opts) => void withReadOnlyWorkspace(opts.workspace, (db, workspaceId) => {
|
|
2124
|
+
const selection = opts.repo ? selectRepository(db, opts.repo, workspaceId) : {};
|
|
2125
|
+
if (selection.diagnostic) {
|
|
2126
|
+
process.stdout.write(renderJson([selection.diagnostic]));
|
|
2313
2127
|
return;
|
|
2314
2128
|
}
|
|
2129
|
+
const repo = selection.repo;
|
|
2315
2130
|
const rows = db.prepare(
|
|
2316
|
-
"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"
|
|
2317
|
-
).all(repo?.id, repo?.id);
|
|
2131
|
+
"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"
|
|
2132
|
+
).all(workspaceId, repo?.id, repo?.id);
|
|
2318
2133
|
process.stdout.write(renderJson(rows));
|
|
2319
2134
|
}).catch(fail)
|
|
2320
2135
|
);
|
|
2321
2136
|
list.command("operations").option("--workspace <path>").option("--repo <name>").option("--service <path>").action(
|
|
2322
|
-
(opts) => void withReadOnlyWorkspace(opts.workspace, (db) => {
|
|
2323
|
-
const
|
|
2324
|
-
if (
|
|
2325
|
-
process.stdout.write(renderJson([
|
|
2137
|
+
(opts) => void withReadOnlyWorkspace(opts.workspace, (db, workspaceId) => {
|
|
2138
|
+
const selection = opts.repo ? selectRepository(db, opts.repo, workspaceId) : {};
|
|
2139
|
+
if (selection.diagnostic) {
|
|
2140
|
+
process.stdout.write(renderJson([selection.diagnostic]));
|
|
2326
2141
|
return;
|
|
2327
2142
|
}
|
|
2143
|
+
const repo = selection.repo;
|
|
2328
2144
|
const rows = db.prepare(
|
|
2329
|
-
"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=?)"
|
|
2330
|
-
).all(repo?.id, repo?.id, opts.service, opts.service);
|
|
2145
|
+
"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=?)"
|
|
2146
|
+
).all(workspaceId, repo?.id, repo?.id, opts.service, opts.service);
|
|
2331
2147
|
process.stdout.write(renderJson(rows));
|
|
2332
2148
|
}).catch(fail)
|
|
2333
2149
|
);
|
|
2334
2150
|
list.command("calls").option("--workspace <path>").option("--repo <name>").option("--operation <name>").action(
|
|
2335
|
-
(opts) => void withReadOnlyWorkspace(opts.workspace, (db) => {
|
|
2336
|
-
const
|
|
2337
|
-
if (
|
|
2338
|
-
process.stdout.write(renderJson([
|
|
2151
|
+
(opts) => void withReadOnlyWorkspace(opts.workspace, (db, workspaceId) => {
|
|
2152
|
+
const selection = opts.repo ? selectRepository(db, opts.repo, workspaceId) : {};
|
|
2153
|
+
if (selection.diagnostic) {
|
|
2154
|
+
process.stdout.write(renderJson([selection.diagnostic]));
|
|
2339
2155
|
return;
|
|
2340
2156
|
}
|
|
2157
|
+
const repo = selection.repo;
|
|
2341
2158
|
const rows = db.prepare(
|
|
2342
|
-
"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 ?)"
|
|
2159
|
+
"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 ?)"
|
|
2343
2160
|
).all(
|
|
2161
|
+
workspaceId,
|
|
2344
2162
|
repo?.id,
|
|
2345
2163
|
repo?.id,
|
|
2346
2164
|
opts.operation,
|
|
@@ -2351,8 +2169,8 @@ function createProgram() {
|
|
|
2351
2169
|
process.stdout.write(renderJson(rows));
|
|
2352
2170
|
}).catch(fail)
|
|
2353
2171
|
);
|
|
2354
|
-
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, []).action(
|
|
2355
|
-
(opts) => void withReadOnlyWorkspace(opts.workspace, (db) => {
|
|
2172
|
+
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(
|
|
2173
|
+
(opts) => void withReadOnlyWorkspace(opts.workspace, (db, workspaceId) => {
|
|
2356
2174
|
const result = trace(
|
|
2357
2175
|
db,
|
|
2358
2176
|
{
|
|
@@ -2363,12 +2181,15 @@ function createProgram() {
|
|
|
2363
2181
|
},
|
|
2364
2182
|
{
|
|
2365
2183
|
depth: 100,
|
|
2184
|
+
workspaceId,
|
|
2366
2185
|
includeAsync: true,
|
|
2367
2186
|
includeDb: true,
|
|
2368
2187
|
includeExternal: true,
|
|
2369
2188
|
vars: parseVars(opts.var),
|
|
2370
2189
|
implementationRepo: opts.implementationRepo,
|
|
2371
|
-
implementationHints: opts.implementationHint.map(parseImplementationHint)
|
|
2190
|
+
implementationHints: opts.implementationHint.map(parseImplementationHint),
|
|
2191
|
+
dynamicMode: parseDynamicMode(opts.dynamicMode),
|
|
2192
|
+
maxDynamicCandidates: parsePositiveInteger(opts.maxDynamicCandidates, 5)
|
|
2372
2193
|
}
|
|
2373
2194
|
);
|
|
2374
2195
|
process.stdout.write(
|
|
@@ -2378,18 +2199,18 @@ function createProgram() {
|
|
|
2378
2199
|
);
|
|
2379
2200
|
const inspect = program.command("inspect");
|
|
2380
2201
|
inspect.command("repo").argument("<name>").option("--workspace <path>").action(
|
|
2381
|
-
(name, opts) => void withReadOnlyWorkspace(
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
)
|
|
2386
|
-
).catch(fail)
|
|
2202
|
+
(name, opts) => void withReadOnlyWorkspace(opts.workspace, (db, workspaceId) => {
|
|
2203
|
+
const selection = selectRepository(db, name, workspaceId);
|
|
2204
|
+
process.stdout.write(renderJson(
|
|
2205
|
+
selection.repo ?? selection.diagnostic ?? { error: "repo not found" }
|
|
2206
|
+
));
|
|
2207
|
+
}).catch(fail)
|
|
2387
2208
|
);
|
|
2388
2209
|
inspect.command("operation").argument("<selector>").option("--workspace <path>").action(
|
|
2389
|
-
(selector, opts) => void withReadOnlyWorkspace(opts.workspace, (db) => {
|
|
2210
|
+
(selector, opts) => void withReadOnlyWorkspace(opts.workspace, (db, workspaceId) => {
|
|
2390
2211
|
const rows = db.prepare(
|
|
2391
|
-
"SELECT
|
|
2392
|
-
).all(selector, selector);
|
|
2212
|
+
"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=?)"
|
|
2213
|
+
).all(workspaceId, selector, selector);
|
|
2393
2214
|
process.stdout.write(renderJson(rows));
|
|
2394
2215
|
}).catch(fail)
|
|
2395
2216
|
);
|
|
@@ -2402,29 +2223,7 @@ function createProgram() {
|
|
|
2402
2223
|
program.command("clean").option("--workspace <path>").option("--db-only").action(
|
|
2403
2224
|
(opts) => void (async () => {
|
|
2404
2225
|
const config = await loadWorkspaceConfig(opts.workspace);
|
|
2405
|
-
|
|
2406
|
-
const workspaceRoot = path6.resolve(config.rootPath);
|
|
2407
|
-
await fs6.rm(config.dbPath, { force: true });
|
|
2408
|
-
if (!opts.dbOnly) {
|
|
2409
|
-
const marker = path6.join(dbDir, ".service-flow-state");
|
|
2410
|
-
const dangerous = /* @__PURE__ */ new Set([
|
|
2411
|
-
path6.parse(dbDir).root,
|
|
2412
|
-
"/tmp",
|
|
2413
|
-
process.env.HOME ? path6.resolve(process.env.HOME) : "",
|
|
2414
|
-
workspaceRoot
|
|
2415
|
-
]);
|
|
2416
|
-
let ownsState;
|
|
2417
|
-
try {
|
|
2418
|
-
ownsState = (await fs6.stat(marker)).isFile();
|
|
2419
|
-
} catch {
|
|
2420
|
-
ownsState = false;
|
|
2421
|
-
}
|
|
2422
|
-
if (!ownsState || dangerous.has(dbDir))
|
|
2423
|
-
throw new Error(
|
|
2424
|
-
`Refusing to recursively delete unowned or dangerous state directory: ${dbDir}. Use --db-only to remove only the database file.`
|
|
2425
|
-
);
|
|
2426
|
-
await fs6.rm(dbDir, { recursive: true, force: true });
|
|
2427
|
-
}
|
|
2226
|
+
await cleanWorkspaceState(config, Boolean(opts.dbOnly));
|
|
2428
2227
|
process.stdout.write("Cleaned service-flow state\n");
|
|
2429
2228
|
})().catch(fail)
|
|
2430
2229
|
);
|
|
@@ -2434,6 +2233,15 @@ function collect(value, previous) {
|
|
|
2434
2233
|
previous.push(value);
|
|
2435
2234
|
return previous;
|
|
2436
2235
|
}
|
|
2236
|
+
function parseDynamicMode(value) {
|
|
2237
|
+
if (value === void 0 || value === "strict") return "strict";
|
|
2238
|
+
if (value === "candidates" || value === "infer") return value;
|
|
2239
|
+
throw new Error(`Invalid --dynamic-mode ${value}; expected strict, candidates, or infer`);
|
|
2240
|
+
}
|
|
2241
|
+
function parsePositiveInteger(value, fallback) {
|
|
2242
|
+
const parsed = Number(value);
|
|
2243
|
+
return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : fallback;
|
|
2244
|
+
}
|
|
2437
2245
|
function fail(error) {
|
|
2438
2246
|
process.stderr.write(
|
|
2439
2247
|
`${error instanceof Error ? error.message : String(error)}
|