diffwiki-core 0.12.0 → 0.14.0-rc.202608012047.5d78e11

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