@williamthorsen/nmr 0.39.0 โ†’ 0.40.0

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.
Files changed (88) hide show
  1. package/.meta/changelog.json +128 -4
  2. package/.readyup/kits/default.js +47 -32
  3. package/.readyup/manifest.json +12 -7
  4. package/CHANGELOG.md +38 -0
  5. package/README.md +41 -73
  6. package/agents/README.md +8 -8
  7. package/agents/guidance/rulebooks/nmr.md +8 -8
  8. package/bin/ensure-prepublish-hooks.js +1 -1
  9. package/bin/nmr-clean.js +1 -1
  10. package/bin/nmr-compile.js +1 -1
  11. package/bin/nmr-fmt.js +1 -1
  12. package/bin/nmr-report-catalog.js +1 -1
  13. package/bin/nmr-report-overrides.js +1 -1
  14. package/bin/nmr-taze.js +1 -1
  15. package/bin/nmr.js +1 -1
  16. package/dist/esm/UserError.d.ts +1 -2
  17. package/dist/esm/check-cache.d.ts +59 -61
  18. package/dist/esm/commands/build-output.d.ts +35 -36
  19. package/dist/esm/commands/build.d.ts +13 -14
  20. package/dist/esm/commands/build.js +3 -3
  21. package/dist/esm/commands/clean.d.ts +6 -6
  22. package/dist/esm/commands/ensure-prepublish-hooks.d.ts +5 -5
  23. package/dist/esm/commands/fmt.d.ts +8 -8
  24. package/dist/esm/commands/fmt.js +1 -1
  25. package/dist/esm/commands/report-catalog.d.ts +5 -7
  26. package/dist/esm/commands/report-catalog.js +1 -1
  27. package/dist/esm/commands/report-overrides.d.ts +0 -2
  28. package/dist/esm/commands/report-overrides.js +1 -1
  29. package/dist/esm/commands/taze.d.ts +9 -8
  30. package/dist/esm/config.d.ts +5 -5
  31. package/dist/esm/config.js +3 -3
  32. package/dist/esm/context.d.ts +1 -2
  33. package/dist/esm/default-scripts.d.ts +5 -6
  34. package/dist/esm/defineConfig.d.ts +1 -1
  35. package/dist/esm/git-ignored-paths.d.ts +9 -0
  36. package/dist/esm/git-ignored-paths.js +10 -0
  37. package/dist/esm/glyphs.d.ts +3 -3
  38. package/dist/esm/help.d.ts +2 -2
  39. package/dist/esm/helpers/clampToBytes.d.ts +4 -5
  40. package/dist/esm/helpers/config-path.d.ts +0 -2
  41. package/dist/esm/helpers/createBoundedBuffer.d.ts +2 -2
  42. package/dist/esm/helpers/deriveExcerpt.d.ts +4 -5
  43. package/dist/esm/helpers/duration.d.ts +8 -9
  44. package/dist/esm/helpers/filter-selection.d.ts +19 -19
  45. package/dist/esm/helpers/findClosestName.d.ts +1 -1
  46. package/dist/esm/helpers/hook-name.d.ts +2 -5
  47. package/dist/esm/helpers/package-json.d.ts +9 -8
  48. package/dist/esm/helpers/readStringValues.d.ts +2 -3
  49. package/dist/esm/helpers/reportClosing.d.ts +8 -9
  50. package/dist/esm/helpers/transcript.d.ts +13 -13
  51. package/dist/esm/helpers/type-guards.d.ts +1 -0
  52. package/dist/esm/output-style.d.ts +12 -13
  53. package/dist/esm/prettier.d.ts +2 -2
  54. package/dist/esm/recording.d.ts +20 -15
  55. package/dist/esm/recording.js +7 -7
  56. package/dist/esm/replay-assembly.d.ts +10 -10
  57. package/dist/esm/report-format.d.ts +14 -19
  58. package/dist/esm/reportCliFailure.d.ts +2 -3
  59. package/dist/esm/resolve-scripts.d.ts +4 -5
  60. package/dist/esm/resolveBinStyles.d.ts +4 -5
  61. package/dist/esm/resolver.d.ts +14 -18
  62. package/dist/esm/runCli.d.ts +6 -6
  63. package/dist/esm/runCli.js +21 -21
  64. package/dist/esm/runner.d.ts +19 -19
  65. package/dist/esm/steps.d.ts +43 -42
  66. package/dist/esm/taze.d.ts +1 -1
  67. package/dist/esm/tests.d.ts +18 -13
  68. package/dist/esm/tests.js +2 -2
  69. package/dist/esm/tiers.d.ts +27 -23
  70. package/dist/esm/tiers.js +6 -2
  71. package/dist/esm/types.d.ts +5 -5
  72. package/dist/esm/verbosity.d.ts +22 -20
  73. package/dist/esm/verdict.d.ts +33 -33
  74. package/dist/esm/verdict.js +1 -1
  75. package/dist/esm/vitest-source-resolution.d.ts +11 -11
  76. package/dist/esm/vitest-source-resolution.js +2 -2
  77. package/dist/esm/vitest.d.ts +25 -24
  78. package/dist/esm/vitest.js +16 -8
  79. package/dist/esm/workspace.d.ts +4 -4
  80. package/docs/check-cache.md +57 -57
  81. package/docs/prettier.md +10 -10
  82. package/docs/reporting.md +46 -46
  83. package/docs/scripts.md +29 -29
  84. package/docs/upgrades.md +18 -18
  85. package/docs/utilities.md +13 -13
  86. package/docs/vitest.md +64 -62
  87. package/docs/workspace.md +19 -19
  88. package/package.json +5 -4
