@saptools/service-flow 0.1.47 → 0.1.48
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 +6 -0
- package/README.md +5 -1
- package/dist/cli.js +125 -12
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/doctor.ts +39 -5
- package/src/cli.ts +4 -7
- package/src/output/doctor-output.ts +98 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.48
|
|
4
|
+
|
|
5
|
+
- Added deterministic `doctor --format json|table` output while preserving legacy-compatible default doctor output; JSON mode returns `[]` for clean workspaces and table mode renders concise diagnostic rows with capped hint lines.
|
|
6
|
+
- Documented the 0.1.47 audit follow-up evidence fields for service-client ownership chains, normalized OData operation paths, wrapper path candidates, and implementation hint suggestions.
|
|
7
|
+
- Bumped the package patch version for the service-flow audit follow-up release.
|
|
8
|
+
|
|
3
9
|
## 0.1.46
|
|
4
10
|
|
|
5
11
|
- Improved ambiguous implementation diagnostics with ready-to-copy scoped hint suggestions for each blocked helper hop.
|
package/README.md
CHANGED
|
@@ -277,14 +277,18 @@ service-flow inspect operation /doWork --workspace /path/to/workspace
|
|
|
277
277
|
|
|
278
278
|
### 🩺 `service-flow doctor`
|
|
279
279
|
|
|
280
|
-
Print stored diagnostics. Default output suppresses high-noise entity-only service checks; `--strict` includes them.
|
|
280
|
+
Print stored diagnostics. Default output suppresses high-noise entity-only service checks; `--strict` includes them. Without `--format`, doctor keeps the legacy-compatible behavior: JSON when diagnostics exist and `No diagnostics recorded` for a clean workspace. Deterministic JSON mode prints `[]` when clean.
|
|
281
281
|
|
|
282
282
|
```bash
|
|
283
283
|
service-flow doctor --workspace /path/to/workspace
|
|
284
284
|
service-flow doctor --workspace /path/to/workspace --strict
|
|
285
285
|
service-flow doctor --workspace /path/to/workspace --strict --detail
|
|
286
|
+
service-flow doctor --workspace /path/to/workspace --strict --format json
|
|
287
|
+
service-flow doctor --workspace /path/to/workspace --strict --format table
|
|
286
288
|
```
|
|
287
289
|
|
|
290
|
+
`--format json` always returns a JSON array, including `[]` for clean workspaces. `--format table` prints a concise human-readable table with capped copyable hint lines; use JSON when automation needs every field or every hint alternative. Omit `--format` when relying on the pre-0.1.48 compatible output contract.
|
|
291
|
+
|
|
288
292
|
Strict output keeps stable category, count, severity, and capped example fields. Use `--detail` to include full `expandedExamples` for implementation candidate, parameter metadata, and dynamic wrapper categories.
|
|
289
293
|
|
|
290
294
|
### 🧹 `service-flow clean`
|
package/dist/cli.js
CHANGED
|
@@ -21,7 +21,6 @@ import {
|
|
|
21
21
|
import { Command } from "commander";
|
|
22
22
|
import path6 from "path";
|
|
23
23
|
import fs6 from "fs/promises";
|
|
24
|
-
import pc from "picocolors";
|
|
25
24
|
|
|
26
25
|
// src/config/defaults.ts
|
|
27
26
|
var DEFAULT_IGNORES = [
|
|
@@ -211,7 +210,7 @@ function migrate(db) {
|
|
|
211
210
|
// package.json
|
|
212
211
|
var package_default = {
|
|
213
212
|
name: "@saptools/service-flow",
|
|
214
|
-
version: "0.1.
|
|
213
|
+
version: "0.1.48",
|
|
215
214
|
description: "Trace SAP CAP service-to-service flows across multi-repository workspaces with runtime-aware graph resolution",
|
|
216
215
|
type: "module",
|
|
217
216
|
publishConfig: {
|
|
@@ -1312,6 +1311,7 @@ function doctorDiagnostics(db, strict, options = {}) {
|
|
|
1312
1311
|
return [
|
|
1313
1312
|
...diagnostics,
|
|
1314
1313
|
...healthDiagnostics(db, strict),
|
|
1314
|
+
...remoteTargetWithoutImplementationDiagnostics(db, strict, Boolean(options.detail)),
|
|
1315
1315
|
...localServiceDiagnostics(db, strict),
|
|
1316
1316
|
...schemaDriftDiagnostics(db, strict),
|
|
1317
1317
|
...analyzerVersionDiagnostics(db, strict),
|
|
@@ -1339,10 +1339,6 @@ function healthDiagnostics(db, strict) {
|
|
|
1339
1339
|
SELECT 'warning','legacy_schema_weaker_foreign_keys','Legacy table lacks fresh-schema foreign-key metadata; rebuild the database or re-run init/index in a new database',NULL,NULL
|
|
1340
1340
|
WHERE (SELECT COUNT(*) FROM pragma_foreign_key_list('graph_edges'))=0 OR (SELECT COUNT(*) FROM pragma_foreign_key_list('index_runs'))=0 OR (SELECT COUNT(*) FROM pragma_foreign_key_list('diagnostics'))=0
|
|
1341
1341
|
UNION ALL
|
|
1342
|
-
SELECT 'warning','remote_target_without_implementation','Remote target operation has no implementation edge: ' || s.service_path || o.operation_path,o.source_file,o.source_line
|
|
1343
|
-
FROM graph_edges remote JOIN cds_operations o ON o.id=CAST(remote.to_id AS INTEGER) JOIN cds_services s ON s.id=o.service_id
|
|
1344
|
-
WHERE remote.edge_type='REMOTE_CALL_RESOLVES_TO_OPERATION' AND remote.to_kind='operation' 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) AND ?
|
|
1345
|
-
UNION ALL
|
|
1346
1342
|
SELECT CASE WHEN ? THEN 'warning' ELSE 'error' END,'local_service_calls_all_unresolved','All local service calls are unresolved; verify local service alias parsing and linking',NULL,NULL
|
|
1347
1343
|
WHERE EXISTS (SELECT 1 FROM outbound_calls WHERE call_type='local_service_call') AND NOT EXISTS (SELECT 1 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' AND e.status='resolved')
|
|
1348
1344
|
UNION ALL
|
|
@@ -1360,7 +1356,41 @@ function healthDiagnostics(db, strict) {
|
|
|
1360
1356
|
UNION ALL
|
|
1361
1357
|
SELECT 'warning','index_run_abandoned','Index run ' || id || ' started at ' || started_at || ' is still running after the 60 minute abandonment threshold',NULL,NULL
|
|
1362
1358
|
FROM index_runs WHERE status='running' AND datetime(started_at) < datetime('now','-60 minutes')`
|
|
1363
|
-
).all(strict, strict, strict, strict, strict, strict
|
|
1359
|
+
).all(strict, strict, strict, strict, strict, strict);
|
|
1360
|
+
}
|
|
1361
|
+
function remoteTargetWithoutImplementationDiagnostics(db, strict, detail) {
|
|
1362
|
+
if (!strict) return [];
|
|
1363
|
+
const groups = db.prepare(`SELECT s.service_path servicePath,o.operation_path operationPath,o.source_file sourceFile,o.source_line sourceLine,COUNT(*) callSiteCount
|
|
1364
|
+
FROM graph_edges remote JOIN cds_operations o ON o.id=CAST(remote.to_id AS INTEGER) JOIN cds_services s ON s.id=o.service_id
|
|
1365
|
+
WHERE remote.edge_type='REMOTE_CALL_RESOLVES_TO_OPERATION' AND remote.to_kind='operation'
|
|
1366
|
+
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)
|
|
1367
|
+
GROUP BY s.service_path,o.operation_path,o.source_file,o.source_line
|
|
1368
|
+
ORDER BY s.service_path,o.operation_path`).all();
|
|
1369
|
+
return groups.map((group) => {
|
|
1370
|
+
const examples = remoteTargetWithoutImplementationExamples(db, String(group.servicePath ?? ""), String(group.operationPath ?? ""));
|
|
1371
|
+
return {
|
|
1372
|
+
severity: "warning",
|
|
1373
|
+
code: "remote_target_without_implementation",
|
|
1374
|
+
message: `Remote target operation has no implementation edge: ${String(group.servicePath ?? "")}${String(group.operationPath ?? "")}`,
|
|
1375
|
+
sourceFile: group.sourceFile,
|
|
1376
|
+
sourceLine: group.sourceLine,
|
|
1377
|
+
servicePath: group.servicePath,
|
|
1378
|
+
operationPath: group.operationPath,
|
|
1379
|
+
callSiteCount: Number(group.callSiteCount ?? 0),
|
|
1380
|
+
examples: examples.slice(0, 3),
|
|
1381
|
+
expandedExamples: detail ? examples : void 0
|
|
1382
|
+
};
|
|
1383
|
+
});
|
|
1384
|
+
}
|
|
1385
|
+
function remoteTargetWithoutImplementationExamples(db, servicePath, operationPath) {
|
|
1386
|
+
return db.prepare(`SELECT r.name repo,c.source_file sourceFile,c.source_line sourceLine,c.operation_path_expr operationPathExpr
|
|
1387
|
+
FROM graph_edges remote JOIN cds_operations o ON o.id=CAST(remote.to_id AS INTEGER) JOIN cds_services s ON s.id=o.service_id
|
|
1388
|
+
LEFT JOIN outbound_calls c ON remote.from_kind='call' AND c.id=CAST(remote.from_id AS INTEGER)
|
|
1389
|
+
LEFT JOIN repositories r ON r.id=c.repo_id
|
|
1390
|
+
WHERE remote.edge_type='REMOTE_CALL_RESOLVES_TO_OPERATION' AND remote.to_kind='operation'
|
|
1391
|
+
AND s.service_path=? AND o.operation_path=?
|
|
1392
|
+
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)
|
|
1393
|
+
ORDER BY r.name,c.source_file,c.source_line`).all(servicePath, operationPath);
|
|
1364
1394
|
}
|
|
1365
1395
|
function schemaDriftDiagnostics(db, strict) {
|
|
1366
1396
|
if (!strict) return [];
|
|
@@ -1753,6 +1783,92 @@ function renderTraceJson(trace2) {
|
|
|
1753
1783
|
return renderJson(trace2);
|
|
1754
1784
|
}
|
|
1755
1785
|
|
|
1786
|
+
// src/output/doctor-output.ts
|
|
1787
|
+
import pc from "picocolors";
|
|
1788
|
+
function renderDoctorDiagnostics(diagnostics, format) {
|
|
1789
|
+
if (format === "json") return renderJson(diagnostics);
|
|
1790
|
+
if (format === "table") return renderDoctorTable(diagnostics);
|
|
1791
|
+
if (format) throw new Error(`Unsupported doctor format: ${format}. Expected json or table.`);
|
|
1792
|
+
return renderLegacyDoctorOutput(diagnostics);
|
|
1793
|
+
}
|
|
1794
|
+
function renderLegacyDoctorOutput(diagnostics) {
|
|
1795
|
+
if (diagnostics.length === 0) return cleanDoctorMessage();
|
|
1796
|
+
return renderJson(diagnostics);
|
|
1797
|
+
}
|
|
1798
|
+
function renderDoctorTable(diagnostics) {
|
|
1799
|
+
if (diagnostics.length === 0) return cleanDoctorMessage();
|
|
1800
|
+
const rows = diagnostics.map((diagnostic) => ({
|
|
1801
|
+
severity: String(diagnostic.severity ?? "info"),
|
|
1802
|
+
code: String(diagnostic.code ?? "diagnostic"),
|
|
1803
|
+
location: diagnosticLocation(diagnostic),
|
|
1804
|
+
message: compactMessage(diagnostic),
|
|
1805
|
+
hints: suggestedHintLines(diagnostic)
|
|
1806
|
+
}));
|
|
1807
|
+
const widths = {
|
|
1808
|
+
severity: columnWidth("Severity", rows.map((row) => row.severity), 10),
|
|
1809
|
+
code: columnWidth("Code", rows.map((row) => row.code), 44),
|
|
1810
|
+
location: columnWidth("Location", rows.map((row) => row.location), 28)
|
|
1811
|
+
};
|
|
1812
|
+
const lines = [
|
|
1813
|
+
`${"Severity".padEnd(widths.severity)} ${"Code".padEnd(widths.code)} ${"Location".padEnd(widths.location)} Message`,
|
|
1814
|
+
`${"-".repeat(widths.severity)} ${"-".repeat(widths.code)} ${"-".repeat(widths.location)} ${"-".repeat(7)}`
|
|
1815
|
+
];
|
|
1816
|
+
for (const row of rows) {
|
|
1817
|
+
lines.push(`${truncate(row.severity, widths.severity).padEnd(widths.severity)} ${truncate(row.code, widths.code).padEnd(widths.code)} ${truncate(row.location, widths.location).padEnd(widths.location)} ${row.message}`);
|
|
1818
|
+
lines.push(...row.hints.map((hint) => ` try ${hint}`));
|
|
1819
|
+
}
|
|
1820
|
+
return `${lines.join("\n")}
|
|
1821
|
+
`;
|
|
1822
|
+
}
|
|
1823
|
+
function diagnosticLocation(diagnostic) {
|
|
1824
|
+
const file = diagnostic.sourceFile ?? diagnostic.file;
|
|
1825
|
+
const line = diagnostic.sourceLine ?? diagnostic.line;
|
|
1826
|
+
if (file || line) return `${String(file ?? "")}:${String(line ?? "")}`;
|
|
1827
|
+
return "-";
|
|
1828
|
+
}
|
|
1829
|
+
function compactMessage(diagnostic) {
|
|
1830
|
+
const message = String(diagnostic.message ?? "");
|
|
1831
|
+
const count = typeof diagnostic.count === "number" ? ` count=${diagnostic.count}` : "";
|
|
1832
|
+
const total = typeof diagnostic.total === "number" ? ` total=${diagnostic.total}` : "";
|
|
1833
|
+
return `${message}${count}${total}`.trim();
|
|
1834
|
+
}
|
|
1835
|
+
function suggestedHintLines(diagnostic) {
|
|
1836
|
+
const direct = cliHints(diagnostic.suggestedHints);
|
|
1837
|
+
if (direct.length > 0) return cappedHints(direct);
|
|
1838
|
+
return cappedHints(cliHintsFromSuggestions(diagnostic.implementationHintSuggestions));
|
|
1839
|
+
}
|
|
1840
|
+
function cliHints(value) {
|
|
1841
|
+
return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
|
|
1842
|
+
}
|
|
1843
|
+
function cliHintsFromSuggestions(value) {
|
|
1844
|
+
if (!Array.isArray(value)) return [];
|
|
1845
|
+
return value.flatMap((item) => {
|
|
1846
|
+
if (!isRecord(item)) return [];
|
|
1847
|
+
return typeof item.cli === "string" ? [item.cli] : [];
|
|
1848
|
+
});
|
|
1849
|
+
}
|
|
1850
|
+
function isRecord(value) {
|
|
1851
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
1852
|
+
}
|
|
1853
|
+
function cappedHints(hints) {
|
|
1854
|
+
const unique = [...new Set(hints)];
|
|
1855
|
+
const shown = unique.slice(0, 3);
|
|
1856
|
+
if (unique.length > shown.length) shown.push(`... ${unique.length - shown.length} more hint(s) available in --format json`);
|
|
1857
|
+
return shown;
|
|
1858
|
+
}
|
|
1859
|
+
function cleanDoctorMessage() {
|
|
1860
|
+
return `${pc.green("No diagnostics recorded")}
|
|
1861
|
+
`;
|
|
1862
|
+
}
|
|
1863
|
+
function columnWidth(header, values, max) {
|
|
1864
|
+
return Math.min(max, Math.max(header.length, ...values.map((value) => value.length)));
|
|
1865
|
+
}
|
|
1866
|
+
function truncate(value, width) {
|
|
1867
|
+
if (value.length <= width) return value;
|
|
1868
|
+
if (width <= 1) return value.slice(0, width);
|
|
1869
|
+
return `${value.slice(0, width - 1)}\u2026`;
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1756
1872
|
// src/output/mermaid-output.ts
|
|
1757
1873
|
function safe(value) {
|
|
1758
1874
|
return value.replace(/[^\w-]/g, "_").slice(0, 60);
|
|
@@ -1985,13 +2101,10 @@ function createProgram() {
|
|
|
1985
2101
|
process.stdout.write(renderJson(rows));
|
|
1986
2102
|
}).catch(fail)
|
|
1987
2103
|
);
|
|
1988
|
-
program.command("doctor").option("--workspace <path>").option("--strict").option("--detail").action(
|
|
2104
|
+
program.command("doctor").option("--workspace <path>").option("--strict").option("--detail").option("--format <format>", "json|table").action(
|
|
1989
2105
|
(opts) => void withReadOnlyWorkspace(opts.workspace, (db) => {
|
|
1990
2106
|
const allDiagnostics = doctorDiagnostics(db, Boolean(opts.strict), { detail: Boolean(opts.detail) });
|
|
1991
|
-
process.stdout.write(
|
|
1992
|
-
allDiagnostics.length ? renderJson(allDiagnostics) : `${pc.green("No diagnostics recorded")}
|
|
1993
|
-
`
|
|
1994
|
-
);
|
|
2107
|
+
process.stdout.write(renderDoctorDiagnostics(allDiagnostics, opts.format));
|
|
1995
2108
|
}).catch(fail)
|
|
1996
2109
|
);
|
|
1997
2110
|
program.command("clean").option("--workspace <path>").option("--db-only").action(
|