@forwardimpact/libwiki 0.2.6 → 0.2.7
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/package.json +1 -1
- package/src/audit/rules.js +28 -4
- package/src/audit/scopes.js +2 -0
- package/src/block-renderer.js +2 -9
- package/src/commands/refresh.js +39 -4
- package/src/constants.js +9 -5
- package/src/issue-list-renderer.js +25 -8
- package/src/marker-scanner.js +8 -4
package/package.json
CHANGED
package/src/audit/rules.js
CHANGED
|
@@ -4,6 +4,8 @@ import {
|
|
|
4
4
|
DECISION_HEADING,
|
|
5
5
|
MEMO_INBOX_MARKER,
|
|
6
6
|
PRIORITY_INDEX_HEADING,
|
|
7
|
+
STORYBOARD_LINE_BUDGET,
|
|
8
|
+
STORYBOARD_WORD_BUDGET,
|
|
7
9
|
SUMMARY_LINE_BUDGET,
|
|
8
10
|
SUMMARY_WORD_BUDGET,
|
|
9
11
|
WEEKLY_LOG_LINE_BUDGET,
|
|
@@ -23,11 +25,15 @@ const CLAIMS_HEADER_RE =
|
|
|
23
25
|
const CLAIMS_SEPARATOR_RE =
|
|
24
26
|
/^\|\s*---\s*\|\s*---\s*\|\s*---\s*\|\s*---\s*\|\s*---\s*\|\s*---\s*\|/m;
|
|
25
27
|
const ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
26
|
-
|
|
27
|
-
|
|
28
|
+
// Marker regexes (mirror of marker-scanner.js): tolerate optional trailing
|
|
29
|
+
// text inside the marker so an open marker can carry an inline notice like
|
|
30
|
+
// "Do not edit. Auto-generated." without breaking the audit's balance check.
|
|
31
|
+
const XMR_OPEN_RE = /^<!--\s*xmr:([^:\s]+):(\S+)(?:\s+[^>]*?)?\s*-->\s*$/;
|
|
32
|
+
const XMR_CLOSE_RE = /^<!--\s*\/xmr(?:\s+[^>]*?)?\s*-->\s*$/;
|
|
28
33
|
const ISSUE_OPEN_RE =
|
|
29
|
-
/^<!--\s*(obstacles|experiments):(open|closed)(?::(\d+d))?\s*-->\s*$/;
|
|
30
|
-
const ISSUE_CLOSE_RE =
|
|
34
|
+
/^<!--\s*(obstacles|experiments):(open|closed)(?::(\d+d))?(?:\s+[^>]*?)?\s*-->\s*$/;
|
|
35
|
+
const ISSUE_CLOSE_RE =
|
|
36
|
+
/^<!--\s*\/(obstacles|experiments)(?:\s+[^>]*?)?\s*-->\s*$/;
|
|
31
37
|
|
|
32
38
|
// improvement-coach is the storyboard facilitator and carries no domain
|
|
33
39
|
// metrics; only the five domain agents need their own H3.
|
|
@@ -419,6 +425,24 @@ export const RULES = [
|
|
|
419
425
|
check: allRequiredLines(AGENT_H3_REQUIREMENTS),
|
|
420
426
|
message: (s, r) => `storyboard: ${s.path} missing '### ${r.label}' H3`,
|
|
421
427
|
},
|
|
428
|
+
{
|
|
429
|
+
id: "storyboard.line-budget",
|
|
430
|
+
scope: "storyboard",
|
|
431
|
+
severity: "fail",
|
|
432
|
+
when: storyboardExists,
|
|
433
|
+
check: lineBudget(STORYBOARD_LINE_BUDGET),
|
|
434
|
+
message: (s, r) =>
|
|
435
|
+
`storyboard: ${s.path} has ${r.value} lines (limit ${STORYBOARD_LINE_BUDGET})`,
|
|
436
|
+
},
|
|
437
|
+
{
|
|
438
|
+
id: "storyboard.word-budget",
|
|
439
|
+
scope: "storyboard",
|
|
440
|
+
severity: "fail",
|
|
441
|
+
when: storyboardExists,
|
|
442
|
+
check: wordBudget(STORYBOARD_WORD_BUDGET),
|
|
443
|
+
message: (s, r) =>
|
|
444
|
+
`storyboard: ${s.path} has ${r.value} words (limit ${STORYBOARD_WORD_BUDGET})`,
|
|
445
|
+
},
|
|
422
446
|
{
|
|
423
447
|
id: "storyboard.markers-balanced.xmr",
|
|
424
448
|
scope: "storyboard",
|
package/src/audit/scopes.js
CHANGED
package/src/block-renderer.js
CHANGED
|
@@ -32,11 +32,8 @@ export function renderBlock({
|
|
|
32
32
|
throw new BlockRenderError(`metric-not-found: ${metric}`);
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
const latestValue = m.latest?.value ?? m.values[m.values.length - 1] ?? "—";
|
|
36
|
-
const status = m.status;
|
|
37
|
-
|
|
38
35
|
let chartLines;
|
|
39
|
-
if (status === "insufficient_data") {
|
|
36
|
+
if (m.status === "insufficient_data") {
|
|
40
37
|
chartLines = [
|
|
41
38
|
`Insufficient data: ${m.n} points (need at least ${MIN_POINTS}).`,
|
|
42
39
|
];
|
|
@@ -45,16 +42,12 @@ export function renderBlock({
|
|
|
45
42
|
chartLines = chartText.split("\n");
|
|
46
43
|
}
|
|
47
44
|
|
|
48
|
-
const signalLine = formatSignals(m.signals);
|
|
49
|
-
|
|
50
45
|
return [
|
|
51
|
-
`**Latest:** ${latestValue} · **Status:** ${status}`,
|
|
52
|
-
"",
|
|
53
46
|
"```",
|
|
54
47
|
...chartLines,
|
|
55
48
|
"```",
|
|
56
49
|
"",
|
|
57
|
-
`**Signals:** ${
|
|
50
|
+
`**Signals:** ${formatSignals(m.signals)}`,
|
|
58
51
|
];
|
|
59
52
|
}
|
|
60
53
|
|
package/src/commands/refresh.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import fsAsync from "node:fs/promises";
|
|
4
5
|
import { Finder } from "@forwardimpact/libutil";
|
|
6
|
+
import { createScriptConfig } from "@forwardimpact/libconfig";
|
|
5
7
|
import { scanMarkers } from "../marker-scanner.js";
|
|
6
8
|
import { renderBlock, BlockRenderError } from "../block-renderer.js";
|
|
7
|
-
import { renderIssueList } from "../issue-list-renderer.js";
|
|
9
|
+
import { renderIssueList, parseRepoSlug } from "../issue-list-renderer.js";
|
|
8
10
|
|
|
9
11
|
function currentStoryboardPath() {
|
|
10
12
|
const now = new Date();
|
|
@@ -13,7 +15,17 @@ function currentStoryboardPath() {
|
|
|
13
15
|
return `wiki/storyboard-${yyyy}-M${mm}.md`;
|
|
14
16
|
}
|
|
15
17
|
|
|
16
|
-
function
|
|
18
|
+
function deriveParentRepo(parentDir) {
|
|
19
|
+
if (process.env.FIT_GH_REPO) return process.env.FIT_GH_REPO;
|
|
20
|
+
const r = spawnSync("git", ["-C", parentDir, "remote", "get-url", "origin"], {
|
|
21
|
+
encoding: "utf-8",
|
|
22
|
+
stdio: "pipe",
|
|
23
|
+
});
|
|
24
|
+
if (r.status !== 0) return null;
|
|
25
|
+
return parseRepoSlug(r.stdout);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function renderForBlock(block, projectRoot, ghContext) {
|
|
17
29
|
if (block.kind === "xmr") {
|
|
18
30
|
return renderBlock({
|
|
19
31
|
metric: block.metric,
|
|
@@ -26,6 +38,9 @@ function renderForBlock(block, projectRoot) {
|
|
|
26
38
|
topic: block.topic,
|
|
27
39
|
state: block.state,
|
|
28
40
|
window: block.window,
|
|
41
|
+
cwd: ghContext.cwd,
|
|
42
|
+
repo: ghContext.repo,
|
|
43
|
+
token: ghContext.token,
|
|
29
44
|
});
|
|
30
45
|
}
|
|
31
46
|
return null;
|
|
@@ -40,7 +55,7 @@ function spliceBlock(lines, block, rendered) {
|
|
|
40
55
|
}
|
|
41
56
|
|
|
42
57
|
/** Re-render XmR chart blocks and issue-list blocks in a storyboard file. */
|
|
43
|
-
export function runRefreshCommand(values, args, _cli) {
|
|
58
|
+
export async function runRefreshCommand(values, args, _cli) {
|
|
44
59
|
const logger = { debug() {} };
|
|
45
60
|
const finder = new Finder(fsAsync, logger, process);
|
|
46
61
|
const projectRoot = finder.findProjectRoot(process.cwd());
|
|
@@ -53,13 +68,33 @@ export function runRefreshCommand(values, args, _cli) {
|
|
|
53
68
|
const blocks = scanMarkers(text);
|
|
54
69
|
if (blocks.length === 0) return;
|
|
55
70
|
|
|
71
|
+
const config = await createScriptConfig("wiki");
|
|
72
|
+
let token = null;
|
|
73
|
+
try {
|
|
74
|
+
token = config.ghToken();
|
|
75
|
+
} catch {
|
|
76
|
+
// Missing token is non-fatal; issue-list renders will fail with a stderr
|
|
77
|
+
// warning and the block will collapse to the notice line.
|
|
78
|
+
}
|
|
79
|
+
// Spawn `gh` from the project root so it resolves the monorepo's origin
|
|
80
|
+
// instead of whatever git context the caller's cwd happens to be in (the
|
|
81
|
+
// wiki sibling repo, a subagent worktree, a service dir, etc.). Also
|
|
82
|
+
// resolve an explicit owner/repo slug so `gh` works when origin has been
|
|
83
|
+
// rewritten to a proxy URL (sandbox environments) — `FIT_GH_REPO` env
|
|
84
|
+
// overrides the parsed origin.
|
|
85
|
+
const ghContext = {
|
|
86
|
+
cwd: projectRoot,
|
|
87
|
+
repo: deriveParentRepo(projectRoot),
|
|
88
|
+
token,
|
|
89
|
+
};
|
|
90
|
+
|
|
56
91
|
const lines = text.split("\n");
|
|
57
92
|
let spliced = false;
|
|
58
93
|
|
|
59
94
|
for (let i = blocks.length - 1; i >= 0; i--) {
|
|
60
95
|
const block = blocks[i];
|
|
61
96
|
try {
|
|
62
|
-
const rendered = renderForBlock(block, projectRoot);
|
|
97
|
+
const rendered = renderForBlock(block, projectRoot, ghContext);
|
|
63
98
|
if (!rendered) continue;
|
|
64
99
|
spliceBlock(lines, block, rendered);
|
|
65
100
|
spliced = true;
|
package/src/constants.js
CHANGED
|
@@ -13,10 +13,14 @@ export const PRIORITY_INDEX_TABLE_HEADER =
|
|
|
13
13
|
"| Item | Agents | Owner | Status | Added |";
|
|
14
14
|
export const DECISION_HEADING = "### Decision";
|
|
15
15
|
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
16
|
+
// Unified budgets for the three audited surfaces (summary, weekly-log,
|
|
17
|
+
// storyboard). They share the same numeric limits today so the
|
|
18
|
+
// context-tax floor is symmetric across surfaces; each surface keeps
|
|
19
|
+
// its own audit rule pair so the limits can diverge later if the
|
|
20
|
+
// context-tax model says one surface should be looser or tighter.
|
|
21
|
+
export const SUMMARY_LINE_BUDGET = 496;
|
|
22
|
+
export const SUMMARY_WORD_BUDGET = 2048;
|
|
19
23
|
export const WEEKLY_LOG_LINE_BUDGET = 496;
|
|
20
|
-
export const SUMMARY_LINE_BUDGET = 72;
|
|
21
24
|
export const WEEKLY_LOG_WORD_BUDGET = 6400;
|
|
22
|
-
export const
|
|
25
|
+
export const STORYBOARD_LINE_BUDGET = 496;
|
|
26
|
+
export const STORYBOARD_WORD_BUDGET = 6400;
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
2
|
|
|
3
|
-
function defaultGh(args) {
|
|
3
|
+
function defaultGh(args, options) {
|
|
4
|
+
const env = options?.token
|
|
5
|
+
? { ...process.env, GH_TOKEN: options.token }
|
|
6
|
+
: undefined;
|
|
4
7
|
return spawnSync("gh", args, {
|
|
5
8
|
encoding: "utf-8",
|
|
6
9
|
stdio: ["ignore", "pipe", "pipe"],
|
|
10
|
+
cwd: options?.cwd,
|
|
11
|
+
env,
|
|
7
12
|
});
|
|
8
13
|
}
|
|
9
14
|
|
|
@@ -13,18 +18,30 @@ function daysAgo(today, n) {
|
|
|
13
18
|
return d.toISOString().slice(0, 10);
|
|
14
19
|
}
|
|
15
20
|
|
|
16
|
-
/**
|
|
21
|
+
/** Parse `owner/repo` from a git origin URL. Tolerates http(s), ssh, and proxy-rewritten URLs (e.g. `http://host/git/owner/repo`) by taking the last two path segments after stripping `.git`. Returns null when nothing parseable is found. */
|
|
22
|
+
export function parseRepoSlug(originUrl) {
|
|
23
|
+
if (!originUrl) return null;
|
|
24
|
+
const stripped = originUrl.trim().replace(/\.git$/, "");
|
|
25
|
+
const match = stripped.match(/([^/:]+)\/([^/:]+)$/);
|
|
26
|
+
if (!match) return null;
|
|
27
|
+
return `${match[1]}/${match[2]}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Render an issue-list block for an obstacles/experiments marker. Returns markdown lines. `cwd` should be the parent monorepo's project root so `gh` resolves the correct origin; `repo` is an explicit `owner/name` slug used when the origin remote is unparseable by `gh` (e.g. sandbox proxy URLs); `token` is the resolved GH token (e.g. via `Config.ghToken()`). */
|
|
17
31
|
export function renderIssueList({
|
|
18
32
|
topic,
|
|
19
33
|
state,
|
|
20
34
|
window,
|
|
35
|
+
cwd,
|
|
36
|
+
repo,
|
|
37
|
+
token,
|
|
21
38
|
today = new Date(),
|
|
22
39
|
gh = defaultGh,
|
|
23
40
|
}) {
|
|
24
41
|
const ghState = state === "closed" ? "closed" : "open";
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
42
|
+
const args = ["issue", "list"];
|
|
43
|
+
if (repo) args.push("--repo", repo);
|
|
44
|
+
args.push(
|
|
28
45
|
"--label",
|
|
29
46
|
topic.replace(/s$/, ""),
|
|
30
47
|
"--state",
|
|
@@ -33,7 +50,8 @@ export function renderIssueList({
|
|
|
33
50
|
"number,title,labels,closedAt",
|
|
34
51
|
"--limit",
|
|
35
52
|
"100",
|
|
36
|
-
|
|
53
|
+
);
|
|
54
|
+
const result = gh(args, { cwd, token });
|
|
37
55
|
if (result.status !== 0) {
|
|
38
56
|
process.stderr.write(
|
|
39
57
|
`refresh: gh issue list failed for ${topic}:${state}\n`,
|
|
@@ -62,8 +80,7 @@ export function renderIssueList({
|
|
|
62
80
|
|
|
63
81
|
const lines = [];
|
|
64
82
|
for (const issue of issues) {
|
|
65
|
-
|
|
66
|
-
lines.push(`- **${tag} #${issue.number} — ${issue.title}**`);
|
|
83
|
+
lines.push(`- #${issue.number} ${issue.title}`);
|
|
67
84
|
}
|
|
68
85
|
return lines;
|
|
69
86
|
}
|
package/src/marker-scanner.js
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
|
-
|
|
1
|
+
// Markers tolerate optional trailing text after the tag (typically an inline
|
|
2
|
+
// "Do not edit. Generated from fit-wiki refresh." notice), so an open or close
|
|
3
|
+
// marker can carry its own warning without needing a separate notice line.
|
|
4
|
+
const XMR_OPEN_RE = /^<!--\s*xmr:([^:\s]+):(\S+)(?:\s+[^>]*?)?\s*-->\s*$/;
|
|
2
5
|
const ISSUE_OPEN_RE =
|
|
3
|
-
/^<!--\s*(obstacles|experiments):(open|closed)(?::(\d+d))?\s*-->\s*$/;
|
|
4
|
-
const XMR_CLOSE_RE = /^<!--\s*\/xmr
|
|
5
|
-
const ISSUE_CLOSE_RE =
|
|
6
|
+
/^<!--\s*(obstacles|experiments):(open|closed)(?::(\d+d))?(?:\s+[^>]*?)?\s*-->\s*$/;
|
|
7
|
+
const XMR_CLOSE_RE = /^<!--\s*\/xmr(?:\s+[^>]*?)?\s*-->\s*$/;
|
|
8
|
+
const ISSUE_CLOSE_RE =
|
|
9
|
+
/^<!--\s*\/(obstacles|experiments)(?:\s+[^>]*?)?\s*-->\s*$/;
|
|
6
10
|
|
|
7
11
|
function openLabel(open) {
|
|
8
12
|
return open.kind === "xmr" ? open.metric : open.topic;
|