@tested/cli 0.1.8 → 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.8
28
+ version: 0.1.10
29
29
  token: ${{ secrets.TESTED_TOKEN }}
30
30
  ```
31
31
 
@@ -132,7 +132,7 @@ If `thresholds` is missing from `.tested.yaml`, `tested check` prints a notice o
132
132
 
133
133
  ### Flags (per package)
134
134
 
135
- Independent floors so a monorepo total cannot hide one package. Each flag is graded from **this run's** coverage files — no carryforward.
135
+ Independent floors so a monorepo total cannot hide one package. Each flag is graded from **this run's** coverage files.
136
136
 
137
137
  ```yaml
138
138
  flags:
@@ -144,11 +144,11 @@ flags:
144
144
  paths: ["apps/api/**"]
145
145
  ```
146
146
 
147
- `tested check` applies global `thresholds` plus each flag whose paths appear in the merged coverage. Per-flag patch is new executable lines in those paths. A flag with no files this run is **missing** (fail / pending) — never last week's numbers.
147
+ `tested check` applies global `thresholds` plus each flag whose paths appear in the merged coverage. Per-flag patch is new executable lines in those paths. A flag with no files this run is **skipped** (not 0%) — an affected-graph or scoped job that did not collect that package is not a fail.
148
148
 
149
- A job already scoped to one package: `tested check --flag frontend` (Action `flag:`). That coverage file **is** the flag.
149
+ A job already scoped to one package: `tested check --flag frontend` (Action `flag:`). That coverage file **is** the flag. Other packages are omitted from this upload.
150
150
 
151
- `--json` lists per-flag results (`flags.frontend.patchCheck` → `tested.dev / patch / frontend`). `tested push` sends that same `flags` map on ingest so the app can post those GitHub checks. `tested diff --json` includes it too.
151
+ `--json` lists per-flag results (`flags.frontend.patchCheck` → `tested.dev / patch / frontend`). Skipped flags have `status: missing` and `skipped: true` with no `executable` / `pct`. `tested push` omits skipped flags from ingest so the last successful upload for that package still stands. `tested diff --json` includes the same map as check.
152
152
 
153
153
  ## Coverage formats
154
154
 
@@ -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.8
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,11 +1128,16 @@ 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
1132
- * only — a missing path is missing (pending/fail), never last week's totals.
1140
+ * only — a missing path is skipped (not 0%), never another flag's totals.
1133
1141
  */
1134
1142
  flags: z2.record(FlagNameSchema, FlagConfigSchema).optional()
1135
1143
  });
@@ -1164,17 +1172,21 @@ var DiffOutputSchema = z2.object({
1164
1172
  ignored: z2.array(z2.string())
1165
1173
  });
1166
1174
  var FlagMetricJsonSchema = z2.object({
1167
- pct: z2.number().min(0).max(100),
1175
+ /** Omitted when this flag had no files this run (`skipped: true`). */
1176
+ pct: z2.number().min(0).max(100).optional(),
1168
1177
  threshold: z2.number().min(0).max(100),
1169
- pass: z2.boolean(),
1170
- executable: z2.number().int().nonnegative(),
1171
- covered: z2.number().int().nonnegative(),
1178
+ /** Omitted when skipped — a missing flag is not a 0% fail. */
1179
+ pass: z2.boolean().optional(),
1180
+ executable: z2.number().int().nonnegative().optional(),
1181
+ covered: z2.number().int().nonnegative().optional(),
1172
1182
  skipped: z2.literal(true).optional(),
1173
1183
  reason: z2.string().optional()
1174
1184
  });
1175
1185
  var FlagResultJsonSchema = z2.object({
1176
1186
  status: z2.enum(["pass", "fail", "missing"]),
1177
1187
  present: z2.boolean(),
1188
+ /** True when this flag had no coverage files this run (not a 0% result). */
1189
+ skipped: z2.literal(true).optional(),
1178
1190
  reason: z2.string().optional(),
1179
1191
  patchCheck: z2.string(),
1180
1192
  projectCheck: z2.string(),
@@ -1182,6 +1194,16 @@ var FlagResultJsonSchema = z2.object({
1182
1194
  project: FlagMetricJsonSchema
1183
1195
  });
1184
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);
1185
1207
 
1186
1208
  // src/config.ts
1187
1209
  var DEFAULT_IGNORES = [
@@ -1696,19 +1718,28 @@ async function computeDiffContext(opts) {
1696
1718
  return { diff, files, addedByFile };
1697
1719
  }
1698
1720
 
1699
- // src/core/flags.ts
1721
+ // src/core/globs.ts
1700
1722
  import { minimatch as minimatch2 } from "minimatch";
1701
- var MISSING_FLAG_REASON = "no coverage files matched this flag in this run";
1702
- var SCOPED_MISSING_FLAG_REASON = "no coverage files in this run for this flag";
1703
1723
  var MATCH_OPTS = { dot: true, matchBase: true };
1704
- function pathMatchesFlag(filePath, patterns) {
1705
- const normalized = filePath.replace(/\\/g, "/");
1706
- return patterns.some(
1707
- (p) => minimatch2(normalized, p, MATCH_OPTS) || minimatch2(normalized, `**/${p}`, MATCH_OPTS)
1708
- );
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));
1709
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";
1710
1741
  function filterFilesByFlag(files, patterns) {
1711
- return files.filter((f) => pathMatchesFlag(f.path, patterns));
1742
+ return filterFilesByGlobs(files, patterns);
1712
1743
  }
1713
1744
  function resolveFlagThresholds(flag, global) {
1714
1745
  return {
@@ -1742,13 +1773,11 @@ function totalsToMetric(totals, threshold, kind) {
1742
1773
  covered: totals.covered
1743
1774
  };
1744
1775
  }
1745
- function missingMetric(threshold) {
1776
+ function missingMetric(threshold, reason) {
1746
1777
  return {
1747
- pct: 0,
1748
1778
  threshold,
1749
- pass: false,
1750
- executable: 0,
1751
- covered: 0
1779
+ skipped: true,
1780
+ reason
1752
1781
  };
1753
1782
  }
1754
1783
  function checkSlug(kind, name) {
@@ -1778,8 +1807,8 @@ function missingFlag(name, thresholds, reason) {
1778
1807
  reason,
1779
1808
  patchCheck: checkSlug("patch", name),
1780
1809
  projectCheck: checkSlug("project", name),
1781
- patch: missingMetric(thresholds.patch),
1782
- project: missingMetric(thresholds.project)
1810
+ patch: missingMetric(thresholds.patch, reason),
1811
+ project: missingMetric(thresholds.project, reason)
1783
1812
  };
1784
1813
  }
1785
1814
  function evaluateFlags(input) {
@@ -1810,7 +1839,7 @@ function evaluateFlags(input) {
1810
1839
  return results;
1811
1840
  }
1812
1841
  function flagsPass(results) {
1813
- return results.every((f) => f.status === "pass");
1842
+ return results.every((f) => f.status !== "fail");
1814
1843
  }
1815
1844
  function flagsToJson(results) {
1816
1845
  const out = {};
@@ -1818,19 +1847,44 @@ function flagsToJson(results) {
1818
1847
  out[flag.name] = {
1819
1848
  status: flag.status,
1820
1849
  present: flag.present,
1850
+ ...flag.status === "missing" ? { skipped: true } : {},
1821
1851
  ...flag.reason ? { reason: flag.reason } : {},
1822
1852
  patchCheck: flag.patchCheck,
1823
1853
  projectCheck: flag.projectCheck,
1824
- patch: flag.patch,
1825
- project: flag.project
1854
+ patch: metricToJson(flag.patch),
1855
+ project: metricToJson(flag.project)
1826
1856
  };
1827
1857
  }
1828
1858
  return out;
1829
1859
  }
1860
+ function metricToJson(metric) {
1861
+ if (metric.skipped) {
1862
+ return {
1863
+ threshold: metric.threshold,
1864
+ skipped: true,
1865
+ ...metric.reason ? { reason: metric.reason } : {}
1866
+ };
1867
+ }
1868
+ return {
1869
+ pct: metric.pct ?? 0,
1870
+ threshold: metric.threshold,
1871
+ pass: metric.pass ?? false,
1872
+ executable: metric.executable ?? 0,
1873
+ covered: metric.covered ?? 0,
1874
+ ...metric.reason ? { reason: metric.reason } : {}
1875
+ };
1876
+ }
1877
+ function flagsToIngestJson(results) {
1878
+ const present = results.filter((f) => f.present);
1879
+ return present.length > 0 ? flagsToJson(present) : void 0;
1880
+ }
1830
1881
  function resolveFlagsJson(input) {
1831
1882
  const results = evaluateFlags(input);
1832
1883
  return results.length > 0 ? flagsToJson(results) : void 0;
1833
1884
  }
1885
+ function resolveIngestFlagsJson(input) {
1886
+ return flagsToIngestJson(evaluateFlags(input));
1887
+ }
1834
1888
 
1835
1889
  // src/core/coverage-merge.ts
1836
1890
  function emptyToUndef(value) {
@@ -2553,7 +2607,7 @@ async function executePush(cli, deps) {
2553
2607
  let flags;
2554
2608
  if (!handshakeOnly && !computeDiffFn) {
2555
2609
  try {
2556
- flags = resolveFlagsJson({
2610
+ flags = resolveIngestFlagsJson({
2557
2611
  config,
2558
2612
  files,
2559
2613
  addedByFile,
@@ -3072,7 +3126,7 @@ import pc3 from "picocolors";
3072
3126
  // package.json
3073
3127
  var package_default = {
3074
3128
  name: "@tested/cli",
3075
- version: "0.1.8",
3129
+ version: "0.1.10",
3076
3130
  description: "Coverage your agent can use. CLI for patch + project coverage with agent-readable JSON output.",
3077
3131
  license: "MIT",
3078
3132
  homepage: "https://tested.dev",
@@ -3482,6 +3536,116 @@ function registerRunCommand(program2) {
3482
3536
  // src/commands/diff.ts
3483
3537
  import "commander";
3484
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
+
3485
3649
  // src/output/human.ts
3486
3650
  function formatRange(r) {
3487
3651
  return r.start === r.end ? `${r.start}` : `${r.start}-${r.end}`;
@@ -3629,7 +3793,12 @@ function registerDiffCommand(program2) {
3629
3793
  });
3630
3794
  if (opts.json) {
3631
3795
  const flags = resolveFlagsJson({ config, files, addedByFile });
3632
- 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
+ };
3633
3802
  process.stdout.write(JSON.stringify(payload, null, 2) + "\n");
3634
3803
  } else {
3635
3804
  process.stdout.write(
@@ -3659,6 +3828,7 @@ function formatIncompleteCheck(state, json) {
3659
3828
  projectPass: true,
3660
3829
  overall: "pass",
3661
3830
  flagResults: [],
3831
+ pathResults: [],
3662
3832
  stdout: JSON.stringify({
3663
3833
  overall: "pending",
3664
3834
  complete: false,
@@ -3676,6 +3846,7 @@ function formatIncompleteCheck(state, json) {
3676
3846
  projectPass: true,
3677
3847
  overall: "pass",
3678
3848
  flagResults: [],
3849
+ pathResults: [],
3679
3850
  stdout: "",
3680
3851
  stderr: `${dim("tested.dev \u2014 coverage gate")} ${badge("info")}
