verderer 0.4.0 → 0.5.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 (3) hide show
  1. package/README.md +2 -0
  2. package/package.json +1 -1
  3. package/src/parse.js +54 -14
package/README.md CHANGED
@@ -21,6 +21,8 @@ npx verderer verdict 9f2c dead --evidence "derived it from neighboring code"
21
21
  npx verderer probe # the ablation recipe (automation is the roadmap)
22
22
  ```
23
23
 
24
+ A rule is a top-level list item (`-`, `*`, `+`, `1.`) or a paragraph that opens with a bold lead (`**Never trust a merged schema.** Every module…`). Code blocks and spec-kit's generated sections (`Active Technologies`, `Recent Changes`) are skipped.
25
+
24
26
  `scan` is idempotent and append-only: known rules keep their provenance and verdicts, new rules are dated, rules that left the file are marked retired — never deleted. The ledger is the history of your harness's law.
25
27
 
26
28
  Rewording a rule doesn't reset it. A rule edited in place (same spot, mostly the same words, or a trim of them) keeps its provenance and verdict, and its old wording goes into `revisions`. The verdict judged the old wording, so it shows as stale until you re-run the inquest.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "verderer",
3
- "version": "0.4.0",
3
+ "version": "0.5.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/parse.js CHANGED
@@ -1,49 +1,89 @@
1
1
  import { createHash } from 'node:crypto';
2
2
 
3
- // A "rule" is a top-level bullet in a harness file. Continuation lines
4
- // (indented 2+ spaces) belong to the bullet above; nested bullets too —
5
- // they qualify the rule, they are not rules of their own.
3
+ // spec-kit rewrites these sections from feature plans on every run (its
4
+ // update-agent-context script finds them by heading): tool output, not rules.
5
+ const GENERATED_SECTIONS = new Set(['Active Technologies', 'Recent Changes']);
6
+
7
+ const LIST_ITEM = /^(?:[-*+]|\d+[.)])\s+/;
8
+ const BOLD_LEAD = /^\*\*[^*]+\*\*/;
9
+ const BOLD_LABEL = /^\*\*[^*]+\*\*:?$/; // "**Why this matters:**" heads what follows
10
+ const FENCE = /^\s*(?:```|~~~)/;
11
+ const THEMATIC_BREAK = /^([-*_])(?:\s*\1){2,}\s*$/;
12
+ const CONTINUATION = /^\s{2,}\S/;
13
+
14
+ // A "rule" is a top-level list item (-, *, +, 1., 1)) or a paragraph that
15
+ // opens with a bold lead ("**Never trust a merged schema.** Every module…");
16
+ // a bold label alone ("**Quick Tips:**") is a heading in disguise, not a rule.
17
+ // Indented lines belong to the item above, nested lists too: they qualify the
18
+ // rule, they are not rules of their own. Plain prose and code are not rules.
6
19
  export function parseRules(markdown, source) {
7
20
  const rules = [];
8
21
  let section = '';
9
22
  let current = null;
23
+ let fence = null;
24
+ let afterBlank = true;
10
25
 
11
26
  const flush = () => {
12
27
  if (!current) return;
13
28
  const text = current.lines.join('\n').trim();
14
- rules.push({
15
- id: ruleId(text),
16
- text,
17
- source,
18
- section,
19
- line: current.line,
20
- });
29
+ if (!(current.paragraph && BOLD_LABEL.test(text))) {
30
+ rules.push({ id: ruleId(text), text, source, section, line: current.line });
31
+ }
21
32
  current = null;
22
33
  };
23
34
 
24
35
  const lines = markdown.split('\n');
25
36
  for (let i = 0; i < lines.length; i++) {
26
37
  const line = lines[i];
27
- const heading = line.match(/^#{1,6}\s+(.*)/);
38
+ const blank = line.trim() === '';
39
+
40
+ // Code: skipped, unless it is indented under a rule (an example inside it).
41
+ if (fence) {
42
+ if (fence.owner && !blank) fence.owner.lines.push(line.trim());
43
+ if (FENCE.test(line)) {
44
+ fence = null;
45
+ afterBlank = true;
46
+ }
47
+ continue;
48
+ }
28
49
 
50
+ if (FENCE.test(line)) {
51
+ const owner = current && CONTINUATION.test(line) ? current : null;
52
+ if (owner) owner.lines.push(line.trim());
53
+ else flush();
54
+ fence = { owner };
55
+ continue;
56
+ }
57
+
58
+ const heading = line.match(/^#{1,6}\s+(.*)/);
29
59
  if (heading) {
30
60
  flush();
31
61
  section = heading[1].trim();
62
+ afterBlank = true;
32
63
  continue;
33
64
  }
34
65
 
35
- if (/^- /.test(line)) {
66
+ if (GENERATED_SECTIONS.has(section)) continue;
67
+
68
+ const item = line.match(LIST_ITEM);
69
+ if (item && !THEMATIC_BREAK.test(line)) {
36
70
  flush();
37
- current = { lines: [line.slice(2)], line: i + 1 };
71
+ current = { lines: [line.slice(item[0].length)], line: i + 1, paragraph: false };
72
+ afterBlank = false;
38
73
  continue;
39
74
  }
40
75
 
41
- if (current && /^\s{2,}\S/.test(line)) {
76
+ if (current && (CONTINUATION.test(line) || (current.paragraph && !blank))) {
42
77
  current.lines.push(line.trim());
43
78
  continue;
44
79
  }
45
80
 
46
81
  flush();
82
+
83
+ if (afterBlank && BOLD_LEAD.test(line)) {
84
+ current = { lines: [line], line: i + 1, paragraph: true };
85
+ }
86
+ afterBlank = blank;
47
87
  }
48
88
  flush();
49
89