@forwardimpact/libwiki 0.1.5 → 0.2.0

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.
@@ -0,0 +1,91 @@
1
+ import { readFileSync, writeFileSync, existsSync, renameSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { WEEKLY_LOG_LINE_BUDGET } from "./constants.js";
4
+
5
+ /** Compute ISO 8601 year-week for a Date. Returns { year, week } where year is the ISO week-year (not necessarily the calendar year for edge weeks). */
6
+ export function isoWeek(date) {
7
+ const d = new Date(
8
+ Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()),
9
+ );
10
+ // Thursday of week: ISO weeks are anchored on Thursday.
11
+ const day = d.getUTCDay() || 7;
12
+ d.setUTCDate(d.getUTCDate() + 4 - day);
13
+ const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
14
+ const week = Math.ceil(((d - yearStart) / 86400000 + 1) / 7);
15
+ return { year: d.getUTCFullYear(), week };
16
+ }
17
+
18
+ function formatIsoWeek(date) {
19
+ const { year, week } = isoWeek(date);
20
+ return `${year}-W${String(week).padStart(2, "0")}`;
21
+ }
22
+
23
+ /** Return the path of the current weekly log file for an agent. */
24
+ export function weeklyLogPath(wikiRoot, agent, today) {
25
+ const date = today instanceof Date ? today : new Date(today);
26
+ return path.join(wikiRoot, `${agent}-${formatIsoWeek(date)}.md`);
27
+ }
28
+
29
+ function countLines(text) {
30
+ if (text.length === 0) return 0;
31
+ let n = 0;
32
+ for (const ch of text) if (ch === "\n") n++;
33
+ if (!text.endsWith("\n")) n++;
34
+ return n;
35
+ }
36
+
37
+ function nextPartPath(filePath) {
38
+ const dir = path.dirname(filePath);
39
+ const base = path.basename(filePath, ".md");
40
+ let n = 1;
41
+ while (existsSync(path.join(dir, `${base}-part${n}.md`))) n++;
42
+ return path.join(dir, `${base}-part${n}.md`);
43
+ }
44
+
45
+ function agentTitle(agent) {
46
+ return agent
47
+ .split("-")
48
+ .map((p) => p.charAt(0).toUpperCase() + p.slice(1))
49
+ .join(" ");
50
+ }
51
+
52
+ function defaultH1(filePath, agent, isoWeekStr) {
53
+ return `# ${agentTitle(agent)} — ${isoWeekStr}\n`;
54
+ }
55
+
56
+ /** Rotate the current weekly log if next append would exceed the budget. Returns { rotated, fromPath, toPath }. */
57
+ export function rotateIfOverBudget(
58
+ wikiRoot,
59
+ agent,
60
+ today,
61
+ appendLines = 0,
62
+ options = {},
63
+ ) {
64
+ const filePath = weeklyLogPath(wikiRoot, agent, today);
65
+ const { force = false } = options;
66
+ if (!existsSync(filePath)) return { rotated: false, fromPath: filePath };
67
+ const text = readFileSync(filePath, "utf-8");
68
+ const current = countLines(text);
69
+ if (!force && current + appendLines <= WEEKLY_LOG_LINE_BUDGET) {
70
+ return { rotated: false, fromPath: filePath };
71
+ }
72
+ const toPath = nextPartPath(filePath);
73
+ renameSync(filePath, toPath);
74
+ const date = today instanceof Date ? today : new Date(today);
75
+ writeFileSync(filePath, defaultH1(filePath, agent, formatIsoWeek(date)));
76
+ return { rotated: true, fromPath: filePath, toPath };
77
+ }
78
+
79
+ /** Append a body to a weekly log file. Creates it with an H1 if missing. */
80
+ export function appendEntry(filePath, body, agent, today) {
81
+ const date = today instanceof Date ? today : new Date(today);
82
+ if (!existsSync(filePath)) {
83
+ writeFileSync(filePath, defaultH1(filePath, agent, formatIsoWeek(date)));
84
+ }
85
+ const text = readFileSync(filePath, "utf-8");
86
+ const separator = text.endsWith("\n") ? "\n" : "\n\n";
87
+ writeFileSync(
88
+ filePath,
89
+ text + separator + body + (body.endsWith("\n") ? "" : "\n"),
90
+ );
91
+ }