@webappwiz/cli 0.0.8 → 0.0.9

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
@@ -9,49 +9,24 @@ bunx @webappwiz/cli skills add arbor # install an agent skill
9
9
  bunx @webappwiz/cli skills update # refresh the ones already installed
10
10
  bunx @webappwiz/cli rules ls # every rule there is
11
11
  bunx @webappwiz/cli judge . # check a directory against them
12
- bunx @webappwiz/cli signoff # does this change need a person?
13
12
  ```
14
13
 
15
14
  ## rules
16
15
 
17
16
  Every rule webappwiz judges itself by is named in [`rules.ts`](./rules.ts), as
18
- `JUDGE_RULES` and `SIGNOFF_RULES`, off the classes
19
- [`@webappwiz/rules`](../rules/rules) ships. There is no config file and no
20
- preset: a rule is in one of those lists or it does not exist.
17
+ `JUDGE_RULES`, off the classes [`@webappwiz/rules`](../rules/rules) ships.
18
+ There is no config file and no preset: a rule is in that list or it does not
19
+ exist.
21
20
 
22
21
  ```
23
- ID RULE SET LEVEL FILES
24
- no-em-dashes No em dashes judge error **/*.ts
25
- one-class-per-file One class per file judge error **/*.ts
26
- visual-work-tested Visual work is tested signoff
22
+ ID RULE LEVEL FILES
23
+ no-em-dashes No em dashes error **/*.ts
24
+ one-class-per-file One class per file error **/*.ts
27
25
  ```
28
26
 
29
27
  `rules show <id>` prints one in full: its glob, its level, and the document an
30
28
  agent is handed verbatim.
31
29
 
32
- The `SET` column is which of the two lists a rule is in. `judge` rules are what
33
- `judge` checks files against, file by file. `signoff` rules have no glob and no
34
- level because they are about a change rather than a file: `signoff` weighs them,
35
- and what they answer is whether it needs a person rather than where the code is
36
- wrong.
37
-
38
- ## signoff
39
-
40
- Weighs a change against the signoff rules and exits 1 with a reason when one of
41
- them wants a person to look before it merges. One agent call over the whole
42
- diff, since that is what these rules are about.
43
-
44
- ```bash
45
- bunx @webappwiz/cli signoff # everything since main
46
- bunx @webappwiz/cli signoff --since HEAD~3 # measured against another ref
47
- bunx @webappwiz/cli signoff --print # the rules, to apply yourself
48
- ```
49
-
50
- `--print` is the cheapest signoff there is: the agent about to merge reads the
51
- rules and weighs its own change, spawning nothing. A project points its agent
52
- instructions at that rather than at a list of rule ids, which goes stale the
53
- next time a rule is added.
54
-
55
30
  ## judge
56
31
 
57
32
  Runs the rules over a directory, one agent call per set of rules sharing a set
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  table
3
- } from "./index-4xj59vmx.js";
3
+ } from "./index-pyjg1rtk.js";
4
4
  // package.json
5
- var version = "0.0.8";
5
+ var version = "0.0.9";
6
6
 
7
7
  // skills/add.ts
8
8
  import { ConsoleLogger } from "webappwiz/log";
@@ -15,7 +15,7 @@ import { dirname } from "node:path";
15
15
  var arbor_skill_default = `---
16
16
  name: arbor
17
17
  description: Use the @webappwiz/arbor CLI to land your work on trunk, or a base branch given as an argument, from an isolated git worktree without pull requests. Read this before making any code change in an arbor repository, since it decides where the work happens, and whenever you need to add, claim, merge, remove, list, show, locate, or escalate a task.
18
- version: 0.0.8
18
+ version: 0.0.9
19
19
  ---
20
20
 
21
21
  # Using arbor
