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