sverka 0.2.15 → 0.2.16

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.
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../src/types.ts","../src/main.ts","../src/output.ts"],"mappings":";;iBAGiB;;EAEf;;EAEA;;EAEA;;EAEA;;EAEA;;;iBAIe;EACf,MAAM;EACN,UAAU;EACV,MAAM;EACN,UAAU;;EAEV,MAAM;;;qBAIK;;;;;;YAOD,mBAAmB,uBAAuB;;YAG1C;;qBAWC,iBAAiB;WACnB,MAAM;WACN,UAAU;WACD;EAEhB,YAAA,iBACA,MAAM,cACN,UAAU,UACV;;;;;UChCa;EACf,SAAS;;;;;;;wBAiZW,KAAK,gBAAgB,OAAO,WAAW;;;;KCvajD,aAAa;;;;;;qBAOZ,+BAA+B;;EAM9B,YAAA,KAAK,WAAW,KAAK,WAAW,iBAAe;EAO3D,MAAM;EAKN,UAAU;EAKV,MAAM;EAIN,UAAU;;EAKV,MAAM;;;;;;wBAwDQ,mBACd,QAAQ,aACR,KAAK,WACL,KAAK,YACJ;;;;;wBAYa,iBACd,QAAQ,aACR,MAAM,eACL"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/types.ts","../src/main.ts","../src/output.ts"],"mappings":";;iBAGiB;;EAEf;;EAEA;;EAEA;;EAEA;;EAEA;;;iBAIe;EACf,MAAM;EACN,UAAU;EACV,MAAM;EACN,UAAU;;EAEV,MAAM;;;qBAIK;;;;;;YAOD,mBAAmB,uBAAuB;;YAG1C;;qBAWC,iBAAiB;WACnB,MAAM;WACN,UAAU;WACD;EAEhB,YAAA,iBACA,MAAM,cACN,UAAU,UACV;;;;;UChCa;EACf,SAAS;;;;;;;wBAuZW,KAAK,gBAAgB,OAAO,WAAW;;;;KC7ajD,aAAa;;;;;;qBAOZ,+BAA+B;;EAM9B,YAAA,KAAK,WAAW,KAAK,WAAW,iBAAe;EAO3D,MAAM;EAKN,UAAU;EAKV,MAAM;EAIN,UAAU;;EAKV,MAAM;;;;;;wBAwDQ,mBACd,QAAQ,aACR,KAAK,WACL,KAAK,YACJ;;;;;wBAYa,iBACd,QAAQ,aACR,MAAM,eACL"}
package/dist/index.mjs CHANGED
@@ -7,7 +7,7 @@ import { constants, existsSync, mkdirSync, readFileSync, readdirSync, realpathSy
7
7
  import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
8
8
  import { fileURLToPath, pathToFileURL } from "node:url";
9
9
  import { mkdir, open, readFile, realpath, writeFile } from "node:fs/promises";
10
- import { collectConstructWarnings, synthesize, validateGraph } from "@sverka/workflow";
10
+ import { Entry, Pipeline, Project, ShellStep, collectConstructWarnings, manual, synthesize, validateGraph } from "@sverka/workflow";
11
11
  import { bindRunPlan, createPlanner } from "@sverka/sdk";
12
12
  import { DEFAULT_POLICY, createBuiltinResolver, evaluatePolicy, loadBaseline, normalizeSarif, serializeSarif, synthesizeCheckSteps } from "@sverka/verification";
13
13
  import { createDockerDriver, createEngine, createHostDriver } from "@sverka/runtime";
@@ -720,6 +720,48 @@ async function graphCommand(global, output, start) {
720
720
  return ExitCode.Success;
721
721
  }
722
722
  //#endregion
723
+ //#region src/internal/implicit.ts
724
+ /** Load the graph `sverka run` should execute: the real config when one
725
+ * exists on disk (or --config names one), else the implicit detected
726
+ * pipeline. */
727
+ async function loadRunGraph(global) {
728
+ if (global.config === null && await findConfig(global.root) === null) return implicitGraph(global.root);
729
+ const { graph, warnings } = await loadProjectGraph(global);
730
+ return {
731
+ graph,
732
+ warnings
733
+ };
734
+ }
735
+ async function implicitGraph(root) {
736
+ const checks = await detectProjectChecks(root);
737
+ if (checks.length === 0) throw new CliError("no config found and no checks detected (run `sverka init` to create one)", "MISSING_ARG", ExitCode.UsageError);
738
+ const project = new Project("default");
739
+ const pipeline = new Pipeline(project, "default");
740
+ const steps = checks.map((check) => new ShellStep(pipeline, check.checkId, shellStepProps(check)));
741
+ const entry = new Entry(pipeline, "run", {
742
+ trigger: manual(),
743
+ roots: steps.map((step) => step.node.id)
744
+ });
745
+ return {
746
+ graph: synthesize(project),
747
+ warnings: collectConstructWarnings(project),
748
+ detected: entry.roots
749
+ };
750
+ }
751
+ /** Detected check → ShellStep props. A SARIF-on-stdout check keeps the
752
+ * `fromStdout` artifact declaration `init --detect` generates, so the
753
+ * implicit run feeds findings collection the same way. */
754
+ function shellStepProps(check) {
755
+ return {
756
+ command: check.command,
757
+ runtime: { mode: "host" },
758
+ ...check.sarifOutput !== void 0 ? { outputs: { [check.sarifOutput]: {
759
+ type: "artifact",
760
+ fromStdout: true
761
+ } } } : {}
762
+ };
763
+ }
764
+ //#endregion
723
765
  //#region src/internal/runtime-check.ts
724
766
  /**
725
767
  * Check if a binary is available on PATH by spawning `<binary> --version`.
@@ -922,7 +964,8 @@ async function runCommand(args, global, output, start) {
922
964
  const fmt = resolveFormats(args, global);
923
965
  output.debug(`run: root=${global.root} executor=${executor} entry=${args.entryId ?? "(first)"} format=${global.format}`);
924
966
  assertExecutorAvailable(executor);
925
- const { graph, warnings } = await loadProjectGraph(global);
967
+ const { graph, warnings, detected } = await loadRunGraph(global);
968
+ if (detected !== void 0) output.errorLine(`no sverka.config — running ${detected.length} detected checks (${detected.join(", ")})`);
926
969
  for (const warning of warnings) output.errorLine(`warning: ${warning}`);
927
970
  const entryId = resolveEntryId(graph, args.entryId);
928
971
  const plan = bindRunPlan({
@@ -947,13 +990,11 @@ async function runCommand(args, global, output, start) {
947
990
  durationMs,
948
991
  evalResult: evaluation.summary,
949
992
  artifactDir,
950
- sinceMs: start
993
+ sinceMs: start,
994
+ ...detected !== void 0 ? { detected } : {},
995
+ ...args.report !== void 0 ? { report: args.report } : {}
951
996
  }, events, output);
952
- if (fmt.isHtml && renderer) {
953
- const reportPath = args.output ?? join(global.root, ".sverka", "report.html");
954
- output.writeLine(`Wrote HTML report to ${reportPath}`);
955
- }
956
- if (renderer && "waitUntilExit" in renderer) await renderer.waitUntilExit();
997
+ await finalizeRenderer(fmt, renderer, args, global, output);
957
998
  if (evaluation.collectionFailed) return evaluation.exitCode;
958
999
  writeRunOutput({
959
1000
  planId: plan.id,
@@ -963,12 +1004,36 @@ async function runCommand(args, global, output, start) {
963
1004
  global,
964
1005
  output,
965
1006
  evalResult: evaluation.summary,
966
- report
1007
+ report,
1008
+ ...detected !== void 0 ? { detected } : {}
967
1009
  });
968
- if (report !== void 0 && global.format === "text") output.writeLine(report.html !== null ? ` report: ${report.html} (sverka view to open)` : ` report: ${report.json}`);
1010
+ printReportTail(report, args.report, global, output);
969
1011
  if (evaluation.exitCode !== 0) return evaluation.exitCode;
970
1012
  return exitCodeForStatus(runStatus);
971
1013
  }
1014
+ /** Post-run: announce the HTML report path (sarif/web print their own),
1015
+ * then keep interactive renderers mounted until the user quits. */
1016
+ async function finalizeRenderer(fmt, renderer, args, global, output) {
1017
+ if (fmt.isHtml && renderer) {
1018
+ const reportPath = args.output ?? join(global.root, ".sverka", "report.html");
1019
+ output.writeLine(`Wrote HTML report to ${reportPath}`);
1020
+ }
1021
+ if (renderer && "waitUntilExit" in renderer) await renderer.waitUntilExit();
1022
+ }
1023
+ /** Human-mode tail (Spec 53): findings summary, then the report location —
1024
+ * the report is discoverable, not hidden. The `sverka view` hint applies
1025
+ * only when report.html sits at the default path — view resolves
1026
+ * .sverka/runs/<latest>/report.html, not a --report relocation. */
1027
+ function printReportTail(report, reportFlag, global, output) {
1028
+ if (report === void 0 || global.format !== "text") return;
1029
+ output.writeLine(` findings: ${report.findings}`);
1030
+ if (report.html === null) {
1031
+ output.writeLine(` report: ${report.json}`);
1032
+ return;
1033
+ }
1034
+ const hint = reportFlag !== void 0 ? "(open in a browser)" : "(sverka view to open)";
1035
+ output.writeLine(` report: ${report.html} ${hint}`);
1036
+ }
972
1037
  /** Derive output modes — `--format html`, or `--output` without
973
1038
  * sarif/web, implies HTML; evaluation runs for any structured format. */
974
1039
  function resolveFormats(args, global) {
@@ -1018,11 +1083,14 @@ async function writeReportSafe(ctx, events, output) {
1018
1083
  durationMs: ctx.durationMs,
1019
1084
  evalResult: ctx.evalResult,
1020
1085
  artifactDir: ctx.artifactDir,
1021
- sinceMs: ctx.sinceMs
1086
+ sinceMs: ctx.sinceMs,
1087
+ ...ctx.detected !== void 0 ? { detected: ctx.detected } : {},
1088
+ ...ctx.report !== void 0 ? { reportPath: ctx.report } : {}
1022
1089
  });
1023
1090
  return {
1024
1091
  html: artifacts.htmlPath,
1025
- json: join(artifacts.dir, "report.json")
1092
+ json: join(artifacts.dir, "report.json"),
1093
+ findings: artifacts.findingsCount
1026
1094
  };
1027
1095
  } catch (err) {
1028
1096
  if (err instanceof CliError) throw err;
@@ -1049,9 +1117,10 @@ async function runWatch(args, global, output) {
1049
1117
  const configPath = global.config === null ? null : resolve(global.root, global.config);
1050
1118
  const extraPaths = configPath !== null && !configPath.startsWith(`${resolve(global.root)}/`) ? [configPath] : [];
1051
1119
  const ignored = [];
1052
- if (args.output !== void 0) {
1053
- const out = resolve(global.root, args.output);
1054
- if (out.startsWith(`${resolve(global.root)}/`)) ignored.push(out);
1120
+ for (const file of [args.output, args.report]) {
1121
+ if (file === void 0) continue;
1122
+ const abs = resolve(global.root, file);
1123
+ if (abs.startsWith(`${resolve(global.root)}/`)) ignored.push(abs);
1055
1124
  }
1056
1125
  return await watchLoop({
1057
1126
  root: global.root,
@@ -1259,6 +1328,7 @@ function writeRunOutput(opts) {
1259
1328
  planId,
1260
1329
  status: runStatus,
1261
1330
  steps,
1331
+ ...opts.detected !== void 0 ? { detected: opts.detected } : {},
1262
1332
  ...evalResult ? {
1263
1333
  findings: evalResult.findings.length,
1264
1334
  verdict: evalResult.verdict,
@@ -1266,7 +1336,8 @@ function writeRunOutput(opts) {
1266
1336
  } : {},
1267
1337
  ...report !== void 0 ? { report: {
1268
1338
  json: report.json,
1269
- ...report.html !== null ? { html: report.html } : {}
1339
+ ...report.html !== null ? { html: report.html } : {},
1340
+ findings: report.findings
1270
1341
  } } : {}
1271
1342
  },
1272
1343
  durationMs
@@ -1341,26 +1412,27 @@ async function collectRunFindings(opts, warnings) {
1341
1412
  }
1342
1413
  /** Best-effort report.html — a render failure never fails the run; it is
1343
1414
  * recorded in the report's `warnings` instead of being dropped silently. */
1344
- async function writeReportHtml(dir, findings, warnings) {
1415
+ async function writeReportHtml(htmlPath, findings, warnings) {
1345
1416
  try {
1346
1417
  const { generateSarifHtml } = await import("@sverka/sarif-viewer-web");
1347
- const candidate = join(dir, "report.html");
1348
- writeFileSync(candidate, generateSarifHtml(findings), "utf-8");
1349
- return candidate;
1418
+ mkdirSync(dirname(htmlPath), { recursive: true });
1419
+ writeFileSync(htmlPath, generateSarifHtml(findings), "utf-8");
1420
+ return htmlPath;
1350
1421
  } catch (e) {
1351
1422
  warnings.push(`report.html generation failed: ${e instanceof Error ? e.message : String(e)}`);
1352
1423
  return null;
1353
1424
  }
1354
1425
  }
1355
- function runReportPayload(opts, warnings) {
1426
+ function runReportPayload(opts, findingsCount, warnings) {
1356
1427
  return {
1357
1428
  schema: "sverka.run/v1",
1358
1429
  data: {
1359
1430
  planId: opts.planId,
1360
1431
  status: opts.runStatus,
1361
1432
  steps: boundStepOutput(summarizeSteps(opts.events)),
1433
+ ...opts.detected !== void 0 ? { detected: opts.detected } : {},
1434
+ findings: findingsCount,
1362
1435
  ...opts.evalResult ? {
1363
- findings: opts.evalResult.findings.length,
1364
1436
  verdict: opts.evalResult.verdict,
1365
1437
  summary: opts.evalResult.summary
1366
1438
  } : {},
@@ -1375,11 +1447,13 @@ function runReportPayload(opts, warnings) {
1375
1447
  async function writeRunArtifacts(opts) {
1376
1448
  const dir = reportDir(opts.root, opts.runId);
1377
1449
  const warnings = [];
1378
- const htmlPath = await writeReportHtml(dir, await collectRunFindings(opts, warnings), warnings);
1379
- writeFileSync(join(dir, "report.json"), JSON.stringify(runReportPayload(opts, warnings), null, 2), "utf-8");
1450
+ const findings = await collectRunFindings(opts, warnings);
1451
+ const htmlPath = await writeReportHtml(opts.reportPath !== void 0 ? resolveUnderRoot(opts.root, opts.reportPath) : join(dir, "report.html"), findings, warnings);
1452
+ writeFileSync(join(dir, "report.json"), JSON.stringify(runReportPayload(opts, findings.length, warnings), null, 2), "utf-8");
1380
1453
  return {
1381
1454
  dir,
1382
- htmlPath
1455
+ htmlPath,
1456
+ findingsCount: findings.length
1383
1457
  };
1384
1458
  }
1385
1459
  function exitCodeForStatus(runStatus) {
@@ -2228,6 +2302,9 @@ function addRunCommand(y) {
2228
2302
  type: "string",
2229
2303
  alias: "o",
2230
2304
  describe: "Output file path for HTML report (implies --format html)"
2305
+ }).option("report", {
2306
+ type: "string",
2307
+ describe: "Relocate the per-run HTML report (report.json stays under .sverka/runs/)"
2231
2308
  }).option("tui", {
2232
2309
  type: "boolean",
2233
2310
  describe: "Interactive terminal UI (off by default; opt-in only)"
@@ -2395,6 +2472,7 @@ function dispatchRun(parsed, global, output, start) {
2395
2472
  }
2396
2473
  if (typeof parsed.entry === "string") args.entryId = parsed.entry;
2397
2474
  if (typeof parsed.output === "string") args.output = parsed.output;
2475
+ if (typeof parsed.report === "string") args.report = parsed.report;
2398
2476
  if (parsed.tui === true) args.tui = true;
2399
2477
  if (parsed.tui === false) args.tui = false;
2400
2478
  if (parsed.watch === true) args.watch = true;