@@ -184,7 +184,7 @@ not squashing.
184
184
  var webappwiz_skill_default = `---
185
185
  name: webappwiz
186
186
  description: Check whether the webappwiz package already covers a piece of infrastructure before writing it by hand or adding a dependency for it. Read this before writing any of: time, clocks, durations or timers; logging; id generation; HTTP serving; CLI argument parsing; background tasks or queues; web workers; markdown parsing; typed event emitters; 2D geometry or spatial indexes; filesystem, env or process access; typed RPC over fetch; schema validation; AbortSignal plumbing; disposable resources; browser scroll, animation frames or visibility. Also use whenever the user says webappwiz.
187
- version: 0.0.8
187
+ version: 0.0.9
188
188
  ---
189
189
 
190
190
  # Using webappwiz
@@ -17,9 +17,7 @@ import {
17
17
  ReactiveOverUseState,
18
18
  ResourcesAreDisposable,
19
19
  SimpleTestSetup,
20
- TestsNotWeakened,
21
- TestsOwnTheirState,
22
- VisualWorkTested
20
+ TestsOwnTheirState
23
21
  } from "@webappwiz/rules/catalog";
24
22
  var JUDGE_RULES = defineRules({
25
23
  rules: [
@@ -42,9 +40,5 @@ var JUDGE_RULES = defineRules({
42
40
  new ResourcesAreDisposable
43
41
  ]
44
42
  });
45
- var SIGNOFF_RULES = [
46
- new TestsNotWeakened,
47
- new VisualWorkTested
48
- ];
49
43
 
50
- export { JUDGE_RULES, SIGNOFF_RULES };
44
+ export { JUDGE_RULES };
@@ -1,16 +1,3 @@
1
- import {
2
- changed,
3
- compact,
4
- count,
5
- divider,
6
- finished,
7
- mode,
8
- planned,
9
- summary,
10
- table,
11
- tokens
12
- } from "./index-4xj59vmx.js";
13
-
14
1
  // judge.ts
15
2
  import {
16
3
  agentCommand,
@@ -18,17 +5,148 @@ import {
18
5
  Harness,
19
6
  prompt as reviewPrompt
20
7
  } from "@webappwiz/rules";
21
- import { ConsoleLogger, color as color2 } from "webappwiz/log";
8
+ import { ConsoleLogger, color as color4 } from "webappwiz/log";
22
9
  import {
23
10
  NodeFs,
24
11
  NodeGlob,
25
- NodePs
12
+ NodePs as NodePs2
26
13
  } from "webappwiz/system";
27
14
  import { SystemClock } from "webappwiz/time";
28
15
 
16
+ // changed.ts
17
+ import { NodePs } from "webappwiz/system";
18
+ async function changed(dir, ref, opts = {}) {
19
+ const ps = opts.ps ?? new NodePs;
20
+ const files = new Set;
21
+ for (const argv of [
22
+ ["diff", "--name-only", "--diff-filter=d", "--relative", ref],
23
+ ["ls-files", "--others", "--exclude-standard"]
24
+ ]) {
25
+ for (const line of (await git(ps, dir, argv)).split(`
26
+ `)) {
27
+ if (line !== "") {
28
+ files.add(line);
29
+ }
30
+ }
31
+ }
32
+ return files;
33
+ }
34
+ async function git(ps, dir, argv) {
35
+ const { exitCode, stdout, stderr } = await ps.spawnCapture([
36
+ "git",
37
+ "-C",
38
+ dir,
39
+ ...argv
40
+ ]);
41
+ if (exitCode !== 0) {
42
+ throw new Error(`git ${argv[0]} failed in ${dir}: ${stderr.trim() || `exit ${exitCode}`}`);
43
+ }
44
+ return stdout;
45
+ }
46
+
47
+ // mode.ts
48
+ function mode({ print, agent, exec }) {
49
+ const named = [
50
+ print === true ? "--print" : undefined,
51
+ agent === undefined ? undefined : "--agent",
52
+ exec === undefined ? undefined : "--exec"
53
+ ].filter((flag) => flag !== undefined);
54
+ if (named.length > 1) {
55
+ throw new Error(`${named.join(" and ")} are different things to do with one run, so pass one`);
56
+ }
57
+ return print === true ? "print" : "run";
58
+ }
59
+
29
60
  // progress.ts
30
- import { color } from "webappwiz/log";
61
+ import { color as color3 } from "webappwiz/log";
31
62
  import { Duration, SystemTimer } from "webappwiz/time";
63
+
64
+ // report.ts
65
+ import { color as color2 } from "webappwiz/log";
66
+
67
+ // table.ts
68
+ import { color } from "webappwiz/log";
69
+ var table = (rows) => {
70
+ const width = (cell) => color.strip(cell).length;
71
+ const widths = rows[0]?.map((_, i) => Math.max(...rows.map((row) => width(row[i] ?? ""))));
72
+ return rows.map((row) => row.map((cell, i) => cell.padEnd((widths?.[i] ?? 0) + cell.length - width(cell))).join(" ").trimEnd());
73
+ };
74
+
75
+ // report.ts
76
+ var count = (total, word) => `${total} ${word}${total === 1 ? "" : "s"}`;
77
+ var divider = (name) => {
78
+ if (name === undefined) {
79
+ return color2.dim("-".repeat(72));
80
+ }
81
+ const opening = `--- ${name} `;
82
+ return color2.dim(opening.padEnd(Math.max(72, opening.length + 3), "-"));
83
+ };
84
+ var compact = new Intl.NumberFormat("en", { notation: "compact" });
85
+ var tokens = (bytes) => Math.ceil(bytes / 4);
86
+ function planned({
87
+ files,
88
+ rules,
89
+ calls,
90
+ estimate,
91
+ concurrency,
92
+ agent
93
+ }) {
94
+ const rows = [
95
+ [color2.dim("files"), String(files)],
96
+ [color2.dim("rules"), String(rules)],
97
+ [color2.dim("calls"), String(calls)],
98
+ ...concurrency === undefined ? [] : [[color2.dim("workers"), String(concurrency)]],
99
+ [color2.dim("reading"), `${compact.format(estimate)}+ tokens`]
100
+ ];
101
+ if (agent !== undefined) {
102
+ rows.push([color2.dim("agent"), agent]);
103
+ }
104
+ return ["", ...table(rows).map((line) => ` ${line}`), ""];
105
+ }
106
+ function finished({
107
+ rules,
108
+ files,
109
+ violations,
110
+ took,
111
+ tokens: tokens2,
112
+ worker,
113
+ workerTokens,
114
+ done,
115
+ total
116
+ }) {
117
+ const heading = `${color2.gray(`[${done}/${total}]`)} ` + color2.gray(`(${count(rules.length, "rule")}, ${count(files, "file")})`);
118
+ const spent = tokens2 === undefined ? "" : ` ${compact.format(tokens2)} tokens` + (workerTokens === undefined ? "" : ` (w${worker + 1}: ${compact.format(workerTokens)})`);
119
+ const tail = `${color2.gray(`in ${took.human()}${spent}`)}`;
120
+ if (violations.length === 0) {
121
+ return [`${color2.green("✓")} ${heading}: clean ${tail}`];
122
+ }
123
+ return [
124
+ `${color2.red("✗")} ${heading}: ${count(violations.length, "problem")} ${tail}`,
125
+ ...violations.flatMap(finding)
126
+ ];
127
+ }
128
+ function finding(violation) {
129
+ const level = violation.level === "error" ? color2.red("error") : color2.yellow(violation.level);
130
+ const lines = [
131
+ ` ${color2.bold(`${violation.file}:${violation.line}`)} ${level} ${violation.message} ${color2.gray(`(${violation.id})`)}`
132
+ ];
133
+ if (violation.code !== "") {
134
+ lines.push(color2.gray(` │ ${violation.code}`));
135
+ }
136
+ return lines;
137
+ }
138
+ function summary(violations, took, tokens2) {
139
+ const spent = tokens2 === undefined ? "" : ` ${compact.format(tokens2)} tokens total`;
140
+ const elapsed = color2.gray(`in ${took.human()}${spent}`);
141
+ if (violations.length === 0) {
142
+ return `${color2.green("✓ no violations")} ${elapsed}`;
143
+ }
144
+ const errors = violations.filter((violation) => violation.level === "error").length;
145
+ const line = `✖ ${count(violations.length, "problem")} (${count(errors, "error")}, ${count(violations.length - errors, "warning")})`;
146
+ return `${errors > 0 ? color2.red(line) : color2.yellow(line)} ${elapsed}`;
147
+ }
148
+
149
+ // progress.ts
32
150
  var terminal = () => ({
33
151
  tty: process.stdout.isTTY === true,
34
152
  write: (text) => process.stdout.write(text)
@@ -38,11 +156,11 @@ var FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "
38
156
  function render(view, frame = 0) {
39
157
  const spin = view.files === 0 ? " " : FRAMES[frame % FRAMES.length] ?? " ";
40
158
  const filled = Math.round(BAR * view.done / Math.max(1, view.total));
41
- const bar = color.green("█".repeat(filled)) + color.dim("░".repeat(BAR - filled));
159
+ const bar = color3.green("█".repeat(filled)) + color3.dim("░".repeat(BAR - filled));
42
160
  const judging = view.files === 0 ? "" : ` · judging ${count(view.files, "file")}`;
43
161
  const spent = view.tokens === undefined ? "" : ` · ${compact.format(view.tokens)} tokens`;
44
162
  const found = view.done === 0 ? "" : ` · ${view.problems === 0 ? "clean so far" : count(view.problems, "problem")}`;
45
- return `${color.green(spin)} ${bar} ${color.gray(`${view.done}/${view.total} calls${judging}${spent}${found}`)}`;
163
+ return `${color3.green(spin)} ${bar} ${color3.gray(`${view.done}/${view.total} calls${judging}${spent}${found}`)}`;
46
164
  }
47
165
 
48
166
  class Progress {
@@ -104,12 +222,10 @@ class Progress {
104
222
 
105
223
  // judge.ts
106
224
  var estimated = (reviews) => tokens(reviews.reduce((bytes, review) => bytes + review.bytes, 0));
107
- var isFileRule = (rule) => ("files" in rule);
108
225
  var title = (rule) => /^#\s+(.+)$/m.exec(rule.document)?.[1]?.trim() ?? rule.id;
109
226
 
110
227
  class JudgeCommands {
111
228
  rules;
112
- signoffRules;
113
229
  screen;
114
230
  log;
115
231
  fs;
@@ -118,38 +234,33 @@ class JudgeCommands {
118
234
  glob;
119
235
  constructor(rules, opts = {}) {
120
236
  this.rules = rules;
121
- this.signoffRules = opts.signoffRules ?? [];
122
237
  this.screen = opts.screen ?? terminal();
123
238
  this.log = opts.log ?? new ConsoleLogger;
124
239
  this.fs = opts.fs ?? new NodeFs;
125
- this.ps = opts.ps ?? new NodePs;
240
+ this.ps = opts.ps ?? new NodePs2;
126
241
  this.clock = opts.clock ?? new SystemClock;
127
242
  this.glob = opts.glob ?? new NodeGlob;
128
243
  }
129
244
  ls() {
130
- const rows = [["id", "rule", "set", "level", "files"].map(color2.dim)];
245
+ const rows = [["id", "rule", "level", "files"].map(color4.dim)];
131
246
  for (const rule of this.rules.rules) {
132
- rows.push([rule.id, title(rule), "judge", rule.level, rule.files]);
133
- }
134
- for (const rule of this.signoffRules) {
135
- rows.push([rule.id, title(rule), "signoff", "", ""]);
247
+ rows.push([rule.id, title(rule), rule.level, rule.files]);
136
248
  }
137
249
  this.log.info(table(rows).join(`
