diffwiki-core 0.11.0 → 0.12.0-rc.202608011809.0c68acd
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/{chunk-MLUJY3HI.js → chunk-7V2DIQNL.js} +4 -3
- package/dist/{events-UCJWB4WP.js → events-AVD3G5AX.js} +1 -1
- package/dist/index.cjs +252 -36
- package/dist/index.d.cts +58 -32
- package/dist/index.d.ts +58 -32
- package/dist/index.js +244 -36
- package/package.json +1 -1
|
@@ -116,7 +116,7 @@ ${problems.map((p) => ` - ${p}`).join("\n")}`);
|
|
|
116
116
|
// src/collections.ts
|
|
117
117
|
async function fireHook(event, collection) {
|
|
118
118
|
try {
|
|
119
|
-
const { emitPluginEvent: emitPluginEvent2 } = await import("./events-
|
|
119
|
+
const { emitPluginEvent: emitPluginEvent2 } = await import("./events-AVD3G5AX.js");
|
|
120
120
|
await emitPluginEvent2(event, collection);
|
|
121
121
|
} catch {
|
|
122
122
|
}
|
|
@@ -130,7 +130,7 @@ async function findCollection(name) {
|
|
|
130
130
|
async function findCollectionById(id) {
|
|
131
131
|
return (await readRegistry()).collections.find((c) => c.id === id);
|
|
132
132
|
}
|
|
133
|
-
async function createCollection(name) {
|
|
133
|
+
async function createCollection(name, opts) {
|
|
134
134
|
if (await findCollection(name)) throw new CollectionExistsError(name);
|
|
135
135
|
const dir = collectionDir(name);
|
|
136
136
|
await fs2.mkdir(dir, { recursive: true });
|
|
@@ -138,7 +138,8 @@ async function createCollection(name) {
|
|
|
138
138
|
name,
|
|
139
139
|
type: "global",
|
|
140
140
|
path: dir,
|
|
141
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
141
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
142
|
+
...opts?.audience ? { audience: opts.audience } : {}
|
|
142
143
|
};
|
|
143
144
|
await registerCollection(entry);
|
|
144
145
|
void fireHook("collection-added", name);
|
package/dist/index.cjs
CHANGED
|
@@ -523,7 +523,7 @@ async function findCollection(name) {
|
|
|
523
523
|
async function findCollectionById(id) {
|
|
524
524
|
return (await readRegistry()).collections.find((c) => c.id === id);
|
|
525
525
|
}
|
|
526
|
-
async function createCollection(name) {
|
|
526
|
+
async function createCollection(name, opts) {
|
|
527
527
|
if (await findCollection(name)) throw new CollectionExistsError(name);
|
|
528
528
|
const dir = collectionDir(name);
|
|
529
529
|
await import_promises3.default.mkdir(dir, { recursive: true });
|
|
@@ -531,7 +531,8 @@ async function createCollection(name) {
|
|
|
531
531
|
name,
|
|
532
532
|
type: "global",
|
|
533
533
|
path: dir,
|
|
534
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
534
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
535
|
+
...opts?.audience ? { audience: opts.audience } : {}
|
|
535
536
|
};
|
|
536
537
|
await registerCollection(entry);
|
|
537
538
|
void fireHook("collection-added", name);
|
|
@@ -607,6 +608,8 @@ __export(index_exports, {
|
|
|
607
608
|
installPlugin: () => installPlugin,
|
|
608
609
|
invalidateLinkGraph: () => invalidateLinkGraph,
|
|
609
610
|
isLinkedWorktree: () => isLinkedWorktree,
|
|
611
|
+
lintArticle: () => lintArticle,
|
|
612
|
+
lintCollections: () => lintCollections,
|
|
610
613
|
listArticleTree: () => listArticleTree,
|
|
611
614
|
listAvailablePlugins: () => listAvailablePlugins,
|
|
612
615
|
listCollections: () => listCollections,
|
|
@@ -625,6 +628,7 @@ __export(index_exports, {
|
|
|
625
628
|
rankDocs: () => rankDocs,
|
|
626
629
|
readArticle: () => readArticle,
|
|
627
630
|
readConfig: () => readConfig,
|
|
631
|
+
readContextCollections: () => readContextCollections,
|
|
628
632
|
readPluginRegistry: () => readPluginRegistry,
|
|
629
633
|
readProjectConfig: () => readProjectConfig,
|
|
630
634
|
readRegistry: () => readRegistry,
|
|
@@ -655,7 +659,9 @@ __export(index_exports, {
|
|
|
655
659
|
setArticleMeta: () => setArticleMeta,
|
|
656
660
|
setConfigValue: () => setConfigValue,
|
|
657
661
|
setTags: () => setTags,
|
|
662
|
+
showSystemEnabled: () => showSystemEnabled,
|
|
658
663
|
slugify: () => slugify,
|
|
664
|
+
slugifyPath: () => slugifyPath,
|
|
659
665
|
toTreeNodes: () => toTreeNodes,
|
|
660
666
|
tokenize: () => tokenize,
|
|
661
667
|
unregisterCollection: () => unregisterCollection,
|
|
@@ -718,6 +724,10 @@ async function getConfigValue(key) {
|
|
|
718
724
|
const value = config[key];
|
|
719
725
|
return typeof value === "string" ? value : void 0;
|
|
720
726
|
}
|
|
727
|
+
async function showSystemEnabled() {
|
|
728
|
+
const config = await readConfig();
|
|
729
|
+
return config.showSystem === true || config.showSystem === "true";
|
|
730
|
+
}
|
|
721
731
|
|
|
722
732
|
// src/index.ts
|
|
723
733
|
init_registry();
|
|
@@ -727,6 +737,10 @@ function slugify(input) {
|
|
|
727
737
|
const slug = input.toLowerCase().replace(/^#+\s*/, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80).replace(/-+$/g, "");
|
|
728
738
|
return slug.length > 0 ? slug : "untitled";
|
|
729
739
|
}
|
|
740
|
+
function slugifyPath(input) {
|
|
741
|
+
const segs = input.split("/").map((s) => s.trim()).filter((s) => s.length > 0).map((s) => slugify(s));
|
|
742
|
+
return segs.length > 0 ? segs.join("/") : "untitled";
|
|
743
|
+
}
|
|
730
744
|
|
|
731
745
|
// src/frontmatter.ts
|
|
732
746
|
var import_gray_matter = __toESM(require("gray-matter"), 1);
|
|
@@ -740,10 +754,10 @@ var YAML_ENGINE = {
|
|
|
740
754
|
};
|
|
741
755
|
var MATTER_OPTS = { engines: { yaml: YAML_ENGINE } };
|
|
742
756
|
function coerceAudience(value) {
|
|
743
|
-
return value === "private"
|
|
757
|
+
return value === "private" || value === "system" ? value : "public";
|
|
744
758
|
}
|
|
745
759
|
function coerceStatus(value) {
|
|
746
|
-
return value === "draft"
|
|
760
|
+
return value === "draft" || value === "deprecated" ? value : "published";
|
|
747
761
|
}
|
|
748
762
|
function parseFallback(raw) {
|
|
749
763
|
const fenceRe = /^---[ \t]*\n([\s\S]*?)\n---[ \t]*\n/;
|
|
@@ -788,8 +802,8 @@ function parseArticle(raw) {
|
|
|
788
802
|
}
|
|
789
803
|
function serializeArticle(article) {
|
|
790
804
|
const front = { title: article.title, tags: article.tags };
|
|
791
|
-
if (article.audience === "private") front.audience =
|
|
792
|
-
if (article.status === "draft") front.status =
|
|
805
|
+
if (article.audience === "private" || article.audience === "system") front.audience = article.audience;
|
|
806
|
+
if (article.status === "draft" || article.status === "deprecated") front.status = article.status;
|
|
793
807
|
return import_gray_matter.default.stringify(article.body, front, MATTER_OPTS);
|
|
794
808
|
}
|
|
795
809
|
|
|
@@ -840,18 +854,15 @@ async function resolveCollectionDir(name) {
|
|
|
840
854
|
}
|
|
841
855
|
function ensureMdPath(file) {
|
|
842
856
|
if (/\.mdx?$/.test(file)) return file;
|
|
843
|
-
|
|
844
|
-
const base = slugify(import_node_path3.default.basename(file));
|
|
845
|
-
const rel = dir === "." ? `${base}.md` : import_node_path3.default.join(dir, `${base}.md`);
|
|
846
|
-
return rel;
|
|
857
|
+
return `${slugifyPath(file)}.md`;
|
|
847
858
|
}
|
|
848
859
|
async function createArticle(opts) {
|
|
849
860
|
const { collection, title } = parseAddTarget(opts.target);
|
|
850
861
|
const { name, dir } = await resolveCollectionDir(collection);
|
|
851
|
-
const filePath = import_node_path3.default.join(dir, `${
|
|
862
|
+
const filePath = import_node_path3.default.join(dir, `${slugifyPath(title)}.md`);
|
|
852
863
|
const body = opts.body ?? `# ${title}
|
|
853
864
|
`;
|
|
854
|
-
await import_promises5.default.mkdir(
|
|
865
|
+
await import_promises5.default.mkdir(import_node_path3.default.dirname(filePath), { recursive: true });
|
|
855
866
|
await import_promises5.default.writeFile(
|
|
856
867
|
filePath,
|
|
857
868
|
serializeArticle({ title, tags: opts.tags, body, audience: opts.audience, status: opts.status }),
|
|
@@ -1201,6 +1212,13 @@ function normaliseSite(raw) {
|
|
|
1201
1212
|
...home !== void 0 ? { home } : {}
|
|
1202
1213
|
};
|
|
1203
1214
|
}
|
|
1215
|
+
function normaliseContext(raw) {
|
|
1216
|
+
if (!raw || typeof raw !== "object") return void 0;
|
|
1217
|
+
const r = raw;
|
|
1218
|
+
if (!Array.isArray(r.collections)) return void 0;
|
|
1219
|
+
const collections = r.collections.filter((c) => typeof c === "string");
|
|
1220
|
+
return collections.length > 0 ? { collections } : void 0;
|
|
1221
|
+
}
|
|
1204
1222
|
async function readProjectConfig(dir) {
|
|
1205
1223
|
let raw;
|
|
1206
1224
|
try {
|
|
@@ -1229,7 +1247,11 @@ async function readProjectConfig(dir) {
|
|
|
1229
1247
|
collections.push({ name, path: parsed.path });
|
|
1230
1248
|
}
|
|
1231
1249
|
const site = normaliseSite(parsed.site);
|
|
1232
|
-
|
|
1250
|
+
const context = normaliseContext(parsed.context);
|
|
1251
|
+
return { ...site ? { site } : {}, ...context ? { context } : {}, collections };
|
|
1252
|
+
}
|
|
1253
|
+
async function readContextCollections(dir) {
|
|
1254
|
+
return (await readProjectConfig(dir))?.context?.collections ?? [];
|
|
1233
1255
|
}
|
|
1234
1256
|
async function resolveProjectCollections(dir) {
|
|
1235
1257
|
const config = await readProjectConfig(dir);
|
|
@@ -1727,8 +1749,12 @@ function localGraph(graph, focusId, include, siblingCap = 12) {
|
|
|
1727
1749
|
// src/site.ts
|
|
1728
1750
|
init_errors();
|
|
1729
1751
|
var isStatic = () => process.env.DIFFWIKI_STATIC === "1";
|
|
1730
|
-
function toTreeNodes(coll, nodes) {
|
|
1731
|
-
const visible =
|
|
1752
|
+
function toTreeNodes(coll, nodes, showSystem = false) {
|
|
1753
|
+
const visible = nodes.filter((n) => {
|
|
1754
|
+
if (!showSystem && n.audience === "system") return false;
|
|
1755
|
+
if (isStatic() && (n.audience === "private" || n.status === "draft")) return false;
|
|
1756
|
+
return true;
|
|
1757
|
+
});
|
|
1732
1758
|
return visible.map(
|
|
1733
1759
|
(n) => n.children ? {
|
|
1734
1760
|
name: n.name,
|
|
@@ -1736,7 +1762,7 @@ function toTreeNodes(coll, nodes) {
|
|
|
1736
1762
|
path: n.slug ? `/${coll}/${n.slug}` : void 0,
|
|
1737
1763
|
...n.audience ? { audience: n.audience } : {},
|
|
1738
1764
|
...n.status ? { status: n.status } : {},
|
|
1739
|
-
children: toTreeNodes(coll, n.children)
|
|
1765
|
+
children: toTreeNodes(coll, n.children, showSystem)
|
|
1740
1766
|
} : {
|
|
1741
1767
|
name: n.name,
|
|
1742
1768
|
title: n.title ?? n.name,
|
|
@@ -1754,44 +1780,50 @@ async function gitInfo(e) {
|
|
|
1754
1780
|
const s = await collectionGitStatus(e);
|
|
1755
1781
|
return s ? { branch: s.branch, behind: s.hasUpstream ? s.behind : void 0 } : {};
|
|
1756
1782
|
}
|
|
1757
|
-
async function buildCollections() {
|
|
1783
|
+
async function buildCollections(opts) {
|
|
1784
|
+
const showSystem = opts?.showSystem ?? false;
|
|
1758
1785
|
const entries = await listCollections();
|
|
1759
1786
|
const infos = await Promise.all(
|
|
1760
1787
|
entries.map(async (e) => {
|
|
1761
|
-
const
|
|
1788
|
+
const meta = await collectionMeta(e.path);
|
|
1789
|
+
const audience = meta.audience ?? e.audience;
|
|
1762
1790
|
return {
|
|
1763
1791
|
name: e.name,
|
|
1764
1792
|
type: e.type,
|
|
1765
1793
|
path: e.path,
|
|
1766
|
-
...title ? { title } : {},
|
|
1794
|
+
...meta.title ? { title: meta.title } : {},
|
|
1767
1795
|
...await gitInfo(e),
|
|
1768
1796
|
...audience ? { audience } : {},
|
|
1769
|
-
...status ? { status } : {}
|
|
1797
|
+
...meta.status ? { status: meta.status } : {}
|
|
1770
1798
|
};
|
|
1771
1799
|
})
|
|
1772
1800
|
);
|
|
1773
|
-
|
|
1801
|
+
const visible = showSystem ? infos : infos.filter((c) => c.audience !== "system");
|
|
1802
|
+
return isStatic() ? visible.filter((c) => c.audience !== "private" && c.status !== "draft") : visible;
|
|
1774
1803
|
}
|
|
1775
|
-
async function buildNavTree() {
|
|
1804
|
+
async function buildNavTree(opts) {
|
|
1805
|
+
const showSystem = opts?.showSystem ?? false;
|
|
1776
1806
|
const entries = await listCollections();
|
|
1777
1807
|
const trees = await Promise.all(
|
|
1778
1808
|
entries.map(async (e) => {
|
|
1779
|
-
const
|
|
1809
|
+
const meta = await collectionMeta(e.path);
|
|
1810
|
+
const audience = meta.audience ?? e.audience;
|
|
1780
1811
|
return {
|
|
1781
1812
|
collection: {
|
|
1782
1813
|
name: e.name,
|
|
1783
1814
|
type: e.type,
|
|
1784
1815
|
path: e.path,
|
|
1785
|
-
...title ? { title } : {},
|
|
1816
|
+
...meta.title ? { title: meta.title } : {},
|
|
1786
1817
|
...await gitInfo(e),
|
|
1787
1818
|
...audience ? { audience } : {},
|
|
1788
|
-
...status ? { status } : {}
|
|
1819
|
+
...meta.status ? { status: meta.status } : {}
|
|
1789
1820
|
},
|
|
1790
|
-
nodes: toTreeNodes(e.name, await listArticleTree(e.name))
|
|
1821
|
+
nodes: toTreeNodes(e.name, await listArticleTree(e.name), showSystem)
|
|
1791
1822
|
};
|
|
1792
1823
|
})
|
|
1793
1824
|
);
|
|
1794
|
-
|
|
1825
|
+
const visible = showSystem ? trees : trees.filter((t) => t.collection.audience !== "system");
|
|
1826
|
+
return isStatic() ? visible.filter((t) => t.collection.audience !== "private" && t.collection.status !== "draft") : visible;
|
|
1795
1827
|
}
|
|
1796
1828
|
function findNode(nodes, slug) {
|
|
1797
1829
|
for (const n of nodes) {
|
|
@@ -1978,12 +2010,20 @@ async function collectMarkdownFiles(dir) {
|
|
|
1978
2010
|
}
|
|
1979
2011
|
return files;
|
|
1980
2012
|
}
|
|
1981
|
-
|
|
2013
|
+
function normalizeCollections(collection) {
|
|
2014
|
+
if (collection === void 0) return [];
|
|
2015
|
+
const names = Array.isArray(collection) ? collection : [collection];
|
|
2016
|
+
return names.map((n) => n.trim()).filter(Boolean);
|
|
2017
|
+
}
|
|
2018
|
+
async function nativeSearch(term, collections, collection, hideSystem = false) {
|
|
1982
2019
|
let targets = collections;
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
2020
|
+
const names = normalizeCollections(collection);
|
|
2021
|
+
if (names.length > 0) {
|
|
2022
|
+
for (const n of names) {
|
|
2023
|
+
if (!collections.some((c) => c.name === n)) throw new CollectionNotFoundError(n);
|
|
2024
|
+
}
|
|
2025
|
+
const wanted = new Set(names);
|
|
2026
|
+
targets = collections.filter((c) => wanted.has(c.name));
|
|
1987
2027
|
}
|
|
1988
2028
|
const docs = [];
|
|
1989
2029
|
for (const entry of targets) {
|
|
@@ -1994,6 +2034,7 @@ async function nativeSearch(term, collections, collection) {
|
|
|
1994
2034
|
let body = "";
|
|
1995
2035
|
try {
|
|
1996
2036
|
const parsed = parseArticle(await import_promises10.default.readFile(full, "utf8"));
|
|
2037
|
+
if (hideSystem && parsed.audience === "system") continue;
|
|
1997
2038
|
if (parsed.title) title = parsed.title;
|
|
1998
2039
|
tags = parsed.tags;
|
|
1999
2040
|
body = parsed.body;
|
|
@@ -2046,28 +2087,49 @@ async function resolveDefaultSearchMode() {
|
|
|
2046
2087
|
}
|
|
2047
2088
|
return { engine, type: searchTypes[0] };
|
|
2048
2089
|
}
|
|
2090
|
+
async function dropSystemHits(hits) {
|
|
2091
|
+
const kept = [];
|
|
2092
|
+
for (const hit of hits) {
|
|
2093
|
+
try {
|
|
2094
|
+
if (parseArticle(await import_promises10.default.readFile(hit.path, "utf8")).audience === "system") continue;
|
|
2095
|
+
} catch {
|
|
2096
|
+
}
|
|
2097
|
+
kept.push(hit);
|
|
2098
|
+
}
|
|
2099
|
+
return kept;
|
|
2100
|
+
}
|
|
2049
2101
|
async function search(term, opts) {
|
|
2050
2102
|
const collections = await listCollections();
|
|
2103
|
+
const wanted = normalizeCollections(opts?.collection);
|
|
2104
|
+
for (const n of wanted) {
|
|
2105
|
+
if (!collections.some((c) => c.name === n)) throw new CollectionNotFoundError(n);
|
|
2106
|
+
}
|
|
2107
|
+
const showSystem = opts?.showSystem ?? await showSystemEnabled();
|
|
2108
|
+
const hideSystem = !showSystem && wanted.length === 0;
|
|
2109
|
+
const searchable = hideSystem ? collections.filter((c) => c.audience !== "system") : collections;
|
|
2110
|
+
const filterHits = (hits) => wanted.length > 0 ? hits.filter((h) => wanted.includes(h.collection)) : hits;
|
|
2051
2111
|
const engine = opts?.engine ?? await resolveDefaultEngine();
|
|
2052
2112
|
log2.debug(`resolved search engine "${engine}"`);
|
|
2053
2113
|
if (engine === NATIVE_ENGINE) {
|
|
2054
|
-
const hits = await nativeSearch(term,
|
|
2114
|
+
const hits = await nativeSearch(term, searchable, opts?.collection, hideSystem);
|
|
2055
2115
|
return { hits, engine, fellBackToNative: false };
|
|
2056
2116
|
}
|
|
2057
2117
|
const provider = await loadSearchProvider(engine);
|
|
2058
2118
|
if (!provider) {
|
|
2059
2119
|
const error = `no provider found for engine "${engine}"`;
|
|
2060
2120
|
log2.warn(`engine "${engine}" unavailable, falling back to native: ${error}`);
|
|
2061
|
-
const hits = await nativeSearch(term,
|
|
2121
|
+
const hits = await nativeSearch(term, searchable, opts?.collection, hideSystem);
|
|
2062
2122
|
return { hits, engine, fellBackToNative: true, error };
|
|
2063
2123
|
}
|
|
2064
2124
|
try {
|
|
2065
|
-
const
|
|
2125
|
+
const providerOpts = { ...opts, collection: wanted.length === 1 ? wanted[0] : void 0 };
|
|
2126
|
+
const raw = filterHits(await provider.handleSearch(term, searchable, providerOpts));
|
|
2127
|
+
const hits = hideSystem ? await dropSystemHits(raw) : raw;
|
|
2066
2128
|
return { hits, engine, fellBackToNative: false };
|
|
2067
2129
|
} catch (err) {
|
|
2068
2130
|
const message = err instanceof Error ? err.message : String(err);
|
|
2069
2131
|
log2.warn(`engine "${engine}" failed, falling back to native: ${message}`);
|
|
2070
|
-
const hits = await nativeSearch(term,
|
|
2132
|
+
const hits = await nativeSearch(term, searchable, opts?.collection, hideSystem);
|
|
2071
2133
|
return { hits, engine, fellBackToNative: true, error: message };
|
|
2072
2134
|
}
|
|
2073
2135
|
}
|
|
@@ -2092,6 +2154,7 @@ async function readArticleParts(collDir, slug) {
|
|
|
2092
2154
|
}
|
|
2093
2155
|
async function flatten(coll, collDir, nodes, out) {
|
|
2094
2156
|
for (const n of nodes) {
|
|
2157
|
+
if (n.audience === "system") continue;
|
|
2095
2158
|
if (isStatic2() && (n.audience === "private" || n.status === "draft")) continue;
|
|
2096
2159
|
if (n.slug && !n.children) {
|
|
2097
2160
|
const title = n.title ?? n.name;
|
|
@@ -2119,6 +2182,7 @@ async function buildSearchIndex(collection) {
|
|
|
2119
2182
|
try {
|
|
2120
2183
|
const collEntry = await findCollection(e.name);
|
|
2121
2184
|
const collDir = import_node_path10.default.resolve(collEntry?.path ?? e.path);
|
|
2185
|
+
if (collEntry?.audience === "system") continue;
|
|
2122
2186
|
if (isStatic2()) {
|
|
2123
2187
|
const cm = await collectionMeta(collDir);
|
|
2124
2188
|
if (cm.audience === "private" || cm.status === "draft") continue;
|
|
@@ -2480,6 +2544,153 @@ async function appendPluginDiagnostics(out, config) {
|
|
|
2480
2544
|
}
|
|
2481
2545
|
}
|
|
2482
2546
|
|
|
2547
|
+
// src/lint.ts
|
|
2548
|
+
var import_promises14 = __toESM(require("fs/promises"), 1);
|
|
2549
|
+
var import_node_path13 = __toESM(require("path"), 1);
|
|
2550
|
+
init_collections();
|
|
2551
|
+
init_errors();
|
|
2552
|
+
var BUDGETS = {
|
|
2553
|
+
recipe: { min: 300, max: 1e3 },
|
|
2554
|
+
"recipe-steps": { min: 500, max: 1500 },
|
|
2555
|
+
guide: { min: 1500, max: 5e3 },
|
|
2556
|
+
"system-design": { min: 2e3, max: 6e3 },
|
|
2557
|
+
"component-design": { min: 1e3, max: 3e3 },
|
|
2558
|
+
// Fallback for a bare `architecture` tag when system vs component isn't declared.
|
|
2559
|
+
architecture: { min: 1e3, max: 6e3 }
|
|
2560
|
+
};
|
|
2561
|
+
function estimateTokens(body) {
|
|
2562
|
+
return Math.ceil(body.trim().length / 4);
|
|
2563
|
+
}
|
|
2564
|
+
function inferShape(tags, body) {
|
|
2565
|
+
const t = new Set(tags.map((x) => x.toLowerCase()));
|
|
2566
|
+
if (t.has("guide")) return "guide";
|
|
2567
|
+
if (t.has("component-design") || t.has("component")) return "component-design";
|
|
2568
|
+
if (t.has("system-design") || t.has("system")) return "system-design";
|
|
2569
|
+
if (t.has("architecture")) return "architecture";
|
|
2570
|
+
if (/^###\s+Step\b/im.test(body)) return "recipe-steps";
|
|
2571
|
+
return "recipe";
|
|
2572
|
+
}
|
|
2573
|
+
function lintArticle(article) {
|
|
2574
|
+
const findings = [];
|
|
2575
|
+
const { title, tags, status, body } = article;
|
|
2576
|
+
const shape = inferShape(tags, body);
|
|
2577
|
+
const tokens = estimateTokens(body);
|
|
2578
|
+
const budget = BUDGETS[shape];
|
|
2579
|
+
if (!title || !title.trim()) {
|
|
2580
|
+
findings.push({ level: "error", rule: "frontmatter-title", message: "missing frontmatter title" });
|
|
2581
|
+
}
|
|
2582
|
+
if (tags.map((x) => x.toLowerCase()).includes("diffwiki-template")) {
|
|
2583
|
+
findings.push({
|
|
2584
|
+
level: "error",
|
|
2585
|
+
rule: "template-tag-leak",
|
|
2586
|
+
message: "instance still tagged diffwiki-template"
|
|
2587
|
+
});
|
|
2588
|
+
}
|
|
2589
|
+
if (tags.length === 0) {
|
|
2590
|
+
findings.push({ level: "warn", rule: "no-tags", message: "no tags -- add topic tags for recall" });
|
|
2591
|
+
}
|
|
2592
|
+
const hasRefsSection = /^##\s+References\b/im.test(body);
|
|
2593
|
+
const hasInlineLink = /\]\([^)]+\)/.test(body);
|
|
2594
|
+
if (!hasRefsSection && !hasInlineLink) {
|
|
2595
|
+
findings.push({
|
|
2596
|
+
level: "error",
|
|
2597
|
+
rule: "no-references",
|
|
2598
|
+
message: "has no ## References section and no inline links"
|
|
2599
|
+
});
|
|
2600
|
+
}
|
|
2601
|
+
if ((shape === "guide" || shape === "architecture") && !hasRefsSection) {
|
|
2602
|
+
findings.push({
|
|
2603
|
+
level: "warn",
|
|
2604
|
+
rule: "missing-references-section",
|
|
2605
|
+
message: `${shape} should end with a ## References section`
|
|
2606
|
+
});
|
|
2607
|
+
}
|
|
2608
|
+
if (tokens > budget.max) {
|
|
2609
|
+
findings.push({
|
|
2610
|
+
level: "error",
|
|
2611
|
+
rule: "token-budget",
|
|
2612
|
+
message: `body ~${tokens} tokens exceeds ${shape} budget (${budget.max})`
|
|
2613
|
+
});
|
|
2614
|
+
} else if (tokens < budget.min) {
|
|
2615
|
+
findings.push({
|
|
2616
|
+
level: "warn",
|
|
2617
|
+
rule: "token-thin",
|
|
2618
|
+
message: `body ~${tokens} tokens is thin for ${shape} (min ${budget.min})`
|
|
2619
|
+
});
|
|
2620
|
+
}
|
|
2621
|
+
if (status === "draft") {
|
|
2622
|
+
findings.push({ level: "warn", rule: "status-draft", message: "status is still draft" });
|
|
2623
|
+
}
|
|
2624
|
+
if (/\*\*Load when:\*\*/i.test(body)) {
|
|
2625
|
+
findings.push({
|
|
2626
|
+
level: "warn",
|
|
2627
|
+
rule: "load-when-prefix",
|
|
2628
|
+
message: "remove the **Load when:** prefix (implicit trigger)"
|
|
2629
|
+
});
|
|
2630
|
+
}
|
|
2631
|
+
if (/^[A-Za-z0-9][\w .+-]*:\s/.test(title)) {
|
|
2632
|
+
findings.push({ level: "warn", rule: "title-prefix", message: `title "${title}" repeats a path-like prefix` });
|
|
2633
|
+
}
|
|
2634
|
+
return findings;
|
|
2635
|
+
}
|
|
2636
|
+
async function walkMarkdown(dir) {
|
|
2637
|
+
let entries;
|
|
2638
|
+
try {
|
|
2639
|
+
entries = await import_promises14.default.readdir(dir, { withFileTypes: true });
|
|
2640
|
+
} catch {
|
|
2641
|
+
return [];
|
|
2642
|
+
}
|
|
2643
|
+
const out = [];
|
|
2644
|
+
for (const e of entries) {
|
|
2645
|
+
const full = import_node_path13.default.join(dir, e.name);
|
|
2646
|
+
if (e.isDirectory()) out.push(...await walkMarkdown(full));
|
|
2647
|
+
else if (e.isFile() && /\.mdx?$/.test(e.name) && !/-template\.mdx?$/.test(e.name)) out.push(full);
|
|
2648
|
+
}
|
|
2649
|
+
return out;
|
|
2650
|
+
}
|
|
2651
|
+
async function resolveScope(scope) {
|
|
2652
|
+
const all = await listCollections();
|
|
2653
|
+
if (scope === "all") return all.filter((c) => c.audience !== "system");
|
|
2654
|
+
if (scope === "current") {
|
|
2655
|
+
const cur = await resolveCwdCollection(process.cwd());
|
|
2656
|
+
if (!cur) throw new Error("No collection for the current directory -- pass -c <name> or -c all.");
|
|
2657
|
+
return [cur];
|
|
2658
|
+
}
|
|
2659
|
+
const names = scope.split(",").map((s) => s.trim()).filter(Boolean);
|
|
2660
|
+
const chosen = [];
|
|
2661
|
+
for (const n of names) {
|
|
2662
|
+
const hit = all.find((c) => c.name === n);
|
|
2663
|
+
if (!hit) throw new CollectionNotFoundError(n);
|
|
2664
|
+
chosen.push(hit);
|
|
2665
|
+
}
|
|
2666
|
+
return chosen;
|
|
2667
|
+
}
|
|
2668
|
+
async function lintCollections(scope) {
|
|
2669
|
+
const collections = await resolveScope(scope);
|
|
2670
|
+
const files = [];
|
|
2671
|
+
let errorCount = 0;
|
|
2672
|
+
let warnCount = 0;
|
|
2673
|
+
for (const coll of collections) {
|
|
2674
|
+
for (const full of await walkMarkdown(coll.path)) {
|
|
2675
|
+
const rel = import_node_path13.default.relative(coll.path, full);
|
|
2676
|
+
const label = `${coll.name}/${rel}`;
|
|
2677
|
+
let findings = [];
|
|
2678
|
+
try {
|
|
2679
|
+
const parsed = parseArticle(await import_promises14.default.readFile(full, "utf8"));
|
|
2680
|
+
findings = lintArticle(parsed);
|
|
2681
|
+
} catch (err) {
|
|
2682
|
+
findings = [{ level: "error", rule: "parse", message: `could not parse: ${err.message}` }];
|
|
2683
|
+
}
|
|
2684
|
+
for (const f of findings) {
|
|
2685
|
+
if (f.level === "error") errorCount++;
|
|
2686
|
+
else warnCount++;
|
|
2687
|
+
}
|
|
2688
|
+
files.push({ path: label, findings });
|
|
2689
|
+
}
|
|
2690
|
+
}
|
|
2691
|
+
return { files, fileCount: files.length, errorCount, warnCount };
|
|
2692
|
+
}
|
|
2693
|
+
|
|
2483
2694
|
// src/index.ts
|
|
2484
2695
|
init_logger();
|
|
2485
2696
|
|
|
@@ -2584,6 +2795,8 @@ init_registry2();
|
|
|
2584
2795
|
installPlugin,
|
|
2585
2796
|
invalidateLinkGraph,
|
|
2586
2797
|
isLinkedWorktree,
|
|
2798
|
+
lintArticle,
|
|
2799
|
+
lintCollections,
|
|
2587
2800
|
listArticleTree,
|
|
2588
2801
|
listAvailablePlugins,
|
|
2589
2802
|
listCollections,
|
|
@@ -2602,6 +2815,7 @@ init_registry2();
|
|
|
2602
2815
|
rankDocs,
|
|
2603
2816
|
readArticle,
|
|
2604
2817
|
readConfig,
|
|
2818
|
+
readContextCollections,
|
|
2605
2819
|
readPluginRegistry,
|
|
2606
2820
|
readProjectConfig,
|
|
2607
2821
|
readRegistry,
|
|
@@ -2632,7 +2846,9 @@ init_registry2();
|
|
|
2632
2846
|
setArticleMeta,
|
|
2633
2847
|
setConfigValue,
|
|
2634
2848
|
setTags,
|
|
2849
|
+
showSystemEnabled,
|
|
2635
2850
|
slugify,
|
|
2851
|
+
slugifyPath,
|
|
2636
2852
|
toTreeNodes,
|
|
2637
2853
|
tokenize,
|
|
2638
2854
|
unregisterCollection,
|