dorfl 0.2.1 → 0.3.1

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.
@@ -2,6 +2,11 @@ import {existsSync} from 'node:fs';
2
2
  import {join} from 'node:path';
3
3
  import {run} from './git.js';
4
4
  import {resolvePrepareCommands, type PrepareConfig} from './prepare.js';
5
+ import {
6
+ resolveVerifyCommands,
7
+ VerifyNotConfiguredError,
8
+ type VerifyConfig,
9
+ } from './verify.js';
5
10
 
6
11
  /**
7
12
  * Pre-claim / pre-build startup GUARD for the fresh-worktree acceptance gate
@@ -21,12 +26,15 @@ import {resolvePrepareCommands, type PrepareConfig} from './prepare.js';
21
26
  * the guard KEYS off LOCKFILE-PRESENT evidence — never off `prepare`-unset
22
27
  * alone — and a genuinely dep-free repo (no lockfile) PROCEEDS unchanged.
23
28
  *
24
- * There is NO "verify unset" case: {@link resolveVerifyCommands} substitutes
25
- * `DEFAULT_VERIFY_COMMAND` when `verify` is unset OR all-blank, so `verify` is
26
- * NEVER statically unrunnable-because-unset. A "verify unset STOP" guard
27
- * would be dead code (or fire wrongly on the default). The guard is deps-only.
29
+ * VERIFY-UNSET is ALSO a static stop, and — unlike the deps case it is
30
+ * MODE-INDEPENDENT: Dorfl has no default gate, so a repo with no `verify`
31
+ * declared can never pass an acceptance gate in ANY mode (fresh-worktree or
32
+ * in-place). {@link resolveVerifyCommands} throws {@link VerifyNotConfiguredError}
33
+ * when `verify` is unset / empty / all-blank; the guard detects that at second
34
+ * zero and STOPS before a wasted claim + build. This check runs regardless of
35
+ * `freshWorktreeGate`.
28
36
  *
29
- * The guard is also gated on `freshWorktreeGate === true` for THIS invocation:
37
+ * The DEPS guard below is also gated on `freshWorktreeGate === true` for THIS invocation:
30
38
  * when the gate is OFF (`--no-fresh-worktree-gate`) the acceptance gate runs in
31
39
  * the agent's BUILD worktree (which HAS deps), so the throwaway-worktree
32
40
  * reasoning does not apply.
@@ -92,6 +100,12 @@ export interface GatePreconditionInput {
92
100
  freshWorktreeGate: boolean | undefined;
93
101
  /** The resolved `prepare` config (unset / string / list). */
94
102
  prepare?: PrepareConfig;
103
+ /**
104
+ * The resolved `verify` config (unset / string / list). When this is unset
105
+ * (or all-blank) there is NO gate to run — a MODE-INDEPENDENT static stop,
106
+ * checked before the deps guard.
107
+ */
108
+ verify?: VerifyConfig;
95
109
  /**
96
110
  * The lockfile basename probed for this repo (one of
97
111
  * {@link LOCKFILE_BASENAMES}), or `undefined` for the intentional dep-free
@@ -103,9 +117,13 @@ export interface GatePreconditionInput {
103
117
  }
104
118
 
105
119
  export interface GatePreconditionFailure {
106
- /** The lockfile basename (from {@link LOCKFILE_BASENAMES}) that tripped the guard. */
107
- lockfile: string;
108
- /** A precise, actionable error message naming the lockfile and the two ways out. */
120
+ /**
121
+ * The lockfile basename (from {@link LOCKFILE_BASENAMES}) that tripped the
122
+ * DEPS guard, or `undefined` for the verify-unset failure (which is not tied
123
+ * to any lockfile).
124
+ */
125
+ lockfile?: string;
126
+ /** A precise, actionable error message naming the cause and the way(s) out. */
109
127
  message: string;
110
128
  }
111
129
 