138
250
  `));
139
251
  }
140
252
  show(opts) {
141
- const all = [...this.rules.rules, ...this.signoffRules];
253
+ const all = this.rules.rules;
142
254
  const rule = all.find((candidate) => candidate.id === opts.id);
143
255
  if (!rule) {
144
256
  throw new Error(`no rule "${opts.id}". Known ids: ${all.map((candidate) => candidate.id).join(", ")}`);
145
257
  }
146
258
  const rows = [
147
- [color2.dim("id"), rule.id],
148
- [color2.dim("rule"), title(rule)]
259
+ [color4.dim("id"), rule.id],
260
+ [color4.dim("rule"), title(rule)],
261
+ [color4.dim("level"), rule.level],
262
+ [color4.dim("files"), rule.files]
149
263
  ];
150
- if (isFileRule(rule)) {
151
- rows.push([color2.dim("level"), rule.level], [color2.dim("files"), rule.files]);
152
- }
153
264
  this.log.info(table(rows).join(`
154
265
  `));
155
266
  this.log.info("");
@@ -259,4 +370,4 @@ ${divider()}`);
259
370
  }
260
371
  }
261
372
 
262
- export { JudgeCommands };
373
+ export { table, JudgeCommands };
package/index.js CHANGED
@@ -6,18 +6,13 @@ import {
6
6
  update,
7
7
  update1 as update2,
8
8
  version
9
- } from "./index-rz4spnkb.js";
9
+ } from "./index-g81gg9gz.js";
10
10
  import {
11
- JUDGE_RULES,
12
- SIGNOFF_RULES
13
- } from "./index-hz6bwqfy.js";
11
+ JUDGE_RULES
12
+ } from "./index-htwb54c6.js";
14
13
  import {
15
14
  JudgeCommands
16
- } from "./index-r14q0cmx.js";
17
- import {
18
- Signoff
19
- } from "./index-fh8js68z.js";
20
- import"./index-4xj59vmx.js";
15
+ } from "./index-pyjg1rtk.js";
21
16
 
