@saptools/service-flow 0.1.46 → 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 -37
- 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`
|
|
@@ -481,39 +485,3 @@ MIT
|
|
|
481
485
|
---
|
|
482
486
|
|
|
483
487
|
Made with ❤️ to make your work life easier!
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
### Service-only trace policy
|
|
487
|
-
|
|
488
|
-
`service-flow trace --service <path>` is intentionally not a broad workspace traversal. Provide `--operation`, `--path`, or `--handler`; otherwise the command returns a typed `trace_start_not_found` diagnostic and no edges.
|
|
489
|
-
|
|
490
|
-
### Graph freshness and last-good snapshots
|
|
491
|
-
|
|
492
|
-
Repository facts are parsed before publication and committed atomically. Failed indexing attempts record diagnostics and retain the last complete published snapshot and successful fingerprint. Successful fact publication marks the workspace graph stale until `service-flow link` rebuilds dependency, remote-call, and implementation edges for the current fact generation.
|
|
493
|
-
|
|
494
|
-
### Cross-package implementation evidence
|
|
495
|
-
|
|
496
|
-
Handler registrations persist parsed class names and import sources. Linking resolves implementation edges only through registered application evidence plus model and handler package dependency edges; a decorator-name match alone is not enough.
|
|
497
|
-
|
|
498
|
-
### Graph variables
|
|
499
|
-
|
|
500
|
-
The `graph` command accepts repeatable `--var key=value` options, matching `trace`, for runtime substitution previews in JSON or Mermaid output.
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
### 0.1.17 parser ownership policy
|
|
504
|
-
|
|
505
|
-
Outbound call extraction is AST-based and ignores comments, block comments, and string literals. CAP/service `.on(...)` registrations are indexed only when the receiver has CAP/service evidence, and top-level registrations receive `module:<relative-file>#event:<event-name>:<line>` synthetic owners. Generic event emitters such as desktop or window events are ignored by default rather than guessed as CAP async edges. Unsupported source shapes are surfaced through diagnostics and strict doctor ownerless categories instead of guessed graph edges.
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
## 0.1.32 hardening notes
|
|
509
|
-
|
|
510
|
-
Operation trace starts resolve indexed CDS operations before handler fallback. Ambiguous operation matches, ambiguous/rejected implementation edges, and resolved edges without an executable handler scope now fail closed with structured `resolutionStage` and `resolutionStatus` diagnostics and no speculative traversal. Handler fallback is only considered when no indexed CDS operation matches and the fallback is unique under the existing registration and ownership policy.
|
|
511
|
-
|
|
512
|
-
Decorator signals are interpreted as resolved, absent, unsupported, or malformed. Only resolved string/template values, generated `Action<Name>.name`/`Func<Name>.name` constants, qualified generated constants ending in `.name`, and narrow safe `String(identifier)` wrappers can contradict method-name fallback. Unsupported expressions are preserved as evidence but are not treated as proof that a handler targets another operation.
|
|
513
|
-
|
|
514
|
-
External HTTP facts persist queryable target kind, stable id, safe label, and dynamic flag on `outbound_calls`; graph evidence uses the same sanitized model. URL user info, fragments, query values, headers, cookies, credentials, tokens, API keys, and request/response bodies are not persisted. Legacy workspaces may relink from old evidence when possible, but full semantic enrichment requires re-index plus relink when old parser facts did not contain safe target metadata.
|
|
515
|
-
|
|
516
|
-
Graph taxonomy now keeps CAP operation candidates separate from external HTTP targets. Dynamic, ambiguous, and unresolved CAP calls use operation-candidate semantics, external destinations/endpoints are produced only by `external_http` calls, and terminal local transport-client methods use transport-method semantics rather than external HTTP edges. `doctor --strict` reports external target quality and taxonomy consistency.
|
|
517
|
-
### Runtime and parser hardening
|
|
518
|
-
|
|
519
|
-
`service-flow` treats URL and destination templates with substitutions as dynamic external targets, preserves operation-path template evidence without promoting unrelated variables, and parses CDS `@(path: ...)` annotations from original source text. Service extension declarations using both `extend Name` and `extend service Name` are recognized for routing provenance.
|
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(
|