@praxisflux/gates 0.29.0 → 0.31.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.
package/README.md CHANGED
@@ -73,7 +73,8 @@ codes, same failure lines.
73
73
 
74
74
  ## The contract
75
75
 
76
- Gate names, inputs, and exit codes (0 all pass · 1 any gate failed · 2 usage error) are
76
+ Gate names, inputs, and exit codes (0 all pass · 1 any gate failed a gate that crashes
77
+ while running counts as failed · 2 usage error) are
77
78
  praxisflux's versioned consumer interface, released and semver-bumped like everything else
78
79
  (`docs/releasing.md`); each failure line names its fix. You can also invoke the runner
79
80
  directly from any praxisflux checkout:
@@ -10,6 +10,8 @@
10
10
  // A gate that resolves no roots is a no-op (this isn't its kind of project). `check` problems
11
11
  // block the stop (exit 2); optional `warn` notices are surfaced on stderr but never block (exit 0)
12
12
  // — for freshness reminders and the like that shouldn't refuse to let the model finish.
13
+ // A gate that CRASHES — in resolveRoots or in check — surfaces as a blocking problem naming
14
+ // the gate: a crash swallowed silently would be permanent non-enforcement wearing a green exit.
13
15
  // `ctx` is { sessionId, input }: the invoking session's identity (hook input `session_id`,
14
16
  // falling back to $CLAUDE_CODE_SESSION_ID) plus the raw hook input — gates that scope state
15
17
  // to its owning session (e.g. reorient run records) key off it; every existing gate ignores it.
