@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
package/docs/reporting.md CHANGED
@@ -1,26 +1,26 @@
1
1
  # What nmr reports
2
2
 
3
- How nmr reports each command it runs, in prose or as JSON, and how loudly it relays the output of those commands.
3
+ How nmr reports each command that it runs, in prose or as JSON, and how loudly it relays the output of those commands.
4
4
 
5
5
  The verdict line and its four outcomes are shown in the [README](../README.md#what-nmr-reports).
6
6
 
7
- A recalled pass also carries an excerpt of the run it recalls, where that run left one; see [Replaying a skipped run's output](check-cache.md#replaying-a-skipped-runs-output).
7
+ A recalled pass also includes an excerpt of the run that it recalls, if that run left one; see [Replaying a skipped run's output](check-cache.md#replaying-a-skipped-runs-output).
8
8
 
9
- The scope is the directory the command's registry belongs to: a package's own directory, or the monorepo root. Durations and ages truncate and carry at most two units, so a check that passed 90 seconds ago reports `1m 30s ago` and never `2m ago`.
9
+ The scope is the directory to which the command's registry belongs: a package's own directory, or the monorepo root. Durations and ages truncate and show at most two units, so a check that passed 119.9 seconds ago reports `1m 59s ago` and never `2m ago`.
10
10
 
11
- Verdicts nest. A composite reports, and so does every command it expands into; a `:pre` or `:post` hook reports nothing of its own, since the command it wraps already reports for the chain, though a hook that delegates to a named command reports under that name. A `-R` or `-F` invocation reports nothing either, every scope it fans out to reporting instead. A cold repo-wide `nmr ci` in this package's own monorepo spends 26 lines on a run that prints roughly 250.
11
+ Verdicts nest. A composite reports, and so does every command into which it expands; a `:pre` or `:post` hook reports nothing of its own, since the command that it wraps already reports for the chain, though a hook that delegates to a named command reports under that name. A `-R` or `-F` invocation reports nothing either; every scope to which it fans out reports instead. A cold repo-wide `nmr ci` in this package's own monorepo uses 26 verdict lines to report a run that prints roughly 250.
12
12
 
13
- **A fan-out that selects no scope fails.** A `-F` pattern matches a package's manifest `name` rather than its directory name, so a plausible directory name selects nothing, and pnpm exits 0 over an empty selection as it does over a passing one. nmr asks pnpm what the pattern selects and refuses the invocation where the answer is nothing, naming the pattern and, where the pattern carries a name, the names it could have named. A pattern of another form states the rule its own shape answers to: a directory (`./dir`, `/dir`, `{dir}`), an exclusion (`!`), or a changed-since selector (`[<ref>]`), none of which a workspace name repairs. A pattern pnpm rejects rather than resolves, such as a bad git ref in a `[since]` selector, is left to pnpm to report. A `-R` typed on the invocation is refused on the same ground where the workspace declares no package at all, which is its only empty selection. That condition answers ahead of the pattern rules above: where the workspace holds no package, no `-F` pattern would have matched, so the refusal reports the workspace rather than the shape of the pattern.
13
+ **A fan-out that does not select any scope fails.** A `-F` pattern matches a package's manifest `name` rather than its directory name, so a plausible directory name selects nothing, and pnpm exits 0 over an empty selection as it does over a passing one. nmr asks pnpm what the pattern selects and refuses the invocation when the answer is nothing, naming the pattern and, when the pattern contains a name, the names that it could have named. For a pattern of another form, the refusal states the rule to which that form answers: a directory (`./dir`, `/dir`, `{dir}`), an exclusion (`!`), or a changed-since selector (`[<ref>]`), none of which a workspace name repairs. A pattern that pnpm rejects rather than resolves, such as a bad git ref in a `[since]` selector, is left to pnpm to report. A `-R` typed on the invocation is refused on the same ground when the workspace does not declare any package at all, which is its only empty selection. That condition takes precedence over the pattern rules above: When the workspace does not contain any package, a `-F` pattern could not have matched one, so the refusal reports the workspace rather than the shape of the pattern.
14
14
 
15
- **A workspace that holds no package is reported as one of four conditions.** `pnpm-workspace.yaml` may declare no positive pattern, declare patterns that match no directory holding a `package.json`, declare `!` exclusions that remove every directory matched by the positive patterns, or hold no valid YAML at all. Each refusal names its own condition and states a remedy, and each quotes the `packages` list the manifest declares, apart from the last, whose manifest nmr could not read. The second is the likeliest, because nmr [counts a directory as a package](workspace.md) only where it holds `package.json` and recognizes neither `package.yaml` nor `package.json5`.
15
+ **nmr reports a workspace that does not contain any package as one of four conditions.** `pnpm-workspace.yaml` may lack a positive pattern, declare patterns that do not match any directory containing a `package.json`, declare `!` exclusions that remove every directory matched by the positive patterns, or not contain valid YAML at all. Each refusal names its own condition and states a remedy, and each quotes the `packages` list that the manifest declares, apart from the last, whose manifest nmr could not read. The second is the likeliest, because nmr [counts a directory as a package](workspace.md) only if it contains `package.json` and recognizes neither `package.yaml` nor `package.json5`.
16
16
 
17
- **A `-R` that nmr composed into a script is not a fan-out the caller asked for.** In a workspace that holds no package, such a step -- in nmr's own default script or in a repo override alike -- is dropped from the chain, the steps beside it run, and the dropped step reports nothing of its own, as the rule above gives a `-R` invocation whose scopes report instead. Where the drop leaves the command with no step at all, as it does the root `build`, the command reports a no-op naming the workspace. A repo that holds no workspace package therefore runs the default root commands with no entry in `.config/nmr.config.ts`.
17
+ **A `-R` that nmr composed into a script is not a fan-out that the caller asked for.** In a workspace that does not contain any package, such a step -- in nmr's own default script or in a repo override alike -- is dropped from the chain, the steps beside it run, and the dropped step reports nothing of its own, as the rule above gives a `-R` invocation whose scopes report instead. When the drop leaves the command without any step at all, as it does the root `build`, the command reports a no-op naming the workspace. A repo that does not contain any workspace package therefore runs the default root commands without any entry in `.config/nmr.config.ts`.
18
18
 
19
- A verdict is one write of at most 512 bytes, the smallest `PIPE_BUF` POSIX permits, so packages running concurrently under `pnpm --recursive` cannot interleave within a line. A line longer than that is cut and marked with `…`; nothing nmr reports today comes near it.
19
+ A verdict is one write of at most 512 bytes, the smallest `PIPE_BUF` that POSIX permits, so packages running concurrently under `pnpm --recursive` cannot interleave within a line. A line longer than that is cut and marked with `…`; nothing nmr reports today comes near it.
20
20
 
21
21
  ## Reporting for a machine
22
22
 
23
- `--json` renders each verdict as a JSON object instead of a line of prose — one object per line, newline-delimited, with no enclosing array and no separators between them. A consumer parses each line as it arrives, and a run cut off partway still leaves every line it already emitted parseable, which an enclosing array would not.
23
+ `--json` renders each verdict as a JSON object instead of a line of prose: one object per line, newline-delimited, without an enclosing array or separators between them. A consumer parses each line as it arrives, and a run cut off partway still leaves every line that it already emitted parseable, which an enclosing array would not.
24
24
 
25
25
  ```console
26
26
  $ nmr --json check
@@ -29,80 +29,80 @@ $ nmr --json check
29
29
  {"command":"check","scope":"nmr-core","outcome":"passed","durationMs":31204}
30
30
  ```
31
31
 
32
- Every object carries `command`, `scope`, and `outcome`, and then what that outcome carries and no other does:
32
+ Every object contains `command`, `scope`, and `outcome`, and then the fields that only that outcome includes:
33
33
 
34
- | `outcome` | Also carries |
35
- | ---------- | ------------------------------------------------------------------------------------------------------------------------ |
36
- | `passed` | `durationMs` |
37
- | `failed` | `durationMs`, `exitCode` |
38
- | `recalled` | `ageMs`, `savedMs`, and `replay` where the skip has excerpts to [replay](check-cache.md#replaying-a-skipped-runs-output) |
39
- | `no-op` | `reason`, one of `empty-override`, `empty-workspace`, or `noop-override` |
34
+ | `outcome` | Also contains |
35
+ | ---------- | ----------------------------------------------------------------------------------------------------------------------- |
36
+ | `passed` | `durationMs` |
37
+ | `failed` | `durationMs`, `exitCode` |
38
+ | `recalled` | `ageMs`, `savedMs`, and `replay` when the skip has excerpts to [replay](check-cache.md#replaying-a-skipped-runs-output) |
39
+ | `no-op` | `reason`, one of `empty-override`, `empty-workspace`, or `noop-override` |
40
40
 
41
- Both renderings are produced from one record, so neither reports what the other does not. Where the prose line makes a presentation decision the record does not — a saving too small to be worth a clause, say — the object carries the fact and leaves the decision to whoever reads it.
41
+ Both renderings are produced from one record, so neither reports what the other does not. When the prose line makes a presentation decision that the record does not (a saving too small to be worth a clause, for example), the object contains the fact and leaves the decision to whoever reads it.
42
42
 
43
- **An object is one write of at most 512 bytes**, the [same ceiling](#what-nmr-reports) a prose line is held to, so packages running concurrently under `pnpm --recursive` cannot interleave within one. Cuts land inside the record's text rather than across its structure, so the line still parses, and a record that overruns is fitted by rungs — each shedding what a reader can better spare than the one below it:
43
+ **An object is one write of at most 512 bytes**, the [same ceiling](#what-nmr-reports) to which a prose line is held. Packages running concurrently under `pnpm --recursive` cannot interleave within one. Because nmr cuts inside the record's text rather than across its structure, the line still parses. nmr fits a record that overruns by rungs, each dropping what a reader can spare more easily than what the rung below it drops:
44
44
 
45
- 1. **The excerpts are shortened toward one another**, so an assembly's constituents come down together rather than the first ones being emptied to leave the last whole. No excerpt is cut below `…`, which keeps a cut one distinguishable from a run that recorded nothing.
46
- 2. **The excerpts go**, leaving the `scope` and `command` that name each constituent. An entry carrying no `excerpt` key is one whose text did not fit.
45
+ 1. **The excerpts are shortened toward one another**, so an assembly's constituents shrink together rather than the first ones being emptied to leave the last whole. nmr does not cut any excerpt below `…`, which keeps a cut one distinguishable from a run that recorded nothing.
46
+ 2. **The excerpts go**, leaving the `scope` and `command` that name each constituent. An entry without an `excerpt` key is one whose text did not fit.
47
47
  3. **The trailing constituents go**, as the prose line drops its own tail, and the `replay` array itself once none fit.
48
- 4. **The `scope` and `command` are cut**, each marked. This is the last resort and the reason for it is the ceiling itself: a line that overruns can be split across a pipe and corrupt the records of every scope sharing it, where a marked cut costs only its own.
48
+ 4. **The `scope` and `command` are cut**, each marked. This is the last resort and the reason for it is the ceiling itself: A line that overruns can be split across a pipe and corrupt the records of every scope sharing it, whereas a marked cut damages only its own.
49
49
 
50
- A package-scoped `check` carries four constituents and fits uncut. A four-package root `check` assembles twelve — `typecheck` and `test` each fan out to every package, and a nested composite's list is spliced in flat — which is more than the ceiling can name: it reaches rung 3, keeping eight and dropping the trailing four. Rung 4 needs a scope and command running to hundreds of bytes between them.
50
+ A package-scoped `check` has four constituents and fits uncut. A four-package root `check` assembles twelve (`typecheck` and `test` each fan out to every package, and a nested composite's list is spliced in flat), which is more than the ceiling can name: It reaches rung 3, keeping eight and dropping the trailing four. Rung 4 needs a scope and command running to hundreds of bytes between them.
51
51
 
52
- **The mode implies quiet.** The commands' own output is withheld, so stdout carries the objects alone — no prose verdict, and no override notice. `NMR_COMMAND_VERBOSITY=full` does not opt back out. A failure still surrenders the failing command's output, on stderr, as under any quiet run. Both modes leave a command on the same channels, so a machine-readable run and a quiet one share a [retention key](check-cache.md#replaying-a-skipped-runs-output): each replays what the other recorded.
52
+ **The mode implies quiet.** The commands' own output is withheld, so stdout contains the objects alone, without a prose verdict or an override notice. `NMR_COMMAND_VERBOSITY=full` does not opt back out. On a failure, nmr still prints the failing command's output on stderr, as under any quiet run. Because both modes leave a command on the same channels, a machine-readable run and a quiet one share a [retention key](check-cache.md#replaying-a-skipped-runs-output): Each replays what the other recorded.
53
53
 
54
- The format is nmr's own reporting and not a mode the commands it runs are told about, so it is out of the [pass key](check-cache.md#what-the-key-is-made-of) and the retention key alike. Selecting it neither invalidates a recorded pass nor costs a replay.
54
+ Because the format is nmr's own reporting, and nmr does not tell the commands that it runs about it, the format is out of the [pass key](check-cache.md#what-the-key-is-made-of) and the retention key alike. Selecting it neither invalidates a recorded pass nor prevents a replay.
55
55
 
56
- `--log`, `--help`, and `--version` are unaffected: none of them reports a verdict, and what `--log` prints is a tool's own transcript, which has no rendering as a record.
56
+ `--log`, `--help`, and `--version` are unaffected: None of them reports a verdict, and what `--log` prints is a tool's own transcript, which cannot be rendered as a record.
57
57
 
58
58
  ## Reserved environment variables
59
59
 
60
- `NMR_COMMAND_VERBOSITY` is nmr's own, and carries how loudly a run reports the output of the commands it runs. Its values are `full` and `quiet`; any other non-empty value is reported and exits 1, rather than falling back to a mode nobody chose, and an empty value reads as unset. Each process in a chain suppresses the output of the command it runs rather than of everything below it, so a failure still surrenders the failing command's output. It governs those commands' output alone: nmr's [verdicts](#what-nmr-reports) print in every verbosity, and the override notice is the one message a quiet run withholds.
60
+ `NMR_COMMAND_VERBOSITY` is nmr's own, and sets how loudly a run reports the output of the commands that it runs. Its values are `full` and `quiet`; nmr reports any other non-empty value and exits 1, rather than falling back to a mode that nobody chose, and an empty value reads as unset. Each process in a chain suppresses the output of the command that it runs rather than of everything below it, so the failing command's output still prints on a failure. It governs those commands' output alone: nmr's [verdicts](#what-nmr-reports) print in every verbosity, and the override notice is the one message that a quiet run withholds.
61
61
 
62
- `-q` sets it for the run and outranks an inherited value. Both values are spelled out so either direction is expressible: export `NMR_COMMAND_VERBOSITY=quiet` for a quiet shell, and set `full` on one invocation to opt back out.
62
+ `nmr -q <command>` sets it for the run and outranks an inherited value. Both values are spelled out so that either direction is expressible: Export `NMR_COMMAND_VERBOSITY=quiet` for a quiet shell, and set `full` on one invocation to opt back out.
63
63
 
64
- `NMR_OUTPUT_STYLE` is nmr's own as well, and carries the style every marker nmr prints is rendered in; see [output style](#output-style) below.
64
+ `NMR_OUTPUT_STYLE` is nmr's own as well, and sets the style in which nmr renders every marker that it prints; see [output style](#output-style) below.
65
65
 
66
- `NMR_REPORT_FORMAT` is nmr's own as well, and carries how nmr renders its own verdicts. Its values are `text` and `json`; any other non-empty value is reported and exits 1, and an empty value reads as unset. `--json` sets it for the run and outranks an inherited value, and both values are spelled out so either direction is expressible: export `NMR_REPORT_FORMAT=json` for a shell whose consumer parses, and set `text` on one invocation to opt back out. Two sources, then, where a verbosity has four: no repo-level default and no harness detection, since detection would hand every agent JSON in place of the prose the [shipped guidance](../README.md#agent-guidance) teaches. See [reporting for a machine](#reporting-for-a-machine) for what the objects carry.
66
+ `NMR_REPORT_FORMAT` is nmr's own as well, and sets how nmr renders its own verdicts. Its values are `text` and `json`; nmr reports any other non-empty value and exits 1, and an empty value reads as unset. `--json` sets it for the run and outranks an inherited value, and both values are spelled out so that either direction is expressible: Export `NMR_REPORT_FORMAT=json` for a shell whose consumer parses, and set `text` on one invocation to opt back out. The format has two sources, then, whereas a verbosity has four: It does not have a repo-level default or harness detection, because detection would hand every agent JSON in place of the prose that the [bundled guidance](../README.md#agent-guidance) teaches. See [reporting for a machine](#reporting-for-a-machine) for what the objects contain.
67
67
 
68
68
  ### Output style
69
69
 
70
- `--output-style <auto|plain|rich>` sets the style for one invocation, and `NMR_OUTPUT_STYLE` sets it for a shell: `rich` marks each verdict with an emoji, such as ✅ or ❌, and `plain` prints a word in its place, such as `PASS` or `FAIL`, which a reader of a log can search for. `auto`, the default, defers to detection. Any other value is a usage error that exits with code 1, whichever source named it. The flag outranks the variable, and both name `auto` as well as a style, so a shell exporting one style is overridable on one invocation in either direction.
70
+ `--output-style <auto|plain|rich>` sets the style for one invocation, and `NMR_OUTPUT_STYLE` sets it for a shell: `rich` marks each verdict with an emoji, such as ✅ or ❌, and `plain` prints a word in its place, such as `PASS` or `FAIL`, which a reader of a log can search for. `auto`, the default, defers to detection. Any other value is a usage error, and nmr exits with code 1, whichever source named it. The flag outranks the variable, and both accept `auto` as well as a style, so a shell exporting one style is overridable on one invocation in either direction.
71
71
 
72
- Detection decides stdout and stderr separately. A stream is plain when `CI` is set to anything other than an empty string or `false`, when the stream is not a terminal, or when `TERM` is `linux`; otherwise it is rich. `--json` output carries no marker in either style.
72
+ Detection decides stdout and stderr separately. A stream is plain when `CI` is set to anything other than an empty string or `false`, when the stream is not a terminal, or when `TERM` is `linux`; otherwise it is rich. `--json` output does not contain a marker in either style.
73
73
 
74
- **The flag belongs to `nmr`.** A standalone bin -- `nmr-clean`, `nmr-compile`, `nmr-report-catalog`, `nmr-report-overrides`, `nmr-ensure-prepublish-hooks` -- takes the variable alone, and passing it the flag gets that bin's unknown-option error. `nmr <command>` exports the style it resolved, so a bin run through nmr renders as nmr does.
74
+ **The flag belongs to `nmr`.** A standalone bin -- `nmr-clean`, `nmr-compile`, `nmr-report-catalog`, `nmr-report-overrides`, `nmr-ensure-prepublish-hooks` -- takes the variable alone, and passing it the flag produces that bin's unknown-option error. `nmr <command>` exports the style that it resolved, so a bin run through nmr renders as nmr does.
75
75
 
76
- **A nested run follows the one that spawned it.** The resolved style, never `auto`, travels down as `NMR_OUTPUT_STYLE`, so a child whose own stdout is a pipe still renders rich where the parent resolved rich. The style exported is the one stdout resolved to, which is where the verdicts and nearly every other line go; a parent at a terminal whose stderr alone is redirected therefore hands its children `rich`.
76
+ **A nested run follows the one that spawned it.** Because nmr passes the resolved style, never `auto`, down as `NMR_OUTPUT_STYLE`, a child whose own stdout is a pipe still renders rich when the parent resolved rich. The exported style is the one to which stdout resolved, and stdout receives the verdicts and nearly every other line; a parent at a terminal whose stderr alone is redirected therefore hands its children `rich`.
77
77
 
78
- The style is out of the [pass key](check-cache.md#what-the-key-is-made-of) and the [retention key](check-cache.md#replaying-a-skipped-runs-output) alike, as the verbosity is: it changes how a run renders and never what a command concludes. Selecting it neither invalidates a recorded pass nor costs a replay, and an excerpt recorded rich replays as recorded in a plain run.
78
+ The style is out of the [pass key](check-cache.md#what-the-key-is-made-of) and the [retention key](check-cache.md#replaying-a-skipped-runs-output) alike, as the verbosity is: It changes how a run renders and never what a command concludes. Selecting it neither invalidates a recorded pass nor prevents a replay, and an excerpt recorded rich replays as recorded in a plain run.
79
79
 
80
80
  ### Where a verbosity comes from
81
81
 
82
- Four sources, in precedence order. Each is read once, at the top of a chain: the resolved value travels down as `NMR_COMMAND_VERBOSITY`, so a nested process reads a decision already made rather than making its own.
82
+ Four sources, in precedence order. Each is read once, at the top of a chain: nmr passes the resolved value down as `NMR_COMMAND_VERBOSITY`, so a nested process reads a decision already made rather than making its own.
83
83
 
84
- | Source | Set it when |
85
- | ----------------------------------------------------- | ---------------------------------------------------------- |
86
- | `-q` | One invocation should be quiet. |
87
- | `NMR_COMMAND_VERBOSITY` | A shell, or a harness that launches one, should be quiet. |
88
- | `output.commandVerbosity` in the monorepo-root config | A repo should be quiet for everyone working in it. |
89
- | An agent harness nmr recognizes in the environment | Nothing above applies and an agent is running the command. |
84
+ | Source | Set it when |
85
+ | ------------------------------------------------------- | ---------------------------------------------------------- |
86
+ | `nmr -q <command>` | One invocation should be quiet. |
87
+ | `NMR_COMMAND_VERBOSITY` | A shell, or a harness that launches one, should be quiet. |
88
+ | `output.commandVerbosity` in the monorepo-root config | A repo should be quiet for everyone working in it. |
89
+ | An agent harness that nmr recognizes in the environment | Nothing above applies and an agent is running the command. |
90
90
 
91
- Falling off the bottom leaves the run `full`, which is what an unrecognized harness gets: detection adds quiet where it fires and changes nothing where it does not.
91
+ Below the last source, a run is `full`, which is what an unrecognized harness gets: Detection adds quiet when it fires and changes nothing when it does not.
92
92
 
93
- Detection reads a set of environment-variable names and fires on any one of them holding a non-empty value. The shipped set is `CLAUDECODE`, `ROVODEV_CLI`, and `ROVO_CLI`. **Treat that set as perishable.** The names belong to ecosystems nmr does not control and that rename, and a rename fails silently here, since the symptom is a return to loud output rather than an error. `output.extraAgentEnvVars` is how a repo adds a name — a harness nmr has never heard of, or one whose marker was renamed after the installed version shipped — without waiting for a release. A repo that wants no detection at all configures `output.commandVerbosity: 'full'`, which outranks it.
93
+ Detection reads a set of environment-variable names and fires on any one of them holding a non-empty value. The bundled set is `CLAUDECODE`, `ROVODEV_CLI`, and `ROVO_CLI`. **Expect that set to change.** The names belong to ecosystems that nmr does not control and that rename them, and a rename fails silently here, since the symptom is a return to loud output rather than an error. A repo adds a name with `output.extraAgentEnvVars` (for a harness unknown to nmr, or one whose marker was renamed after the installed version was released) without waiting for a release. A repo that does not want any detection at all configures `output.commandVerbosity: 'full'`, which outranks it.
94
94
 
95
95
  ## Configuring
96
96
 
97
97
  ```ts
98
98
  export default defineConfig({
99
99
  output: {
100
- // The verbosity a run takes when neither `-q` nor the environment named one.
100
+ // The verbosity that a run takes when neither `-q` nor the environment named one.
101
101
  commandVerbosity: 'quiet',
102
- // Markers of a harness the shipped set does not name, added to it rather than replacing it.
102
+ // Markers of a harness that the bundled set does not name, added to it rather than replacing it.
103
103
  extraAgentEnvVars: ['MY_HARNESS'],
104
104
  },
105
105
  });
106
106
  ```
107
107
 
108
- `output` is a monorepo-root key, and both of its fields feed the [verbosity ladder](#where-a-verbosity-comes-from).
108
+ `output` is a monorepo-root key, and both of its fields are inputs to the [verbosity ladder](#where-a-verbosity-comes-from).
package/docs/scripts.md CHANGED
@@ -16,49 +16,49 @@ Given the `build` command for a package that defines its own build script:
16
16
  | 2 | `.config/nmr.config.ts` | _(not set)_ | — |
17
17
  | 3 | `package.json` scripts | `"tsx custom-build.ts"` | ✓ |
18
18
 
19
- If no per-package override exists, the highest-tier value that is set wins. Set a script to `""` in `package.json` to skip it for that package.
19
+ If the package does not declare its own override, the highest-tier value that is set wins. Set a script to `""` in `package.json` to skip it for that package.
20
20
 
21
- > **Tip:** If your repo uses `eslint-plugin-package-json/valid-scripts`, empty strings are flagged as invalid. Use `":"` (the POSIX null command) instead — nmr treats it as a regular override that exits successfully and does nothing.
21
+ > **Tip:** If a repo uses `eslint-plugin-package-json/valid-scripts`, that rule flags empty strings as invalid. Use `":"` (the POSIX null command) instead: nmr treats it as a regular override that exits successfully and does nothing.
22
22
 
23
- A `package.json` entry that re-invokes the command it is declared under is never honored. Honoring `"build": "nmr build"` would spawn a shell running `nmr build` in the same directory, reaching the same entry again without bound, so nmr discards the entry and resolves the command from the tier below. An entry that carries only the self-reference, trailing arguments included, is discarded in silence: it declares no step to lose. An entry that chains anything else onto it — `"build": "nmr build && rdy compile"`, or `"build": "rdy compile && nmr build"` — would lose those steps, so nmr rejects the invocation instead, naming the file, the key, and where the steps belong. For an ordinary command that is a `<command>:pre` or `<command>:post` script: `"build:pre": "rdy verify"` and `"build:post": "rdy compile"` wrap nmr's own `build` rather than replacing it. For a hook entry it is the entry itself, since nmr wraps a `:pre` or `:post` script in no hooks of its own — `"build:post": "nmr build:post && rdy compile"` becomes `"build:post": "rdy compile"`.
23
+ A `package.json` entry that re-invokes the command under which it is declared is never honored. Honoring `"build": "nmr build"` would spawn a shell running `nmr build` in the same directory, reaching the same entry again without bound, so nmr discards the entry and resolves the command from the tier below. An entry that contains only the self-reference, trailing arguments included, is discarded in silence: It does not declare any step that nmr would lose. Because discarding an entry that chains anything else onto it (`"build": "nmr build && rdy compile"`, or `"build": "rdy compile && nmr build"`) would lose those steps, nmr rejects the invocation instead, naming the file, the key, and where the steps belong. For an ordinary command that is a `<command>:pre` or `<command>:post` script: `"build:pre": "rdy verify"` and `"build:post": "rdy compile"` wrap nmr's own `build` rather than replacing it. For a hook entry it is the entry itself, since nmr does not wrap a `:pre` or `:post` script in any hooks of its own: `"build:post": "nmr build:post && rdy compile"` becomes `"build:post": "rdy compile"`.
24
24
 
25
- The re-invocation is recognized wherever it stands in the entry, on the grammar [the shelled-nmr line](#script-values) uses, and a segment carrying `-F` or `-R` is not one — it hands the command to other scopes rather than back to this one. Nor is `-w` from inside a package, which reaches the root's registry and the root's `package.json`. A filter that selects the declaring package is the hole in that exemption: `nmr -F own-pkg build` in `own-pkg`'s own `build` re-enters without bound, and nmr does not recognize it.
25
+ nmr recognizes the re-invocation wherever it appears in the entry, on the grammar that [the shelled-nmr line](#script-values) uses, and a segment containing `-F` or `-R` is not one: It hands the command to other scopes rather than back to this one. Nor is `-w` from inside a package, which targets the root's registry and the root's `package.json`. A filter that selects the declaring package is the hole in that exemption: `nmr -F own-pkg build` in `own-pkg`'s own `build` re-enters without bound, and nmr does not recognize it.
26
26
 
27
27
  ### Script values
28
28
 
29
- A script value is a string or an array of steps. Only the built-in defaults and `.config/nmr.config.ts` carry an array; a `package.json` script must be a string, and any other value is rejected when the scripts are read. Arrays expand to chained `nmr` sub-invocations:
29
+ A script value is a string or an array of steps. Only the built-in defaults and `.config/nmr.config.ts` can contain an array; a `package.json` script must be a string, and any other value is rejected when the scripts are read. Arrays expand to chained `nmr` sub-invocations:
30
30
 
31
31
  ```ts
32
32
  // "fix": ["lint", "fmt"]
33
33
  // expands to: nmr lint && nmr fmt
34
34
  ```
35
35
 
36
- Each array element is a command name, optionally preceded by nmr's own flags, split on whitespace — `-R test` runs the element in every package, the way `nmr -R test` does. An element carrying a quoted argument or shell syntax is rejected when the config loads: name that command as a script of its own and reference the name.
36
+ Each array element is a command name, optionally preceded by nmr's own flags, split on whitespace: `-R test` runs the element in every package, the way `nmr -R test` does. An element containing a quoted argument or shell syntax is rejected when the config loads: Name that command as a script of its own and reference the name.
37
37
 
38
38
  #### Where trailing arguments go
39
39
 
40
- Trailing arguments reach every step of a composite, so `nmr fix --dry-run` runs `nmr lint --dry-run`, then `nmr fmt --dry-run`. A step whose work the arguments cannot narrow declines them by taking the long form of an element, and runs unnarrowed:
40
+ nmr passes trailing arguments to every step of a composite, so `nmr fix --dry-run` runs `nmr lint --dry-run`, then `nmr fmt --dry-run`. A step whose work the arguments cannot narrow declines them by taking the long form of an element, and runs unnarrowed:
41
41
 
42
42
  ```ts
43
43
  // "ci": [{ run: "build", shouldDeclineArguments: true }, "check:strict"]
44
44
  // nmr ci src/foo.ts -> nmr build && nmr check:strict src/foo.ts
45
45
  ```
46
46
 
47
- `{ run }` is the bare string element with room for the declaration; `shouldDeclineArguments` defaults to `false`, and an element that accepts is written as the string. Decline where the step is a prerequisite the narrowed steps run against, or where the tool it reaches would be misled — `tsgo --noEmit foo.ts` abandons the tsconfig, which is why every default `typecheck` step declines. A leaf tool that merely ignores what it is handed is not a reason to decline: nmr forwards, and the tool decides.
47
+ `{ run }` is the bare string element with room for the declaration; `shouldDeclineArguments` defaults to `false`, and an element that accepts is written as the string. Decline when the step is a prerequisite against which the narrowed steps run, or when the tool that it invokes would be misled: `tsgo --noEmit foo.ts` abandons the tsconfig, which is why every default `typecheck` step declines. A leaf tool that merely ignores what it is handed is not a reason to decline: nmr forwards, and the tool decides.
48
48
 
49
- Where no step of a command accepts them, nmr rejects the invocation and runs nothing, rather than running the whole command unnarrowed. That is what `nmr typecheck src/foo.ts` gets at the root, whose two steps both decline.
49
+ When every step of a command declines them, nmr rejects the invocation and runs nothing, rather than running the whole command unnarrowed. That is what `nmr typecheck src/foo.ts` gets at the root, whose two steps both decline.
50
50
 
51
- Two kinds of step are not declarable. A string script is one command handed to a leaf tool, so it always receives the arguments — which is why `nmr typecheck src/foo.ts` inside a package still reaches `tsgo`, where the root rejects it. A `:pre` or `:post` hook never receives them, the arguments having been placed before the hooks wrap the chain.
51
+ Two kinds of step are not declarable. A string script is one command handed to a leaf tool, so it always receives the arguments. That is why `nmr typecheck src/foo.ts` inside a package still runs `tsgo`, whereas the root rejects it. A `:pre` or `:post` hook never receives them, the arguments having been placed before the hooks wrap the chain.
52
52
 
53
- An invocation carrying trailing arguments also serves no part of its work from the [check-result cache](check-cache.md#check-result-cache), the steps that decline them included.
53
+ nmr also does not serve any part of an invocation with trailing arguments from the [check-result cache](check-cache.md#check-result-cache), including the steps that decline them.
54
54
 
55
- A string value is spawned through a shell as one command, so an `nmr` invocation inside it is invisible to the process above: a quiet failure surrenders the whole nested subtree rather than the failing step, and a loud run relays the nested tree through a forwarding pipe. nmr writes one line to stderr, in every verbosity, when a command resolves to a step reaching nmr through a shell.
55
+ A string value is spawned through a shell as one command. An `nmr` invocation inside it is invisible to the process above: A quiet failure prints the output of the whole nested subtree rather than that of the failing step, and a loud run relays the nested tree through a forwarding pipe. nmr writes one line to stderr, in every verbosity, when a command resolves to a step that runs nmr through a shell.
56
56
 
57
- The line names the declaration it is an edit to — the file, the field, and the key — and its remedy follows from that site. A `.config/nmr.config.ts` entry keeps its nmr steps as a step list and moves anything else to a `<command>:pre` or `<command>:post` script: a step-list element is composed into `nmr <element>`, so it can name only an nmr command. A `package.json` entry has to go, since `package.json` holds no step list: delete it outright when it restates what nmr already runs for that command, and move the steps it adds to a `<command>:pre` or `<command>:post` script when they are the package's own. Where the registry defines no such command, both apply: name the command in `.config/nmr.config.ts`, and put the package-specific steps in a hook. The pair is named rather than `:post` alone because an entry's extra steps may run on either side of the chain: `rdy verify && nmr compile` puts them first. A hook is the exception at both tiers: nmr wraps a `:pre` or `:post` script in no hooks of its own, so a hook's other steps become a script of their own that the step list names.
57
+ The line names the declaration to which it is an edit (the file, the field, and the key), and its remedy follows from that site. In a `.config/nmr.config.ts` entry, keep the nmr steps as a step list and move anything else to a `<command>:pre` or `<command>:post` script: A step-list element can name only an nmr command, because nmr composes it into `nmr <element>`. A `package.json` entry has to go, since `package.json` cannot declare a step list: Delete it outright when it restates what nmr already runs for that command, and move the steps that it adds to a `<command>:pre` or `<command>:post` script when they are the package's own. When the registry does not define that command, both apply: Name the command in `.config/nmr.config.ts`, and put the package-specific steps in a hook. The pair is named rather than `:post` alone because an entry's extra steps may run on either side of the chain: `rdy verify && nmr compile` puts them first. A hook is the exception at both tiers: nmr does not wrap a `:pre` or `:post` script in any hooks of its own. Move a hook's other steps to a script of their own, and name that script in the step list.
58
58
 
59
- nmr recognizes `nmr` in command position: at the start of a step, after an unquoted `&&`, `||`, `;`, `|`, or a newline, past any leading `NAME=value` assignments, and behind `npx`, `bunx`, or an `exec`, `dlx`, or `x` subcommand of `npm`, `pnpm`, `yarn`, or `bun`. This line reports a boundary rather than enforcing a rule — it covers a step nmr runs as written, and only the reporting of the nested run is degraded — and it stays partial in stated ways: a value-taking flag standing immediately before the program name hides it (`npx -p foo nmr`), a launcher outside that set goes unreported, and a `devBin` substitution that introduces `nmr` itself is not reported, the line being an edit to a declaration rather than to a substitution.
59
+ nmr recognizes `nmr` in command position: at the start of a step, after an unquoted `&&`, `||`, `;`, `|`, or a newline, past any leading `NAME=value` assignments, and behind `npx`, `bunx`, or an `exec`, `dlx`, or `x` subcommand of `npm`, `pnpm`, `yarn`, or `bun`. This line reports a boundary rather than enforcing a rule: It covers a step that nmr runs as written, and only the reporting of the nested run is degraded. The line is also partial in three ways: a value-taking flag standing immediately before the program name hides it (`npx -p foo nmr`), a launcher outside that set goes unreported, and a `devBin` substitution that introduces `nmr` itself is not reported, the line being an edit to a declaration rather than to a substitution.
60
60
 
61
- A [self-referential `package.json` entry](#three-tier-override-system) is the shape nmr cannot run as written, and it is rejected rather than reported. The same recognition finds it, so the same residuals apply — what goes unrecognized there re-enters nmr without bound, which hangs rather than passing quietly.
61
+ A [self-referential `package.json` entry](#three-tier-override-system) is the shape that nmr cannot run as written, and it is rejected rather than reported. The same recognition finds it, so the same residuals apply: What goes unrecognized there re-enters nmr without bound, which hangs rather than passing quietly.
62
62
 
63
63
  ## Pre and post hooks
64
64
 
@@ -66,8 +66,8 @@ The wrap rule and a repo-wide example are in the [README](../README.md#pre-and-p
66
66
 
67
67
  Behaviors worth knowing:
68
68
 
69
- - **Silent when absent**: Missing hooks produce no error and no output.
70
- - **Skip overrides apply to hooks**: A hook value of `""` or `":"` is treated the same as not defining the hook, with no console message.
69
+ - **Silent when absent**: A missing hook does not produce any error or output.
70
+ - **Skip overrides apply to hooks**: A hook value of `""` or `":"` is treated the same as not defining the hook, without a console message.
71
71
  - **Skipping the main command skips its hooks**: When `X` is overridden to `""` or `":"`, neither `X:pre` nor `X:post` fires.
72
72
  - **Recursion guard**: Direct invocation of a hook (e.g. `nmr build:pre`) is treated as a leaf operation. It does not itself attempt to resolve `build:pre:pre` or `build:pre:post`.
73
73
  - **Passthrough args attach only to the main command**: `nmr X --flag value` runs hooks without `--flag value`.
@@ -102,11 +102,11 @@ A minimal `.config/nmr.config.ts` is in the [README](../README.md#configuration)
102
102
 
103
103
  All fields are optional. [Script values](#script-values) describes `ScriptValue` and its step form, and both types are exported from `@williamthorsen/nmr/config`.
104
104
 
105
- Each field belongs to exactly one tier, and a config loads only the fields its own tier honors: `build` in a package config, the other five in the monorepo-root config. Declaring a field at the wrong tier fails with a message naming it and where it goes, as does a key nmr does not recognize at all — a typo cannot degrade into a setting that silently applies nowhere.
105
+ Each field belongs to exactly one tier, and nmr loads from a config only the fields that its tier honors: `build` in a package config, the other five in the monorepo-root config. Declaring a field at the wrong tier fails with a message naming it and where it goes, as does a key that nmr does not recognize at all: A typo cannot degrade into a setting that silently applies nowhere.
106
106
 
107
107
  ### Package-level configuration
108
108
 
109
- A package may carry its own `.config/nmr.config.ts`, which `nmr-compile` reads from the package it is compiling. This tier honors `build` alone; declaring `workspaceScripts`, `rootScripts`, or `devBin` there fails with a message naming them, rather than being quietly ignored — those belong in the monorepo-root config, and a package that appeared to set them would otherwise build on settings nothing applied. The monorepo-root config carries the mirror restriction: `build` there fails the same way, since only a package's own config reaches the compile.
109
+ A package may have its own `.config/nmr.config.ts`, which `nmr-compile` reads from the package that it is compiling. This tier honors `build` alone; declaring `workspaceScripts`, `rootScripts`, or `devBin` there fails with a message naming them, rather than being quietly ignored: Those belong in the monorepo-root config, and a package that appeared to set them would otherwise build on settings that nothing applied. The monorepo-root config has the mirror restriction: `build` there fails the same way, since only a package's own config affects the compile.
110
110
 
111
111
  ```ts
112
112
  // packages/my-package/.config/nmr.config.ts
@@ -117,13 +117,13 @@ export default defineConfig({
117
117
  });
118
118
  ```
119
119
 
120
- `extraIgnorePatterns` adds to the build's [default ignore set](utilities.md#nmr-compile) rather than replacing it, so declaring a pattern cannot start a package shipping its own tests. The programmatic `buildPackage` option of the same name behaves identically; its bare `ignorePatterns` is the one that replaces.
120
+ `extraIgnorePatterns` adds to the build's [default ignore set](utilities.md#nmr-compile) rather than replacing it, so declaring a pattern cannot make a package publish its own tests. The programmatic `buildPackage` option of the same name behaves identically; its bare `ignorePatterns` is the one that replaces.
121
121
 
122
- > **Building nmr from source:** a config file that has to load while nmr itself is being built — before its `dist` exists — must use the type-only form, `import type { NmrConfig } from '@williamthorsen/nmr/config'` with `satisfies NmrConfig`, which carries no runtime import. `defineConfig` resolves through nmr's build output and so is unavailable at that moment. Every other config, including every consumer's, installs nmr from a tarball that ships `dist` and can use `defineConfig` freely.
122
+ > **Building nmr from source:** A config file that has to load while nmr itself is being built (before its `dist` exists) must use the type-only form, `import type { NmrConfig } from '@williamthorsen/nmr/config'` with `satisfies NmrConfig`, which does not produce a runtime import. `defineConfig` is unavailable at that moment, because it resolves through nmr's build output. Every other config, including every consumer's, imports nmr from an installed tarball that contains `dist`, and can use `defineConfig` freely.
123
123
 
124
- ### `devBin` — source-repo binary substitution
124
+ ### `devBin`: source-repo binary substitution
125
125
 
126
- When developing a CLI tool inside the monorepo, the published binary may not reflect your latest source changes. `devBin` lets you map a binary name to a replacement command that runs from source:
126
+ During development of a CLI tool inside the monorepo, the published binary may not reflect the latest source changes. `devBin` maps a binary name to a replacement command that runs from source:
127
127
 
128
128
  ```ts
129
129
  export default defineConfig({
@@ -137,23 +137,23 @@ When nmr resolves a command whose first token matches a `devBin` key, it replace
137
137
 
138
138
  For example, if a workspace script resolves to `my-cli --verbose`, nmr rewrites it to `tsx /absolute/path/to/packages/my-cli/src/cli.ts --verbose`.
139
139
 
140
- > **Note:** Path resolution uses a heuristic: any non-flag token containing `/` is treated as a relative path. This works well for typical dev-tool commands but may incorrectly resolve URL-like values or glob patterns. Flags using `--flag=value` syntax are not resolved; use the spaced form `--flag value` for paths that need resolution.
140
+ > **Note:** Path resolution uses a heuristic: Any non-flag token containing `/` is treated as a relative path. This works well for typical dev-tool commands but may incorrectly resolve URL-like values or glob patterns. Flags using `--flag=value` syntax are not resolved; use the spaced form `--flag value` for paths that need resolution.
141
141
 
142
142
  ## Default commands
143
143
 
144
- `nmr --help` lists every default command and the shell command it resolves to. Repo-wide config (tier 2) and per-package overrides (tier 3) can add to or replace any of them. What the listing does not show is below.
144
+ `nmr --help` lists every default command and the shell command to which it resolves. Repo-wide config (tier 2) and per-package overrides (tier 3) can add to or replace any of them. What the listing does not show is below.
145
145
 
146
146
  ### Build and CI
147
147
 
148
- `ci` is what a code-quality workflow runs; it leaves out the audit, which belongs in a workflow of its own. `prepush` is what a developer runs before pushing: both gates, audit first. The audit takes seconds and `ci` takes minutes, so a vulnerability surfaces before the long gate runs. It composes `ci` rather than restating its stages, so a stage added to `ci` joins the pre-push run too.
148
+ `ci` is what a code-quality workflow runs; it leaves out the audit, which belongs in a workflow of its own. `prepush` is what a developer runs before pushing: both gates, audit first. The audit takes seconds and `ci` takes minutes, so a vulnerability is reported before the long gate runs. Because `prepush` composes `ci` rather than restating its stages, a stage added to `ci` runs in `prepush` too.
149
149
 
150
- The audit reaches the network. With no network, `ci` is the gate that still runs.
150
+ The audit needs network access. Without network access, `ci` is the gate that still runs.
151
151
 
152
- Neither is bound to a git hook. `prepush` is named for when you run it, not for a hook nmr installs.
152
+ Neither is bound to a git hook. `prepush` is named for when a developer runs it, not for a hook that nmr installs.
153
153
 
154
154
  ### Check and quality
155
155
 
156
- Neither gate computes coverage: Instrumentation multiplies the wall time of the run they exist to keep short. Coverage is [`test:coverage`](vitest.md#test-selections), run as a step of its own; a repo that wants it inside the gate [overrides](#configuration) `check:strict`.
156
+ Neither gate computes coverage: Instrumentation multiplies the wall time of the run that they exist to keep short. Coverage is [`test:coverage`](vitest.md#test-selections), run as a step of its own; a repo that wants it inside the gate [overrides](#configuration) `check:strict`.
157
157
 
158
158
  ### Lint
159
159
 
package/docs/upgrades.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  What `nmr upgrade` covers from each scope, how to declare the upgrade policy and its ceilings, and the subcommands and binary that the upgrade chains run.
4
4
 
5
- `nmr upgrade` reports the dependency upgrades available to your repo. What it covers depends on where you run it:
5
+ `nmr upgrade` reports the dependency upgrades available to the repo. What it covers depends on where it runs:
6
6
 
7
7
  | Invocation | Covers | Catalogs |
8
8
  | ---------------------------- | ------------------------------------------- | -------- |
@@ -11,17 +11,17 @@ What `nmr upgrade` covers from each scope, how to declare the upgrade policy and
11
11
  | `nmr -F <package> upgrade` | that package, from anywhere | no |
12
12
  | `nmr root:upgrade` | the root `package.json` | yes |
13
13
 
14
- A [pnpm catalog](https://pnpm.io/catalogs) is declared in `pnpm-workspace.yaml`, and the upgrade tool reads that file only when it sits at the working directory, irrespective of `--recursive`, which is why `root:upgrade` covers every catalog too. A package-scoped invocation therefore cannot see a dependency the catalog declares, and a package whose dependencies are all catalogued would otherwise report itself up to date. It runs [`report-catalog`](#report-catalog) first instead, which names each catalogued dependency and the root a covering pass runs from.
14
+ A [pnpm catalog](https://pnpm.io/catalogs) is declared in `pnpm-workspace.yaml`, and the upgrade tool reads that file only when it is in the working directory, irrespective of `--recursive`, which is why `root:upgrade` covers every catalog too. A package-scoped invocation therefore cannot see a dependency declared by the catalog, and a package whose dependencies are all catalogued would otherwise report itself up to date. It runs [`report-catalog`](#report-catalog) first instead, which names each catalogued dependency and the root from which a covering pass runs.
15
15
 
16
16
  Both root-scoped invocations precede their report with any active override, which pnpm declares in `pnpm-workspace.yaml`. An override pins a transitive dependency, so an upgrade masked by one never appears in the report.
17
17
 
18
- A `pnpm.overrides` block left in the root `package.json` fails the command instead. pnpm reads no setting from that field as of pnpm 11, so the block pins nothing, while the upgrade tool keeps its own list of dependency fields and goes on rewriting the versions in it under `--write` — a block that looks maintained and governs nothing. Failing ahead of the tool is what keeps that write from happening, on `root:upgrade` as much as on `upgrade`. nmr supports pnpm 11 and later.
18
+ A `pnpm.overrides` block left in the root `package.json` fails the command instead. Because pnpm does not read any setting from that field as of pnpm 11, the block pins nothing, while the upgrade tool keeps its own list of dependency fields and goes on rewriting the versions in it under `--write`: a block that looks maintained and governs nothing. Failing ahead of the tool keeps that write from happening, on `root:upgrade` as much as on `upgrade`. nmr supports pnpm 11 and later.
19
19
 
20
- The upgrade tool ([taze](https://github.com/antfu-collective/taze)) arrives with nmr, so your repo declares no dependency on it. Everything after the command name is passed through, including the range mode:
20
+ The upgrade tool ([taze](https://github.com/antfu-collective/taze)) is installed with nmr, so a consuming repo does not declare any dependency on it. Everything after the command name is passed through, including the range mode:
21
21
 
22
22
  ```bash
23
- nmr upgrade # minor and patch upgrades, regardless of the range each dependency declares
24
- nmr upgrade major # major upgrades, for dependencies carrying no per-package ceiling
23
+ nmr upgrade # minor and patch upgrades, regardless of the range that each dependency declares
24
+ nmr upgrade major # major upgrades, for dependencies without a per-package ceiling
25
25
  nmr upgrade --write # apply the proposals to package.json
26
26
  ```
27
27
 
@@ -34,26 +34,26 @@ import { defineConfig } from '@williamthorsen/nmr/taze';
34
34
 
35
35
  export default defineConfig({
36
36
  packageMode: {
37
- // Hold @types/node at the Node major your `engines` floor requires.
37
+ // Hold @types/node at the Node major that the `engines` floor requires.
38
38
  '@types/node': 'minor',
39
39
  },
40
40
  });
41
41
  ```
42
42
 
43
- `defineConfig` supplies nmr's shared upgrade policy — a seven-day quarantine on brand-new releases, and the pair of settings that report a dependency pinned to an exact version: locked dependencies are included, and the range searched is `minor` — so your config carries only what is specific to your repo. Any setting you declare wins over nmr's default. Passing `undefined` clears a default rather than falling back to it, which is how you hand the quarantine policy back to `pnpm-workspace.yaml`'s `minimumReleaseAge`. `mode` is the exception: the upgrade tool carries a default of its own for it, and clearing nmr's leaves no valid range to search, so name a mode instead.
43
+ `defineConfig` supplies nmr's shared upgrade policy, so a repo's config contains only what is specific to that repo. The policy is a seven-day quarantine on brand-new releases and the pair of settings that report a dependency pinned to an exact version: Locked dependencies are included, and the range searched is `minor`. Any setting that a repo's config declares wins over nmr's default. Passing `undefined` clears a default rather than falling back to it; that is how a config hands the quarantine policy back to `pnpm-workspace.yaml`'s `minimumReleaseAge`. `mode` is the exception: The upgrade tool has a default of its own for it, and clearing nmr's leaves the tool without a valid range to search. Name a mode instead.
44
44
 
45
- Everything the [taze configuration](https://github.com/antfu-collective/taze#config-file) accepts is accepted here, with five exceptions that reach the upgrade tool from no config file at all: `concurrency`, `githubActions`, `ignoreOtherWorkspaces`, `nodeVersion`, and `requestTimeout`. The tool's CLI writes a default for each of them over whatever the config declares ([taze#317](https://github.com/antfu-collective/taze/issues/317)), so the command line is the only place they can be set. nmr's ReadyUp kit warns when your config declares one, and [request timeout](#request-timeout) below shows how to set it instead.
45
+ Everything the [taze configuration](https://github.com/antfu-collective/taze#config-file) accepts is accepted here, with five exceptions that the upgrade tool does not read from any config file at all: `concurrency`, `githubActions`, `ignoreOtherWorkspaces`, `nodeVersion`, and `requestTimeout`. The tool's CLI writes a default for each of them over whatever the config declares ([taze#317](https://github.com/antfu-collective/taze/issues/317)), so the command line is the only place they can be set. nmr's ReadyUp kit warns when a repo's config declares one, and [request timeout](#request-timeout) below shows how to set it instead.
46
46
 
47
- > **Note:** This file is what activates nmr's upgrade policy at all. A repo without one gets neither the release quarantine nor the settings that report a dependency pinned to an exact version, and `nmr upgrade` reports nothing in a repo using pnpm's `savePrefix: ''`. nmr's ReadyUp kit warns when the file is missing.
47
+ > **Note:** Only this file activates nmr's upgrade policy. A repo without one gets neither the release quarantine nor the settings that report a dependency pinned to an exact version, and `nmr upgrade` reports nothing in a repo using pnpm's `savePrefix: ''`. nmr's ReadyUp kit warns when the file is missing.
48
48
 
49
- Two consequences of the `minor` range mode the policy declares:
49
+ Two consequences of the `minor` range mode declared by the policy:
50
50
 
51
- - A `packageMode` entry is honored only by the pass whose mode matches it, or by a `default` pass; every other pass drops that dependency rather than narrowing it. Under the policy's `minor` mode, a `patch` ceiling hides its dependency until you run `nmr upgrade patch`, and a `minor` ceiling hides it from `nmr upgrade major`.
51
+ - A `packageMode` entry is honored only by the pass whose mode matches it, or by a `default` pass; every other pass drops that dependency rather than narrowing it. Under the policy's `minor` mode, a `patch` ceiling hides its dependency until a developer runs `nmr upgrade patch`, and a `minor` ceiling hides it from `nmr upgrade major`.
52
52
  - A `~` range is searched as `^`, so minor upgrades are proposed for it and `--write` applies them under the original `~`. To hold one dependency to patches, give it a `packageMode` entry of `patch` and read it from `nmr upgrade patch`; to hold the whole repo there, declare `mode: 'patch'`.
53
53
 
54
54
  ## Request timeout
55
55
 
56
- Every `upgrade` script reaches the upgrade tool with a 30-second request timeout, in place of the tool's own 5 seconds.
56
+ Every `upgrade` script calls the upgrade tool with a 30-second request timeout, in place of the tool's own 5 seconds.
57
57
 
58
58
  Five seconds is too tight for a registry that proxies npm. The tool fetches a full packument whenever the resolved registry is not `registry.npmjs.org`, and a large one runs to tens of megabytes; the budget covers the whole retry chain rather than a single attempt, so one slow response ends the run with `Timeout requesting "<package>"`. The symptom is an upgrade pass that succeeds or fails according to how warm the tool's cache happens to be.
59
59
 
@@ -83,7 +83,7 @@ These commands are available as `nmr` subcommands and as standalone `nmr-`-prefi
83
83
 
84
84
  ### `report-catalog`
85
85
 
86
- Report the dependencies the current package takes from a [pnpm catalog](https://pnpm.io/catalogs), which a package-scoped `upgrade` cannot reach (see [dependency upgrades](#dependency-upgrades) for why). Each line names the dependency, its `catalog:` specifier, and the monorepo root a covering pass runs from. The workspace `upgrade` script runs it automatically, so no per-repo wiring is needed. The subcommand belongs to the workspace registry, so it runs from a package directory; the `nmr-report-catalog` bin reports nothing when run from the monorepo root, because a root-scoped pass reads the catalog itself.
86
+ Report the dependencies that the current package takes from a [pnpm catalog](https://pnpm.io/catalogs), which a package-scoped `upgrade` cannot read (see [dependency upgrades](#dependency-upgrades) for why). Each line names the dependency, its `catalog:` specifier, and the monorepo root from which a covering pass runs. The workspace `upgrade` script runs it automatically, so a repo does not need to wire it up. The subcommand belongs to the workspace registry and runs from a package directory. The `nmr-report-catalog` bin reports nothing when run from the monorepo root, because a root-scoped pass reads the catalog itself.
87
87
 
88
88
  ```bash
89
89
  nmr report-catalog
@@ -91,9 +91,9 @@ nmr report-catalog
91
91
 
92
92
  ### `report-overrides`
93
93
 
94
- Report any active pnpm override, declared in the root `pnpm-workspace.yaml`, reminding developers of overrides that may need cleanup. The root `upgrade` script runs it automatically, so no per-repo wiring is needed. Invoking it directly is useful for a one-off report.
94
+ Report any active pnpm override, declared in the root `pnpm-workspace.yaml`, reminding developers of overrides that may need cleanup. The root `upgrade` script runs it automatically, so a repo does not need to wire it up. Invoking it directly is useful for a one-off report.
95
95
 
96
- A `pnpm.overrides` block in the root `package.json` fails the command, naming every entry it holds and where to move it — see [dependency upgrades](#dependency-upgrades) for why the block is inert. `pnpx codemod run pnpm-v10-to-v11` performs the move.
96
+ A `pnpm.overrides` block in the root `package.json` fails the command, which names every entry that the block contains and where to move it; see [dependency upgrades](#dependency-upgrades) for why the block is inert. `pnpx codemod run pnpm-v10-to-v11` performs the move.
97
97
 
98
98
  ```bash
99
99
  nmr report-overrides
@@ -101,9 +101,9 @@ nmr report-overrides
101
101
 
102
102
  ## `nmr-taze`
103
103
 
104
- Run the [taze](https://github.com/antfu-collective/taze) dependency-upgrade tool, forwarding every argument to it untouched and adding one of its own: a 30-second request timeout, unless the invocation already carries one. This is what every `upgrade` chain ends with, `root:upgrade` included — see [dependency upgrades](#dependency-upgrades) for the workflow, and [request timeout](#request-timeout) for why the timeout is set here.
104
+ Run the [taze](https://github.com/antfu-collective/taze) dependency-upgrade tool, forwarding every argument to it untouched and adding one of its own: a 30-second request timeout, unless the invocation already includes one. Every `upgrade` chain ends with it, `root:upgrade` included; see [dependency upgrades](#dependency-upgrades) for the workflow, and [request timeout](#request-timeout) for why the timeout is set here.
105
105
 
106
- Under pnpm's isolated `node_modules`, a transitive package's binary is absent from the consuming repo's `node_modules/.bin`, so a repo that depends on nmr cannot run `taze` directly. `nmr-taze` can, because nmr is a direct dependency, and it resolves the tool from the tree nmr controls.
106
+ Under pnpm's isolated `node_modules`, a transitive package's binary is absent from the consuming repo's `node_modules/.bin`, so a repo that depends on nmr cannot run `taze` directly. `nmr-taze` can, because nmr is a direct dependency, and it resolves the tool from the tree that nmr controls.
107
107
 
108
108
  ```bash
109
109
  nmr-taze --recursive
package/docs/utilities.md CHANGED
@@ -4,7 +4,7 @@ The binaries behind nmr's default `clean`, `compile`, and `fmt` commands, and a
4
4
 
5
5
  ## `nmr-clean`
6
6
 
7
- Remove a package's build output (`dist`) and its `nmr-compile` cache entry, leaving no state behind for the next build to skip on. Run from a package directory it cleans that package; run from the monorepo root it sweeps every workspace package in a single pass, running each package's resolved `clean` — so a package that overrides `clean`, in `.config/nmr.config.ts` or in its own `package.json`, gets its own command rather than the sweep. Removal is idempotent — cleaning an unbuilt package is a silent no-op. This is the default `clean` script at both levels.
7
+ Remove a package's build output (`dist`) and its `nmr-compile` cache entry, so that the next build cannot skip on leftover state. Run from a package directory, it cleans that package; run from the monorepo root, it sweeps every workspace package in a single pass, running each package's resolved `clean`, so a package that overrides `clean`, in `.config/nmr.config.ts` or in its own `package.json`, gets its own command rather than the sweep. Removal is idempotent: Cleaning an unbuilt package is a silent no-op. This is the default `clean` script at both levels.
8
8
 
9
9
  ```bash
10
10
  nmr-clean
@@ -12,17 +12,17 @@ nmr-clean
12
12
 
13
13
  ## `nmr-compile`
14
14
 
15
- Compile a single package's `src` tree to `dist/esm` with the TypeScript compiler API, emitting `.js` from one program and `.d.ts` from a second that reuses it. The package's own `removeComments` governs the `.js` emit; the declarations emit forces it off, so `.d.ts` files carry their doc comments either way. Because the compiler parses each source file, every relative import form — static, re-export, dynamic `import()`, and bare side-effect — is rewritten from `.ts` to `.js` in both outputs, and `.ts` occurrences inside strings and comments are left intact. tsconfig `paths` aliases are resolved to runnable relative `.js` specifiers in both outputs, sourced from the package's tsconfig. An aliased import whose target resolves outside the package's `src/` and is not resolvable without the alias mapping fails the build with a diagnostic, rather than being emitted verbatim to produce output that fails at runtime. The build is skipped when no input has changed and the previous output is still on disk (the key is cached under `node_modules/.cache/nmr-compile/`, outside the published output). Deleting the output by any means — `nmr clean`, `rm -rf dist`, `git clean` — therefore forces a rebuild rather than a skip. This is the default `compile` script — run it from a package directory.
15
+ Compile a single package's `src` tree to `dist/esm` with the TypeScript compiler API, emitting `.js` from one program and `.d.ts` from a second that reuses it. The package's own `removeComments` governs the `.js` emit; the declarations emit forces it off, so `.d.ts` files keep their doc comments either way. Because the compiler parses each source file, every relative import form (static, re-export, dynamic `import()`, and bare side-effect) is rewritten from `.ts` to `.js` in both outputs, and `.ts` occurrences inside strings and comments are left intact. tsconfig `paths` aliases are resolved to runnable relative `.js` specifiers in both outputs, sourced from the package's tsconfig. An aliased import whose target resolves outside the package's `src/` and is not resolvable without the alias mapping fails the build with a diagnostic, rather than being emitted verbatim to produce output that fails at runtime. The build is skipped when every input is unchanged and the previous output is still on disk (the key is cached under `node_modules/.cache/nmr-compile/`, outside the published output). Deleting the output by any means (`nmr clean`, `rm -rf dist`, `git clean`) therefore forces a rebuild rather than a skip. This is the default `compile` script; run it from a package directory.
16
16
 
17
- **What it compiles.** Entry points are `src/**/*.ts` less the directories that hold test scaffolding rather than shipped code: `__fixtures__/`, `__mocks__/`, `__tests__/`, and `test-utils/`. Add to that list per package with [`build.extraIgnorePatterns`](scripts.md#package-level-configuration). Ignoring a directory drops it as an _entry point_, not from the emit: the compiler still emits whatever the surviving entry points import, so a helper that production code uses is still compiled and its importer never emits a dangling specifier. This list is deliberately not the Vitest [coverage exclusions](vitest.md#what-the-config-excludes) — helpers live in `test-utils/` precisely so they stay covered.
17
+ **What it compiles.** Entry points are `src/**/*.ts` less the directories that contain test scaffolding rather than published code: `__fixtures__/`, `__mocks__/`, `__tests__/`, and `test-utils/`. Add to that list per package with [`build.extraIgnorePatterns`](scripts.md#package-level-configuration). Ignoring a directory drops it as an _entry point_, not from the emit: The compiler still emits whatever the surviving entry points import, so a helper that production code uses is still compiled and the compiler never emits a dangling specifier in its importer. This list is deliberately not the Vitest [coverage exclusions](vitest.md#what-the-config-excludes): Helpers live in `test-utils/` precisely so that they stay covered.
18
18
 
19
- **The build owns its output directory.** Every rebuild replaces `dist/esm` wholesale, so the directory holds exactly the current emit: a source that was deleted, or that a widened ignore set now covers, leaves nothing behind. Assets therefore belong outside the output directory, or in a `build:post` hook, which runs after the output is published and so composes correctly. Note that a bare `nmr compile` runs no hook, so assets a `build:post` step copies in are absent until a full `nmr build` restores them. An `outdir` that does not resolve inside the package is rejected rather than replaced.
19
+ **The build owns its output directory.** Every rebuild replaces `dist/esm` wholesale, so the directory contains exactly the current emit: A source that was deleted, or that a widened ignore set now covers, leaves nothing behind. Assets therefore belong outside the output directory, or in a `build:post` hook, which composes correctly because it runs after the output is published. Because a bare `nmr compile` does not run any hook, assets that a `build:post` step copies in are absent until a full `nmr build` restores them. An `outdir` that does not resolve inside the package is rejected rather than replaced.
20
20
 
21
- **The replacement is atomic.** The emit is buffered in memory, written to a staging directory beside the output directory, and swapped into place by rename. A build that fails for any reason -- a malformed tsconfig, an unresolvable alias, a full disk -- therefore leaves the previous output exactly as it was. `dist/esm` is never observed mid-write: it holds the previous build or the new one, and is absent only between the two renames. This matters most when the package being rebuilt supplies a binary that other packages in the same recursive build invoke.
21
+ **The replacement is atomic.** The emit is buffered in memory, written to a staging directory beside the output directory, and swapped into place by rename. A build that fails for any reason -- a malformed tsconfig, an unresolvable alias, a full disk -- therefore leaves the previous output exactly as it was. `dist/esm` is never observed mid-write: It contains the previous build or the new one, and is absent only between the two renames. This matters most when the package being rebuilt supplies a binary that other packages in the same recursive build invoke.
22
22
 
23
- **What busts the cache.** The key folds each input's content and path, the emit options, the resolved TypeScript version, and a fingerprint of the nmr running the build -- its own build digest in a workspace checkout, its package version otherwise. Upgrading nmr in a consuming repo therefore rebuilds every package nmr compiles, and an edit to nmr's own build sources rebuilds each sibling once nmr itself has been rebuilt. A build of nmr's own package folds the version rather than the digest, which is the cache entry that build is about to write.
23
+ **What busts the cache.** The key folds each input's content and path, the emit options, the resolved TypeScript version, and a fingerprint of the nmr running the build -- its own build digest in a workspace checkout, its package version otherwise. Upgrading nmr in a consuming repo therefore rebuilds every package that nmr compiles, and an edit to nmr's own build sources rebuilds each sibling once nmr itself has been rebuilt. A build of nmr's own package folds the version rather than the digest, which is the cache entry that this build is about to write.
24
24
 
25
- `typescript` is a peer dependency (`>=5.7.0 <7`); the consuming repo provides it. The floor is what `rewriteRelativeImportExtensions` requires; the ceiling is because TypeScript 7 ships no compiler API — its root export is a version constant, so `nmr-compile` cannot run on it. Relative imports in source must carry explicit `.ts` extensions for them to be rewritten.
25
+ `typescript` is a peer dependency (`>=5.7.0 <7`); the consuming repo provides it. The floor is what `rewriteRelativeImportExtensions` requires; the ceiling exists because TypeScript 7 does not include a compiler API: Its root export is a version constant, so `nmr-compile` cannot run on it. Relative imports in source must use explicit `.ts` extensions for them to be rewritten.
26
26
 
27
27
  ```bash
28
28
  nmr-compile
@@ -30,11 +30,11 @@ nmr-compile
30
30
 
31
31
  ## `nmr-fmt`
32
32
 
33
- Format, or check the formatting of, the files git reports for the working directory. Exactly one of `--check` and `--write` is required; a bare invocation prints usage and exits non-zero rather than defaulting to a mutation. `--write` also lists the files it rewrote. This is what `fmt` and `fmt:check` resolve to, in both registries.
33
+ Format, or check the formatting of, the files that git reports for the working directory. Exactly one of `--check` and `--write` is required; a bare invocation prints usage and exits non-zero rather than defaulting to a mutation. `--write` also lists the files that it rewrote. This is what `fmt` and `fmt:check` resolve to, in both registries.
34
34
 
35
- `prettier` is an optional peer dependency (`>=3.9.5 <4`); the consuming repo provides it, and it is resolved through the module graph rather than from `PATH`, so the copy that runs is the one the repo declares and not whichever `prettier` happens to come first. A repository's formatter has to be the one its editor and pre-commit hook also run, which is why nmr takes it from the consumer instead of bundling a copy. It is optional because a repo can use nmr purely as a script runner; one that formats without a resolvable Prettier gets a message naming the package and the range.
35
+ `prettier` is an optional peer dependency (`>=3.9.5 <4`); the consuming repo provides it, and it is resolved through the module graph rather than from `PATH`, so the copy that runs is the one that the repo declares and not whichever `prettier` happens to come first. A repository's formatter has to be the one that its editor and pre-commit hook also run, which is why nmr takes it from the consumer instead of bundling a copy. It is optional because a repo can use nmr purely as a script runner; one that formats without a resolvable Prettier gets a message naming the package and the range.
36
36
 
37
- The floor is a currency policy, not a capability boundary. What the design requires is `--ignore-path` honouring every flag rather than only the last, so a repository-root ignore file passed alongside a package-level one is not silently dropped — Prettier has done that since 3.0.0, while 2.x honoured only the final flag. The floor sits at the current release because every consuming repo tracks it; lowering it to `>=3.0.0` would cost correctness nothing.
37
+ The floor is a currency policy, not a capability boundary. The design requires `--ignore-path` to honour every flag rather than only the last, so that a repository-root ignore file passed alongside a package-level one is not silently dropped. Prettier has done that since 3.0.0, while 2.x honoured only the final flag. The floor is at the current release because every consuming repo tracks it; lowering it to `>=3.0.0` would not affect correctness.
38
38
 
39
39
  ```bash
40
40
  nmr-fmt --check
@@ -43,11 +43,11 @@ nmr-fmt --write packages/nmr
43
43
 
44
44
  ### File selection
45
45
 
46
- `fmt` and `fmt:check` format the files git reports: tracked files, plus untracked files git does not ignore. Prettier reads `.gitignore` and `.prettierignore` from the working directory alone, so a pattern in `packages/<pkg>/.gitignore` never reaches a run started at the monorepo root. git knows the whole hierarchy, including `.git/info/exclude` and `core.excludesFile`, and every `.prettierignore` in the tree is discovered from the repository root, so package-level exclusions apply from any directory as well.
46
+ `fmt` and `fmt:check` format the files that git reports: tracked files, plus untracked files that git does not ignore. Prettier reads `.gitignore` and `.prettierignore` from the working directory alone, so a pattern in `packages/<pkg>/.gitignore` never applies to a run started at the monorepo root. Because git knows the whole hierarchy, including `.git/info/exclude` and `core.excludesFile`, and every `.prettierignore` in the tree is discovered from the repository root, package-level exclusions apply from any directory as well.
47
47
 
48
- A file git ignores is never formatted, even when named directly. Trailing arguments are git pathspecs rather than Prettier flags, so `nmr fmt:check packages/nmr` narrows the run, while an unrecognized option and a pathspec matching no formattable file both fail it. Running outside a git repository fails rather than reporting a clean run.
48
+ A file that git ignores is never formatted, even when named directly. Because trailing arguments are git pathspecs rather than Prettier flags, `nmr fmt:check packages/nmr` narrows the run, while an unrecognized option and a pathspec that does not match any formattable file both fail it. Running outside a git repository fails rather than reporting a clean run.
49
49
 
50
- Paths the index names but the filesystem does not have (a file deleted but not yet staged) and submodule gitlinks are both dropped, so neither reaches Prettier.
50
+ `nmr-fmt` drops paths that the index names but the filesystem does not have (a file deleted but not yet staged), and submodule gitlinks, before calling Prettier.
51
51
 
52
52
  ## `ensure-prepublish-hooks`
53
53