@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forwardimpact/libwiki",
3
- "version": "0.2.6",
3
+ "version": "0.2.7",
4
4
  "description": "Wiki lifecycle primitives — stable memory for agent teams so coordination persists across sessions.",
5
5
  "keywords": [
6
6
  "wiki",
@@ -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
- const XMR_OPEN_RE = /^<!--\s*xmr:([^:\s]+):([^\s]+)\s*-->\s*$/;
27
- const XMR_CLOSE_RE = /^<!--\s*\/xmr\s*-->\s*$/;
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 = /^<!--\s*\/(obstacles|experiments)\s*-->\s*$/;
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",
@@ -107,6 +107,8 @@ function loadStoryboard(wikiRoot, today) {
107
107
  fileLines: text.split("\n"),
108
108
  exists,
109
109
  yearMonth: `${yyyy}-M${mm}`,
110
+ lines: countLines(text),
111
+ words: countWords(text),
110
112
  };
111
113
  }
112
114
 
@@ -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:** ${signalLine}`,
50
+ `**Signals:** ${formatSignals(m.signals)}`,
58
51
  ];
59
52
  }
60
53
 
@@ -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 renderForBlock(block, projectRoot) {
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
- // Cap derivation: ≤2.5% of a 1M-token context window = 25k tokens;
17
- // converted via ≈42 tokens/line empirical proxy, then 64-aligned.
18
- // See spec 1060 design-a.md § Decision area 2 for the full anchor.
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 SUMMARY_WORD_BUDGET = 12800;
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
- /** Render an issue-list block for an obstacles/experiments marker. Returns markdown lines. */
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 result = gh([
26
- "issue",
27
- "list",
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
- const tag = topic === "experiments" ? "Exp" : "Obs";
66
- lines.push(`- **${tag} #${issue.number} — ${issue.title}**`);
83
+ lines.push(`- #${issue.number} ${issue.title}`);
67
84
  }
68
85
  return lines;
69
86
  }
@@ -1,8 +1,12 @@
1
- const XMR_OPEN_RE = /^<!--\s*xmr:([^:\s]+):([^\s]+)\s*-->\s*$/;
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\s*-->\s*$/;
5
- const ISSUE_CLOSE_RE = /^<!--\s*\/(obstacles|experiments)\s*-->\s*$/;
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;