@@ -119,6 +137,17 @@ export interface GatePreconditionFailure {
119
137
  export function checkGatePreconditions(
120
138
  input: GatePreconditionInput,
121
139
  ): GatePreconditionFailure | undefined {
140
+ // VERIFY-UNSET — MODE-INDEPENDENT (checked FIRST, before the fresh-worktree
141
+ // short-circuit): Dorfl has no default gate, so an unconfigured `verify` can
142
+ // never pass in ANY mode. `resolveVerifyCommands` throws when unset/all-blank.
143
+ try {
144
+ resolveVerifyCommands(input.verify);
145
+ } catch (err) {
146
+ if (err instanceof VerifyNotConfiguredError) {
147
+ return {message: err.message};
148
+ }
149
+ throw err;
150
+ }
122
151
  // Gate OFF for THIS invocation ⇒ no throwaway worktree ⇒ the deps reasoning
123
152
  // does not apply. (The default is ON; the guard fires when ON.)
124
153
  if (input.freshWorktreeGate !== true) {
package/src/index.ts CHANGED
@@ -572,7 +572,8 @@ export type {RunVerifyOptions, RunVerifyResult} from './verify.js';
572
572
  export {
573
573
  runVerify,
574
574
  resolveVerifyCommands,
575
- DEFAULT_VERIFY_COMMAND,
575
+ VerifyNotConfiguredError,
576
+ VERIFY_NOT_CONFIGURED_MESSAGE,
576
577
  } from './verify.js';
577
578
 
578
579
  export type {
package/src/prepare.ts CHANGED
@@ -16,10 +16,12 @@ import type {VerifyConfig} from './verify.js';
16
16
  * cheaply-re-runnable acceptance check and make every gate run pay the install
17
17
  * cost). `prepare` is where install belongs; `verify` stays the gate.
18
18
  *
19
- * Unlike `verify`, `prepare` has NO default command: an unset `prepare` is a
20
- * genuine NO-OP (a repo with no deps needs no install — we never invent a
21
- * default that would run `pnpm install` in a repo that has no lockfile). This is
22
- * the one shape difference from `verify` (whose unset a sensible default gate).
19
+ * Like `verify`, `prepare` has NO default command but the two differ in what
20
+ * "unset" MEANS. An unset `prepare` is a genuine NO-OP (a repo with no deps
21
+ * needs no install we never invent a default that would run `pnpm install` in
22
+ * a repo that has no lockfile). An unset `verify`, by contrast, is a hard error
23
+ * (a repo MUST declare its acceptance gate; there is no gate to invent). So both
24
+ * are default-free, but unset-prepare passes vacuously while unset-verify FAILS.
23
25
  *
24
26
  * Like `verify` it is a DECLARED, deterministic shell step (a single command or
25
27
  * an ordered list, all must pass) — no model in the loop. It is read-only with
@@ -40,6 +42,9 @@ export type PrepareConfig = VerifyConfig;
40
42
  * difference from {@link resolveVerifyCommands}). A string ⇒ a single command. A
41
43
  * list ⇒ the list, with blank/whitespace-only entries dropped (which can leave
42
44
  * an empty list ⇒ still a no-op). Each command is run in sequence; all must pass.
45
+ *
46
+ * Contrast {@link resolveVerifyCommands}, which THROWS on the unset/all-blank
47
+ * case (there is no default gate) rather than resolving to a no-op.
43
48
  */
44
49
  export function resolvePrepareCommands(
45
50
  prepare: PrepareConfig | undefined,
package/src/run.ts CHANGED
@@ -630,8 +630,9 @@ async function runOneItem(
630
630
  // same `config-error` axis the PR-intent guard above uses (a wiring/config
631
631
  // fault, not a task fault). The repo discovery is either a working checkout
632
632
  // (in-place tests) OR a BARE hub mirror (production fleet); detect the
633
- // lockfile from whichever shape `repoPath` is. Deps-only there is no
634
- // verify-unset case (the gate substitutes `DEFAULT_VERIFY_COMMAND`).
633
+ // lockfile from whichever shape `repoPath` is. ALSO fails fast when
634
+ // `verify` is unset/all-blank (a MODE-INDEPENDENT stop — Dorfl has no
635
+ // default gate, so an unconfigured `verify` can never pass in any mode).
635
636
  {
636
637
  const lockfile =
637
638
  detectLockfileOnDisk(repoPath) ??
@@ -639,6 +640,7 @@ async function runOneItem(
639
640
  const guard = checkGatePreconditions({
640
641
  freshWorktreeGate: config.freshWorktreeGate,
641
642
  prepare: config.prepare,
643
+ verify: config.verify,
642
644
  lockfile,
643
645
  });
644
646
  if (guard !== undefined) {
package/src/verify.ts CHANGED
@@ -12,43 +12,77 @@ import {spawn} from 'node:child_process';
12
12
  *
13
13
  * It is read-only with respect to `work/`: it runs the declared check and never
14
14
  * moves or commits anything.
15
+ *
16
+ * There is NO default gate. A repo MUST declare its own `verify`; an unset gate
17
+ * is a loud failure (`notConfigured`), never a silent `pnpm -r …` fallback that
18
+ * could run the wrong check or pass vacuously.
15
19
  */
16
20
 
17
21
  /**
18
22
  * The per-repo gate, as declared in config. A single shell command, or an
19
23
  * ordered list of commands run in sequence (each must pass for the gate to
20
- * pass). `undefined` (unset) {@link DEFAULT_VERIFY_COMMAND} is used.
24
+ * pass). `undefined` (unset) is NOT valid: there is NO default gate — a repo
25
+ * MUST declare its own `verify` (see {@link VerifyNotConfiguredError}).
21
26
  */
22
27
  export type VerifyConfig = string | string[];
23
28
 
24
29
  /**
25
- * The sensible default when no `verify` is configured: build, then test, then
26
- * check formatting across the workspace. Deterministic, auditable, no model.
30
+ * The precise, actionable message emitted whenever a `verify` gate is required
31
+ * but the repo declares none (unset, empty string, or an all-blank list). There
32
+ * is deliberately NO default gate: a silent `pnpm -r build && …` fallback runs
33
+ * the WRONG check in a repo whose real gate is something else (and passes
34
+ * vacuously in a repo pnpm knows nothing about — e.g. `pnpm -r` printing
35
+ * "No projects found" and exiting 0, a FALSE green). Failing loud here forces
36
+ * the repo to declare the gate it actually means.
37
+ */
38
+ export const VERIFY_NOT_CONFIGURED_MESSAGE =
39
+ 'no `verify` gate is configured for this repo. Dorfl has no default ' +
40
+ 'acceptance gate: declare the exact command(s) in `dorfl.json` ' +
41
+ '(e.g. "verify": "pnpm -r build && pnpm -r test && pnpm format:check"), ' +
42
+ 'as a single string or an ordered list of commands.';
43
+
44
+ /**
45
+ * Thrown by {@link resolveVerifyCommands} when a gate is required but none is
46
+ * declared. A typed error (not a bare `Error`) so callers that must translate
47
+ * an unconfigured gate into a routed gate-FAILURE (rather than an uncaught
48
+ * crash) can detect it precisely — see {@link runVerify}, which catches it and
49
+ * returns a failing {@link RunVerifyResult} with `notConfigured: true`.
27
50
  */
28
- export const DEFAULT_VERIFY_COMMAND =
29
- 'pnpm -r build && pnpm -r test && pnpm -r format:check';
51
+ export class VerifyNotConfiguredError extends Error {
52
+ constructor(message: string = VERIFY_NOT_CONFIGURED_MESSAGE) {
53
+ super(message);
54
+ this.name = 'VerifyNotConfiguredError';
55
+ }
56
+ }
30
57
 
31
58
  /**
32
59
  * Resolve the declared gate into an ordered, non-empty list of shell commands.
33
- * Unset ⇒ the default. A string ⇒ a single command. A list ⇒ the list, with
34
- * blank/whitespace-only entries dropped (and the default substituted if that
35
- * leaves nothing). Each command is run in sequence; all must pass.
60
+ * A string ⇒ a single command. A list ⇒ the list, with blank/whitespace-only
61
+ * entries dropped. Each command is run in sequence; all must pass.
62
+ *
63
+ * There is NO default: unset, an empty string, or a list that is empty/all-blank
64
+ * THROWS {@link VerifyNotConfiguredError}. Dorfl never invents an acceptance
65
+ * gate — a repo must declare the exact check it means, so the gate can never
66
+ * silently run the wrong command (or pass vacuously).
36
67
  */
37
68
  export function resolveVerifyCommands(
38
69
  verify: VerifyConfig | undefined,
39
70
  ): string[] {
40
71
  if (verify === undefined) {
41
- return [DEFAULT_VERIFY_COMMAND];
72
+ throw new VerifyNotConfiguredError();
42
73
  }
43
74
  const list = Array.isArray(verify) ? verify : [verify];
44
75
  const commands = list.filter((command) => command.trim() !== '');
45
- return commands.length > 0 ? commands : [DEFAULT_VERIFY_COMMAND];
76
+ if (commands.length === 0) {
77
+ throw new VerifyNotConfiguredError();
78
+ }
79
+ return commands;
46
80
  }
47
81
 
48
82
  export interface RunVerifyOptions {
49
83
  /** The repo to run the gate in (its working directory). */
50
84
  cwd: string;
51
- /** The declared gate (string | list). Unset ⇒ the default command. */
85
+ /** The declared gate (string | list). Unset/blanka failing, not-configured result. */
52
86
  verify?: VerifyConfig;
53
87
  /** Environment for the gate's child processes. */
54
88
  env?: NodeJS.ProcessEnv;
@@ -61,10 +95,17 @@ export interface RunVerifyOptions {
61
95
  export interface RunVerifyResult {
62
96
  /** The exit code of the gate: 0 iff every command passed. */
63
97
  exitCode: number;
64
- /** The ordered commands that were run (resolved from config). */
98
+ /** The ordered commands that were run (resolved from config). Empty when not configured. */
65
99
  commands: string[];
66
100
  /** Whether the gate passed (exitCode === 0). */
67
101
  passed: boolean;
102
+ /**
103
+ * True iff the gate could not run because NO `verify` is declared (unset /
104
+ * empty / all-blank). A distinct, always-failing outcome (`passed: false`)
105
+ * so runner callers route it as a normal gate FAILURE with a clear reason
106
+ * rather than crashing on the thrown {@link VerifyNotConfiguredError}.
107
+ */
108
+ notConfigured?: boolean;
68
109
  }
69
110
 
70
111
  /**
@@ -73,11 +114,28 @@ export interface RunVerifyResult {
73
114
  * the first non-zero exit short-circuits the rest and becomes the result
74
115
  * (mirroring `&&` semantics). Each command is run through `bash -c` so that
75
116
  * shell operators in the declared gate (`&&`, pipes, etc.) behave as written.
117
+ *
118
+ * A repo with NO `verify` declared yields a failing result with
119
+ * `notConfigured: true` (never throws): the shared runner call sites
120
+ * (`do`/`run`/`complete` → `performIntegration`) already route a non-passing
121
+ * gate to needs-attention, so an unconfigured gate surfaces the same way with a
122
+ * precise reason instead of an uncaught crash.
76
123
  */
77
124
  export async function runVerify(
78
125
  options: RunVerifyOptions,
79
126
  ): Promise<RunVerifyResult> {
80
- const commands = resolveVerifyCommands(options.verify);
127
+ let commands: string[];
128
+ try {
129
+ commands = resolveVerifyCommands(options.verify);
130
+ } catch (err) {
131
+ if (err instanceof VerifyNotConfiguredError) {
132
+ const onStderr =
133
+ options.onStderr ?? ((chunk: string) => process.stderr.write(chunk));
134
+ onStderr(`${err.message}\n`);
135
+ return {exitCode: 1, commands: [], passed: false, notConfigured: true};
136
+ }
137
+ throw err;
138
+ }
81
139
  const onStdout =
82
140
  options.onStdout ?? ((chunk: string) => process.stdout.write(chunk));
83
141
  const onStderr =