@mulmoclaude/core 4.3.0 → 4.4.1
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/assets/helps/custom-view-remote.md +28 -11
- package/assets/helps/error-recovery.md +20 -3
- package/dist/{graph-BZJxiv1E.cjs → graph-CURotEkH.cjs} +88 -37
- package/dist/graph-CURotEkH.cjs.map +1 -0
- package/dist/{graph-Ce7xsDTt.js → graph-DBxSOx3v.js} +77 -38
- package/dist/graph-DBxSOx3v.js.map +1 -0
- package/dist/scheduler/index.cjs +116 -63
- package/dist/scheduler/index.cjs.map +1 -1
- package/dist/scheduler/index.js +117 -64
- package/dist/scheduler/index.js.map +1 -1
- package/dist/scheduler/schedule-window.d.ts +35 -0
- package/dist/scheduler/task-manager.d.ts +2 -8
- package/dist/{slug-CdN-pQX1.js → slug-BpjumQfe.js} +26 -5
- package/dist/{slug-CdN-pQX1.js.map → slug-BpjumQfe.js.map} +1 -1
- package/dist/{slug-DbwORN9z.cjs → slug-JxgB0ru9.cjs} +31 -4
- package/dist/{slug-DbwORN9z.cjs.map → slug-JxgB0ru9.cjs.map} +1 -1
- package/dist/wiki/graph.d.ts +0 -5
- package/dist/wiki/index.cjs +5 -2
- package/dist/wiki/index.d.ts +3 -2
- package/dist/wiki/index.js +3 -3
- package/dist/wiki/lint.d.ts +8 -9
- package/dist/wiki/paths.cjs +1 -1
- package/dist/wiki/paths.js +1 -1
- package/dist/wiki/resolve.d.ts +24 -0
- package/dist/wiki/server/engine.d.ts +12 -4
- package/dist/wiki/server/index.cjs +46 -34
- package/dist/wiki/server/index.cjs.map +1 -1
- package/dist/wiki/server/index.js +46 -34
- package/dist/wiki/server/index.js.map +1 -1
- package/dist/wiki/slug.d.ts +14 -3
- package/package.json +1 -1
- package/dist/graph-BZJxiv1E.cjs.map +0 -1
- package/dist/graph-Ce7xsDTt.js.map +0 -1
package/dist/wiki/graph.d.ts
CHANGED
|
@@ -16,11 +16,6 @@ export interface WikiPageContent {
|
|
|
16
16
|
slug: string;
|
|
17
17
|
content: string;
|
|
18
18
|
}
|
|
19
|
-
/** Resolve a raw `[[link]]` target to an existing page slug, or null.
|
|
20
|
-
* Mirrors the route resolver's strategy: slugify first, then fall
|
|
21
|
-
* back to matching an index entry title so non-ASCII targets like
|
|
22
|
-
* `[[さくらインターネット]]` resolve to their ASCII file slug. */
|
|
23
|
-
export declare function resolveLinkTarget(target: string, fileSlugs: ReadonlySet<string>, slugByTitle: ReadonlyMap<string, string>): string | null;
|
|
24
19
|
/** Resolved, deduped outgoing slugs for one page body. Self-links and
|
|
25
20
|
* links to non-existent pages are dropped (the latter are already a
|
|
26
21
|
* lint "broken link", not a graph edge). */
|
package/dist/wiki/index.cjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
const require_dist = require("../dist-CsgSfWwR.cjs");
|
|
3
|
-
const require_graph = require("../graph-
|
|
4
|
-
const require_slug = require("../slug-
|
|
3
|
+
const require_graph = require("../graph-CURotEkH.cjs");
|
|
4
|
+
const require_slug = require("../slug-JxgB0ru9.cjs");
|
|
5
5
|
//#region src/wiki/route.ts
|
|
6
6
|
var WIKI_ROUTE_SECTION = {
|
|
7
7
|
pages: "pages",
|
|
@@ -155,6 +155,7 @@ exports.formatLintReport = require_graph.formatLintReport;
|
|
|
155
155
|
exports.incomingLinks = require_graph.incomingLinks;
|
|
156
156
|
exports.isSafeSlug = require_slug.isSafeSlug;
|
|
157
157
|
exports.isSafeWikiSlug = isSafeWikiSlug;
|
|
158
|
+
exports.matchWikiSlug = require_graph.matchWikiSlug;
|
|
158
159
|
exports.pageOutgoingSlugs = require_graph.pageOutgoingSlugs;
|
|
159
160
|
exports.parseIndexEntries = require_graph.parseIndexEntries;
|
|
160
161
|
exports.parseTagsCell = require_graph.parseTagsCell;
|
|
@@ -162,7 +163,9 @@ exports.parseWikiLink = require_graph.parseWikiLink;
|
|
|
162
163
|
exports.readWikiRouteTarget = readWikiRouteTarget;
|
|
163
164
|
exports.renderWikiLinks = renderWikiLinks;
|
|
164
165
|
exports.resolveLinkTarget = require_graph.resolveLinkTarget;
|
|
166
|
+
exports.slugByIndexTitle = require_graph.slugByIndexTitle;
|
|
165
167
|
exports.wikiActionFor = wikiActionFor;
|
|
168
|
+
exports.wikiPageStem = require_slug.wikiPageStem;
|
|
166
169
|
exports.wikiSlugify = require_slug.wikiSlugify;
|
|
167
170
|
|
|
168
171
|
//# sourceMappingURL=index.cjs.map
|
package/dist/wiki/index.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
export { WIKI_LINK_PATTERN, parseWikiLink, type WikiLink } from './link.js';
|
|
2
|
-
export { isSafeSlug, wikiSlugify } from './slug.js';
|
|
2
|
+
export { isSafeSlug, wikiPageStem, wikiSlugify } from './slug.js';
|
|
3
|
+
export { matchWikiSlug, resolveLinkTarget, slugByIndexTitle, type SlugLookup } from './resolve.js';
|
|
3
4
|
export { type WikiPageEntry, BULLET_LINK_PATTERN, BULLET_WIKI_LINK_PATTERN, buildTableColumnMap, extractHashTags, extractSlugFromBulletHref, parseIndexEntries, parseTagsCell, } from './index-parse.js';
|
|
4
5
|
export { findBrokenLinksInPage, findMissingFiles, findOrphanPages, findTagDrift, formatLintReport } from './lint.js';
|
|
5
|
-
export { type WikiGraph, type WikiGraphNode, type WikiGraphEdge, type WikiPageContent, buildWikiGraph, incomingLinks, pageOutgoingSlugs
|
|
6
|
+
export { type WikiGraph, type WikiGraphNode, type WikiGraphEdge, type WikiPageContent, buildWikiGraph, incomingLinks, pageOutgoingSlugs } from './graph.js';
|
|
6
7
|
export { type WikiAction, type WikiRouteSection, type WikiTarget, WIKI_ACTION, WIKI_ROUTE_SECTION, buildWikiRouteParams, isSafeWikiSlug, readWikiRouteTarget, wikiActionFor, } from './route.js';
|
|
7
8
|
export { escapeHtml, renderWikiLinks } from './render.js';
|
package/dist/wiki/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { n as escapeHtml, s as isRecord } from "../dist-D8zokgGo.js";
|
|
2
|
-
import { _ as
|
|
3
|
-
import { n as wikiSlugify, t as isSafeSlug } from "../slug-
|
|
2
|
+
import { _ as resolveLinkTarget, a as findMissingFiles, b as parseWikiLink, c as formatLintReport, d as buildTableColumnMap, f as extractHashTags, g as matchWikiSlug, h as parseTagsCell, i as findBrokenLinksInPage, l as BULLET_LINK_PATTERN, m as parseIndexEntries, n as incomingLinks, o as findOrphanPages, p as extractSlugFromBulletHref, r as pageOutgoingSlugs, s as findTagDrift, t as buildWikiGraph, u as BULLET_WIKI_LINK_PATTERN, v as slugByIndexTitle, y as WIKI_LINK_PATTERN } from "../graph-DBxSOx3v.js";
|
|
3
|
+
import { n as wikiPageStem, r as wikiSlugify, t as isSafeSlug } from "../slug-BpjumQfe.js";
|
|
4
4
|
//#region src/wiki/route.ts
|
|
5
5
|
var WIKI_ROUTE_SECTION = {
|
|
6
6
|
pages: "pages",
|
|
@@ -135,6 +135,6 @@ function findNextCloseBrackets(str, from) {
|
|
|
135
135
|
return -1;
|
|
136
136
|
}
|
|
137
137
|
//#endregion
|
|
138
|
-
export { BULLET_LINK_PATTERN, BULLET_WIKI_LINK_PATTERN, WIKI_ACTION, WIKI_LINK_PATTERN, WIKI_ROUTE_SECTION, buildTableColumnMap, buildWikiGraph, buildWikiRouteParams, escapeHtml, extractHashTags, extractSlugFromBulletHref, findBrokenLinksInPage, findMissingFiles, findOrphanPages, findTagDrift, formatLintReport, incomingLinks, isSafeSlug, isSafeWikiSlug, pageOutgoingSlugs, parseIndexEntries, parseTagsCell, parseWikiLink, readWikiRouteTarget, renderWikiLinks, resolveLinkTarget, wikiActionFor, wikiSlugify };
|
|
138
|
+
export { BULLET_LINK_PATTERN, BULLET_WIKI_LINK_PATTERN, WIKI_ACTION, WIKI_LINK_PATTERN, WIKI_ROUTE_SECTION, buildTableColumnMap, buildWikiGraph, buildWikiRouteParams, escapeHtml, extractHashTags, extractSlugFromBulletHref, findBrokenLinksInPage, findMissingFiles, findOrphanPages, findTagDrift, formatLintReport, incomingLinks, isSafeSlug, isSafeWikiSlug, matchWikiSlug, pageOutgoingSlugs, parseIndexEntries, parseTagsCell, parseWikiLink, readWikiRouteTarget, renderWikiLinks, resolveLinkTarget, slugByIndexTitle, wikiActionFor, wikiPageStem, wikiSlugify };
|
|
139
139
|
|
|
140
140
|
//# sourceMappingURL=index.js.map
|
package/dist/wiki/lint.d.ts
CHANGED
|
@@ -3,16 +3,15 @@ import { WikiPageEntry } from './index-parse.js';
|
|
|
3
3
|
export declare function findOrphanPages(fileSlugs: ReadonlySet<string>, indexedSlugs: ReadonlySet<string>): string[];
|
|
4
4
|
/** Slugs referenced by index.md that have no corresponding file. */
|
|
5
5
|
export declare function findMissingFiles(pageEntries: readonly WikiPageEntry[], fileSlugs: ReadonlySet<string>): string[];
|
|
6
|
-
/** Walk a page's body for `[[…]]` links and flag any whose
|
|
7
|
-
*
|
|
6
|
+
/** Walk a page's body for `[[…]]` links and flag any whose target
|
|
7
|
+
* doesn't resolve to a file.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
|
|
15
|
-
export declare function findBrokenLinksInPage(fileName: string, content: string, fileSlugs: ReadonlySet<string>): string[];
|
|
9
|
+
* `slugByTitle` (from `slugByIndexTitle`) is what lets a link written
|
|
10
|
+
* as an index.md display title count as resolved — without it the
|
|
11
|
+
* lint calls broken exactly the links the page resolver and the graph
|
|
12
|
+
* follow happily. It defaults to empty so a caller linting an
|
|
13
|
+
* in-memory snapshot with no index still works. */
|
|
14
|
+
export declare function findBrokenLinksInPage(fileName: string, content: string, fileSlugs: ReadonlySet<string>, slugByTitle?: ReadonlyMap<string, string>): string[];
|
|
16
15
|
/** Flag any slug whose index.md tags differ from the page's own
|
|
17
16
|
* frontmatter `tags:` field. Comparison is set-based and order-
|
|
18
17
|
* insensitive; both sides are lowercased at parse time. Slugs
|
package/dist/wiki/paths.cjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
const require_rolldown_runtime = require("../rolldown-runtime-VH7oDXx4.cjs");
|
|
3
|
-
const require_slug = require("../slug-
|
|
3
|
+
const require_slug = require("../slug-JxgB0ru9.cjs");
|
|
4
4
|
let node_path = require("node:path");
|
|
5
5
|
node_path = require_rolldown_runtime.__toESM(node_path, 1);
|
|
6
6
|
//#region src/wiki/server/paths.ts
|
package/dist/wiki/paths.js
CHANGED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { WikiPageEntry } from './index-parse.js';
|
|
2
|
+
/** Anything that can answer "do you hold this slug?" — the page index
|
|
3
|
+
* (`Map<slug, filename>`) and the lint's `Set<slug>` both qualify. */
|
|
4
|
+
export interface SlugLookup {
|
|
5
|
+
has: (slug: string) => boolean;
|
|
6
|
+
}
|
|
7
|
+
/** Match a raw link target against known page slugs, returning the
|
|
8
|
+
* matched slug or null.
|
|
9
|
+
*
|
|
10
|
+
* Literal first: page slugs ARE filename stems, so a non-ASCII page
|
|
11
|
+
* only ever matches its own untouched name — `wikiSlugify` strips
|
|
12
|
+
* every non-ASCII character and would reduce `不耕起栽培-カバークロップ4年計画`
|
|
13
|
+
* to `-4` (#2940). The slugified form still runs second so
|
|
14
|
+
* `[[Sakura Internet]]` keeps resolving to `sakura-internet.md`. */
|
|
15
|
+
export declare function matchWikiSlug(target: string, known: SlugLookup): string | null;
|
|
16
|
+
/** index.md title → slug, first entry wins. The fallback for links
|
|
17
|
+
* written as the display title rather than the page's own name. */
|
|
18
|
+
export declare function slugByIndexTitle(entries: readonly WikiPageEntry[]): Map<string, string>;
|
|
19
|
+
/** Resolve a raw `[[link]]` target to an existing page slug, or null:
|
|
20
|
+
* known slug first, then an index entry's title. Every consumer that
|
|
21
|
+
* judges whether a link points at a real page — the graph and the
|
|
22
|
+
* lint — shares this, so a link cannot be an edge in one and a broken
|
|
23
|
+
* link in the other. */
|
|
24
|
+
export declare function resolveLinkTarget(target: string, fileSlugs: SlugLookup, slugByTitle: ReadonlyMap<string, string>): string | null;
|
|
@@ -6,10 +6,10 @@ import { WikiGraph } from '../graph.js';
|
|
|
6
6
|
* title-match fallback). Score = min/max length, decoupled from Map
|
|
7
7
|
* iteration order so resolution is deterministic across hosts. */
|
|
8
8
|
export declare function pickFuzzyMatch(slug: string, slugs: ReadonlyMap<string, string>): string | null;
|
|
9
|
-
/** Resolve a page name to an absolute `.md` path:
|
|
10
|
-
* index-title fallback
|
|
11
|
-
*
|
|
12
|
-
*
|
|
9
|
+
/** Resolve a page name to an absolute `.md` path: known slug (literal
|
|
10
|
+
* or slugified) → fuzzy → index-title fallback. `pageName` may carry
|
|
11
|
+
* the `[[target|display]]` form; `parseWikiLink` strips the display
|
|
12
|
+
* half so the lookup uses just the target. */
|
|
13
13
|
export declare function resolvePagePath(workspace: string, pageName: string): Promise<string | null>;
|
|
14
14
|
/** Raw `index.md` content + its parsed entries. */
|
|
15
15
|
export declare function readWikiIndex(workspace: string): {
|
|
@@ -35,5 +35,13 @@ export declare function readWikiPage(workspace: string, pageName: string): Promi
|
|
|
35
35
|
* No cache: the graph is requested explicitly and a content edit does
|
|
36
36
|
* not advance the pagesDir mtime the page index caches on. */
|
|
37
37
|
export declare function loadWikiGraph(workspace: string): Promise<WikiGraph>;
|
|
38
|
+
export interface PageBody {
|
|
39
|
+
fileName: string;
|
|
40
|
+
content: string;
|
|
41
|
+
}
|
|
42
|
+
/** Frontmatter `tags:` per page, keyed for `findTagDrift`. Lowercased so
|
|
43
|
+
* a `MyPage.md` filename matches an `entry.slug` of `mypage`, which is
|
|
44
|
+
* what the drift rule looks up. */
|
|
45
|
+
export declare function frontmatterTagIndex(bodies: readonly PageBody[]): Map<string, string[]>;
|
|
38
46
|
/** Run every lint rule over the on-disk wiki, returning issue strings. */
|
|
39
47
|
export declare function collectLintIssues(workspace: string): Promise<string[]>;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
const require_rolldown_runtime = require("../../rolldown-runtime-VH7oDXx4.cjs");
|
|
3
|
-
const require_graph = require("../../graph-
|
|
4
|
-
const require_slug = require("../../slug-
|
|
3
|
+
const require_graph = require("../../graph-CURotEkH.cjs");
|
|
4
|
+
const require_slug = require("../../slug-JxgB0ru9.cjs");
|
|
5
5
|
const require_wiki_paths = require("../paths.cjs");
|
|
6
6
|
let node_fs = require("node:fs");
|
|
7
7
|
let node_path = require("node:path");
|
|
@@ -113,28 +113,31 @@ function pickFuzzyMatch(slug, slugs) {
|
|
|
113
113
|
}
|
|
114
114
|
return bestIsTied ? null : bestFile;
|
|
115
115
|
}
|
|
116
|
-
/**
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
|
|
116
|
+
/** File matching an index.md entry whose title equals `target` — the
|
|
117
|
+
* last resort for a link written as the display title rather than
|
|
118
|
+
* the page's own name. */
|
|
119
|
+
function fileByIndexTitle(indexFile, target, slugs) {
|
|
120
|
+
const entries = require_graph.parseIndexEntries(readFileOrEmpty(indexFile));
|
|
121
|
+
const trimmed = target.trim();
|
|
122
|
+
const titleMatch = entries.find((entry) => entry.title === trimmed);
|
|
123
|
+
return titleMatch ? slugs.get(titleMatch.slug) : void 0;
|
|
124
|
+
}
|
|
125
|
+
/** Resolve a page name to an absolute `.md` path: known slug (literal
|
|
126
|
+
* or slugified) → fuzzy → index-title fallback. `pageName` may carry
|
|
127
|
+
* the `[[target|display]]` form; `parseWikiLink` strips the display
|
|
128
|
+
* half so the lookup uses just the target. */
|
|
120
129
|
async function resolvePagePath(workspace, pageName) {
|
|
121
130
|
const { pagesDir, indexFile } = require_wiki_paths.wikiDirs(workspace);
|
|
122
131
|
const { slugs } = await getPageIndex(pagesDir);
|
|
123
132
|
if (slugs.size === 0) return null;
|
|
124
133
|
const { target } = require_graph.parseWikiLink(pageName);
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
const titleMatch = require_graph.parseIndexEntries(readFileOrEmpty(indexFile)).find((entry) => entry.title === target);
|
|
133
|
-
if (titleMatch) {
|
|
134
|
-
const file = slugs.get(titleMatch.slug);
|
|
135
|
-
if (file) return node_path.default.join(pagesDir, file);
|
|
136
|
-
}
|
|
137
|
-
return null;
|
|
134
|
+
const matched = require_graph.matchWikiSlug(target, slugs);
|
|
135
|
+
const matchedFile = matched === null ? void 0 : slugs.get(matched);
|
|
136
|
+
if (matchedFile) return node_path.default.join(pagesDir, matchedFile);
|
|
137
|
+
const fuzzy = pickFuzzyMatch(require_slug.wikiSlugify(target), slugs);
|
|
138
|
+
if (fuzzy) return node_path.default.join(pagesDir, fuzzy);
|
|
139
|
+
const titleFile = fileByIndexTitle(indexFile, target, slugs);
|
|
140
|
+
return titleFile ? node_path.default.join(pagesDir, titleFile) : null;
|
|
138
141
|
}
|
|
139
142
|
/** Raw `index.md` content + its parsed entries. */
|
|
140
143
|
function readWikiIndex(workspace) {
|
|
@@ -178,28 +181,37 @@ async function loadWikiGraph(workspace) {
|
|
|
178
181
|
const indexEntries = require_graph.parseIndexEntries(readFileOrEmpty(indexFile));
|
|
179
182
|
return require_graph.buildWikiGraph(pages, indexEntries);
|
|
180
183
|
}
|
|
184
|
+
async function readPageBodies(pagesDir, fileNames) {
|
|
185
|
+
return Promise.all(fileNames.map(async (fileName) => ({
|
|
186
|
+
fileName,
|
|
187
|
+
content: await readPageBody(pagesDir, fileName)
|
|
188
|
+
})));
|
|
189
|
+
}
|
|
190
|
+
/** Frontmatter `tags:` per page, keyed for `findTagDrift`. Lowercased so
|
|
191
|
+
* a `MyPage.md` filename matches an `entry.slug` of `mypage`, which is
|
|
192
|
+
* what the drift rule looks up. */
|
|
193
|
+
function frontmatterTagIndex(bodies) {
|
|
194
|
+
const tagsBySlug = /* @__PURE__ */ new Map();
|
|
195
|
+
bodies.forEach(({ fileName, content }) => {
|
|
196
|
+
tagsBySlug.set(fileName.replace(/\.md$/i, "").toLowerCase(), parseFrontmatterTags(content));
|
|
197
|
+
});
|
|
198
|
+
return tagsBySlug;
|
|
199
|
+
}
|
|
181
200
|
/** Run every lint rule over the on-disk wiki, returning issue strings. */
|
|
182
201
|
async function collectLintIssues(workspace) {
|
|
183
202
|
const { pagesDir, indexFile } = require_wiki_paths.wikiDirs(workspace);
|
|
184
203
|
const { slugs } = await getPageIndex(pagesDir);
|
|
185
204
|
if (slugs.size === 0) return ["- Wiki `pages/` directory does not exist yet. Start ingesting sources."];
|
|
186
205
|
const pageEntries = require_graph.parseIndexEntries(readFileOrEmpty(indexFile));
|
|
187
|
-
const indexedSlugs = new Set(pageEntries.map((entry) => entry.slug));
|
|
188
206
|
const fileSlugs = new Set(slugs.keys());
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
for (const { fileName, content } of bodies) {
|
|
198
|
-
issues.push(...require_graph.findBrokenLinksInPage(fileName, content, fileSlugs));
|
|
199
|
-
frontmatterTagsBySlug.set(fileName.replace(/\.md$/i, "").toLowerCase(), parseFrontmatterTags(content));
|
|
200
|
-
}
|
|
201
|
-
issues.push(...require_graph.findTagDrift(pageEntries, frontmatterTagsBySlug));
|
|
202
|
-
return issues;
|
|
207
|
+
const slugByTitle = require_graph.slugByIndexTitle(pageEntries);
|
|
208
|
+
const bodies = await readPageBodies(pagesDir, [...slugs.values()]);
|
|
209
|
+
return [
|
|
210
|
+
...require_graph.findOrphanPages(fileSlugs, new Set(pageEntries.map((entry) => entry.slug))),
|
|
211
|
+
...require_graph.findMissingFiles(pageEntries, fileSlugs),
|
|
212
|
+
...bodies.flatMap(({ fileName, content }) => require_graph.findBrokenLinksInPage(fileName, content, fileSlugs, slugByTitle)),
|
|
213
|
+
...require_graph.findTagDrift(pageEntries, frontmatterTagIndex(bodies))
|
|
214
|
+
];
|
|
203
215
|
}
|
|
204
216
|
//#endregion
|
|
205
217
|
exports.__resetPageIndexCache = __resetPageIndexCache;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":[],"sources":["../../../src/wiki/server/fs.ts","../../../src/wiki/server/pageIndex.ts","../../../src/wiki/server/frontmatter.ts","../../../src/wiki/server/engine.ts"],"sourcesContent":["// Minimal ENOENT-swallowing fs wrappers for the wiki read-engine.\n// Self-contained (core can't reach the host's `server/utils/files/safe.ts`);\n// only the handful of helpers the wiki engine needs. Same \"return\n// null / [] on any error\" contract so callers branch on the value\n// instead of try/catch.\n\nimport type { Dirent, Stats } from \"node:fs\";\nimport { readFileSync } from \"node:fs\";\nimport { readFile, readdir, stat } from \"node:fs/promises\";\n\nexport async function readTextSafe(absPath: string): Promise<string | null> {\n try {\n return await readFile(absPath, \"utf-8\");\n } catch {\n return null;\n }\n}\n\nexport function readTextSafeSync(absPath: string): string | null {\n try {\n return readFileSync(absPath, \"utf-8\");\n } catch {\n return null;\n }\n}\n\nexport async function statSafeAsync(absPath: string): Promise<Stats | null> {\n try {\n return await stat(absPath);\n } catch {\n return null;\n }\n}\n\nexport async function readDirSafeAsync(absPath: string): Promise<Dirent[]> {\n try {\n return await readdir(absPath, { withFileTypes: true });\n } catch {\n return [];\n }\n}\n","// In-memory index of `<pagesDir>/*.md` keyed by slug (= filename\n// without the `.md` extension). Kept fresh via `pagesDir` mtime —\n// adding, removing, or renaming a file under `pagesDir` advances the\n// directory's mtime on every major filesystem we target (macOS APFS,\n// Linux ext4, Windows NTFS), so one cheap `stat()` per request is\n// enough to decide whether to rebuild.\n\nimport { readDirSafeAsync, statSafeAsync } from \"./fs.js\";\n\nexport interface PageIndex {\n mtimeMs: number;\n /** slug → filename (e.g. \"sakura-internet\" → \"sakura-internet.md\"). */\n slugs: Map<string, string>;\n}\n\n// Keyed by `pagesDir`: the engine is workspace-injected and may be\n// called with more than one pages directory in a single process. A\n// single global slot would let one workspace's slug map be returned\n// for another whenever their mtimes happen to tie (Codex P2 on #1876).\nconst cache = new Map<string, PageIndex>();\n\n/**\n * Get the page index for `pagesDir`. Returns a cached value as long as\n * THAT directory's mtime hasn't advanced; otherwise rebuilds. Safe to\n * call concurrently — racing builds produce the same result.\n */\nexport async function getPageIndex(pagesDir: string): Promise<PageIndex> {\n const stat = await statSafeAsync(pagesDir);\n if (!stat) {\n // Dir doesn't exist yet (never ingested). Return empty but don't\n // cache a stale-forever value — the next call re-stats.\n return { mtimeMs: 0, slugs: new Map() };\n }\n const cached = cache.get(pagesDir);\n if (cached && cached.mtimeMs >= stat.mtimeMs) return cached;\n const entries = await readDirSafeAsync(pagesDir);\n const slugs = new Map<string, string>();\n for (const entry of entries) {\n if (!entry.isFile()) continue;\n const { name } = entry;\n if (!name.endsWith(\".md\")) continue;\n slugs.set(name.slice(0, -\".md\".length), name);\n }\n const built: PageIndex = { mtimeMs: stat.mtimeMs, slugs };\n cache.set(pagesDir, built);\n return built;\n}\n\n/** Test-only: drop the module-level cache (all directories). */\nexport function __resetPageIndexCache(): void {\n cache.clear();\n}\n","// Read-side frontmatter helpers for the wiki engine. The generic\n// `---\\n…\\n---\\n` envelope parsing is the canonical value-preserving\n// (FAILSAFE_SCHEMA) parser in `@mulmoclaude/markdown-utils`; only the\n// wiki-specific `tags:` reader stays here. Re-exporting `parseFrontmatter`\n// keeps the `@mulmoclaude/core/wiki/server` barrel surface stable — it now\n// returns the richer `{ meta, body, hasHeader }` shape.\n\nimport { parseFrontmatter } from \"@mulmoclaude/markdown-utils/markdown/frontmatter\";\n\nexport { parseFrontmatter };\n\nfunction cleanTagToken(token: string): string {\n return token\n .trim()\n .replace(/^['\"]|['\"]$/g, \"\")\n .replace(/^#/, \"\")\n .toLowerCase();\n}\n\n/** Narrow `tags:` reader. Handles flow (`tags: [a, b]`) and block-list\n * style; anything unparseable returns `[]` (best-effort, never throws). */\nexport function parseFrontmatterTags(content: string): string[] {\n const parsed = parseFrontmatter(content);\n if (!parsed.hasHeader) return [];\n const tagsValue = parsed.meta.tags;\n if (!Array.isArray(tagsValue)) return [];\n return tagsValue\n .filter((item): item is string => typeof item === \"string\")\n .map(cleanTagToken)\n .filter((token) => token.length > 0);\n}\n","// Workspace-injected wiki read-engine — the filesystem layer that feeds\n// the pure helpers in `@mulmoclaude/core/wiki`. Shared by every host so\n// the two apps reading the same `data/wiki/` can't disagree on slug\n// resolution, graph edges, or lint findings. Mirrors the bodies that\n// previously lived in MulmoClaude's `server/api/routes/wiki.ts`; the\n// hosts keep only their HTTP response shaping on top.\n//\n// The read/write WRITE side (writeWikiPage / snapshots) stays host-side\n// until a host needs it shared.\n\nimport path from \"node:path\";\nimport { parseWikiLink } from \"../link.js\";\nimport { wikiSlugify } from \"../slug.js\";\nimport { type WikiPageEntry, parseIndexEntries } from \"../index-parse.js\";\nimport { findBrokenLinksInPage, findMissingFiles, findOrphanPages, findTagDrift } from \"../lint.js\";\nimport { type WikiGraph, buildWikiGraph } from \"../graph.js\";\nimport { readTextSafe, readTextSafeSync } from \"./fs.js\";\nimport { getPageIndex } from \"./pageIndex.js\";\nimport { parseFrontmatterTags } from \"./frontmatter.js\";\nimport { wikiDirs } from \"./paths.js\";\n\n// Below this length the fuzzy `includes` step is skipped — CJK /\n// emoji-only / very-short page names slugify down to a short noise\n// tail that partial-matches almost anything; the index.md title-match\n// fallback still handles the legitimate non-ASCII case (#1194).\nconst MIN_FUZZY_SLUG_LEN = 6;\n\nfunction readFileOrEmpty(absPath: string): string {\n return readTextSafeSync(absPath) ?? \"\";\n}\n\n/** Walk every indexed slug for an `includes`-style match. Returns the\n * single best candidate, or null when the slug is too short OR several\n * candidates tie at the top score (ambiguous → defer to the caller's\n * title-match fallback). Score = min/max length, decoupled from Map\n * iteration order so resolution is deterministic across hosts. */\nexport function pickFuzzyMatch(slug: string, slugs: ReadonlyMap<string, string>): string | null {\n if (slug.length < MIN_FUZZY_SLUG_LEN) return null;\n let bestFile: string | null = null;\n let bestScore = 0;\n let bestIsTied = false;\n for (const [key, file] of slugs) {\n if (!slug.includes(key) && !key.includes(slug)) continue;\n const shorter = Math.min(slug.length, key.length);\n const longer = Math.max(slug.length, key.length);\n const score = shorter / longer;\n if (score > bestScore) {\n bestScore = score;\n bestFile = file;\n bestIsTied = false;\n } else if (score === bestScore) {\n bestIsTied = true;\n }\n }\n return bestIsTied ? null : bestFile;\n}\n\n/** Resolve a page name to an absolute `.md` path: exact slug → fuzzy →\n * index-title fallback (for non-ASCII names that slugify to empty).\n * `pageName` may carry the `[[target|display]]` form; `parseWikiLink`\n * strips the display half so the lookup uses just the target. */\nexport async function resolvePagePath(workspace: string, pageName: string): Promise<string | null> {\n const { pagesDir, indexFile } = wikiDirs(workspace);\n const { slugs } = await getPageIndex(pagesDir);\n if (slugs.size === 0) return null;\n\n const { target } = parseWikiLink(pageName);\n const slug = wikiSlugify(target);\n\n if (slug.length > 0) {\n const exact = slugs.get(slug);\n if (exact) return path.join(pagesDir, exact);\n const fuzzy = pickFuzzyMatch(slug, slugs);\n if (fuzzy) return path.join(pagesDir, fuzzy);\n }\n\n const entries = parseIndexEntries(readFileOrEmpty(indexFile));\n const titleMatch = entries.find((entry) => entry.title === target);\n if (titleMatch) {\n const file = slugs.get(titleMatch.slug);\n if (file) return path.join(pagesDir, file);\n }\n return null;\n}\n\n/** Raw `index.md` content + its parsed entries. */\nexport function readWikiIndex(workspace: string): { content: string; entries: WikiPageEntry[] } {\n const content = readFileOrEmpty(wikiDirs(workspace).indexFile);\n return { content, entries: parseIndexEntries(content) };\n}\n\n/** Raw `log.md` content (empty string if absent). */\nexport function readWikiLog(workspace: string): string {\n return readFileOrEmpty(wikiDirs(workspace).logFile);\n}\n\nexport interface WikiPageRead {\n /** Absolute path of the resolved file, or null when nothing matched. */\n filePath: string | null;\n /** File body (empty when missing OR when the file is an empty placeholder). */\n content: string;\n /** True iff a page file resolved (distinct from empty content). */\n exists: boolean;\n /** Title to display — the resolved filename stem, or the raw pageName. */\n resolvedTitle: string;\n}\n\n/** Resolve + read a page. Distinguishes missing (`exists: false`) from\n * empty-but-present (`exists: true`, `content: \"\"`). */\nexport async function readWikiPage(workspace: string, pageName: string): Promise<WikiPageRead> {\n const filePath = await resolvePagePath(workspace, pageName);\n const content = filePath ? readFileOrEmpty(filePath) : \"\";\n const resolvedTitle = filePath ? path.basename(filePath, \".md\") : pageName;\n return { filePath, content, exists: Boolean(filePath), resolvedTitle };\n}\n\n/** Page body, or \"\" when the file vanished between indexing and reading. */\nasync function readPageBody(pagesDir: string, fileName: string): Promise<string> {\n return (await readTextSafe(path.join(pagesDir, fileName))) ?? \"\";\n}\n\n/** Read every page + the index and build the page→page link graph.\n * No cache: the graph is requested explicitly and a content edit does\n * not advance the pagesDir mtime the page index caches on. */\nexport async function loadWikiGraph(workspace: string): Promise<WikiGraph> {\n const { pagesDir, indexFile } = wikiDirs(workspace);\n const { slugs } = await getPageIndex(pagesDir);\n const pages = await Promise.all([...slugs.entries()].map(async ([slug, fileName]) => ({ slug, content: await readPageBody(pagesDir, fileName) })));\n const indexEntries = parseIndexEntries(readFileOrEmpty(indexFile));\n return buildWikiGraph(pages, indexEntries);\n}\n\n/** Run every lint rule over the on-disk wiki, returning issue strings. */\nexport async function collectLintIssues(workspace: string): Promise<string[]> {\n const { pagesDir, indexFile } = wikiDirs(workspace);\n const { slugs } = await getPageIndex(pagesDir);\n if (slugs.size === 0) {\n return [\"- Wiki `pages/` directory does not exist yet. Start ingesting sources.\"];\n }\n const pageEntries = parseIndexEntries(readFileOrEmpty(indexFile));\n const indexedSlugs = new Set(pageEntries.map((entry) => entry.slug));\n const fileSlugs = new Set(slugs.keys());\n const bodies = await Promise.all([...slugs.values()].map(async (fileName) => ({ fileName, content: await readPageBody(pagesDir, fileName) })));\n\n const issues: string[] = [];\n issues.push(...findOrphanPages(fileSlugs, indexedSlugs));\n issues.push(...findMissingFiles(pageEntries, fileSlugs));\n const frontmatterTagsBySlug = new Map<string, string[]>();\n for (const { fileName, content } of bodies) {\n issues.push(...findBrokenLinksInPage(fileName, content, fileSlugs));\n // Lowercase the key so a `MyPage.md` filename matches an\n // `entry.slug` of `mypage`; `findTagDrift` lowercases the lookup.\n frontmatterTagsBySlug.set(fileName.replace(/\\.md$/i, \"\").toLowerCase(), parseFrontmatterTags(content));\n }\n issues.push(...findTagDrift(pageEntries, frontmatterTagsBySlug));\n return issues;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.cjs","names":[],"sources":["../../../src/wiki/server/fs.ts","../../../src/wiki/server/pageIndex.ts","../../../src/wiki/server/frontmatter.ts","../../../src/wiki/server/engine.ts"],"sourcesContent":["// Minimal ENOENT-swallowing fs wrappers for the wiki read-engine.\n// Self-contained (core can't reach the host's `server/utils/files/safe.ts`);\n// only the handful of helpers the wiki engine needs. Same \"return\n// null / [] on any error\" contract so callers branch on the value\n// instead of try/catch.\n\nimport type { Dirent, Stats } from \"node:fs\";\nimport { readFileSync } from \"node:fs\";\nimport { readFile, readdir, stat } from \"node:fs/promises\";\n\nexport async function readTextSafe(absPath: string): Promise<string | null> {\n try {\n return await readFile(absPath, \"utf-8\");\n } catch {\n return null;\n }\n}\n\nexport function readTextSafeSync(absPath: string): string | null {\n try {\n return readFileSync(absPath, \"utf-8\");\n } catch {\n return null;\n }\n}\n\nexport async function statSafeAsync(absPath: string): Promise<Stats | null> {\n try {\n return await stat(absPath);\n } catch {\n return null;\n }\n}\n\nexport async function readDirSafeAsync(absPath: string): Promise<Dirent[]> {\n try {\n return await readdir(absPath, { withFileTypes: true });\n } catch {\n return [];\n }\n}\n","// In-memory index of `<pagesDir>/*.md` keyed by slug (= filename\n// without the `.md` extension). Kept fresh via `pagesDir` mtime —\n// adding, removing, or renaming a file under `pagesDir` advances the\n// directory's mtime on every major filesystem we target (macOS APFS,\n// Linux ext4, Windows NTFS), so one cheap `stat()` per request is\n// enough to decide whether to rebuild.\n\nimport { readDirSafeAsync, statSafeAsync } from \"./fs.js\";\n\nexport interface PageIndex {\n mtimeMs: number;\n /** slug → filename (e.g. \"sakura-internet\" → \"sakura-internet.md\"). */\n slugs: Map<string, string>;\n}\n\n// Keyed by `pagesDir`: the engine is workspace-injected and may be\n// called with more than one pages directory in a single process. A\n// single global slot would let one workspace's slug map be returned\n// for another whenever their mtimes happen to tie (Codex P2 on #1876).\nconst cache = new Map<string, PageIndex>();\n\n/**\n * Get the page index for `pagesDir`. Returns a cached value as long as\n * THAT directory's mtime hasn't advanced; otherwise rebuilds. Safe to\n * call concurrently — racing builds produce the same result.\n */\nexport async function getPageIndex(pagesDir: string): Promise<PageIndex> {\n const stat = await statSafeAsync(pagesDir);\n if (!stat) {\n // Dir doesn't exist yet (never ingested). Return empty but don't\n // cache a stale-forever value — the next call re-stats.\n return { mtimeMs: 0, slugs: new Map() };\n }\n const cached = cache.get(pagesDir);\n if (cached && cached.mtimeMs >= stat.mtimeMs) return cached;\n const entries = await readDirSafeAsync(pagesDir);\n const slugs = new Map<string, string>();\n for (const entry of entries) {\n if (!entry.isFile()) continue;\n const { name } = entry;\n if (!name.endsWith(\".md\")) continue;\n slugs.set(name.slice(0, -\".md\".length), name);\n }\n const built: PageIndex = { mtimeMs: stat.mtimeMs, slugs };\n cache.set(pagesDir, built);\n return built;\n}\n\n/** Test-only: drop the module-level cache (all directories). */\nexport function __resetPageIndexCache(): void {\n cache.clear();\n}\n","// Read-side frontmatter helpers for the wiki engine. The generic\n// `---\\n…\\n---\\n` envelope parsing is the canonical value-preserving\n// (FAILSAFE_SCHEMA) parser in `@mulmoclaude/markdown-utils`; only the\n// wiki-specific `tags:` reader stays here. Re-exporting `parseFrontmatter`\n// keeps the `@mulmoclaude/core/wiki/server` barrel surface stable — it now\n// returns the richer `{ meta, body, hasHeader }` shape.\n\nimport { parseFrontmatter } from \"@mulmoclaude/markdown-utils/markdown/frontmatter\";\n\nexport { parseFrontmatter };\n\nfunction cleanTagToken(token: string): string {\n return token\n .trim()\n .replace(/^['\"]|['\"]$/g, \"\")\n .replace(/^#/, \"\")\n .toLowerCase();\n}\n\n/** Narrow `tags:` reader. Handles flow (`tags: [a, b]`) and block-list\n * style; anything unparseable returns `[]` (best-effort, never throws). */\nexport function parseFrontmatterTags(content: string): string[] {\n const parsed = parseFrontmatter(content);\n if (!parsed.hasHeader) return [];\n const tagsValue = parsed.meta.tags;\n if (!Array.isArray(tagsValue)) return [];\n return tagsValue\n .filter((item): item is string => typeof item === \"string\")\n .map(cleanTagToken)\n .filter((token) => token.length > 0);\n}\n","// Workspace-injected wiki read-engine — the filesystem layer that feeds\n// the pure helpers in `@mulmoclaude/core/wiki`. Shared by every host so\n// the two apps reading the same `data/wiki/` can't disagree on slug\n// resolution, graph edges, or lint findings. Mirrors the bodies that\n// previously lived in MulmoClaude's `server/api/routes/wiki.ts`; the\n// hosts keep only their HTTP response shaping on top.\n//\n// The read/write WRITE side (writeWikiPage / snapshots) stays host-side\n// until a host needs it shared.\n\nimport path from \"node:path\";\nimport { parseWikiLink } from \"../link.js\";\nimport { wikiSlugify } from \"../slug.js\";\nimport { matchWikiSlug, slugByIndexTitle } from \"../resolve.js\";\nimport { type WikiPageEntry, parseIndexEntries } from \"../index-parse.js\";\nimport { findBrokenLinksInPage, findMissingFiles, findOrphanPages, findTagDrift } from \"../lint.js\";\nimport { type WikiGraph, buildWikiGraph } from \"../graph.js\";\nimport { readTextSafe, readTextSafeSync } from \"./fs.js\";\nimport { getPageIndex } from \"./pageIndex.js\";\nimport { parseFrontmatterTags } from \"./frontmatter.js\";\nimport { wikiDirs } from \"./paths.js\";\n\n// Below this length the fuzzy `includes` step is skipped — CJK /\n// emoji-only / very-short page names slugify down to a short noise\n// tail that partial-matches almost anything; the index.md title-match\n// fallback still handles the legitimate non-ASCII case (#1194).\nconst MIN_FUZZY_SLUG_LEN = 6;\n\nfunction readFileOrEmpty(absPath: string): string {\n return readTextSafeSync(absPath) ?? \"\";\n}\n\n/** Walk every indexed slug for an `includes`-style match. Returns the\n * single best candidate, or null when the slug is too short OR several\n * candidates tie at the top score (ambiguous → defer to the caller's\n * title-match fallback). Score = min/max length, decoupled from Map\n * iteration order so resolution is deterministic across hosts. */\nexport function pickFuzzyMatch(slug: string, slugs: ReadonlyMap<string, string>): string | null {\n if (slug.length < MIN_FUZZY_SLUG_LEN) return null;\n let bestFile: string | null = null;\n let bestScore = 0;\n let bestIsTied = false;\n for (const [key, file] of slugs) {\n if (!slug.includes(key) && !key.includes(slug)) continue;\n const shorter = Math.min(slug.length, key.length);\n const longer = Math.max(slug.length, key.length);\n const score = shorter / longer;\n if (score > bestScore) {\n bestScore = score;\n bestFile = file;\n bestIsTied = false;\n } else if (score === bestScore) {\n bestIsTied = true;\n }\n }\n return bestIsTied ? null : bestFile;\n}\n\n/** File matching an index.md entry whose title equals `target` — the\n * last resort for a link written as the display title rather than\n * the page's own name. */\nfunction fileByIndexTitle(indexFile: string, target: string, slugs: ReadonlyMap<string, string>): string | undefined {\n const entries = parseIndexEntries(readFileOrEmpty(indexFile));\n const trimmed = target.trim();\n const titleMatch = entries.find((entry) => entry.title === trimmed);\n return titleMatch ? slugs.get(titleMatch.slug) : undefined;\n}\n\n/** Resolve a page name to an absolute `.md` path: known slug (literal\n * or slugified) → fuzzy → index-title fallback. `pageName` may carry\n * the `[[target|display]]` form; `parseWikiLink` strips the display\n * half so the lookup uses just the target. */\nexport async function resolvePagePath(workspace: string, pageName: string): Promise<string | null> {\n const { pagesDir, indexFile } = wikiDirs(workspace);\n const { slugs } = await getPageIndex(pagesDir);\n if (slugs.size === 0) return null;\n\n const { target } = parseWikiLink(pageName);\n const matched = matchWikiSlug(target, slugs);\n const matchedFile = matched === null ? undefined : slugs.get(matched);\n if (matchedFile) return path.join(pagesDir, matchedFile);\n\n const fuzzy = pickFuzzyMatch(wikiSlugify(target), slugs);\n if (fuzzy) return path.join(pagesDir, fuzzy);\n\n const titleFile = fileByIndexTitle(indexFile, target, slugs);\n return titleFile ? path.join(pagesDir, titleFile) : null;\n}\n\n/** Raw `index.md` content + its parsed entries. */\nexport function readWikiIndex(workspace: string): { content: string; entries: WikiPageEntry[] } {\n const content = readFileOrEmpty(wikiDirs(workspace).indexFile);\n return { content, entries: parseIndexEntries(content) };\n}\n\n/** Raw `log.md` content (empty string if absent). */\nexport function readWikiLog(workspace: string): string {\n return readFileOrEmpty(wikiDirs(workspace).logFile);\n}\n\nexport interface WikiPageRead {\n /** Absolute path of the resolved file, or null when nothing matched. */\n filePath: string | null;\n /** File body (empty when missing OR when the file is an empty placeholder). */\n content: string;\n /** True iff a page file resolved (distinct from empty content). */\n exists: boolean;\n /** Title to display — the resolved filename stem, or the raw pageName. */\n resolvedTitle: string;\n}\n\n/** Resolve + read a page. Distinguishes missing (`exists: false`) from\n * empty-but-present (`exists: true`, `content: \"\"`). */\nexport async function readWikiPage(workspace: string, pageName: string): Promise<WikiPageRead> {\n const filePath = await resolvePagePath(workspace, pageName);\n const content = filePath ? readFileOrEmpty(filePath) : \"\";\n const resolvedTitle = filePath ? path.basename(filePath, \".md\") : pageName;\n return { filePath, content, exists: Boolean(filePath), resolvedTitle };\n}\n\n/** Page body, or \"\" when the file vanished between indexing and reading. */\nasync function readPageBody(pagesDir: string, fileName: string): Promise<string> {\n return (await readTextSafe(path.join(pagesDir, fileName))) ?? \"\";\n}\n\n/** Read every page + the index and build the page→page link graph.\n * No cache: the graph is requested explicitly and a content edit does\n * not advance the pagesDir mtime the page index caches on. */\nexport async function loadWikiGraph(workspace: string): Promise<WikiGraph> {\n const { pagesDir, indexFile } = wikiDirs(workspace);\n const { slugs } = await getPageIndex(pagesDir);\n const pages = await Promise.all([...slugs.entries()].map(async ([slug, fileName]) => ({ slug, content: await readPageBody(pagesDir, fileName) })));\n const indexEntries = parseIndexEntries(readFileOrEmpty(indexFile));\n return buildWikiGraph(pages, indexEntries);\n}\n\nexport interface PageBody {\n fileName: string;\n content: string;\n}\n\nasync function readPageBodies(pagesDir: string, fileNames: readonly string[]): Promise<PageBody[]> {\n return Promise.all(fileNames.map(async (fileName) => ({ fileName, content: await readPageBody(pagesDir, fileName) })));\n}\n\n/** Frontmatter `tags:` per page, keyed for `findTagDrift`. Lowercased so\n * a `MyPage.md` filename matches an `entry.slug` of `mypage`, which is\n * what the drift rule looks up. */\nexport function frontmatterTagIndex(bodies: readonly PageBody[]): Map<string, string[]> {\n const tagsBySlug = new Map<string, string[]>();\n bodies.forEach(({ fileName, content }) => {\n tagsBySlug.set(fileName.replace(/\\.md$/i, \"\").toLowerCase(), parseFrontmatterTags(content));\n });\n return tagsBySlug;\n}\n\n/** Run every lint rule over the on-disk wiki, returning issue strings. */\nexport async function collectLintIssues(workspace: string): Promise<string[]> {\n const { pagesDir, indexFile } = wikiDirs(workspace);\n const { slugs } = await getPageIndex(pagesDir);\n if (slugs.size === 0) {\n return [\"- Wiki `pages/` directory does not exist yet. Start ingesting sources.\"];\n }\n const pageEntries = parseIndexEntries(readFileOrEmpty(indexFile));\n const fileSlugs = new Set(slugs.keys());\n const slugByTitle = slugByIndexTitle(pageEntries);\n const bodies = await readPageBodies(pagesDir, [...slugs.values()]);\n return [\n ...findOrphanPages(fileSlugs, new Set(pageEntries.map((entry) => entry.slug))),\n ...findMissingFiles(pageEntries, fileSlugs),\n ...bodies.flatMap(({ fileName, content }) => findBrokenLinksInPage(fileName, content, fileSlugs, slugByTitle)),\n ...findTagDrift(pageEntries, frontmatterTagIndex(bodies)),\n ];\n}\n"],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { a as
|
|
2
|
-
import {
|
|
1
|
+
import { a as findMissingFiles, b as parseWikiLink, g as matchWikiSlug, i as findBrokenLinksInPage, m as parseIndexEntries, o as findOrphanPages, s as findTagDrift, t as buildWikiGraph, v as slugByIndexTitle } from "../../graph-DBxSOx3v.js";
|
|
2
|
+
import { r as wikiSlugify } from "../../slug-BpjumQfe.js";
|
|
3
3
|
import { wikiDirs, wikiSlugFromAbsPath } from "../paths.js";
|
|
4
4
|
import { readFileSync } from "node:fs";
|
|
5
5
|
import path from "node:path";
|
|
@@ -110,28 +110,31 @@ function pickFuzzyMatch(slug, slugs) {
|
|
|
110
110
|
}
|
|
111
111
|
return bestIsTied ? null : bestFile;
|
|
112
112
|
}
|
|
113
|
-
/**
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
|
|
113
|
+
/** File matching an index.md entry whose title equals `target` — the
|
|
114
|
+
* last resort for a link written as the display title rather than
|
|
115
|
+
* the page's own name. */
|
|
116
|
+
function fileByIndexTitle(indexFile, target, slugs) {
|
|
117
|
+
const entries = parseIndexEntries(readFileOrEmpty(indexFile));
|
|
118
|
+
const trimmed = target.trim();
|
|
119
|
+
const titleMatch = entries.find((entry) => entry.title === trimmed);
|
|
120
|
+
return titleMatch ? slugs.get(titleMatch.slug) : void 0;
|
|
121
|
+
}
|
|
122
|
+
/** Resolve a page name to an absolute `.md` path: known slug (literal
|
|
123
|
+
* or slugified) → fuzzy → index-title fallback. `pageName` may carry
|
|
124
|
+
* the `[[target|display]]` form; `parseWikiLink` strips the display
|
|
125
|
+
* half so the lookup uses just the target. */
|
|
117
126
|
async function resolvePagePath(workspace, pageName) {
|
|
118
127
|
const { pagesDir, indexFile } = wikiDirs(workspace);
|
|
119
128
|
const { slugs } = await getPageIndex(pagesDir);
|
|
120
129
|
if (slugs.size === 0) return null;
|
|
121
130
|
const { target } = parseWikiLink(pageName);
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
const titleMatch = parseIndexEntries(readFileOrEmpty(indexFile)).find((entry) => entry.title === target);
|
|
130
|
-
if (titleMatch) {
|
|
131
|
-
const file = slugs.get(titleMatch.slug);
|
|
132
|
-
if (file) return path.join(pagesDir, file);
|
|
133
|
-
}
|
|
134
|
-
return null;
|
|
131
|
+
const matched = matchWikiSlug(target, slugs);
|
|
132
|
+
const matchedFile = matched === null ? void 0 : slugs.get(matched);
|
|
133
|
+
if (matchedFile) return path.join(pagesDir, matchedFile);
|
|
134
|
+
const fuzzy = pickFuzzyMatch(wikiSlugify(target), slugs);
|
|
135
|
+
if (fuzzy) return path.join(pagesDir, fuzzy);
|
|
136
|
+
const titleFile = fileByIndexTitle(indexFile, target, slugs);
|
|
137
|
+
return titleFile ? path.join(pagesDir, titleFile) : null;
|
|
135
138
|
}
|
|
136
139
|
/** Raw `index.md` content + its parsed entries. */
|
|
137
140
|
function readWikiIndex(workspace) {
|
|
@@ -175,28 +178,37 @@ async function loadWikiGraph(workspace) {
|
|
|
175
178
|
const indexEntries = parseIndexEntries(readFileOrEmpty(indexFile));
|
|
176
179
|
return buildWikiGraph(pages, indexEntries);
|
|
177
180
|
}
|
|
181
|
+
async function readPageBodies(pagesDir, fileNames) {
|
|
182
|
+
return Promise.all(fileNames.map(async (fileName) => ({
|
|
183
|
+
fileName,
|
|
184
|
+
content: await readPageBody(pagesDir, fileName)
|
|
185
|
+
})));
|
|
186
|
+
}
|
|
187
|
+
/** Frontmatter `tags:` per page, keyed for `findTagDrift`. Lowercased so
|
|
188
|
+
* a `MyPage.md` filename matches an `entry.slug` of `mypage`, which is
|
|
189
|
+
* what the drift rule looks up. */
|
|
190
|
+
function frontmatterTagIndex(bodies) {
|
|
191
|
+
const tagsBySlug = /* @__PURE__ */ new Map();
|
|
192
|
+
bodies.forEach(({ fileName, content }) => {
|
|
193
|
+
tagsBySlug.set(fileName.replace(/\.md$/i, "").toLowerCase(), parseFrontmatterTags(content));
|
|
194
|
+
});
|
|
195
|
+
return tagsBySlug;
|
|
196
|
+
}
|
|
178
197
|
/** Run every lint rule over the on-disk wiki, returning issue strings. */
|
|
179
198
|
async function collectLintIssues(workspace) {
|
|
180
199
|
const { pagesDir, indexFile } = wikiDirs(workspace);
|
|
181
200
|
const { slugs } = await getPageIndex(pagesDir);
|
|
182
201
|
if (slugs.size === 0) return ["- Wiki `pages/` directory does not exist yet. Start ingesting sources."];
|
|
183
202
|
const pageEntries = parseIndexEntries(readFileOrEmpty(indexFile));
|
|
184
|
-
const indexedSlugs = new Set(pageEntries.map((entry) => entry.slug));
|
|
185
203
|
const fileSlugs = new Set(slugs.keys());
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
for (const { fileName, content } of bodies) {
|
|
195
|
-
issues.push(...findBrokenLinksInPage(fileName, content, fileSlugs));
|
|
196
|
-
frontmatterTagsBySlug.set(fileName.replace(/\.md$/i, "").toLowerCase(), parseFrontmatterTags(content));
|
|
197
|
-
}
|
|
198
|
-
issues.push(...findTagDrift(pageEntries, frontmatterTagsBySlug));
|
|
199
|
-
return issues;
|
|
204
|
+
const slugByTitle = slugByIndexTitle(pageEntries);
|
|
205
|
+
const bodies = await readPageBodies(pagesDir, [...slugs.values()]);
|
|
206
|
+
return [
|
|
207
|
+
...findOrphanPages(fileSlugs, new Set(pageEntries.map((entry) => entry.slug))),
|
|
208
|
+
...findMissingFiles(pageEntries, fileSlugs),
|
|
209
|
+
...bodies.flatMap(({ fileName, content }) => findBrokenLinksInPage(fileName, content, fileSlugs, slugByTitle)),
|
|
210
|
+
...findTagDrift(pageEntries, frontmatterTagIndex(bodies))
|
|
211
|
+
];
|
|
200
212
|
}
|
|
201
213
|
//#endregion
|
|
202
214
|
export { __resetPageIndexCache, collectLintIssues, getPageIndex, loadWikiGraph, parseFrontmatter, parseFrontmatterTags, pickFuzzyMatch, readWikiIndex, readWikiLog, readWikiPage, resolvePagePath, wikiDirs, wikiSlugFromAbsPath };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../../src/wiki/server/fs.ts","../../../src/wiki/server/pageIndex.ts","../../../src/wiki/server/frontmatter.ts","../../../src/wiki/server/engine.ts"],"sourcesContent":["// Minimal ENOENT-swallowing fs wrappers for the wiki read-engine.\n// Self-contained (core can't reach the host's `server/utils/files/safe.ts`);\n// only the handful of helpers the wiki engine needs. Same \"return\n// null / [] on any error\" contract so callers branch on the value\n// instead of try/catch.\n\nimport type { Dirent, Stats } from \"node:fs\";\nimport { readFileSync } from \"node:fs\";\nimport { readFile, readdir, stat } from \"node:fs/promises\";\n\nexport async function readTextSafe(absPath: string): Promise<string | null> {\n try {\n return await readFile(absPath, \"utf-8\");\n } catch {\n return null;\n }\n}\n\nexport function readTextSafeSync(absPath: string): string | null {\n try {\n return readFileSync(absPath, \"utf-8\");\n } catch {\n return null;\n }\n}\n\nexport async function statSafeAsync(absPath: string): Promise<Stats | null> {\n try {\n return await stat(absPath);\n } catch {\n return null;\n }\n}\n\nexport async function readDirSafeAsync(absPath: string): Promise<Dirent[]> {\n try {\n return await readdir(absPath, { withFileTypes: true });\n } catch {\n return [];\n }\n}\n","// In-memory index of `<pagesDir>/*.md` keyed by slug (= filename\n// without the `.md` extension). Kept fresh via `pagesDir` mtime —\n// adding, removing, or renaming a file under `pagesDir` advances the\n// directory's mtime on every major filesystem we target (macOS APFS,\n// Linux ext4, Windows NTFS), so one cheap `stat()` per request is\n// enough to decide whether to rebuild.\n\nimport { readDirSafeAsync, statSafeAsync } from \"./fs.js\";\n\nexport interface PageIndex {\n mtimeMs: number;\n /** slug → filename (e.g. \"sakura-internet\" → \"sakura-internet.md\"). */\n slugs: Map<string, string>;\n}\n\n// Keyed by `pagesDir`: the engine is workspace-injected and may be\n// called with more than one pages directory in a single process. A\n// single global slot would let one workspace's slug map be returned\n// for another whenever their mtimes happen to tie (Codex P2 on #1876).\nconst cache = new Map<string, PageIndex>();\n\n/**\n * Get the page index for `pagesDir`. Returns a cached value as long as\n * THAT directory's mtime hasn't advanced; otherwise rebuilds. Safe to\n * call concurrently — racing builds produce the same result.\n */\nexport async function getPageIndex(pagesDir: string): Promise<PageIndex> {\n const stat = await statSafeAsync(pagesDir);\n if (!stat) {\n // Dir doesn't exist yet (never ingested). Return empty but don't\n // cache a stale-forever value — the next call re-stats.\n return { mtimeMs: 0, slugs: new Map() };\n }\n const cached = cache.get(pagesDir);\n if (cached && cached.mtimeMs >= stat.mtimeMs) return cached;\n const entries = await readDirSafeAsync(pagesDir);\n const slugs = new Map<string, string>();\n for (const entry of entries) {\n if (!entry.isFile()) continue;\n const { name } = entry;\n if (!name.endsWith(\".md\")) continue;\n slugs.set(name.slice(0, -\".md\".length), name);\n }\n const built: PageIndex = { mtimeMs: stat.mtimeMs, slugs };\n cache.set(pagesDir, built);\n return built;\n}\n\n/** Test-only: drop the module-level cache (all directories). */\nexport function __resetPageIndexCache(): void {\n cache.clear();\n}\n","// Read-side frontmatter helpers for the wiki engine. The generic\n// `---\\n…\\n---\\n` envelope parsing is the canonical value-preserving\n// (FAILSAFE_SCHEMA) parser in `@mulmoclaude/markdown-utils`; only the\n// wiki-specific `tags:` reader stays here. Re-exporting `parseFrontmatter`\n// keeps the `@mulmoclaude/core/wiki/server` barrel surface stable — it now\n// returns the richer `{ meta, body, hasHeader }` shape.\n\nimport { parseFrontmatter } from \"@mulmoclaude/markdown-utils/markdown/frontmatter\";\n\nexport { parseFrontmatter };\n\nfunction cleanTagToken(token: string): string {\n return token\n .trim()\n .replace(/^['\"]|['\"]$/g, \"\")\n .replace(/^#/, \"\")\n .toLowerCase();\n}\n\n/** Narrow `tags:` reader. Handles flow (`tags: [a, b]`) and block-list\n * style; anything unparseable returns `[]` (best-effort, never throws). */\nexport function parseFrontmatterTags(content: string): string[] {\n const parsed = parseFrontmatter(content);\n if (!parsed.hasHeader) return [];\n const tagsValue = parsed.meta.tags;\n if (!Array.isArray(tagsValue)) return [];\n return tagsValue\n .filter((item): item is string => typeof item === \"string\")\n .map(cleanTagToken)\n .filter((token) => token.length > 0);\n}\n","// Workspace-injected wiki read-engine — the filesystem layer that feeds\n// the pure helpers in `@mulmoclaude/core/wiki`. Shared by every host so\n// the two apps reading the same `data/wiki/` can't disagree on slug\n// resolution, graph edges, or lint findings. Mirrors the bodies that\n// previously lived in MulmoClaude's `server/api/routes/wiki.ts`; the\n// hosts keep only their HTTP response shaping on top.\n//\n// The read/write WRITE side (writeWikiPage / snapshots) stays host-side\n// until a host needs it shared.\n\nimport path from \"node:path\";\nimport { parseWikiLink } from \"../link.js\";\nimport { wikiSlugify } from \"../slug.js\";\nimport { type WikiPageEntry, parseIndexEntries } from \"../index-parse.js\";\nimport { findBrokenLinksInPage, findMissingFiles, findOrphanPages, findTagDrift } from \"../lint.js\";\nimport { type WikiGraph, buildWikiGraph } from \"../graph.js\";\nimport { readTextSafe, readTextSafeSync } from \"./fs.js\";\nimport { getPageIndex } from \"./pageIndex.js\";\nimport { parseFrontmatterTags } from \"./frontmatter.js\";\nimport { wikiDirs } from \"./paths.js\";\n\n// Below this length the fuzzy `includes` step is skipped — CJK /\n// emoji-only / very-short page names slugify down to a short noise\n// tail that partial-matches almost anything; the index.md title-match\n// fallback still handles the legitimate non-ASCII case (#1194).\nconst MIN_FUZZY_SLUG_LEN = 6;\n\nfunction readFileOrEmpty(absPath: string): string {\n return readTextSafeSync(absPath) ?? \"\";\n}\n\n/** Walk every indexed slug for an `includes`-style match. Returns the\n * single best candidate, or null when the slug is too short OR several\n * candidates tie at the top score (ambiguous → defer to the caller's\n * title-match fallback). Score = min/max length, decoupled from Map\n * iteration order so resolution is deterministic across hosts. */\nexport function pickFuzzyMatch(slug: string, slugs: ReadonlyMap<string, string>): string | null {\n if (slug.length < MIN_FUZZY_SLUG_LEN) return null;\n let bestFile: string | null = null;\n let bestScore = 0;\n let bestIsTied = false;\n for (const [key, file] of slugs) {\n if (!slug.includes(key) && !key.includes(slug)) continue;\n const shorter = Math.min(slug.length, key.length);\n const longer = Math.max(slug.length, key.length);\n const score = shorter / longer;\n if (score > bestScore) {\n bestScore = score;\n bestFile = file;\n bestIsTied = false;\n } else if (score === bestScore) {\n bestIsTied = true;\n }\n }\n return bestIsTied ? null : bestFile;\n}\n\n/** Resolve a page name to an absolute `.md` path: exact slug → fuzzy →\n * index-title fallback (for non-ASCII names that slugify to empty).\n * `pageName` may carry the `[[target|display]]` form; `parseWikiLink`\n * strips the display half so the lookup uses just the target. */\nexport async function resolvePagePath(workspace: string, pageName: string): Promise<string | null> {\n const { pagesDir, indexFile } = wikiDirs(workspace);\n const { slugs } = await getPageIndex(pagesDir);\n if (slugs.size === 0) return null;\n\n const { target } = parseWikiLink(pageName);\n const slug = wikiSlugify(target);\n\n if (slug.length > 0) {\n const exact = slugs.get(slug);\n if (exact) return path.join(pagesDir, exact);\n const fuzzy = pickFuzzyMatch(slug, slugs);\n if (fuzzy) return path.join(pagesDir, fuzzy);\n }\n\n const entries = parseIndexEntries(readFileOrEmpty(indexFile));\n const titleMatch = entries.find((entry) => entry.title === target);\n if (titleMatch) {\n const file = slugs.get(titleMatch.slug);\n if (file) return path.join(pagesDir, file);\n }\n return null;\n}\n\n/** Raw `index.md` content + its parsed entries. */\nexport function readWikiIndex(workspace: string): { content: string; entries: WikiPageEntry[] } {\n const content = readFileOrEmpty(wikiDirs(workspace).indexFile);\n return { content, entries: parseIndexEntries(content) };\n}\n\n/** Raw `log.md` content (empty string if absent). */\nexport function readWikiLog(workspace: string): string {\n return readFileOrEmpty(wikiDirs(workspace).logFile);\n}\n\nexport interface WikiPageRead {\n /** Absolute path of the resolved file, or null when nothing matched. */\n filePath: string | null;\n /** File body (empty when missing OR when the file is an empty placeholder). */\n content: string;\n /** True iff a page file resolved (distinct from empty content). */\n exists: boolean;\n /** Title to display — the resolved filename stem, or the raw pageName. */\n resolvedTitle: string;\n}\n\n/** Resolve + read a page. Distinguishes missing (`exists: false`) from\n * empty-but-present (`exists: true`, `content: \"\"`). */\nexport async function readWikiPage(workspace: string, pageName: string): Promise<WikiPageRead> {\n const filePath = await resolvePagePath(workspace, pageName);\n const content = filePath ? readFileOrEmpty(filePath) : \"\";\n const resolvedTitle = filePath ? path.basename(filePath, \".md\") : pageName;\n return { filePath, content, exists: Boolean(filePath), resolvedTitle };\n}\n\n/** Page body, or \"\" when the file vanished between indexing and reading. */\nasync function readPageBody(pagesDir: string, fileName: string): Promise<string> {\n return (await readTextSafe(path.join(pagesDir, fileName))) ?? \"\";\n}\n\n/** Read every page + the index and build the page→page link graph.\n * No cache: the graph is requested explicitly and a content edit does\n * not advance the pagesDir mtime the page index caches on. */\nexport async function loadWikiGraph(workspace: string): Promise<WikiGraph> {\n const { pagesDir, indexFile } = wikiDirs(workspace);\n const { slugs } = await getPageIndex(pagesDir);\n const pages = await Promise.all([...slugs.entries()].map(async ([slug, fileName]) => ({ slug, content: await readPageBody(pagesDir, fileName) })));\n const indexEntries = parseIndexEntries(readFileOrEmpty(indexFile));\n return buildWikiGraph(pages, indexEntries);\n}\n\n/** Run every lint rule over the on-disk wiki, returning issue strings. */\nexport async function collectLintIssues(workspace: string): Promise<string[]> {\n const { pagesDir, indexFile } = wikiDirs(workspace);\n const { slugs } = await getPageIndex(pagesDir);\n if (slugs.size === 0) {\n return [\"- Wiki `pages/` directory does not exist yet. Start ingesting sources.\"];\n }\n const pageEntries = parseIndexEntries(readFileOrEmpty(indexFile));\n const indexedSlugs = new Set(pageEntries.map((entry) => entry.slug));\n const fileSlugs = new Set(slugs.keys());\n const bodies = await Promise.all([...slugs.values()].map(async (fileName) => ({ fileName, content: await readPageBody(pagesDir, fileName) })));\n\n const issues: string[] = [];\n issues.push(...findOrphanPages(fileSlugs, indexedSlugs));\n issues.push(...findMissingFiles(pageEntries, fileSlugs));\n const frontmatterTagsBySlug = new Map<string, string[]>();\n for (const { fileName, content } of bodies) {\n issues.push(...findBrokenLinksInPage(fileName, content, fileSlugs));\n // Lowercase the key so a `MyPage.md` filename matches an\n // `entry.slug` of `mypage`; `findTagDrift` lowercases the lookup.\n frontmatterTagsBySlug.set(fileName.replace(/\\.md$/i, \"\").toLowerCase(), parseFrontmatterTags(content));\n }\n issues.push(...findTagDrift(pageEntries, frontmatterTagsBySlug));\n return issues;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../../src/wiki/server/fs.ts","../../../src/wiki/server/pageIndex.ts","../../../src/wiki/server/frontmatter.ts","../../../src/wiki/server/engine.ts"],"sourcesContent":["// Minimal ENOENT-swallowing fs wrappers for the wiki read-engine.\n// Self-contained (core can't reach the host's `server/utils/files/safe.ts`);\n// only the handful of helpers the wiki engine needs. Same \"return\n// null / [] on any error\" contract so callers branch on the value\n// instead of try/catch.\n\nimport type { Dirent, Stats } from \"node:fs\";\nimport { readFileSync } from \"node:fs\";\nimport { readFile, readdir, stat } from \"node:fs/promises\";\n\nexport async function readTextSafe(absPath: string): Promise<string | null> {\n try {\n return await readFile(absPath, \"utf-8\");\n } catch {\n return null;\n }\n}\n\nexport function readTextSafeSync(absPath: string): string | null {\n try {\n return readFileSync(absPath, \"utf-8\");\n } catch {\n return null;\n }\n}\n\nexport async function statSafeAsync(absPath: string): Promise<Stats | null> {\n try {\n return await stat(absPath);\n } catch {\n return null;\n }\n}\n\nexport async function readDirSafeAsync(absPath: string): Promise<Dirent[]> {\n try {\n return await readdir(absPath, { withFileTypes: true });\n } catch {\n return [];\n }\n}\n","// In-memory index of `<pagesDir>/*.md` keyed by slug (= filename\n// without the `.md` extension). Kept fresh via `pagesDir` mtime —\n// adding, removing, or renaming a file under `pagesDir` advances the\n// directory's mtime on every major filesystem we target (macOS APFS,\n// Linux ext4, Windows NTFS), so one cheap `stat()` per request is\n// enough to decide whether to rebuild.\n\nimport { readDirSafeAsync, statSafeAsync } from \"./fs.js\";\n\nexport interface PageIndex {\n mtimeMs: number;\n /** slug → filename (e.g. \"sakura-internet\" → \"sakura-internet.md\"). */\n slugs: Map<string, string>;\n}\n\n// Keyed by `pagesDir`: the engine is workspace-injected and may be\n// called with more than one pages directory in a single process. A\n// single global slot would let one workspace's slug map be returned\n// for another whenever their mtimes happen to tie (Codex P2 on #1876).\nconst cache = new Map<string, PageIndex>();\n\n/**\n * Get the page index for `pagesDir`. Returns a cached value as long as\n * THAT directory's mtime hasn't advanced; otherwise rebuilds. Safe to\n * call concurrently — racing builds produce the same result.\n */\nexport async function getPageIndex(pagesDir: string): Promise<PageIndex> {\n const stat = await statSafeAsync(pagesDir);\n if (!stat) {\n // Dir doesn't exist yet (never ingested). Return empty but don't\n // cache a stale-forever value — the next call re-stats.\n return { mtimeMs: 0, slugs: new Map() };\n }\n const cached = cache.get(pagesDir);\n if (cached && cached.mtimeMs >= stat.mtimeMs) return cached;\n const entries = await readDirSafeAsync(pagesDir);\n const slugs = new Map<string, string>();\n for (const entry of entries) {\n if (!entry.isFile()) continue;\n const { name } = entry;\n if (!name.endsWith(\".md\")) continue;\n slugs.set(name.slice(0, -\".md\".length), name);\n }\n const built: PageIndex = { mtimeMs: stat.mtimeMs, slugs };\n cache.set(pagesDir, built);\n return built;\n}\n\n/** Test-only: drop the module-level cache (all directories). */\nexport function __resetPageIndexCache(): void {\n cache.clear();\n}\n","// Read-side frontmatter helpers for the wiki engine. The generic\n// `---\\n…\\n---\\n` envelope parsing is the canonical value-preserving\n// (FAILSAFE_SCHEMA) parser in `@mulmoclaude/markdown-utils`; only the\n// wiki-specific `tags:` reader stays here. Re-exporting `parseFrontmatter`\n// keeps the `@mulmoclaude/core/wiki/server` barrel surface stable — it now\n// returns the richer `{ meta, body, hasHeader }` shape.\n\nimport { parseFrontmatter } from \"@mulmoclaude/markdown-utils/markdown/frontmatter\";\n\nexport { parseFrontmatter };\n\nfunction cleanTagToken(token: string): string {\n return token\n .trim()\n .replace(/^['\"]|['\"]$/g, \"\")\n .replace(/^#/, \"\")\n .toLowerCase();\n}\n\n/** Narrow `tags:` reader. Handles flow (`tags: [a, b]`) and block-list\n * style; anything unparseable returns `[]` (best-effort, never throws). */\nexport function parseFrontmatterTags(content: string): string[] {\n const parsed = parseFrontmatter(content);\n if (!parsed.hasHeader) return [];\n const tagsValue = parsed.meta.tags;\n if (!Array.isArray(tagsValue)) return [];\n return tagsValue\n .filter((item): item is string => typeof item === \"string\")\n .map(cleanTagToken)\n .filter((token) => token.length > 0);\n}\n","// Workspace-injected wiki read-engine — the filesystem layer that feeds\n// the pure helpers in `@mulmoclaude/core/wiki`. Shared by every host so\n// the two apps reading the same `data/wiki/` can't disagree on slug\n// resolution, graph edges, or lint findings. Mirrors the bodies that\n// previously lived in MulmoClaude's `server/api/routes/wiki.ts`; the\n// hosts keep only their HTTP response shaping on top.\n//\n// The read/write WRITE side (writeWikiPage / snapshots) stays host-side\n// until a host needs it shared.\n\nimport path from \"node:path\";\nimport { parseWikiLink } from \"../link.js\";\nimport { wikiSlugify } from \"../slug.js\";\nimport { matchWikiSlug, slugByIndexTitle } from \"../resolve.js\";\nimport { type WikiPageEntry, parseIndexEntries } from \"../index-parse.js\";\nimport { findBrokenLinksInPage, findMissingFiles, findOrphanPages, findTagDrift } from \"../lint.js\";\nimport { type WikiGraph, buildWikiGraph } from \"../graph.js\";\nimport { readTextSafe, readTextSafeSync } from \"./fs.js\";\nimport { getPageIndex } from \"./pageIndex.js\";\nimport { parseFrontmatterTags } from \"./frontmatter.js\";\nimport { wikiDirs } from \"./paths.js\";\n\n// Below this length the fuzzy `includes` step is skipped — CJK /\n// emoji-only / very-short page names slugify down to a short noise\n// tail that partial-matches almost anything; the index.md title-match\n// fallback still handles the legitimate non-ASCII case (#1194).\nconst MIN_FUZZY_SLUG_LEN = 6;\n\nfunction readFileOrEmpty(absPath: string): string {\n return readTextSafeSync(absPath) ?? \"\";\n}\n\n/** Walk every indexed slug for an `includes`-style match. Returns the\n * single best candidate, or null when the slug is too short OR several\n * candidates tie at the top score (ambiguous → defer to the caller's\n * title-match fallback). Score = min/max length, decoupled from Map\n * iteration order so resolution is deterministic across hosts. */\nexport function pickFuzzyMatch(slug: string, slugs: ReadonlyMap<string, string>): string | null {\n if (slug.length < MIN_FUZZY_SLUG_LEN) return null;\n let bestFile: string | null = null;\n let bestScore = 0;\n let bestIsTied = false;\n for (const [key, file] of slugs) {\n if (!slug.includes(key) && !key.includes(slug)) continue;\n const shorter = Math.min(slug.length, key.length);\n const longer = Math.max(slug.length, key.length);\n const score = shorter / longer;\n if (score > bestScore) {\n bestScore = score;\n bestFile = file;\n bestIsTied = false;\n } else if (score === bestScore) {\n bestIsTied = true;\n }\n }\n return bestIsTied ? null : bestFile;\n}\n\n/** File matching an index.md entry whose title equals `target` — the\n * last resort for a link written as the display title rather than\n * the page's own name. */\nfunction fileByIndexTitle(indexFile: string, target: string, slugs: ReadonlyMap<string, string>): string | undefined {\n const entries = parseIndexEntries(readFileOrEmpty(indexFile));\n const trimmed = target.trim();\n const titleMatch = entries.find((entry) => entry.title === trimmed);\n return titleMatch ? slugs.get(titleMatch.slug) : undefined;\n}\n\n/** Resolve a page name to an absolute `.md` path: known slug (literal\n * or slugified) → fuzzy → index-title fallback. `pageName` may carry\n * the `[[target|display]]` form; `parseWikiLink` strips the display\n * half so the lookup uses just the target. */\nexport async function resolvePagePath(workspace: string, pageName: string): Promise<string | null> {\n const { pagesDir, indexFile } = wikiDirs(workspace);\n const { slugs } = await getPageIndex(pagesDir);\n if (slugs.size === 0) return null;\n\n const { target } = parseWikiLink(pageName);\n const matched = matchWikiSlug(target, slugs);\n const matchedFile = matched === null ? undefined : slugs.get(matched);\n if (matchedFile) return path.join(pagesDir, matchedFile);\n\n const fuzzy = pickFuzzyMatch(wikiSlugify(target), slugs);\n if (fuzzy) return path.join(pagesDir, fuzzy);\n\n const titleFile = fileByIndexTitle(indexFile, target, slugs);\n return titleFile ? path.join(pagesDir, titleFile) : null;\n}\n\n/** Raw `index.md` content + its parsed entries. */\nexport function readWikiIndex(workspace: string): { content: string; entries: WikiPageEntry[] } {\n const content = readFileOrEmpty(wikiDirs(workspace).indexFile);\n return { content, entries: parseIndexEntries(content) };\n}\n\n/** Raw `log.md` content (empty string if absent). */\nexport function readWikiLog(workspace: string): string {\n return readFileOrEmpty(wikiDirs(workspace).logFile);\n}\n\nexport interface WikiPageRead {\n /** Absolute path of the resolved file, or null when nothing matched. */\n filePath: string | null;\n /** File body (empty when missing OR when the file is an empty placeholder). */\n content: string;\n /** True iff a page file resolved (distinct from empty content). */\n exists: boolean;\n /** Title to display — the resolved filename stem, or the raw pageName. */\n resolvedTitle: string;\n}\n\n/** Resolve + read a page. Distinguishes missing (`exists: false`) from\n * empty-but-present (`exists: true`, `content: \"\"`). */\nexport async function readWikiPage(workspace: string, pageName: string): Promise<WikiPageRead> {\n const filePath = await resolvePagePath(workspace, pageName);\n const content = filePath ? readFileOrEmpty(filePath) : \"\";\n const resolvedTitle = filePath ? path.basename(filePath, \".md\") : pageName;\n return { filePath, content, exists: Boolean(filePath), resolvedTitle };\n}\n\n/** Page body, or \"\" when the file vanished between indexing and reading. */\nasync function readPageBody(pagesDir: string, fileName: string): Promise<string> {\n return (await readTextSafe(path.join(pagesDir, fileName))) ?? \"\";\n}\n\n/** Read every page + the index and build the page→page link graph.\n * No cache: the graph is requested explicitly and a content edit does\n * not advance the pagesDir mtime the page index caches on. */\nexport async function loadWikiGraph(workspace: string): Promise<WikiGraph> {\n const { pagesDir, indexFile } = wikiDirs(workspace);\n const { slugs } = await getPageIndex(pagesDir);\n const pages = await Promise.all([...slugs.entries()].map(async ([slug, fileName]) => ({ slug, content: await readPageBody(pagesDir, fileName) })));\n const indexEntries = parseIndexEntries(readFileOrEmpty(indexFile));\n return buildWikiGraph(pages, indexEntries);\n}\n\nexport interface PageBody {\n fileName: string;\n content: string;\n}\n\nasync function readPageBodies(pagesDir: string, fileNames: readonly string[]): Promise<PageBody[]> {\n return Promise.all(fileNames.map(async (fileName) => ({ fileName, content: await readPageBody(pagesDir, fileName) })));\n}\n\n/** Frontmatter `tags:` per page, keyed for `findTagDrift`. Lowercased so\n * a `MyPage.md` filename matches an `entry.slug` of `mypage`, which is\n * what the drift rule looks up. */\nexport function frontmatterTagIndex(bodies: readonly PageBody[]): Map<string, string[]> {\n const tagsBySlug = new Map<string, string[]>();\n bodies.forEach(({ fileName, content }) => {\n tagsBySlug.set(fileName.replace(/\\.md$/i, \"\").toLowerCase(), parseFrontmatterTags(content));\n });\n return tagsBySlug;\n}\n\n/** Run every lint rule over the on-disk wiki, returning issue strings. */\nexport async function collectLintIssues(workspace: string): Promise<string[]> {\n const { pagesDir, indexFile } = wikiDirs(workspace);\n const { slugs } = await getPageIndex(pagesDir);\n if (slugs.size === 0) {\n return [\"- Wiki `pages/` directory does not exist yet. Start ingesting sources.\"];\n }\n const pageEntries = parseIndexEntries(readFileOrEmpty(indexFile));\n const fileSlugs = new Set(slugs.keys());\n const slugByTitle = slugByIndexTitle(pageEntries);\n const bodies = await readPageBodies(pagesDir, [...slugs.values()]);\n return [\n ...findOrphanPages(fileSlugs, new Set(pageEntries.map((entry) => entry.slug))),\n ...findMissingFiles(pageEntries, fileSlugs),\n ...bodies.flatMap(({ fileName, content }) => findBrokenLinksInPage(fileName, content, fileSlugs, slugByTitle)),\n ...findTagDrift(pageEntries, frontmatterTagIndex(bodies)),\n ];\n}\n"],"mappings":";;;;;;;;AAUA,eAAsB,aAAa,SAAyC;CAC1E,IAAI;EACF,OAAO,MAAM,SAAS,SAAS,OAAO;CACxC,QAAQ;EACN,OAAO;CACT;AACF;AAEA,SAAgB,iBAAiB,SAAgC;CAC/D,IAAI;EACF,OAAO,aAAa,SAAS,OAAO;CACtC,QAAQ;EACN,OAAO;CACT;AACF;AAEA,eAAsB,cAAc,SAAwC;CAC1E,IAAI;EACF,OAAO,MAAM,KAAK,OAAO;CAC3B,QAAQ;EACN,OAAO;CACT;AACF;AAEA,eAAsB,iBAAiB,SAAoC;CACzE,IAAI;EACF,OAAO,MAAM,QAAQ,SAAS,EAAE,eAAe,KAAK,CAAC;CACvD,QAAQ;EACN,OAAO,CAAC;CACV;AACF;;;ACrBA,IAAM,wBAAQ,IAAI,IAAuB;;;;;;AAOzC,eAAsB,aAAa,UAAsC;CACvE,MAAM,OAAO,MAAM,cAAc,QAAQ;CACzC,IAAI,CAAC,MAGH,OAAO;EAAE,SAAS;EAAG,uBAAO,IAAI,IAAI;CAAE;CAExC,MAAM,SAAS,MAAM,IAAI,QAAQ;CACjC,IAAI,UAAU,OAAO,WAAW,KAAK,SAAS,OAAO;CACrD,MAAM,UAAU,MAAM,iBAAiB,QAAQ;CAC/C,MAAM,wBAAQ,IAAI,IAAoB;CACtC,KAAK,MAAM,SAAS,SAAS;EAC3B,IAAI,CAAC,MAAM,OAAO,GAAG;EACrB,MAAM,EAAE,SAAS;EACjB,IAAI,CAAC,KAAK,SAAS,KAAK,GAAG;EAC3B,MAAM,IAAI,KAAK,MAAM,GAAG,EAAa,GAAG,IAAI;CAC9C;CACA,MAAM,QAAmB;EAAE,SAAS,KAAK;EAAS;CAAM;CACxD,MAAM,IAAI,UAAU,KAAK;CACzB,OAAO;AACT;;AAGA,SAAgB,wBAA8B;CAC5C,MAAM,MAAM;AACd;;;ACxCA,SAAS,cAAc,OAAuB;CAC5C,OAAO,MACJ,KAAK,CAAC,CACN,QAAQ,gBAAgB,EAAE,CAAC,CAC3B,QAAQ,MAAM,EAAE,CAAC,CACjB,YAAY;AACjB;;;AAIA,SAAgB,qBAAqB,SAA2B;CAC9D,MAAM,SAAS,iBAAiB,OAAO;CACvC,IAAI,CAAC,OAAO,WAAW,OAAO,CAAC;CAC/B,MAAM,YAAY,OAAO,KAAK;CAC9B,IAAI,CAAC,MAAM,QAAQ,SAAS,GAAG,OAAO,CAAC;CACvC,OAAO,UACJ,QAAQ,SAAyB,OAAO,SAAS,QAAQ,CAAC,CAC1D,IAAI,aAAa,CAAC,CAClB,QAAQ,UAAU,MAAM,SAAS,CAAC;AACvC;;;ACJA,IAAM,qBAAqB;AAE3B,SAAS,gBAAgB,SAAyB;CAChD,OAAO,iBAAiB,OAAO,KAAK;AACtC;;;;;;AAOA,SAAgB,eAAe,MAAc,OAAmD;CAC9F,IAAI,KAAK,SAAS,oBAAoB,OAAO;CAC7C,IAAI,WAA0B;CAC9B,IAAI,YAAY;CAChB,IAAI,aAAa;CACjB,KAAK,MAAM,CAAC,KAAK,SAAS,OAAO;EAC/B,IAAI,CAAC,KAAK,SAAS,GAAG,KAAK,CAAC,IAAI,SAAS,IAAI,GAAG;EAGhD,MAAM,QAFU,KAAK,IAAI,KAAK,QAAQ,IAAI,MAE5B,IADC,KAAK,IAAI,KAAK,QAAQ,IAAI,MACjB;EACxB,IAAI,QAAQ,WAAW;GACrB,YAAY;GACZ,WAAW;GACX,aAAa;EACf,OAAO,IAAI,UAAU,WACnB,aAAa;CAEjB;CACA,OAAO,aAAa,OAAO;AAC7B;;;;AAKA,SAAS,iBAAiB,WAAmB,QAAgB,OAAwD;CACnH,MAAM,UAAU,kBAAkB,gBAAgB,SAAS,CAAC;CAC5D,MAAM,UAAU,OAAO,KAAK;CAC5B,MAAM,aAAa,QAAQ,MAAM,UAAU,MAAM,UAAU,OAAO;CAClE,OAAO,aAAa,MAAM,IAAI,WAAW,IAAI,IAAI,KAAA;AACnD;;;;;AAMA,eAAsB,gBAAgB,WAAmB,UAA0C;CACjG,MAAM,EAAE,UAAU,cAAc,SAAS,SAAS;CAClD,MAAM,EAAE,UAAU,MAAM,aAAa,QAAQ;CAC7C,IAAI,MAAM,SAAS,GAAG,OAAO;CAE7B,MAAM,EAAE,WAAW,cAAc,QAAQ;CACzC,MAAM,UAAU,cAAc,QAAQ,KAAK;CAC3C,MAAM,cAAc,YAAY,OAAO,KAAA,IAAY,MAAM,IAAI,OAAO;CACpE,IAAI,aAAa,OAAO,KAAK,KAAK,UAAU,WAAW;CAEvD,MAAM,QAAQ,eAAe,YAAY,MAAM,GAAG,KAAK;CACvD,IAAI,OAAO,OAAO,KAAK,KAAK,UAAU,KAAK;CAE3C,MAAM,YAAY,iBAAiB,WAAW,QAAQ,KAAK;CAC3D,OAAO,YAAY,KAAK,KAAK,UAAU,SAAS,IAAI;AACtD;;AAGA,SAAgB,cAAc,WAAkE;CAC9F,MAAM,UAAU,gBAAgB,SAAS,SAAS,CAAC,CAAC,SAAS;CAC7D,OAAO;EAAE;EAAS,SAAS,kBAAkB,OAAO;CAAE;AACxD;;AAGA,SAAgB,YAAY,WAA2B;CACrD,OAAO,gBAAgB,SAAS,SAAS,CAAC,CAAC,OAAO;AACpD;;;AAeA,eAAsB,aAAa,WAAmB,UAAyC;CAC7F,MAAM,WAAW,MAAM,gBAAgB,WAAW,QAAQ;CAC1D,MAAM,UAAU,WAAW,gBAAgB,QAAQ,IAAI;CACvD,MAAM,gBAAgB,WAAW,KAAK,SAAS,UAAU,KAAK,IAAI;CAClE,OAAO;EAAE;EAAU;EAAS,QAAQ,QAAQ,QAAQ;EAAG;CAAc;AACvE;;AAGA,eAAe,aAAa,UAAkB,UAAmC;CAC/E,OAAQ,MAAM,aAAa,KAAK,KAAK,UAAU,QAAQ,CAAC,KAAM;AAChE;;;;AAKA,eAAsB,cAAc,WAAuC;CACzE,MAAM,EAAE,UAAU,cAAc,SAAS,SAAS;CAClD,MAAM,EAAE,UAAU,MAAM,aAAa,QAAQ;CAC7C,MAAM,QAAQ,MAAM,QAAQ,IAAI,CAAC,GAAG,MAAM,QAAQ,CAAC,CAAC,CAAC,IAAI,OAAO,CAAC,MAAM,eAAe;EAAE;EAAM,SAAS,MAAM,aAAa,UAAU,QAAQ;CAAE,EAAE,CAAC;CACjJ,MAAM,eAAe,kBAAkB,gBAAgB,SAAS,CAAC;CACjE,OAAO,eAAe,OAAO,YAAY;AAC3C;AAOA,eAAe,eAAe,UAAkB,WAAmD;CACjG,OAAO,QAAQ,IAAI,UAAU,IAAI,OAAO,cAAc;EAAE;EAAU,SAAS,MAAM,aAAa,UAAU,QAAQ;CAAE,EAAE,CAAC;AACvH;;;;AAKA,SAAgB,oBAAoB,QAAoD;CACtF,MAAM,6BAAa,IAAI,IAAsB;CAC7C,OAAO,SAAS,EAAE,UAAU,cAAc;EACxC,WAAW,IAAI,SAAS,QAAQ,UAAU,EAAE,CAAC,CAAC,YAAY,GAAG,qBAAqB,OAAO,CAAC;CAC5F,CAAC;CACD,OAAO;AACT;;AAGA,eAAsB,kBAAkB,WAAsC;CAC5E,MAAM,EAAE,UAAU,cAAc,SAAS,SAAS;CAClD,MAAM,EAAE,UAAU,MAAM,aAAa,QAAQ;CAC7C,IAAI,MAAM,SAAS,GACjB,OAAO,CAAC,wEAAwE;CAElF,MAAM,cAAc,kBAAkB,gBAAgB,SAAS,CAAC;CAChE,MAAM,YAAY,IAAI,IAAI,MAAM,KAAK,CAAC;CACtC,MAAM,cAAc,iBAAiB,WAAW;CAChD,MAAM,SAAS,MAAM,eAAe,UAAU,CAAC,GAAG,MAAM,OAAO,CAAC,CAAC;CACjE,OAAO;EACL,GAAG,gBAAgB,WAAW,IAAI,IAAI,YAAY,KAAK,UAAU,MAAM,IAAI,CAAC,CAAC;EAC7E,GAAG,iBAAiB,aAAa,SAAS;EAC1C,GAAG,OAAO,SAAS,EAAE,UAAU,cAAc,sBAAsB,UAAU,SAAS,WAAW,WAAW,CAAC;EAC7G,GAAG,aAAa,aAAa,oBAAoB,MAAM,CAAC;CAC1D;AACF"}
|
package/dist/wiki/slug.d.ts
CHANGED
|
@@ -18,7 +18,18 @@ export declare function isSafeSlug(slug: string): boolean;
|
|
|
18
18
|
* Pure: no normalisation, no transliteration — this is the
|
|
19
19
|
* same shape the index parser, page resolver, and frontend
|
|
20
20
|
* renderer all need to agree on. Non-ASCII titles (e.g.
|
|
21
|
-
* Japanese) collapse to an
|
|
22
|
-
*
|
|
23
|
-
*
|
|
21
|
+
* Japanese) collapse to an ASCII remnant here, so a caller
|
|
22
|
+
* matching against real page files must go through
|
|
23
|
+
* `matchWikiSlug`, which tries the untouched name first. */
|
|
24
24
|
export declare function wikiSlugify(text: string): string;
|
|
25
|
+
/** Filename stem to suggest when a page has to be created for `name`,
|
|
26
|
+
* or null when `name` cannot yield one.
|
|
27
|
+
*
|
|
28
|
+
* ASCII names keep the hyphenated slug convention. A non-ASCII name is
|
|
29
|
+
* suggested verbatim — the write chokepoint accepts any `isSafeSlug`
|
|
30
|
+
* filename, and slugifying it would propose a stripped remnant like
|
|
31
|
+
* `-4.md` (#2940). Null rather than a salvaged stem for a name the
|
|
32
|
+
* write guard would reject anyway: suggesting `wiki/pages/.md`, or
|
|
33
|
+
* `secrets.md` for `../secrets`, sends the caller to a file it did
|
|
34
|
+
* not ask for. */
|
|
35
|
+
export declare function wikiPageStem(name: string): string | null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mulmoclaude/core",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.4.1",
|
|
4
4
|
"description": "Shared server-side core for MulmoClaude and MulmoTerminal — the always-shipped-together subsystems consolidated behind subpath exports so the two hosts can't drift. Server-only except the browser-safe ./artifacts, ./whisper/client, ./workspace-setup/slug, ./translation/client, ./remote-view, ./remote-host and ./plugin-vue entries. All host specifics are injected.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|