@@ -1,4 +1,127 @@
1
1
  [
2
+ {
3
+ "version": "0.40.0",
4
+ "date": "2026-10-01",
5
+ "sections": [
6
+ {
7
+ "title": "๐ŸŽ‰ Features",
8
+ "audience": "all",
9
+ "items": [
10
+ {
11
+ "description": "Makes `checkTestFileConventions` and the readyup kit's tier and placement checks skip untracked paths that git ignores, so that local build output such as a stale `.netlify/` directory no longer fails the check. (#898)",
12
+ "hash": "15132d5e797b734f1fc78bce7abc5d82c7e81974",
13
+ "entry": 1
14
+ },
15
+ {
16
+ "description": "Excludes untracked paths that git ignores from collection in `defineVitestConfig` and `defineRootVitestConfig`, so that Vitest does not run a test file that the conventions check skips. (#898)",
17
+ "hash": "15132d5e797b734f1fc78bce7abc5d82c7e81974",
18
+ "entry": 2
19
+ },
20
+ {
21
+ "description": "Rewords the `nmr --log` refusal for a run that the check-result cache does not cover to `the check-result cache does not apply here (NMR_DEBUG=1 reports why)`, and renames the matching `docs/check-cache.md` heading to \"When the cache does not apply\", which changes its anchor. (#923)",
22
+ "hash": "71d8c3dfb45aca872ca897ee589aae6351a1f516",
23
+ "entry": 1
24
+ }
25
+ ]
26
+ },
27
+ {
28
+ "title": "๐Ÿ› Bug fixes",
29
+ "audience": "all",
30
+ "items": [
31
+ {
32
+ "description": "Rewords the messages printed by `nmr`, its bin wrappers, and its readyup kit, which used figures such as \"sits under\" and \"holds\", em-dashes, and \"no\" as a determiner, so that a diagnosis such as \"-F/--filter did not match any workspace\" states its condition directly. (#932)",
33
+ "hash": "570f1103437d22b81b556f2d2b2e02f1fc32942c",
34
+ "entry": 1
35
+ },
36
+ {
37
+ "description": "Fixes the issue that the `nmr` rulebook recommended `-q` without stating that nmr reads its own flags only before the command name, which led agents to run `nmr ci -q` and fail when `nmr-fmt` rejected the flag. (#948)",
38
+ "hash": "b7cf3b3dfcd901db11546f153ec6de61580e6a69",
39
+ "entry": 1
40
+ }
41
+ ]
42
+ },
43
+ {
44
+ "title": "โ™ป๏ธ Refactoring",
45
+ "audience": "dev",
46
+ "items": [
47
+ {
48
+ "description": "Replaces every `.sort()` call that suppressed `unicorn/no-array-sort` with `toSorted()` and drops the spread copy wherever the source was already an array. (#925)",
49
+ "hash": "4680fb4e865ffce0bd135fac20d49b766b156992",
50
+ "entry": 1
51
+ }
52
+ ]
53
+ },
54
+ {
55
+ "title": "๐Ÿงช Tests",
56
+ "audience": "dev",
57
+ "items": [
58
+ {
59
+ "description": "Stubs `GIT_CEILING_DIRECTORIES` to the fixture's parent in every test that expects git to find no repository, so that a repository above the temp directory can no longer change the test's result. (#923)",
60
+ "hash": "71d8c3dfb45aca872ca897ee589aae6351a1f516",
61
+ "entry": 2
62
+ },
63
+ {
64
+ "description": "Moves the `everyBinTargetIsACommittedWrapper` tests from `kit-bin-checks.unit.test.ts` to `kit-bin-checks.tool.test.ts`, because they spawn git through readyup's `listTrackedFiles`. (#923)",
65
+ "hash": "71d8c3dfb45aca872ca897ee589aae6351a1f516",
66
+ "entry": 3
67
+ }
68
+ ]
69
+ },
70
+ {
71
+ "title": "โš™๏ธ Tooling",
72
+ "audience": "dev",
73
+ "items": [
74
+ {
75
+ "description": "Adds a check to `bootstrap.tool.test.ts` that fails when a package's `prepare` runs the build bootstrap without either depending on nmr-core or passing `--conditions nmr-source`, which would make `pnpm install` fail on a fresh clone. (#903)",
76
+ "hash": "b00b17d277197c1b18133da2e5258cf52719946a",
77
+ "entry": 3
78
+ }
79
+ ]
80
+ },
81
+ {
82
+ "title": "๐Ÿ“ฆ Dependencies",
83
+ "audience": "dev",
84
+ "items": [
85
+ {
86
+ "description": "Adds `esbuild` 0.28.2 to the pnpm catalog and declares it as a devDependency of `nmr`, `release-kit`, and `v11y-check`, because `readyup` declares it only as an optional peer and `rdy compile` failed in those packages once a regenerated lockfile stopped installing it. (#944)",
87
+ "hash": "6dffd5df5bc4209dc225d08390ef2b67fe381f1b",
88
+ "entry": 2
89
+ }
90
+ ]
91
+ },
92
+ {
93
+ "title": "๐Ÿ“š Documentation",
94
+ "audience": "dev",
95
+ "items": [
96
+ {
97
+ "description": "Revises the comments in `packages/nmr/src` and `packages/nmr/.readyup/kits/default.ts` against the comment-discipline rules, correcting comments that misstated the code, such as the Levenshtein distance quoted in `runCli.ts` and a `root-tests-config` kit check that does not exist. (#919)",
98
+ "hash": "84fe905d4142059425d82eb8d10821b687f522c2",
99
+ "entry": 1
100
+ },
101
+ {
102
+ "description": "Corrects the truncation example in `packages/nmr/docs/reporting.md`, replacing 90 seconds, which renders as `1m 30s` whether it is truncated or rounded, with 119.9 seconds, which truncates to `1m 59s`. (#919)",
103
+ "hash": "84fe905d4142059425d82eb8d10821b687f522c2",
104
+ "entry": 2
105
+ },
106
+ {
107
+ "description": "Rewrites the sentences in the README, the guides under `docs/`, the agent rulebook, and the code comments of `@williamthorsen/nmr` that broke the repository's writing rules on plain speech, em-dashes, second-person address, and relative clauses. (#932)",
108
+ "hash": "570f1103437d22b81b556f2d2b2e02f1fc32942c",
109
+ "entry": 2
110
+ },
111
+ {
112
+ "description": "Replaces \"where\" in two doc comments in `packages/nmr/src/check-cache.ts` and in the fixture comment in two tier-budget tests, because each use introduced a consequence rather than a place. (#945)",
113
+ "hash": "6f2c93ee0c73810ebd4cb4ab76618c00235edc42",
114
+ "entry": 1
115
+ },
116
+ {
117
+ "description": "Writes `-q` as `nmr -q <command>` in `docs/reporting.md` and in the README's verbosity paragraph. (#948)",
118
+ "hash": "b7cf3b3dfcd901db11546f153ec6de61580e6a69",
119
+ "entry": 2
120
+ }
121
+ ]
122
+ }
123
+ ]
124
+ },
2
125
  {
3
126
  "version": "0.39.0",
4
127
  "date": "2026-09-22",
@@ -229,7 +352,8 @@
229
352
  "items": [
230
353
  {
231
354
  "description": "Keep global git ignore rules and attributes out of test subprocesses (#773)",
232
- "body": "Fixes an issue where a test that spawns `git` read ignore rules and attributes from outside the repository, so a suite could pass or fail on whatever a developer or their machine happened to have configured. The setup file that `defineVitestConfig` loads by default now isolates both, alongside the git configuration already covered.\r\n\r\nMigration: Where a test needs its own `core.excludesFile` or `core.attributesFile`, pass it on the invocation as `git -c core.excludesFile=<path>`. An environment-injected key now outranks repository-local config.",
355
+ "body": "Fixes an issue where a test that spawns `git` read ignore rules and attributes from outside the repository, so a suite could pass or fail on whatever a developer or their machine happened to have configured. The setup file that `defineVitestConfig` loads by default now isolates both, alongside the git configuration already covered.\n\nMigration: Where a test needs its own `core.excludesFile` or `core.attributesFile`, pass it on the invocation as `git -c core.excludesFile=<path>`. An environment-injected key now outranks repository-local config.",
356
+ "migration": "Where a test needs its own `core.excludesFile` or `core.attributesFile`, pass it on the invocation as `git -c core.excludesFile=<path>`. An environment-injected key now outranks repository-local config.",
233
357
  "hash": "59b454ff3344a7c00d766f041a5862faa1ee6f1f"
234
358
  }
235
359
  ]
@@ -466,7 +590,7 @@
466
590
  "items": [
467
591
  {
468
592
  "description": "Carry the test-tier convention in the ambient rulebook (#689)",
469
- "body": "Adds the test-tier convention to `nmr.md`, the ambient rulebook `codeassembly sync` deploys to each consuming repo's agent guidance. The guidance describes the `<subject>[.<aspect>].<tier>.test.ts` grammar and the four tiers, and warns that tests outside a `__tests__` directory are ignored. Tests not explicitly attributed to a tier are flagged by the ReadyUp kit bundled with `nmr`.\r\n\r\nThe previous guidance is removed from `AGENTS.md`.",
593
+ "body": "Adds the test-tier convention to `nmr.md`, the ambient rulebook `codeassembly sync` deploys to each consuming repo's agent guidance. The guidance describes the `<subject>[.<aspect>].<tier>.test.ts` grammar and the four tiers, and warns that tests outside a `__tests__` directory are ignored. Tests not explicitly attributed to a tier are flagged by the ReadyUp kit bundled with `nmr`.\n\nThe previous guidance is removed from `AGENTS.md`.",
470
594
  "hash": "a1fa0e0eb19362f0506ce4f98cd527b205baf1e7"
471
595
  },
472
596
  {
@@ -503,7 +627,7 @@
503
627
  },
504
628
  {
505
629
  "description": "Adopt `isError` and consolidate errno narrowing on a shared helper (#697)",
506
- "body": "Replaces every hand-rolled `instanceof Error` narrowing with `hasErrnoCode`, a new predicate exported from `@williamthorsen/nmr-core`, or with `isError` from `@williamthorsen/toolbelt.errors`. Previously the errno question was answered by inline copies alongside two private helpers of differing shape; both helpers are removed.\r\n\r\nSeparately, `eslint.config.ts` now bans `instanceof Error` in every position rather than in a ternary alone.",
630
+ "body": "Replaces every hand-rolled `instanceof Error` narrowing with `hasErrnoCode`, a new predicate exported from `@williamthorsen/nmr-core`, or with `isError` from `@williamthorsen/toolbelt.errors`. Previously the errno question was answered by inline copies alongside two private helpers of differing shape; both helpers are removed.\n\nSeparately, `eslint.config.ts` now bans `instanceof Error` in every position rather than in a ternary alone.",
507
631
  "hash": "c5e8e8a5e006b451e8c5aeea75d35772926d503c"
508
632
  }
509
633
  ]
@@ -581,7 +705,7 @@
581
705
  "items": [
582
706
  {
583
707
  "description": "Derive the shelled-nmr remedy from the declaration site and reject a malformed package.json (#658)",
584
- "body": "Fixes a shelled-nmr warning that named `.config/nmr.config.ts` as the remedy for every step it reported, including the `package.json` entries that tier cannot resolve. The line now names the relevant declaration and remedy. Shelled nmr can now be detected not only in a step's first token but anywhere in the step.\r\n\r\nSeparately, a configuration mistake now reports as a plain message rather than a Node stack trace.",
708
+ "body": "Fixes a shelled-nmr warning that named `.config/nmr.config.ts` as the remedy for every step it reported, including the `package.json` entries that tier cannot resolve. The line now names the relevant declaration and remedy. Shelled nmr can now be detected not only in a step's first token but anywhere in the step.\n\nSeparately, a configuration mistake now reports as a plain message rather than a Node stack trace.",
585
709
  "hash": "74ea98c361cf13e213d257952c582a4ebd631d9a"
586
710
  }
587
711
  ]
@@ -64,7 +64,7 @@ var workspaceScripts = {
64
64
  "test:unit": "pnpm exec vitest --project unit",
65
65
  "test:watch": `pnpm exec vitest ${GATE_PROJECTS} --watch`,
66
66
  typecheck: "tsgo --noEmit",
67
- // The command is a string because neither half names an nmr command: both are binaries.
67
+ // The command is a string because neither half names an nmr command: Both are binaries.
68
68
  upgrade: "nmr-report-catalog && nmr-taze",
69
69
  "view-coverage": "open coverage/index.html"
70
70
  };
@@ -75,8 +75,8 @@ var rootScripts = {
75
75
  build: ["-R build"],
76
76
  check: [TYPECHECK_STEP, "fmt:check", "lint:check", "test"],
77
77
  "check:strict": [TYPECHECK_STEP, "fmt:check", "lint:strict", "test"],
78
- // Excludes the audit, which in CI has a workflow of its own. The build is what the narrowed check runs
79
- // against, so it declines the arguments rather than being narrowed by them.
78
+ // Excludes the audit, which in CI has a workflow of its own. The narrowed check runs against the build, so
79
+ // the build declines the arguments rather than being narrowed by them.
80
80
  ci: [{ run: "build", shouldDeclineArguments: true }, "check:strict"],
81
81
  clean: "nmr-clean",
82
82
  fix: ["lint", "fmt"],
@@ -86,8 +86,8 @@ var rootScripts = {
86
86
  lint: "eslint --fix .",
87
87
  "lint:check": "eslint .",
88
88
  "lint:strict": "strict-lint",
89
- // The audit costs seconds and `ci` costs minutes, so the cheap gate fails first. The audit reads the
90
- // dependency tree, which no argument narrowing the code under test says anything about.
89
+ // The audit takes seconds and `ci` takes minutes, so the cheap gate fails first. The audit reads the
90
+ // dependency tree, and an argument narrowing the code under test does not say anything about that tree.
91
91
  prepush: [{ run: "audit", shouldDeclineArguments: true }, "ci"],
92
92
  "report-overrides": "nmr-report-overrides",
93
93
  "root:check": [ROOT_TYPECHECK_STEP, "fmt:check", "root:lint:check", "root:test"],
@@ -99,7 +99,7 @@ var rootScripts = {
99
99
  "root:test:tool": "vitest --config ./vitest.root.config.ts --project tool",
100
100
  "root:test:unit": "vitest --config ./vitest.root.config.ts --project unit",
101
101
  "root:typecheck": "tsgo --noEmit",
102
- // Carries the override report for the same reason `upgrade` does: both end in the tool that rewrites a
102
+ // Includes the override report for the same reason `upgrade` does: Both end in the tool that rewrites a
103
103
  // `pnpm.overrides` block, so both need the reporter's rejection ahead of them.
104
104
  "root:upgrade": "nmr-report-overrides && nmr-taze",
105
105
  test: ["root:test", "-R test"],
@@ -109,9 +109,9 @@ var rootScripts = {
109
109
  "test:unit": ["root:test:unit", "-R test:unit"],
110
110
  "test:watch": `vitest ${GATE_PROJECTS} --watch`,
111
111
  // Neither step is narrowable, so `nmr typecheck <file>` is rejected rather than checking that file under
112
- // default options at the root and hunting for it in every package.
112
+ // default options at the root and searching for it in every package.
113
113
  typecheck: [ROOT_TYPECHECK_STEP, { run: "-R typecheck", shouldDeclineArguments: true }],
114
- // The command is a string because neither half names an nmr command: both are binaries, and a composite
114
+ // The command is a string because neither half names an nmr command: Both are binaries, and a composite
115
115
  // element can name only a command.
116
116
  upgrade: "nmr-report-overrides && nmr-taze --recursive"
117
117
  };
@@ -124,6 +124,20 @@ function getDefaultRootScripts() {
124
124
  // src/tiers.ts
125
125
  import { readdirSync } from "node:fs";
126
126
  import path from "node:path";
127
+
128
+ // src/git-ignored-paths.ts
129
+ import { spawnSync } from "node:child_process";
130
+ function listGitIgnoredPaths(dir) {
131
+ const result = spawnSync(
132
+ "git",
133
+ ["ls-files", "-z", "--others", "--ignored", "--exclude-standard", "--directory", "--no-empty-directory"],
134
+ { cwd: dir, encoding: "utf8" }
135
+ );
136
+ if (result.error !== void 0 || result.status !== 0) return [];
137
+ return result.stdout.split("\0").filter((entry) => entry !== "").toSorted();
138
+ }
139
+
140
+ // src/tiers.ts
127
141
  var TEST_DIR = "__tests__";
128
142
  var TEST_EXTENSIONS = "{ts,tsx}";
129
143
  var TEST_GLOB_PREFIX = `**/${TEST_DIR}/**`;
@@ -149,9 +163,9 @@ function collectTestFiles(dir, relativeDir, isInTestDir, context) {
149
163
  for (const entry of entries) {
150
164
  const relativePath = relativeDir === "" ? entry.name : `${relativeDir}/${entry.name}`;
151
165
  if (entry.isDirectory()) {
152
- if (context.pruned.has(entry.name)) continue;
166
+ if (context.pruned.has(entry.name) || context.ignoredPaths.has(`${relativePath}/`)) continue;
153
167
  collectTestFiles(path.join(dir, entry.name), relativePath, isInTestDir || entry.name === TEST_DIR, context);
154
- } else if (isInTestDir !== context.misplaced && TEST_FILE_PATTERN.test(entry.name)) {
168
+ } else if (isInTestDir !== context.misplaced && TEST_FILE_PATTERN.test(entry.name) && !context.ignoredPaths.has(relativePath)) {
155
169
  context.foundPaths.push(relativePath);
156
170
  }
157
171
  }
@@ -159,6 +173,7 @@ function collectTestFiles(dir, relativeDir, isInTestDir, context) {
159
173
  function walkTestFiles(rootDir, { excludedBasenames = [] }, misplaced) {
160
174
  const context = {
161
175
  foundPaths: [],
176
+ ignoredPaths: new Set(listGitIgnoredPaths(rootDir)),
162
177
  misplaced,
163
178
  pruned: /* @__PURE__ */ new Set([...TEST_COLLECTION_EXCLUDE, ...excludedBasenames])
164
179
  };
@@ -210,21 +225,21 @@ var default_default = defineRdyKit({
210
225
  severity: "warn",
211
226
  quiet: true,
212
227
  check: toolVersionsHasNoPnpm,
213
- fix: "Remove pnpm from .tool-versions \u2014 manage via the packageManager field"
228
+ fix: "Remove pnpm from .tool-versions, and manage it through the packageManager field"
214
229
  },
215
230
  {
216
231
  name: "no package.json declares a pnpm field",
217
232
  severity: "error",
218
233
  quiet: true,
219
234
  check: () => noPnpmFieldInPackageJson(),
220
- fix: "Move these settings into pnpm-workspace.yaml, quoting each version under `overrides`, or run `pnpx codemod run pnpm-v10-to-v11`. pnpm 11 reads no key from the `pnpm` field, so an override left there pins nothing while an upgrade run with `--write` goes on rewriting it"
235
+ fix: "Move these settings into pnpm-workspace.yaml, quoting each version under `overrides`, or run `pnpx codemod run pnpm-v10-to-v11`. pnpm 11 does not read any key from the `pnpm` field, so an override left there pins nothing while an upgrade run with `--write` goes on rewriting it"
221
236
  },
222
237
  {
223
238
  name: ".config/nmr.config.ts uses defineConfig",
224
239
  severity: "recommend",
225
240
  skip: () => !fileExists(".config/nmr.config.ts") ? "no nmr config file" : false,
226
241
  check: () => fileContains(".config/nmr.config.ts", /defineConfig/),
227
- fix: "Wrap your config export with defineConfig() from @williamthorsen/nmr/config for type safety"
242
+ fix: "Wrap the config export with defineConfig() from @williamthorsen/nmr/config for type safety"
228
243
  },
229
244
  // `error` because falling short produces wrong results rather than a failure. Names and fixes are
230
245
  // getters because their version constants are declared below the kit.
@@ -236,7 +251,7 @@ var default_default = defineRdyKit({
236
251
  skip: () => !hasDevDependency("eslint") ? "eslint not installed" : false,
237
252
  check: hasSupportedEslintVersion,
238
253
  get fix() {
239
- return `pnpm add --save-dev eslint@^${MIN_ESLINT_VERSION} \u2014 earlier releases resolve config from the working directory, so nmr's root lint and lint:check would apply the root config to every package`;
254
+ return `pnpm add --save-dev eslint@^${MIN_ESLINT_VERSION}. Earlier releases resolve config from the working directory, so nmr's root lint and lint:check would apply the root config to every package`;
240
255
  }
241
256
  },
242
257
  {
@@ -247,7 +262,7 @@ var default_default = defineRdyKit({
247
262
  skip: () => !hasDevDependency("@williamthorsen/strict-lint") ? "strict-lint not installed" : false,
248
263
  check: hasSupportedStrictLintVersion,
249
264
  get fix() {
250
- return `pnpm add --save-dev @williamthorsen/strict-lint@^${MIN_STRICT_LINT_VERSION} \u2014 earlier releases pin ESLint to one config and resolve ceilings from the working directory, so nmr's root lint:strict would report the wrong rules for every package`;
265
+ return `pnpm add --save-dev @williamthorsen/strict-lint@^${MIN_STRICT_LINT_VERSION}. Earlier releases pin ESLint to one config and resolve ceilings from the working directory, so nmr's root lint:strict would report the wrong rules for every package`;
251
266
  }
252
267
  },
253
268
  // -- Root script cleanup -------------------------------------------------
@@ -256,7 +271,7 @@ var default_default = defineRdyKit({
256
271
  severity: "warn",
257
272
  quiet: true,
258
273
  check: noRedundantRootScripts,
259
- fix: "Remove scripts from root package.json that nmr provides as built-in root scripts \u2014 invoke via nmr directly"
274
+ fix: "Remove scripts from root package.json that nmr provides as built-in root scripts, and invoke them through nmr directly"
260
275
  },
261
276
  // -- Git hooks -----------------------------------------------------------
262
277
  {
@@ -278,13 +293,13 @@ var default_default = defineRdyKit({
278
293
  name: "every bin target is a committed wrapper",
279
294
  severity: "error",
280
295
  check: () => everyBinTargetIsACommittedWrapper(),
281
- fix: `Point each listed entry at a committed wrapper under bin/ that loads the build output at runtime. pnpm links a workspace package's bins during the install's link phase, which runs before anything is built, so a target that is not committed does not exist when pnpm reaches for it \u2014 and pnpm never retries, leaving the link missing for the life of the node_modules tree`
296
+ fix: `Point each listed entry at a committed wrapper under bin/ that loads the build output at runtime. pnpm links a workspace package's bins during the install's link phase, which runs before anything is built, so a target that is not committed does not exist when pnpm tries to link it. pnpm never retries, which leaves the link missing for the life of the node_modules tree`
282
297
  },
283
298
  {
284
299
  name: "every bin wrapper's build-output target is covered by files",
285
300
  severity: "warn",
286
301
  check: () => everyBinWrapperTargetIsCoveredByFiles(),
287
- fix: "Add the build output directory to `files` in each listed package. npm and pnpm publish the bin target itself whatever `files` says, so the wrapper ships pointing at build output missing from the tarball"
302
+ fix: "Add the build output directory to `files` in each listed package. npm and pnpm publish the bin target itself whatever `files` says, so the wrapper is published pointing at build output missing from the tarball"
288
303
  },
289
304
  // -- Vitest projects -----------------------------------------------------
290
305
  {
@@ -298,7 +313,7 @@ var default_default = defineRdyKit({
298
313
  name: "every vitest.config builds on @williamthorsen/nmr/vitest",
299
314
  severity: "error",
300
315
  check: () => vitestConfigBuildsOnSharedConfig(),
301
- fix: "Replace each listed config with: import { defineVitestConfig } from '@williamthorsen/nmr/vitest'; export default defineVitestConfig(); -- a config that does not call the factory declares no projects, so every tier-selecting test command fails against it. Pass your own settings to the factory as layers to keep them"
316
+ fix: "Replace each listed config with: import { defineVitestConfig } from '@williamthorsen/nmr/vitest'; export default defineVitestConfig(); -- a config that does not call the factory does not declare any projects, so every tier-selecting test command fails against it. Pass the repo's own settings to the factory as layers to keep them"
302
317
  },
303
318
  {
304
319
  name: "vitest.root.config.ts builds on @williamthorsen/nmr/vitest",
@@ -310,27 +325,27 @@ var default_default = defineRdyKit({
310
325
  name: "every workspace with a Vite config has a Vitest config",
311
326
  severity: "error",
312
327
  check: () => everyViteConfigHasVitestConfig(),
313
- fix: "Add a vitest.config.ts calling defineVitestConfig() from @williamthorsen/nmr/vitest beside each listed vite.config -- Vitest stops its config search at the first directory holding either name, so the Vite config otherwise wins and the projects model is never reached"
328
+ fix: "Add a vitest.config.ts calling defineVitestConfig() from @williamthorsen/nmr/vitest beside each listed vite.config -- Vitest stops its config search at the first directory containing either name, so the Vite config otherwise wins and the projects model is never reached"
314
329
  },
315
330
  {
316
331
  name: "the test suite gates the test-file conventions",
317
332
  severity: "warn",
318
333
  check: () => testSuiteGatesTestFileConventions(),
319
- fix: "Add a test of the repo's own under a __tests__ directory that declares the check: import { checkTestFileConventions } from '@williamthorsen/nmr/tests'; checkTestFileConventions(); -- passing `excludedBasenames` the directory names that the repo passes to `testCollectionExclude`. Without it no test run reports an untiered or misplaced test file, and this kit reports them in its place against nmr's built-in exclusions alone"
334
+ fix: "Add a test of the repo's own under a __tests__ directory that declares the check: import { checkTestFileConventions } from '@williamthorsen/nmr/tests'; checkTestFileConventions(); -- passing `excludedBasenames` the directory names that the repo passes to `testCollectionExclude`. Without it, the repo's test run does not report an untiered or misplaced test file, and this kit reports them in its place against only nmr's built-in exclusions and the paths that git ignores"
320
335
  },
321
336
  {
322
337
  name: "every test file names its isolation tier",
323
338
  severity: "error",
324
339
  skip: () => describeConventionsGuardSkip(),
325
340
  check: () => everyTestFileNamesItsTier(),
326
- fix: `Rename each to <subject>[.<aspect>].<tier>.test.ts, naming one of ${TIER_NAMES.join(", ")}. Use tool for a test that reaches a program the environment supplies, which is where a retired .int. or .integration. file belongs. Only the segment before .test. selects a project, so an untiered file runs under the residual unit project and reports success`
341
+ fix: `Rename each to <subject>[.<aspect>].<tier>.test.ts, naming one of ${TIER_NAMES.join(", ")}. Use tool for a test that reaches a program supplied by the environment, which is the tier for a retired .int. or .integration. file. Only the segment before .test. selects a project, so an untiered file runs under the residual unit project and reports success`
327
342
  },
328
343
  {
329
- name: "every test file sits under a __tests__ directory",
344
+ name: "every test file is under a __tests__ directory",
330
345
  severity: "error",
331
346
  skip: () => describeConventionsGuardSkip(),
332
347
  check: () => everyTestFileSitsUnderTestsDir(),
333
- fix: "Move each into a __tests__ directory, the only place from which the shared Vitest config collects. No project collects a file outside one, so it runs nowhere and reports nothing. For a file that is not a test, declare checkTestFileConventions with its directory in `excludedBasenames` instead, which this check then defers to"
348
+ fix: "Move each into a __tests__ directory, the only place from which the shared Vitest config collects. Vitest does not collect a file outside one, so it runs nowhere and reports nothing. For a file that is not a test, declare checkTestFileConventions with its directory in `excludedBasenames` instead, which this check then defers to"
334
349
  },
335
350
  {
336
351
  name: "no package re-exports the ancestor Vitest config",
@@ -351,14 +366,14 @@ var default_default = defineRdyKit({
351
366
  name: "taze.config.ts builds on @williamthorsen/nmr/taze",
352
367
  severity: "warn",
353
368
  check: () => tazeConfigBuildsOnSharedConfig(),
354
- fix: "Replace the taze config with: import { defineConfig } from '@williamthorsen/nmr/taze'; export default defineConfig(); \u2014 nmr's upgrade policy reaches a repo only through this file, so without it `nmr upgrade` reports nothing where dependencies are pinned to exact versions"
369
+ fix: "Replace the taze config with: import { defineConfig } from '@williamthorsen/nmr/taze'; export default defineConfig(); -- nmr's upgrade policy applies to a repo only through this file, so without it `nmr upgrade` does not report a dependency pinned to an exact version"
355
370
  },
356
371
  {
357
372
  name: "taze config declares no option taze discards",
358
373
  severity: "warn",
359
374
  quiet: true,
360
375
  check: () => tazeConfigAvoidsClobberedOptions(),
361
- fix: "Set these through the upgrade script instead, as rootScripts: { upgrade: 'nmr-report-overrides && nmr-taze --recursive --request-timeout 90000' } in .config/nmr.config.ts, keeping the rest of the default script \u2014 taze's CLI writes a default for each of them over whatever the config file declares, so the value there never reaches taze (antfu-collective/taze#317). nmr already forwards a 30-second request timeout"
376
+ fix: "Set these through the upgrade script instead, as rootScripts: { upgrade: 'nmr-report-overrides && nmr-taze --recursive --request-timeout 90000' } in .config/nmr.config.ts, keeping the rest of the default script. taze's CLI writes a default for each of them over whatever the config file declares, so taze never uses the file's value (antfu-collective/taze#317). nmr already forwards a 30-second request timeout"
362
377
  },
363
378
  // -- Audit dependency --------------------------------------------------------
364
379
  {
@@ -373,7 +388,7 @@ var default_default = defineRdyKit({
373
388
  severity: "error",
374
389
  quiet: true,
375
390
  check: () => !fileExists("scripts/run-workspace-script.ts"),
376
- fix: "Delete scripts/run-workspace-script.ts \u2014 nmr replaces this custom script runner"
391
+ fix: "Delete scripts/run-workspace-script.ts, because nmr replaces this custom script runner"
377
392
  },
378
393
  {
379
394
  name: 'no workspace packages reference run-workspace-script or "pnpm run ws"',
@@ -481,9 +496,9 @@ function prettierConfigBuildsOnSharedConfig(cwd = process.cwd()) {
481
496
  }
482
497
  function describeMissingPrettierConfig(cwd) {
483
498
  const inertConfigs = findFiles(INERT_PRETTIER_CONFIGS, cwd);
484
- if (inertConfigs.length > 0) return `holds no code to call the factory: ${inertConfigs.join(", ")}`;
499
+ if (inertConfigs.length > 0) return `does not contain code that calls the factory: ${inertConfigs.join(", ")}`;
485
500
  if (hasPrettierConfigKey(cwd)) {
486
- return 'holds no code to call the factory: the "prettier" key in package.json';
501
+ return 'does not contain code that calls the factory: the "prettier" key in package.json';
487
502
  }
488
503
  return ".prettierrc.js is missing";
489
504
  }
@@ -602,7 +617,7 @@ function formatPaths(paths) {
602
617
  ${paths.map((path2) => ` ${path2}`).join("\n")}`;
603
618
  }
604
619
  function getMinVersion() {
605
- const pickedFields = { "version": "0.39.0" };
620
+ const pickedFields = { "version": "0.40.0" };
606
621
  if (typeof pickedFields["version"] !== "string") {
607
622
  throw new TypeError("nmr/package.json: 'version' must be a string");
608
623
  }
@@ -754,14 +769,14 @@ function tazeConfigAvoidsClobberedOptions(cwd = process.cwd()) {
754
769
  }
755
770
  function describeMissingTazeConfig(cwd) {
756
771
  const inertConfigs = findFiles(INERT_TAZE_CONFIGS, cwd);
757
- if (inertConfigs.length > 0) return `holds no code to call the factory: ${inertConfigs.join(", ")}`;
772
+ if (inertConfigs.length > 0) return `does not contain code that calls the factory: ${inertConfigs.join(", ")}`;
758
773
  return "taze.config.ts is missing";
759
774
  }
760
775
  function testSuiteGatesTestFileConventions(cwd = process.cwd()) {
761
776
  if (findConventionsGuard(cwd) !== void 0) return true;
762
777
  return {
763
778
  ok: false,
764
- detail: `no test file under __tests__ imports checkTestFileConventions from ${SHARED_TESTS_MODULE}`
779
+ detail: `the test files under __tests__ do not import checkTestFileConventions from ${SHARED_TESTS_MODULE}`
765
780
  };
766
781
  }
767
782
  function describeConventionsGuardSkip() {
@@ -11,12 +11,12 @@
11
11
  "esbuildVersion": "0.28.2",
12
12
  "inputs": [
13
13
  {
14
- "hash": "0ae7745f",
14
+ "hash": "5f7bd316",
15
15
  "kind": "module",
16
16
  "path": "kits/default.ts"
17
17
  },
18
18
  {
19
- "hash": "30230373",
19
+ "hash": "9c1005ba",
20
20
  "kind": "inline",
21
21
  "path": "../package.json",
22
22
  "paths": [
@@ -24,17 +24,22 @@
24
24
  ]
25
25
  },
26
26
  {
27
- "hash": "06bffeed",
27
+ "hash": "9336f4c0",
28
28
  "kind": "module",
29
29
  "path": "../src/default-scripts.ts"
30
30
  },
31
31
  {
32
- "hash": "c53d0c51",
32
+ "hash": "d4828172",
33
+ "kind": "module",
34
+ "path": "../src/git-ignored-paths.ts"
35
+ },
36
+ {
37
+ "hash": "23646439",
33
38
  "kind": "module",
34
39
  "path": "../src/resolve-scripts.ts"
35
40
  },
36
41
  {
37
- "hash": "beb92e94",
42
+ "hash": "19dec003",
38
43
  "kind": "module",
39
44
  "path": "../src/tiers.ts"
40
45
  }
@@ -43,8 +48,8 @@
43
48
  "path": "kits/default.js",
44
49
  "readyupVersion": "0.38.0",
45
50
  "source": "kits/default.ts",
46
- "sourceHash": "0ae7745f",
47
- "targetHash": "06f47835"
51
+ "sourceHash": "5f7bd316",
52
+ "targetHash": "da11ca1f"
48
53
  }
49
54
  ]
50
55
  }
package/CHANGELOG.md CHANGED
@@ -2,6 +2,44 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## 0.40.0 โ€” 2026-10-01
6
+
7
+ ### ๐ŸŽ‰ Features
8
+
9
+ - Makes `checkTestFileConventions` and the readyup kit's tier and placement checks skip untracked paths that git ignores, so that local build output such as a stale `.netlify/` directory no longer fails the check. (#898)
10
+ - Excludes untracked paths that git ignores from collection in `defineVitestConfig` and `defineRootVitestConfig`, so that Vitest does not run a test file that the conventions check skips. (#898)
11
+ - Rewords the `nmr --log` refusal for a run that the check-result cache does not cover to `the check-result cache does not apply here (NMR_DEBUG=1 reports why)`, and renames the matching `docs/check-cache.md` heading to "When the cache does not apply", which changes its anchor. (#923)
12
+
13
+ ### ๐Ÿ› Bug fixes
14
+
15
+ - Rewords the messages printed by `nmr`, its bin wrappers, and its readyup kit, which used figures such as "sits under" and "holds", em-dashes, and "no" as a determiner, so that a diagnosis such as "-F/--filter did not match any workspace" states its condition directly. (#932)
16
+ - Fixes the issue that the `nmr` rulebook recommended `-q` without stating that nmr reads its own flags only before the command name, which led agents to run `nmr ci -q` and fail when `nmr-fmt` rejected the flag. (#948)
17
+
18
+ ### โ™ป๏ธ Refactoring
19
+
20
+ - Replaces every `.sort()` call that suppressed `unicorn/no-array-sort` with `toSorted()` and drops the spread copy wherever the source was already an array. (#925)
21
+
22
+ ### ๐Ÿงช Tests
23
+
24
+ - Stubs `GIT_CEILING_DIRECTORIES` to the fixture's parent in every test that expects git to find no repository, so that a repository above the temp directory can no longer change the test's result. (#923)
25
+ - Moves the `everyBinTargetIsACommittedWrapper` tests from `kit-bin-checks.unit.test.ts` to `kit-bin-checks.tool.test.ts`, because they spawn git through readyup's `listTrackedFiles`. (#923)
26
+
27
+ ### โš™๏ธ Tooling
28
+
29
+ - Adds a check to `bootstrap.tool.test.ts` that fails when a package's `prepare` runs the build bootstrap without either depending on nmr-core or passing `--conditions nmr-source`, which would make `pnpm install` fail on a fresh clone. (#903)
30
+
31
+ ### ๐Ÿ“ฆ Dependencies
32
+
33
+ - Adds `esbuild` 0.28.2 to the pnpm catalog and declares it as a devDependency of `nmr`, `release-kit`, and `v11y-check`, because `readyup` declares it only as an optional peer and `rdy compile` failed in those packages once a regenerated lockfile stopped installing it. (#944)
34
+
35
+ ### ๐Ÿ“š Documentation
36
+
37
+ - Revises the comments in `packages/nmr/src` and `packages/nmr/.readyup/kits/default.ts` against the comment-discipline rules, correcting comments that misstated the code, such as the Levenshtein distance quoted in `runCli.ts` and a `root-tests-config` kit check that does not exist. (#919)
38
+ - Corrects the truncation example in `packages/nmr/docs/reporting.md`, replacing 90 seconds, which renders as `1m 30s` whether it is truncated or rounded, with 119.9 seconds, which truncates to `1m 59s`. (#919)
39
+ - Rewrites the sentences in the README, the guides under `docs/`, the agent rulebook, and the code comments of `@williamthorsen/nmr` that broke the repository's writing rules on plain speech, em-dashes, second-person address, and relative clauses. (#932)
40
+ - Replaces "where" in two doc comments in `packages/nmr/src/check-cache.ts` and in the fixture comment in two tier-budget tests, because each use introduced a consequence rather than a place. (#945)
41
+ - Writes `-q` as `nmr -q <command>` in `docs/reporting.md` and in the README's verbosity paragraph. (#948)
42
+
5
43
  ## 0.39.0 โ€” 2026-09-22
6
44
 
7
45
  ### ๐ŸŽ‰ Features