22
17
  // index.ts
23
18
  import { NodeFs, NodeGlob } from "webappwiz/system";
@@ -35,14 +30,7 @@ webappwiz.command("update").description("pin every webappwiz dependency in a tre
35
30
  default: version,
36
31
  description: "version to pin to"
37
32
  }).action((opts, { log, fs }) => update2({ ...opts, log, fs }));
38
- var judge = ({ log, fs, ps, clock, glob }) => new JudgeCommands(JUDGE_RULES, {
39
- signoffRules: SIGNOFF_RULES,
40
- log,
41
- fs,
42
- ps,
43
- clock,
44
- glob
45
- });
33
+ var judge = ({ log, fs, ps, clock, glob }) => new JudgeCommands(JUDGE_RULES, { log, fs, ps, clock, glob });
46
34
  webappwiz.command("judge").description("check a directory against the config, one agent per glob").arg("dir", t.string(), {
47
35
  default: ".",
48
36
  description: "directory to judge (default: .)"
@@ -64,24 +52,6 @@ webappwiz.command("judge").description("check a directory against the config, on
64
52
  default: false,
65
53
  description: "line-by-line output with no live progress block"
66
54
  }).action((opts, deps) => judge(deps).judge(opts));
67
- webappwiz.command("signoff").description("weigh a change against the rules that ask for a person").arg("dir", t.string(), {
68
- default: ".",
69
- description: "directory whose change is weighed (default: .)"
70
- }).option("agent", t.optional(t.enum(Object.keys(AGENTS))), {
71
- description: "model to weigh it with (default: the config's agent)"
72
- }).option("exec", t.optional(t.string()), {
73
- description: "command the prompt is passed to, instead of --agent"
74
- }).option("print", t.boolean(), {
75
- default: false,
76
- description: "print the rules to apply yourself, and run no agent"
77
- }).option("since", t.string(), {
78
- default: "main",
79
- description: "the ref the change is measured against"
80
- }).action((opts, { log, ps, clock }) => new Signoff(SIGNOFF_RULES, JUDGE_RULES.agent, {
81
- log,
82
- ps,
83
- clock
84
- }).run(opts));
85
55
  var rules = webappwiz.group("rules").description("list and print the rules, to run or to read yourself");
86
56
  rules.command("ls").description("list the rules").action((_opts, deps) => judge(deps).ls());
87
57
  rules.command("show").description("print one rule in full, by the id `rules ls` gives it").arg("id", t.string(), { description: "rule id" }).action((opts, deps) => judge(deps).show(opts));
package/judge.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type Rule, type RuleSet } from "@webappwiz/rules";
1
+ import { type RuleSet } from "@webappwiz/rules";
2
2
  import { type Logger } from "webappwiz/log";
3
3
  import { type Fs, type Glob, type Ps } from "webappwiz/system";
4
4
  import { type Clock } from "webappwiz/time";
@@ -25,8 +25,6 @@ export interface JudgeOptions {
25
25
  }
26
26
  /** What a `JudgeCommands` runs through, and what else it lists. */