3681
3852
 
@@ -3694,6 +3865,7 @@ function formatCompleteHandshakeCheck(json) {
3694
3865
  projectPass: true,
3695
3866
  overall: "pass",
3696
3867
  flagResults: [],
3868
+ pathResults: [],
3697
3869
  stdout: JSON.stringify({ overall: "pending", complete: true, note }) + "\n",
3698
3870
  stderr: "",
3699
3871
  exitCode: 0
@@ -3705,6 +3877,7 @@ function formatCompleteHandshakeCheck(json) {
3705
3877
  projectPass: true,
3706
3878
  overall: "pass",
3707
3879
  flagResults: [],
3880
+ pathResults: [],
3708
3881
  stdout: "",
3709
3882
  stderr: `${dim("tested.dev \u2014 coverage gate")} ${badge("info")}
3710
3883
 
@@ -3718,6 +3891,12 @@ function formatMetricLine(label, pct3, threshold, pass, indent = " ") {
3718
3891
  const status = pass ? badge("pass") : badge("fail");
3719
3892
  return `${indent}${label.padEnd(8)} ${pctStr}% (threshold ${threshold}) ${status}`;
3720
3893
  }
3894
+ function presentPct(metric) {
3895
+ if (metric.pct === void 0 || metric.pass === void 0) {
3896
+ throw new Error("present metric missing pct/pass");
3897
+ }
3898
+ return { pct: metric.pct, pass: metric.pass };
3899
+ }
3721
3900
  function formatFlagLines(results) {
3722
3901
  if (results.length === 0) return [];
3723
3902
  const lines = [""];
@@ -3734,16 +3913,52 @@ function formatFlagLines(results) {
3734
3913
  ` ${"Patch".padEnd(8)} ${dim("-")} ${EMPTY_PATCH_REASON} ${badge("skip")}`
3735
3914
  );
3736
3915
  } else {
3916
+ const patch = presentPct(flag.patch);
3737
3917
  lines.push(
3738
- formatMetricLine("Patch", flag.patch.pct, flag.patch.threshold, flag.patch.pass, " ")
3918
+ formatMetricLine("Patch", patch.pct, flag.patch.threshold, patch.pass, " ")
3739
3919
  );
3740
3920
  }
3921
+ const project = presentPct(flag.project);
3741
3922
  lines.push(
3742
3923
  formatMetricLine(
3743
3924
  "Project",
3744
- flag.project.pct,
3925
+ project.pct,
3745
3926
  flag.project.threshold,
3746
- flag.project.pass,
3927
+ project.pass,
3928
+ " "
3929
+ )
3930
+ );
3931
+ }
3932
+ return lines;
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,
3747
3962
  " "
3748
3963
  )
3749
3964
  );
@@ -3759,6 +3974,7 @@ function runCheck(input) {
3759
3974
  projectPass: true,
3760
3975
  overall: "pass",
3761
3976
  flagResults: [],
3977
+ pathResults: [],
3762
3978
  stdout: "",
3763
3979
  stderr: `${dim("tested.dev \u2014 coverage gate")} ${badge("info")}
3764
3980
 
@@ -3781,8 +3997,14 @@ ${tip("add thresholds.patch / thresholds.project to enforce")}
3781
3997
  addedByFile: input.addedByFile ?? /* @__PURE__ */ new Map(),
3782
3998
  ...input.onlyFlag !== void 0 ? { onlyFlag: input.onlyFlag } : {}
3783
3999
  });
