diffwiki-core 0.11.0 → 0.12.0-rc.202608010154.072979f

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-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);
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  emitPluginEvent
3
- } from "./chunk-MLUJY3HI.js";
3
+ } from "./chunk-7V2DIQNL.js";
4
4
  export {
5
5
  emitPluginEvent
6
6
  };
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" ? "private" : "public";
757
+ return value === "private" || value === "system" ? value : "public";
744
758
  }
745
759
  function coerceStatus(value) {
746
- return value === "draft" ? "draft" : "published";
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 = "private";
792
- if (article.status === "draft") front.status = "draft";
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
- 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;
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, `${slugify(title)}.md`);
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(dir, { recursive: true });
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
- return { ...site ? { site } : {}, collections };
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 = isStatic() ? nodes.filter((n) => n.audience !== "private" && n.status !== "draft") : nodes;
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 { title, audience, status } = await collectionMeta(e.path);
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
- return isStatic() ? infos.filter((c) => c.audience !== "private" && c.status !== "draft") : infos;
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 { title, audience, status } = await collectionMeta(e.path);
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
- return isStatic() ? trees.filter((t) => t.collection.audience !== "private" && t.collection.status !== "draft") : trees;
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
- async function nativeSearch(term, collections, collection) {
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
- if (collection) {
1984
- const one = collections.find((c) => c.name === collection);
1985
- if (!one) throw new CollectionNotFoundError(collection);
1986
- targets = [one];
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, collections, opts?.collection);
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, collections, opts?.collection);
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 hits = await provider.handleSearch(term, collections, opts);
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, collections, opts?.collection);
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,