27
27
  export interface JudgeCommandsOptions {
28
- /** Rules only a reader applies, listed beside the ones a run checks. */
29
- signoffRules?: Rule[];
30
28
  /** Where live progress draws; this process's terminal by default. */
31
29
  screen?: Screen;
32
30
  log?: Logger;
@@ -37,7 +35,6 @@ export interface JudgeCommandsOptions {
37
35
  }
38
36
  export declare class JudgeCommands {
39
37
  private rules;
40
- private signoffRules;
41
38
  private screen;
42
39
  private log;
43
40
  private fs;
@@ -45,16 +42,11 @@ export declare class JudgeCommands {
45
42
  private clock;
46
43
  private glob;
47
44
  constructor(rules: RuleSet, opts?: JudgeCommandsOptions);
48
- /**
49
- * Lists every rule there is, one row each, ids first for citing. The rules a
50
- * run checks and the ones only a reader applies are one list with a SET
51
- * column, because "what rules are there" is one question.
52
- */
45
+ /** Lists every rule there is, one row each, ids first for citing. */
53
46
  ls(): void;
54
47
  /**
55
48
  * Prints one rule in full: what it covers, and the document an agent is
56
- * given, verbatim. Take the id from `rules ls` or from a finding. This is
57
- * how a reader applies a rule nothing runs for them.
49
+ * given, verbatim. Take the id from `rules ls` or from a finding.
58
50
  */
59
51
  show(opts: ShowOptions): void;
60
52
  /**
package/judge.js CHANGED
@@ -1,7 +1,6 @@
1
1
  import {
2
2
  JudgeCommands
3
- } from "./index-r14q0cmx.js";
4
- import"./index-4xj59vmx.js";
3
+ } from "./index-pyjg1rtk.js";
5
4
  export {
6
5
  JudgeCommands
7
6
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@webappwiz/cli",
3
- "version": "0.0.8",
4
- "description": "The webappwiz CLI: judge code against rules, sign off a diff, and manage agent skills",
3
+ "version": "0.0.9",
4
+ "description": "The webappwiz CLI: judge code against rules and manage agent skills",
5
5
  "license": "MIT",
6
6
  "author": "Jared Johnson",
7
7
  "repository": {
@@ -17,8 +17,8 @@
17
17
  "access": "public"
18
18
  },
19
19
  "dependencies": {
20
- "@webappwiz/rules": "^0.0.8",
21
- "webappwiz": "^0.0.8"
20
+ "@webappwiz/rules": "^0.0.9",
21
+ "webappwiz": "^0.0.9"
22
22
  },
23
23
  "peerDependencies": {
24
24
  "typescript": "^7"
@@ -41,10 +41,6 @@
41
41
  "./judge": {
42
42
  "types": "./judge.d.ts",
43
43
  "default": "./judge.js"
44
- },
45
- "./signoff": {
46
- "types": "./signoff.d.ts",
47
- "default": "./signoff.js"
48
44
  }
49
45
  },
50
46
  "bin": {
package/rules.d.ts CHANGED
@@ -1,4 +1,3 @@
1
- import { type Rule } from "@webappwiz/rules";
2
1
  /**
3
2
  * Every rule webappwiz judges itself by, named one by one. There is no preset
4
3
  * to spread and nothing runs implicitly: a rule is here or it does not run.
@@ -9,12 +8,3 @@ import { type Rule } from "@webappwiz/rules";
9
8
  * module for one of these to import.
10
9
  */
11
10
  export declare const JUDGE_RULES: import("@webappwiz/rules").RuleSet;
12
- /**
13
- * What an agent weighs before merging a change, rather than what it checks a
14
- * file against: whether the change needs a person to look at it.
15
- *
16
- * No command runs these. They are documents an agent reads and applies itself,
17
- * which is why they carry no glob and no check, and `wiz rules show` prints
18
- * them the same as any other.
19
- */
20
- export declare const SIGNOFF_RULES: Rule[];
package/rules.js CHANGED
@@ -1,8 +1,6 @@
1
1
  import {
2
- JUDGE_RULES,
3
- SIGNOFF_RULES
4
- } from "./index-hz6bwqfy.js";
2
+ JUDGE_RULES
3
+ } from "./index-htwb54c6.js";
5
4
  export {
6
- SIGNOFF_RULES,
7
5
  JUDGE_RULES
8
6
  };
package/webappwiz.js CHANGED
@@ -4,18 +4,13 @@ import {
4
4
  update,
5
5
  update1 as update2,
6
6
  version
7
- } from "./index-rz4spnkb.js";
7
+ } from "./index-g81gg9gz.js";
8
8
  import {
9
- JUDGE_RULES,
10
- SIGNOFF_RULES
11
- } from "./index-hz6bwqfy.js";
9
+ JUDGE_RULES
10
+ } from "./index-htwb54c6.js";
12
11
  import {
13
12
  JudgeCommands
14
- } from "./index-r14q0cmx.js";
15
- import {
16
- Signoff
17
- } from "./index-fh8js68z.js";
18
- import"./index-4xj59vmx.js";
13
+ } from "./index-pyjg1rtk.js";
19
14
 
20
15
  // webappwiz.ts
21
16
  import { AGENTS } from "@webappwiz/rules";
