chapterhouse 0.3.18 → 0.3.19
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/dist/api/server-runtime.js +0 -16
- package/dist/api/server.js +3 -14
- package/dist/api/server.test.js +0 -25
- package/dist/copilot/skills.test.js +4 -0
- package/dist/copilot/tools.js +32 -0
- package/dist/copilot/tools.wiki.test.js +46 -0
- package/dist/wiki/fix.js +335 -0
- package/dist/wiki/fix.test.js +350 -0
- package/dist/wiki/frontmatter.js +105 -0
- package/dist/wiki/frontmatter.test.js +120 -0
- package/package.json +1 -1
- package/web/dist/assets/{index-Bjaa3b4i.js → index-9We9vWBC.js} +63 -63
- package/web/dist/assets/index-9We9vWBC.js.map +1 -0
- package/web/dist/assets/{index-lvHFM_ut.css → index-DYx2idiH.css} +1 -1
- package/web/dist/index.html +2 -2
- package/skills/squad/SKILL.md +0 -76
- package/web/dist/assets/index-Bjaa3b4i.js.map +0 -1
|
@@ -46,20 +46,4 @@ export function shouldServeSpaPath(pathname) {
|
|
|
46
46
|
export function getDisplayHost(host) {
|
|
47
47
|
return host === "0.0.0.0" || host === "::" || host === "127.0.0.1" || host === "::1" ? "localhost" : host;
|
|
48
48
|
}
|
|
49
|
-
export function buildHistoryEntries(pageIds, options) {
|
|
50
|
-
const resolveWikiPath = options?.resolveWikiPath ?? ((pageId) => pageId);
|
|
51
|
-
const stat = options?.stat;
|
|
52
|
-
return pageIds
|
|
53
|
-
.map((pageId) => {
|
|
54
|
-
try {
|
|
55
|
-
const fullPath = resolveWikiPath(pageId);
|
|
56
|
-
const mtime = stat ? stat(fullPath).mtimeMs : 0;
|
|
57
|
-
return { path: pageId, mtime };
|
|
58
|
-
}
|
|
59
|
-
catch {
|
|
60
|
-
return { path: pageId, mtime: 0 };
|
|
61
|
-
}
|
|
62
|
-
})
|
|
63
|
-
.sort((a, b) => b.mtime - a.mtime);
|
|
64
|
-
}
|
|
65
49
|
//# sourceMappingURL=server-runtime.js.map
|
package/dist/api/server.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import cors from "cors";
|
|
2
2
|
import express from "express";
|
|
3
3
|
import helmet from "helmet";
|
|
4
|
-
import { existsSync
|
|
4
|
+
import { existsSync } from "fs";
|
|
5
5
|
import { join, dirname } from "path";
|
|
6
6
|
import { fileURLToPath } from "url";
|
|
7
7
|
import { z } from "zod";
|
|
@@ -19,13 +19,13 @@ import { readWikiPage, teamWikiSync } from "../wiki/team-sync.js";
|
|
|
19
19
|
import { withWikiWrite } from "../wiki/lock.js";
|
|
20
20
|
import { listSkills, removeSkill } from "../copilot/skills.js";
|
|
21
21
|
import { restartDaemon } from "../daemon.js";
|
|
22
|
-
import { API_TOKEN_PATH
|
|
22
|
+
import { API_TOKEN_PATH } from "../paths.js";
|
|
23
23
|
import { getDb, getSessionMessages, getTaskEvents } from "../store/db.js";
|
|
24
24
|
import { getTaskLogEvents, subscribeTaskLog } from "../copilot/task-event-log.js";
|
|
25
25
|
import { subscribeSession, getSessionEventsFromDb, oldestSessionSeq, } from "../copilot/turn-event-log.js";
|
|
26
26
|
import { getStatus, onStatusChange } from "../status.js";
|
|
27
27
|
import { formatSseData, formatSseEvent } from "./sse.js";
|
|
28
|
-
import { assertAuthenticationConfigured, createHealthPayload, createPublicConfigPayload,
|
|
28
|
+
import { assertAuthenticationConfigured, createHealthPayload, createPublicConfigPayload, getDisplayHost, resolveApiToken, shouldServeSpaPath, } from "./server-runtime.js";
|
|
29
29
|
import { BadRequestError, ForbiddenError, InternalServerError, NotFoundError, apiNotFoundHandler, asBadRequest, createApiErrorHandler, parseRequest, } from "./errors.js";
|
|
30
30
|
import { childLogger } from "../util/logger.js";
|
|
31
31
|
const log = childLogger("server");
|
|
@@ -784,17 +784,6 @@ app.delete("/api/wiki/page", async (req, res) => {
|
|
|
784
784
|
res.json({ ok: removed, path });
|
|
785
785
|
});
|
|
786
786
|
// ---------------------------------------------------------------------------
|
|
787
|
-
// History — past conversation summaries auto-written to pages/conversations/
|
|
788
|
-
// ---------------------------------------------------------------------------
|
|
789
|
-
app.get("/api/history", (_req, res) => {
|
|
790
|
-
ensureWikiStructure();
|
|
791
|
-
const entries = buildHistoryEntries(listPages().filter((p) => p.startsWith("pages/conversations/")), {
|
|
792
|
-
resolveWikiPath: resolveWikiRelativePath,
|
|
793
|
-
stat: statSync,
|
|
794
|
-
});
|
|
795
|
-
res.json(entries);
|
|
796
|
-
});
|
|
797
|
-
// ---------------------------------------------------------------------------
|
|
798
787
|
// Skills
|
|
799
788
|
// ---------------------------------------------------------------------------
|
|
800
789
|
app.get("/api/skills", (_req, res) => {
|
package/dist/api/server.test.js
CHANGED
|
@@ -66,31 +66,6 @@ test("formats named SSE status events", async () => {
|
|
|
66
66
|
assert.equal(typeof sse.formatSseEvent, "function", "formatSseEvent should be exported");
|
|
67
67
|
assert.equal(sse.formatSseEvent("status", { status: "dreaming", message: "Consolidating memories..." }), 'event: status\ndata: {"status":"dreaming","message":"Consolidating memories..."}\n\n');
|
|
68
68
|
});
|
|
69
|
-
test("buildHistoryEntries resolves wiki files through the shared path resolver", async () => {
|
|
70
|
-
const runtime = await import("./server-runtime.js");
|
|
71
|
-
assert.equal(typeof runtime.buildHistoryEntries, "function", "buildHistoryEntries should be exported");
|
|
72
|
-
const seen = [];
|
|
73
|
-
const entries = runtime.buildHistoryEntries([
|
|
74
|
-
"pages/conversations/older.md",
|
|
75
|
-
"pages/conversations/newer.md",
|
|
76
|
-
], {
|
|
77
|
-
resolveWikiPath: (pageId) => {
|
|
78
|
-
seen.push(pageId);
|
|
79
|
-
return `/wiki/${pageId}`;
|
|
80
|
-
},
|
|
81
|
-
stat: (fullPath) => ({
|
|
82
|
-
mtimeMs: fullPath.endsWith("newer.md") ? 200 : 100,
|
|
83
|
-
}),
|
|
84
|
-
});
|
|
85
|
-
assert.deepEqual(seen, [
|
|
86
|
-
"pages/conversations/older.md",
|
|
87
|
-
"pages/conversations/newer.md",
|
|
88
|
-
]);
|
|
89
|
-
assert.deepEqual(entries, [
|
|
90
|
-
{ path: "pages/conversations/newer.md", mtime: 200 },
|
|
91
|
-
{ path: "pages/conversations/older.md", mtime: 100 },
|
|
92
|
-
]);
|
|
93
|
-
});
|
|
94
69
|
const repoRoot = process.cwd();
|
|
95
70
|
const serverTestRoot = join(repoRoot, ".test-work", `server-routes-${process.pid}`);
|
|
96
71
|
async function getFreePort() {
|
|
@@ -7,4 +7,8 @@ test("listSkills includes bundled wiki-conventions skill metadata", () => {
|
|
|
7
7
|
assert.equal(skill.name, "wiki-conventions");
|
|
8
8
|
assert.match(skill.description, /creating, editing, linting, restructuring, or reviewing Chapterhouse wiki content/i);
|
|
9
9
|
});
|
|
10
|
+
test("listSkills does not include a bundled squad skill", () => {
|
|
11
|
+
const skill = listSkills().find((entry) => entry.slug === "squad" && entry.source === "bundled");
|
|
12
|
+
assert.equal(skill, undefined);
|
|
13
|
+
});
|
|
10
14
|
//# sourceMappingURL=skills.test.js.map
|
package/dist/copilot/tools.js
CHANGED
|
@@ -11,7 +11,9 @@ import { getRouterConfig, updateRouterConfig } from "./router.js";
|
|
|
11
11
|
import { ensureWikiStructure, readPage, writePage, deletePage, writeRawSource, assertPagePath } from "../wiki/fs.js";
|
|
12
12
|
import { searchIndex, addToIndex, removeFromIndex, buildIndexEntryForPage } from "../wiki/index-manager.js";
|
|
13
13
|
import { validateWikiFrontmatter } from "../wiki/frontmatter.js";
|
|
14
|
+
import { fixWiki } from "../wiki/fix.js";
|
|
14
15
|
import { lintWiki, renderWikiLintReport } from "../wiki/lint.js";
|
|
16
|
+
import { rebuildIndexFromPages } from "../wiki/index-manager.js";
|
|
15
17
|
import { appendLog } from "../wiki/log-manager.js";
|
|
16
18
|
import { loadTaxonomy } from "../wiki/taxonomy.js";
|
|
17
19
|
import { getCategoryDir, topicPagePath, slugify, entityCategories, FLAT_CATEGORIES } from "../wiki/topic-structure.js";
|
|
@@ -1088,6 +1090,36 @@ export function createTools(deps) {
|
|
|
1088
1090
|
});
|
|
1089
1091
|
},
|
|
1090
1092
|
}),
|
|
1093
|
+
defineTool("wiki_fix", {
|
|
1094
|
+
description: "Auto-repair common wiki inconsistencies. Defaults to dry-run preview mode and returns a per-file report " +
|
|
1095
|
+
"plus unified diff. Supports frontmatter backfill, taxonomy tag normalization, and autostub marking.",
|
|
1096
|
+
parameters: z.object({
|
|
1097
|
+
dry_run: z.boolean().optional().describe("Preview only when true (default). Set false to apply fixes."),
|
|
1098
|
+
fixes: z.array(z.enum(["frontmatter-backfill", "tag-normalize", "autostub-mark"])).optional()
|
|
1099
|
+
.describe("Optional subset of fixes to apply. Defaults to all fixes."),
|
|
1100
|
+
path_glob: z.string().optional().describe("Optional glob to limit which wiki page paths are considered."),
|
|
1101
|
+
}),
|
|
1102
|
+
handler: async (args) => {
|
|
1103
|
+
ensureWikiStructure();
|
|
1104
|
+
const dryRun = args.dry_run ?? true;
|
|
1105
|
+
const runFixer = () => {
|
|
1106
|
+
const report = fixWiki({
|
|
1107
|
+
dryRun,
|
|
1108
|
+
fixes: args.fixes,
|
|
1109
|
+
pathGlob: args.path_glob,
|
|
1110
|
+
logAction: dryRun ? undefined : (type, path) => appendLog(type, path),
|
|
1111
|
+
});
|
|
1112
|
+
if (!dryRun && report.changedFiles > 0) {
|
|
1113
|
+
rebuildIndexFromPages();
|
|
1114
|
+
}
|
|
1115
|
+
return report;
|
|
1116
|
+
};
|
|
1117
|
+
if (dryRun) {
|
|
1118
|
+
return runFixer();
|
|
1119
|
+
}
|
|
1120
|
+
return withWikiWrite(runFixer);
|
|
1121
|
+
},
|
|
1122
|
+
}),
|
|
1091
1123
|
defineTool("restart_chapterhouse", {
|
|
1092
1124
|
description: "Restart the Chapterhouse daemon process. Use when the user asks Chapterhouse to restart himself, " +
|
|
1093
1125
|
"or when a restart is needed to pick up configuration changes. " +
|
|
@@ -140,4 +140,50 @@ Runtime notes with enough content to avoid incidental lint noise in the audit-lo
|
|
|
140
140
|
assert.match(log, /rebuild-index \| wiki_rebuild_index: rebuilt \d+ entries from pages on disk \| tools-test-agent/);
|
|
141
141
|
assert.match(log, /delete \| forget: deleted page pages\/shared\/chapterhouse\.md \| tools-test-agent/);
|
|
142
142
|
});
|
|
143
|
+
test("wiki_fix defaults to dry-run previews and logs applied fixes in write mode", async () => {
|
|
144
|
+
const toolsModule = await loadToolsModule();
|
|
145
|
+
const tools = toolsModule.createTools({
|
|
146
|
+
client: { async listModels() { return []; } },
|
|
147
|
+
onAgentTaskComplete: () => { },
|
|
148
|
+
});
|
|
149
|
+
const wikiFix = tools.find((entry) => entry.name === "wiki_fix");
|
|
150
|
+
assert.ok(wikiFix);
|
|
151
|
+
const wikiFs = await readWikiArtifacts();
|
|
152
|
+
wikiFs.writePage("pages/_meta/taxonomy.md", `## Process
|
|
153
|
+
- runbook
|
|
154
|
+
`);
|
|
155
|
+
wikiFs.writePage("pages/projects/chapterhouse/index.md", `---
|
|
156
|
+
title: Chapterhouse
|
|
157
|
+
summary: Runtime notes
|
|
158
|
+
updated: 2026-05-12
|
|
159
|
+
tags: [Run Book]
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
# Chapterhouse
|
|
163
|
+
|
|
164
|
+
Runtime notes with enough content to avoid incidental stub handling.
|
|
165
|
+
|
|
166
|
+
## Details
|
|
167
|
+
|
|
168
|
+
One
|
|
169
|
+
Two
|
|
170
|
+
Three
|
|
171
|
+
Four
|
|
172
|
+
Five
|
|
173
|
+
Six
|
|
174
|
+
Seven
|
|
175
|
+
Eight
|
|
176
|
+
`);
|
|
177
|
+
const before = wikiFs.readPage("pages/projects/chapterhouse/index.md");
|
|
178
|
+
const preview = await wikiFix.handler({});
|
|
179
|
+
assert.equal(typeof preview, "object");
|
|
180
|
+
assert.equal(preview.dryRun, true);
|
|
181
|
+
assert.equal(preview.changedFiles, 1);
|
|
182
|
+
assert.match(preview.diff, /--- a\/pages\/projects\/chapterhouse\/index\.md/);
|
|
183
|
+
assert.equal(wikiFs.readPage("pages/projects/chapterhouse/index.md"), before);
|
|
184
|
+
const applied = await wikiFix.handler({ dry_run: false });
|
|
185
|
+
assert.equal(applied.dryRun, false);
|
|
186
|
+
assert.match(wikiFs.readPage("pages/projects/chapterhouse/index.md") ?? "", /tags: \[runbook\]/);
|
|
187
|
+
assert.match(wikiFs.readLogFile(), /fix-tags \| pages\/projects\/chapterhouse\/index\.md \| tools-test-agent/);
|
|
188
|
+
});
|
|
143
189
|
//# sourceMappingURL=tools.wiki.test.js.map
|
package/dist/wiki/fix.js
ADDED
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
import { statSync } from "node:fs";
|
|
2
|
+
import { basename, dirname } from "node:path";
|
|
3
|
+
import { resolveWikiRelativePath } from "../paths.js";
|
|
4
|
+
import { parseWikiFrontmatter } from "./frontmatter.js";
|
|
5
|
+
import { ensureWikiStructure, listPages, readPage, writePage } from "./fs.js";
|
|
6
|
+
import { loadTaxonomy } from "./taxonomy.js";
|
|
7
|
+
const DEFAULT_FIXES = [
|
|
8
|
+
"frontmatter-backfill",
|
|
9
|
+
"tag-normalize",
|
|
10
|
+
"autostub-mark",
|
|
11
|
+
];
|
|
12
|
+
const KNOWN_FIELD_ORDER = [
|
|
13
|
+
"title",
|
|
14
|
+
"summary",
|
|
15
|
+
"updated",
|
|
16
|
+
"tags",
|
|
17
|
+
"autostub",
|
|
18
|
+
"confidence",
|
|
19
|
+
"contested",
|
|
20
|
+
"contradictions",
|
|
21
|
+
"related",
|
|
22
|
+
];
|
|
23
|
+
export function fixWiki(options = {}) {
|
|
24
|
+
ensureWikiStructure();
|
|
25
|
+
const dryRun = options.dryRun ?? true;
|
|
26
|
+
const enabledFixes = new Set(options.fixes?.length ? options.fixes : DEFAULT_FIXES);
|
|
27
|
+
const pathFilter = options.pathGlob ? compileGlob(options.pathGlob) : undefined;
|
|
28
|
+
const allowedTags = enabledFixes.has("tag-normalize") ? loadTaxonomy() : [];
|
|
29
|
+
const canonicalTags = new Map(allowedTags.map((tag) => [normalizeTagKey(tag), tag]));
|
|
30
|
+
const reports = [];
|
|
31
|
+
const diffs = [];
|
|
32
|
+
let changedFiles = 0;
|
|
33
|
+
const pages = listPages()
|
|
34
|
+
.filter(isFixablePage)
|
|
35
|
+
.filter((path) => !pathFilter || pathFilter.test(path))
|
|
36
|
+
.sort();
|
|
37
|
+
for (const path of pages) {
|
|
38
|
+
const content = readPage(path);
|
|
39
|
+
if (!content)
|
|
40
|
+
continue;
|
|
41
|
+
const parsedResult = parseWikiFrontmatter(content);
|
|
42
|
+
if (parsedResult.parsed.metadata.autofix === false) {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
const nextFrontmatter = cloneFrontmatter(parsedResult.parsed);
|
|
46
|
+
const body = parsedResult.body;
|
|
47
|
+
const changes = [];
|
|
48
|
+
let unknownTagsForFile = [];
|
|
49
|
+
if (enabledFixes.has("frontmatter-backfill")) {
|
|
50
|
+
let addedTitle = false;
|
|
51
|
+
let addedSummary = false;
|
|
52
|
+
let addedUpdated = false;
|
|
53
|
+
let addedAutostub = false;
|
|
54
|
+
if (!nextFrontmatter.title?.trim()) {
|
|
55
|
+
nextFrontmatter.title = inferTitle(path, body);
|
|
56
|
+
addedTitle = true;
|
|
57
|
+
}
|
|
58
|
+
if (!nextFrontmatter.summary?.trim()) {
|
|
59
|
+
const summary = inferSummary(body);
|
|
60
|
+
nextFrontmatter.summary = summary.text;
|
|
61
|
+
addedSummary = true;
|
|
62
|
+
if (summary.shouldMarkAutostub && nextFrontmatter.autostub !== true && nextFrontmatter.autostub === undefined) {
|
|
63
|
+
nextFrontmatter.autostub = true;
|
|
64
|
+
addedAutostub = true;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (!nextFrontmatter.updated?.trim()) {
|
|
68
|
+
nextFrontmatter.updated = formatDate(statSync(resolveWikiRelativePath(path)).mtime);
|
|
69
|
+
addedUpdated = true;
|
|
70
|
+
}
|
|
71
|
+
const details = [
|
|
72
|
+
...(addedTitle ? ["title"] : []),
|
|
73
|
+
...(addedSummary ? ["summary"] : []),
|
|
74
|
+
...(addedUpdated ? ["updated"] : []),
|
|
75
|
+
...(addedAutostub ? ["autostub"] : []),
|
|
76
|
+
];
|
|
77
|
+
if (details.length > 0) {
|
|
78
|
+
changes.push({ rule: "frontmatter-backfill", details });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (enabledFixes.has("tag-normalize") && nextFrontmatter.tags?.length) {
|
|
82
|
+
const normalizedTags = [];
|
|
83
|
+
const details = [];
|
|
84
|
+
const unknownTags = [];
|
|
85
|
+
for (const tag of nextFrontmatter.tags) {
|
|
86
|
+
const normalized = canonicalTags.get(normalizeTagKey(tag));
|
|
87
|
+
if (normalized) {
|
|
88
|
+
normalizedTags.push(normalized);
|
|
89
|
+
if (normalized !== tag) {
|
|
90
|
+
details.push(`${tag} -> ${normalized}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
normalizedTags.push(tag);
|
|
95
|
+
unknownTags.push(tag);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (details.length > 0) {
|
|
99
|
+
nextFrontmatter.tags = normalizedTags;
|
|
100
|
+
changes.push({ rule: "tag-normalize", details });
|
|
101
|
+
}
|
|
102
|
+
unknownTagsForFile = uniqueStrings(unknownTags);
|
|
103
|
+
}
|
|
104
|
+
if (enabledFixes.has("autostub-mark") && nextFrontmatter.autostub !== true && isStubBody(body)) {
|
|
105
|
+
nextFrontmatter.autostub = true;
|
|
106
|
+
changes.push({ rule: "autostub-mark", details: ["autostub"] });
|
|
107
|
+
}
|
|
108
|
+
if (changes.length === 0 && unknownTagsForFile.length === 0) {
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
const fileReport = {
|
|
112
|
+
path,
|
|
113
|
+
changes,
|
|
114
|
+
};
|
|
115
|
+
if (unknownTagsForFile.length > 0) {
|
|
116
|
+
fileReport.unknownTags = unknownTagsForFile;
|
|
117
|
+
}
|
|
118
|
+
reports.push(fileReport);
|
|
119
|
+
if (changes.length === 0) {
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
changedFiles += 1;
|
|
123
|
+
const nextContent = renderPage(nextFrontmatter, body);
|
|
124
|
+
if (dryRun) {
|
|
125
|
+
diffs.push(renderUnifiedDiff(path, content, nextContent));
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
writePage(path, nextContent);
|
|
129
|
+
for (const change of changes) {
|
|
130
|
+
const logType = logTypeForRule(change.rule);
|
|
131
|
+
if (logType) {
|
|
132
|
+
options.logAction?.(logType, path);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return {
|
|
137
|
+
dryRun,
|
|
138
|
+
scannedFiles: pages.length,
|
|
139
|
+
changedFiles,
|
|
140
|
+
files: reports,
|
|
141
|
+
diff: dryRun ? diffs.join("\n") : "",
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
function cloneFrontmatter(frontmatter) {
|
|
145
|
+
return {
|
|
146
|
+
...frontmatter,
|
|
147
|
+
tags: frontmatter.tags ? [...frontmatter.tags] : undefined,
|
|
148
|
+
contradictions: frontmatter.contradictions ? [...frontmatter.contradictions] : undefined,
|
|
149
|
+
related: frontmatter.related ? [...frontmatter.related] : undefined,
|
|
150
|
+
metadata: { ...frontmatter.metadata },
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
function inferTitle(path, body) {
|
|
154
|
+
const heading = firstHeading(body);
|
|
155
|
+
if (heading) {
|
|
156
|
+
return stripMarkdown(heading);
|
|
157
|
+
}
|
|
158
|
+
return titleFromPath(path);
|
|
159
|
+
}
|
|
160
|
+
function inferSummary(body) {
|
|
161
|
+
const lines = body.split("\n");
|
|
162
|
+
const headingIndex = lines.findIndex((line) => /^#\s+/.test(line.trim()));
|
|
163
|
+
const startIndex = headingIndex >= 0 ? headingIndex + 1 : 0;
|
|
164
|
+
const paragraph = [];
|
|
165
|
+
for (let index = startIndex; index < lines.length; index += 1) {
|
|
166
|
+
const line = lines[index]?.trim() ?? "";
|
|
167
|
+
if (!line) {
|
|
168
|
+
if (paragraph.length > 0)
|
|
169
|
+
break;
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
if (line.startsWith("#")) {
|
|
173
|
+
if (paragraph.length > 0)
|
|
174
|
+
break;
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
paragraph.push(line);
|
|
178
|
+
}
|
|
179
|
+
const summary = stripMarkdown(paragraph.join(" ")).trim();
|
|
180
|
+
if (!summary) {
|
|
181
|
+
return {
|
|
182
|
+
text: "(no summary yet)",
|
|
183
|
+
shouldMarkAutostub: true,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
return {
|
|
187
|
+
text: truncate(summary, 200),
|
|
188
|
+
shouldMarkAutostub: false,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
function firstHeading(body) {
|
|
192
|
+
for (const rawLine of body.split("\n")) {
|
|
193
|
+
const match = rawLine.trim().match(/^#\s+(.+)$/);
|
|
194
|
+
if (match) {
|
|
195
|
+
return match[1].trim();
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return undefined;
|
|
199
|
+
}
|
|
200
|
+
function titleFromPath(path) {
|
|
201
|
+
const file = basename(path, ".md");
|
|
202
|
+
const stem = file === "index" ? basename(dirname(path)) : file;
|
|
203
|
+
return stem
|
|
204
|
+
.split(/[-_]+/)
|
|
205
|
+
.filter(Boolean)
|
|
206
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
207
|
+
.join(" ");
|
|
208
|
+
}
|
|
209
|
+
function stripMarkdown(value) {
|
|
210
|
+
return value
|
|
211
|
+
.replace(/!\[([^\]]*)\]\([^)]+\)/g, "$1")
|
|
212
|
+
.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
|
|
213
|
+
.replace(/[`*_~>#]/g, "")
|
|
214
|
+
.replace(/\s+/g, " ")
|
|
215
|
+
.trim();
|
|
216
|
+
}
|
|
217
|
+
function truncate(value, maxLength) {
|
|
218
|
+
if (value.length <= maxLength) {
|
|
219
|
+
return value;
|
|
220
|
+
}
|
|
221
|
+
return value.slice(0, maxLength).trimEnd();
|
|
222
|
+
}
|
|
223
|
+
function formatDate(date) {
|
|
224
|
+
return date.toISOString().slice(0, 10);
|
|
225
|
+
}
|
|
226
|
+
function normalizeTagKey(tag) {
|
|
227
|
+
return tag.toLowerCase().replace(/[^a-z0-9]+/g, "");
|
|
228
|
+
}
|
|
229
|
+
function isStubBody(body) {
|
|
230
|
+
const meaningfulLines = body
|
|
231
|
+
.split("\n")
|
|
232
|
+
.map((line) => line.trim())
|
|
233
|
+
.filter(Boolean);
|
|
234
|
+
return meaningfulLines.length < 10;
|
|
235
|
+
}
|
|
236
|
+
function renderPage(frontmatter, body) {
|
|
237
|
+
const lines = ["---"];
|
|
238
|
+
for (const field of KNOWN_FIELD_ORDER) {
|
|
239
|
+
const value = frontmatter[field];
|
|
240
|
+
if (value === undefined)
|
|
241
|
+
continue;
|
|
242
|
+
lines.push(formatFrontmatterLine(field, value));
|
|
243
|
+
}
|
|
244
|
+
for (const [key, value] of Object.entries(frontmatter.metadata)) {
|
|
245
|
+
lines.push(formatFrontmatterLine(key, value));
|
|
246
|
+
}
|
|
247
|
+
lines.push("---");
|
|
248
|
+
const normalizedBody = body.replace(/^\n+/, "");
|
|
249
|
+
if (!normalizedBody) {
|
|
250
|
+
return `${lines.join("\n")}\n`;
|
|
251
|
+
}
|
|
252
|
+
return `${lines.join("\n")}\n\n${normalizedBody.endsWith("\n") ? normalizedBody : `${normalizedBody}\n`}`;
|
|
253
|
+
}
|
|
254
|
+
function formatFrontmatterLine(key, value) {
|
|
255
|
+
if (Array.isArray(value)) {
|
|
256
|
+
return `${key}: [${value.map(formatInlineValue).join(", ")}]`;
|
|
257
|
+
}
|
|
258
|
+
if (typeof value === "boolean") {
|
|
259
|
+
return `${key}: ${value ? "true" : "false"}`;
|
|
260
|
+
}
|
|
261
|
+
return `${key}: ${formatScalar(value)}`;
|
|
262
|
+
}
|
|
263
|
+
function formatInlineValue(value) {
|
|
264
|
+
return /^[A-Za-z0-9_./()-]+$/.test(value) ? value : formatScalar(value);
|
|
265
|
+
}
|
|
266
|
+
function formatScalar(value) {
|
|
267
|
+
if (value === "" || /[:#[\]{}'",]|^\s|\s$/.test(value)) {
|
|
268
|
+
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
269
|
+
}
|
|
270
|
+
return value;
|
|
271
|
+
}
|
|
272
|
+
function uniqueStrings(values) {
|
|
273
|
+
return [...new Set(values)];
|
|
274
|
+
}
|
|
275
|
+
function renderUnifiedDiff(path, before, after) {
|
|
276
|
+
const beforeLines = splitLines(before);
|
|
277
|
+
const afterLines = splitLines(after);
|
|
278
|
+
const beforeCount = beforeLines.length;
|
|
279
|
+
const afterCount = afterLines.length;
|
|
280
|
+
const diffLines = [
|
|
281
|
+
`--- a/${path}`,
|
|
282
|
+
`+++ b/${path}`,
|
|
283
|
+
`@@ -1,${beforeCount} +1,${afterCount} @@`,
|
|
284
|
+
...beforeLines.map((line) => `-${line}`),
|
|
285
|
+
...afterLines.map((line) => `+${line}`),
|
|
286
|
+
];
|
|
287
|
+
return diffLines.join("\n");
|
|
288
|
+
}
|
|
289
|
+
function splitLines(content) {
|
|
290
|
+
return content.endsWith("\n")
|
|
291
|
+
? content.slice(0, -1).split("\n")
|
|
292
|
+
: content.split("\n");
|
|
293
|
+
}
|
|
294
|
+
function compileGlob(pattern) {
|
|
295
|
+
let regex = "^";
|
|
296
|
+
for (let index = 0; index < pattern.length; index += 1) {
|
|
297
|
+
const char = pattern[index];
|
|
298
|
+
const next = pattern[index + 1];
|
|
299
|
+
if (char === "*" && next === "*") {
|
|
300
|
+
regex += ".*";
|
|
301
|
+
index += 1;
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
if (char === "*") {
|
|
305
|
+
regex += "[^/]*";
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
if (char === "?") {
|
|
309
|
+
regex += "[^/]";
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
regex += escapeRegex(char);
|
|
313
|
+
}
|
|
314
|
+
regex += "$";
|
|
315
|
+
return new RegExp(regex);
|
|
316
|
+
}
|
|
317
|
+
function escapeRegex(value) {
|
|
318
|
+
return value.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
|
|
319
|
+
}
|
|
320
|
+
function logTypeForRule(rule) {
|
|
321
|
+
switch (rule) {
|
|
322
|
+
case "frontmatter-backfill":
|
|
323
|
+
return "fix-frontmatter";
|
|
324
|
+
case "tag-normalize":
|
|
325
|
+
return "fix-tags";
|
|
326
|
+
case "autostub-mark":
|
|
327
|
+
return "fix-autostub";
|
|
328
|
+
default:
|
|
329
|
+
return undefined;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
function isFixablePage(path) {
|
|
333
|
+
return !path.startsWith("pages/_meta/") && path !== "pages/index.md";
|
|
334
|
+
}
|
|
335
|
+
//# sourceMappingURL=fix.js.map
|