@tested/cli 0.1.9 → 0.1.10

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/README.md CHANGED
@@ -25,7 +25,7 @@ CI uses the composite Action (`tested-hq/cli/action@main`). It installs `@tested
25
25
  ```yaml
26
26
  - uses: tested-hq/cli/action@main
27
27
  with:
28
- version: 0.1.9
28
+ version: 0.1.10
29
29
  token: ${{ secrets.TESTED_TOKEN }}
30
30
  ```
31
31
 
@@ -195,7 +195,7 @@ Or pass `--file` repeatedly / Action `files`. A CI matrix must not conclude the
195
195
  ```yaml
196
196
  - uses: tested-hq/cli/action@main
197
197
  with:
198
- version: 0.1.9
198
+ version: 0.1.10
199
199
  push: true
200
200
  pr-number: ${{ github.event.pull_request.number }}
201
201
  token: ${{ secrets.TESTED_TOKEN }}
package/dist/td.js CHANGED
@@ -1106,6 +1106,9 @@ var FlagConfigSchema = z2.object({
1106
1106
  paths: z2.array(z2.string().min(1)).min(1),
1107
1107
  thresholds: FlagThresholdsSchema.optional()
1108
1108
  });
1109
+ var PathThresholdSchema = FlagThresholdsSchema.extend({
1110
+ glob: z2.string().min(1)
1111
+ });
1109
1112
  var TestedConfigSchema = z2.object({
1110
1113
  ignores: z2.array(z2.string()).default([]),
1111
1114
  coverage: z2.object({
@@ -1125,7 +1128,12 @@ var TestedConfigSchema = z2.object({
1125
1128
  // doesn't silently drop the field.
1126
1129
  thresholds: z2.object({
1127
1130
  patch: z2.number().min(0).max(100),
1128
- project: z2.number().min(0).max(100)
1131
+ project: z2.number().min(0).max(100),
1132
+ /**
1133
+ * Per-path glob floors. Graded from this run's matched files only.
1134
+ * A glob with no files this run is skipped (not 0%). Not flags.
1135
+ */
1136
+ paths: z2.array(PathThresholdSchema).optional()
1129
1137
  }).optional(),
1130
1138
  /**
1131
1139
  * Per-package gates. Each flag is graded from this run's coverage files
@@ -1186,6 +1194,16 @@ var FlagResultJsonSchema = z2.object({
1186
1194
  project: FlagMetricJsonSchema
1187
1195
  });
1188
1196
  var FlagsJsonMapSchema = z2.record(FlagNameSchema, FlagResultJsonSchema);
1197
+ var PathResultJsonSchema = z2.object({
1198
+ glob: z2.string().min(1),
1199
+ status: z2.enum(["pass", "fail", "missing"]),
1200
+ present: z2.boolean(),
1201
+ skipped: z2.literal(true).optional(),
1202
+ reason: z2.string().optional(),
1203
+ patch: FlagMetricJsonSchema,
1204
+ project: FlagMetricJsonSchema
1205
+ });
1206
+ var PathsJsonSchema = z2.array(PathResultJsonSchema);
1189
1207
 
1190
1208
  // src/config.ts
1191
1209
  var DEFAULT_IGNORES = [
@@ -1700,19 +1718,28 @@ async function computeDiffContext(opts) {
1700
1718
  return { diff, files, addedByFile };
1701
1719
  }
1702
1720
 
1703
- // src/core/flags.ts
1721
+ // src/core/globs.ts
1704
1722
  import { minimatch as minimatch2 } from "minimatch";
1705
- var MISSING_FLAG_REASON = "no coverage files matched this flag in this run";
1706
- var SCOPED_MISSING_FLAG_REASON = "no coverage files in this run for this flag";
1707
1723
  var MATCH_OPTS = { dot: true, matchBase: true };
1708
- function pathMatchesFlag(filePath, patterns) {
1709
- const normalized = filePath.replace(/\\/g, "/");
1710
- return patterns.some(
1711
- (p) => minimatch2(normalized, p, MATCH_OPTS) || minimatch2(normalized, `**/${p}`, MATCH_OPTS)
1712
- );
1724
+ function normalizeRepoPath(filePath) {
1725
+ return filePath.replace(/\\/g, "/");
1726
+ }
1727
+ function pathMatchesGlob(filePath, pattern) {
1728
+ const normalized = normalizeRepoPath(filePath);
1729
+ return minimatch2(normalized, pattern, MATCH_OPTS) || minimatch2(normalized, `**/${pattern}`, MATCH_OPTS);
1730
+ }
1731
+ function pathMatchesAnyGlob(filePath, patterns) {
1732
+ return patterns.some((p) => pathMatchesGlob(filePath, p));
1713
1733
  }
1734
+ function filterFilesByGlobs(files, patterns) {
1735
+ return files.filter((f) => pathMatchesAnyGlob(f.path, patterns));
1736
+ }
1737
+
1738
+ // src/core/flags.ts
1739
+ var MISSING_FLAG_REASON = "no coverage files matched this flag in this run";
1740
+ var SCOPED_MISSING_FLAG_REASON = "no coverage files in this run for this flag";
1714
1741
  function filterFilesByFlag(files, patterns) {
1715
- return files.filter((f) => pathMatchesFlag(f.path, patterns));
1742
+ return filterFilesByGlobs(files, patterns);
1716
1743
  }
1717
1744
  function resolveFlagThresholds(flag, global) {
1718
1745
  return {
@@ -3099,7 +3126,7 @@ import pc3 from "picocolors";
3099
3126
  // package.json
3100
3127
  var package_default = {
3101
3128
  name: "@tested/cli",
3102
- version: "0.1.9",
3129
+ version: "0.1.10",
3103
3130
  description: "Coverage your agent can use. CLI for patch + project coverage with agent-readable JSON output.",
3104
3131
  license: "MIT",
3105
3132
  homepage: "https://tested.dev",
@@ -3509,6 +3536,116 @@ function registerRunCommand(program2) {
3509
3536
  // src/commands/diff.ts
3510
3537
  import "commander";
3511
3538
 
3539
+ // src/core/path-thresholds.ts
3540
+ var MISSING_PATH_REASON = "no coverage files matched this path glob in this run";
3541
+ function resolvePathThresholds(entry, global) {
3542
+ return {
3543
+ patch: entry.patch ?? global.patch,
3544
+ project: entry.project ?? global.project
3545
+ };
3546
+ }
3547
+ function totalsToMetric2(totals, threshold, kind) {
3548
+ const empty = isEmptyPatch(totals);
3549
+ if (kind === "patch" && empty) {
3550
+ return {
3551
+ pct: totals.pct,
3552
+ threshold,
3553
+ pass: true,
3554
+ executable: totals.executable,
3555
+ covered: totals.covered,
3556
+ skipped: true,
3557
+ reason: EMPTY_PATCH_REASON
3558
+ };
3559
+ }
3560
+ const pass = totals.pct >= threshold;
3561
+ return {
3562
+ pct: totals.pct,
3563
+ threshold,
3564
+ pass,
3565
+ executable: totals.executable,
3566
+ covered: totals.covered
3567
+ };
3568
+ }
3569
+ function missingMetric2(threshold, reason) {
3570
+ return {
3571
+ threshold,
3572
+ skipped: true,
3573
+ reason
3574
+ };
3575
+ }
3576
+ function evaluatePresentPath(glob, files, addedByFile, thresholds) {
3577
+ const patch = computePatchCoverage(files, addedByFile);
3578
+ const project = computeProjectCoverage(files);
3579
+ const patchMetric = totalsToMetric2(patch.totals, thresholds.patch, "patch");
3580
+ const projectMetric = totalsToMetric2(project.totals, thresholds.project, "project");
3581
+ const status = patchMetric.pass && projectMetric.pass ? "pass" : "fail";
3582
+ return {
3583
+ glob,
3584
+ present: true,
3585
+ status,
3586
+ patch: patchMetric,
3587
+ project: projectMetric
3588
+ };
3589
+ }
3590
+ function missingPath(glob, thresholds, reason) {
3591
+ return {
3592
+ glob,
3593
+ present: false,
3594
+ status: "missing",
3595
+ reason,
3596
+ patch: missingMetric2(thresholds.patch, reason),
3597
+ project: missingMetric2(thresholds.project, reason)
3598
+ };
3599
+ }
3600
+ function evaluatePathThresholds(input) {
3601
+ const global = input.config.thresholds;
3602
+ const entries = global?.paths;
3603
+ if (!global || !entries || entries.length === 0) return [];
3604
+ return entries.map((entry) => {
3605
+ const thresholds = resolvePathThresholds(entry, global);
3606
+ const matched = filterFilesByGlobs(input.files, [entry.glob]);
3607
+ if (matched.length === 0) {
3608
+ return missingPath(entry.glob, thresholds, MISSING_PATH_REASON);
3609
+ }
3610
+ return evaluatePresentPath(entry.glob, matched, input.addedByFile, thresholds);
3611
+ });
3612
+ }
3613
+ function pathThresholdsPass(results) {
3614
+ return results.every((p) => p.status !== "fail");
3615
+ }
3616
+ function pathThresholdsToJson(results) {
3617
+ return results.map((path) => ({
3618
+ glob: path.glob,
3619
+ status: path.status,
3620
+ present: path.present,
3621
+ ...path.status === "missing" ? { skipped: true } : {},
3622
+ ...path.reason ? { reason: path.reason } : {},
3623
+ patch: metricToJson2(path.patch),
3624
+ project: metricToJson2(path.project)
3625
+ }));
3626
+ }
3627
+ function metricToJson2(metric) {
3628
+ if (metric.skipped) {
3629
+ return {
3630
+ threshold: metric.threshold,
3631
+ skipped: true,
3632
+ ...metric.reason ? { reason: metric.reason } : {}
3633
+ };
3634
+ }
3635
+ return {
3636
+ pct: metric.pct ?? 0,
3637
+ threshold: metric.threshold,
3638
+ pass: metric.pass ?? false,
3639
+ executable: metric.executable ?? 0,
3640
+ covered: metric.covered ?? 0,
3641
+ ...metric.reason ? { reason: metric.reason } : {}
3642
+ };
3643
+ }
3644
+ function resolvePathThresholdsJson(input) {
3645
+ const results = evaluatePathThresholds(input);
3646
+ return results.length > 0 ? pathThresholdsToJson(results) : void 0;
3647
+ }
3648
+
3512
3649
  // src/output/human.ts
3513
3650
  function formatRange(r) {
3514
3651
  return r.start === r.end ? `${r.start}` : `${r.start}-${r.end}`;
@@ -3656,7 +3793,12 @@ function registerDiffCommand(program2) {
3656
3793
  });
3657
3794
  if (opts.json) {
3658
3795
  const flags = resolveFlagsJson({ config, files, addedByFile });
3659
- const payload = flags ? { ...diff, flags } : diff;
3796
+ const paths = resolvePathThresholdsJson({ config, files, addedByFile });
3797
+ const payload = {
3798
+ ...diff,
3799
+ ...flags ? { flags } : {},
3800
+ ...paths ? { paths } : {}
3801
+ };
3660
3802
  process.stdout.write(JSON.stringify(payload, null, 2) + "\n");
3661
3803
  } else {
3662
3804
  process.stdout.write(
@@ -3686,6 +3828,7 @@ function formatIncompleteCheck(state, json) {
3686
3828
  projectPass: true,
3687
3829
  overall: "pass",
3688
3830
  flagResults: [],
3831
+ pathResults: [],
3689
3832
  stdout: JSON.stringify({
3690
3833
  overall: "pending",
3691
3834
  complete: false,
@@ -3703,6 +3846,7 @@ function formatIncompleteCheck(state, json) {
3703
3846
  projectPass: true,
3704
3847
  overall: "pass",
3705
3848
  flagResults: [],
3849
+ pathResults: [],
3706
3850
  stdout: "",
3707
3851
  stderr: `${dim("tested.dev \u2014 coverage gate")} ${badge("info")}
3708
3852
 
@@ -3721,6 +3865,7 @@ function formatCompleteHandshakeCheck(json) {
3721
3865
  projectPass: true,
3722
3866
  overall: "pass",
3723
3867
  flagResults: [],
3868
+ pathResults: [],
3724
3869
  stdout: JSON.stringify({ overall: "pending", complete: true, note }) + "\n",
3725
3870
  stderr: "",
3726
3871
  exitCode: 0
@@ -3732,6 +3877,7 @@ function formatCompleteHandshakeCheck(json) {
3732
3877
  projectPass: true,
3733
3878
  overall: "pass",
3734
3879
  flagResults: [],
3880
+ pathResults: [],
3735
3881
  stdout: "",
3736
3882
  stderr: `${dim("tested.dev \u2014 coverage gate")} ${badge("info")}
3737
3883
 
@@ -3747,7 +3893,7 @@ function formatMetricLine(label, pct3, threshold, pass, indent = " ") {
3747
3893
  }
3748
3894
  function presentPct(metric) {
3749
3895
  if (metric.pct === void 0 || metric.pass === void 0) {
3750
- throw new Error("present flag metric missing pct/pass");
3896
+ throw new Error("present metric missing pct/pass");
3751
3897
  }
3752
3898
  return { pct: metric.pct, pass: metric.pass };
3753
3899
  }
@@ -3785,6 +3931,40 @@ function formatFlagLines(results) {
3785
3931
  }
3786
3932
  return lines;
3787
3933
  }
3934
+ function formatPathLines(results) {
3935
+ if (results.length === 0) return [];
3936
+ const lines = [""];
3937
+ for (const path of results) {
3938
+ if (path.status === "missing") {
3939
+ lines.push(
3940
+ ` ${path.glob} ${dim(path.reason ?? "missing this run")} ${badge("missing")}`
3941
+ );
3942
+ continue;
3943
+ }
3944
+ lines.push(` ${path.glob} ${path.status === "pass" ? badge("pass") : badge("fail")}`);
3945
+ if (path.patch.skipped) {
3946
+ lines.push(
3947
+ ` ${"Patch".padEnd(8)} ${dim("-")} ${EMPTY_PATCH_REASON} ${badge("skip")}`
3948
+ );
3949
+ } else {
3950
+ const patch = presentPct(path.patch);
3951
+ lines.push(
3952
+ formatMetricLine("Patch", patch.pct, path.patch.threshold, patch.pass, " ")
3953
+ );
3954
+ }
3955
+ const project = presentPct(path.project);
3956
+ lines.push(
3957
+ formatMetricLine(
3958
+ "Project",
3959
+ project.pct,
3960
+ path.project.threshold,
3961
+ project.pass,
3962
+ " "
3963
+ )
3964
+ );
3965
+ }
3966
+ return lines;
3967
+ }
3788
3968
  function runCheck(input) {
3789
3969
  const { config, diff, json } = input;
3790
3970
  if (!config.thresholds) {
@@ -3794,6 +3974,7 @@ function runCheck(input) {
3794
3974
  projectPass: true,
3795
3975
  overall: "pass",
3796
3976
  flagResults: [],
3977
+ pathResults: [],
3797
3978
  stdout: "",
3798
3979
  stderr: `${dim("tested.dev \u2014 coverage gate")} ${badge("info")}
3799
3980
 
@@ -3816,8 +3997,14 @@ ${tip("add thresholds.patch / thresholds.project to enforce")}
3816
3997
  addedByFile: input.addedByFile ?? /* @__PURE__ */ new Map(),
3817
3998
  ...input.onlyFlag !== void 0 ? { onlyFlag: input.onlyFlag } : {}
3818
3999
  });
4000
+ const pathResults = evaluatePathThresholds({
4001
+ config,
4002
+ files: input.files ?? [],
4003
+ addedByFile: input.addedByFile ?? /* @__PURE__ */ new Map()
4004
+ });
3819
4005
  const flagsOk = flagsPass(flagResults);
3820
- const overall = patchPass && projectPass && flagsOk ? "pass" : "fail";
4006
+ const pathsOk = pathThresholdsPass(pathResults);
4007
+ const overall = patchPass && projectPass && flagsOk && pathsOk ? "pass" : "fail";
3821
4008
  const exitCode = overall === "pass" ? 0 : 1;
3822
4009
  if (json) {
3823
4010
  const payload = {
@@ -3829,6 +4016,7 @@ ${tip("add thresholds.patch / thresholds.project to enforce")}
3829
4016
  },
3830
4017
  project: { pct: projectPct, threshold: projectThreshold, pass: projectPass },
3831
4018
  ...flagResults.length > 0 ? { flags: flagsToJson(flagResults) } : {},
4019
+ ...pathResults.length > 0 ? { paths: pathThresholdsToJson(pathResults) } : {},
3832
4020
  overall,
3833
4021
  ...patchSkipped ? { note: EMPTY_PATCH_REASON } : {}
3834
4022
  };
@@ -3838,6 +4026,7 @@ ${tip("add thresholds.patch / thresholds.project to enforce")}
3838
4026
  projectPass,
3839
4027
  overall,
3840
4028
  flagResults,
4029
+ pathResults,
3841
4030
  stdout: JSON.stringify(payload) + "\n",
3842
4031
  stderr: "",
3843
4032
  exitCode
@@ -3857,6 +4046,7 @@ ${tip("add thresholds.patch / thresholds.project to enforce")}
3857
4046
  }
3858
4047
  lines.push(formatMetricLine("Project", projectPct, projectThreshold, projectPass));
3859
4048
  lines.push(...formatFlagLines(flagResults));
4049
+ lines.push(...formatPathLines(pathResults));
3860
4050
  if (overall === "fail") {
3861
4051
  lines.push("");
3862
4052
  if (patchSkipped) {
@@ -3870,6 +4060,12 @@ ${tip("add thresholds.patch / thresholds.project to enforce")}
3870
4060
  )
3871
4061
  );
3872
4062
  }
4063
+ const missingPaths = pathResults.filter((p) => p.status === "missing");
4064
+ if (missingPaths.length > 0) {
4065
+ lines.push(
4066
+ dim("Path globs with no files this run are skipped (not 0%).")
4067
+ );
4068
+ }
3873
4069
  lines.push(tip("add tests for uncovered ranges: tested diff"));
3874
4070
  } else {
3875
4071
  lines.push("");
@@ -3886,6 +4082,7 @@ ${tip("add thresholds.patch / thresholds.project to enforce")}
3886
4082
  projectPass,
3887
4083
  overall,
3888
4084
  flagResults,
4085
+ pathResults,
3889
4086
  stdout: lines.join("\n"),
3890
4087
  stderr: "",
3891
4088
  exitCode
@@ -3893,7 +4090,7 @@ ${tip("add thresholds.patch / thresholds.project to enforce")}
3893
4090
  }
3894
4091
  function registerCheckCommand(program2) {
3895
4092
  program2.command("check").description(
3896
- "Exit non-zero if patch or project coverage falls below configured thresholds."
4093
+ "Exit non-zero if patch, project, or path coverage falls below configured thresholds."
3897
4094
  ).option("--json", "Emit machine-readable JSON to stdout (exit code unchanged).", false).option("--base <ref>", "Git base ref to diff against", void 0).option(
3898
4095
  "--file <path>",
3899
4096
  "Coverage file to merge (repeatable). Overrides coverage.path.",
package/dist/tested.js CHANGED
@@ -1106,6 +1106,9 @@ var FlagConfigSchema = z2.object({
1106
1106
  paths: z2.array(z2.string().min(1)).min(1),
1107
1107
  thresholds: FlagThresholdsSchema.optional()
1108
1108
  });
1109
+ var PathThresholdSchema = FlagThresholdsSchema.extend({
1110
+ glob: z2.string().min(1)
1111
+ });
1109
1112
  var TestedConfigSchema = z2.object({
1110
1113
  ignores: z2.array(z2.string()).default([]),
1111
1114
  coverage: z2.object({
@@ -1125,7 +1128,12 @@ var TestedConfigSchema = z2.object({
1125
1128
  // doesn't silently drop the field.
1126
1129
  thresholds: z2.object({
1127
1130
  patch: z2.number().min(0).max(100),
1128
- project: z2.number().min(0).max(100)
1131
+ project: z2.number().min(0).max(100),
1132
+ /**
1133
+ * Per-path glob floors. Graded from this run's matched files only.
1134
+ * A glob with no files this run is skipped (not 0%). Not flags.
1135
+ */
1136
+ paths: z2.array(PathThresholdSchema).optional()
1129
1137
  }).optional(),
1130
1138
  /**
1131
1139
  * Per-package gates. Each flag is graded from this run's coverage files
@@ -1186,6 +1194,16 @@ var FlagResultJsonSchema = z2.object({
1186
1194
  project: FlagMetricJsonSchema
1187
1195
  });
1188
1196
  var FlagsJsonMapSchema = z2.record(FlagNameSchema, FlagResultJsonSchema);
1197
+ var PathResultJsonSchema = z2.object({
1198
+ glob: z2.string().min(1),
1199
+ status: z2.enum(["pass", "fail", "missing"]),
1200
+ present: z2.boolean(),
1201
+ skipped: z2.literal(true).optional(),
1202
+ reason: z2.string().optional(),
1203
+ patch: FlagMetricJsonSchema,
1204
+ project: FlagMetricJsonSchema
1205
+ });
1206
+ var PathsJsonSchema = z2.array(PathResultJsonSchema);
1189
1207
 
1190
1208
  // src/config.ts
1191
1209
  var DEFAULT_IGNORES = [
@@ -1700,19 +1718,28 @@ async function computeDiffContext(opts) {
1700
1718
  return { diff, files, addedByFile };
1701
1719
  }
1702
1720
 
1703
- // src/core/flags.ts
1721
+ // src/core/globs.ts
1704
1722
  import { minimatch as minimatch2 } from "minimatch";
1705
- var MISSING_FLAG_REASON = "no coverage files matched this flag in this run";
1706
- var SCOPED_MISSING_FLAG_REASON = "no coverage files in this run for this flag";
1707
1723
  var MATCH_OPTS = { dot: true, matchBase: true };
1708
- function pathMatchesFlag(filePath, patterns) {
1709
- const normalized = filePath.replace(/\\/g, "/");
1710
- return patterns.some(
1711
- (p) => minimatch2(normalized, p, MATCH_OPTS) || minimatch2(normalized, `**/${p}`, MATCH_OPTS)
1712
- );
1724
+ function normalizeRepoPath(filePath) {
1725
+ return filePath.replace(/\\/g, "/");
1726
+ }
1727
+ function pathMatchesGlob(filePath, pattern) {
1728
+ const normalized = normalizeRepoPath(filePath);
1729
+ return minimatch2(normalized, pattern, MATCH_OPTS) || minimatch2(normalized, `**/${pattern}`, MATCH_OPTS);
1730
+ }
1731
+ function pathMatchesAnyGlob(filePath, patterns) {
1732
+ return patterns.some((p) => pathMatchesGlob(filePath, p));
1713
1733
  }
1734
+ function filterFilesByGlobs(files, patterns) {
1735
+ return files.filter((f) => pathMatchesAnyGlob(f.path, patterns));
1736
+ }
1737
+
1738
+ // src/core/flags.ts
1739
+ var MISSING_FLAG_REASON = "no coverage files matched this flag in this run";
1740
+ var SCOPED_MISSING_FLAG_REASON = "no coverage files in this run for this flag";
1714
1741
  function filterFilesByFlag(files, patterns) {
1715
- return files.filter((f) => pathMatchesFlag(f.path, patterns));
1742
+ return filterFilesByGlobs(files, patterns);
1716
1743
  }
1717
1744
  function resolveFlagThresholds(flag, global) {
1718
1745
  return {
@@ -3099,7 +3126,7 @@ import pc3 from "picocolors";
3099
3126
  // package.json
3100
3127
  var package_default = {
3101
3128
  name: "@tested/cli",
3102
- version: "0.1.9",
3129
+ version: "0.1.10",
3103
3130
  description: "Coverage your agent can use. CLI for patch + project coverage with agent-readable JSON output.",
3104
3131
  license: "MIT",
3105
3132
  homepage: "https://tested.dev",
@@ -3509,6 +3536,116 @@ function registerRunCommand(program2) {
3509
3536
  // src/commands/diff.ts
3510
3537
  import "commander";
3511
3538
 
3539
+ // src/core/path-thresholds.ts
3540
+ var MISSING_PATH_REASON = "no coverage files matched this path glob in this run";
3541
+ function resolvePathThresholds(entry, global) {
3542
+ return {
3543
+ patch: entry.patch ?? global.patch,
3544
+ project: entry.project ?? global.project
3545
+ };
3546
+ }
3547
+ function totalsToMetric2(totals, threshold, kind) {
3548
+ const empty = isEmptyPatch(totals);
3549
+ if (kind === "patch" && empty) {
3550
+ return {
3551
+ pct: totals.pct,
3552
+ threshold,
3553
+ pass: true,
3554
+ executable: totals.executable,
3555
+ covered: totals.covered,
3556
+ skipped: true,
3557
+ reason: EMPTY_PATCH_REASON
3558
+ };
3559
+ }
3560
+ const pass = totals.pct >= threshold;
3561
+ return {
3562
+ pct: totals.pct,
3563
+ threshold,
3564
+ pass,
3565
+ executable: totals.executable,
3566
+ covered: totals.covered
3567
+ };
3568
+ }
3569
+ function missingMetric2(threshold, reason) {
3570
+ return {
3571
+ threshold,
3572
+ skipped: true,
3573
+ reason
3574
+ };
3575
+ }
3576
+ function evaluatePresentPath(glob, files, addedByFile, thresholds) {
3577
+ const patch = computePatchCoverage(files, addedByFile);
3578
+ const project = computeProjectCoverage(files);
3579
+ const patchMetric = totalsToMetric2(patch.totals, thresholds.patch, "patch");
3580
+ const projectMetric = totalsToMetric2(project.totals, thresholds.project, "project");
3581
+ const status = patchMetric.pass && projectMetric.pass ? "pass" : "fail";
3582
+ return {
3583
+ glob,
3584
+ present: true,
3585
+ status,
3586
+ patch: patchMetric,
3587
+ project: projectMetric
3588
+ };
3589
+ }
3590
+ function missingPath(glob, thresholds, reason) {
3591
+ return {
3592
+ glob,
3593
+ present: false,
3594
+ status: "missing",
3595
+ reason,
3596
+ patch: missingMetric2(thresholds.patch, reason),
3597
+ project: missingMetric2(thresholds.project, reason)
3598
+ };
3599
+ }
3600
+ function evaluatePathThresholds(input) {
3601
+ const global = input.config.thresholds;
3602
+ const entries = global?.paths;
3603
+ if (!global || !entries || entries.length === 0) return [];
3604
+ return entries.map((entry) => {
3605
+ const thresholds = resolvePathThresholds(entry, global);
3606
+ const matched = filterFilesByGlobs(input.files, [entry.glob]);
3607
+ if (matched.length === 0) {
3608
+ return missingPath(entry.glob, thresholds, MISSING_PATH_REASON);
3609
+ }
3610
+ return evaluatePresentPath(entry.glob, matched, input.addedByFile, thresholds);
3611
+ });
3612
+ }
3613
+ function pathThresholdsPass(results) {
3614
+ return results.every((p) => p.status !== "fail");
3615
+ }
3616
+ function pathThresholdsToJson(results) {
3617
+ return results.map((path) => ({
3618
+ glob: path.glob,
3619
+ status: path.status,
3620
+ present: path.present,
3621
+ ...path.status === "missing" ? { skipped: true } : {},
3622
+ ...path.reason ? { reason: path.reason } : {},
3623
+ patch: metricToJson2(path.patch),
3624
+ project: metricToJson2(path.project)
3625
+ }));
3626
+ }
3627
+ function metricToJson2(metric) {
3628
+ if (metric.skipped) {
3629
+ return {
3630
+ threshold: metric.threshold,
3631
+ skipped: true,
3632
+ ...metric.reason ? { reason: metric.reason } : {}
3633
+ };
3634
+ }
3635
+ return {
3636
+ pct: metric.pct ?? 0,
3637
+ threshold: metric.threshold,
3638
+ pass: metric.pass ?? false,
3639
+ executable: metric.executable ?? 0,
3640
+ covered: metric.covered ?? 0,
3641
+ ...metric.reason ? { reason: metric.reason } : {}
3642
+ };
3643
+ }
3644
+ function resolvePathThresholdsJson(input) {
3645
+ const results = evaluatePathThresholds(input);
3646
+ return results.length > 0 ? pathThresholdsToJson(results) : void 0;
3647
+ }
3648
+
3512
3649
  // src/output/human.ts
3513
3650
  function formatRange(r) {
3514
3651
  return r.start === r.end ? `${r.start}` : `${r.start}-${r.end}`;
@@ -3656,7 +3793,12 @@ function registerDiffCommand(program2) {
3656
3793
  });
3657
3794
  if (opts.json) {
3658
3795
  const flags = resolveFlagsJson({ config, files, addedByFile });
3659
- const payload = flags ? { ...diff, flags } : diff;
3796
+ const paths = resolvePathThresholdsJson({ config, files, addedByFile });
3797
+ const payload = {
3798
+ ...diff,
3799
+ ...flags ? { flags } : {},
3800
+ ...paths ? { paths } : {}
3801
+ };
3660
3802
  process.stdout.write(JSON.stringify(payload, null, 2) + "\n");
3661
3803
  } else {
3662
3804
  process.stdout.write(
@@ -3686,6 +3828,7 @@ function formatIncompleteCheck(state, json) {
3686
3828
  projectPass: true,
3687
3829
  overall: "pass",
3688
3830
  flagResults: [],
3831
+ pathResults: [],
3689
3832
  stdout: JSON.stringify({
3690
3833
  overall: "pending",
3691
3834
  complete: false,
@@ -3703,6 +3846,7 @@ function formatIncompleteCheck(state, json) {
3703
3846
  projectPass: true,
3704
3847
  overall: "pass",
3705
3848
  flagResults: [],
3849
+ pathResults: [],
3706
3850
  stdout: "",
3707
3851
  stderr: `${dim("tested.dev \u2014 coverage gate")} ${badge("info")}
3708
3852
 
@@ -3721,6 +3865,7 @@ function formatCompleteHandshakeCheck(json) {
3721
3865
  projectPass: true,
3722
3866
  overall: "pass",
3723
3867
  flagResults: [],
3868
+ pathResults: [],
3724
3869
  stdout: JSON.stringify({ overall: "pending", complete: true, note }) + "\n",
3725
3870
  stderr: "",
3726
3871
  exitCode: 0
@@ -3732,6 +3877,7 @@ function formatCompleteHandshakeCheck(json) {
3732
3877
  projectPass: true,
3733
3878
  overall: "pass",
3734
3879
  flagResults: [],
3880
+ pathResults: [],
3735
3881
  stdout: "",
3736
3882
  stderr: `${dim("tested.dev \u2014 coverage gate")} ${badge("info")}
3737
3883
 
@@ -3747,7 +3893,7 @@ function formatMetricLine(label, pct3, threshold, pass, indent = " ") {
3747
3893
  }
3748
3894
  function presentPct(metric) {
3749
3895
  if (metric.pct === void 0 || metric.pass === void 0) {
3750
- throw new Error("present flag metric missing pct/pass");
3896
+ throw new Error("present metric missing pct/pass");
3751
3897
  }
3752
3898
  return { pct: metric.pct, pass: metric.pass };
3753
3899
  }
@@ -3785,6 +3931,40 @@ function formatFlagLines(results) {
3785
3931
  }
3786
3932
  return lines;
3787
3933
  }
3934
+ function formatPathLines(results) {
3935
+ if (results.length === 0) return [];
3936
+ const lines = [""];
3937
+ for (const path of results) {
3938
+ if (path.status === "missing") {
3939
+ lines.push(
3940
+ ` ${path.glob} ${dim(path.reason ?? "missing this run")} ${badge("missing")}`
3941
+ );
3942
+ continue;
3943
+ }
3944
+ lines.push(` ${path.glob} ${path.status === "pass" ? badge("pass") : badge("fail")}`);
3945
+ if (path.patch.skipped) {
3946
+ lines.push(
3947
+ ` ${"Patch".padEnd(8)} ${dim("-")} ${EMPTY_PATCH_REASON} ${badge("skip")}`
3948
+ );
3949
+ } else {
3950
+ const patch = presentPct(path.patch);
3951
+ lines.push(
3952
+ formatMetricLine("Patch", patch.pct, path.patch.threshold, patch.pass, " ")
3953
+ );
3954
+ }
3955
+ const project = presentPct(path.project);
3956
+ lines.push(
3957
+ formatMetricLine(
3958
+ "Project",
3959
+ project.pct,
3960
+ path.project.threshold,
3961
+ project.pass,
3962
+ " "
3963
+ )
3964
+ );
3965
+ }
3966
+ return lines;
3967
+ }
3788
3968
  function runCheck(input) {
3789
3969
  const { config, diff, json } = input;
3790
3970
  if (!config.thresholds) {
@@ -3794,6 +3974,7 @@ function runCheck(input) {
3794
3974
  projectPass: true,
3795
3975
  overall: "pass",
3796
3976
  flagResults: [],
3977
+ pathResults: [],
3797
3978
  stdout: "",
3798
3979
  stderr: `${dim("tested.dev \u2014 coverage gate")} ${badge("info")}
3799
3980
 
@@ -3816,8 +3997,14 @@ ${tip("add thresholds.patch / thresholds.project to enforce")}
3816
3997
  addedByFile: input.addedByFile ?? /* @__PURE__ */ new Map(),
3817
3998
  ...input.onlyFlag !== void 0 ? { onlyFlag: input.onlyFlag } : {}
3818
3999
  });
4000
+ const pathResults = evaluatePathThresholds({
4001
+ config,
4002
+ files: input.files ?? [],
4003
+ addedByFile: input.addedByFile ?? /* @__PURE__ */ new Map()
4004
+ });
3819
4005
  const flagsOk = flagsPass(flagResults);
3820
- const overall = patchPass && projectPass && flagsOk ? "pass" : "fail";
4006
+ const pathsOk = pathThresholdsPass(pathResults);
4007
+ const overall = patchPass && projectPass && flagsOk && pathsOk ? "pass" : "fail";
3821
4008
  const exitCode = overall === "pass" ? 0 : 1;
3822
4009
  if (json) {
3823
4010
  const payload = {
@@ -3829,6 +4016,7 @@ ${tip("add thresholds.patch / thresholds.project to enforce")}
3829
4016
  },
3830
4017
  project: { pct: projectPct, threshold: projectThreshold, pass: projectPass },
3831
4018
  ...flagResults.length > 0 ? { flags: flagsToJson(flagResults) } : {},
4019
+ ...pathResults.length > 0 ? { paths: pathThresholdsToJson(pathResults) } : {},
3832
4020
  overall,
3833
4021
  ...patchSkipped ? { note: EMPTY_PATCH_REASON } : {}
3834
4022
  };
@@ -3838,6 +4026,7 @@ ${tip("add thresholds.patch / thresholds.project to enforce")}
3838
4026
  projectPass,
3839
4027
  overall,
3840
4028
  flagResults,
4029
+ pathResults,
3841
4030
  stdout: JSON.stringify(payload) + "\n",
3842
4031
  stderr: "",
3843
4032
  exitCode
@@ -3857,6 +4046,7 @@ ${tip("add thresholds.patch / thresholds.project to enforce")}
3857
4046
  }
3858
4047
  lines.push(formatMetricLine("Project", projectPct, projectThreshold, projectPass));
3859
4048
  lines.push(...formatFlagLines(flagResults));
4049
+ lines.push(...formatPathLines(pathResults));
3860
4050
  if (overall === "fail") {
3861
4051
  lines.push("");
3862
4052
  if (patchSkipped) {
@@ -3870,6 +4060,12 @@ ${tip("add thresholds.patch / thresholds.project to enforce")}
3870
4060
  )
3871
4061
  );
3872
4062
  }
4063
+ const missingPaths = pathResults.filter((p) => p.status === "missing");
4064
+ if (missingPaths.length > 0) {
4065
+ lines.push(
4066
+ dim("Path globs with no files this run are skipped (not 0%).")
4067
+ );
4068
+ }
3873
4069
  lines.push(tip("add tests for uncovered ranges: tested diff"));
3874
4070
  } else {
3875
4071
  lines.push("");
@@ -3886,6 +4082,7 @@ ${tip("add thresholds.patch / thresholds.project to enforce")}
3886
4082
  projectPass,
3887
4083
  overall,
3888
4084
  flagResults,
4085
+ pathResults,
3889
4086
  stdout: lines.join("\n"),
3890
4087
  stderr: "",
3891
4088
  exitCode
@@ -3893,7 +4090,7 @@ ${tip("add thresholds.patch / thresholds.project to enforce")}
3893
4090
  }
3894
4091
  function registerCheckCommand(program2) {
3895
4092
  program2.command("check").description(
3896
- "Exit non-zero if patch or project coverage falls below configured thresholds."
4093
+ "Exit non-zero if patch, project, or path coverage falls below configured thresholds."
3897
4094
  ).option("--json", "Emit machine-readable JSON to stdout (exit code unchanged).", false).option("--base <ref>", "Git base ref to diff against", void 0).option(
3898
4095
  "--file <path>",
3899
4096
  "Coverage file to merge (repeatable). Overrides coverage.path.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tested/cli",
3
- "version": "0.1.9",
3
+ "version": "0.1.10",
4
4
  "description": "Coverage your agent can use. CLI for patch + project coverage with agent-readable JSON output.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://tested.dev",