verderer 0.2.0 → 0.3.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
@@ -16,6 +16,8 @@ Validated on a real repo before a line of this tool existed: of 3 long-standing
16
16
  npx verderer scan # inventory harness rules into .claude/verderer.json
17
17
  npx verderer status # active/retired rules, provenance, verdicts
18
18
  npx verderer checkpoint --model <id> # record the model by hand (tools without an adapter)
19
+ npx verderer verdict 9f2c dead --evidence "derived it from neighboring code"
20
+ # record an inquest (id prefix is enough)
19
21
  npx verderer probe # the ablation recipe (automation is the roadmap)
20
22
  ```
21
23
 
@@ -58,6 +60,8 @@ For tools without a detection adapter yet, `verderer checkpoint --model <id>` re
58
60
 
59
61
  Verdicts are three-valued on purpose: **dead** (the model does it unaided), **derivable** (unaided but an expensive miss — keeping it is cheap insurance), **needed**. Blast radius is a judgment call, so verderer reports and you decide; it never auto-deletes a rule.
60
62
 
63
+ `verderer verdict` records one: it stamps today's date and the ledger's current model, so the next model change flags it as stale. Evidence is required.
64
+
61
65
  ## Roadmap
62
66
 
63
67
  - `probe` automation: per-rule ablation worktrees + headless runs + verdict recording (today it prints the validated manual recipe).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "verderer",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "A rule ledger for AI agent harnesses: per-rule provenance for CLAUDE.md/AGENTS.md, plus ablation inquests that tell you which rules still earn their place after a model upgrade.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { resolve } from 'node:path';
2
2
  import { scan } from './scan.js';
3
- import { loadLedger, saveLedger, ledgerPath, staleVerdicts, isoToday } from './ledger.js';
3
+ import { loadLedger, saveLedger, ledgerPath, staleVerdicts, recordVerdict, isoToday } from './ledger.js';
4
4
 
5
5
  const HELP = `verderer — a rule ledger for AI agent harnesses
6
6
 
@@ -8,6 +8,8 @@ Usage:
8
8
  verderer scan [dir] inventory harness rules (CLAUDE.md, AGENTS.md) into .claude/verderer.json
9
9
  verderer status [dir] show the ledger: active/retired rules, provenance, verdicts
10
10
  verderer checkpoint [dir] --model <id> record the model by hand (for tools without a detection adapter)
11
+ verderer verdict <id> <result> [dir] --evidence <text>
12
+ record an inquest: dead | derivable | needed (id prefix is enough)
11
13
  verderer probe print the manual ablation recipe (automation is the roadmap)
12
14
 
13
15
  Scan also records which model the repo is actually running on (read from the
@@ -24,11 +26,12 @@ export async function run(argv) {
24
26
  const positional = [];
25
27
  for (let i = 0; i < argv.length; i++) {
26
28
  if (argv[i] === '--model') flags.model = argv[++i];
29
+ else if (argv[i] === '--evidence') flags.evidence = argv[++i];
27
30
  else positional.push(argv[i]);
28
31
  }
29
32
 
30
- const [cmd, dirArg] = positional;
31
- const dir = resolve(dirArg ?? '.');
33
+ const [cmd, ...args] = positional;
34
+ const dir = resolve(args[0] ?? '.');
32
35
 
33
36
  switch (cmd) {
34
37
  case 'scan':
@@ -37,6 +40,10 @@ export async function run(argv) {
37
40
  return cmdStatus(dir);
38
41
  case 'checkpoint':
39
42
  return cmdCheckpoint(dir, flags.model);
43
+ case 'verdict': {
44
+ const [id, result, dirArg] = args;
45
+ return cmdVerdict(resolve(dirArg ?? '.'), id, result, flags.evidence);
46
+ }
40
47
  case 'probe':
41
48
  return cmdProbe();
42
49
  case undefined:
@@ -125,6 +132,27 @@ function cmdCheckpoint(dir, modelId) {
125
132
  return 0;
126
133
  }
127
134
 
135
+ function cmdVerdict(dir, id, result, evidence) {
136
+ const prior = loadLedger(dir);
137
+
138
+ if (prior.rules.length === 0) {
139
+ console.error(`no ledger at ${ledgerPath(dir)} — run \`verderer scan\` first`);
140
+ return 1;
141
+ }
142
+
143
+ try {
144
+ const { ledger, rule } = recordVerdict(prior, id, result, { evidence });
145
+ saveLedger(dir, ledger);
146
+ console.log(`${rule.id} ${result} on ${rule.lastVerdict.model}`);
147
+ console.log(` ${truncate(rule.text, 100)}`);
148
+ return 0;
149
+ } catch (err) {
150
+ console.error(err.message);
151
+ console.error('usage: verderer verdict <id> <dead|derivable|needed> [dir] --evidence <text>');
152
+ return 1;
153
+ }
154
+ }
155
+
128
156
  function cmdProbe() {
129
157
  console.log(`probe automation is on the roadmap. The manual recipe (validated on a real repo):
130
158
 
@@ -136,7 +164,8 @@ function cmdProbe() {
136
164
  - yes, by reading the codebase → the rule is dead (the code now carries the convention)
137
165
  - yes, but a miss would be expensive → derivable; keeping it is cheap insurance
138
166
  - no → the rule still earns its place
139
- 5. Record the verdict in the ledger, prune with evidence.
167
+ 5. Record it: verderer verdict <id> <dead|derivable|needed> --evidence "..."
168
+ then prune with evidence.
140
169
 
141
170
  Verdicts are judgment calls on blast radius — verderer reports, you decide. It never auto-deletes.`);
142
171
  return 0;
package/src/ledger.js CHANGED
@@ -70,6 +70,34 @@ export function staleVerdicts(ledger) {
70
70
  return ledger.rules.filter((r) => r.status === 'active' && r.lastVerdict?.model && r.lastVerdict.model !== current);
71
71
  }
72
72
 
73
+ export const VERDICTS = ['dead', 'derivable', 'needed'];
74
+
75
+ // Stamp a verdict on one rule, found by id prefix (like a git sha).
76
+ // The verdict records the ledger's current model so a later model change
77
+ // can flag it as stale; without a model there is nothing to judge against.
78
+ export function recordVerdict(ledger, prefix, result, { evidence, today = isoToday() } = {}) {
79
+ if (!VERDICTS.includes(result)) {
80
+ throw new Error(`verdict must be one of: ${VERDICTS.join(', ')}`);
81
+ }
82
+
83
+ if (!evidence) throw new Error('a verdict needs --evidence');
84
+
85
+ const model = ledger.model?.id;
86
+ if (!model) throw new Error('no model recorded — run `verderer scan` or `verderer checkpoint --model <id>` first');
87
+
88
+ const matches = prefix ? ledger.rules.filter((r) => r.id.startsWith(prefix)) : [];
89
+ if (matches.length === 0) throw new Error(`no rule matches id ${prefix ?? '(none)'}`);
90
+ if (matches.length > 1) throw new Error(`id ${prefix} is ambiguous: ${matches.map((r) => r.id).join(', ')}`);
91
+
92
+ const [rule] = matches;
93
+ const verdict = { date: today, result, model, evidence };
94
+
95
+ return {
96
+ ledger: { ...ledger, rules: ledger.rules.map((r) => (r === rule ? { ...r, lastVerdict: verdict } : r)) },
97
+ rule: { ...rule, lastVerdict: verdict },
98
+ };
99
+ }
100
+
73
101
  function stripBlame({ blame, ...rule }) {
74
102
  return rule;
75
103
  }