@williamthorsen/nmr 0.34.0 → 0.35.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 (56) hide show
  1. package/.readyup/kits/default.js +92 -19
  2. package/.readyup/manifest.json +11 -8
  3. package/README.md +83 -17
  4. package/agents/guidance/rulebooks/nmr.md +1 -1
  5. package/dist/esm/UserError.d.ts +6 -0
  6. package/dist/esm/check-cache.d.ts +145 -0
  7. package/dist/esm/commands/build-output.d.ts +64 -0
  8. package/dist/esm/commands/build.d.ts +29 -1
  9. package/dist/esm/commands/build.js +25 -5
  10. package/dist/esm/commands/clean.d.ts +15 -0
  11. package/dist/esm/commands/ensure-prepublish-hooks.d.ts +11 -0
  12. package/dist/esm/commands/fmt.d.ts +32 -0
  13. package/dist/esm/commands/report-catalog.d.ts +10 -0
  14. package/dist/esm/commands/report-overrides.d.ts +6 -0
  15. package/dist/esm/commands/taze.d.ts +18 -0
  16. package/dist/esm/config.d.ts +25 -0
  17. package/dist/esm/context.d.ts +11 -0
  18. package/dist/esm/default-scripts.d.ts +15 -0
  19. package/dist/esm/defineConfig.d.ts +9 -0
  20. package/dist/esm/help.d.ts +13 -0
  21. package/dist/esm/helpers/clampToBytes.d.ts +12 -0
  22. package/dist/esm/helpers/createBoundedBuffer.d.ts +7 -0
  23. package/dist/esm/helpers/deriveExcerpt.d.ts +10 -0
  24. package/dist/esm/helpers/duration.d.ts +15 -0
  25. package/dist/esm/helpers/hook-name.d.ts +8 -0
  26. package/dist/esm/helpers/package-json.d.ts +25 -0
  27. package/dist/esm/helpers/readStringValues.d.ts +7 -0
  28. package/dist/esm/helpers/reportClosing.d.ts +15 -0
  29. package/dist/esm/helpers/transcript.d.ts +25 -0
  30. package/dist/esm/helpers/type-guards.d.ts +1 -0
  31. package/dist/esm/helpers/workspace-patterns.d.ts +7 -0
  32. package/dist/esm/prettier.d.ts +8 -0
  33. package/dist/esm/recording.d.ts +39 -1
  34. package/dist/esm/replay-assembly.d.ts +18 -2
  35. package/dist/esm/report-format.d.ts +33 -0
  36. package/dist/esm/reportCliFailure.d.ts +8 -0
  37. package/dist/esm/resolve-scripts.d.ts +10 -1
  38. package/dist/esm/resolver.d.ts +67 -2
  39. package/dist/esm/resolver.js +1 -1
  40. package/dist/esm/runCli.d.ts +17 -0
  41. package/dist/esm/runCli.js +1 -1
  42. package/dist/esm/runner.d.ts +52 -0
  43. package/dist/esm/scripts.d.ts +1 -0
  44. package/dist/esm/steps.d.ts +71 -0
  45. package/dist/esm/taze.d.ts +16 -0
  46. package/dist/esm/tests.d.ts +40 -0
  47. package/dist/esm/tests.js +25 -0
  48. package/dist/esm/tiers.d.ts +56 -1
  49. package/dist/esm/tiers.js +22 -9
  50. package/dist/esm/types.d.ts +27 -0
  51. package/dist/esm/verbosity.d.ts +37 -0
  52. package/dist/esm/verdict.d.ts +45 -0
  53. package/dist/esm/vitest.d.ts +72 -2
  54. package/dist/esm/vitest.js +17 -9
  55. package/dist/esm/workspace.d.ts +18 -0
  56. package/package.json +18 -10