@@ -43,7 +45,9 @@ export function evaluate(input, gates, { cwd = process.cwd() } = {}) {
43
45
  const warnings = [];
44
46
  for (const gate of gates) {
45
47
  let roots = [];
46
- try { roots = gate.resolveRoots(start, ctx) || []; } catch { roots = []; }
48
+ try { roots = gate.resolveRoots(start, ctx) || []; } catch (e) {
49
+ problems.push(`[${gate.name || "gate"}] resolveRoots crashed on ${start}: ${e.message}`);
50
+ }
47
51
  for (const root of roots) {
48
52
  try { problems.push(...(gate.check(root, ctx) || [])); } catch (e) {
49
53
  problems.push(`[${gate.name || "gate"}] crashed on ${root}: ${e.message}`);
@@ -10,6 +10,8 @@
10
10
  // A gate that resolves no roots is a no-op (this isn't its kind of project). `check` problems
11
11
  // block the stop (exit 2); optional `warn` notices are surfaced on stderr but never block (exit 0)
12
12
  // — for freshness reminders and the like that shouldn't refuse to let the model finish.
13
+ // A gate that CRASHES — in resolveRoots or in check — surfaces as a blocking problem naming
14
+ // the gate: a crash swallowed silently would be permanent non-enforcement wearing a green exit.
13
15
  // `ctx` is { sessionId, input }: the invoking session's identity (hook input `session_id`,
14
16
  // falling back to $CLAUDE_CODE_SESSION_ID) plus the raw hook input — gates that scope state
15
17
  // to its owning session (e.g. reorient run records) key off it; every existing gate ignores it.
@@ -43,7 +45,9 @@ export function evaluate(input, gates, { cwd = process.cwd() } = {}) {
43
45
  const warnings = [];
44
46
  for (const gate of gates) {
45
47
  let roots = [];
46
- try { roots = gate.resolveRoots(start, ctx) || []; } catch { roots = []; }
48
+ try { roots = gate.resolveRoots(start, ctx) || []; } catch (e) {
49
+ problems.push(`[${gate.name || "gate"}] resolveRoots crashed on ${start}: ${e.message}`);
50
+ }
47
51
  for (const root of roots) {
48
52
  try { problems.push(...(gate.check(root, ctx) || [])); } catch (e) {
49
53
  problems.push(`[${gate.name || "gate"}] crashed on ${root}: ${e.message}`);
@@ -10,6 +10,8 @@
10
10
  // A gate that resolves no roots is a no-op (this isn't its kind of project). `check` problems
11
11
  // block the stop (exit 2); optional `warn` notices are surfaced on stderr but never block (exit 0)
12
12
  // — for freshness reminders and the like that shouldn't refuse to let the model finish.
13
+ // A gate that CRASHES — in resolveRoots or in check — surfaces as a blocking problem naming
14
+ // the gate: a crash swallowed silently would be permanent non-enforcement wearing a green exit.
13
15
  // `ctx` is { sessionId, input }: the invoking session's identity (hook input `session_id`,
14
16
  // falling back to $CLAUDE_CODE_SESSION_ID) plus the raw hook input — gates that scope state
15
17
  // to its owning session (e.g. reorient run records) key off it; every existing gate ignores it.
@@ -43,7 +45,9 @@ export function evaluate(input, gates, { cwd = process.cwd() } = {}) {
43
45
  const warnings = [];
44
46
  for (const gate of gates) {
45
47
  let roots = [];
46
- try { roots = gate.resolveRoots(start, ctx) || []; } catch { roots = []; }
48
+ try { roots = gate.resolveRoots(start, ctx) || []; } catch (e) {
49
+ problems.push(`[${gate.name || "gate"}] resolveRoots crashed on ${start}: ${e.message}`);
50
+ }
47
51
  for (const root of roots) {
48
52
  try { problems.push(...(gate.check(root, ctx) || [])); } catch (e) {
49
53
  problems.push(`[${gate.name || "gate"}] crashed on ${root}: ${e.message}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@praxisflux/gates",
3
- "version": "0.29.0",
3
+ "version": "0.31.0",
4
4
  "description": "praxisflux gate checks as a zero-dependency CLI (spec-bridge, wiki-freshness, course) — status can't exceed proven artifacts",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -7,9 +7,10 @@
7
7
  // [--wiki-dir docs/wiki] [--course-dir docs/course]
8
8
  //
9
9
  // Gate names, options, and exit codes are praxisflux's versioned consumer contract
10
- // (docs/consuming-gates.md): exit 0 when every gate passes, 1 when any gate fails, 2 on a
11
- // usage error (unknown gate, missing --gates). Each failure line names its fix. This same
12
- // file ships as the @praxisflux/gates npm bin (scripts/build-npm.mjs carves the package).
10
+ // (docs/consuming-gates.md): exit 0 when every gate passes, 1 when any gate fails including
11
+ // a gate that crashes while running — and 2 on a usage error (unknown gate, missing --gates).
12
+ // Each failure line names its fix. This same file ships as the @praxisflux/gates npm bin
13
+ // (scripts/build-npm.mjs carves the package).
13
14
  import { join, resolve } from "node:path";
14
15
  import { spawnSync } from "node:child_process";
15
16
  import { runAsCli } from "../lib/cli.mjs";
@@ -46,14 +47,29 @@ export const GATES = {
46
47
  },
47
48
  };
48
49
 
49
- /** Run the named gates against opts.root. Returns [{gate, problems, warnings, ok}].
50
- * Throws on an unknown or empty gate list a misspelled gate must fail the build loudly,
51
- * never skip silently. */
52
- export function runGates(names, opts) {
50
+ /** Validate a requested gate list. Throws on an unknown or empty list — a misspelled gate
51
+ * must fail the build loudly, never skip silently. These throws are the ONLY usage errors
52
+ * (exit 2); anything that goes wrong after validation is a gate result, not usage. */
53
+ export function validateGateNames(names) {
53
54
  if (!names.length) throw new Error(`no gates requested — pass --gates with any of: ${Object.keys(GATES).join(", ")}`);
54
55
  for (const n of names)
55
56
  if (!GATES[n]) throw new Error(`unknown gate "${n}" — valid gates: ${Object.keys(GATES).join(", ")}`);
56
- return names.map((gate) => ({ gate, ...GATES[gate](opts) }));
57
+ }
58
+
59
+ /** Run the named gates against opts.root. Returns [{gate, problems, warnings, ok}].
60
+ * Throws on an unknown or empty gate list (validateGateNames). An exception thrown WHILE a
61
+ * gate runs is a gate failure, never a usage error: it becomes a problem on that gate's
62
+ * result naming the gate and the error, so the CLI exits 1 — the exit code CI consumers
63
+ * branch on for "a gate did not pass" (docs/consuming-gates.md). */
64
+ export function runGates(names, opts) {
65
+ validateGateNames(names);
66
+ return names.map((gate) => {
67
+ try {
68
+ return { gate, ...GATES[gate](opts) };
69
+ } catch (e) {
70
+ return { gate, problems: [`gate "${gate}" crashed while running: ${e.message}`], warnings: [], ok: "" };
71
+ }
72
+ });
57
73
  }
58
74
 
59
75
  if (runAsCli(import.meta.url)) {
@@ -69,13 +85,16 @@ if (runAsCli(import.meta.url)) {
69
85
  };
70
86
  const names = opt("gates", "").split(",").map((s) => s.trim()).filter(Boolean);
71
87
 
72
- let results;
88
+ // Usage validation alone lives inside the exit-2 try/catch; gate execution happens
89
+ // outside it, so an exception thrown while a gate runs can never masquerade as usage —
90
+ // runGates converts it into that gate's failure result and the run exits 1.
73
91
  try {
74
- results = runGates(names, opts);
92
+ validateGateNames(names);
75
93
  } catch (e) {
76
94
  console.error(`usage error: ${e.message}`);
77
95
  process.exit(2);
78
96
  }
97
+ const results = runGates(names, opts);
79
98
  let failed = 0;
80
99
  for (const { gate, problems, warnings, ok } of results) {
81
100
  for (const w of warnings) console.log(`[${gate}] warn: ${w}`);
@@ -10,6 +10,8 @@
10
10
  // A gate that resolves no roots is a no-op (this isn't its kind of project). `check` problems
11
11
  // block the stop (exit 2); optional `warn` notices are surfaced on stderr but never block (exit 0)
12
12
  // — for freshness reminders and the like that shouldn't refuse to let the model finish.
13
+ // A gate that CRASHES — in resolveRoots or in check — surfaces as a blocking problem naming
14
+ // the gate: a crash swallowed silently would be permanent non-enforcement wearing a green exit.
13
15
  // `ctx` is { sessionId, input }: the invoking session's identity (hook input `session_id`,
14
16
  // falling back to $CLAUDE_CODE_SESSION_ID) plus the raw hook input — gates that scope state
15
17
  // to its owning session (e.g. reorient run records) key off it; every existing gate ignores it.
@@ -43,7 +45,9 @@ export function evaluate(input, gates, { cwd = process.cwd() } = {}) {
43
45
  const warnings = [];
44
46
  for (const gate of gates) {
45
47
  let roots = [];
46
- try { roots = gate.resolveRoots(start, ctx) || []; } catch { roots = []; }
48
+ try { roots = gate.resolveRoots(start, ctx) || []; } catch (e) {
49
+ problems.push(`[${gate.name || "gate"}] resolveRoots crashed on ${start}: ${e.message}`);
50
+ }
47
51
  for (const root of roots) {
48
52
  try { problems.push(...(gate.check(root, ctx) || [])); } catch (e) {
49
53
  problems.push(`[${gate.name || "gate"}] crashed on ${root}: ${e.message}`);