4000
+ const pathResults = evaluatePathThresholds({
4001
+ config,
4002
+ files: input.files ?? [],
4003
+ addedByFile: input.addedByFile ?? /* @__PURE__ */ new Map()
4004
+ });
3784
4005
  const flagsOk = flagsPass(flagResults);
3785
- const overall = patchPass && projectPass && flagsOk ? "pass" : "fail";
4006
+ const pathsOk = pathThresholdsPass(pathResults);
4007
+ const overall = patchPass && projectPass && flagsOk && pathsOk ? "pass" : "fail";
3786
4008
  const exitCode = overall === "pass" ? 0 : 1;
3787
4009
  if (json) {
3788
4010
  const payload = {
@@ -3794,6 +4016,7 @@ ${tip("add thresholds.patch / thresholds.project to enforce")}
3794
4016
  },
3795
4017
  project: { pct: projectPct, threshold: projectThreshold, pass: projectPass },
3796
4018
  ...flagResults.length > 0 ? { flags: flagsToJson(flagResults) } : {},
4019
+ ...pathResults.length > 0 ? { paths: pathThresholdsToJson(pathResults) } : {},
3797
4020
  overall,
3798
4021
  ...patchSkipped ? { note: EMPTY_PATCH_REASON } : {}
3799
4022
  };