@@ -29,14 +24,7 @@ webappwiz.command("update").description("pin every webappwiz dependency in a tre
29
24
  default: version,
30
25
  description: "version to pin to"
31
26
  }).action((opts, { log, fs }) => update2({ ...opts, log, fs }));
32
- var judge = ({ log, fs, ps, clock, glob }) => new JudgeCommands(JUDGE_RULES, {
33
- signoffRules: SIGNOFF_RULES,
34
- log,
35
- fs,
36
- ps,
37
- clock,
38
- glob
39
- });
27
+ var judge = ({ log, fs, ps, clock, glob }) => new JudgeCommands(JUDGE_RULES, { log, fs, ps, clock, glob });
40
28
  webappwiz.command("judge").description("check a directory against the config, one agent per glob").arg("dir", t.string(), {
41
29
  default: ".",
42
30
  description: "directory to judge (default: .)"
@@ -58,24 +46,6 @@ webappwiz.command("judge").description("check a directory against the config, on
58
46
  default: false,
59
47
  description: "line-by-line output with no live progress block"
60
48
  }).action((opts, deps) => judge(deps).judge(opts));
61
- webappwiz.command("signoff").description("weigh a change against the rules that ask for a person").arg("dir", t.string(), {
62
- default: ".",
63
- description: "directory whose change is weighed (default: .)"
64
- }).option("agent", t.optional(t.enum(Object.keys(AGENTS))), {
65
- description: "model to weigh it with (default: the config's agent)"
66
- }).option("exec", t.optional(t.string()), {
67
- description: "command the prompt is passed to, instead of --agent"
68
- }).option("print", t.boolean(), {
69
- default: false,
70
- description: "print the rules to apply yourself, and run no agent"
71
- }).option("since", t.string(), {
72
- default: "main",
73
- description: "the ref the change is measured against"
74
- }).action((opts, { log, ps, clock }) => new Signoff(SIGNOFF_RULES, JUDGE_RULES.agent, {
75
- log,
76
- ps,
77
- clock
78
- }).run(opts));
79
49
  var rules = webappwiz.group("rules").description("list and print the rules, to run or to read yourself");
80
50
  rules.command("ls").description("list the rules").action((_opts, deps) => judge(deps).ls());
81
51
  rules.command("show").description("print one rule in full, by the id `rules ls` gives it").arg("id", t.string(), { description: "rule id" }).action((opts, deps) => judge(deps).show(opts));
package/index-4xj59vmx.js DELETED
@@ -1,135 +0,0 @@
1
- // changed.ts
2
- import { NodePs } from "webappwiz/system";
3
- async function changed(dir, ref, opts = {}) {
4
- const ps = opts.ps ?? new NodePs;
5
- const files = new Set;
6
- for (const argv of [
7
- ["diff", "--name-only", "--diff-filter=d", "--relative", ref],
8
- ["ls-files", "--others", "--exclude-standard"]
9
- ]) {
10
- for (const line of (await git(ps, dir, argv)).split(`
11
- `)) {
12
- if (line !== "") {
13
- files.add(line);
14
- }
15
- }
16
- }
17
- return files;
18
- }
19
- async function diff(dir, ref, opts = {}) {
20
- const ps = opts.ps ?? new NodePs;
21
- const patch = await git(ps, dir, ["diff", "--relative", ref]);
22
- const added = (await git(ps, dir, ["ls-files", "--others", "--exclude-standard"])).split(`
23
- `).filter((line) => line !== "");
24
- return { patch: patch.trim(), added };
25
- }
26
- async function git(ps, dir, argv) {
27
- const { exitCode, stdout, stderr } = await ps.spawnCapture([
28
- "git",
29
- "-C",
30
- dir,
31
- ...argv
32
- ]);
33
- if (exitCode !== 0) {
34
- throw new Error(`git ${argv[0]} failed in ${dir}: ${stderr.trim() || `exit ${exitCode}`}`);
35
- }
36
- return stdout;
37
- }
38
-
39
- // mode.ts
40
- function mode({ print, agent, exec }) {
41
- const named = [
42
- print === true ? "--print" : undefined,
43
- agent === undefined ? undefined : "--agent",
44
- exec === undefined ? undefined : "--exec"
45
- ].filter((flag) => flag !== undefined);
46
- if (named.length > 1) {
47
- throw new Error(`${named.join(" and ")} are different things to do with one run, so pass one`);
48
- }
49
- return print === true ? "print" : "run";
50
- }
51
-
52
- // table.ts
53
- import { color } from "webappwiz/log";
54
- var table = (rows) => {
55
- const width = (cell) => color.strip(cell).length;
56
- const widths = rows[0]?.map((_, i) => Math.max(...rows.map((row) => width(row[i] ?? ""))));
57
- return rows.map((row) => row.map((cell, i) => cell.padEnd((widths?.[i] ?? 0) + cell.length - width(cell))).join(" ").trimEnd());
58
- };
59
-
60
- // report.ts
61
- import { color as color2 } from "webappwiz/log";
62
- var count = (total, word) => `${total} ${word}${total === 1 ? "" : "s"}`;
63
- var divider = (name) => {
64
- if (name === undefined) {
65
- return color2.dim("-".repeat(72));
66
- }
67
- const opening = `--- ${name} `;
68
- return color2.dim(opening.padEnd(Math.max(72, opening.length + 3), "-"));
69
- };
70
- var compact = new Intl.NumberFormat("en", { notation: "compact" });
71
- var tokens = (bytes) => Math.ceil(bytes / 4);
72
- function planned({
73
- files,
74
- rules,
75
- calls,
76
- estimate,
77
- concurrency,
78
- agent
79
- }) {
80
- const rows = [
81
- [color2.dim("files"), String(files)],
82
- [color2.dim("rules"), String(rules)],
83
- [color2.dim("calls"), String(calls)],
84
- ...concurrency === undefined ? [] : [[color2.dim("workers"), String(concurrency)]],
85
- [color2.dim("reading"), `${compact.format(estimate)}+ tokens`]
86
- ];
87
- if (agent !== undefined) {
88
- rows.push([color2.dim("agent"), agent]);
89
- }
90
- return ["", ...table(rows).map((line) => ` ${line}`), ""];
91
- }
92
- function finished({
93
- rules,
94
- files,
95
- violations,
96
- took,
97
- tokens: tokens2,
98
- worker,
99
- workerTokens,
100
- done,
101
- total
102
- }) {
103
- const heading = `${color2.gray(`[${done}/${total}]`)} ` + color2.gray(`(${count(rules.length, "rule")}, ${count(files, "file")})`);
104
- const spent = tokens2 === undefined ? "" : ` ${compact.format(tokens2)} tokens` + (workerTokens === undefined ? "" : ` (w${worker + 1}: ${compact.format(workerTokens)})`);
105
- const tail = `${color2.gray(`in ${took.human()}${spent}`)}`;
106
- if (violations.length === 0) {
107
- return [`${color2.green("✓")} ${heading}: clean ${tail}`];
108
- }
109
- return [
110
- `${color2.red("✗")} ${heading}: ${count(violations.length, "problem")} ${tail}`,
111
- ...violations.flatMap(finding)
112
- ];
113
- }
114
- function finding(violation) {
115
- const level = violation.level === "error" ? color2.red("error") : color2.yellow(violation.level);
116
- const lines = [
117
- ` ${color2.bold(`${violation.file}:${violation.line}`)} ${level} ${violation.message} ${color2.gray(`(${violation.id})`)}`
118
- ];
119
- if (violation.code !== "") {
120
- lines.push(color2.gray(` │ ${violation.code}`));
121
- }
122
- return lines;
123
- }
124
- function summary(violations, took, tokens2) {
125
- const spent = tokens2 === undefined ? "" : ` ${compact.format(tokens2)} tokens total`;
126
- const elapsed = color2.gray(`in ${took.human()}${spent}`);
127
- if (violations.length === 0) {
128
- return `${color2.green("✓ no violations")} ${elapsed}`;
129
- }
130
- const errors = violations.filter((violation) => violation.level === "error").length;
131
- const line = `✖ ${count(violations.length, "problem")} (${count(errors, "error")}, ${count(violations.length - errors, "warning")})`;
132
- return `${errors > 0 ? color2.red(line) : color2.yellow(line)} ${elapsed}`;
133
- }
134
-
135
- export { changed, diff, mode, table, count, divider, compact, tokens, planned, finished, summary };
package/index-fh8js68z.js DELETED
@@ -1,116 +0,0 @@
1
- import {
2
- count,
3
- diff,
4
- divider,
5
- mode,
6
- tokens
7
- } from "./index-4xj59vmx.js";
8
-
9
- // signoff.ts
10
- import {
11
- agentCommand,
12
- Harness,
13
- prompt as reviewPrompt
14
- } from "@webappwiz/rules";
15
- import { ConsoleLogger, color } from "webappwiz/log";
16
- import { NodePs } from "webappwiz/system";
17
- import { SystemClock } from "webappwiz/time";
18
- class Signoff {
19
- rules;
20
- defaultAgent;
21
- log;
22
- ps;
23
- clock;
24
- constructor(rules, defaultAgent, opts = {}) {
25
- this.rules = rules;
26
- this.defaultAgent = defaultAgent;
27
- this.log = opts.log ?? new ConsoleLogger;
28
- this.ps = opts.ps ?? new NodePs;
29
- this.clock = opts.clock ?? new SystemClock;
30
- }
31
- async run(opts) {
32
- if (mode(opts) === "print") {
33
- this.print();
34
- return;
35
- }
36
- const dir = opts.dir.replace(/\/+$/, "") || "/";
37
- const { patch, added } = await diff(dir, opts.since, { ps: this.ps });
38
- if (patch === "" && added.length === 0) {
39
- this.log.info(`nothing has changed since ${opts.since}`);
40
- return;
41
- }
42
- const agent = agentCommand(opts.exec === undefined ? { agent: opts.agent ?? this.defaultAgent } : opts);
43
- const review = this.review(patch, added, opts.since);
44
- this.log.info(`weighing ${opts.since}..working tree against ` + `${count(this.rules.length, "rule")}, reading ` + `${tokens(review.bytes ?? 0)}+ tokens with ${agent.label}`);
45
- this.say(await this.judge(review, agent, dir));
46
- }
47
- async judge(review, agent, dir) {
48
- const harness = new Harness({
49
- log: this.log,
50
- ps: this.ps,
51
- clock: this.clock
52
- });
53
- harness.events.on("finished", ({ took, tokens: read }) => {
54
- const spent = read === undefined ? "" : ` ${read} tokens`;
55
- this.log.info(color.gray(`read in ${took.human()}${spent}`));
56
- });
57
- return await harness.run([review], agent, { cwd: dir });
58
- }
59
- say(findings) {
60
- if (findings.length === 0) {
61
- this.log.info(color.green("✓ nothing in this change needs a person"));
62
- return;
63
- }
64
- for (const finding of findings) {
65
- const where = finding.file === undefined ? "" : `${color.bold(finding.file + (finding.line === undefined ? "" : `:${finding.line}`))} `;
66
- this.log.info(` ${where}${finding.message} ${color.gray(`(${finding.rule})`)}`);
67
- }
68
- throw new Error(`${count(findings.length, "reason")} to escalate rather than merge`);
69
- }
70
- review(patch, added, ref) {
71
- const draft = {
72
- rules: this.rules,
73
- label: "signoff",
74
- context: [
75
- `The change, as \`git diff ${ref}\` prints it:`,
76
- `\`\`\`diff
77
- ${patch}
78
- \`\`\``,
79
- ...added.length === 0 ? [] : [
80
- "These files are new and are in no diff yet. Read each one from " + "your working directory:",
81
- added.map((file) => `- ${file}`).join(`
82
- `)
83
- ]
84
- ].join(`
85
-
86
- `),
87
- instructions: WEIGHING
88
- };
89
- return { ...draft, bytes: Buffer.byteLength(reviewPrompt(draft)) };
90
- }
91
- print() {
92
- if (this.rules.length === 0) {
93
- this.log.info("no signoff rules");
94
- return;
95
- }
96
- this.log.info("Weigh your change against each rule below. Anything that needs " + "review goes to a person instead of trunk.");
97
- for (const rule of this.rules) {
98
- this.log.info(`
99
- ${divider(rule.id)}
100
- `);
101
- this.log.info(rule.document.trim());
102
- }
103
- this.log.info(`
104
- ${divider()}`);
105
- }
106
- }
107
- var WEIGHING = [
108
- "These rules decide one thing: whether this change can merge on its own, " + "or needs a person to look at it first.",
109
- "",
110
- "Each rule's `Ships` section is what merges with nobody looking, and its " + "`Needs review` section is what does not. Report a violation only for " + "what the `Needs review` side covers, and only where the change " + "actually does it.",
111
- "",
112
- "These rules are about the change as a whole, so leave `file` and `line` " + "out unless one place in the change is the whole of the answer."
113
- ].join(`
114
- `);
115
-
116
- export { Signoff };
package/signoff.d.ts DELETED
@@ -1,57 +0,0 @@
1
- import { type Rule } from "@webappwiz/rules";
2
- import { type Logger } from "webappwiz/log";
3
- import { type Ps } from "webappwiz/system";
4
- import { type Clock } from "webappwiz/time";
5
- export interface SignoffRunOptions {
6
- /** The directory whose change is being weighed. */
7
- dir: string;
8
- agent?: string;
9
- exec?: string;
10
- /** Print the rules to the logger and spawn nothing, for the reader who is
11
- * going to apply them itself. */
12
- print?: boolean;
13
- /** The ref the change is measured against: everything since it is the
14
- * change. */
15
- since: string;
16
- }
17
- /**
18
- * Weighs a change against the rules that decide whether it can merge on its
19
- * own or needs a person to look at it first.
20
- *
21
- * One agent call over the diff, rather than one per file like `judge`: these
22
- * rules are about the change as a whole, and a run that saw one file at a time
23
- * could not answer them. Exits 1 with a reason when the change needs a person,
24
- * so it reads the same to a merge gate as to whoever ran it.
25
- */
26
- /** What a `Signoff` runs through. */
27
- export interface SignoffOptions {
28
- log?: Logger;
29
- ps?: Ps;
30
- clock?: Clock;
31
- }
32
- export declare class Signoff {
33
- private rules;
34
- private defaultAgent;
35
- private log;
36
- private ps;
37
- private clock;
38
- constructor(rules: Rule[], defaultAgent: string, opts?: SignoffOptions);
39
- run(opts: SignoffRunOptions): Promise<void>;
40
- /** The one call, and what it says about the change. */
41
- private judge;
42
- /**
43
- * The verdict, and a reason for each rule that wants a person. Throws on any
44
- * of them: an agent that ran this before merging should escalate rather than
45
- * merge, and an exit code is what says so whoever is reading.
46
- */
47
- private say;
48
- /** The change as the agent reads it, priced by the prompt: what a new file
49
- * costs to open is the agent's business and unknowable from here. */
50
- private review;
51
- /**
52
- * The rules in full, for the reader who is going to apply them. Nothing has
53
- * to run these: an agent about to merge its own work can weigh it against
54
- * them itself, and that is the cheapest signoff there is.
55
- */
56
- private print;
57
- }
package/signoff.js DELETED
@@ -1,7 +0,0 @@
1
- import {
2
- Signoff
3
- } from "./index-fh8js68z.js";
4
- import"./index-4xj59vmx.js";
5
- export {
6
- Signoff
7
- };