@forwardimpact/libwiki 0.3.0 → 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.
- package/README.md +30 -29
- package/package.json +1 -1
- package/src/active-claims.js +5 -5
- package/src/agent-roster.js +2 -2
- package/src/audit/admission.js +14 -11
- package/src/audit/conflict-markers-rule.js +9 -9
- package/src/audit/grammar.js +21 -18
- package/src/audit/rule-builders.js +20 -19
- package/src/audit/rules.js +30 -27
- package/src/audit/scopes.js +34 -33
- package/src/audit/status-row.js +14 -15
- package/src/block-renderer.js +5 -4
- package/src/boot.js +10 -8
- package/src/budget-gate.js +39 -36
- package/src/budget.js +3 -3
- package/src/cli-definition.js +19 -16
- package/src/commands/audit.js +3 -3
- package/src/commands/boot.js +1 -1
- package/src/commands/claim.js +44 -38
- package/src/commands/curate.js +34 -31
- package/src/commands/fix.js +68 -64
- package/src/commands/inbox.js +1 -1
- package/src/commands/init.js +12 -7
- package/src/commands/ledger.js +11 -11
- package/src/commands/log.js +19 -17
- package/src/commands/memo.js +4 -1
- package/src/commands/product-mix.js +16 -15
- package/src/commands/refresh.js +25 -22
- package/src/commands/rotate.js +9 -8
- package/src/commands/sync.js +26 -17
- package/src/conflict-markers.js +21 -21
- package/src/constants.js +37 -33
- package/src/gitattributes.js +10 -9
- package/src/integrity.js +29 -27
- package/src/issue-list-renderer.js +24 -16
- package/src/lane-files.js +11 -10
- package/src/ledger/anchor.js +6 -6
- package/src/ledger/projection.js +35 -32
- package/src/ledger/reader.js +4 -4
- package/src/marker-scanner.js +3 -2
- package/src/sanitize.js +12 -11
- package/src/secret-gate.js +41 -40
- package/src/status.js +12 -11
- package/src/storyboard-skeleton.js +20 -18
- package/src/util/agent-flag.js +8 -8
- package/src/util/clock.js +1 -1
- package/src/util/wiki-dir.js +7 -7
- package/src/weekly-log.js +115 -101
- package/src/wiki-sync.js +393 -361
package/README.md
CHANGED
|
@@ -8,11 +8,11 @@ parallel work.
|
|
|
8
8
|
|
|
9
9
|
<!-- END:description -->
|
|
10
10
|
|
|
11
|
-
A wiki under `wiki/` holds each agent's
|
|
11
|
+
A wiki under `wiki/` holds each agent's current state: per-agent summaries,
|
|
12
12
|
weekly logs, shared memory (priorities and active claims), and monthly
|
|
13
|
-
storyboards. `libwiki` keeps that wiki coherent across sessions
|
|
14
|
-
from it
|
|
15
|
-
result against a declarative rule set.
|
|
13
|
+
storyboards. `libwiki` keeps that wiki coherent across sessions. Agents boot
|
|
14
|
+
from it. They write decisions back. They send memos to each other. They audit
|
|
15
|
+
the result against a declarative rule set.
|
|
16
16
|
|
|
17
17
|
The primary interface is the `gemba-wiki` CLI. The library also exposes a few
|
|
18
18
|
helpers for programmatic use.
|
|
@@ -29,7 +29,7 @@ npx gemba-wiki audit
|
|
|
29
29
|
|
|
30
30
|
Every command accepts `--wiki-root` (default `wiki/`) and `--today` (default
|
|
31
31
|
today, ISO date). Agent-scoped commands require an explicit `--agent <name>`
|
|
32
|
-
(`--from` for `memo`)
|
|
32
|
+
(`--from` for `memo`). They fail closed without it. There is no environment
|
|
33
33
|
fallback. The only exception is `release --expired`, a cross-agent cleanup
|
|
34
34
|
sweep that runs without `--agent`.
|
|
35
35
|
|
|
@@ -50,8 +50,8 @@ npx gemba-wiki log note --agent X --field "PR Status" --body "merged"
|
|
|
50
50
|
npx gemba-wiki log done --agent X
|
|
51
51
|
```
|
|
52
52
|
|
|
53
|
-
|
|
54
|
-
|
|
53
|
+
`log` appends to `wiki/<agent>-YYYY-WVV.md`. It auto-rotates to `*-partN.md`
|
|
54
|
+
when the log would exceed the line budget.
|
|
55
55
|
|
|
56
56
|
### `claim` / `release` — coordinate work
|
|
57
57
|
|
|
@@ -61,9 +61,10 @@ npx gemba-wiki release --agent X --target spec-NNNN
|
|
|
61
61
|
npx gemba-wiki release --expired
|
|
62
62
|
```
|
|
63
63
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
claim is a short-lived "shipping this
|
|
64
|
+
These commands maintain the `## Active Claims` table in `MEMORY.md`. They
|
|
65
|
+
refuse duplicates. An absent row means the claim is settled. `expires_at`
|
|
66
|
+
defaults to `claimed_at + 1 day`. A claim is a short-lived "shipping this
|
|
67
|
+
now" assertion. It is not a lease.
|
|
67
68
|
|
|
68
69
|
### `inbox` — triage memos
|
|
69
70
|
|
|
@@ -74,8 +75,8 @@ npx gemba-wiki inbox promote --agent X --index 0 [--owner X]
|
|
|
74
75
|
npx gemba-wiki inbox drop --agent X --index 0
|
|
75
76
|
```
|
|
76
77
|
|
|
77
|
-
|
|
78
|
-
`promote` moves a bullet into the cross-cutting priorities table.
|
|
78
|
+
`inbox` reads bullets under the `<!-- memo:inbox -->` marker in the agent's
|
|
79
|
+
summary. `promote` moves a bullet into the cross-cutting priorities table.
|
|
79
80
|
|
|
80
81
|
### `memo` — cross-team coordination
|
|
81
82
|
|
|
@@ -84,7 +85,7 @@ npx gemba-wiki memo --from X --to Y --message "audit d642ff0c"
|
|
|
84
85
|
npx gemba-wiki memo --from X --to all --message "new XmR baseline"
|
|
85
86
|
```
|
|
86
87
|
|
|
87
|
-
|
|
88
|
+
`memo` inserts a bullet `- YYYY-MM-DD from **X**: ...` after the recipient's
|
|
88
89
|
`<!-- memo:inbox -->` marker.
|
|
89
90
|
|
|
90
91
|
### `audit` — verify wiki state
|
|
@@ -93,16 +94,16 @@ Inserts a bullet `- YYYY-MM-DD from **X**: ...` after the recipient's
|
|
|
93
94
|
npx gemba-wiki audit [--format text|json]
|
|
94
95
|
```
|
|
95
96
|
|
|
96
|
-
|
|
97
|
-
on any failure. Text output: `WARN ...` and `FAIL ...` lines plus a
|
|
97
|
+
`audit` runs a declarative catalogue of rules across the wiki. It exits 0 on
|
|
98
|
+
pass and 1 on any failure. Text output: `WARN ...` and `FAIL ...` lines plus a
|
|
98
99
|
`RESULT: ...` trailer. JSON output:
|
|
99
100
|
|
|
100
101
|
```json
|
|
101
102
|
{ "result": "pass|fail", "failures": [...], "warnings": [...] }
|
|
102
103
|
```
|
|
103
104
|
|
|
104
|
-
Each finding carries a stable `id`
|
|
105
|
-
`src/audit/rules.js
|
|
105
|
+
Each finding carries a stable `id` so you can filter on it. The catalogue
|
|
106
|
+
lives in `src/audit/rules.js`. Each new rule is one literal.
|
|
106
107
|
|
|
107
108
|
### `rotate` — force a part split
|
|
108
109
|
|
|
@@ -110,8 +111,8 @@ Each finding carries a stable `id` for filtering. The catalogue lives in
|
|
|
110
111
|
npx gemba-wiki rotate --agent X
|
|
111
112
|
```
|
|
112
113
|
|
|
113
|
-
|
|
114
|
-
main file.
|
|
114
|
+
`rotate` renames the current weekly log to the next `-partN.md`. It then
|
|
115
|
+
starts a fresh main file.
|
|
115
116
|
|
|
116
117
|
### `refresh` — re-render storyboard blocks
|
|
117
118
|
|
|
@@ -119,11 +120,11 @@ main file.
|
|
|
119
120
|
npx gemba-wiki refresh [storyboard-path]
|
|
120
121
|
```
|
|
121
122
|
|
|
122
|
-
|
|
123
|
-
marker blocks inside a storyboard from
|
|
124
|
-
Default path: `wiki/storyboard-YYYY-MMM.md` for
|
|
125
|
-
|
|
126
|
-
deterministic refresh.
|
|
123
|
+
`refresh` re-renders `<!-- xmr:metric:csv-path -->` and
|
|
124
|
+
`<!-- obstacles:open[:Nd] -->` marker blocks inside a storyboard from the CSV
|
|
125
|
+
and GitHub state behind them. Default path: `wiki/storyboard-YYYY-MMM.md` for
|
|
126
|
+
the current month. It also sweeps every expired row from
|
|
127
|
+
`MEMORY.md ## Active Claims` as part of the same deterministic refresh.
|
|
127
128
|
|
|
128
129
|
### `init` / `push` / `pull` — wiki working tree
|
|
129
130
|
|
|
@@ -133,9 +134,9 @@ npx gemba-wiki push
|
|
|
133
134
|
npx gemba-wiki pull
|
|
134
135
|
```
|
|
135
136
|
|
|
136
|
-
`init` clones the wiki repo if missing
|
|
137
|
-
`MEMORY.md
|
|
138
|
-
`pull` are thin wrappers over `git`
|
|
137
|
+
`init` clones the wiki repo if it is missing. It scaffolds Active Claims in
|
|
138
|
+
`MEMORY.md`. It creates `wiki/metrics/<skill>/` directories. `push` and
|
|
139
|
+
`pull` are thin wrappers over `git` that handle conflicts.
|
|
139
140
|
|
|
140
141
|
## Programmatic API
|
|
141
142
|
|
|
@@ -149,8 +150,8 @@ import {
|
|
|
149
150
|
bullet after the `<!-- memo:inbox -->` marker.
|
|
150
151
|
- `listAgents({ agentsDir, wikiRoot })` — discover agents from
|
|
151
152
|
`.claude/agents/*.md` and derive wiki summary paths.
|
|
152
|
-
- `insertMarkers({ agentsDir, wikiRoot })` —
|
|
153
|
-
|
|
153
|
+
- `insertMarkers({ agentsDir, wikiRoot })` — insert the memo marker into
|
|
154
|
+
existing summaries. The call is idempotent.
|
|
154
155
|
- `runAudit(rules, ctx)` — pure audit engine: `(rules, ctx) → findings[]`.
|
|
155
156
|
- `RULES` — the audit rule catalogue (one literal per rule).
|
|
156
157
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forwardimpact/libwiki",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Wiki lifecycle for agent teams — persistent memory, declarative integrity audits, and a collision ledger so coordination survives across sessions and parallel work.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"wiki",
|
package/src/active-claims.js
CHANGED
|
@@ -5,9 +5,9 @@ import {
|
|
|
5
5
|
ACTIVE_CLAIMS_TABLE_SEPARATOR,
|
|
6
6
|
} from "./constants.js";
|
|
7
7
|
|
|
8
|
-
//
|
|
9
|
-
// separator and the 6-cell row parser stay local
|
|
10
|
-
// audit's column-count check.
|
|
8
|
+
// This module and the audit share the header matcher through constants.js.
|
|
9
|
+
// The loose separator and the 6-cell row parser stay local. This module uses
|
|
10
|
+
// them to parse the table. The audit's column-count check does not use them.
|
|
11
11
|
const SEPARATOR_RE = /^\|\s*---\s*\|/;
|
|
12
12
|
const ROW_RE =
|
|
13
13
|
/^\|\s*([^|]*?)\s*\|\s*([^|]*?)\s*\|\s*([^|]*?)\s*\|\s*([^|]*?)\s*\|\s*([^|]*?)\s*\|\s*([^|]*?)\s*\|\s*$/;
|
|
@@ -169,7 +169,7 @@ export function appendClaim(memoryText, claim, _today) {
|
|
|
169
169
|
};
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
-
/** Remove the claim row
|
|
172
|
+
/** Remove the claim row that matches (agent, target). Idempotent. */
|
|
173
173
|
export function removeClaim(memoryText, { agent, target }) {
|
|
174
174
|
const lines = memoryText.split("\n");
|
|
175
175
|
const heading = findSection(lines);
|
|
@@ -186,7 +186,7 @@ export function removeClaim(memoryText, { agent, target }) {
|
|
|
186
186
|
return { text: memoryText, removed: false };
|
|
187
187
|
}
|
|
188
188
|
|
|
189
|
-
/** Split claims into active
|
|
189
|
+
/** Split claims into active and expired, based on `expires_at >= today`. */
|
|
190
190
|
export function filterExpired(claims, today) {
|
|
191
191
|
const active = [];
|
|
192
192
|
const expired = [];
|
package/src/agent-roster.js
CHANGED
|
@@ -2,8 +2,8 @@ import path from "node:path";
|
|
|
2
2
|
import { BROADCAST_TARGET } from "./constants.js";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* List all agent markdown files in the agents directory
|
|
6
|
-
* and summary paths.
|
|
5
|
+
* List all agent markdown files in the agents directory. Return the agent
|
|
6
|
+
* names and the summary paths.
|
|
7
7
|
* @param {{agentsDir: string, wikiRoot: string}} dirs
|
|
8
8
|
* @param {object} fs - Sync filesystem surface (`runtime.fsSync`).
|
|
9
9
|
*/
|
package/src/audit/admission.js
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
|
|
3
|
-
// Tracked-file enumerator for the admission scope.
|
|
4
|
-
// admission universe
|
|
3
|
+
// Tracked-file enumerator for the admission scope. It yields the
|
|
4
|
+
// admission universe. That universe holds the wiki-relative paths the
|
|
5
|
+
// filename grammar governs.
|
|
5
6
|
//
|
|
6
|
-
// The
|
|
7
|
-
// git index when git state is present.
|
|
8
|
-
// bootstrap or a test fixture
|
|
9
|
-
// rule's true positive (a *git-tracked* residue) in
|
|
10
|
-
// internals and uncommitted scratch where a real
|
|
7
|
+
// The enumerator walks the on-disk file tree under `wikiRoot`. It then
|
|
8
|
+
// intersects the walk with the git index when git state is present. Without
|
|
9
|
+
// git state (a fresh bootstrap or a test fixture), the universe is the whole
|
|
10
|
+
// walk. This keeps the rule's true positive (a *git-tracked* residue) in
|
|
11
|
+
// scope. It also excludes VCS internals and uncommitted scratch where a real
|
|
12
|
+
// repo exists.
|
|
11
13
|
|
|
12
14
|
/** Recursively collect file paths (wiki-relative, POSIX) under `dir`. */
|
|
13
15
|
function walk(absDir, wikiRoot, fs, out) {
|
|
@@ -25,9 +27,10 @@ function walk(absDir, wikiRoot, fs, out) {
|
|
|
25
27
|
|
|
26
28
|
/**
|
|
27
29
|
* Read the git index at `wikiRoot` as a set of wiki-relative paths, or `null`
|
|
28
|
-
* when there is no git state
|
|
29
|
-
*
|
|
30
|
-
*
|
|
30
|
+
* when there is no git state. There is no git state when `.git` is absent, or
|
|
31
|
+
* when the path is not a work tree. Both cases surface as a non-zero
|
|
32
|
+
* `git ls-files` exit. The `-z` flag makes paths with unusual characters
|
|
33
|
+
* round-trip. Git delimits the output with NUL. The paths are relative to the
|
|
31
34
|
* repository root, which is `wikiRoot`.
|
|
32
35
|
*/
|
|
33
36
|
function trackedSet(wikiRoot, subprocess) {
|
|
@@ -39,7 +42,7 @@ function trackedSet(wikiRoot, subprocess) {
|
|
|
39
42
|
/**
|
|
40
43
|
* Enumerate the admission universe under `wikiRoot`.
|
|
41
44
|
* @param {{wikiRoot: string, fs: object, subprocess: object}} options
|
|
42
|
-
* `fs` is the sync filesystem surface (`runtime.fsSync`)
|
|
45
|
+
* `fs` is the sync filesystem surface (`runtime.fsSync`). `subprocess` is
|
|
43
46
|
* `runtime.subprocess` (its `runSync` shells out to git).
|
|
44
47
|
* @returns {string[]} Wiki-relative POSIX paths, tracked-filtered when git state exists.
|
|
45
48
|
*/
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { scanConflictMarkers } from "../conflict-markers.js";
|
|
2
2
|
|
|
3
3
|
// Fail-severity audit rule that flags unresolved git conflict markers in any
|
|
4
|
-
// audited wiki surface.
|
|
5
|
-
//
|
|
6
|
-
// deliberately distinct from the budget rules
|
|
7
|
-
// the word/line budget
|
|
8
|
-
// ("trim history") is actively wrong for this defect.
|
|
9
|
-
// breach
|
|
10
|
-
//
|
|
11
|
-
// trim.
|
|
4
|
+
// audited wiki surface. The `conflict-scan` scope (scopes.js) resolves it.
|
|
5
|
+
// That scope yields one `{ path, text, fenceExempt }` subject per file. The
|
|
6
|
+
// rule is deliberately distinct from the budget rules. A marker block can fit
|
|
7
|
+
// inside the word/line budget. When it does trip the budget, the budget hint
|
|
8
|
+
// ("trim history") is actively wrong for this defect. So a co-occurring size
|
|
9
|
+
// breach reports BOTH findings. It never misattributes structure as size. The
|
|
10
|
+
// hint directs the writer to adjudicate the merged form. It never directs the
|
|
11
|
+
// writer to trim.
|
|
12
12
|
export const CONFLICT_MARKER_RULE = {
|
|
13
13
|
id: "conflict.markers",
|
|
14
14
|
scope: "conflict-scan",
|
|
@@ -20,5 +20,5 @@ export const CONFLICT_MARKER_RULE = {
|
|
|
20
20
|
: hits.map((h) => ({ lineNo: h.lineNo, kind: h.kind }));
|
|
21
21
|
},
|
|
22
22
|
message: (_s, r) => `unresolved git conflict marker (${r.kind})`,
|
|
23
|
-
hint: "adjudicate the merged form
|
|
23
|
+
hint: "adjudicate the merged form. reconcile the two variants into the intended content, then delete the markers. this defect is corruption. it is not a size breach, so do not shorten history to clear it",
|
|
24
24
|
};
|
package/src/audit/grammar.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { WEEKLY_LOG_NAME_RE, WEEKLY_LOG_PART_NAME_RE } from "../constants.js";
|
|
3
3
|
|
|
4
|
-
// The wiki filename admission grammar.
|
|
5
|
-
// wiki-relative path
|
|
4
|
+
// The wiki filename admission grammar. It is a pure classifier. It takes a
|
|
5
|
+
// wiki-relative path and decides whether the filename grammar admits it. The
|
|
6
6
|
// normative prose lives in memory-protocol.md's "Wiki Filename Grammar"
|
|
7
|
-
// section
|
|
7
|
+
// section. This module enforces that prose. One home per policy, so the two
|
|
8
8
|
// cannot drift. The audit's `admission` scope is the only consumer.
|
|
9
9
|
|
|
10
10
|
const NAMED_LEDGERS = new Set(["Home.md", "MEMORY.md", "STATUS.md"]);
|
|
@@ -12,9 +12,9 @@ const STORYBOARD_RE = /^storyboard-\d{4}-M\d{2}\.md$/;
|
|
|
12
12
|
const DATED_DELIVERABLE_RE = /^(.+)-\d{4}-\d{2}-\d{2}\.md$/;
|
|
13
13
|
|
|
14
14
|
// Calendar tokens, anchored at hyphen-segment boundaries so a token must occupy
|
|
15
|
-
// whole `-`-delimited segments
|
|
16
|
-
// (`release8080-notes`) is not a token
|
|
17
|
-
// bare year.
|
|
15
|
+
// whole `-`-delimited segments. `8080` *inside* a longer segment
|
|
16
|
+
// (`release8080-notes`) is not a token. A standalone `8080` segment is a
|
|
17
|
+
// bare year. The anchors `(?:^|-)…(?=-|$)` are load-bearing. A per-segment
|
|
18
18
|
// split would silently miss the multi-segment week/month/date tokens.
|
|
19
19
|
const CALENDAR_TOKEN_RES = [
|
|
20
20
|
/(?:^|-)\d{4}-W\d{2}(?=-|$)/, // week YYYY-Www
|
|
@@ -33,7 +33,10 @@ export function hasCalendarToken(stem) {
|
|
|
33
33
|
return CALENDAR_TOKEN_RES.some((re) => re.test(stem));
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
/**
|
|
36
|
+
/**
|
|
37
|
+
* A root-level `.md` file whose stem carries no calendar token. This is the
|
|
38
|
+
* summary class.
|
|
39
|
+
*/
|
|
37
40
|
function isSummaryName(base) {
|
|
38
41
|
if (!base.endsWith(".md")) return false;
|
|
39
42
|
return !hasCalendarToken(base.slice(0, -".md".length));
|
|
@@ -41,8 +44,8 @@ function isSummaryName(base) {
|
|
|
41
44
|
|
|
42
45
|
/**
|
|
43
46
|
* The summary-class stem of a root-level basename, or `null`. Named ledgers
|
|
44
|
-
* (`MEMORY.md` etc.) are not summaries.
|
|
45
|
-
* set that gates `<agent>/` sidecar directory admission.
|
|
47
|
+
* (`MEMORY.md` etc.) are not summaries. Callers use it to derive the
|
|
48
|
+
* `rootSummaryAgents` set that gates `<agent>/` sidecar directory admission.
|
|
46
49
|
* @param {string} base - A root-level basename.
|
|
47
50
|
* @returns {string|null}
|
|
48
51
|
*/
|
|
@@ -59,23 +62,23 @@ function classifyRootFile(base) {
|
|
|
59
62
|
}
|
|
60
63
|
if (STORYBOARD_RE.test(base)) return "admitted";
|
|
61
64
|
const dated = base.match(DATED_DELIVERABLE_RE);
|
|
62
|
-
// A dated deliverable's `<topic>` must itself be token-free, so a
|
|
63
|
-
//
|
|
65
|
+
// A dated deliverable's `<topic>` must itself be token-free, so a date at
|
|
66
|
+
// the end cannot smuggle a token-bearing stem (`…-history-YYYY-MM-DD.md`) in.
|
|
64
67
|
if (dated && !hasCalendarToken(dated[1])) return "admitted";
|
|
65
68
|
if (isSummaryName(base)) return "admitted";
|
|
66
|
-
//
|
|
67
|
-
// exact dated shape
|
|
69
|
+
// The classifier rejects anything else at the root. That covers a non-`.md`
|
|
70
|
+
// name, and a token-bearing name that matches no exact dated shape.
|
|
68
71
|
return "rejected";
|
|
69
72
|
}
|
|
70
73
|
|
|
71
74
|
/**
|
|
72
75
|
* Classify one wiki-relative path against the filename admission grammar.
|
|
73
76
|
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
* `metrics
|
|
77
|
-
* file beneath
|
|
78
|
-
*
|
|
77
|
+
* The grammar classifies root files (no `/`) by their basename. It admits a
|
|
78
|
+
* nested path iff the first segment is an admitted root-level directory. That
|
|
79
|
+
* is `metrics`, or an `<agent>` that has a root summary-class file. It then
|
|
80
|
+
* admits every file beneath that directory by membership (innards unpoliced).
|
|
81
|
+
* The grammar evaluates directories at the wiki root level only.
|
|
79
82
|
*
|
|
80
83
|
* @param {string} relPath - Path relative to the wiki root (POSIX separators).
|
|
81
84
|
* @param {{rootSummaryAgents: Set<string>|string[]}} options
|
|
@@ -15,8 +15,8 @@ import {
|
|
|
15
15
|
// Check builders and the derived matchers they share, extracted from rules.js
|
|
16
16
|
// so the rule table stays under the per-file line cap. Each builder takes a
|
|
17
17
|
// subject (plus optional ctx) and returns null | finding | finding[]. rules.js
|
|
18
|
-
// imports exactly the symbols its rule table references
|
|
19
|
-
// constants.js/scopes.js
|
|
18
|
+
// imports exactly the symbols its rule table references. It still imports the
|
|
19
|
+
// pure constants in constants.js/scopes.js straight from source.
|
|
20
20
|
|
|
21
21
|
export const PRIORITY_INDEX_HEADING_RE = new RegExp(
|
|
22
22
|
`^${PRIORITY_INDEX_HEADING}$`,
|
|
@@ -28,7 +28,7 @@ export const PRIORITY_SEPARATOR_RE =
|
|
|
28
28
|
export const ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
29
29
|
|
|
30
30
|
// improvement-coach is the storyboard facilitator and carries no domain
|
|
31
|
-
// metrics
|
|
31
|
+
// metrics. Only the five domain agents need their own H3.
|
|
32
32
|
const STORYBOARD_DOMAIN_AGENTS = [
|
|
33
33
|
"product-manager",
|
|
34
34
|
"release-engineer",
|
|
@@ -69,9 +69,9 @@ export const columnCount = (expected) => (s) =>
|
|
|
69
69
|
export const exists = (s) => (s.exists ? null : {});
|
|
70
70
|
export const expired = (s, ctx) => (s.expires_at < ctx.today ? {} : null);
|
|
71
71
|
|
|
72
|
-
// The heading must equal `requiredLine` exactly
|
|
73
|
-
// "### Decision — <summary>" does not satisfy it
|
|
74
|
-
// near miss so the writer fixes the heading
|
|
72
|
+
// The heading must equal `requiredLine` exactly. A suffixed variant like
|
|
73
|
+
// "### Decision — <summary>" does not satisfy it. The rule reports the variant
|
|
74
|
+
// as a near miss, so the writer fixes the heading and does not hunt for a
|
|
75
75
|
// "missing" line that is right there.
|
|
76
76
|
function entryHasDecision(lines, startIdx, requiredLine, stopRe) {
|
|
77
77
|
let seen = 0;
|
|
@@ -99,11 +99,11 @@ export const decisionWithin5 =
|
|
|
99
99
|
return offenders.length === 0 ? null : offenders;
|
|
100
100
|
};
|
|
101
101
|
|
|
102
|
-
// Flag entry-shaped `## ` headings that the rotation seam-finder would skip
|
|
103
|
-
// the grammar-drift that degrades a whole file to one unsplittable
|
|
104
|
-
// is otherwise silent (the decision-block rule matches only dated
|
|
105
|
-
//
|
|
106
|
-
// exactly the complement of the rotatable set.
|
|
102
|
+
// Flag entry-shaped `## ` headings that the rotation seam-finder would skip.
|
|
103
|
+
// This is the grammar-drift that degrades a whole file to one unsplittable
|
|
104
|
+
// prologue and is otherwise silent (the decision-block rule matches only dated
|
|
105
|
+
// headings). The check uses the same WEEKLY_LOG_SEAM_RE the seam-finder uses,
|
|
106
|
+
// so the flagged set is exactly the complement of the rotatable set.
|
|
107
107
|
export const headingGrammarDrift = (s) => {
|
|
108
108
|
const offenders = [];
|
|
109
109
|
for (let i = 0; i < s.fileLines.length; i++) {
|
|
@@ -178,7 +178,7 @@ export const weeklyAgentMismatch = (s) => {
|
|
|
178
178
|
// Carry surface: the H1 agent slug (`# <agent> — Carries`) must agree with the
|
|
179
179
|
// filename prefix (`<agent>-carries.md`), the carry analogue of the summary /
|
|
180
180
|
// weekly-log agreement rules. The H1 is the slug form already, so slugify is a
|
|
181
|
-
// no-op for well-formed files
|
|
181
|
+
// no-op for well-formed files. Otherwise it normalises a stray capitalisation.
|
|
182
182
|
export const carryAgentMismatch = (s) => {
|
|
183
183
|
const m = s.firstLine.match(CARRY_SURFACE_H1_RE);
|
|
184
184
|
if (!m) return null;
|
|
@@ -186,10 +186,10 @@ export const carryAgentMismatch = (s) => {
|
|
|
186
186
|
return titleSlug === s.agentPrefix ? null : { titleSlug };
|
|
187
187
|
};
|
|
188
188
|
|
|
189
|
-
// Each Carry entry is an H3 block
|
|
189
|
+
// Each Carry entry is an H3 block. Every block must name a clearance trigger
|
|
190
190
|
// (the `**Carry-clearance:**` marker). Walk the file's H3 boundaries and emit
|
|
191
|
-
// one finding
|
|
192
|
-
// `nothingAfterH2
|
|
191
|
+
// one finding for each block that lacks the marker. This uses the same
|
|
192
|
+
// finding[] shape as `nothingAfterH2`.
|
|
193
193
|
export const carryEntryHasClearance = (s) => {
|
|
194
194
|
const offenders = [];
|
|
195
195
|
let blockStart = -1;
|
|
@@ -221,10 +221,11 @@ export const AGENT_H3_REQUIREMENTS = STORYBOARD_DOMAIN_AGENTS.map((agent) => ({
|
|
|
221
221
|
// -- Metrics CSV duplicate rows --
|
|
222
222
|
|
|
223
223
|
// Report every data line byte-identical to an earlier data line in the same
|
|
224
|
-
// CSV. Line 1 is the header (positionally) and
|
|
225
|
-
// header is never a duplicate subject.
|
|
226
|
-
// spec's exit path for free
|
|
227
|
-
// the pair non-identical
|
|
224
|
+
// CSV. Line 1 is the header (positionally) and the check skips blank lines.
|
|
225
|
+
// The header is never a duplicate subject. The check keys on exact line
|
|
226
|
+
// equality, which gives the spec's exit path for free. Any column edit (run id
|
|
227
|
+
// or note) on one row makes the pair non-identical, so the finding no longer
|
|
228
|
+
// fires.
|
|
228
229
|
export const duplicateCsvRows = (s) => {
|
|
229
230
|
const seen = new Set();
|
|
230
231
|
const findings = [];
|
package/src/audit/rules.js
CHANGED
|
@@ -56,9 +56,9 @@ import {
|
|
|
56
56
|
import { STATUS_ROW_RULES } from "./status-row.js";
|
|
57
57
|
|
|
58
58
|
// The budget predicates the post-landing pre-push gate re-runs over the
|
|
59
|
-
// outgoing tree.
|
|
60
|
-
// membership of
|
|
61
|
-
// rules flows through the gate with no gate-code change.
|
|
59
|
+
// outgoing tree. This set names the ids, so the gate selects rules by
|
|
60
|
+
// membership of the set. A future predicate change to any of these
|
|
61
|
+
// rules then flows through the gate with no gate-code change.
|
|
62
62
|
export const BUDGET_RULE_IDS = new Set([
|
|
63
63
|
"summary.line-budget",
|
|
64
64
|
"summary.word-budget",
|
|
@@ -110,7 +110,7 @@ export const RULES = [
|
|
|
110
110
|
severity: "fail",
|
|
111
111
|
check: lineBudget(SUMMARY_LINE_BUDGET),
|
|
112
112
|
message: (_s, r) => `${r.value} lines (limit ${SUMMARY_LINE_BUDGET})`,
|
|
113
|
-
hint: "trim history into the weekly log
|
|
113
|
+
hint: "trim history into the weekly log. the summary holds settled state. it does not hold history",
|
|
114
114
|
},
|
|
115
115
|
{
|
|
116
116
|
id: "summary.word-budget",
|
|
@@ -118,7 +118,7 @@ export const RULES = [
|
|
|
118
118
|
severity: "fail",
|
|
119
119
|
check: wordBudget(SUMMARY_WORD_BUDGET),
|
|
120
120
|
message: (_s, r) => `${r.value} words (limit ${SUMMARY_WORD_BUDGET})`,
|
|
121
|
-
hint: "trim history into the weekly log
|
|
121
|
+
hint: "trim history into the weekly log. the summary holds settled state. it does not hold history",
|
|
122
122
|
},
|
|
123
123
|
{
|
|
124
124
|
id: "summary.h1-agent-matches-filename",
|
|
@@ -176,7 +176,7 @@ export const RULES = [
|
|
|
176
176
|
check: headingGrammarDrift,
|
|
177
177
|
message: (_s, r) =>
|
|
178
178
|
`Entry heading '${r.observed}' does not match the dated grammar`,
|
|
179
|
-
hint: "weekly-log entry headings must be '## YYYY-MM-DD'
|
|
179
|
+
hint: "weekly-log entry headings must be '## YYYY-MM-DD'. open entries with `gemba-wiki log decision/note`, which emit a heading that conforms and that the rotation seam-finder can split",
|
|
180
180
|
},
|
|
181
181
|
{
|
|
182
182
|
id: "decision-block.heading-within-5",
|
|
@@ -189,9 +189,9 @@ export const RULES = [
|
|
|
189
189
|
}),
|
|
190
190
|
message: (_s, r) =>
|
|
191
191
|
r.nearMiss
|
|
192
|
-
? `Entry opens with '${r.nearMiss}'
|
|
192
|
+
? `Entry opens with '${r.nearMiss}'. The heading must be exactly '${DECISION_HEADING}'. Move the suffix into the body`
|
|
193
193
|
: `Entry lacks a line that is exactly '${DECISION_HEADING}'`,
|
|
194
|
-
hint: `open each '## YYYY-MM-DD' entry with \`gemba-wiki log decision
|
|
194
|
+
hint: `open each '## YYYY-MM-DD' entry with \`gemba-wiki log decision\`. it emits a line that contains exactly '${DECISION_HEADING}' (no suffix, because the check is an exact match). put the one-line summary in the body below it, drawn from the entry's own narrative. do not invent rationale the entry does not support`,
|
|
195
195
|
},
|
|
196
196
|
|
|
197
197
|
// -- Weekly logs (sealed parts) --
|
|
@@ -211,7 +211,7 @@ export const RULES = [
|
|
|
211
211
|
check: headingGrammarDrift,
|
|
212
212
|
message: (_s, r) =>
|
|
213
213
|
`Entry heading '${r.observed}' does not match the dated grammar`,
|
|
214
|
-
hint: "weekly-log entry headings must be '## YYYY-MM-DD'
|
|
214
|
+
hint: "weekly-log entry headings must be '## YYYY-MM-DD'. open entries with `gemba-wiki log decision/note`, which emit a heading that conforms and that the rotation seam-finder can split",
|
|
215
215
|
},
|
|
216
216
|
{
|
|
217
217
|
id: "weekly-log-part.line-budget",
|
|
@@ -220,7 +220,7 @@ export const RULES = [
|
|
|
220
220
|
remediation: "rotate",
|
|
221
221
|
check: lineBudget(WEEKLY_LOG_LINE_BUDGET),
|
|
222
222
|
message: (_s, r) => `${r.value} lines (limit ${WEEKLY_LOG_LINE_BUDGET})`,
|
|
223
|
-
hint: "`bunx gemba-wiki fix` re-bisects an over-budget part at its day-section seams and, for a lone over-cap day, at its '### ' block seams
|
|
223
|
+
hint: "`bunx gemba-wiki fix` re-bisects an over-budget part at its day-section seams and, for a lone over-cap day, at its '### ' block seams. only a single '### ' block that alone exceeds the budget remains for a human to shorten",
|
|
224
224
|
},
|
|
225
225
|
{
|
|
226
226
|
id: "weekly-log-part.word-budget",
|
|
@@ -229,7 +229,7 @@ export const RULES = [
|
|
|
229
229
|
remediation: "rotate",
|
|
230
230
|
check: wordBudget(WEEKLY_LOG_WORD_BUDGET),
|
|
231
231
|
message: (_s, r) => `${r.value} words (limit ${WEEKLY_LOG_WORD_BUDGET})`,
|
|
232
|
-
hint: "`bunx gemba-wiki fix` re-bisects an over-budget part at its day-section seams and, for a lone over-cap day, at its '### ' block seams
|
|
232
|
+
hint: "`bunx gemba-wiki fix` re-bisects an over-budget part at its day-section seams and, for a lone over-cap day, at its '### ' block seams. only a single '### ' block that alone exceeds the budget remains for a human to shorten",
|
|
233
233
|
},
|
|
234
234
|
{
|
|
235
235
|
id: "weekly-log-part.h1-agent-matches-filename",
|
|
@@ -242,10 +242,11 @@ export const RULES = [
|
|
|
242
242
|
},
|
|
243
243
|
|
|
244
244
|
// -- Carry surfaces --
|
|
245
|
-
//
|
|
246
|
-
//
|
|
247
|
-
//
|
|
248
|
-
// h1-shape rule. The two rules
|
|
245
|
+
// There is no `h1-shape` rule. The classifier classifies a weekly log on
|
|
246
|
+
// filename alone. The carry classifier (scopes.js) instead requires the
|
|
247
|
+
// Carry H1 before it assigns the scope. So it leaves a malformed-H1 file
|
|
248
|
+
// unclassified, and that file never reaches an h1-shape rule. The two rules
|
|
249
|
+
// below are the reachable, failable set (SC #2).
|
|
249
250
|
|
|
250
251
|
{
|
|
251
252
|
id: "carry-surface.h1-agent-matches-filename",
|
|
@@ -282,7 +283,7 @@ export const RULES = [
|
|
|
282
283
|
when: memoryExists,
|
|
283
284
|
check: lineBudget(MEMORY_LINE_BUDGET),
|
|
284
285
|
message: (_s, r) => `${r.value} lines (limit ${MEMORY_LINE_BUDGET})`,
|
|
285
|
-
hint: "MEMORY.md holds settled cross-cutting state
|
|
286
|
+
hint: "MEMORY.md holds settled cross-cutting state. it does not hold history. release settled claims, prune stale priority rows, and move event-by-event detail to the relevant ledger page or weekly log",
|
|
286
287
|
},
|
|
287
288
|
{
|
|
288
289
|
id: "memory.word-budget",
|
|
@@ -291,7 +292,7 @@ export const RULES = [
|
|
|
291
292
|
when: memoryExists,
|
|
292
293
|
check: wordBudget(MEMORY_WORD_BUDGET),
|
|
293
294
|
message: (_s, r) => `${r.value} words (limit ${MEMORY_WORD_BUDGET})`,
|
|
294
|
-
hint: "MEMORY.md holds settled cross-cutting state
|
|
295
|
+
hint: "MEMORY.md holds settled cross-cutting state. it does not hold history. release settled claims, prune stale priority rows, and move event-by-event detail to the relevant ledger page or weekly log",
|
|
295
296
|
},
|
|
296
297
|
{
|
|
297
298
|
id: "memory.priority-heading",
|
|
@@ -400,7 +401,7 @@ export const RULES = [
|
|
|
400
401
|
when: storyboardExists,
|
|
401
402
|
check: lineBudget(STORYBOARD_LINE_BUDGET),
|
|
402
403
|
message: (_s, r) => `${r.value} lines (limit ${STORYBOARD_LINE_BUDGET})`,
|
|
403
|
-
hint: "see per-section word budgets in storyboard-template.md
|
|
404
|
+
hint: "see per-section word budgets in storyboard-template.md. retire prior-session Headlines/Notes/Next-review entries to weekly logs",
|
|
404
405
|
},
|
|
405
406
|
{
|
|
406
407
|
id: "storyboard.word-budget",
|
|
@@ -409,7 +410,7 @@ export const RULES = [
|
|
|
409
410
|
when: storyboardExists,
|
|
410
411
|
check: wordBudget(STORYBOARD_WORD_BUDGET),
|
|
411
412
|
message: (_s, r) => `${r.value} words (limit ${STORYBOARD_WORD_BUDGET})`,
|
|
412
|
-
hint: "see per-section word budgets in storyboard-template.md
|
|
413
|
+
hint: "see per-section word budgets in storyboard-template.md. retire prior-session Headlines/Notes/Next-review entries to weekly logs",
|
|
413
414
|
},
|
|
414
415
|
{
|
|
415
416
|
id: "storyboard.markers-balanced.xmr",
|
|
@@ -454,8 +455,9 @@ export const RULES = [
|
|
|
454
455
|
hint: "every '<!-- agent-experiments -->' needs a matching '<!-- /agent-experiments -->'",
|
|
455
456
|
},
|
|
456
457
|
|
|
457
|
-
// -- Metrics CSVs (union merge keeps both sides on concurrent appends
|
|
458
|
-
// exact-duplicate rows
|
|
458
|
+
// -- Metrics CSVs (union merge keeps both sides on concurrent appends. The
|
|
459
|
+
// audit surfaces exact-duplicate rows here. It never silently removes
|
|
460
|
+
// them) --
|
|
459
461
|
|
|
460
462
|
{
|
|
461
463
|
id: "metrics-csv.duplicate-row",
|
|
@@ -464,7 +466,7 @@ export const RULES = [
|
|
|
464
466
|
check: duplicateCsvRows,
|
|
465
467
|
message: (_s, r) =>
|
|
466
468
|
`Duplicate metrics row at line ${r.lineNo} (exact match of an earlier row)`,
|
|
467
|
-
hint: "remove the surplus row, or differentiate a genuinely-distinct measurement
|
|
469
|
+
hint: "remove the surplus row, or differentiate a genuinely-distinct measurement. edit its run id or note so the rows are no longer identical",
|
|
468
470
|
},
|
|
469
471
|
|
|
470
472
|
// -- STATUS.md rows (per-migration-unit sub-row schema) --
|
|
@@ -478,17 +480,18 @@ export const RULES = [
|
|
|
478
480
|
// -- Filename admission --
|
|
479
481
|
|
|
480
482
|
// The `admission` resolver yields one subject per git-tracked path the
|
|
481
|
-
// filename grammar rejects, so the check always fires.
|
|
482
|
-
// wrong automated move or delete destroys memory, so
|
|
483
|
-
//
|
|
484
|
-
//
|
|
483
|
+
// filename grammar rejects, so the check always fires. This finding is a
|
|
484
|
+
// flag for a human. A wrong automated move or delete destroys memory, so
|
|
485
|
+
// `fix` routes it to the human report and never touches the file. Any
|
|
486
|
+
// non-`agent` remediation class does the same.
|
|
485
487
|
{
|
|
486
488
|
id: "admission.not-in-grammar",
|
|
487
489
|
scope: "admission",
|
|
488
490
|
severity: "fail",
|
|
489
491
|
remediation: "flag",
|
|
490
492
|
check: () => ({}),
|
|
491
|
-
message: (s) =>
|
|
493
|
+
message: (s) =>
|
|
494
|
+
`${s.relPath} matches no class in the wiki filename grammar`,
|
|
492
495
|
hint: "rename to an admitted class, or extend the Wiki Filename Grammar section in memory-protocol.md and audit/grammar.js together (the single admission path)",
|
|
493
496
|
},
|
|
494
497
|
];
|