@@ -3803,6 +4026,7 @@ ${tip("add thresholds.patch / thresholds.project to enforce")}
3803
4026
  projectPass,
3804
4027
  overall,
3805
4028
  flagResults,
4029
+ pathResults,
3806
4030
  stdout: JSON.stringify(payload) + "\n",
3807
4031
  stderr: "",
3808
4032
  exitCode
@@ -3822,6 +4046,7 @@ ${tip("add thresholds.patch / thresholds.project to enforce")}
3822
4046
  }
3823
4047
  lines.push(formatMetricLine("Project", projectPct, projectThreshold, projectPass));
3824
4048
  lines.push(...formatFlagLines(flagResults));
4049
+ lines.push(...formatPathLines(pathResults));
3825
4050
  if (overall === "fail") {
3826
4051
  lines.push("");
3827
4052
  if (patchSkipped) {
@@ -3831,10 +4056,16 @@ ${tip("add thresholds.patch / thresholds.project to enforce")}
3831
4056
  if (missing.length > 0) {
3832
4057
  lines.push(
3833
4058
  dim(
3834
- "Missing flags fail this run (no carryforward). Collect that package's coverage or scope the job with --flag."
4059
+ "Flags with no files this run are skipped (not 0%). Scope the job with --flag when this coverage file is one package."
3835
4060
  )
3836
4061
  );
3837
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
+ }
3838
4069
  lines.push(tip("add tests for uncovered ranges: tested diff"));
3839
4070
  } else {
3840
4071
  lines.push("");
@@ -3851,6 +4082,7 @@ ${tip("add thresholds.patch / thresholds.project to enforce")}
3851
4082
  projectPass,
3852
4083
  overall,
3853
4084
  flagResults,
4085
+ pathResults,
3854
4086
  stdout: lines.join("\n"),
3855
4087
  stderr: "",
3856
4088
  exitCode
@@ -3858,7 +4090,7 @@ ${tip("add thresholds.patch / thresholds.project to enforce")}
3858
4090
  }
3859
4091
  function registerCheckCommand(program2) {
3860
4092
  program2.command("check").description(
3861
- "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."
3862
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(
3863
4095
  "--file <path>",
3864
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,11 +1128,16 @@ 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
1132
- * only — a missing path is missing (pending/fail), never last week's totals.
1140
+ * only — a missing path is skipped (not 0%), never another flag's totals.
1133
1141
  */
1134
1142
  flags: z2.record(FlagNameSchema, FlagConfigSchema).optional()
1135
1143
  });
@@ -1164,17 +1172,21 @@ var DiffOutputSchema = z2.object({
1164
1172
  ignored: z2.array(z2.string())
1165
1173
  });
1166
1174
  var FlagMetricJsonSchema = z2.object({
1167
- pct: z2.number().min(0).max(100),
1175
+ /** Omitted when this flag had no files this run (`skipped: true`). */
1176
+ pct: z2.number().min(0).max(100).optional(),
1168
1177
  threshold: z2.number().min(0).max(100),
1169
- pass: z2.boolean(),
1170
- executable: z2.number().int().nonnegative(),
1171
- covered: z2.number().int().nonnegative(),
1178
+ /** Omitted when skipped — a missing flag is not a 0% fail. */
1179
+ pass: z2.boolean().optional(),
1180
+ executable: z2.number().int().nonnegative().optional(),
1181
+ covered: z2.number().int().nonnegative().optional(),
1172
1182
  skipped: z2.literal(true).optional(),
1173
1183
  reason: z2.string().optional()
1174
1184
  });
1175
1185
  var FlagResultJsonSchema = z2.object({
1176
1186
  status: z2.enum(["pass", "fail", "missing"]),
1177
1187
  present: z2.boolean(),
1188
+ /** True when this flag had no coverage files this run (not a 0% result). */
1189
+ skipped: z2.literal(true).optional(),
1178
1190
  reason: z2.string().optional(),
1179
1191
  patchCheck: z2.string(),
1180
1192
  projectCheck: z2.string(),
@@ -1182,6 +1194,16 @@ var FlagResultJsonSchema = z2.object({
1182
1194
  project: FlagMetricJsonSchema
1183
1195
  });
1184
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);
1185
1207
 
1186
1208
  // src/config.ts
1187
1209
  var DEFAULT_IGNORES = [
@@ -1696,19 +1718,28 @@ async function computeDiffContext(opts) {
1696
1718
  return { diff, files, addedByFile };
1697
1719
  }
1698
1720
 
1699
- // src/core/flags.ts
1721
+ // src/core/globs.ts
1700
1722
  import { minimatch as minimatch2 } from "minimatch";
1701
- var MISSING_FLAG_REASON = "no coverage files matched this flag in this run";
1702
- var SCOPED_MISSING_FLAG_REASON = "no coverage files in this run for this flag";
1703
1723
  var MATCH_OPTS = { dot: true, matchBase: true };
1704
- function pathMatchesFlag(filePath, patterns) {
1705
- const normalized = filePath.replace(/\\/g, "/");
1706
- return patterns.some(
1707
- (p) => minimatch2(normalized, p, MATCH_OPTS) || minimatch2(normalized, `**/${p}`, MATCH_OPTS)
1708
- );
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));
1709
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";
1710
1741
  function filterFilesByFlag(files, patterns) {
1711
- return files.filter((f) => pathMatchesFlag(f.path, patterns));
1742
+ return filterFilesByGlobs(files, patterns);
1712
1743
  }
1713
1744
  function resolveFlagThresholds(flag, global) {
1714
1745
  return {
@@ -1742,13 +1773,11 @@ function totalsToMetric(totals, threshold, kind) {
1742
1773
  covered: totals.covered
1743
1774
  };
1744
1775
  }
1745
- function missingMetric(threshold) {
1776
+ function missingMetric(threshold, reason) {
1746
1777
  return {
1747
- pct: 0,
1748
1778
  threshold,
1749
- pass: false,
1750
- executable: 0,
1751
- covered: 0
1779
+ skipped: true,
1780
+ reason
1752
1781
  };
1753
1782
  }
1754
1783
  function checkSlug(kind, name) {
@@ -1778,8 +1807,8 @@ function missingFlag(name, thresholds, reason) {
1778
1807
  reason,
1779
1808
  patchCheck: checkSlug("patch", name),
1780
1809
  projectCheck: checkSlug("project", name),
1781
- patch: missingMetric(thresholds.patch),
1782
- project: missingMetric(thresholds.project)
1810
+ patch: missingMetric(thresholds.patch, reason),
1811
+ project: missingMetric(thresholds.project, reason)
1783
1812
  };
1784
1813
  }
1785
1814
  function evaluateFlags(input) {
@@ -1810,7 +1839,7 @@ function evaluateFlags(input) {
1810
1839
  return results;
1811
1840
  }
1812
1841
  function flagsPass(results) {
1813
- return results.every((f) => f.status === "pass");
1842
+ return results.every((f) => f.status !== "fail");
1814
1843
  }
1815
1844
  function flagsToJson(results) {
1816
1845
  const out = {};
@@ -1818,19 +1847,44 @@ function flagsToJson(results) {
1818
1847
  out[flag.name] = {
1819
1848
  status: flag.status,
1820
1849
  present: flag.present,
1850
+ ...flag.status === "missing" ? { skipped: true } : {},
1821
1851
  ...flag.reason ? { reason: flag.reason } : {},
1822
1852
  patchCheck: flag.patchCheck,
1823
1853
  projectCheck: flag.projectCheck,
1824
- patch: flag.patch,
1825
- project: flag.project
1854
+ patch: metricToJson(flag.patch),
1855
+ project: metricToJson(flag.project)
1826
1856
  };
1827
1857
  }
1828
1858
  return out;
1829
1859
  }
1860
+ function metricToJson(metric) {
1861
+ if (metric.skipped) {
1862
+ return {
1863
+ threshold: metric.threshold,
1864
+ skipped: true,
1865
+ ...metric.reason ? { reason: metric.reason } : {}
1866
+ };
1867
+ }
1868
+ return {
1869
+ pct: metric.pct ?? 0,
1870
+ threshold: metric.threshold,
1871
+ pass: metric.pass ?? false,
1872
+ executable: metric.executable ?? 0,
1873
+ covered: metric.covered ?? 0,
1874
+ ...metric.reason ? { reason: metric.reason } : {}
1875
+ };
1876
+ }
1877
+ function flagsToIngestJson(results) {
1878
+ const present = results.filter((f) => f.present);
1879
+ return present.length > 0 ? flagsToJson(present) : void 0;
1880
+ }
1830
1881
  function resolveFlagsJson(input) {
1831
1882
  const results = evaluateFlags(input);
1832
1883
  return results.length > 0 ? flagsToJson(results) : void 0;
1833
1884
  }
1885
+ function resolveIngestFlagsJson(input) {
1886
+ return flagsToIngestJson(evaluateFlags(input));
1887
+ }
1834
1888
 
1835
1889
  // src/core/coverage-merge.ts
1836
1890
  function emptyToUndef(value) {
@@ -2553,7 +2607,7 @@ async function executePush(cli, deps) {
2553
2607
  let flags;
2554
2608
  if (!handshakeOnly && !computeDiffFn) {
2555
2609
  try {
2556
- flags = resolveFlagsJson({
2610
+ flags = resolveIngestFlagsJson({
2557
2611
  config,
2558
2612
  files,
2559
2613
  addedByFile,
@@ -3072,7 +3126,7 @@ import pc3 from "picocolors";
3072
3126
  // package.json
3073
3127
  var package_default = {
3074
3128
  name: "@tested/cli",
3075
- version: "0.1.8",
3129
+ version: "0.1.10",
3076
3130
  description: "Coverage your agent can use. CLI for patch + project coverage with agent-readable JSON output.",
3077
3131
  license: "MIT",
3078
3132
  homepage: "https://tested.dev",
@@ -3482,6 +3536,116 @@ function registerRunCommand(program2) {
3482
3536
  // src/commands/diff.ts
3483
3537
  import "commander";
3484
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
+
3485
3649
  // src/output/human.ts
3486
3650
  function formatRange(r) {
3487
3651
  return r.start === r.end ? `${r.start}` : `${r.start}-${r.end}`;
@@ -3629,7 +3793,12 @@ function registerDiffCommand(program2) {
3629
3793
  });
3630
3794
  if (opts.json) {
3631
3795
  const flags = resolveFlagsJson({ config, files, addedByFile });
3632
- 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
+ };
3633
3802
  process.stdout.write(JSON.stringify(payload, null, 2) + "\n");
3634
3803
  } else {
3635
3804
  process.stdout.write(
@@ -3659,6 +3828,7 @@ function formatIncompleteCheck(state, json) {
3659
3828
  projectPass: true,
3660
3829
  overall: "pass",
3661
3830
  flagResults: [],
3831
+ pathResults: [],
3662
3832
  stdout: JSON.stringify({
3663
3833
  overall: "pending",
3664
3834
  complete: false,
@@ -3676,6 +3846,7 @@ function formatIncompleteCheck(state, json) {
3676
3846
  projectPass: true,
3677
3847
  overall: "pass",
3678
3848
  flagResults: [],
3849
+ pathResults: [],
3679
3850
  stdout: "",
3680
3851
  stderr: `${dim("tested.dev \u2014 coverage gate")} ${badge("info")}
3681
3852
 
@@ -3694,6 +3865,7 @@ function formatCompleteHandshakeCheck(json) {
3694
3865
  projectPass: true,
3695
3866
  overall: "pass",
3696
3867
  flagResults: [],
3868
+ pathResults: [],
3697
3869
  stdout: JSON.stringify({ overall: "pending", complete: true, note }) + "\n",
3698
3870
  stderr: "",
3699
3871
  exitCode: 0
@@ -3705,6 +3877,7 @@ function formatCompleteHandshakeCheck(json) {
3705
3877
  projectPass: true,
3706
3878
  overall: "pass",
3707
3879
  flagResults: [],
3880
+ pathResults: [],
3708
3881
  stdout: "",
3709
3882
  stderr: `${dim("tested.dev \u2014 coverage gate")} ${badge("info")}
3710
3883
 
@@ -3718,6 +3891,12 @@ function formatMetricLine(label, pct3, threshold, pass, indent = " ") {
3718
3891
  const status = pass ? badge("pass") : badge("fail");
3719
3892
  return `${indent}${label.padEnd(8)} ${pctStr}% (threshold ${threshold}) ${status}`;
3720
3893
  }
3894
+ function presentPct(metric) {
3895
+ if (metric.pct === void 0 || metric.pass === void 0) {
3896
+ throw new Error("present metric missing pct/pass");
3897
+ }
3898
+ return { pct: metric.pct, pass: metric.pass };
3899
+ }
3721
3900
  function formatFlagLines(results) {
3722
3901
  if (results.length === 0) return [];
3723
3902
  const lines = [""];
@@ -3734,16 +3913,52 @@ function formatFlagLines(results) {
3734
3913
  ` ${"Patch".padEnd(8)} ${dim("-")} ${EMPTY_PATCH_REASON} ${badge("skip")}`
3735
3914
  );
3736
3915
  } else {
3916
+ const patch = presentPct(flag.patch);
3737
3917
  lines.push(
3738
- formatMetricLine("Patch", flag.patch.pct, flag.patch.threshold, flag.patch.pass, " ")
3918
+ formatMetricLine("Patch", patch.pct, flag.patch.threshold, patch.pass, " ")
3739
3919
  );
3740
3920
  }
3921
+ const project = presentPct(flag.project);
3741
3922
  lines.push(
3742
3923
  formatMetricLine(
3743
3924
  "Project",
3744
- flag.project.pct,
3925
+ project.pct,
3745
3926
  flag.project.threshold,
3746
- flag.project.pass,
3927
+ project.pass,
3928
+ " "
3929
+ )
3930
+ );
3931
+ }
3932
+ return lines;
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,
3747
3962
  " "
3748
3963
  )
3749
3964
  );
@@ -3759,6 +3974,7 @@ function runCheck(input) {
3759
3974
  projectPass: true,
3760
3975
  overall: "pass",
3761
3976
  flagResults: [],
3977
+ pathResults: [],
3762
3978
  stdout: "",
3763
3979
  stderr: `${dim("tested.dev \u2014 coverage gate")} ${badge("info")}
3764
3980
 
@@ -3781,8 +3997,14 @@ ${tip("add thresholds.patch / thresholds.project to enforce")}
3781
3997
  addedByFile: input.addedByFile ?? /* @__PURE__ */ new Map(),
3782
3998
  ...input.onlyFlag !== void 0 ? { onlyFlag: input.onlyFlag } : {}
3783
3999
  });
4000
+ const pathResults = evaluatePathThresholds({
4001
+ config,
4002
+ files: input.files ?? [],
4003
+ addedByFile: input.addedByFile ?? /* @__PURE__ */ new Map()
4004
+ });
3784
4005
  const flagsOk = flagsPass(flagResults);
3785
- const overall = patchPass && projectPass && flagsOk ? "pass" : "fail";
4006
+ const pathsOk = pathThresholdsPass(pathResults);
4007
+ const overall = patchPass && projectPass && flagsOk && pathsOk ? "pass" : "fail";
3786
4008
  const exitCode = overall === "pass" ? 0 : 1;
3787
4009
  if (json) {
3788
4010
  const payload = {
@@ -3794,6 +4016,7 @@ ${tip("add thresholds.patch / thresholds.project to enforce")}
3794
4016
  },
3795
4017
  project: { pct: projectPct, threshold: projectThreshold, pass: projectPass },
3796
4018
  ...flagResults.length > 0 ? { flags: flagsToJson(flagResults) } : {},
4019
+ ...pathResults.length > 0 ? { paths: pathThresholdsToJson(pathResults) } : {},
3797
4020
  overall,
3798
4021
  ...patchSkipped ? { note: EMPTY_PATCH_REASON } : {}
3799
4022
  };
@@ -3803,6 +4026,7 @@ ${tip("add thresholds.patch / thresholds.project to enforce")}
3803
4026
  projectPass,
3804
4027
  overall,
3805
4028
  flagResults,
4029
+ pathResults,
3806
4030
  stdout: JSON.stringify(payload) + "\n",
3807
4031
  stderr: "",
3808
4032
  exitCode
@@ -3822,6 +4046,7 @@ ${tip("add thresholds.patch / thresholds.project to enforce")}
3822
4046
  }
3823
4047
  lines.push(formatMetricLine("Project", projectPct, projectThreshold, projectPass));
3824
4048
  lines.push(...formatFlagLines(flagResults));
4049
+ lines.push(...formatPathLines(pathResults));
3825
4050
  if (overall === "fail") {
3826
4051
  lines.push("");
3827
4052
  if (patchSkipped) {
@@ -3831,10 +4056,16 @@ ${tip("add thresholds.patch / thresholds.project to enforce")}
3831
4056
  if (missing.length > 0) {
3832
4057
  lines.push(
3833
4058
  dim(
3834
- "Missing flags fail this run (no carryforward). Collect that package's coverage or scope the job with --flag."
4059
+ "Flags with no files this run are skipped (not 0%). Scope the job with --flag when this coverage file is one package."
3835
4060
  )
3836
4061
  );
3837
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
+ }
3838
4069
  lines.push(tip("add tests for uncovered ranges: tested diff"));
3839
4070
  } else {
3840
4071
  lines.push("");
@@ -3851,6 +4082,7 @@ ${tip("add thresholds.patch / thresholds.project to enforce")}
3851
4082
  projectPass,
3852
4083
  overall,
3853
4084
  flagResults,
4085
+ pathResults,
3854
4086
  stdout: lines.join("\n"),
3855
4087
  stderr: "",
3856
4088
  exitCode
@@ -3858,7 +4090,7 @@ ${tip("add thresholds.patch / thresholds.project to enforce")}
3858
4090
  }
3859
4091
  function registerCheckCommand(program2) {
3860
4092
  program2.command("check").description(
3861
- "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."
3862
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(
3863
4095
  "--file <path>",
3864
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.8",
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",