chapterhouse 0.6.0 → 0.8.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.
- package/agents/korg.agent.md +65 -0
- package/dist/api/agent-edit-access.js +11 -0
- package/dist/api/agents.api.test.js +48 -0
- package/dist/api/korg.js +34 -0
- package/dist/api/korg.test.js +42 -0
- package/dist/api/server.js +420 -13
- package/dist/api/server.test.js +533 -3
- package/dist/config.js +28 -0
- package/dist/config.test.js +20 -0
- package/dist/copilot/agent-event-bus.js +1 -0
- package/dist/copilot/agents.js +117 -50
- package/dist/copilot/agents.mcp-servers.test.js +87 -0
- package/dist/copilot/agents.parse.test.js +69 -0
- package/dist/copilot/agents.test.js +137 -2
- package/dist/copilot/orchestrator.js +62 -13
- package/dist/copilot/orchestrator.test.js +130 -8
- package/dist/copilot/session-manager.js +34 -0
- package/dist/copilot/system-message.js +11 -10
- package/dist/copilot/system-message.test.js +6 -1
- package/dist/copilot/tools.js +184 -376
- package/dist/copilot/tools.memory.test.js +32 -0
- package/dist/copilot/tools.wiki.test.js +53 -59
- package/dist/daemon.js +9 -0
- package/dist/memory/decisions.js +6 -5
- package/dist/memory/entities.js +20 -9
- package/dist/memory/hooks.js +151 -0
- package/dist/memory/hooks.test.js +325 -0
- package/dist/memory/hot-tier.js +37 -0
- package/dist/memory/hot-tier.test.js +30 -0
- package/dist/memory/housekeeping-scheduler.js +35 -0
- package/dist/memory/housekeeping-scheduler.test.js +50 -0
- package/dist/memory/inbox.js +10 -0
- package/dist/memory/index.js +3 -1
- package/dist/memory/migration.js +244 -0
- package/dist/memory/migration.test.js +100 -0
- package/dist/memory/reflect.js +273 -0
- package/dist/memory/reflect.test.js +254 -0
- package/dist/store/db.js +119 -4
- package/dist/store/db.test.js +19 -1
- package/dist/test/setup-env.js +3 -1
- package/dist/test/setup-env.test.js +8 -1
- package/dist/wiki/consolidation.js +641 -0
- package/dist/wiki/consolidation.test.js +140 -0
- package/dist/wiki/frontmatter.js +48 -0
- package/dist/wiki/frontmatter.test.js +42 -0
- package/dist/wiki/index-manager.js +246 -330
- package/dist/wiki/index-manager.test.js +138 -145
- package/dist/wiki/ingest.js +347 -0
- package/dist/wiki/ingest.test.js +111 -0
- package/dist/wiki/links.js +151 -0
- package/dist/wiki/links.test.js +176 -0
- package/dist/wiki/migrate-topics.test.js +16 -6
- package/dist/wiki/scheduler.js +118 -0
- package/dist/wiki/scheduler.test.js +64 -0
- package/dist/wiki/timeline.js +51 -0
- package/dist/wiki/timeline.test.js +65 -0
- package/dist/wiki/topic-structure.js +1 -1
- package/package.json +3 -1
- package/skills/pkb-ideas/SKILL.md +78 -0
- package/skills/pkb-ideas/_meta.json +4 -0
- package/skills/pkb-org/SKILL.md +82 -0
- package/skills/pkb-org/_meta.json +4 -0
- package/skills/pkb-people/SKILL.md +74 -0
- package/skills/pkb-people/_meta.json +4 -0
- package/skills/pkb-research/SKILL.md +83 -0
- package/skills/pkb-research/_meta.json +4 -0
- package/skills/pkb-source/SKILL.md +38 -0
- package/skills/pkb-source/_meta.json +4 -0
- package/skills/wiki-conventions/SKILL.md +5 -5
- package/web/dist/assets/index-5kz9aRU9.css +10 -0
- package/web/dist/assets/{index-B5oDsQ5y.js → index-BbX9RKf3.js} +101 -99
- package/web/dist/assets/index-BbX9RKf3.js.map +1 -0
- package/web/dist/index.html +2 -2
- package/dist/wiki/context.js +0 -138
- package/dist/wiki/fix.js +0 -335
- package/dist/wiki/fix.test.js +0 -350
- package/dist/wiki/lint.js +0 -451
- package/dist/wiki/lint.test.js +0 -329
- package/web/dist/assets/index-B5oDsQ5y.js.map +0 -1
- package/web/dist/assets/index-DknKAtDS.css +0 -10
package/dist/wiki/lint.js
DELETED
|
@@ -1,451 +0,0 @@
|
|
|
1
|
-
import { accessSync, constants as fsConstants, existsSync, statSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
import { config } from "../config.js";
|
|
4
|
-
import { WIKI_DIR } from "../paths.js";
|
|
5
|
-
import { parseIndex } from "./index-manager.js";
|
|
6
|
-
import { listPages, listSources, readPage } from "./fs.js";
|
|
7
|
-
import { parseWikiFrontmatter, validateProjectRulesFrontmatter, validateWikiFrontmatter, } from "./frontmatter.js";
|
|
8
|
-
import { ACTION_LOG_PATH } from "./log-manager.js";
|
|
9
|
-
import { TAXONOMY_PATH, loadTaxonomy, parseTaxonomyTags } from "./taxonomy.js";
|
|
10
|
-
const DEFAULT_STALE_OVERRIDES = {
|
|
11
|
-
decisions: "never",
|
|
12
|
-
"feature-ideas": 90,
|
|
13
|
-
conversations: "never",
|
|
14
|
-
default: 180,
|
|
15
|
-
};
|
|
16
|
-
const DEFAULT_OPTIONS = {
|
|
17
|
-
requireUpdated: true,
|
|
18
|
-
requireFrontmatter: true,
|
|
19
|
-
disallowSkippedHeadingLevels: true,
|
|
20
|
-
pageWarnLines: 300,
|
|
21
|
-
pageErrorLines: 800,
|
|
22
|
-
prematureBodyChars: 100,
|
|
23
|
-
autostubExtraLines: 10,
|
|
24
|
-
reportDeadTags: true,
|
|
25
|
-
surfaceContestedFirst: true,
|
|
26
|
-
staleOverrides: DEFAULT_STALE_OVERRIDES,
|
|
27
|
-
};
|
|
28
|
-
const SPECIAL_META_PAGE_RE = /^pages\/_meta\/(?:taxonomy|log(?:-\d{4})?)\.md$/;
|
|
29
|
-
const LEGACY_DATE_STAMP_RE = /^(?:Logged|Updated):\s*\d{4}-\d{2}-\d{2}\b/m;
|
|
30
|
-
export function lintWiki(options = {}) {
|
|
31
|
-
const staleOverrides = {
|
|
32
|
-
...DEFAULT_STALE_OVERRIDES,
|
|
33
|
-
...(options.staleOverrides ?? {}),
|
|
34
|
-
};
|
|
35
|
-
const resolved = {
|
|
36
|
-
...DEFAULT_OPTIONS,
|
|
37
|
-
...options,
|
|
38
|
-
staleOverrides,
|
|
39
|
-
};
|
|
40
|
-
const actionLogIssues = lintActionLog();
|
|
41
|
-
const indexEntries = parseIndex();
|
|
42
|
-
const pages = listPages().filter((path) => path !== ACTION_LOG_PATH).sort();
|
|
43
|
-
const sources = listSources();
|
|
44
|
-
const indexPaths = new Set(indexEntries.map((entry) => entry.path));
|
|
45
|
-
const issues = [...actionLogIssues];
|
|
46
|
-
let allowedTags = [];
|
|
47
|
-
try {
|
|
48
|
-
allowedTags = loadTaxonomy();
|
|
49
|
-
}
|
|
50
|
-
catch (error) {
|
|
51
|
-
issues.push({
|
|
52
|
-
rule: "frontmatter-shape",
|
|
53
|
-
severity: "error",
|
|
54
|
-
path: TAXONOMY_PATH,
|
|
55
|
-
message: error instanceof Error ? error.message : String(error),
|
|
56
|
-
suggestion: "Fix pages/_meta/taxonomy.md so it only contains '## Group' headings and '- tag' bullets.",
|
|
57
|
-
});
|
|
58
|
-
}
|
|
59
|
-
for (const path of pages.filter((page) => !indexPaths.has(page) && !isSpecialMetaPage(page))) {
|
|
60
|
-
issues.push({
|
|
61
|
-
rule: "orphan-page",
|
|
62
|
-
severity: "warning",
|
|
63
|
-
path,
|
|
64
|
-
message: "Page exists on disk but is missing from the wiki index.",
|
|
65
|
-
suggestion: "Rebuild the index or add the page entry explicitly.",
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
|
-
for (const entry of indexEntries.filter((candidate) => !readPage(candidate.path))) {
|
|
69
|
-
issues.push({
|
|
70
|
-
rule: "missing-page",
|
|
71
|
-
severity: "warning",
|
|
72
|
-
path: entry.path,
|
|
73
|
-
message: `Index entry points to a page that does not exist on disk: ${entry.title}`,
|
|
74
|
-
suggestion: "Delete the stale index entry or restore the page.",
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
issues.push(...lintIndexIntegrity(pages));
|
|
78
|
-
for (const path of pages) {
|
|
79
|
-
const content = readPage(path);
|
|
80
|
-
if (!content)
|
|
81
|
-
continue;
|
|
82
|
-
const { parsed, body } = parseWikiFrontmatter(content);
|
|
83
|
-
const autostub = parsed.autostub === true;
|
|
84
|
-
const specialMetaPage = isSpecialMetaPage(path);
|
|
85
|
-
if (resolved.surfaceContestedFirst && (parsed.contested === true || parsed.confidence === "low")) {
|
|
86
|
-
const state = parsed.contested === true && parsed.confidence === "low"
|
|
87
|
-
? "contested and low confidence"
|
|
88
|
-
: parsed.contested === true
|
|
89
|
-
? "contested"
|
|
90
|
-
: "low confidence";
|
|
91
|
-
issues.push({
|
|
92
|
-
rule: "contested-review",
|
|
93
|
-
severity: "info",
|
|
94
|
-
path,
|
|
95
|
-
message: `Page is marked ${state} and should be reviewed first.`,
|
|
96
|
-
});
|
|
97
|
-
}
|
|
98
|
-
if (resolved.requireFrontmatter && !specialMetaPage) {
|
|
99
|
-
const validationOptions = allowedTags.length > 0 ? { allowedTags } : undefined;
|
|
100
|
-
if (isProjectRulesPage(path)) {
|
|
101
|
-
const validation = validateProjectRulesFrontmatter(content, validationOptions);
|
|
102
|
-
if (!validation.valid) {
|
|
103
|
-
issues.push({
|
|
104
|
-
rule: "frontmatter-shape",
|
|
105
|
-
severity: "error",
|
|
106
|
-
path,
|
|
107
|
-
message: validation.errors.map((error) => error.message).join(" "),
|
|
108
|
-
suggestion: "Rewrite the page frontmatter to match the canonical wiki shape before the next update.",
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
for (const warning of validation.warnings) {
|
|
112
|
-
issues.push({
|
|
113
|
-
rule: warning.rule,
|
|
114
|
-
severity: "warning",
|
|
115
|
-
path,
|
|
116
|
-
message: warning.message,
|
|
117
|
-
suggestion: "Remove the unknown key or add it to the project-rules schema before relying on it.",
|
|
118
|
-
});
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
else {
|
|
122
|
-
const validation = validateWikiFrontmatter(content, validationOptions);
|
|
123
|
-
if (!validation.valid) {
|
|
124
|
-
issues.push({
|
|
125
|
-
rule: "frontmatter-shape",
|
|
126
|
-
severity: "error",
|
|
127
|
-
path,
|
|
128
|
-
message: validation.errors.map((error) => error.message).join(" "),
|
|
129
|
-
suggestion: "Rewrite the page frontmatter to match the canonical wiki shape before the next update.",
|
|
130
|
-
});
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
if (resolved.requireUpdated && !autostub && !specialMetaPage && !parsed.updated && !LEGACY_DATE_STAMP_RE.test(body)) {
|
|
135
|
-
issues.push({
|
|
136
|
-
rule: "missing-updated",
|
|
137
|
-
severity: "warning",
|
|
138
|
-
path,
|
|
139
|
-
message: "Page does not declare an updated date stamp.",
|
|
140
|
-
suggestion: "Add 'updated: YYYY-MM-DD' to the frontmatter.",
|
|
141
|
-
});
|
|
142
|
-
}
|
|
143
|
-
const staleThreshold = staleThresholdForPath(path, resolved.staleOverrides);
|
|
144
|
-
if (!autostub && staleThreshold !== "never" && isPageOlderThan(path, staleThreshold)) {
|
|
145
|
-
issues.push({
|
|
146
|
-
rule: "stale-page",
|
|
147
|
-
severity: "warning",
|
|
148
|
-
path,
|
|
149
|
-
message: `Page has not been touched in more than ${staleThreshold} days.`,
|
|
150
|
-
suggestion: "Refresh the page or archive it if the information is no longer active.",
|
|
151
|
-
});
|
|
152
|
-
}
|
|
153
|
-
if (!autostub && !specialMetaPage && resolved.disallowSkippedHeadingLevels) {
|
|
154
|
-
const headingIssue = lintHeadingDepth(path, body);
|
|
155
|
-
if (headingIssue)
|
|
156
|
-
issues.push(headingIssue);
|
|
157
|
-
}
|
|
158
|
-
if (isDecisionMisfile(path)) {
|
|
159
|
-
issues.push({
|
|
160
|
-
rule: "decision-misfile",
|
|
161
|
-
severity: "error",
|
|
162
|
-
path,
|
|
163
|
-
message: "decisions.md must live under an entity directory.",
|
|
164
|
-
suggestion: "Move this page to pages/<entity-category>/<topic>/decisions.md.",
|
|
165
|
-
});
|
|
166
|
-
}
|
|
167
|
-
if (!autostub && !specialMetaPage) {
|
|
168
|
-
const lineCount = content.split("\n").length;
|
|
169
|
-
if (lineCount > resolved.pageErrorLines) {
|
|
170
|
-
issues.push({
|
|
171
|
-
rule: "page-size",
|
|
172
|
-
severity: "error",
|
|
173
|
-
path,
|
|
174
|
-
message: `Page is ${lineCount} lines long, which exceeds the ${resolved.pageErrorLines}-line error threshold.`,
|
|
175
|
-
suggestion: "Split the page into focused sub-pages or archive older material.",
|
|
176
|
-
});
|
|
177
|
-
}
|
|
178
|
-
else if (lineCount > resolved.pageWarnLines) {
|
|
179
|
-
issues.push({
|
|
180
|
-
rule: "page-size",
|
|
181
|
-
severity: "warning",
|
|
182
|
-
path,
|
|
183
|
-
message: `Page is ${lineCount} lines long, which exceeds the ${resolved.pageWarnLines}-line warning threshold.`,
|
|
184
|
-
suggestion: "Consider splitting this page before it becomes unwieldy.",
|
|
185
|
-
});
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
if (!autostub && !specialMetaPage && bodyVisibleCharCount(body) <= resolved.prematureBodyChars) {
|
|
189
|
-
issues.push({
|
|
190
|
-
rule: "premature-page",
|
|
191
|
-
severity: "info",
|
|
192
|
-
path,
|
|
193
|
-
message: `Page body is too small to justify its own page (${bodyVisibleCharCount(body)} chars).`,
|
|
194
|
-
suggestion: "Fold the content into a parent page until there is enough material to stand alone.",
|
|
195
|
-
});
|
|
196
|
-
}
|
|
197
|
-
if (autostub && autostubExtraLines(body) > resolved.autostubExtraLines) {
|
|
198
|
-
issues.push({
|
|
199
|
-
rule: "autostub-not-flipped",
|
|
200
|
-
severity: "info",
|
|
201
|
-
path,
|
|
202
|
-
message: "Page has grown past stub size but still declares autostub: true.",
|
|
203
|
-
suggestion: "Remove autostub: true once the page becomes a real page.",
|
|
204
|
-
});
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
if (resolved.reportDeadTags) {
|
|
208
|
-
issues.push(...lintDeadTaxonomyEntries(pages));
|
|
209
|
-
}
|
|
210
|
-
return {
|
|
211
|
-
pageCount: pages.length,
|
|
212
|
-
sourceCount: sources.length,
|
|
213
|
-
issues: sortIssues(issues),
|
|
214
|
-
};
|
|
215
|
-
}
|
|
216
|
-
export function renderWikiLintReport(report) {
|
|
217
|
-
const sections = [
|
|
218
|
-
`Wiki health report (${report.pageCount} pages, ${report.sourceCount} sources):`,
|
|
219
|
-
];
|
|
220
|
-
if (report.issues.length === 0) {
|
|
221
|
-
sections.push("\n✅ No issues found. Index and pages are in sync.");
|
|
222
|
-
sections.push("\n**Suggestions**: Look for pages that should link to each other, topics mentioned but lacking their own page, and stale content that needs updating.");
|
|
223
|
-
return sections.join("\n");
|
|
224
|
-
}
|
|
225
|
-
const contested = report.issues.filter((issue) => issue.rule === "contested-review");
|
|
226
|
-
const general = report.issues.filter((issue) => issue.rule !== "contested-review");
|
|
227
|
-
if (contested.length > 0) {
|
|
228
|
-
sections.push("\n**Review first**:");
|
|
229
|
-
for (const issue of contested) {
|
|
230
|
-
sections.push(formatIssueLine(issue));
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
if (general.length > 0) {
|
|
234
|
-
sections.push("\n**Issues**:");
|
|
235
|
-
for (const issue of general) {
|
|
236
|
-
sections.push(formatIssueLine(issue));
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
sections.push("\n**Suggestions**: Look for pages that should link to each other, topics mentioned but lacking their own page, and stale content that needs updating.");
|
|
240
|
-
return sections.join("\n");
|
|
241
|
-
}
|
|
242
|
-
function lintIndexIntegrity(pages) {
|
|
243
|
-
const issues = [];
|
|
244
|
-
const entityCategories = new Set(config.wikiEntityCategories);
|
|
245
|
-
const dirsWithFacets = new Set();
|
|
246
|
-
const dirsWithIndex = new Set();
|
|
247
|
-
for (const path of pages) {
|
|
248
|
-
const match = path.match(/^pages\/([^/]+)\/([^/]+)\/([^/]+)\.md$/);
|
|
249
|
-
if (!match)
|
|
250
|
-
continue;
|
|
251
|
-
const [, category, topic, page] = match;
|
|
252
|
-
if (!entityCategories.has(category))
|
|
253
|
-
continue;
|
|
254
|
-
const dirPath = `pages/${category}/${topic}`;
|
|
255
|
-
if (page === "index") {
|
|
256
|
-
dirsWithIndex.add(dirPath);
|
|
257
|
-
}
|
|
258
|
-
else {
|
|
259
|
-
dirsWithFacets.add(dirPath);
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
for (const dirPath of [...dirsWithFacets].sort()) {
|
|
263
|
-
if (!dirsWithIndex.has(dirPath)) {
|
|
264
|
-
issues.push({
|
|
265
|
-
rule: "index-integrity",
|
|
266
|
-
severity: "error",
|
|
267
|
-
path: dirPath,
|
|
268
|
-
message: "Entity directory has facet pages but no index.md overview page.",
|
|
269
|
-
suggestion: `Add ${dirPath}/index.md so the topic has a canonical overview page.`,
|
|
270
|
-
});
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
return issues;
|
|
274
|
-
}
|
|
275
|
-
function lintHeadingDepth(path, body) {
|
|
276
|
-
const headings = body
|
|
277
|
-
.split("\n")
|
|
278
|
-
.map((line) => line.match(/^(#{1,6})\s+(.+)$/))
|
|
279
|
-
.filter((match) => match !== null);
|
|
280
|
-
if (headings.length === 0)
|
|
281
|
-
return undefined;
|
|
282
|
-
const firstLevel = headings[0][1].length;
|
|
283
|
-
if (firstLevel !== 1) {
|
|
284
|
-
return {
|
|
285
|
-
rule: "heading-depth",
|
|
286
|
-
severity: "warning",
|
|
287
|
-
path,
|
|
288
|
-
message: "Page headings must start with a single '#' title heading.",
|
|
289
|
-
suggestion: "Start the page with '# Title' before any deeper sections.",
|
|
290
|
-
};
|
|
291
|
-
}
|
|
292
|
-
let previousLevel = firstLevel;
|
|
293
|
-
for (const heading of headings.slice(1)) {
|
|
294
|
-
const level = heading[1].length;
|
|
295
|
-
if (level > previousLevel + 1) {
|
|
296
|
-
return {
|
|
297
|
-
rule: "heading-depth",
|
|
298
|
-
severity: "warning",
|
|
299
|
-
path,
|
|
300
|
-
message: `Heading depth skips from h${previousLevel} to h${level}.`,
|
|
301
|
-
suggestion: "Add the missing intermediate heading level or flatten the structure.",
|
|
302
|
-
};
|
|
303
|
-
}
|
|
304
|
-
previousLevel = level;
|
|
305
|
-
}
|
|
306
|
-
return undefined;
|
|
307
|
-
}
|
|
308
|
-
function lintDeadTaxonomyEntries(pages) {
|
|
309
|
-
const taxonomyContent = readPage(TAXONOMY_PATH);
|
|
310
|
-
if (!taxonomyContent) {
|
|
311
|
-
return [];
|
|
312
|
-
}
|
|
313
|
-
let overrideTags;
|
|
314
|
-
try {
|
|
315
|
-
overrideTags = parseTaxonomyTags(taxonomyContent);
|
|
316
|
-
}
|
|
317
|
-
catch {
|
|
318
|
-
return [];
|
|
319
|
-
}
|
|
320
|
-
const usedTags = new Set();
|
|
321
|
-
for (const path of pages) {
|
|
322
|
-
const content = readPage(path);
|
|
323
|
-
if (!content)
|
|
324
|
-
continue;
|
|
325
|
-
const { parsed } = parseWikiFrontmatter(content);
|
|
326
|
-
for (const tag of parsed.tags ?? []) {
|
|
327
|
-
usedTags.add(tag.toLowerCase());
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
return overrideTags
|
|
331
|
-
.filter((tag) => !usedTags.has(tag.toLowerCase()))
|
|
332
|
-
.sort()
|
|
333
|
-
.map((tag) => ({
|
|
334
|
-
rule: "dead-taxonomy-entry",
|
|
335
|
-
severity: "info",
|
|
336
|
-
path: TAXONOMY_PATH,
|
|
337
|
-
message: `Tag '${tag}' is declared in the taxonomy but unused by any page.`,
|
|
338
|
-
suggestion: `Remove '${tag}' from pages/_meta/taxonomy.md or tag a page with it.`,
|
|
339
|
-
}));
|
|
340
|
-
}
|
|
341
|
-
function staleThresholdForPath(path, overrides) {
|
|
342
|
-
if (path.startsWith("pages/conversations/")) {
|
|
343
|
-
return overrides.conversations ?? "never";
|
|
344
|
-
}
|
|
345
|
-
if (path.endsWith("/decisions.md")) {
|
|
346
|
-
return overrides.decisions ?? "never";
|
|
347
|
-
}
|
|
348
|
-
if (path.endsWith("/feature-ideas.md")) {
|
|
349
|
-
return overrides["feature-ideas"] ?? 90;
|
|
350
|
-
}
|
|
351
|
-
return overrides.default ?? 180;
|
|
352
|
-
}
|
|
353
|
-
function isPageOlderThan(path, ageInDays) {
|
|
354
|
-
const stats = statSync(join(WIKI_DIR, path));
|
|
355
|
-
const ageMs = Date.now() - stats.mtime.getTime();
|
|
356
|
-
return ageMs > ageInDays * 24 * 60 * 60 * 1000;
|
|
357
|
-
}
|
|
358
|
-
function lintActionLog() {
|
|
359
|
-
const fullPath = join(WIKI_DIR, ACTION_LOG_PATH);
|
|
360
|
-
if (!existsSync(fullPath)) {
|
|
361
|
-
return [{
|
|
362
|
-
rule: "action-log",
|
|
363
|
-
severity: "error",
|
|
364
|
-
path: ACTION_LOG_PATH,
|
|
365
|
-
message: "The wiki action log is missing.",
|
|
366
|
-
suggestion: "Restore pages/_meta/log.md so wiki operations keep an append-only audit trail.",
|
|
367
|
-
}];
|
|
368
|
-
}
|
|
369
|
-
try {
|
|
370
|
-
const stats = statSync(fullPath);
|
|
371
|
-
if (!stats.isFile()) {
|
|
372
|
-
return [{
|
|
373
|
-
rule: "action-log",
|
|
374
|
-
severity: "error",
|
|
375
|
-
path: ACTION_LOG_PATH,
|
|
376
|
-
message: "The wiki action log path exists but is not a writable file.",
|
|
377
|
-
suggestion: "Replace pages/_meta/log.md with a regular writable Markdown file.",
|
|
378
|
-
}];
|
|
379
|
-
}
|
|
380
|
-
accessSync(fullPath, fsConstants.W_OK);
|
|
381
|
-
return [];
|
|
382
|
-
}
|
|
383
|
-
catch (error) {
|
|
384
|
-
return [{
|
|
385
|
-
rule: "action-log",
|
|
386
|
-
severity: "error",
|
|
387
|
-
path: ACTION_LOG_PATH,
|
|
388
|
-
message: error instanceof Error
|
|
389
|
-
? `The wiki action log is not writable: ${error.message}`
|
|
390
|
-
: "The wiki action log is not writable.",
|
|
391
|
-
suggestion: "Fix permissions on pages/_meta/log.md so wiki operations can append to it.",
|
|
392
|
-
}];
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
function isDecisionMisfile(path) {
|
|
396
|
-
if (!path.endsWith("/decisions.md")) {
|
|
397
|
-
return false;
|
|
398
|
-
}
|
|
399
|
-
return !config.wikiEntityCategories.some((category) => {
|
|
400
|
-
const expectedPrefix = `pages/${category}/`;
|
|
401
|
-
return path.startsWith(expectedPrefix) && path.split("/").length === 4;
|
|
402
|
-
});
|
|
403
|
-
}
|
|
404
|
-
function isProjectRulesPage(path) {
|
|
405
|
-
return /^pages\/projects\/[^/]+\/rules\.md$/.test(path);
|
|
406
|
-
}
|
|
407
|
-
function isSpecialMetaPage(path) {
|
|
408
|
-
return SPECIAL_META_PAGE_RE.test(path);
|
|
409
|
-
}
|
|
410
|
-
function bodyVisibleCharCount(body) {
|
|
411
|
-
const visible = body
|
|
412
|
-
.split("\n")
|
|
413
|
-
.map((line) => line.trim())
|
|
414
|
-
.filter((line) => line.length > 0 && !line.startsWith("#"))
|
|
415
|
-
.join(" ")
|
|
416
|
-
.trim();
|
|
417
|
-
return visible.length;
|
|
418
|
-
}
|
|
419
|
-
function autostubExtraLines(body) {
|
|
420
|
-
const meaningfulLines = body
|
|
421
|
-
.split("\n")
|
|
422
|
-
.map((line) => line.trim())
|
|
423
|
-
.filter(Boolean);
|
|
424
|
-
return Math.max(0, meaningfulLines.length - 1);
|
|
425
|
-
}
|
|
426
|
-
function sortIssues(issues) {
|
|
427
|
-
const severityRank = {
|
|
428
|
-
error: 0,
|
|
429
|
-
warning: 1,
|
|
430
|
-
info: 2,
|
|
431
|
-
};
|
|
432
|
-
return [...issues].sort((left, right) => {
|
|
433
|
-
if (left.rule === "contested-review" && right.rule !== "contested-review")
|
|
434
|
-
return -1;
|
|
435
|
-
if (right.rule === "contested-review" && left.rule !== "contested-review")
|
|
436
|
-
return 1;
|
|
437
|
-
const severityDelta = severityRank[left.severity] - severityRank[right.severity];
|
|
438
|
-
if (severityDelta !== 0)
|
|
439
|
-
return severityDelta;
|
|
440
|
-
const pathDelta = (left.path ?? "").localeCompare(right.path ?? "");
|
|
441
|
-
if (pathDelta !== 0)
|
|
442
|
-
return pathDelta;
|
|
443
|
-
return left.rule.localeCompare(right.rule);
|
|
444
|
-
});
|
|
445
|
-
}
|
|
446
|
-
function formatIssueLine(issue) {
|
|
447
|
-
const path = issue.path ?? "-";
|
|
448
|
-
const suggestion = issue.suggestion ? `\n suggestion: ${issue.suggestion}` : "";
|
|
449
|
-
return `- ${issue.severity} | ${issue.rule} | ${path} | ${issue.message}${suggestion}`;
|
|
450
|
-
}
|
|
451
|
-
//# sourceMappingURL=lint.js.map
|