@@ -1,3 +1,13 @@
1
+ /**
2
+ * One element of a resolved command chain, carrying how it was composed rather than how its text reads.
3
+ * A `structural` step is nmr's own composition, held as argv; an `opaque` step is a command nmr does not parse.
4
+ *
5
+ * A structural step's argv leads with the file to spawn, so the runner has one to hand `spawn` without a shell.
6
+ *
7
+ * `declinesArgs` travels from the composite element that composed the step to the one reader that acts on it,
8
+ * the binding of the invocation's trailing arguments. Every stage between the two -- the devBin substitution,
9
+ * the chain rendering, the replay assembly, the runner -- passes it through and asks nothing of it.
10
+ */
1
11
  export type Step = {
2
12
  kind: 'opaque';
3
13
  command: string;
@@ -6,19 +16,80 @@ export type Step = {
6
16
  argv: readonly [string, ...(readonly string[])];
7
17
  declinesArgs?: boolean;
8
18
  };
19
+ /** What a structural step asks of the nmr process it spawns. */
9
20
  export interface NmrStepTarget {
10
21
  command: string;
22
+ /** Whether the command is fanned out to other scopes, where a `-R` or `-F` sends it. */
11
23
  isDelegate: boolean;
24
+ /** Whether the command runs against the root registry, where a `-w` anchors it. */
12
25
  isWorkspaceRoot: boolean;
13
26
  }
27
+ /**
28
+ * How a `package.json` entry names the command it is declared under: as its whole value, or alongside other
29
+ * steps. `sole` covers an entry carrying trailing arguments, which declare no step of their own.
30
+ */
14
31
  export type SelfReference = 'chained' | 'sole';
32
+ /**
33
+ * Composes the structural step that re-invokes nmr for one composite element.
34
+ *
35
+ * The element tokenizes on whitespace, so it may carry nmr's own flags but cannot carry a space-bearing token.
36
+ * `-w` is prepended as its own token, so the child selects the root registry on its own.
37
+ *
38
+ * `declinesArgs` is set only where it holds, so a step that takes the trailing arguments renders and compares
39
+ * exactly as it did before any element declared anything.
40
+ */
15
41
  export declare function composeNmrStep(element: string, workspaceRoot: boolean, declinesArgs?: boolean): Step;
42
+ /**
43
+ * Returns the text of the first opaque step that reaches nmr through a shell, or `undefined` when none does.
44
+ *
45
+ * Recognizes nmr in command position: at the start of the step, after `&&`, `||`, `;`, `|`, or a newline, past
46
+ * any leading environment assignments, and behind a launcher such as `npx` or `pnpm exec`. A separator inside
47
+ * quotes opens no position, so a command merely naming nmr in an argument is not a crossing.
48
+ *
49
+ * Partial by construction, and partial in stated ways rather than arbitrary ones: a value-taking flag standing
50
+ * immediately before the program name hides it (`npx -p foo nmr`), and a launcher outside `LAUNCHERS` goes
51
+ * unreported. What it finds is the boundary below which nmr cannot tell its own processes from the tools it
52
+ * runs.
53
+ */
16
54
  export declare function findNmrCrossing(steps: readonly Step[]): string | undefined;
55
+ /**
56
+ * Returns the first token of a composite element that falls outside the grammar, or `undefined` when the
57
+ * element is a command name optionally preceded by nmr's own flags. A token the shell would not read
58
+ * literally is one the rendering would quote whole, turning the element into a command nobody wrote.
59
+ */
17
60
  export declare function findUnexpressibleToken(element: string): string | undefined;
61
+ /**
62
+ * Returns the command a structural step's nmr process runs, whether the step hands that command to other
63
+ * scopes rather than running it where it stands, and whether it anchors the command at the monorepo root.
64
+ * Reports nothing for a step that names no nmr command.
65
+ *
66
+ * The inverse of `composeNmrStep`, and beside it because the two share one grammar: an element may lead with
67
+ * nmr's own flags, and the command is the first token that is not one.
68
+ */
18
69
  export declare function readNmrStep(step: Step): NmrStepTarget | undefined;
70
+ /**
71
+ * Reports whether a `package.json` entry re-invokes the command it is declared under, and whether it declares
72
+ * anything besides. Reports nothing for an entry that names another command, or none.
73
+ *
74
+ * Honouring such an entry would spawn a shell running the same command in the same directory, reaching the
75
+ * same entry again without bound, so resolution discards it however it reads. `chained` is what separates an
76
+ * entry that thereby loses steps from one that declares nothing to lose.
77
+ *
78
+ * A segment that delegates carries the command to other scopes rather than back to this one. `-w` re-enters
79
+ * only from the root, a package's entry reaching the root's registry and `package.json` instead of its own.
80
+ *
81
+ * Partial in the same ways `findNmrCrossing` is, and for the same reason: what goes unrecognized re-enters
82
+ * without bound, which hangs rather than passing quietly.
83
+ */
19
84
  export declare function readSelfReference(options: {
20
85
  anchoredAtRoot: boolean;
21
86
  commandName: string;
22
87
  script: string;
23
88
  }): SelfReference | undefined;
89
+ /**
90
+ * Renders a step list as the `&&` chain a shell runs.
91
+ *
92
+ * The sole producer of a chain string: the check-result cache keys on that string, so a change to the rendering
93
+ * invalidates every recorded pass by construction rather than by anyone remembering to.
94
+ */
24
95
  export declare function renderChain(steps: readonly Step[]): string;
@@ -1,5 +1,21 @@
1
1
  import type { CheckOptions } from 'taze';
2
+ /**
3
+ * A taze configuration.
4
+ *
5
+ * Every property admits `undefined` explicitly, unlike `Partial<CheckOptions>`, which rejects it under
6
+ * `exactOptionalPropertyTypes`. Passing `undefined` is how a consumer clears `maturityPeriod`, so the type
7
+ * has to allow it.
8
+ */
2
9
  export type TazeConfig = {
3
10
  [K in keyof CheckOptions]?: CheckOptions[K] | undefined;
4
11
  };
12
+ /**
13
+ * Builds a taze configuration from nmr's shared upgrade policy and a repo's own settings.
14
+ *
15
+ * Any property the caller supplies wins, including `0` and an explicit `undefined` — the latter restores
16
+ * taze's inheritance of pnpm's `minimumReleaseAge`, which only applies while `maturityPeriod` is unset.
17
+ *
18
+ * The merge is shallow, which suits a policy of scalar defaults. A nested default would clobber rather
19
+ * than merge, so adding one means revisiting this.
20
+ */
5
21
  export declare function defineConfig(config: TazeConfig): TazeConfig;
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Declares a suite asserting that a repo's test files hold to nmr's conventions: every collected file names an
3
+ * isolation tier, and every test file sits under a `__tests__` directory.
4
+ *
5
+ * Wire it from a one-line test of the repo's own, so the file stays under the repo's `__tests__` and the repo
6
+ * scopes what the sweep covers. Both halves are silent without it: `unit` is the residual project, so an untiered
7
+ * file runs and passes, and a file outside `__tests__` is collected by nothing at all.
8
+ *
9
+ * `exclude` names directory basenames the sweep prunes at any depth, additive to the ones nmr always prunes. Pass
10
+ * the same array to `defineVitestConfig`'s `testCollectionExclude`, or the sweep and the collection glob describe
11
+ * different trees: a directory pruned here alone still runs the files this suite stopped reporting.
12
+ */
13
+ export declare function checkTestFileConventions(options?: TestFileConventionsOptions): void;
14
+ /**
15
+ * Sweeps a repo for both halves of the convention, reporting the root it swept alongside what it found.
16
+ *
17
+ * The root is what makes the result trustworthy, and it is the one thing an assertion cannot check: a sweep
18
+ * started from the directory Vitest supplies covers one package, finds nothing, and passes.
19
+ *
20
+ * @internal - Exported only to enable testing
21
+ */
22
+ export declare function reportTestFileConventions({ exclude, rootDir, }?: TestFileConventionsOptions): TestFileConventionsReport;
23
+ export interface TestFileConventionsOptions {
24
+ /**
25
+ * Directory basenames the sweep prunes at any depth, additive to the ones nmr always prunes. The array a repo
26
+ * passes here is the array it passes to `defineVitestConfig`'s `testCollectionExclude`.
27
+ */
28
+ exclude?: readonly string[];
29
+ /**
30
+ * The directory to sweep. Defaults to the monorepo root found from the working directory, so one call covers the
31
+ * whole repo from whichever package Vitest started in.
32
+ */
33
+ rootDir?: string;
34
+ }
35
+ /** What one sweep found, and the root it found it in. Paths are relative to that root and POSIX-separated. */
36
+ export interface TestFileConventionsReport {
37
+ misplaced: string[];
38
+ rootDir: string;
39
+ untiered: string[];
40
+ }
@@ -0,0 +1,25 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { findMisplacedTestFiles, findUntieredTestFiles, TIER_NAMES } from "./tiers.js";
3
+ import { findMonorepoRoot } from "./workspace.js";
4
+ const MISPLACED_REMEDY = 'Every test file must sit under a `__tests__` directory, the only place the shared Vitest config collects from. Move each file below into one, or name its directory in `exclude`.';
5
+ const UNTIERED_REMEDY = `Every collected test file must name its isolation tier in the segment before \`.test.\`, one of: ${TIER_NAMES.join(', ')}. Rename each file below to <subject>[.<aspect>].<tier>.test.ts.`;
6
+ export function checkTestFileConventions(options = {}) {
7
+ describe('test file conventions', () => {
8
+ const { misplaced, rootDir, untiered } = reportTestFileConventions(options);
9
+ it('every collected test file names its tier', () => {
10
+ expect(untiered, `${UNTIERED_REMEDY} Swept from ${rootDir}.`).toStrictEqual([]);
11
+ });
12
+ it('every test file sits under a __tests__ directory', () => {
13
+ expect(misplaced, `${MISPLACED_REMEDY} Swept from ${rootDir}.`).toStrictEqual([]);
14
+ });
15
+ });
16
+ }
17
+ export function reportTestFileConventions({ exclude, rootDir, } = {}) {
18
+ const sweptRoot = rootDir ?? findMonorepoRoot();
19
+ const scanOptions = { ...(exclude !== undefined && { exclude }) };
20
+ return {
21
+ misplaced: findMisplacedTestFiles(sweptRoot, scanOptions),
22
+ rootDir: sweptRoot,
23
+ untiered: findUntieredTestFiles(sweptRoot, scanOptions),
24
+ };
25
+ }
@@ -1,7 +1,62 @@
1
+ /** Collection patterns for the residual project, which claims every test file the named tiers leave. */
1
2
  export declare const ALL_TEST_PATTERNS: string[];
3
+ /** Builds the collection patterns for one tier. */
2
4
  export declare function buildTierPatterns(tier: string): string[];
3
- export declare function findTestFiles(rootDir: string): string[];
5
+ /**
6
+ * Walks `rootDir` for every `*.test.ts` or `*.test.tsx` file sitting outside a `__tests__` directory, returning
7
+ * paths relative to it, POSIX-separated and sorted.
8
+ *
9
+ * No project collects such a file, so it runs nowhere and reports nothing. Only a sweep of the tree tells it apart
10
+ * from a file that runs and passes.
11
+ */
12
+ export declare function findMisplacedTestFiles(rootDir: string, options?: TestFileScanOptions): string[];
13
+ /**
14
+ * Walks `rootDir` for every test file the shared config's projects collect, returning paths relative to it,
15
+ * POSIX-separated and sorted.
16
+ *
17
+ * A walk, because `node:fs` `globSync` does not descend into a dot-directory and honours no option that would make
18
+ * it. Vitest's globber does, so the same pattern string means different things to the two: a check built on the glob
19
+ * would report clean over every test file under a dot-directory.
20
+ */
21
+ export declare function findTestFiles(rootDir: string, options?: TestFileScanOptions): string[];
22
+ /**
23
+ * Walks `rootDir` for every collected test file whose name selects no tier, returning paths relative to it,
24
+ * POSIX-separated and sorted.
25
+ *
26
+ * `unit` is the residual project, so such a file runs under it and reports success: no test run tells it apart from
27
+ * a conformant file.
28
+ */
29
+ export declare function findUntieredTestFiles(rootDir: string, options?: TestFileScanOptions): string[];
30
+ /**
31
+ * Reports whether a test file's name selects a tier.
32
+ *
33
+ * Only the dot-delimited segment immediately before `.test.` selects one, so any earlier segment is free-form
34
+ * documentation: `scaffold.packaged.unit.test.ts` names the `unit` tier.
35
+ */
4
36
  export declare function hasTierInfix(filePath: string): boolean;
37
+ /**
38
+ * Directory names that hold no test worth collecting: dependencies, build output, and generated reports.
39
+ *
40
+ * The one point where the collection glob and the walk must agree about scope. Over-reporting is a failure a
41
+ * consumer cannot fix; under-reporting is the silence a conformance check exists to end.
42
+ */
5
43
  export declare const TEST_COLLECTION_EXCLUDE: string[];
44
+ /** Options every sweep over a repo's test files takes. */
45
+ export interface TestFileScanOptions {
46
+ /**
47
+ * Directory basenames pruned at any depth, additive to `TEST_COLLECTION_EXCLUDE`.
48
+ *
49
+ * Basenames rather than globs, so the array a repo passes here is the array it passes to the shared Vitest
50
+ * config's `testCollectionExclude`. A directory pruned from the sweep but still collected by Vitest is the
51
+ * silence these sweeps exist to end.
52
+ */
53
+ exclude?: readonly string[];
54
+ }
55
+ /**
56
+ * The isolation ladder, ordered by the furthest thing a test reaches. A tier names what a test reaches, never how it
57
+ * invokes it: a test driving a compiler through its JavaScript API is `tool`, exactly as one spawning `tsc` would be.
58
+ * Each named tier's name is also its filename infix, so `parse.tool.test.ts` lands in `tool`.
59
+ */
6
60
  export declare const TIER_NAMES: readonly ["unit", "tool", "localhost", "remote"];
61
+ /** One of the four isolation tiers. */
7
62
  export type TierName = (typeof TIER_NAMES)[number];
package/dist/esm/tiers.js CHANGED
@@ -8,10 +8,14 @@ export const ALL_TEST_PATTERNS = [`${TEST_GLOB_PREFIX}/*.test.${TEST_EXTENSIONS}
8
8
  export function buildTierPatterns(tier) {
9
9
  return [`${TEST_GLOB_PREFIX}/*.${tier}.test.${TEST_EXTENSIONS}`];
10
10
  }
11
- export function findTestFiles(rootDir) {
12
- const found = [];
13
- collectTestFiles(rootDir, '', false, found);
14
- return found.toSorted();
11
+ export function findMisplacedTestFiles(rootDir, options = {}) {
12
+ return walkTestFiles(rootDir, options, true);
13
+ }
14
+ export function findTestFiles(rootDir, options = {}) {
15
+ return walkTestFiles(rootDir, options, false);
16
+ }
17
+ export function findUntieredTestFiles(rootDir, options = {}) {
18
+ return findTestFiles(rootDir, options).filter((file) => !hasTierInfix(file));
15
19
  }
16
20
  export function hasTierInfix(filePath) {
17
21
  const tiers = TIER_NAMES;
@@ -19,17 +23,26 @@ export function hasTierInfix(filePath) {
19
23
  }
20
24
  export const TEST_COLLECTION_EXCLUDE = ['.git', 'coverage', 'dist', 'node_modules'];
21
25
  export const TIER_NAMES = ['unit', 'tool', 'localhost', 'remote'];
22
- function collectTestFiles(dir, relativeDir, inTestDir, found) {
26
+ function collectTestFiles(dir, relativeDir, inTestDir, context) {
23
27
  const entries = readdirSync(dir, { withFileTypes: true });
24
28
  for (const entry of entries) {
25
29
  const relativePath = relativeDir === '' ? entry.name : `${relativeDir}/${entry.name}`;
26
30
  if (entry.isDirectory()) {
27
- if (TEST_COLLECTION_EXCLUDE.includes(entry.name))
31
+ if (context.pruned.has(entry.name))
28
32
  continue;
29
- collectTestFiles(path.join(dir, entry.name), relativePath, inTestDir || entry.name === TEST_DIR, found);
33
+ collectTestFiles(path.join(dir, entry.name), relativePath, inTestDir || entry.name === TEST_DIR, context);
30
34
  }
31
- else if (inTestDir && TEST_FILE_PATTERN.test(entry.name)) {
32
- found.push(relativePath);
35
+ else if (inTestDir !== context.misplaced && TEST_FILE_PATTERN.test(entry.name)) {
36
+ context.found.push(relativePath);
33
37
  }
34
38
  }
35
39
  }
40
+ function walkTestFiles(rootDir, { exclude = [] }, misplaced) {
41
+ const context = {
42
+ found: [],
43
+ misplaced,
44
+ pruned: new Set([...TEST_COLLECTION_EXCLUDE, ...exclude]),
45
+ };
46
+ collectTestFiles(rootDir, '', false, context);
47
+ return context.found.toSorted();
48
+ }
@@ -2,16 +2,43 @@ import type { ScriptValue } from './default-scripts.js';
2
2
  import type { CommandVerbosity } from './verbosity.js';
3
3
  export type { ScriptValue, StepSpec } from './default-scripts.js';
4
4
  export type { CommandVerbosity } from './verbosity.js';
5
+ /** Build settings honored by `nmr-compile`. */
5
6
  export interface BuildConfig {
7
+ /**
8
+ * Patterns added to the build's default ignore set. Extends rather than replaces, so that declaring one pattern
9
+ * cannot silently drop the defaults and start shipping a package's own tests.
10
+ * The programmatic `buildPackage` option of the same name behaves identically; its bare `ignorePatterns` replaces.
11
+ */
6
12
  extraIgnorePatterns?: string[];
7
13
  }
14
+ /** Check-result cache settings honored at the monorepo root. */
8
15
  export interface CheckCacheConfig {
16
+ /** Set to `false` to turn the gate off entirely, so every command runs. */
9
17
  enabled?: boolean;
18
+ /**
19
+ * Command names removed from the cacheable set, applied after `extraCommands`. This is how a repo retires a
20
+ * name whose chain turned out to do more than report an exit status.
21
+ */
10
22
  excludeCommands?: string[];
23
+ /**
24
+ * Command names added to the cacheable set. Extends rather than replaces, so that declaring one command
25
+ * cannot silently drop the defaults. A name listed here promises exit-status-only semantics through its whole
26
+ * chain, hooks included.
27
+ */
11
28
  extraCommands?: string[];
12
29
  }
30
+ /** Output settings honored at the monorepo root. */
13
31
  export interface OutputConfig {
32
+ /**
33
+ * The verbosity a run takes when neither `-q` nor the environment named one. Both of those outrank it, and it
34
+ * outranks agent-harness detection, so `full` here is also how a repo declines to be detected.
35
+ */
14
36
  commandVerbosity?: CommandVerbosity;
37
+ /**
38
+ * Environment-variable names added to the set whose presence marks an agent harness. Extends rather than
39
+ * replaces, so that declaring one name cannot silently drop the shipped list, and a repo absorbs a harness's
40
+ * rename without waiting for an nmr release.
41
+ */
15
42
  extraAgentEnvVars?: string[];
16
43
  }
17
44
  export interface NmrConfig {
@@ -1,18 +1,55 @@
1
1
  import type { OutputConfig } from './types.js';
2
+ /**
3
+ * Environment variables a known agent harness sets, whose presence selects quiet when no level above them chose.
4
+ * The identifiers belong to ecosystems nmr does not control and that rename, so `output.extraAgentEnvVars` extends
5
+ * this list rather than a repo waiting for a release. `ROVO_CLI` anticipates a rename of `ROVODEV_CLI` that Rovo
6
+ * has signalled but not made; a variable nobody sets never fires.
7
+ */
2
8
  export declare const AGENT_ENV_VARS: readonly ["CLAUDECODE", "ROVODEV_CLI", "ROVO_CLI"];
9
+ /**
10
+ * The points on the loudness ladder. Both are spelled out rather than leaving `full` implicit in the variable's
11
+ * absence, so a repo-level default is overridable from the environment in either direction.
12
+ */
3
13
  export declare const COMMAND_VERBOSITIES: readonly ["full", "quiet"];
14
+ /**
15
+ * Carries the resolved verbosity down the spawned chain, so `-q` reaches every process rather than the first.
16
+ * Deliberately not a keyed variable: it changes what a run prints and never what a command concludes, so folding
17
+ * it into the cache key would stop a quiet run from hitting a pass a loud one recorded.
18
+ */
4
19
  export declare const COMMAND_VERBOSITY_ENV_VAR = "NMR_COMMAND_VERBOSITY";
20
+ /** How loudly nmr reports the output of the commands it runs, which is never its own verdicts. */
5
21
  export type CommandVerbosity = (typeof COMMAND_VERBOSITIES)[number];
22
+ /**
23
+ * Renders the rejection of a value off the loudness ladder, naming where it was written. Every source that accepts
24
+ * a verbosity renders through this, so no two of them can come to name different ladders.
25
+ */
6
26
  export declare function formatVerbosityRejection(source: string, value: string): string;
27
+ /** Narrows a raw value to a point on the loudness ladder. */
7
28
  export declare function isCommandVerbosity(value: string): value is CommandVerbosity;
29
+ /**
30
+ * Reads the verbosity the environment names. An unrecognized value resolves to nothing at all, since falling back
31
+ * would pick a mode nobody chose and hide a misspelling for the life of the shell.
32
+ *
33
+ * An unset or empty variable names no verbosity rather than resolving to `full`, which is what leaves the config
34
+ * and detection levels reachable: a floor written in here would outrank both of them.
35
+ */
8
36
  export declare function readVerbosityEnv(env: NodeJS.ProcessEnv): VerbosityRead;
9
37
  export interface ResolveVerbosityOptions {
38
+ /** Read for the detection level, and for that alone: the variable this module owns arrives as `envVerbosity`. */
10
39
  env: NodeJS.ProcessEnv;
11
40
  envVerbosity: CommandVerbosity | undefined;
12
41
  output: OutputConfig | undefined;
13
42
  quietFlag: boolean;
14
43
  }
44
+ /**
45
+ * Resolves the verbosity this process runs at: the flag, then the environment, then the repo's config, then
46
+ * detection of a known agent harness, then `full`.
47
+ *
48
+ * Total by construction. The environment was validated as it was read and the config as it was loaded, so no
49
+ * rejection is left for this to report and the whole ladder reads in one place.
50
+ */
15
51
  export declare function resolveVerbosity(options: ResolveVerbosityOptions): CommandVerbosity;
52
+ /** The verbosity the environment names, if any, or the message naming why its value could not be read. */
16
53
  export type VerbosityRead = {
17
54
  ok: true;
18
55
  verbosity?: CommandVerbosity;
@@ -1,11 +1,35 @@
1
1
  import type { Writable } from 'node:stream';
2
2
  import type { ReplayLine } from './check-cache.js';
3
3
  import type { ReportFormat } from './report-format.js';
4
+ /**
5
+ * The ceiling a rendered verdict, its newline included, is held to.
6
+ *
7
+ * POSIX guarantees a write at or below `PIPE_BUF` reaches a pipe whole, and 512 is the smallest bound the
8
+ * standard permits, so one write of a line this size cannot be interleaved by a concurrent scope under
9
+ * `pnpm --recursive`. No Node API reports the platform's own bound, and a per-platform table would move the
10
+ * truncation point from one machine to the next. The ceiling governs pipes: a terminal offers no such
11
+ * guarantee at any size.
12
+ */
4
13
  export declare const VERDICT_LINE_LIMIT = 512;
14
+ /**
15
+ * What a command nmr ran came to, holding the facts a reporting line is rendered from rather than the line
16
+ * itself, so a machine-readable rendering spends the same record a human-readable one does.
17
+ *
18
+ * A recalled pass carries its saving as a duration and not as a decision about whether to mention it: whether
19
+ * one is worth naming belongs to rendering, and a consumer bypassing the renderer inherits neither the
20
+ * threshold nor an obligation to restate it.
21
+ */
5
22
  export type Verdict = {
6
23
  command: string;
7
24
  scope: string;
8
25
  } & VerdictOutcome;
26
+ /**
27
+ * How a command ended, together with the facts that ending carries and no other does, and the trailing detail
28
+ * a line reserves room for.
29
+ *
30
+ * A recalled pass carries the excerpts it replays rather than a composed detail string, so the marker naming
31
+ * them a recording, and the ceiling they are held to, stay with the module that owns the line's grammar.
32
+ */
9
33
  export type VerdictOutcome = {
10
34
  detail?: string;
11
35
  } & ({
@@ -24,6 +48,27 @@ export type VerdictOutcome = {
24
48
  outcome: 'no-op';
25
49
  reason: 'empty-override' | 'noop-override';
26
50
  });
51
+ /**
52
+ * Renders a verdict as the line nmr reports it on, without the newline that terminates it.
53
+ *
54
+ * The line ends without terminal punctuation and reserves its tail for `detail`, so a later change appends to
55
+ * the grammar rather than rewriting it. A detail carrying nothing but line breaks takes its whole clause with
56
+ * it, rather than leaving a separator pointing at nothing.
57
+ */
27
58
  export declare function renderVerdict(verdict: Verdict): string;
59
+ /**
60
+ * Renders a verdict as the JSON object a machine consumer reads, without the newline that terminates it.
61
+ *
62
+ * Held to the ceiling the prose line is held to, so one write still reaches a pipe whole where concurrent
63
+ * scopes share a descriptor. Cuts land inside the record's text rather than across its structure, which is
64
+ * what leaves the line parseable, and a record is fitted by rungs, each shedding what a reader can better
65
+ * spare than the rung below it. What the ceiling costs is [documented](../README.md#reporting-for-a-machine)
66
+ * in the same order.
67
+ */
28
68
  export declare function serializeVerdict(verdict: Verdict): string;
69
+ /**
70
+ * Writes a verdict to a stream as a single write, which is what holds a line together when concurrent scopes
71
+ * share one descriptor. Both renderings spend the one record, so neither can come to report what the other
72
+ * does not.
73
+ */
29
74
  export declare function writeVerdict(verdict: Verdict, stream: Writable, format: ReportFormat): void;
@@ -1,21 +1,91 @@
1
- import type { ViteUserConfig } from 'vitest/config';
1
+ import { type ViteUserConfig } from 'vitest/config';
2
2
  import type { InlineConfig, ProjectConfig } from 'vitest/node';
3
- import type { TierName } from './tiers.js';
3
+ import { type TierName } from './tiers.js';
4
4
  export type { TierName } from './tiers.js';
5
+ /**
6
+ * Test options Vitest honours only at the root of a `projects` config. Derived from Vitest's own types rather than
7
+ * hand-listed, so an option that changes scope in a later release changes scope here too.
8
+ */
5
9
  export type RootTestOptions = Omit<InlineConfig, keyof ProjectConfig>;
10
+ /** Root-scoped overrides: Vite-level options plus the test options that survive at the root. */
6
11
  export type RootOverrides = Omit<ViteUserConfig, 'test'> & {
7
12
  test?: RootTestOptions;
8
13
  };
9
14
  export interface VitestConfigOptions {
15
+ /**
16
+ * Merged into the root config, which every project inherits because each declares `extends: true`.
17
+ *
18
+ * `resolve` is per-environment. Vitest resolves a test's own imports through the server environment, so a
19
+ * condition meant for them goes under `ssr`; a top-level `resolve.conditions` entry reaches the client
20
+ * environment, which only browser-mode tests resolve through.
21
+ */
10
22
  root?: RootOverrides;
23
+ /** Merged into every declared project. This is where per-project options such as `setupFiles` go. */
11
24
  project?: ProjectConfig;
25
+ /**
26
+ * Merged into one tier's project alone, after this layer's `project` block. Raising a budget for `tool` here
27
+ * leaves `unit` on the tight default that makes a hung unit test fail fast, which a uniform `project` override
28
+ * would flatten.
29
+ */
12
30
  tiers?: Partial<Record<TierName, ProjectConfig>>;
31
+ /**
32
+ * Directory basenames kept out of every project's collection, additive to the ones the shared config always
33
+ * prunes. Each name is matched at any depth, and every layer's entries are concatenated rather than the last
34
+ * winning, matching the config's rule for arrays.
35
+ *
36
+ * The same array declares the scope of nmr's exported test-file conventions check, so the sweep and the
37
+ * collection glob cannot drift. Excluding a directory from the sweep alone leaves a test file that runs and
38
+ * reports nothing; excluding it from collection alone leaves a report a consumer cannot act on. A repo wanting
39
+ * a glob rather than a directory name has the `project` seam's own `exclude`.
40
+ */
41
+ testCollectionExclude?: readonly string[];
42
+ /**
43
+ * Loads nmr's git-isolation setup file into every project, ahead of any the layers supply. Defaults to `true`.
44
+ * Turn it off only where a suite is meant to read the developer's own git configuration.
45
+ */
13
46
  isolateGit?: boolean;
47
+ /**
48
+ * Resolves workspace packages through their `source` export condition, so a suite runs without a prior build.
49
+ * Defaults to `true`. A package declaring no such condition is unaffected.
50
+ */
14
51
  resolveFromSource?: boolean;
52
+ /**
53
+ * Resolves a specifier through the `paths` a `tsconfig.json` declares, so a test reaches an alias the way `tsc`
54
+ * does. Defaults to `false`, matching Vite, and requires Vite 8.
55
+ *
56
+ * Not a default, because both directions are safe to leave to the consumer: omitting it fails loudly with an
57
+ * unresolved import, while turning it on for a repo that declares `paths` for `tsc` alone changes which module a
58
+ * specifier reaches with nothing in the run reporting it. A repo declaring no `paths` is unaffected either way.
59
+ */
60
+ tsconfigPaths?: boolean;
15
61
  }
16
62
  export interface RootVitestConfigOptions extends VitestConfigOptions {
63
+ /**
64
+ * An absolute path to the monorepo root, which must hold `pnpm-workspace.yaml`. A root config sits at that
65
+ * directory by construction, so this is `import.meta.dirname`. Stated rather than searched for: resolving it
66
+ * from the working directory would make the config describe whichever monorepo the run happened to start in.
67
+ */
17
68
  monorepoRoot: string;
18
69
  }
70
+ /** Any number of shared layers, then the config file's own, which states the monorepo root. */
19
71
  type RootConfigLayers = [...(VitestConfigOptions | undefined)[], RootVitestConfigOptions];
72
+ /**
73
+ * Builds the shared Vitest config for a workspace package, declaring the `unit`, `tool`, `localhost`, and `remote`
74
+ * projects. Select them at run time with `--project`, which unions when repeated and accepts negation.
75
+ *
76
+ * Layers fold left to right, later winning and arrays composing, so a config file shares settings by passing a
77
+ * layer ahead of its own. Merging two of this function's *outputs* is not the way: both declare the same four
78
+ * project names, which Vitest rejects at startup.
79
+ *
80
+ * An `undefined` layer is skipped, so `defineVitestConfig(shared, isCI ? ciLayer : undefined)` composes.
81
+ */
20
82
  export declare function defineVitestConfig(...layers: (VitestConfigOptions | undefined)[]): ViteUserConfig;
83
+ /**
84
+ * Builds the shared Vitest config for repo-root tests. Excludes every workspace package from all projects, and
85
+ * reports no coverage of its own — packages cover their own sources.
86
+ *
87
+ * `monorepoRoot` rides on the last layer, which is the config file's own: a shared layer describes settings, not
88
+ * which repo they belong to, and only the root config's `import.meta.dirname` states this one. The guard below
89
+ * still catches the JavaScript config that types never reach.
90
+ */
21
91
  export declare function defineRootVitestConfig(...layers: RootConfigLayers): ViteUserConfig;
@@ -11,7 +11,6 @@ const GIT_ISOLATION_SETUP_FILE = resolveGitIsolationSetupFile();
11
11
  const SOURCE_CLIENT_CONDITIONS = ['source', 'module', 'browser', 'development|production'];
12
12
  const SOURCE_SERVER_CONDITIONS = ['source', 'module', 'node', 'development|production'];
13
13
  const COVERAGE_EXCLUDE = ['**/__{fixtures,mocks,tests}__/**', '**/index.ts', '**/*.d.ts'];
14
- const COLLECTION_EXCLUDE = [...new Set([...defaultExclude, ...TEST_COLLECTION_EXCLUDE.map((dir) => `**/${dir}/**`)])];
15
14
  const PACKAGE_COVERAGE_INCLUDE = ['**/src/**/*.{ts,tsx}'];
16
15
  const MISSING_MONOREPO_ROOT = 'defineRootVitestConfig requires `monorepoRoot`, an absolute path to the directory holding pnpm-workspace.yaml. Pass `import.meta.dirname` in the last options layer, from the root config.';
17
16
  export function defineVitestConfig(...layers) {
@@ -32,11 +31,14 @@ export function defineRootVitestConfig(...layers) {
32
31
  function buildConfig(declaredLayers, { coverageInclude, projectExclude = [], projectRoot }) {
33
32
  const layers = declaredLayers.filter((layer) => layer !== undefined);
34
33
  assertKnownTiers(layers);
34
+ const resolveFromSource = resolveFlag(layers, 'resolveFromSource', true);
35
+ const resolve = {
36
+ ...(resolveFromSource && { conditions: SOURCE_CLIENT_CONDITIONS }),
37
+ ...(resolveFlag(layers, 'tsconfigPaths', false) && { tsconfigPaths: true }),
38
+ };
35
39
  const config = {
36
- ...(resolveFlag(layers, 'resolveFromSource') && {
37
- resolve: { conditions: SOURCE_CLIENT_CONDITIONS },
38
- ssr: { resolve: { conditions: SOURCE_SERVER_CONDITIONS } },
39
- }),
40
+ ...(Object.keys(resolve).length > 0 && { resolve }),
41
+ ...(resolveFromSource && { ssr: { resolve: { conditions: SOURCE_SERVER_CONDITIONS } } }),
40
42
  test: {
41
43
  coverage: {
42
44
  enabled: false,
@@ -67,13 +69,14 @@ function buildProjects(layers, extraExclude, projectRoot) {
67
69
  timeout: TIER_TIMEOUT,
68
70
  })),
69
71
  ];
70
- const isolateGit = resolveFlag(layers, 'isolateGit');
72
+ const collectionExclude = buildCollectionExclude(layers);
73
+ const isolateGit = resolveFlag(layers, 'isolateGit', true);
71
74
  return projectTiers.map(({ exclude, include, name, timeout }) => {
72
75
  const project = {
73
76
  extends: true,
74
77
  ...(projectRoot !== undefined && { root: projectRoot }),
75
78
  test: {
76
- exclude: [...COLLECTION_EXCLUDE, ...exclude, ...extraExclude],
79
+ exclude: [...collectionExclude, ...exclude, ...extraExclude],
77
80
  include,
78
81
  name,
79
82
  ...(isolateGit && { setupFiles: [GIT_ISOLATION_SETUP_FILE] }),
@@ -103,13 +106,18 @@ function assertKnownTiers(layers) {
103
106
  }
104
107
  }
105
108
  }
109
+ function buildCollectionExclude(layers) {
110
+ const declared = layers.flatMap((layer) => layer.testCollectionExclude ?? []);
111
+ const globs = [...TEST_COLLECTION_EXCLUDE, ...declared].map((dir) => `**/${dir}/**`);
112
+ return [...new Set([...defaultExclude, ...globs])];
113
+ }
106
114
  function getWorkspaceExcludePatterns(monorepoRoot) {
107
115
  return getWorkspacePackageDirs(monorepoRoot)
108
116
  .map((dir) => `${path.relative(monorepoRoot, dir).split(path.sep).join('/')}/**`)
109
117
  .toSorted();
110
118
  }
111
- function resolveFlag(layers, name) {
112
- let resolved = true;
119
+ function resolveFlag(layers, name, fallback) {
120
+ let resolved = fallback;
113
121
  for (const layer of layers) {
114
122
  const declared = layer[name];
115
123
  if (declared !== undefined)
@@ -1,4 +1,22 @@
1
+ /** Reports whether a directory is the monorepo root, which the workspace manifest's presence marks. */
1
2
  export declare function isMonorepoRoot(dir: string): boolean;
3
+ /**
4
+ * Finds the monorepo root by walking up from `startDir` to find `pnpm-workspace.yaml`.
5
+ * Throws if no workspace root is found.
6
+ */
2
7
  export declare function findMonorepoRoot(startDir?: string): string;
8
+ /**
9
+ * Reads the workspace patterns from `pnpm-workspace.yaml` and resolves them to absolute package
10
+ * directories, applying pnpm's pattern semantics — including `!`-prefixed exclusions.
11
+ *
12
+ * Returns an empty array when the manifest declares no usable `packages` list, and throws when
13
+ * `monorepoRoot` holds no manifest at all — the caller named a directory that is not a monorepo root.
14
+ */
3
15
  export declare function getWorkspacePackageDirs(monorepoRoot: string): string[];
16
+ /**
17
+ * Reads the `overrides` block from the monorepo root's `pnpm-workspace.yaml`, the site pnpm reads an override
18
+ * from.
19
+ *
20
+ * Returns nothing when the manifest or the block is missing, and drops an entry whose value is not a string.
21
+ */
4
22
  export declare function readWorkspaceOverrides(monorepoRoot: string): Record<string, string> | undefined;