@saptools/service-flow 0.1.64 → 0.1.66
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 +18 -0
- package/README.md +67 -11
- package/TECHNICAL-NOTE.md +41 -1
- package/dist/{chunk-TBH33OYC.js → chunk-TOVX4WYH.js} +3765 -643
- package/dist/chunk-TOVX4WYH.js.map +1 -0
- package/dist/cli.js +231 -292
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +196 -1
- package/dist/index.js +7 -3
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/002-doctor-lifecycle.ts +66 -0
- package/src/cli/doctor.ts +14 -27
- package/src/cli.ts +130 -102
- package/src/db/000-call-fact-repository.ts +537 -0
- package/src/db/001-fact-lifecycle.ts +111 -0
- package/src/db/migrations.ts +16 -1
- package/src/db/repositories.ts +1 -315
- package/src/db/schema.ts +4 -2
- package/src/index.ts +22 -0
- package/src/linker/004-event-subscription-handler-linker.ts +300 -0
- package/src/linker/cross-repo-linker.ts +23 -2
- package/src/output/001-compact-json-output.ts +6 -0
- package/src/parsers/imported-wrapper-parser.ts +4 -0
- package/src/parsers/outbound-call-parser.ts +11 -1
- package/src/parsers/symbol-parser.ts +110 -2
- package/src/trace/010-traversal-scope.ts +188 -0
- package/src/trace/011-event-subscriber-traversal.ts +366 -0
- package/src/trace/012-trace-graph-lookups.ts +74 -0
- package/src/trace/013-trace-root-scopes.ts +279 -0
- package/src/trace/014-compact-contract.ts +347 -0
- package/src/trace/015-trace-edge-recorder.ts +336 -0
- package/src/trace/016-compact-projector.ts +697 -0
- package/src/trace/017-trace-context.ts +378 -0
- package/src/trace/018-compact-trace.ts +87 -0
- package/src/trace/019-trace-edge-semantics.ts +328 -0
- package/src/trace/020-compact-field-projection.ts +387 -0
- package/src/trace/dynamic-branches.ts +35 -13
- package/src/trace/trace-engine.ts +271 -245
- package/src/types.ts +10 -0
- package/src/version.ts +1 -1
- package/dist/chunk-TBH33OYC.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
+
ANALYZER_VERSION,
|
|
4
|
+
VERSION,
|
|
3
5
|
classifyODataPathIntent,
|
|
6
|
+
classifyOutboundCallsInSource,
|
|
4
7
|
clearRepoFacts,
|
|
8
|
+
compactTrace,
|
|
5
9
|
containsSupportedOutboundCall,
|
|
6
10
|
discoverRepositories,
|
|
11
|
+
factLifecycleDiagnostic,
|
|
7
12
|
getWorkspace,
|
|
8
13
|
handlerMethodIsExecutable,
|
|
9
14
|
implementationHintSuggestions,
|
|
@@ -19,6 +24,7 @@ import {
|
|
|
19
24
|
listRepositories,
|
|
20
25
|
loadPackageJsonSnapshot,
|
|
21
26
|
loadRepositorySourceContext,
|
|
27
|
+
migrate,
|
|
22
28
|
normalizeODataOperationInvocationPath,
|
|
23
29
|
normalizePath,
|
|
24
30
|
parseCdsFile,
|
|
@@ -30,15 +36,16 @@ import {
|
|
|
30
36
|
parseServiceBindings,
|
|
31
37
|
parseVars,
|
|
32
38
|
projectBoundedInOrder,
|
|
39
|
+
redactValue,
|
|
33
40
|
reposByName,
|
|
34
41
|
selectorRepoAmbiguousDiagnostic,
|
|
35
42
|
trace,
|
|
36
43
|
upsertRepository,
|
|
37
44
|
upsertWorkspace
|
|
38
|
-
} from "./chunk-
|
|
45
|
+
} from "./chunk-TOVX4WYH.js";
|
|
39
46
|
|
|
40
47
|
// src/cli.ts
|
|
41
|
-
import { Command } from "commander";
|
|
48
|
+
import { Command, Option } from "commander";
|
|
42
49
|
|
|
43
50
|
// src/config/defaults.ts
|
|
44
51
|
var DEFAULT_IGNORES = [
|
|
@@ -108,212 +115,6 @@ function createWorkspaceConfig(rootPath, dbPath, ignore = [...DEFAULT_IGNORES])
|
|
|
108
115
|
// src/db/connection.ts
|
|
109
116
|
import fs2 from "fs";
|
|
110
117
|
import path2 from "path";
|
|
111
|
-
|
|
112
|
-
// src/db/schema.ts
|
|
113
|
-
var schemaTablesSql = `
|
|
114
|
-
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);
|
|
115
|
-
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);
|
|
116
|
-
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);
|
|
117
|
-
CREATE TABLE IF NOT EXISTS cds_requires (id INTEGER PRIMARY KEY, repo_id INTEGER NOT NULL, alias TEXT NOT NULL, kind TEXT, model TEXT, destination TEXT, service_path TEXT, request_timeout INTEGER, raw_json TEXT NOT NULL, FOREIGN KEY(repo_id) REFERENCES repositories(id) ON DELETE CASCADE);
|
|
118
|
-
CREATE TABLE IF NOT EXISTS cds_services (id INTEGER PRIMARY KEY, repo_id INTEGER NOT NULL, namespace TEXT, service_name TEXT NOT NULL, qualified_name TEXT NOT NULL, service_path TEXT NOT NULL, is_extend INTEGER NOT NULL, source_file TEXT NOT NULL, source_line INTEGER NOT NULL, extension_local_ref TEXT, extension_imported_symbol TEXT, extension_local_alias TEXT, extension_module_specifier TEXT, extension_import_kind TEXT, extension_base_service_id INTEGER, extension_base_status TEXT, FOREIGN KEY(repo_id) REFERENCES repositories(id) ON DELETE CASCADE, FOREIGN KEY(extension_base_service_id) REFERENCES cds_services(id) ON DELETE SET NULL);
|
|
119
|
-
CREATE TABLE IF NOT EXISTS cds_operations (id INTEGER PRIMARY KEY, service_id INTEGER NOT NULL, operation_type TEXT NOT NULL, operation_name TEXT NOT NULL, operation_path TEXT NOT NULL, params_json TEXT NOT NULL, return_type TEXT, source_file TEXT NOT NULL, source_line INTEGER NOT NULL, provenance TEXT NOT NULL DEFAULT 'direct', base_operation_id INTEGER, FOREIGN KEY(service_id) REFERENCES cds_services(id) ON DELETE CASCADE, FOREIGN KEY(base_operation_id) REFERENCES cds_operations(id) ON DELETE SET NULL);
|
|
120
|
-
CREATE TABLE IF NOT EXISTS symbols (id INTEGER PRIMARY KEY, repo_id INTEGER NOT NULL, file_id INTEGER, kind TEXT NOT NULL, name TEXT NOT NULL, qualified_name TEXT NOT NULL, exported INTEGER NOT NULL, start_line INTEGER NOT NULL, end_line INTEGER NOT NULL, start_offset INTEGER, end_offset INTEGER, source_file TEXT, exported_name TEXT, evidence_json TEXT, FOREIGN KEY(repo_id) REFERENCES repositories(id) ON DELETE CASCADE, FOREIGN KEY(file_id) REFERENCES files(id) ON DELETE CASCADE);
|
|
121
|
-
CREATE TABLE IF NOT EXISTS handler_classes (id INTEGER PRIMARY KEY, repo_id INTEGER NOT NULL, symbol_id INTEGER, class_name TEXT NOT NULL, source_file TEXT NOT NULL, source_line INTEGER NOT NULL, FOREIGN KEY(repo_id) REFERENCES repositories(id) ON DELETE CASCADE, FOREIGN KEY(symbol_id) REFERENCES symbols(id) ON DELETE SET NULL);
|
|
122
|
-
CREATE TABLE IF NOT EXISTS handler_methods (id INTEGER PRIMARY KEY, handler_class_id INTEGER NOT NULL, method_name TEXT NOT NULL, decorator_kind TEXT NOT NULL, decorator_value TEXT, decorator_raw_expression TEXT NOT NULL, decorator_resolution_json TEXT NOT NULL DEFAULT '{}', source_file TEXT NOT NULL, source_line INTEGER NOT NULL, FOREIGN KEY(handler_class_id) REFERENCES handler_classes(id) ON DELETE CASCADE);
|
|
123
|
-
CREATE TABLE IF NOT EXISTS handler_registrations (id INTEGER PRIMARY KEY, repo_id INTEGER NOT NULL, handler_class_id INTEGER, class_name TEXT, import_source TEXT, registration_file TEXT NOT NULL, registration_line INTEGER NOT NULL, registration_kind TEXT NOT NULL, confidence REAL NOT NULL, FOREIGN KEY(repo_id) REFERENCES repositories(id) ON DELETE CASCADE, FOREIGN KEY(handler_class_id) REFERENCES handler_classes(id) ON DELETE SET NULL);
|
|
124
|
-
CREATE TABLE IF NOT EXISTS service_bindings (id INTEGER PRIMARY KEY, repo_id INTEGER NOT NULL, symbol_id INTEGER, variable_name TEXT NOT NULL, alias TEXT, alias_expr TEXT, destination_expr TEXT, service_path_expr TEXT, is_dynamic INTEGER NOT NULL, placeholders_json TEXT NOT NULL, source_file TEXT NOT NULL, source_line INTEGER NOT NULL, helper_chain_json TEXT, FOREIGN KEY(repo_id) REFERENCES repositories(id) ON DELETE CASCADE, FOREIGN KEY(symbol_id) REFERENCES symbols(id) ON DELETE SET NULL);
|
|
125
|
-
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);
|
|
126
|
-
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);
|
|
127
|
-
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);
|
|
128
|
-
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);
|
|
129
|
-
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);
|
|
130
|
-
`;
|
|
131
|
-
var schemaIndexesSql = `
|
|
132
|
-
CREATE INDEX IF NOT EXISTS idx_repo_name ON repositories(name);
|
|
133
|
-
CREATE INDEX IF NOT EXISTS idx_service_path ON cds_services(service_path);
|
|
134
|
-
CREATE INDEX IF NOT EXISTS idx_extension_import ON cds_services(extension_module_specifier, extension_imported_symbol, is_extend);
|
|
135
|
-
CREATE INDEX IF NOT EXISTS idx_operation_name ON cds_operations(operation_name, operation_path);
|
|
136
|
-
CREATE INDEX IF NOT EXISTS idx_operation_base ON cds_operations(base_operation_id);
|
|
137
|
-
CREATE INDEX IF NOT EXISTS idx_calls_repo ON outbound_calls(repo_id, call_type);
|
|
138
|
-
CREATE INDEX IF NOT EXISTS idx_symbol_calls_caller ON symbol_calls(repo_id, caller_symbol_id);
|
|
139
|
-
CREATE VIRTUAL TABLE IF NOT EXISTS search_index USING fts5(kind, name, path, repo);
|
|
140
|
-
`;
|
|
141
|
-
var schemaSql = `${schemaTablesSql}
|
|
142
|
-
${schemaIndexesSql}`;
|
|
143
|
-
|
|
144
|
-
// src/db/migrations.ts
|
|
145
|
-
var CURRENT_SCHEMA_VERSION = 11;
|
|
146
|
-
var columns = {
|
|
147
|
-
handler_methods: [
|
|
148
|
-
{ name: "decorator_resolution_json", ddl: "ALTER TABLE handler_methods ADD COLUMN decorator_resolution_json TEXT NOT NULL DEFAULT '{}'" }
|
|
149
|
-
],
|
|
150
|
-
service_bindings: [
|
|
151
|
-
{ name: "helper_chain_json", ddl: "ALTER TABLE service_bindings ADD COLUMN helper_chain_json TEXT" },
|
|
152
|
-
{ name: "alias_expr", ddl: "ALTER TABLE service_bindings ADD COLUMN alias_expr TEXT" }
|
|
153
|
-
],
|
|
154
|
-
repositories: [
|
|
155
|
-
{ name: "fingerprint", ddl: "ALTER TABLE repositories ADD COLUMN fingerprint TEXT" },
|
|
156
|
-
{ name: "fact_generation", ddl: "ALTER TABLE repositories ADD COLUMN fact_generation INTEGER NOT NULL DEFAULT 0" },
|
|
157
|
-
{ name: "graph_generation", ddl: "ALTER TABLE repositories ADD COLUMN graph_generation INTEGER NOT NULL DEFAULT 0" },
|
|
158
|
-
{ name: "graph_stale_reason", ddl: "ALTER TABLE repositories ADD COLUMN graph_stale_reason TEXT" },
|
|
159
|
-
{ name: "graph_stale_at", ddl: "ALTER TABLE repositories ADD COLUMN graph_stale_at TEXT" },
|
|
160
|
-
{ name: "fact_analyzer_version", ddl: "ALTER TABLE repositories ADD COLUMN fact_analyzer_version TEXT DEFAULT 'legacy'" }
|
|
161
|
-
],
|
|
162
|
-
graph_edges: [
|
|
163
|
-
{ name: "status", ddl: "ALTER TABLE graph_edges ADD COLUMN status TEXT NOT NULL DEFAULT 'unresolved'" },
|
|
164
|
-
{ name: "generation", ddl: "ALTER TABLE graph_edges ADD COLUMN generation INTEGER NOT NULL DEFAULT 0" }
|
|
165
|
-
],
|
|
166
|
-
handler_registrations: [
|
|
167
|
-
{ name: "class_name", ddl: "ALTER TABLE handler_registrations ADD COLUMN class_name TEXT" },
|
|
168
|
-
{ name: "import_source", ddl: "ALTER TABLE handler_registrations ADD COLUMN import_source TEXT" }
|
|
169
|
-
],
|
|
170
|
-
symbols: [
|
|
171
|
-
{ name: "start_offset", ddl: "ALTER TABLE symbols ADD COLUMN start_offset INTEGER" },
|
|
172
|
-
{ name: "end_offset", ddl: "ALTER TABLE symbols ADD COLUMN end_offset INTEGER" },
|
|
173
|
-
{ name: "source_file", ddl: "ALTER TABLE symbols ADD COLUMN source_file TEXT" },
|
|
174
|
-
{ name: "exported_name", ddl: "ALTER TABLE symbols ADD COLUMN exported_name TEXT" },
|
|
175
|
-
{ name: "evidence_json", ddl: "ALTER TABLE symbols ADD COLUMN evidence_json TEXT" }
|
|
176
|
-
],
|
|
177
|
-
cds_services: [
|
|
178
|
-
{ name: "extension_local_ref", ddl: "ALTER TABLE cds_services ADD COLUMN extension_local_ref TEXT" },
|
|
179
|
-
{ name: "extension_imported_symbol", ddl: "ALTER TABLE cds_services ADD COLUMN extension_imported_symbol TEXT" },
|
|
180
|
-
{ name: "extension_local_alias", ddl: "ALTER TABLE cds_services ADD COLUMN extension_local_alias TEXT" },
|
|
181
|
-
{ name: "extension_module_specifier", ddl: "ALTER TABLE cds_services ADD COLUMN extension_module_specifier TEXT" },
|
|
182
|
-
{ name: "extension_import_kind", ddl: "ALTER TABLE cds_services ADD COLUMN extension_import_kind TEXT" },
|
|
183
|
-
{ name: "extension_base_service_id", ddl: "ALTER TABLE cds_services ADD COLUMN extension_base_service_id INTEGER" },
|
|
184
|
-
{ name: "extension_base_status", ddl: "ALTER TABLE cds_services ADD COLUMN extension_base_status TEXT" }
|
|
185
|
-
],
|
|
186
|
-
cds_operations: [
|
|
187
|
-
{ name: "provenance", ddl: "ALTER TABLE cds_operations ADD COLUMN provenance TEXT NOT NULL DEFAULT 'direct'" },
|
|
188
|
-
{ name: "base_operation_id", ddl: "ALTER TABLE cds_operations ADD COLUMN base_operation_id INTEGER" }
|
|
189
|
-
],
|
|
190
|
-
outbound_calls: [
|
|
191
|
-
{ name: "local_service_name", ddl: "ALTER TABLE outbound_calls ADD COLUMN local_service_name TEXT" },
|
|
192
|
-
{ name: "local_service_lookup", ddl: "ALTER TABLE outbound_calls ADD COLUMN local_service_lookup TEXT" },
|
|
193
|
-
{ name: "alias_chain_json", ddl: "ALTER TABLE outbound_calls ADD COLUMN alias_chain_json TEXT" },
|
|
194
|
-
{ name: "evidence_json", ddl: "ALTER TABLE outbound_calls ADD COLUMN evidence_json TEXT" },
|
|
195
|
-
{ name: "external_target_kind", ddl: "ALTER TABLE outbound_calls ADD COLUMN external_target_kind TEXT" },
|
|
196
|
-
{ name: "external_target_id", ddl: "ALTER TABLE outbound_calls ADD COLUMN external_target_id TEXT" },
|
|
197
|
-
{ name: "external_target_label", ddl: "ALTER TABLE outbound_calls ADD COLUMN external_target_label TEXT" },
|
|
198
|
-
{ name: "external_target_dynamic", ddl: "ALTER TABLE outbound_calls ADD COLUMN external_target_dynamic INTEGER NOT NULL DEFAULT 0" }
|
|
199
|
-
],
|
|
200
|
-
index_runs: [
|
|
201
|
-
{ name: "error_message", ddl: "ALTER TABLE index_runs ADD COLUMN error_message TEXT" },
|
|
202
|
-
{ name: "owner_pid", ddl: "ALTER TABLE index_runs ADD COLUMN owner_pid INTEGER" }
|
|
203
|
-
]
|
|
204
|
-
};
|
|
205
|
-
function hasColumn(db, table, column) {
|
|
206
|
-
return db.prepare(`PRAGMA table_info(${table})`).all().some((row) => row.name === column);
|
|
207
|
-
}
|
|
208
|
-
function userVersion(db) {
|
|
209
|
-
const row = db.pragma("user_version")[0];
|
|
210
|
-
return Number(row?.user_version ?? 0);
|
|
211
|
-
}
|
|
212
|
-
function addMissingColumns(db) {
|
|
213
|
-
for (const [table, tableColumns] of Object.entries(columns)) {
|
|
214
|
-
for (const column of tableColumns) {
|
|
215
|
-
if (!hasColumn(db, table, column.name)) db.prepare(column.ddl).run();
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
function normalizeLegacyStatus(db) {
|
|
220
|
-
db.prepare("UPDATE graph_edges SET status=CASE WHEN edge_type='REMOTE_CALL_RESOLVES_TO_OPERATION' THEN 'resolved' WHEN edge_type IN ('HANDLER_RUNS_DB_QUERY','HANDLER_CALLS_EXTERNAL_HTTP','HANDLER_EMITS_EVENT','EVENT_CONSUMED_BY_HANDLER') THEN 'terminal' WHEN edge_type='DYNAMIC_EDGE_CANDIDATE' THEN 'dynamic' WHEN status='ambiguous' THEN 'ambiguous' ELSE status END").run();
|
|
221
|
-
db.prepare("UPDATE repositories SET graph_stale_reason='schema_migration_requires_relink', graph_stale_at=COALESCE(graph_stale_at, datetime('now')) WHERE EXISTS (SELECT 1 FROM graph_edges WHERE graph_edges.workspace_id=repositories.workspace_id) AND graph_generation=0").run();
|
|
222
|
-
}
|
|
223
|
-
function migrate(db) {
|
|
224
|
-
db.transaction(() => {
|
|
225
|
-
const version = userVersion(db);
|
|
226
|
-
if (version > CURRENT_SCHEMA_VERSION) throw new Error(`Unsupported future service-flow schema version ${version}`);
|
|
227
|
-
db.exec(schemaTablesSql);
|
|
228
|
-
addMissingColumns(db);
|
|
229
|
-
db.exec(schemaIndexesSql);
|
|
230
|
-
normalizeLegacyStatus(db);
|
|
231
|
-
const violations = db.pragma("foreign_key_check");
|
|
232
|
-
if (violations.length > 0) throw new Error("SQLite foreign_key_check failed during migration");
|
|
233
|
-
db.pragma(`user_version = ${CURRENT_SCHEMA_VERSION}`);
|
|
234
|
-
});
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
// package.json
|
|
238
|
-
var package_default = {
|
|
239
|
-
name: "@saptools/service-flow",
|
|
240
|
-
version: "0.1.64",
|
|
241
|
-
description: "Trace SAP CAP service-to-service flows across multi-repository workspaces with runtime-aware graph resolution",
|
|
242
|
-
type: "module",
|
|
243
|
-
publishConfig: {
|
|
244
|
-
access: "public",
|
|
245
|
-
registry: "https://registry.npmjs.org/"
|
|
246
|
-
},
|
|
247
|
-
bin: {
|
|
248
|
-
"service-flow": "dist/cli.js"
|
|
249
|
-
},
|
|
250
|
-
main: "./dist/index.js",
|
|
251
|
-
types: "./dist/index.d.ts",
|
|
252
|
-
exports: {
|
|
253
|
-
".": {
|
|
254
|
-
types: "./dist/index.d.ts",
|
|
255
|
-
import: "./dist/index.js"
|
|
256
|
-
}
|
|
257
|
-
},
|
|
258
|
-
files: [
|
|
259
|
-
"CHANGELOG.md",
|
|
260
|
-
"README.md",
|
|
261
|
-
"TECHNICAL-NOTE.md",
|
|
262
|
-
"dist",
|
|
263
|
-
"src"
|
|
264
|
-
],
|
|
265
|
-
engines: {
|
|
266
|
-
node: ">=24.0.0"
|
|
267
|
-
},
|
|
268
|
-
scripts: {
|
|
269
|
-
build: "tsup",
|
|
270
|
-
typecheck: "tsc --noEmit",
|
|
271
|
-
lint: "eslint src tests",
|
|
272
|
-
test: "vitest run tests/unit",
|
|
273
|
-
"test:unit": "vitest run tests/unit",
|
|
274
|
-
"test:e2e": "vitest run tests/e2e",
|
|
275
|
-
"test:e2e:fake": "vitest run tests/e2e"
|
|
276
|
-
},
|
|
277
|
-
keywords: [
|
|
278
|
-
"sap",
|
|
279
|
-
"cap",
|
|
280
|
-
"cds",
|
|
281
|
-
"btp",
|
|
282
|
-
"cli",
|
|
283
|
-
"service-graph",
|
|
284
|
-
"call-graph",
|
|
285
|
-
"sqlite",
|
|
286
|
-
"saptools"
|
|
287
|
-
],
|
|
288
|
-
author: "Dong Tran",
|
|
289
|
-
license: "MIT",
|
|
290
|
-
repository: {
|
|
291
|
-
type: "git",
|
|
292
|
-
url: "git+https://github.com/dongitran/saptools.git",
|
|
293
|
-
directory: "packages/service-flow"
|
|
294
|
-
},
|
|
295
|
-
homepage: "https://github.com/dongitran/saptools/tree/main/packages/service-flow#readme",
|
|
296
|
-
bugs: {
|
|
297
|
-
url: "https://github.com/dongitran/saptools/issues"
|
|
298
|
-
},
|
|
299
|
-
dependencies: {
|
|
300
|
-
commander: "13.1.0",
|
|
301
|
-
picocolors: "1.1.1",
|
|
302
|
-
typescript: "5.9.3",
|
|
303
|
-
zod: "4.4.3"
|
|
304
|
-
},
|
|
305
|
-
devDependencies: {
|
|
306
|
-
"@vitest/coverage-v8": "3.2.4",
|
|
307
|
-
tsup: "8.5.1",
|
|
308
|
-
vitest: "3.2.4"
|
|
309
|
-
}
|
|
310
|
-
};
|
|
311
|
-
|
|
312
|
-
// src/version.ts
|
|
313
|
-
var VERSION = package_default.version;
|
|
314
|
-
var ANALYZER_VERSION = package_default.version;
|
|
315
|
-
|
|
316
|
-
// src/db/connection.ts
|
|
317
118
|
var sqliteWarningFilterInstalled = false;
|
|
318
119
|
function installSqliteWarningFilter() {
|
|
319
120
|
if (sqliteWarningFilterInstalled) return;
|
|
@@ -490,6 +291,52 @@ function exportDeclarations(source) {
|
|
|
490
291
|
function isRelativeImport(value) {
|
|
491
292
|
return Boolean(value?.startsWith("."));
|
|
492
293
|
}
|
|
294
|
+
function directHandlerReferenceTarget(expression, source, imports, namespaceImports) {
|
|
295
|
+
if (ts.isIdentifier(expression)) {
|
|
296
|
+
const importSource2 = imports.get(expression.text);
|
|
297
|
+
return {
|
|
298
|
+
calleeExpression: expression.text,
|
|
299
|
+
calleeLocalName: expression.text,
|
|
300
|
+
importSource: importSource2,
|
|
301
|
+
relation: importSource2 ? isRelativeImport(importSource2) ? "relative_import" : "package_import" : "indexed_local_symbol"
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
if (!ts.isPropertyAccessExpression(expression) || expression.questionDotToken || !ts.isIdentifier(expression.expression) || !ts.isIdentifier(expression.name))
|
|
305
|
+
return void 0;
|
|
306
|
+
const objectName = expression.expression.text;
|
|
307
|
+
const memberName = expression.name.text;
|
|
308
|
+
const importSource = imports.get(objectName);
|
|
309
|
+
if (namespaceImports.has(objectName)) return {
|
|
310
|
+
calleeExpression: expression.getText(source),
|
|
311
|
+
calleeLocalName: memberName,
|
|
312
|
+
importSource,
|
|
313
|
+
relation: "relative_import_namespace_member"
|
|
314
|
+
};
|
|
315
|
+
const qualifiedName = `${objectName}.${memberName}`;
|
|
316
|
+
return {
|
|
317
|
+
calleeExpression: qualifiedName,
|
|
318
|
+
calleeLocalName: qualifiedName,
|
|
319
|
+
importSource,
|
|
320
|
+
relation: importSource ? isRelativeImport(importSource) ? "relative_import" : "package_import" : "indexed_local_symbol"
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
function handlerReferenceTarget(expression, source, imports, namespaceImports) {
|
|
324
|
+
const direct = directHandlerReferenceTarget(
|
|
325
|
+
expression,
|
|
326
|
+
source,
|
|
327
|
+
imports,
|
|
328
|
+
namespaceImports
|
|
329
|
+
);
|
|
330
|
+
if (direct) return direct;
|
|
331
|
+
if (!ts.isCallExpression(expression) || expression.questionDotToken || expression.arguments.length !== 1) return void 0;
|
|
332
|
+
const inner = directHandlerReferenceTarget(
|
|
333
|
+
expression.arguments[0],
|
|
334
|
+
source,
|
|
335
|
+
imports,
|
|
336
|
+
namespaceImports
|
|
337
|
+
);
|
|
338
|
+
return inner ? { ...inner, wrapperFunction: expression.expression.getText(source) } : void 0;
|
|
339
|
+
}
|
|
493
340
|
function isObjectFunction(node) {
|
|
494
341
|
return ts.isFunctionExpression(node) || ts.isArrowFunction(node) || ts.isMethodDeclaration(node);
|
|
495
342
|
}
|
|
@@ -655,6 +502,9 @@ async function parseExecutableSymbols(repoPath, filePath, context) {
|
|
|
655
502
|
const calls = [];
|
|
656
503
|
const imports = /* @__PURE__ */ new Map();
|
|
657
504
|
const namespaceImports = /* @__PURE__ */ new Set();
|
|
505
|
+
const eventSubscriptionOffsets = new Set(
|
|
506
|
+
classifyOutboundCallsInSource(source, sourceFile).filter((call) => call.fact.callType === "async_subscribe").map((call) => `${call.node.getStart(source)}:${call.node.getEnd()}`)
|
|
507
|
+
);
|
|
658
508
|
const exportNames = exportDeclarations(source);
|
|
659
509
|
const objectExports = /* @__PURE__ */ new Set();
|
|
660
510
|
const exportedClasses = /* @__PURE__ */ new Set();
|
|
@@ -771,6 +621,35 @@ async function parseExecutableSymbols(repoPath, filePath, context) {
|
|
|
771
621
|
const name = `event:${eventName}:${startLine}`;
|
|
772
622
|
symbols.push({ kind: "event_registration", localName: name, qualifiedName: `module:${sourceFile}#${name}`, sourceFile, startLine, endLine: lineOf(source, node.getEnd()), startOffset: node.getStart(source), endOffset: node.getEnd(), exported: false, importExportEvidence: { source: "synthetic_event_registration", eventName: eventArg.text, registrationLine: startLine, receiver } });
|
|
773
623
|
}
|
|
624
|
+
if (eventSubscriptionOffsets.has(`${node.getStart(source)}:${node.getEnd()}`)) {
|
|
625
|
+
const startLine = lineOf(source, node.getStart(source));
|
|
626
|
+
const handlerArgument = node.arguments[1];
|
|
627
|
+
const target = handlerArgument ? handlerReferenceTarget(
|
|
628
|
+
handlerArgument,
|
|
629
|
+
source,
|
|
630
|
+
imports,
|
|
631
|
+
namespaceImports
|
|
632
|
+
) : void 0;
|
|
633
|
+
const anchor = nearest(symbols, startLine);
|
|
634
|
+
if (target && anchor) calls.push({
|
|
635
|
+
callerQualifiedName: anchor.qualifiedName,
|
|
636
|
+
calleeExpression: target.calleeExpression,
|
|
637
|
+
calleeLocalName: target.calleeLocalName,
|
|
638
|
+
importSource: target.importSource,
|
|
639
|
+
sourceFile,
|
|
640
|
+
sourceLine: startLine,
|
|
641
|
+
callSiteStartOffset: node.getStart(source),
|
|
642
|
+
callSiteEndOffset: node.getEnd(),
|
|
643
|
+
callRole: "event_subscribe_handler",
|
|
644
|
+
evidence: {
|
|
645
|
+
relation: target.relation,
|
|
646
|
+
caller: anchor.qualifiedName,
|
|
647
|
+
targetName: target.calleeLocalName,
|
|
648
|
+
...target.wrapperFunction ? { wrapperFunction: target.wrapperFunction } : {},
|
|
649
|
+
factOrigin: "event_subscribe_handler_reference"
|
|
650
|
+
}
|
|
651
|
+
});
|
|
652
|
+
}
|
|
774
653
|
}
|
|
775
654
|
ts.forEachChild(node, visitEventRegistrationSymbols);
|
|
776
655
|
};
|
|
@@ -825,7 +704,7 @@ async function parseExecutableSymbols(repoPath, filePath, context) {
|
|
|
825
704
|
const ignored = loggerLike || terminalMember || ignoredFrameworkCall(callee);
|
|
826
705
|
const resolvedTarget = provenThisMethod ? thisTarget : targetName;
|
|
827
706
|
const keep = Boolean(resolvedTarget) && !ignored && (provenLocal || provenThisMethod || provenRelativeImport || provenClassInstance || provenPackageImport);
|
|
828
|
-
if (keep) calls.push({ callerQualifiedName: caller.qualifiedName, calleeExpression: callee.expression, calleeLocalName: resolvedTarget, receiverLocalName: callee.member ? callee.local ?? callee.receiver : void 0, importSource, sourceFile, sourceLine: line, evidence: { relation: instance ? "class_instance_method" : proxy ? "relative_import_proxy_member" : packageImport ? "package_import" : namespaceMember ? "relative_import_namespace_member" : importSource ? "relative_import" : provenThisMethod ? "indexed_this_method" : "indexed_local_symbol", caller: caller.qualifiedName, targetName: resolvedTarget, instanceVariable: instance ? instance.propertyName ?? callee.local : void 0, className: instance?.className, methodName: instance ? callee.member : void 0, classImportSource: instance?.importSource, callArguments: argumentEvidence(node.arguments, source), proxyVariableName: proxy?.variableName, factory: proxy?.factory, factoryExpression: proxy?.factory, factoryImportSource: proxy?.importSource, candidateStrategy: instance ? instance.importSource ? "relative_import_class_instance_method" : "same_file_class_instance_method" : proxy ? "proxy_member_exact_export_or_unique_member" : void 0 } });
|
|
707
|
+
if (keep) calls.push({ callerQualifiedName: caller.qualifiedName, calleeExpression: callee.expression, calleeLocalName: resolvedTarget, receiverLocalName: callee.member ? callee.local ?? callee.receiver : void 0, importSource, sourceFile, sourceLine: line, callSiteStartOffset: node.getStart(source), callSiteEndOffset: node.getEnd(), callRole: "ordinary_call", evidence: { relation: instance ? "class_instance_method" : proxy ? "relative_import_proxy_member" : packageImport ? "package_import" : namespaceMember ? "relative_import_namespace_member" : importSource ? "relative_import" : provenThisMethod ? "indexed_this_method" : "indexed_local_symbol", caller: caller.qualifiedName, targetName: resolvedTarget, instanceVariable: instance ? instance.propertyName ?? callee.local : void 0, className: instance?.className, methodName: instance ? callee.member : void 0, classImportSource: instance?.importSource, callArguments: argumentEvidence(node.arguments, source), proxyVariableName: proxy?.variableName, factory: proxy?.factory, factoryExpression: proxy?.factory, factoryImportSource: proxy?.importSource, candidateStrategy: instance ? instance.importSource ? "relative_import_class_instance_method" : "same_file_class_instance_method" : proxy ? "proxy_member_exact_export_or_unique_member" : void 0 } });
|
|
829
708
|
}
|
|
830
709
|
}
|
|
831
710
|
ts.forEachChild(node, visitCalls);
|
|
@@ -1258,19 +1137,74 @@ function upperFirst(value) {
|
|
|
1258
1137
|
return value ? `${value[0]?.toUpperCase() ?? ""}${value.slice(1)}` : value;
|
|
1259
1138
|
}
|
|
1260
1139
|
|
|
1261
|
-
// src/cli/doctor.ts
|
|
1262
|
-
function linkUpgradeWarnings(db) {
|
|
1263
|
-
|
|
1140
|
+
// src/cli/002-doctor-lifecycle.ts
|
|
1141
|
+
function linkUpgradeWarnings(db, workspaceId) {
|
|
1142
|
+
const lifecycle = factLifecycleDiagnostic(db, workspaceId);
|
|
1143
|
+
if (lifecycle) return [lifecycle];
|
|
1144
|
+
return [
|
|
1145
|
+
...schemaDriftDiagnostics(db, true, workspaceId),
|
|
1146
|
+
...analyzerVersionDiagnostics(db, true, workspaceId)
|
|
1147
|
+
].filter((item) => [
|
|
1148
|
+
"schema_legacy_columns_present",
|
|
1149
|
+
"external_target_columns_missing_data",
|
|
1150
|
+
"reindex_required_after_upgrade",
|
|
1151
|
+
"reindex_required_after_analyzer_upgrade"
|
|
1152
|
+
].includes(String(item.code)));
|
|
1153
|
+
}
|
|
1154
|
+
function schemaDriftDiagnostics(db, strict, workspaceId) {
|
|
1155
|
+
if (!strict) return [];
|
|
1156
|
+
const columns = db.prepare("PRAGMA table_info(symbols)").all();
|
|
1157
|
+
const legacy = columns.filter((row) => [
|
|
1158
|
+
"external_target_kind",
|
|
1159
|
+
"external_target_id",
|
|
1160
|
+
"external_target_label",
|
|
1161
|
+
"external_target_dynamic"
|
|
1162
|
+
].includes(String(row.name))).map((row) => row.name);
|
|
1163
|
+
const missing = db.prepare(`SELECT c.id id,c.source_file sourceFile,
|
|
1164
|
+
c.source_line sourceLine FROM outbound_calls c
|
|
1165
|
+
JOIN repositories r ON r.id=c.repo_id
|
|
1166
|
+
WHERE c.call_type='external_http'
|
|
1167
|
+
AND (? IS NULL OR r.workspace_id=?)
|
|
1168
|
+
AND (c.external_target_id IS NULL OR c.external_target_label IS NULL
|
|
1169
|
+
OR c.external_target_kind IS NULL) LIMIT 20`).all(
|
|
1170
|
+
workspaceId,
|
|
1171
|
+
workspaceId
|
|
1172
|
+
);
|
|
1173
|
+
const out = [];
|
|
1174
|
+
if (legacy.length > 0) out.push({ severity: "warning", code: "schema_legacy_columns_present", message: "Legacy external-target columns are present on symbols; run service-flow clean --db-only, then init/index/link to rebuild with the current schema.", scope: "workspace", affectedColumns: legacy, remediation: "service-flow clean --db-only && service-flow init <workspace> && service-flow index && service-flow link" });
|
|
1175
|
+
if (missing.length > 0) out.push({ severity: "warning", code: "external_target_columns_missing_data", message: "External HTTP calls are missing queryable external target metadata; reindex is required after upgrade.", scope: "workspace", affectedRows: missing, remediation: "service-flow index --force && service-flow link" });
|
|
1176
|
+
if (legacy.length > 0 || missing.length > 0) out.push({ severity: "warning", code: "reindex_required_after_upgrade", message: "This database cannot be made equivalent to a fresh index by relink alone.", scope: "workspace", remediation: "Rebuild or force reindex the workspace, then run service-flow doctor --strict." });
|
|
1177
|
+
return out;
|
|
1178
|
+
}
|
|
1179
|
+
function analyzerVersionDiagnostics(db, strict, workspaceId) {
|
|
1180
|
+
if (!strict) return [];
|
|
1181
|
+
const rows = db.prepare(`SELECT name,
|
|
1182
|
+
COALESCE(fact_analyzer_version,'legacy') factAnalyzerVersion
|
|
1183
|
+
FROM repositories WHERE index_status='indexed'
|
|
1184
|
+
AND (? IS NULL OR workspace_id=?)
|
|
1185
|
+
AND COALESCE(fact_analyzer_version,'legacy')<>?`).all(
|
|
1186
|
+
workspaceId,
|
|
1187
|
+
workspaceId,
|
|
1188
|
+
ANALYZER_VERSION
|
|
1189
|
+
);
|
|
1190
|
+
if (rows.length === 0) return [];
|
|
1191
|
+
return [{ severity: "warning", code: "reindex_required_after_analyzer_upgrade", message: "Repository facts were produced by an older or unknown analyzer; run service-flow index --force before relink to apply current parser semantics.", scope: "workspace", affectedRepositoryCount: rows.length, currentAnalyzerVersion: ANALYZER_VERSION, repositories: rows, remediation: "service-flow index --force && service-flow link" }];
|
|
1264
1192
|
}
|
|
1193
|
+
|
|
1194
|
+
// src/cli/doctor.ts
|
|
1265
1195
|
function doctorDiagnostics(db, strict, options = {}) {
|
|
1196
|
+
const lifecycle = factLifecycleDiagnostic(db, options.workspaceId);
|
|
1197
|
+
if (lifecycle?.code === "schema_upgrade_required" || lifecycle?.code === "unsupported_future_schema")
|
|
1198
|
+
return boundDoctorDiagnostics([lifecycle]);
|
|
1266
1199
|
const diagnostics = db.prepare("SELECT severity,code,message,source_file sourceFile,source_line sourceLine FROM diagnostics ORDER BY id").all();
|
|
1267
1200
|
return boundDoctorDiagnostics([
|
|
1201
|
+
...lifecycle ? [lifecycle] : [],
|
|
1268
1202
|
...diagnostics,
|
|
1269
1203
|
...healthDiagnostics(db, strict),
|
|
1270
1204
|
...remoteTargetWithoutImplementationDiagnostics(db, strict, Boolean(options.detail)),
|
|
1271
1205
|
...localServiceDiagnostics(db, strict),
|
|
1272
|
-
...schemaDriftDiagnostics(db, strict),
|
|
1273
|
-
...analyzerVersionDiagnostics(db, strict),
|
|
1206
|
+
...schemaDriftDiagnostics(db, strict, options.workspaceId),
|
|
1207
|
+
...analyzerVersionDiagnostics(db, strict, options.workspaceId),
|
|
1274
1208
|
...parserQualityDiagnostics(db, strict, options)
|
|
1275
1209
|
]);
|
|
1276
1210
|
}
|
|
@@ -1348,23 +1282,6 @@ function remoteTargetWithoutImplementationExamples(db, servicePath, operationPat
|
|
|
1348
1282
|
AND NOT EXISTS (SELECT 1 FROM graph_edges impl WHERE impl.edge_type='OPERATION_IMPLEMENTED_BY_HANDLER' AND impl.from_kind='operation' AND impl.from_id=remote.to_id)
|
|
1349
1283
|
ORDER BY r.name,c.source_file,c.source_line`).all(servicePath, operationPath);
|
|
1350
1284
|
}
|
|
1351
|
-
function schemaDriftDiagnostics(db, strict) {
|
|
1352
|
-
if (!strict) return [];
|
|
1353
|
-
const columns2 = db.prepare("PRAGMA table_info(symbols)").all();
|
|
1354
|
-
const legacy = columns2.filter((row) => ["external_target_kind", "external_target_id", "external_target_label", "external_target_dynamic"].includes(String(row.name))).map((row) => row.name);
|
|
1355
|
-
const missing = db.prepare("SELECT id id,source_file sourceFile,source_line sourceLine FROM outbound_calls WHERE call_type='external_http' AND (external_target_id IS NULL OR external_target_label IS NULL OR external_target_kind IS NULL) LIMIT 20").all();
|
|
1356
|
-
const out = [];
|
|
1357
|
-
if (legacy.length > 0) out.push({ severity: "warning", code: "schema_legacy_columns_present", message: "Legacy external-target columns are present on symbols; run service-flow clean --db-only, then init/index/link to rebuild with the current schema.", scope: "workspace", affectedColumns: legacy, remediation: "service-flow clean --db-only && service-flow init <workspace> && service-flow index && service-flow link" });
|
|
1358
|
-
if (missing.length > 0) out.push({ severity: "warning", code: "external_target_columns_missing_data", message: "External HTTP calls are missing queryable external target metadata; reindex is required after upgrade.", scope: "workspace", affectedRows: missing, remediation: "service-flow index --force && service-flow link" });
|
|
1359
|
-
if (legacy.length > 0 || missing.length > 0) out.push({ severity: "warning", code: "reindex_required_after_upgrade", message: "This database cannot be made equivalent to a fresh index by relink alone.", scope: "workspace", remediation: "Rebuild or force reindex the workspace, then run service-flow doctor --strict." });
|
|
1360
|
-
return out;
|
|
1361
|
-
}
|
|
1362
|
-
function analyzerVersionDiagnostics(db, strict) {
|
|
1363
|
-
if (!strict) return [];
|
|
1364
|
-
const rows = db.prepare("SELECT name,COALESCE(fact_analyzer_version,'legacy') factAnalyzerVersion FROM repositories WHERE index_status='indexed' AND COALESCE(fact_analyzer_version,'legacy')<>?").all(ANALYZER_VERSION);
|
|
1365
|
-
if (rows.length === 0) return [];
|
|
1366
|
-
return [{ severity: "warning", code: "reindex_required_after_analyzer_upgrade", message: "Repository facts were produced by an older or unknown analyzer; run service-flow index --force before relink to apply current parser semantics.", scope: "workspace", affectedRepositoryCount: rows.length, currentAnalyzerVersion: ANALYZER_VERSION, repositories: rows, remediation: "service-flow index --force && service-flow link" }];
|
|
1367
|
-
}
|
|
1368
1285
|
function localServiceDiagnostics(db, strict) {
|
|
1369
1286
|
const rows = db.prepare("SELECT e.status status,e.unresolved_reason reason,e.evidence_json evidenceJson FROM graph_edges e JOIN outbound_calls c ON e.from_kind='call' AND c.id=CAST(e.from_id AS INTEGER) WHERE c.call_type='local_service_call'").all();
|
|
1370
1287
|
const implementationContext = rows.filter((row) => row.status === "resolved" && String(row.evidenceJson ?? "").includes("implementation_context_caller_ownership")).length;
|
|
@@ -2114,6 +2031,12 @@ function createStdoutWriter(stream, onUnexpectedError) {
|
|
|
2114
2031
|
return writer;
|
|
2115
2032
|
}
|
|
2116
2033
|
|
|
2034
|
+
// src/output/001-compact-json-output.ts
|
|
2035
|
+
function renderCompactJson(value) {
|
|
2036
|
+
return `${JSON.stringify(redactValue(value))}
|
|
2037
|
+
`;
|
|
2038
|
+
}
|
|
2039
|
+
|
|
2117
2040
|
// src/cli/000-clean.ts
|
|
2118
2041
|
import fs6 from "fs/promises";
|
|
2119
2042
|
import path6 from "path";
|
|
@@ -2175,6 +2098,8 @@ async function markCleanClaimFailed(dbPath, runId, error) {
|
|
|
2175
2098
|
|
|
2176
2099
|
// src/cli.ts
|
|
2177
2100
|
var stdout = createStdoutWriter(process.stdout, fail);
|
|
2101
|
+
var TRACE_FORMATS = ["table", "json", "mermaid", "compact-json"];
|
|
2102
|
+
var GRAPH_FORMATS = ["mermaid", "json", "compact-json"];
|
|
2178
2103
|
function writeStdout(value) {
|
|
2179
2104
|
stdout.write(value);
|
|
2180
2105
|
}
|
|
@@ -2252,6 +2177,72 @@ function selectRepository(db, selector, workspaceId) {
|
|
|
2252
2177
|
)
|
|
2253
2178
|
};
|
|
2254
2179
|
}
|
|
2180
|
+
function traceFormatOption() {
|
|
2181
|
+
return new Option("--format <format>", TRACE_FORMATS.join("|")).choices([...TRACE_FORMATS]).default("table");
|
|
2182
|
+
}
|
|
2183
|
+
function graphFormatOption() {
|
|
2184
|
+
return new Option("--format <format>", GRAPH_FORMATS.join("|")).choices([...GRAPH_FORMATS]).default("mermaid");
|
|
2185
|
+
}
|
|
2186
|
+
function writeTraceOutput(db, start, options, format) {
|
|
2187
|
+
if (format === "compact-json") {
|
|
2188
|
+
writeStdout(renderCompactJson(compactTrace(db, start, options)));
|
|
2189
|
+
return;
|
|
2190
|
+
}
|
|
2191
|
+
const result = trace(db, start, options);
|
|
2192
|
+
writeStdout(renderDetailedTrace(result, format));
|
|
2193
|
+
}
|
|
2194
|
+
function renderDetailedTrace(result, format) {
|
|
2195
|
+
if (format === "json") return renderTraceJson(result);
|
|
2196
|
+
if (format === "mermaid") return renderMermaid(result);
|
|
2197
|
+
return renderTraceTable(result);
|
|
2198
|
+
}
|
|
2199
|
+
function runTraceCommand(opts) {
|
|
2200
|
+
return withReadOnlyWorkspace(opts.workspace, (db, workspaceId) => {
|
|
2201
|
+
const start = {
|
|
2202
|
+
repo: opts.repo,
|
|
2203
|
+
servicePath: opts.service,
|
|
2204
|
+
operation: opts.operation,
|
|
2205
|
+
operationPath: opts.path,
|
|
2206
|
+
handler: opts.handler
|
|
2207
|
+
};
|
|
2208
|
+
const options = {
|
|
2209
|
+
depth: Number(opts.depth),
|
|
2210
|
+
workspaceId,
|
|
2211
|
+
vars: parseVars(opts.var),
|
|
2212
|
+
includeExternal: Boolean(opts.includeExternal),
|
|
2213
|
+
includeDb: Boolean(opts.includeDb),
|
|
2214
|
+
includeAsync: Boolean(opts.includeAsync),
|
|
2215
|
+
implementationRepo: opts.implementationRepo,
|
|
2216
|
+
implementationHints: opts.implementationHint.map(parseImplementationHint),
|
|
2217
|
+
dynamicMode: parseDynamicMode(opts.dynamicMode),
|
|
2218
|
+
maxDynamicCandidates: parsePositiveInteger(opts.maxDynamicCandidates, 5)
|
|
2219
|
+
};
|
|
2220
|
+
writeTraceOutput(db, start, options, opts.format);
|
|
2221
|
+
});
|
|
2222
|
+
}
|
|
2223
|
+
function runGraphCommand(opts) {
|
|
2224
|
+
return withReadOnlyWorkspace(opts.workspace, (db, workspaceId) => {
|
|
2225
|
+
const start = {
|
|
2226
|
+
repo: opts.repo,
|
|
2227
|
+
operation: opts.operation,
|
|
2228
|
+
servicePath: opts.service,
|
|
2229
|
+
operationPath: opts.path
|
|
2230
|
+
};
|
|
2231
|
+
const options = {
|
|
2232
|
+
depth: 100,
|
|
2233
|
+
workspaceId,
|
|
2234
|
+
includeAsync: true,
|
|
2235
|
+
includeDb: true,
|
|
2236
|
+
includeExternal: true,
|
|
2237
|
+
vars: parseVars(opts.var),
|
|
2238
|
+
implementationRepo: opts.implementationRepo,
|
|
2239
|
+
implementationHints: opts.implementationHint.map(parseImplementationHint),
|
|
2240
|
+
dynamicMode: parseDynamicMode(opts.dynamicMode),
|
|
2241
|
+
maxDynamicCandidates: parsePositiveInteger(opts.maxDynamicCandidates, 5)
|
|
2242
|
+
};
|
|
2243
|
+
writeTraceOutput(db, start, options, opts.format);
|
|
2244
|
+
});
|
|
2245
|
+
}
|
|
2255
2246
|
function createProgram() {
|
|
2256
2247
|
const program = new Command();
|
|
2257
2248
|
program.name("service-flow").description(
|
|
@@ -2275,43 +2266,15 @@ function createProgram() {
|
|
|
2275
2266
|
program.command("link").option("--workspace <path>").option("--force").action(
|
|
2276
2267
|
(opts) => void withWorkspace(opts.workspace, (db, workspaceId) => {
|
|
2277
2268
|
const r = linkWorkspace(db, workspaceId);
|
|
2278
|
-
const upgradeWarnings = linkUpgradeWarnings(db);
|
|
2269
|
+
const upgradeWarnings = linkUpgradeWarnings(db, workspaceId);
|
|
2279
2270
|
writeStdout(
|
|
2280
2271
|
`${upgradeWarnings.length ? `Warnings: ${upgradeWarnings.map((item) => String(item.code)).join(", ")}. Run service-flow doctor --strict for remediation.
|
|
2281
|
-
` : ""}Linked ${r.edgeCount} edges: ${r.remoteResolvedCount} remote operation calls resolved, ${r.localResolvedCount} local operation calls resolved, ${r.unresolvedCount} unresolved operation calls, ${r.ambiguousCount} ambiguous operation calls, ${r.dynamicCount} dynamic operation calls, ${r.terminalCount} terminal call edges, ${r.dependencyResolvedCount} dependency resolved, ${r.dependencyAmbiguousCount} dependency ambiguous, ${r.implementationResolvedCount} implementation resolved, ${r.implementationAmbiguousCount} implementation ambiguous, ${r.implementationUnresolvedCount} implementation unresolved
|
|
2272
|
+
` : ""}Linked ${r.edgeCount} edges: ${r.remoteResolvedCount} remote operation calls resolved, ${r.localResolvedCount} local operation calls resolved, ${r.unresolvedCount} unresolved operation calls, ${r.ambiguousCount} ambiguous operation calls, ${r.dynamicCount} dynamic operation calls, ${r.terminalCount} terminal call edges, ${r.dependencyResolvedCount} dependency resolved, ${r.dependencyAmbiguousCount} dependency ambiguous, ${r.implementationResolvedCount} implementation resolved, ${r.implementationAmbiguousCount} implementation ambiguous, ${r.implementationUnresolvedCount} implementation unresolved, ${r.subscriptionHandlerResolvedCount} subscription handlers resolved, ${r.subscriptionHandlerAmbiguousCount} subscription handlers ambiguous, ${r.subscriptionHandlerUnresolvedCount} subscription handlers unresolved, ${r.subscriptionHandlerMissingAssociationCount} subscription handler associations missing
|
|
2282
2273
|
`
|
|
2283
2274
|
);
|
|
2284
2275
|
}).catch(fail)
|
|
2285
2276
|
);
|
|
2286
|
-
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").
|
|
2287
|
-
(opts) => void withReadOnlyWorkspace(opts.workspace, (db, workspaceId) => {
|
|
2288
|
-
const result = trace(
|
|
2289
|
-
db,
|
|
2290
|
-
{
|
|
2291
|
-
repo: opts.repo,
|
|
2292
|
-
servicePath: opts.service,
|
|
2293
|
-
operation: opts.operation,
|
|
2294
|
-
operationPath: opts.path,
|
|
2295
|
-
handler: opts.handler
|
|
2296
|
-
},
|
|
2297
|
-
{
|
|
2298
|
-
depth: Number(opts.depth),
|
|
2299
|
-
workspaceId,
|
|
2300
|
-
vars: parseVars(opts.var),
|
|
2301
|
-
includeExternal: Boolean(opts.includeExternal),
|
|
2302
|
-
includeDb: Boolean(opts.includeDb),
|
|
2303
|
-
includeAsync: Boolean(opts.includeAsync),
|
|
2304
|
-
implementationRepo: opts.implementationRepo,
|
|
2305
|
-
implementationHints: opts.implementationHint.map(parseImplementationHint),
|
|
2306
|
-
dynamicMode: parseDynamicMode(opts.dynamicMode),
|
|
2307
|
-
maxDynamicCandidates: parsePositiveInteger(opts.maxDynamicCandidates, 5)
|
|
2308
|
-
}
|
|
2309
|
-
);
|
|
2310
|
-
writeStdout(
|
|
2311
|
-
opts.format === "json" ? renderTraceJson(result) : opts.format === "mermaid" ? renderMermaid(result) : renderTraceTable(result)
|
|
2312
|
-
);
|
|
2313
|
-
}).catch(fail)
|
|
2314
|
-
);
|
|
2277
|
+
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").addOption(traceFormatOption()).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((opts) => void runTraceCommand(opts).catch(fail));
|
|
2315
2278
|
const list = program.command("list");
|
|
2316
2279
|
list.command("repos").option("--workspace <path>").action(
|
|
2317
2280
|
(opts) => void withReadOnlyWorkspace(
|
|
@@ -2377,34 +2340,7 @@ function createProgram() {
|
|
|
2377
2340
|
writeStdout(renderJson(rows));
|
|
2378
2341
|
}).catch(fail)
|
|
2379
2342
|
);
|
|
2380
|
-
program.command("graph").option("--workspace <path>").option("--repo <name>").option("--operation <name>").option("--service <path>").option("--path <operationPath>").
|
|
2381
|
-
(opts) => void withReadOnlyWorkspace(opts.workspace, (db, workspaceId) => {
|
|
2382
|
-
const result = trace(
|
|
2383
|
-
db,
|
|
2384
|
-
{
|
|
2385
|
-
repo: opts.repo,
|
|
2386
|
-
operation: opts.operation,
|
|
2387
|
-
servicePath: opts.service,
|
|
2388
|
-
operationPath: opts.path
|
|
2389
|
-
},
|
|
2390
|
-
{
|
|
2391
|
-
depth: 100,
|
|
2392
|
-
workspaceId,
|
|
2393
|
-
includeAsync: true,
|
|
2394
|
-
includeDb: true,
|
|
2395
|
-
includeExternal: true,
|
|
2396
|
-
vars: parseVars(opts.var),
|
|
2397
|
-
implementationRepo: opts.implementationRepo,
|
|
2398
|
-
implementationHints: opts.implementationHint.map(parseImplementationHint),
|
|
2399
|
-
dynamicMode: parseDynamicMode(opts.dynamicMode),
|
|
2400
|
-
maxDynamicCandidates: parsePositiveInteger(opts.maxDynamicCandidates, 5)
|
|
2401
|
-
}
|
|
2402
|
-
);
|
|
2403
|
-
writeStdout(
|
|
2404
|
-
opts.format === "json" ? renderTraceJson(result) : renderMermaid(result)
|
|
2405
|
-
);
|
|
2406
|
-
}).catch(fail)
|
|
2407
|
-
);
|
|
2343
|
+
program.command("graph").option("--workspace <path>").option("--repo <name>").option("--operation <name>").option("--service <path>").option("--path <operationPath>").addOption(graphFormatOption()).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((opts) => void runGraphCommand(opts).catch(fail));
|
|
2408
2344
|
const inspect = program.command("inspect");
|
|
2409
2345
|
inspect.command("repo").argument("<name>").option("--workspace <path>").action(
|
|
2410
2346
|
(name, opts) => void withReadOnlyWorkspace(opts.workspace, (db, workspaceId) => {
|
|
@@ -2423,8 +2359,11 @@ function createProgram() {
|
|
|
2423
2359
|
}).catch(fail)
|
|
2424
2360
|
);
|
|
2425
2361
|
program.command("doctor").option("--workspace <path>").option("--strict").option("--detail").option("--format <format>", "json|table").action(
|
|
2426
|
-
(opts) => void withReadOnlyWorkspace(opts.workspace, (db) => {
|
|
2427
|
-
const allDiagnostics = doctorDiagnostics(db, Boolean(opts.strict), {
|
|
2362
|
+
(opts) => void withReadOnlyWorkspace(opts.workspace, (db, workspaceId) => {
|
|
2363
|
+
const allDiagnostics = doctorDiagnostics(db, Boolean(opts.strict), {
|
|
2364
|
+
detail: Boolean(opts.detail),
|
|
2365
|
+
workspaceId
|
|
2366
|
+
});
|
|
2428
2367
|
writeStdout(renderDoctorDiagnostics(allDiagnostics, opts.format));
|
|
2429
2368
|
}).catch(fail)
|
|
2430
2369
|
);
|