@saptools/service-flow 0.1.32 → 0.1.34
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/README.md +4 -0
- package/TECHNICAL-NOTE.md +8 -0
- package/dist/{chunk-VT5FVMOA.js → chunk-5SR4SFSU.js} +70 -11
- package/dist/chunk-5SR4SFSU.js.map +1 -0
- package/dist/cli.js +62 -8
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-VT5FVMOA.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
parsePackageJson,
|
|
13
13
|
parseServiceBindings,
|
|
14
14
|
trace
|
|
15
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-5SR4SFSU.js";
|
|
16
16
|
|
|
17
17
|
// src/cli.ts
|
|
18
18
|
import { Command } from "commander";
|
|
@@ -92,7 +92,7 @@ import path2 from "path";
|
|
|
92
92
|
// src/db/schema.ts
|
|
93
93
|
var schemaSql = `
|
|
94
94
|
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);
|
|
95
|
-
CREATE TABLE IF NOT EXISTS repositories (id INTEGER PRIMARY KEY, workspace_id INTEGER NOT NULL, name TEXT NOT NULL, absolute_path TEXT NOT NULL, relative_path TEXT NOT NULL, package_name TEXT, package_version TEXT, dependencies_json TEXT DEFAULT '{}', kind TEXT NOT NULL, is_git_repo INTEGER NOT NULL, last_indexed_at TEXT, index_status TEXT DEFAULT 'pending', error_count INTEGER DEFAULT 0, fingerprint TEXT, fact_generation INTEGER NOT NULL DEFAULT 0, graph_generation INTEGER NOT NULL DEFAULT 0, graph_stale_reason TEXT, graph_stale_at TEXT, UNIQUE(workspace_id, absolute_path), FOREIGN KEY(workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE);
|
|
95
|
+
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);
|
|
96
96
|
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);
|
|
97
97
|
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);
|
|
98
98
|
CREATE TABLE IF NOT EXISTS cds_services (id INTEGER PRIMARY KEY, repo_id INTEGER NOT NULL, namespace TEXT, service_name TEXT NOT NULL, qualified_name TEXT NOT NULL, service_path TEXT NOT NULL, is_extend INTEGER NOT NULL, source_file TEXT NOT NULL, source_line INTEGER NOT NULL, FOREIGN KEY(repo_id) REFERENCES repositories(id) ON DELETE CASCADE);
|
|
@@ -116,7 +116,7 @@ CREATE VIRTUAL TABLE IF NOT EXISTS search_index USING fts5(kind, name, path, rep
|
|
|
116
116
|
`;
|
|
117
117
|
|
|
118
118
|
// src/db/migrations.ts
|
|
119
|
-
var CURRENT_SCHEMA_VERSION =
|
|
119
|
+
var CURRENT_SCHEMA_VERSION = 8;
|
|
120
120
|
var columns = {
|
|
121
121
|
service_bindings: [
|
|
122
122
|
{ name: "helper_chain_json", ddl: "ALTER TABLE service_bindings ADD COLUMN helper_chain_json TEXT" },
|
|
@@ -127,7 +127,8 @@ var columns = {
|
|
|
127
127
|
{ name: "fact_generation", ddl: "ALTER TABLE repositories ADD COLUMN fact_generation INTEGER NOT NULL DEFAULT 0" },
|
|
128
128
|
{ name: "graph_generation", ddl: "ALTER TABLE repositories ADD COLUMN graph_generation INTEGER NOT NULL DEFAULT 0" },
|
|
129
129
|
{ name: "graph_stale_reason", ddl: "ALTER TABLE repositories ADD COLUMN graph_stale_reason TEXT" },
|
|
130
|
-
{ name: "graph_stale_at", ddl: "ALTER TABLE repositories ADD COLUMN graph_stale_at TEXT" }
|
|
130
|
+
{ name: "graph_stale_at", ddl: "ALTER TABLE repositories ADD COLUMN graph_stale_at TEXT" },
|
|
131
|
+
{ name: "fact_analyzer_version", ddl: "ALTER TABLE repositories ADD COLUMN fact_analyzer_version TEXT DEFAULT 'legacy'" }
|
|
131
132
|
],
|
|
132
133
|
graph_edges: [
|
|
133
134
|
{ name: "status", ddl: "ALTER TABLE graph_edges ADD COLUMN status TEXT NOT NULL DEFAULT 'unresolved'" },
|
|
@@ -192,7 +193,7 @@ function migrate(db) {
|
|
|
192
193
|
// package.json
|
|
193
194
|
var package_default = {
|
|
194
195
|
name: "@saptools/service-flow",
|
|
195
|
-
version: "0.1.
|
|
196
|
+
version: "0.1.34",
|
|
196
197
|
description: "Trace SAP CAP service-to-service flows across multi-repository workspaces with runtime-aware graph resolution",
|
|
197
198
|
type: "module",
|
|
198
199
|
publishConfig: {
|
|
@@ -1053,7 +1054,7 @@ async function indexRepository(db, repo, force) {
|
|
|
1053
1054
|
insertRegistrations(db, repo.id, parsed.registrations);
|
|
1054
1055
|
insertBindings(db, repo.id, parsed.bindings);
|
|
1055
1056
|
insertCalls(db, repo.id, parsed.calls);
|
|
1056
|
-
db.prepare("UPDATE repositories SET last_indexed_at=?, index_status='indexed', error_count=0, fingerprint=?, fact_generation=COALESCE(fact_generation,0)+1, graph_stale_reason='facts_changed', graph_stale_at=? WHERE id=?").run((/* @__PURE__ */ new Date()).toISOString(), fingerprint, (/* @__PURE__ */ new Date()).toISOString(), repo.id);
|
|
1057
|
+
db.prepare("UPDATE repositories SET last_indexed_at=?, index_status='indexed', error_count=0, fingerprint=?, fact_generation=COALESCE(fact_generation,0)+1, graph_stale_reason='facts_changed', graph_stale_at=?, fact_analyzer_version=? WHERE id=?").run((/* @__PURE__ */ new Date()).toISOString(), fingerprint, (/* @__PURE__ */ new Date()).toISOString(), ANALYZER_VERSION, repo.id);
|
|
1057
1058
|
});
|
|
1058
1059
|
return { fileCount: sourceFiles.length, diagnosticCount: 0, skipped: false };
|
|
1059
1060
|
} catch (error) {
|
|
@@ -1258,6 +1259,53 @@ async function withReadOnlyWorkspace(workspace, fn) {
|
|
|
1258
1259
|
db.close();
|
|
1259
1260
|
}
|
|
1260
1261
|
}
|
|
1262
|
+
function schemaDriftDiagnostics(db, strict) {
|
|
1263
|
+
if (!strict) return [];
|
|
1264
|
+
const symbolColumns = db.prepare("PRAGMA table_info(symbols)").all();
|
|
1265
|
+
const legacy = symbolColumns.filter((row) => ["external_target_kind", "external_target_id", "external_target_label", "external_target_dynamic"].includes(String(row.name))).map((row) => row.name);
|
|
1266
|
+
const missingExternal = 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();
|
|
1267
|
+
const diagnostics = [];
|
|
1268
|
+
if (legacy.length > 0) diagnostics.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" });
|
|
1269
|
+
if (missingExternal.length > 0) diagnostics.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: missingExternal, remediation: "service-flow index --force && service-flow link" });
|
|
1270
|
+
if (legacy.length > 0 || missingExternal.length > 0) diagnostics.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." });
|
|
1271
|
+
return diagnostics;
|
|
1272
|
+
}
|
|
1273
|
+
function linkUpgradeWarnings(db) {
|
|
1274
|
+
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)));
|
|
1275
|
+
}
|
|
1276
|
+
function analyzerVersionDiagnostics(db, strict) {
|
|
1277
|
+
if (!strict) return [];
|
|
1278
|
+
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);
|
|
1279
|
+
if (rows.length === 0) return [];
|
|
1280
|
+
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" }];
|
|
1281
|
+
}
|
|
1282
|
+
function remoteEntityOperationCollisionQuality(db) {
|
|
1283
|
+
const rows = db.prepare(`SELECT c.id callId,c.source_file sourceFile,c.source_line sourceLine,c.method method,c.operation_path_expr rawPath,c.query_entity entitySegment,e.to_id selectedTerminalEntityTarget,e.evidence_json evidenceJson
|
|
1284
|
+
FROM outbound_calls c JOIN graph_edges e ON e.from_kind='call' AND e.from_id=CAST(c.id AS TEXT)
|
|
1285
|
+
WHERE c.call_type LIKE 'remote_entity_%' AND e.edge_type='HANDLER_ACCESSES_REMOTE_ENTITY' AND e.status='terminal'
|
|
1286
|
+
ORDER BY c.source_file,c.source_line LIMIT 100`).all();
|
|
1287
|
+
const examples = [];
|
|
1288
|
+
for (const row of rows) {
|
|
1289
|
+
const normalized = normalizeODataOperationInvocationPath(String(row.rawPath ?? ""));
|
|
1290
|
+
const rawPath = String(row.rawPath ?? "");
|
|
1291
|
+
const candidatePath = normalized?.wasInvocation ? normalized.normalizedOperationPath : rawPath;
|
|
1292
|
+
const name = candidatePath.replace(/^\//, "");
|
|
1293
|
+
const simple = name.split(".").at(-1) ?? name;
|
|
1294
|
+
const candidates = db.prepare("SELECT COUNT(*) count FROM cds_operations WHERE operation_path IN (?,?) OR operation_name IN (?,?)").get(candidatePath, `/${simple}`, name, simple);
|
|
1295
|
+
const candidateCount = Number(candidates.count ?? 0);
|
|
1296
|
+
const operationLike = Boolean(normalized?.wasInvocation) || candidateCount > 0;
|
|
1297
|
+
if (!operationLike) continue;
|
|
1298
|
+
let classifierReason;
|
|
1299
|
+
try {
|
|
1300
|
+
const evidence = JSON.parse(String(row.evidenceJson ?? "{}"));
|
|
1301
|
+
classifierReason = evidence.odataPathIntent?.reason;
|
|
1302
|
+
} catch {
|
|
1303
|
+
classifierReason = void 0;
|
|
1304
|
+
}
|
|
1305
|
+
examples.push({ callId: row.callId, sourceFile: row.sourceFile, sourceLine: row.sourceLine, method: row.method, rawPath, normalizedOperationPath: normalized?.wasInvocation ? normalized.normalizedOperationPath : candidatePath, entitySegment: row.entitySegment, operationCandidateCount: candidateCount, selectedTerminalEntityTarget: row.selectedTerminalEntityTarget, classifierReason });
|
|
1306
|
+
}
|
|
1307
|
+
return { severity: examples.length > 0 ? "warning" : "info", code: "strict_remote_entity_operation_collision_quality", message: "Terminal remote entity edges that look like indexed operation invocations", collisionCount: examples.length, examples: examples.slice(0, 10) };
|
|
1308
|
+
}
|
|
1261
1309
|
function localServiceDiagnostics(db, strict) {
|
|
1262
1310
|
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();
|
|
1263
1311
|
const implementationContext = rows.filter((row) => row.status === "resolved" && String(row.evidenceJson ?? "").includes("implementation_context_caller_ownership")).length;
|
|
@@ -1326,6 +1374,7 @@ function parserQualityDiagnostics(db, strict) {
|
|
|
1326
1374
|
const remoteQuery = remoteQueryTargetQuality(db);
|
|
1327
1375
|
const invocation = odataInvocationResolutionQuality(db);
|
|
1328
1376
|
const remoteAction = remoteActionTargetQuality(db);
|
|
1377
|
+
const entityOperationCollision = remoteEntityOperationCollisionQuality(db);
|
|
1329
1378
|
const externalHttp = externalHttpTargetQuality(db);
|
|
1330
1379
|
const aliasQuality = identityAliasBindingQuality(db);
|
|
1331
1380
|
const noBindingQuality = remoteActionNoBindingQuality(db);
|
|
@@ -1343,6 +1392,7 @@ function parserQualityDiagnostics(db, strict) {
|
|
|
1343
1392
|
wrapperQuality,
|
|
1344
1393
|
nestedThisQuality,
|
|
1345
1394
|
remoteQuery,
|
|
1395
|
+
entityOperationCollision,
|
|
1346
1396
|
invocation,
|
|
1347
1397
|
remoteAction,
|
|
1348
1398
|
externalHttp,
|
|
@@ -1572,8 +1622,10 @@ function createProgram() {
|
|
|
1572
1622
|
program.command("link").option("--workspace <path>").option("--force").action(
|
|
1573
1623
|
(opts) => void withWorkspace(opts.workspace, (db, workspaceId) => {
|
|
1574
1624
|
const r = linkWorkspace(db, workspaceId);
|
|
1625
|
+
const upgradeWarnings = linkUpgradeWarnings(db);
|
|
1575
1626
|
process.stdout.write(
|
|
1576
|
-
|
|
1627
|
+
`${upgradeWarnings.length ? `Warnings: ${upgradeWarnings.map((item) => String(item.code)).join(", ")}. Run service-flow doctor --strict for remediation.
|
|
1628
|
+
` : ""}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
|
|
1577
1629
|
`
|
|
1578
1630
|
);
|
|
1579
1631
|
}).catch(fail)
|
|
@@ -1760,7 +1812,9 @@ function createProgram() {
|
|
|
1760
1812
|
).all(Boolean(opts.strict), Boolean(opts.strict), Boolean(opts.strict), Boolean(opts.strict), Boolean(opts.strict), Boolean(opts.strict), Boolean(opts.strict), Boolean(opts.strict));
|
|
1761
1813
|
const localServiceHealth = localServiceDiagnostics(db, Boolean(opts.strict));
|
|
1762
1814
|
const parserQualityHealth = parserQualityDiagnostics(db, Boolean(opts.strict));
|
|
1763
|
-
const
|
|
1815
|
+
const schemaDriftHealth = schemaDriftDiagnostics(db, Boolean(opts.strict));
|
|
1816
|
+
const analyzerVersionHealth = analyzerVersionDiagnostics(db, Boolean(opts.strict));
|
|
1817
|
+
const allDiagnostics = [...diagnostics, ...health, ...localServiceHealth, ...schemaDriftHealth, ...analyzerVersionHealth, ...parserQualityHealth];
|
|
1764
1818
|
process.stdout.write(
|
|
1765
1819
|
allDiagnostics.length ? renderJson(allDiagnostics) : `${pc.green("No diagnostics recorded")}
|
|
1766
1820
|
`
|