@forwardimpact/libwiki 0.3.0 → 0.3.2
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 +53 -33
- 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 +23 -20
- 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,14 +150,33 @@ 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
|
|
|
157
158
|
## Documentation
|
|
158
159
|
|
|
159
|
-
- [Operate a Predictable Agent Team](https://www.
|
|
160
|
-
- [Send a Memo or Update a Storyboard](https://www.
|
|
161
|
-
- [Audit and Auto-Fix the Wiki](https://www.
|
|
162
|
-
- [Allocate Collision-Ledger Entries for Parallel Work](https://www.
|
|
160
|
+
- [Operate a Predictable Agent Team](https://www.gemba.team/docs/predictable-team/index.md)
|
|
161
|
+
- [Send a Memo or Update a Storyboard](https://www.gemba.team/docs/predictable-team/wiki-operations/index.md)
|
|
162
|
+
- [Audit and Auto-Fix the Wiki](https://www.gemba.team/docs/predictable-team/wiki-integrity/index.md)
|
|
163
|
+
- [Allocate Collision-Ledger Entries for Parallel Work](https://www.gemba.team/docs/predictable-team/collision-ledger/index.md)
|
|
164
|
+
|
|
165
|
+
## Documentation home
|
|
166
|
+
|
|
167
|
+
libwiki is an import-only library. It declares no `bin`. The `gemba-wiki`
|
|
168
|
+
command ships with the Gemba product, which imports these modules. Run it
|
|
169
|
+
with `npx gemba-wiki`.
|
|
170
|
+
|
|
171
|
+
The package publishes as `@forwardimpact/libwiki` on the Forward Impact npm
|
|
172
|
+
scope. Install it with `npm install @forwardimpact/libwiki`. Its task guides
|
|
173
|
+
live on the Gemba site at <https://www.gemba.team/>. The Forward Impact library
|
|
174
|
+
guide tree at <https://www.forwardimpact.team/docs/libraries/index.md> is not
|
|
175
|
+
this library's guide home.
|
|
176
|
+
|
|
177
|
+
**Decision (2026-08-26):** the split package scope and guide host are
|
|
178
|
+
deliberate. libwiki stays a Gear npm package, so `package.json .homepage`
|
|
179
|
+
keeps <https://www.forwardimpact.team>. The agent-memory guides moved to
|
|
180
|
+
gemba.team with the rest of the Gemba product. The `## Documentation` list
|
|
181
|
+
above carries the current URLs. Old `forwardimpact.team/docs/libraries/`
|
|
182
|
+
addresses forward to gemba.team.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forwardimpact/libwiki",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
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 = [];
|