@forwardimpact/libwiki 0.2.11 → 0.2.12
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/bin/fit-wiki.js +49 -286
- package/package.json +1 -1
- package/src/agent-roster.js +7 -6
- package/src/audit/rules.js +5 -0
- package/src/audit/scopes.js +81 -28
- package/src/audit/status-row.js +51 -0
- package/src/block-renderer.js +7 -8
- package/src/boot.js +19 -19
- package/src/cli-definition.js +284 -0
- package/src/commands/audit.js +14 -12
- package/src/commands/boot.js +13 -14
- package/src/commands/claim.js +76 -82
- package/src/commands/fix.js +116 -37
- package/src/commands/inbox.js +61 -54
- package/src/commands/init.js +47 -53
- package/src/commands/log.js +58 -52
- package/src/commands/memo.js +59 -50
- package/src/commands/refresh.js +40 -36
- package/src/commands/rotate.js +26 -15
- package/src/commands/sync.js +17 -16
- package/src/index.js +1 -1
- package/src/issue-list-renderer.js +29 -28
- package/src/marker-migrator.js +8 -7
- package/src/marker-scanner.js +13 -8
- package/src/memo-writer.js +7 -6
- package/src/skill-roster.js +7 -3
- package/src/status.js +19 -0
- package/src/util/clock.js +13 -0
- package/src/util/wiki-dir.js +24 -0
- package/src/weekly-log.js +37 -37
- package/src/wiki-sync.js +164 -0
- package/src/build-repo.js +0 -20
- package/src/io.js +0 -77
- package/src/wiki-repo.js +0 -167
package/bin/fit-wiki.js
CHANGED
|
@@ -2,303 +2,66 @@
|
|
|
2
2
|
|
|
3
3
|
import "@forwardimpact/libpreflight/node22";
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { createDefaultRuntime } from "@forwardimpact/libutil/runtime";
|
|
6
|
+
import { GitClient } from "@forwardimpact/libutil/git-client";
|
|
7
|
+
import { createScriptConfig } from "@forwardimpact/libconfig";
|
|
6
8
|
import { createCli } from "@forwardimpact/libcli";
|
|
9
|
+
import { WikiSync } from "../src/wiki-sync.js";
|
|
10
|
+
import { resolveProjectRoot, resolveWikiRoot } from "../src/util/wiki-dir.js";
|
|
11
|
+
import { createDefinition } from "../src/cli-definition.js";
|
|
7
12
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
import { runPushCommand, runPullCommand } from "../src/commands/sync.js";
|
|
12
|
-
import { runBootCommand } from "../src/commands/boot.js";
|
|
13
|
-
import { runLogCommand } from "../src/commands/log.js";
|
|
14
|
-
import { runClaimCommand, runReleaseCommand } from "../src/commands/claim.js";
|
|
15
|
-
import { runInboxCommand } from "../src/commands/inbox.js";
|
|
16
|
-
import { runRotateCommand } from "../src/commands/rotate.js";
|
|
17
|
-
import { runAuditCommand } from "../src/commands/audit.js";
|
|
18
|
-
import { runFixCommand } from "../src/commands/fix.js";
|
|
19
|
-
|
|
20
|
-
const { version: VERSION } = JSON.parse(
|
|
21
|
-
readFileSync(new URL("../package.json", import.meta.url), "utf8"),
|
|
22
|
-
);
|
|
23
|
-
|
|
24
|
-
const wikiRootOpt = {
|
|
25
|
-
"wiki-root": {
|
|
26
|
-
type: "string",
|
|
27
|
-
description: "Override wiki root directory (default: wiki)",
|
|
28
|
-
},
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
const agentOpt = {
|
|
32
|
-
agent: {
|
|
33
|
-
type: "string",
|
|
34
|
-
description:
|
|
35
|
-
"Agent name (falls back to LIBEVAL_AGENT_PROFILE, then staff-engineer)",
|
|
36
|
-
default: process.env.LIBEVAL_AGENT_PROFILE || "staff-engineer",
|
|
37
|
-
},
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
const todayOpt = {
|
|
41
|
-
today: {
|
|
42
|
-
type: "string",
|
|
43
|
-
description: "Override today's ISO date (testing)",
|
|
44
|
-
},
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
const definition = {
|
|
48
|
-
name: "fit-wiki",
|
|
49
|
-
version: VERSION,
|
|
50
|
-
description: "Wiki lifecycle management for the Kata agent system",
|
|
51
|
-
commands: [
|
|
52
|
-
{
|
|
53
|
-
name: "boot",
|
|
54
|
-
description:
|
|
55
|
-
"Print on-boot digest (priorities, claims, storyboard items) as JSON",
|
|
56
|
-
options: {
|
|
57
|
-
...agentOpt,
|
|
58
|
-
...wikiRootOpt,
|
|
59
|
-
...todayOpt,
|
|
60
|
-
format: {
|
|
61
|
-
type: "string",
|
|
62
|
-
description: "Output format: json (default) or markdown",
|
|
63
|
-
},
|
|
64
|
-
},
|
|
65
|
-
},
|
|
66
|
-
{
|
|
67
|
-
name: "log",
|
|
68
|
-
description:
|
|
69
|
-
"Append a decision/note/done entry to the current weekly log",
|
|
70
|
-
args: "[subcommand]",
|
|
71
|
-
options: {
|
|
72
|
-
...agentOpt,
|
|
73
|
-
...wikiRootOpt,
|
|
74
|
-
...todayOpt,
|
|
75
|
-
surveyed: {
|
|
76
|
-
type: "string",
|
|
77
|
-
description: "Decision: routing levels surveyed",
|
|
78
|
-
},
|
|
79
|
-
chosen: { type: "string", description: "Decision: chosen action" },
|
|
80
|
-
rationale: { type: "string", description: "Decision: rationale" },
|
|
81
|
-
alternatives: { type: "string", description: "Decision: alternatives" },
|
|
82
|
-
field: { type: "string", description: "Note: field heading" },
|
|
83
|
-
body: { type: "string", description: "Note: field body" },
|
|
84
|
-
},
|
|
85
|
-
},
|
|
86
|
-
{
|
|
87
|
-
name: "claim",
|
|
88
|
-
description:
|
|
89
|
-
"Claim a target in MEMORY.md ## Active Claims (refuses duplicates)",
|
|
90
|
-
options: {
|
|
91
|
-
...agentOpt,
|
|
92
|
-
...wikiRootOpt,
|
|
93
|
-
...todayOpt,
|
|
94
|
-
target: {
|
|
95
|
-
type: "string",
|
|
96
|
-
description: "What is being claimed (spec id, PR id, etc.)",
|
|
97
|
-
},
|
|
98
|
-
branch: { type: "string", description: "Branch carrying the work" },
|
|
99
|
-
pr: { type: "string", description: "Optional PR id" },
|
|
100
|
-
"expires-at": {
|
|
101
|
-
type: "string",
|
|
102
|
-
description: "Override expiry ISO date (default claim+7d)",
|
|
103
|
-
},
|
|
104
|
-
},
|
|
105
|
-
},
|
|
106
|
-
{
|
|
107
|
-
name: "release",
|
|
108
|
-
description: "Release a claim (or all expired claims with --expired)",
|
|
109
|
-
options: {
|
|
110
|
-
...agentOpt,
|
|
111
|
-
...wikiRootOpt,
|
|
112
|
-
...todayOpt,
|
|
113
|
-
target: { type: "string", description: "Target to release" },
|
|
114
|
-
expired: {
|
|
115
|
-
type: "boolean",
|
|
116
|
-
description: "Release every row past expires_at",
|
|
117
|
-
},
|
|
118
|
-
},
|
|
119
|
-
},
|
|
120
|
-
{
|
|
121
|
-
name: "inbox",
|
|
122
|
-
description: "Triage the agent's Message Inbox (list/ack/promote/drop)",
|
|
123
|
-
args: "[subcommand]",
|
|
124
|
-
options: {
|
|
125
|
-
...agentOpt,
|
|
126
|
-
...wikiRootOpt,
|
|
127
|
-
...todayOpt,
|
|
128
|
-
index: {
|
|
129
|
-
type: "string",
|
|
130
|
-
description: "Bullet index (0-based) for ack/promote/drop",
|
|
131
|
-
},
|
|
132
|
-
owner: {
|
|
133
|
-
type: "string",
|
|
134
|
-
description: "Owner field when promoting (default: --agent)",
|
|
135
|
-
},
|
|
136
|
-
},
|
|
137
|
-
},
|
|
138
|
-
{
|
|
139
|
-
name: "rotate",
|
|
140
|
-
description: "Force-rotate the current weekly log to a sealed part",
|
|
141
|
-
options: {
|
|
142
|
-
...agentOpt,
|
|
143
|
-
...wikiRootOpt,
|
|
144
|
-
...todayOpt,
|
|
145
|
-
},
|
|
146
|
-
},
|
|
147
|
-
{
|
|
148
|
-
name: "audit",
|
|
149
|
-
description:
|
|
150
|
-
"Audit the wiki against the declarative rule catalogue (line and word budgets, headings, decision blocks, storyboards, claims)",
|
|
151
|
-
options: {
|
|
152
|
-
...wikiRootOpt,
|
|
153
|
-
...todayOpt,
|
|
154
|
-
format: {
|
|
155
|
-
type: "string",
|
|
156
|
-
description: "Output format: text (default) or json",
|
|
157
|
-
},
|
|
158
|
-
},
|
|
159
|
-
},
|
|
160
|
-
{
|
|
161
|
-
name: "fix",
|
|
162
|
-
description:
|
|
163
|
-
"Auto-fix wiki audit findings using an AI agent (technical-writer, Haiku)",
|
|
164
|
-
options: {
|
|
165
|
-
...wikiRootOpt,
|
|
166
|
-
...todayOpt,
|
|
167
|
-
},
|
|
168
|
-
},
|
|
169
|
-
{
|
|
170
|
-
name: "memo",
|
|
171
|
-
description: "Send a cross-team memo into a teammate's Message Inbox",
|
|
172
|
-
options: {
|
|
173
|
-
from: {
|
|
174
|
-
type: "string",
|
|
175
|
-
description:
|
|
176
|
-
"Sender agent name (falls back to LIBEVAL_AGENT_PROFILE env var)",
|
|
177
|
-
default: process.env.LIBEVAL_AGENT_PROFILE,
|
|
178
|
-
},
|
|
179
|
-
to: {
|
|
180
|
-
type: "string",
|
|
181
|
-
description:
|
|
182
|
-
'Target agent name, or "all" to broadcast (sender is skipped)',
|
|
183
|
-
},
|
|
184
|
-
message: {
|
|
185
|
-
type: "string",
|
|
186
|
-
description: "Memo text",
|
|
187
|
-
},
|
|
188
|
-
...wikiRootOpt,
|
|
189
|
-
},
|
|
190
|
-
},
|
|
191
|
-
{
|
|
192
|
-
name: "refresh",
|
|
193
|
-
description:
|
|
194
|
-
"Regenerate XmR and obstacle/experiment marker blocks in a storyboard",
|
|
195
|
-
args: "[storyboard-path]",
|
|
196
|
-
options: {
|
|
197
|
-
format: {
|
|
198
|
-
type: "string",
|
|
199
|
-
description: "Output format: (default off) or json",
|
|
200
|
-
},
|
|
201
|
-
},
|
|
202
|
-
},
|
|
203
|
-
{
|
|
204
|
-
name: "init",
|
|
205
|
-
description: "Bootstrap a wiki working tree and scaffold Active Claims",
|
|
206
|
-
options: {
|
|
207
|
-
...wikiRootOpt,
|
|
208
|
-
"skills-dir": {
|
|
209
|
-
type: "string",
|
|
210
|
-
description: "Override skills directory (default: .claude/skills)",
|
|
211
|
-
},
|
|
212
|
-
},
|
|
213
|
-
},
|
|
214
|
-
{
|
|
215
|
-
name: "push",
|
|
216
|
-
description: "Commit and push local wiki changes to the remote",
|
|
217
|
-
options: { ...wikiRootOpt },
|
|
218
|
-
},
|
|
219
|
-
{
|
|
220
|
-
name: "pull",
|
|
221
|
-
description: "Pull remote wiki changes into the local working tree",
|
|
222
|
-
options: { ...wikiRootOpt },
|
|
223
|
-
},
|
|
224
|
-
],
|
|
225
|
-
globalOptions: {
|
|
226
|
-
help: { type: "boolean", short: "h", description: "Show this help" },
|
|
227
|
-
version: { type: "boolean", description: "Show version" },
|
|
228
|
-
json: {
|
|
229
|
-
type: "boolean",
|
|
230
|
-
description: "Render --help output as JSON",
|
|
231
|
-
},
|
|
232
|
-
},
|
|
233
|
-
examples: [
|
|
234
|
-
"fit-wiki boot --agent staff-engineer",
|
|
235
|
-
'fit-wiki log decision --agent staff-engineer --surveyed "..." --chosen "..." --rationale "..."',
|
|
236
|
-
"fit-wiki claim --agent staff-engineer --target spec-NNNN --branch claude/...",
|
|
237
|
-
"fit-wiki release --agent staff-engineer --target spec-NNNN",
|
|
238
|
-
"fit-wiki inbox list --agent staff-engineer",
|
|
239
|
-
"fit-wiki rotate --agent staff-engineer",
|
|
240
|
-
"fit-wiki audit",
|
|
241
|
-
"fit-wiki fix",
|
|
242
|
-
'fit-wiki memo --from staff-engineer --to security-engineer --message "audit d642ff0c"',
|
|
243
|
-
"fit-wiki refresh",
|
|
244
|
-
"fit-wiki init",
|
|
245
|
-
"fit-wiki push",
|
|
246
|
-
"fit-wiki pull",
|
|
247
|
-
],
|
|
248
|
-
documentation: [
|
|
249
|
-
{
|
|
250
|
-
title: "Operate a Predictable Agent Team",
|
|
251
|
-
url: "https://www.forwardimpact.team/docs/libraries/predictable-team/index.md",
|
|
252
|
-
description:
|
|
253
|
-
"End-to-end guide to wiki memory, XmR charts, and team coordination.",
|
|
254
|
-
},
|
|
255
|
-
{
|
|
256
|
-
title: "Send a Memo or Update a Storyboard",
|
|
257
|
-
url: "https://www.forwardimpact.team/docs/libraries/predictable-team/wiki-operations/index.md",
|
|
258
|
-
description:
|
|
259
|
-
"Send cross-team memos, refresh storyboard charts, and sync the wiki.",
|
|
260
|
-
},
|
|
261
|
-
],
|
|
262
|
-
};
|
|
263
|
-
|
|
264
|
-
const cli = createCli(definition);
|
|
265
|
-
|
|
266
|
-
const COMMANDS = {
|
|
267
|
-
boot: runBootCommand,
|
|
268
|
-
log: runLogCommand,
|
|
269
|
-
claim: runClaimCommand,
|
|
270
|
-
release: runReleaseCommand,
|
|
271
|
-
inbox: runInboxCommand,
|
|
272
|
-
rotate: runRotateCommand,
|
|
273
|
-
audit: runAuditCommand,
|
|
274
|
-
fix: runFixCommand,
|
|
275
|
-
memo: runMemoCommand,
|
|
276
|
-
refresh: runRefreshCommand,
|
|
277
|
-
init: runInitCommand,
|
|
278
|
-
push: runPushCommand,
|
|
279
|
-
pull: runPullCommand,
|
|
280
|
-
};
|
|
13
|
+
// Commands that mutate or sync the remote wiki need a constructed WikiSync
|
|
14
|
+
// (and its config-backed token resolver); the rest run against the local tree.
|
|
15
|
+
const NEEDS_WIKI_SYNC = new Set(["claim", "release", "push", "pull", "init"]);
|
|
281
16
|
|
|
282
17
|
async function main() {
|
|
283
|
-
const
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
18
|
+
const runtime = createDefaultRuntime();
|
|
19
|
+
const { version } = JSON.parse(
|
|
20
|
+
runtime.fsSync.readFileSync(
|
|
21
|
+
new URL("../package.json", import.meta.url),
|
|
22
|
+
"utf8",
|
|
23
|
+
),
|
|
24
|
+
);
|
|
25
|
+
const definition = createDefinition(runtime.proc.env, version);
|
|
26
|
+
const cli = createCli(definition, { runtime });
|
|
27
|
+
|
|
28
|
+
const parsed = cli.parse(runtime.proc.argv.slice(2));
|
|
29
|
+
if (!parsed) return runtime.proc.exit(0); // --help / --version already printed
|
|
30
|
+
|
|
31
|
+
const { positionals } = parsed;
|
|
288
32
|
if (positionals.length === 0) {
|
|
289
33
|
cli.showHelp();
|
|
290
|
-
|
|
34
|
+
return runtime.proc.exit(0);
|
|
291
35
|
}
|
|
292
36
|
|
|
293
|
-
const
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
if (!handler) {
|
|
37
|
+
const command = positionals[0];
|
|
38
|
+
if (!definition.commands.some((c) => c.name === command)) {
|
|
297
39
|
cli.usageError(`unknown command "${command}"`);
|
|
298
|
-
|
|
40
|
+
return runtime.proc.exit(2);
|
|
299
41
|
}
|
|
300
42
|
|
|
301
|
-
|
|
43
|
+
const gitClient = new GitClient({ runtime });
|
|
44
|
+
let wikiSync;
|
|
45
|
+
if (NEEDS_WIKI_SYNC.has(command)) {
|
|
46
|
+
const projectRoot = resolveProjectRoot(runtime);
|
|
47
|
+
const wikiDir = resolveWikiRoot(runtime, parsed.values);
|
|
48
|
+
const config = await createScriptConfig("wiki");
|
|
49
|
+
wikiSync = new WikiSync({
|
|
50
|
+
runtime,
|
|
51
|
+
gitClient,
|
|
52
|
+
wikiDir,
|
|
53
|
+
parentDir: projectRoot,
|
|
54
|
+
resolveToken: () => config.ghToken(),
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const result = await cli.dispatch(parsed, {
|
|
59
|
+
deps: { runtime, wikiSync, gitClient },
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const envelope = result ?? { ok: true };
|
|
63
|
+
if (!envelope.ok && envelope.error) cli.usageError(envelope.error);
|
|
64
|
+
runtime.proc.exit(envelope.ok ? 0 : (envelope.code ?? 1));
|
|
302
65
|
}
|
|
303
66
|
|
|
304
67
|
main();
|
package/package.json
CHANGED
package/src/agent-roster.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import { readdirSync, statSync } from "node:fs";
|
|
2
1
|
import path from "node:path";
|
|
3
2
|
import { BROADCAST_TARGET } from "./constants.js";
|
|
4
3
|
|
|
5
|
-
/**
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
4
|
+
/**
|
|
5
|
+
* List all agent markdown files in the agents directory, returning agent names
|
|
6
|
+
* and summary paths.
|
|
7
|
+
* @param {{agentsDir: string, wikiRoot: string}} dirs
|
|
8
|
+
* @param {object} fs - Sync filesystem surface (`runtime.fsSync`).
|
|
9
|
+
*/
|
|
10
|
+
export function listAgents({ agentsDir, wikiRoot }, fs) {
|
|
10
11
|
const entries = fs.readdirSync(agentsDir);
|
|
11
12
|
const agents = [];
|
|
12
13
|
|
package/src/audit/rules.js
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
WEEKLY_LOG_WORD_BUDGET,
|
|
13
13
|
} from "../constants.js";
|
|
14
14
|
import { PRIORITY_HEADER_RE, WEEKLY_LOG_H1_RE } from "./scopes.js";
|
|
15
|
+
import { STATUS_ROW_RULES } from "./status-row.js";
|
|
15
16
|
|
|
16
17
|
const PRIORITY_INDEX_HEADING_RE = new RegExp(
|
|
17
18
|
`^${PRIORITY_INDEX_HEADING}$`,
|
|
@@ -483,6 +484,10 @@ export const RULES = [
|
|
|
483
484
|
hint: "every '<!-- obstacles:* -->' or '<!-- experiments:* -->' needs a matching close marker",
|
|
484
485
|
},
|
|
485
486
|
|
|
487
|
+
// -- STATUS.md rows (per-migration-unit sub-row schema) --
|
|
488
|
+
|
|
489
|
+
...STATUS_ROW_RULES,
|
|
490
|
+
|
|
486
491
|
// -- Stray files --
|
|
487
492
|
|
|
488
493
|
{
|
package/src/audit/scopes.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { readFileSync, existsSync, readdirSync } from "node:fs";
|
|
2
1
|
import path from "node:path";
|
|
2
|
+
import { yearMonth } from "@forwardimpact/libutil";
|
|
3
3
|
import { parseClaims } from "../active-claims.js";
|
|
4
4
|
import { PRIORITY_INDEX_HEADING } from "../constants.js";
|
|
5
5
|
|
|
@@ -11,7 +11,7 @@ export const WEEKLY_LOG_H1_RE =
|
|
|
11
11
|
export const PRIORITY_HEADER_RE =
|
|
12
12
|
/^\|\s*Item\s*\|\s*Agents\s*\|\s*Owner\s*\|\s*Status\s*\|\s*Added\s*\|/m;
|
|
13
13
|
|
|
14
|
-
const EXCLUDED_BASES = new Set(["MEMORY.md", "Home.md"
|
|
14
|
+
const EXCLUDED_BASES = new Set(["MEMORY.md", "Home.md"]);
|
|
15
15
|
const NON_SUMMARY_PREFIXES = [
|
|
16
16
|
"storyboard-",
|
|
17
17
|
"downstream-",
|
|
@@ -20,9 +20,10 @@ const NON_SUMMARY_PREFIXES = [
|
|
|
20
20
|
"fit-trace-",
|
|
21
21
|
];
|
|
22
22
|
|
|
23
|
-
function listMdFiles(wikiRoot) {
|
|
24
|
-
if (!existsSync(wikiRoot)) return [];
|
|
25
|
-
return
|
|
23
|
+
function listMdFiles(wikiRoot, fs) {
|
|
24
|
+
if (!fs.existsSync(wikiRoot)) return [];
|
|
25
|
+
return fs
|
|
26
|
+
.readdirSync(wikiRoot)
|
|
26
27
|
.filter((e) => e.endsWith(".md"))
|
|
27
28
|
.map((e) => path.join(wikiRoot, e));
|
|
28
29
|
}
|
|
@@ -46,8 +47,8 @@ function countWords(text) {
|
|
|
46
47
|
return count;
|
|
47
48
|
}
|
|
48
49
|
|
|
49
|
-
function loadFile(filePath) {
|
|
50
|
-
const text = readFileSync(filePath, "utf-8");
|
|
50
|
+
function loadFile(filePath, fs) {
|
|
51
|
+
const text = fs.readFileSync(filePath, "utf-8");
|
|
51
52
|
const fileLines = text.split("\n");
|
|
52
53
|
const h2s = [];
|
|
53
54
|
for (const line of fileLines) {
|
|
@@ -69,44 +70,86 @@ function loadFile(filePath) {
|
|
|
69
70
|
};
|
|
70
71
|
}
|
|
71
72
|
|
|
72
|
-
function classifyFile(filePath) {
|
|
73
|
+
function classifyFile(filePath, fs) {
|
|
73
74
|
const base = path.basename(filePath);
|
|
74
75
|
if (EXCLUDED_BASES.has(base)) return null;
|
|
76
|
+
// STATUS.md is loaded separately (loadStatus) and audited via the dedicated
|
|
77
|
+
// `status-row` scope — skip the per-file classification so it is not treated
|
|
78
|
+
// as a stray file.
|
|
79
|
+
if (base === "STATUS.md") return null;
|
|
75
80
|
if (NON_SUMMARY_PREFIXES.some((p) => base.startsWith(p))) return null;
|
|
76
81
|
if (WEEKLY_LOG_NAME_RE.test(base)) {
|
|
77
|
-
return { kind: "weekly-log-main", subject: loadFile(filePath) };
|
|
82
|
+
return { kind: "weekly-log-main", subject: loadFile(filePath, fs) };
|
|
78
83
|
}
|
|
79
84
|
if (WEEKLY_LOG_PART_NAME_RE.test(base)) {
|
|
80
|
-
return { kind: "weekly-log-part", subject: loadFile(filePath) };
|
|
85
|
+
return { kind: "weekly-log-part", subject: loadFile(filePath, fs) };
|
|
81
86
|
}
|
|
82
|
-
const subject = loadFile(filePath);
|
|
87
|
+
const subject = loadFile(filePath, fs);
|
|
83
88
|
const kind = SUMMARY_H1_RE.test(subject.firstLine) ? "summary" : "stray";
|
|
84
89
|
return { kind, subject };
|
|
85
90
|
}
|
|
86
91
|
|
|
87
|
-
function loadMemory(wikiRoot) {
|
|
92
|
+
function loadMemory(wikiRoot, fs) {
|
|
88
93
|
const filePath = path.join(wikiRoot, "MEMORY.md");
|
|
89
|
-
const exists = existsSync(filePath);
|
|
94
|
+
const exists = fs.existsSync(filePath);
|
|
90
95
|
return {
|
|
91
96
|
path: filePath,
|
|
92
|
-
text: exists ? readFileSync(filePath, "utf-8") : "",
|
|
97
|
+
text: exists ? fs.readFileSync(filePath, "utf-8") : "",
|
|
93
98
|
exists,
|
|
94
99
|
};
|
|
95
100
|
}
|
|
96
101
|
|
|
97
|
-
function
|
|
98
|
-
const
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
102
|
+
function loadStatus(wikiRoot, fs) {
|
|
103
|
+
const filePath = path.join(wikiRoot, "STATUS.md");
|
|
104
|
+
const exists = fs.existsSync(filePath);
|
|
105
|
+
return {
|
|
106
|
+
path: filePath,
|
|
107
|
+
text: exists ? fs.readFileSync(filePath, "utf-8") : "",
|
|
108
|
+
exists,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Parse the rows inside STATUS.md's fenced block into audit subjects. Lines
|
|
114
|
+
* outside the ``` fence (header prose) and blank lines are skipped.
|
|
115
|
+
* @param {string} statusText - The full STATUS.md contents.
|
|
116
|
+
* @returns {Array<{lineNo: number, text: string, cells: string[], id: string, phase: string, status: string}>}
|
|
117
|
+
*/
|
|
118
|
+
function parseStatusRows(statusText) {
|
|
119
|
+
const lines = statusText.split("\n");
|
|
120
|
+
const rows = [];
|
|
121
|
+
let inFence = false;
|
|
122
|
+
for (let i = 0; i < lines.length; i++) {
|
|
123
|
+
const line = lines[i];
|
|
124
|
+
if (line.trimStart().startsWith("```")) {
|
|
125
|
+
inFence = !inFence;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (!inFence || line.trim() === "") continue;
|
|
129
|
+
const cells = line.split("\t");
|
|
130
|
+
rows.push({
|
|
131
|
+
lineNo: i + 1,
|
|
132
|
+
text: line,
|
|
133
|
+
cells,
|
|
134
|
+
id: cells[0],
|
|
135
|
+
phase: cells[1],
|
|
136
|
+
status: cells[2],
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
return rows;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function loadStoryboard(wikiRoot, today, fs) {
|
|
143
|
+
const ym = yearMonth(today);
|
|
144
|
+
const filePath = path.join(wikiRoot, `storyboard-${ym}.md`);
|
|
145
|
+
const exists = fs.existsSync(filePath);
|
|
146
|
+
const text = exists ? fs.readFileSync(filePath, "utf-8") : "";
|
|
104
147
|
return {
|
|
105
148
|
path: filePath,
|
|
106
149
|
text,
|
|
107
150
|
fileLines: text.split("\n"),
|
|
108
151
|
exists,
|
|
109
|
-
yearMonth:
|
|
152
|
+
yearMonth: ym,
|
|
110
153
|
lines: countLines(text),
|
|
111
154
|
words: countWords(text),
|
|
112
155
|
};
|
|
@@ -168,6 +211,11 @@ const SCOPE_RESOLVERS = {
|
|
|
168
211
|
path: ctx.memory.path,
|
|
169
212
|
})),
|
|
170
213
|
storyboard: (ctx) => [ctx.storyboard],
|
|
214
|
+
"status-row": (ctx) =>
|
|
215
|
+
parseStatusRows(ctx.status.text).map((r) => ({
|
|
216
|
+
...r,
|
|
217
|
+
path: ctx.status.path,
|
|
218
|
+
})),
|
|
171
219
|
"stray-file": (ctx) => ctx.subjects.stray,
|
|
172
220
|
};
|
|
173
221
|
|
|
@@ -178,23 +226,28 @@ export function resolveScope(scopeKey, ctx) {
|
|
|
178
226
|
return resolver(ctx);
|
|
179
227
|
}
|
|
180
228
|
|
|
181
|
-
/**
|
|
182
|
-
|
|
229
|
+
/**
|
|
230
|
+
* Build the audit context: classifies and loads every wiki file once.
|
|
231
|
+
* @param {{wikiRoot: string, today: string, fs: object}} options
|
|
232
|
+
* `fs` is the sync filesystem surface (`runtime.fsSync`).
|
|
233
|
+
*/
|
|
234
|
+
export function buildContext({ wikiRoot, today, fs }) {
|
|
183
235
|
const subjects = {
|
|
184
236
|
summary: [],
|
|
185
237
|
"weekly-log-main": [],
|
|
186
238
|
"weekly-log-part": [],
|
|
187
239
|
stray: [],
|
|
188
240
|
};
|
|
189
|
-
for (const file of listMdFiles(wikiRoot)) {
|
|
190
|
-
const classified = classifyFile(file);
|
|
241
|
+
for (const file of listMdFiles(wikiRoot, fs)) {
|
|
242
|
+
const classified = classifyFile(file, fs);
|
|
191
243
|
if (classified) subjects[classified.kind].push(classified.subject);
|
|
192
244
|
}
|
|
193
245
|
return {
|
|
194
246
|
wikiRoot,
|
|
195
247
|
today,
|
|
196
248
|
subjects,
|
|
197
|
-
memory: loadMemory(wikiRoot),
|
|
198
|
-
|
|
249
|
+
memory: loadMemory(wikiRoot, fs),
|
|
250
|
+
status: loadStatus(wikiRoot, fs),
|
|
251
|
+
storyboard: loadStoryboard(wikiRoot, today, fs),
|
|
199
252
|
};
|
|
200
253
|
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { STATUS_ID_REGEX } from "../status.js";
|
|
2
|
+
|
|
3
|
+
// Validate every row inside wiki/STATUS.md's code fence against the
|
|
4
|
+
// `{id}<TAB>{phase}<TAB>{status}` shape. Rows are resolved by the `status-row`
|
|
5
|
+
// scope in scopes.js; each subject carries `{ cells, id, phase, status, text }`.
|
|
6
|
+
|
|
7
|
+
const PHASES = new Set(["spec", "design", "plan"]);
|
|
8
|
+
const STATUSES = new Set(["draft", "approved", "implemented", "cancelled"]);
|
|
9
|
+
|
|
10
|
+
const hasThreeCells = (s) => s.cells.length === 3;
|
|
11
|
+
|
|
12
|
+
export const STATUS_ROW_RULES = [
|
|
13
|
+
{
|
|
14
|
+
id: "status-row.shape",
|
|
15
|
+
scope: "status-row",
|
|
16
|
+
severity: "fail",
|
|
17
|
+
check: (s) =>
|
|
18
|
+
hasThreeCells(s) ? null : { actual: s.cells.length, text: s.text },
|
|
19
|
+
message: (_s, r) =>
|
|
20
|
+
`${r.actual} tab-separated field(s), expected 3: "${r.text}"`,
|
|
21
|
+
hint: "each STATUS row is `{id}<TAB>{phase}<TAB>{status}`",
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
id: "status-row.id-format",
|
|
25
|
+
scope: "status-row",
|
|
26
|
+
severity: "fail",
|
|
27
|
+
when: hasThreeCells,
|
|
28
|
+
check: (s) => (STATUS_ID_REGEX.test(s.id) ? null : { id: s.id }),
|
|
29
|
+
message: (_s, r) => `Bad id '${r.id}' (expected ^\\d{4}(/[a-z0-9-]+)?$)`,
|
|
30
|
+
hint: "spec ids are four digits; a sub-row appends `/<unit>` (e.g. 1370/libutil)",
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
id: "status-row.phase",
|
|
34
|
+
scope: "status-row",
|
|
35
|
+
severity: "fail",
|
|
36
|
+
when: hasThreeCells,
|
|
37
|
+
check: (s) => (PHASES.has(s.phase) ? null : { phase: s.phase }),
|
|
38
|
+
message: (_s, r) => `Bad phase '${r.phase}' (expected spec|design|plan)`,
|
|
39
|
+
hint: "phase is one of spec, design, plan",
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
id: "status-row.status",
|
|
43
|
+
scope: "status-row",
|
|
44
|
+
severity: "fail",
|
|
45
|
+
when: hasThreeCells,
|
|
46
|
+
check: (s) => (STATUSES.has(s.status) ? null : { status: s.status }),
|
|
47
|
+
message: (_s, r) =>
|
|
48
|
+
`Bad status '${r.status}' (expected draft|approved|implemented|cancelled)`,
|
|
49
|
+
hint: "status is one of draft, approved, implemented, cancelled",
|
|
50
|
+
},
|
|
51
|
+
];
|
package/src/block-renderer.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
2
1
|
import path from "node:path";
|
|
3
2
|
import { analyze, renderChart, MIN_POINTS } from "@forwardimpact/libxmr";
|
|
4
3
|
|
|
@@ -11,13 +10,13 @@ export class BlockRenderError extends Error {
|
|
|
11
10
|
}
|
|
12
11
|
}
|
|
13
12
|
|
|
14
|
-
/**
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
}) {
|
|
13
|
+
/**
|
|
14
|
+
* Render an XmR chart block for a metric by reading its CSV and producing
|
|
15
|
+
* markdown lines.
|
|
16
|
+
* @param {{metric: string, csvPath: string, projectRoot: string, fs: object}} options
|
|
17
|
+
* `fs` is the sync filesystem surface (`runtime.fsSync`).
|
|
18
|
+
*/
|
|
19
|
+
export function renderBlock({ metric, csvPath, projectRoot, fs }) {
|
|
21
20
|
const fullPath = path.resolve(projectRoot, csvPath);
|
|
22
21
|
let csvText;
|
|
23
22
|
try {
|