diffwiki-core 0.14.0 → 0.15.0

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/index.cjs CHANGED
@@ -110,6 +110,9 @@ function configPath() {
110
110
  function collectionsDir() {
111
111
  return import_node_path.default.join(resolveHome(), "collections");
112
112
  }
113
+ function templatesDir() {
114
+ return import_node_path.default.join(resolveHome(), "templates");
115
+ }
113
116
  function collectionDir(name) {
114
117
  return import_node_path.default.join(collectionsDir(), name);
115
118
  }
@@ -134,7 +137,7 @@ function isMissing(err) {
134
137
  }
135
138
  async function readRegistry() {
136
139
  try {
137
- const parsed = JSON.parse(await import_promises.default.readFile(registryPath(), "utf8"));
140
+ const parsed = JSON.parse(await import_promises2.default.readFile(registryPath(), "utf8"));
138
141
  return { version: parsed.version ?? 1, collections: parsed.collections ?? [] };
139
142
  } catch (err) {
140
143
  if (isMissing(err)) return { version: 1, collections: [] };
@@ -142,8 +145,8 @@ async function readRegistry() {
142
145
  }
143
146
  }
144
147
  async function writeRegistry(registry) {
145
- await import_promises.default.mkdir(resolveHome(), { recursive: true });
146
- await import_promises.default.writeFile(registryPath(), `${JSON.stringify(registry, null, 2)}
148
+ await import_promises2.default.mkdir(resolveHome(), { recursive: true });
149
+ await import_promises2.default.writeFile(registryPath(), `${JSON.stringify(registry, null, 2)}
147
150
  `, "utf8");
148
151
  }
149
152
  async function registerCollection(entry) {
@@ -161,11 +164,11 @@ async function unregisterCollection(name) {
161
164
  registry.collections = registry.collections.filter((c) => c.name !== name);
162
165
  await writeRegistry(registry);
163
166
  }
164
- var import_promises;
167
+ var import_promises2;
165
168
  var init_registry = __esm({
166
169
  "src/registry.ts"() {
167
170
  "use strict";
168
- import_promises = __toESM(require("fs/promises"), 1);
171
+ import_promises2 = __toESM(require("fs/promises"), 1);
169
172
  init_paths();
170
173
  }
171
174
  });
@@ -176,7 +179,7 @@ function isMissing2(err) {
176
179
  }
177
180
  async function readPluginRegistry() {
178
181
  try {
179
- const parsed = JSON.parse(await import_promises2.default.readFile(pluginsRegistryPath(), "utf8"));
182
+ const parsed = JSON.parse(await import_promises3.default.readFile(pluginsRegistryPath(), "utf8"));
180
183
  return { version: parsed.version ?? 1, plugins: parsed.plugins ?? [] };
181
184
  } catch (err) {
182
185
  if (isMissing2(err)) return { version: 1, plugins: [] };
@@ -184,8 +187,8 @@ async function readPluginRegistry() {
184
187
  }
185
188
  }
186
189
  async function writePluginRegistry(registry) {
187
- await import_promises2.default.mkdir(resolveHome(), { recursive: true });
188
- await import_promises2.default.writeFile(pluginsRegistryPath(), `${JSON.stringify(registry, null, 2)}
190
+ await import_promises3.default.mkdir(resolveHome(), { recursive: true });
191
+ await import_promises3.default.writeFile(pluginsRegistryPath(), `${JSON.stringify(registry, null, 2)}
189
192
  `, "utf8");
190
193
  }
191
194
  async function upsertPlugin(record) {
@@ -216,11 +219,11 @@ async function findPluginRecord(name) {
216
219
  const registry = await readPluginRegistry();
217
220
  return registry.plugins.find((p) => p.name === name);
218
221
  }
219
- var import_promises2;
222
+ var import_promises3;
220
223
  var init_registry2 = __esm({
221
224
  "src/plugins/registry.ts"() {
222
225
  "use strict";
223
- import_promises2 = __toESM(require("fs/promises"), 1);
226
+ import_promises3 = __toESM(require("fs/promises"), 1);
224
227
  init_paths();
225
228
  }
226
229
  });
@@ -247,7 +250,7 @@ var init_logger = __esm({
247
250
  // src/plugins/host.ts
248
251
  async function invokePlugin(command, op, params, opts) {
249
252
  const timeoutMs = opts?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
250
- const scope = opts?.name ?? (0, import_node_path2.basename)(command[1] ?? command[0] ?? "plugin");
253
+ const scope = opts?.name ?? (0, import_node_path3.basename)(command[1] ?? command[0] ?? "plugin");
251
254
  return new Promise((resolve, reject) => {
252
255
  const child = (0, import_node_child_process.spawn)(command[0], command.slice(1), {
253
256
  stdio: ["pipe", "pipe", "pipe"]
@@ -351,7 +354,7 @@ function mapHits(result, collections) {
351
354
  if (typeof h.collection !== "string" || typeof h.relPath !== "string") continue;
352
355
  const title = typeof h.title === "string" ? h.title : h.relPath;
353
356
  const coll = collections.find((c) => c.name === h.collection);
354
- const abs = coll ? import_node_path2.default.join(coll.path, h.relPath) : h.relPath;
357
+ const abs = coll ? import_node_path3.default.join(coll.path, h.relPath) : h.relPath;
355
358
  out.push({
356
359
  collection: h.collection,
357
360
  path: abs,
@@ -359,7 +362,11 @@ function mapHits(result, collections) {
359
362
  tags: Array.isArray(h.tags) ? h.tags.filter((t) => typeof t === "string") : [],
360
363
  snippet: typeof h.snippet === "string" ? h.snippet : void 0,
361
364
  score: typeof h.score === "number" ? h.score : void 0,
362
- docid: typeof h.docid === "string" ? h.docid : void 0
365
+ docid: typeof h.docid === "string" ? h.docid : void 0,
366
+ rawChunk: typeof h.rawChunk === "string" ? h.rawChunk : void 0,
367
+ cleanedChunk: typeof h.cleanedChunk === "string" ? h.cleanedChunk : void 0,
368
+ line: typeof h.line === "number" ? h.line : void 0,
369
+ snippetFromBody: h.snippetFromBody === true ? true : void 0
363
370
  });
364
371
  }
365
372
  return out;
@@ -389,7 +396,8 @@ async function loadSearchProvider(name) {
389
396
  collections: toWireCollections(collections),
390
397
  collection: opts?.collection,
391
398
  type: opts?.type,
392
- limit: opts?.limit
399
+ limit: opts?.limit,
400
+ expansions: opts?.expansions
393
401
  },
394
402
  { timeoutMs: SEARCH_TIMEOUT_MS, name: record.name }
395
403
  );
@@ -447,12 +455,12 @@ async function listSearchModes() {
447
455
  }
448
456
  return modes;
449
457
  }
450
- var import_node_child_process, import_node_path2, LEVEL_METHOD, DEFAULT_TIMEOUT_MS, SEARCH_TIMEOUT_MS, LONG_TIMEOUT_MS;
458
+ var import_node_child_process, import_node_path3, LEVEL_METHOD, DEFAULT_TIMEOUT_MS, SEARCH_TIMEOUT_MS, LONG_TIMEOUT_MS;
451
459
  var init_host = __esm({
452
460
  "src/plugins/host.ts"() {
453
461
  "use strict";
454
462
  import_node_child_process = require("child_process");
455
- import_node_path2 = __toESM(require("path"), 1);
463
+ import_node_path3 = __toESM(require("path"), 1);
456
464
  init_logger();
457
465
  init_types();
458
466
  init_registry2();
@@ -526,7 +534,7 @@ async function findCollectionById(id) {
526
534
  async function createCollection(name, opts) {
527
535
  if (await findCollection(name)) throw new CollectionExistsError(name);
528
536
  const dir = collectionDir(name);
529
- await import_promises3.default.mkdir(dir, { recursive: true });
537
+ await import_promises4.default.mkdir(dir, { recursive: true });
530
538
  const entry = {
531
539
  name,
532
540
  type: "global",
@@ -543,16 +551,16 @@ async function removeCollection(name) {
543
551
  if (!entry) throw new CollectionNotFoundError(name);
544
552
  await unregisterCollection(name);
545
553
  if (entry.type === "global") {
546
- await import_promises3.default.rm(entry.path, { recursive: true, force: true });
554
+ await import_promises4.default.rm(entry.path, { recursive: true, force: true });
547
555
  }
548
556
  void fireHook("collection-removed", name);
549
557
  return entry;
550
558
  }
551
- var import_promises3;
559
+ var import_promises4;
552
560
  var init_collections = __esm({
553
561
  "src/collections.ts"() {
554
562
  "use strict";
555
- import_promises3 = __toESM(require("fs/promises"), 1);
563
+ import_promises4 = __toESM(require("fs/promises"), 1);
556
564
  init_paths();
557
565
  init_registry();
558
566
  init_errors();
@@ -616,11 +624,14 @@ __export(index_exports, {
616
624
  listEnabledPlugins: () => listEnabledPlugins,
617
625
  listPlugins: () => listPlugins,
618
626
  listSearchModes: () => listSearchModes,
627
+ listTemplateShapes: () => listTemplateShapes,
628
+ listTemplates: () => listTemplates,
619
629
  loadSearchProvider: () => loadSearchProvider,
620
630
  localGraph: () => localGraph,
621
631
  nativeSearch: () => nativeSearch,
622
632
  parseAddTarget: () => parseAddTarget,
623
633
  parseArticle: () => parseArticle,
634
+ parseFrontmatter: () => parseFrontmatter,
624
635
  parsePathTarget: () => parsePathTarget,
625
636
  pluginsDir: () => pluginsDir,
626
637
  pluginsRegistryPath: () => pluginsRegistryPath,
@@ -642,6 +653,7 @@ __export(index_exports, {
642
653
  removePlugin: () => removePlugin,
643
654
  removeTags: () => removeTags,
644
655
  renderArticle: () => renderArticle,
656
+ repairPlugins: () => repairPlugins,
645
657
  repoId: () => repoId,
646
658
  repoRemoteName: () => repoRemoteName,
647
659
  resolveCollectionSelection: () => resolveCollectionSelection,
@@ -655,13 +667,17 @@ __export(index_exports, {
655
667
  resolveProjectCollections: () => resolveProjectCollections,
656
668
  search: () => search,
657
669
  seedProjectHome: () => seedProjectHome,
670
+ seedTemplates: () => seedTemplates,
658
671
  serializeArticle: () => serializeArticle,
659
672
  setArticleMeta: () => setArticleMeta,
660
673
  setConfigValue: () => setConfigValue,
674
+ setContextCollections: () => setContextCollections,
661
675
  setTags: () => setTags,
662
676
  showSystemEnabled: () => showSystemEnabled,
663
677
  slugify: () => slugify,
664
678
  slugifyPath: () => slugifyPath,
679
+ stripFrontmatter: () => stripFrontmatter,
680
+ templatesDir: () => templatesDir,
665
681
  toTreeNodes: () => toTreeNodes,
666
682
  tokenize: () => tokenize,
667
683
  unregisterCollection: () => unregisterCollection,
@@ -677,70 +693,10 @@ init_types();
677
693
  init_errors();
678
694
  init_paths();
679
695
 
680
- // src/config.ts
681
- var import_promises4 = __toESM(require("fs/promises"), 1);
696
+ // src/templates.ts
697
+ var import_promises = __toESM(require("fs/promises"), 1);
698
+ var import_node_path2 = __toESM(require("path"), 1);
682
699
  init_paths();
683
- init_collections();
684
- init_errors();
685
- init_registry2();
686
- init_types();
687
- function isMissing3(err) {
688
- return err?.code === "ENOENT";
689
- }
690
- async function readConfig() {
691
- try {
692
- return JSON.parse(await import_promises4.default.readFile(configPath(), "utf8"));
693
- } catch (err) {
694
- if (isMissing3(err)) return {};
695
- throw err;
696
- }
697
- }
698
- async function writeConfig(config) {
699
- await import_promises4.default.mkdir(resolveHome(), { recursive: true });
700
- await import_promises4.default.writeFile(configPath(), `${JSON.stringify(config, null, 2)}
701
- `, "utf8");
702
- }
703
- async function setConfigValue(key, value) {
704
- if (key === "defaultCollection" && !await findCollection(value)) {
705
- throw new CollectionNotFoundError(value);
706
- }
707
- if (key === "defaultSearch") {
708
- const engine = value.split(":")[0];
709
- if (engine !== NATIVE_ENGINE) {
710
- const enabled = await listEnabledPlugins("search");
711
- if (!enabled.some((p) => p.name === engine)) {
712
- console.warn(
713
- `! defaultSearch "${value}" names an engine that is not installed/enabled \u2014 native will be used until it is`
714
- );
715
- }
716
- }
717
- }
718
- const config = await readConfig();
719
- config[key] = value;
720
- await writeConfig(config);
721
- }
722
- async function getConfigValue(key) {
723
- const config = await readConfig();
724
- const value = config[key];
725
- return typeof value === "string" ? value : void 0;
726
- }
727
- async function showSystemEnabled() {
728
- const config = await readConfig();
729
- return config.showSystem === true || config.showSystem === "true";
730
- }
731
-
732
- // src/index.ts
733
- init_registry();
734
-
735
- // src/slugify.ts
736
- function slugify(input) {
737
- const slug = input.toLowerCase().replace(/^#+\s*/, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80).replace(/-+$/g, "");
738
- return slug.length > 0 ? slug : "untitled";
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
- }
744
700
 
745
701
  // src/frontmatter.ts
746
702
  var import_gray_matter = __toESM(require("gray-matter"), 1);
@@ -787,6 +743,18 @@ function parseFallback(raw) {
787
743
  }
788
744
  return { title, tags: [], audience, status, body: body.replace(/^\n+/, "") };
789
745
  }
746
+ var FRONTMATTER_BLOCK_RE = /^\uFEFF?[ \t]*---\r?\n[\s\S]*?\r?\n---[ \t]*(?:\r?\n|$)/;
747
+ function stripFrontmatter(raw) {
748
+ return raw.replace(FRONTMATTER_BLOCK_RE, "");
749
+ }
750
+ function parseFrontmatter(raw) {
751
+ try {
752
+ const { data, content } = (0, import_gray_matter.default)(raw, MATTER_OPTS);
753
+ return { data: data ?? {}, body: content };
754
+ } catch {
755
+ return { data: {}, body: raw };
756
+ }
757
+ }
790
758
  function parseArticle(raw) {
791
759
  try {
792
760
  const { data, content } = (0, import_gray_matter.default)(raw, MATTER_OPTS);
@@ -807,12 +775,118 @@ function serializeArticle(article) {
807
775
  return import_gray_matter.default.stringify(article.body, front, MATTER_OPTS);
808
776
  }
809
777
 
778
+ // src/templates.ts
779
+ async function seedTemplates(srcDir) {
780
+ const dir = templatesDir();
781
+ await import_promises.default.mkdir(dir, { recursive: true });
782
+ const files = (await import_promises.default.readdir(srcDir)).filter((f) => f.endsWith(".md")).sort();
783
+ const seeded = [];
784
+ const skipped = [];
785
+ for (const file of files) {
786
+ const dest = import_node_path2.default.join(dir, file);
787
+ try {
788
+ await import_promises.default.access(dest);
789
+ skipped.push(file);
790
+ continue;
791
+ } catch {
792
+ }
793
+ await import_promises.default.copyFile(import_node_path2.default.join(srcDir, file), dest);
794
+ seeded.push(file);
795
+ }
796
+ return { seeded, skipped, dir };
797
+ }
798
+ async function listTemplateShapes(srcDir) {
799
+ const files = await import_promises.default.readdir(srcDir);
800
+ return files.filter((f) => f.endsWith("-template.md")).map((f) => f.replace(/-template\.md$/, "")).sort();
801
+ }
802
+ async function listTemplates(srcDir) {
803
+ const files = (await import_promises.default.readdir(srcDir)).filter((f) => f.endsWith("-template.md")).sort();
804
+ const out = [];
805
+ for (const file of files) {
806
+ let data = {};
807
+ try {
808
+ ({ data } = parseFrontmatter(await import_promises.default.readFile(import_node_path2.default.join(srcDir, file), "utf8")));
809
+ } catch {
810
+ }
811
+ const shape = typeof data.shape === "string" && data.shape ? data.shape : file.replace(/-template\.md$/, "");
812
+ const purpose = typeof data.purpose === "string" ? data.purpose : "";
813
+ const budget = Array.isArray(data.budget) && data.budget.length === 2 ? [Number(data.budget[0]), Number(data.budget[1])] : void 0;
814
+ out.push({ shape, purpose, budget, file });
815
+ }
816
+ return out;
817
+ }
818
+
819
+ // src/config.ts
820
+ var import_promises5 = __toESM(require("fs/promises"), 1);
821
+ init_paths();
822
+ init_collections();
823
+ init_errors();
824
+ init_registry2();
825
+ init_types();
826
+ function isMissing3(err) {
827
+ return err?.code === "ENOENT";
828
+ }
829
+ async function readConfig() {
830
+ try {
831
+ return JSON.parse(await import_promises5.default.readFile(configPath(), "utf8"));
832
+ } catch (err) {
833
+ if (isMissing3(err)) return {};
834
+ throw err;
835
+ }
836
+ }
837
+ async function writeConfig(config) {
838
+ await import_promises5.default.mkdir(resolveHome(), { recursive: true });
839
+ await import_promises5.default.writeFile(configPath(), `${JSON.stringify(config, null, 2)}
840
+ `, "utf8");
841
+ }
842
+ async function setConfigValue(key, value) {
843
+ if (key === "defaultCollection" && !await findCollection(value)) {
844
+ throw new CollectionNotFoundError(value);
845
+ }
846
+ if (key === "defaultSearch") {
847
+ const engine = value.split(":")[0];
848
+ if (engine !== NATIVE_ENGINE) {
849
+ const enabled = await listEnabledPlugins("search");
850
+ if (!enabled.some((p) => p.name === engine)) {
851
+ console.warn(
852
+ `! defaultSearch "${value}" names an engine that is not installed/enabled \u2014 native will be used until it is`
853
+ );
854
+ }
855
+ }
856
+ }
857
+ const config = await readConfig();
858
+ config[key] = value;
859
+ await writeConfig(config);
860
+ }
861
+ async function getConfigValue(key) {
862
+ const config = await readConfig();
863
+ const value = config[key];
864
+ return typeof value === "string" ? value : void 0;
865
+ }
866
+ async function showSystemEnabled() {
867
+ const config = await readConfig();
868
+ return config.showSystem === true || config.showSystem === "true";
869
+ }
870
+
871
+ // src/index.ts
872
+ init_registry();
873
+
874
+ // src/slugify.ts
875
+ function slugify(input) {
876
+ const slug = input.toLowerCase().replace(/^#+\s*/, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80).replace(/-+$/g, "");
877
+ return slug.length > 0 ? slug : "untitled";
878
+ }
879
+ function slugifyPath(input) {
880
+ const segs = input.split("/").map((s) => s.trim()).filter((s) => s.length > 0).map((s) => slugify(s));
881
+ return segs.length > 0 ? segs.join("/") : "untitled";
882
+ }
883
+
810
884
  // src/index.ts
811
885
  init_collections();
812
886
 
813
887
  // src/articles.ts
814
- var import_promises5 = __toESM(require("fs/promises"), 1);
815
- var import_node_path3 = __toESM(require("path"), 1);
888
+ var import_promises6 = __toESM(require("fs/promises"), 1);
889
+ var import_node_path4 = __toESM(require("path"), 1);
816
890
  init_collections();
817
891
  init_errors();
818
892
  async function fireHook2(event, collection, relPath) {
@@ -859,24 +933,24 @@ function ensureMdPath(file) {
859
933
  async function createArticle(opts) {
860
934
  const { collection, title } = parseAddTarget(opts.target);
861
935
  const { name, dir } = await resolveCollectionDir(collection);
862
- const filePath = import_node_path3.default.join(dir, `${slugifyPath(title)}.md`);
936
+ const filePath = import_node_path4.default.join(dir, `${slugifyPath(title)}.md`);
863
937
  const body = opts.body ?? `# ${title}
864
938
  `;
865
- await import_promises5.default.mkdir(import_node_path3.default.dirname(filePath), { recursive: true });
866
- await import_promises5.default.writeFile(
939
+ await import_promises6.default.mkdir(import_node_path4.default.dirname(filePath), { recursive: true });
940
+ await import_promises6.default.writeFile(
867
941
  filePath,
868
942
  serializeArticle({ title, tags: opts.tags, body, audience: opts.audience, status: opts.status }),
869
943
  "utf8"
870
944
  );
871
- void fireHook2("article-created", name, import_node_path3.default.relative(dir, filePath));
945
+ void fireHook2("article-created", name, import_node_path4.default.relative(dir, filePath));
872
946
  return { title, tags: opts.tags, path: filePath, collection: name };
873
947
  }
874
948
  async function loadArticle(target) {
875
949
  const { collection, file } = parsePathTarget(target);
876
950
  const { dir } = await resolveCollectionDir(collection);
877
- const filePath = import_node_path3.default.join(dir, ensureMdPath(file));
951
+ const filePath = import_node_path4.default.join(dir, ensureMdPath(file));
878
952
  try {
879
- return { filePath, dir, raw: await import_promises5.default.readFile(filePath, "utf8"), collection };
953
+ return { filePath, dir, raw: await import_promises6.default.readFile(filePath, "utf8"), collection };
880
954
  } catch (err) {
881
955
  if (err.code === "ENOENT") throw new ArticleNotFoundError(target);
882
956
  throw err;
@@ -885,7 +959,7 @@ async function loadArticle(target) {
885
959
  async function updateArticleBody(target, body) {
886
960
  const { filePath, dir, raw, collection } = await loadArticle(target);
887
961
  const parsed = parseArticle(raw);
888
- await import_promises5.default.writeFile(
962
+ await import_promises6.default.writeFile(
889
963
  filePath,
890
964
  serializeArticle({
891
965
  title: parsed.title,
@@ -896,14 +970,14 @@ async function updateArticleBody(target, body) {
896
970
  }),
897
971
  "utf8"
898
972
  );
899
- void fireHook2("article-updated", collection, import_node_path3.default.relative(dir, filePath));
973
+ void fireHook2("article-updated", collection, import_node_path4.default.relative(dir, filePath));
900
974
  return { title: parsed.title, tags: parsed.tags, path: filePath, collection };
901
975
  }
902
976
  async function mutateTags(target, fn) {
903
977
  const { filePath, dir, raw, collection } = await loadArticle(target);
904
978
  const parsed = parseArticle(raw);
905
979
  const tags = fn(parsed.tags);
906
- await import_promises5.default.writeFile(
980
+ await import_promises6.default.writeFile(
907
981
  filePath,
908
982
  serializeArticle({
909
983
  title: parsed.title,
@@ -914,7 +988,7 @@ async function mutateTags(target, fn) {
914
988
  }),
915
989
  "utf8"
916
990
  );
917
- void fireHook2("article-updated", collection, import_node_path3.default.relative(dir, filePath));
991
+ void fireHook2("article-updated", collection, import_node_path4.default.relative(dir, filePath));
918
992
  return { title: parsed.title, tags, path: filePath, collection };
919
993
  }
920
994
  function addTags(target, tags) {
@@ -932,24 +1006,24 @@ async function setArticleMeta(target, meta) {
932
1006
  const parsed = parseArticle(raw);
933
1007
  const audience = meta.audience ?? parsed.audience;
934
1008
  const status = meta.status ?? parsed.status;
935
- await import_promises5.default.writeFile(
1009
+ await import_promises6.default.writeFile(
936
1010
  filePath,
937
1011
  serializeArticle({ title: parsed.title, tags: parsed.tags, audience, status, body: parsed.body }),
938
1012
  "utf8"
939
1013
  );
940
- void fireHook2("article-updated", collection, import_node_path3.default.relative(dir, filePath));
1014
+ void fireHook2("article-updated", collection, import_node_path4.default.relative(dir, filePath));
941
1015
  return { title: parsed.title, tags: parsed.tags, path: filePath, collection };
942
1016
  }
943
1017
  async function removeArticle(target) {
944
1018
  const { filePath, dir, collection } = await loadArticle(target);
945
- await import_promises5.default.rm(filePath);
946
- void fireHook2("article-removed", collection, import_node_path3.default.relative(dir, filePath));
1019
+ await import_promises6.default.rm(filePath);
1020
+ void fireHook2("article-removed", collection, import_node_path4.default.relative(dir, filePath));
947
1021
  return filePath;
948
1022
  }
949
1023
 
950
1024
  // src/repo.ts
951
- var import_promises7 = __toESM(require("fs/promises"), 1);
952
- var import_node_path5 = __toESM(require("path"), 1);
1025
+ var import_promises8 = __toESM(require("fs/promises"), 1);
1026
+ var import_node_path6 = __toESM(require("path"), 1);
953
1027
  var import_node_child_process3 = require("child_process");
954
1028
  var import_node_util2 = require("util");
955
1029
  var import_yaml2 = require("yaml");
@@ -957,8 +1031,8 @@ init_collections();
957
1031
  init_registry();
958
1032
 
959
1033
  // src/git.ts
960
- var import_promises6 = __toESM(require("fs/promises"), 1);
961
- var import_node_path4 = __toESM(require("path"), 1);
1034
+ var import_promises7 = __toESM(require("fs/promises"), 1);
1035
+ var import_node_path5 = __toESM(require("path"), 1);
962
1036
  var import_node_child_process2 = require("child_process");
963
1037
  var import_node_util = require("util");
964
1038
  init_logger();
@@ -984,8 +1058,8 @@ async function isLinkedWorktree(cwd) {
984
1058
  const gitDir = await git(cwd, ["rev-parse", "--absolute-git-dir"]);
985
1059
  const commonDir = await git(cwd, ["rev-parse", "--git-common-dir"]);
986
1060
  if (!gitDir || !commonDir) return false;
987
- const commonAbs = import_node_path4.default.isAbsolute(commonDir) ? commonDir : import_node_path4.default.resolve(cwd, commonDir);
988
- return import_node_path4.default.resolve(gitDir) !== import_node_path4.default.resolve(commonAbs);
1061
+ const commonAbs = import_node_path5.default.isAbsolute(commonDir) ? commonDir : import_node_path5.default.resolve(cwd, commonDir);
1062
+ return import_node_path5.default.resolve(gitDir) !== import_node_path5.default.resolve(commonAbs);
989
1063
  }
990
1064
  async function repoRemoteName(cwd) {
991
1065
  const url = await git(cwd, ["remote", "get-url", "origin"]);
@@ -1027,8 +1101,8 @@ var timestampCache = /* @__PURE__ */ new Map();
1027
1101
  async function fileTimestamps(absPath) {
1028
1102
  const cached2 = timestampCache.get(absPath);
1029
1103
  if (cached2) return cached2;
1030
- const dir = import_node_path4.default.dirname(absPath);
1031
- const base = import_node_path4.default.basename(absPath);
1104
+ const dir = import_node_path5.default.dirname(absPath);
1105
+ const base = import_node_path5.default.basename(absPath);
1032
1106
  const [addLog, lastLog] = await Promise.all([
1033
1107
  git(dir, ["log", "--follow", "--diff-filter=A", "--format=%aI", "--", base]),
1034
1108
  git(dir, ["log", "-1", "--format=%aI", "--", base])
@@ -1039,7 +1113,7 @@ async function fileTimestamps(absPath) {
1039
1113
  let updated = gitUpdated;
1040
1114
  if (!created || !updated) {
1041
1115
  try {
1042
- const st = await import_promises6.default.stat(absPath);
1116
+ const st = await import_promises7.default.stat(absPath);
1043
1117
  if (!updated) updated = st.mtime.toISOString();
1044
1118
  if (!created) created = isEpochish(st.birthtime) ? st.mtime.toISOString() : st.birthtime.toISOString();
1045
1119
  } catch {
@@ -1057,18 +1131,18 @@ var REPO_CONFIG_FILE = "diffwiki.yaml";
1057
1131
  async function detectRepoName(cwd) {
1058
1132
  try {
1059
1133
  const { stdout } = await execFileAsync2("git", ["rev-parse", "--show-toplevel"], { cwd });
1060
- return import_node_path5.default.basename(stdout.trim());
1134
+ return import_node_path6.default.basename(stdout.trim());
1061
1135
  } catch {
1062
- return import_node_path5.default.basename(import_node_path5.default.resolve(cwd));
1136
+ return import_node_path6.default.basename(import_node_path6.default.resolve(cwd));
1063
1137
  }
1064
1138
  }
1065
1139
  async function readRepoConfig(cwd) {
1066
1140
  try {
1067
- const raw = await import_promises7.default.readFile(import_node_path5.default.join(cwd, REPO_CONFIG_FILE), "utf8");
1141
+ const raw = await import_promises8.default.readFile(import_node_path6.default.join(cwd, REPO_CONFIG_FILE), "utf8");
1068
1142
  const parsed = (0, import_yaml2.parse)(raw) ?? {};
1069
1143
  if (!parsed.path) return void 0;
1070
1144
  return {
1071
- collection: parsed.collection ?? import_node_path5.default.basename(import_node_path5.default.resolve(cwd)),
1145
+ collection: parsed.collection ?? import_node_path6.default.basename(import_node_path6.default.resolve(cwd)),
1072
1146
  path: parsed.path
1073
1147
  };
1074
1148
  } catch (err) {
@@ -1077,9 +1151,9 @@ async function readRepoConfig(cwd) {
1077
1151
  }
1078
1152
  }
1079
1153
  async function initRepoWiki(opts) {
1080
- const cwd = import_node_path5.default.resolve(opts.cwd);
1154
+ const cwd = import_node_path6.default.resolve(opts.cwd);
1081
1155
  const wikiPath = opts.wikiPath ?? "wiki";
1082
- const absWiki = import_node_path5.default.join(cwd, wikiPath);
1156
+ const absWiki = import_node_path6.default.join(cwd, wikiPath);
1083
1157
  let name = opts.collection ?? await detectRepoName(cwd);
1084
1158
  const existing = await findCollection(name);
1085
1159
  if (existing && existing.path !== absWiki) {
@@ -1094,9 +1168,9 @@ async function initRepoWiki(opts) {
1094
1168
  n += 1;
1095
1169
  }
1096
1170
  }
1097
- await import_promises7.default.mkdir(absWiki, { recursive: true });
1171
+ await import_promises8.default.mkdir(absWiki, { recursive: true });
1098
1172
  const config = { collection: name, path: wikiPath };
1099
- await import_promises7.default.writeFile(import_node_path5.default.join(cwd, REPO_CONFIG_FILE), (0, import_yaml2.stringify)(config), "utf8");
1173
+ await import_promises8.default.writeFile(import_node_path6.default.join(cwd, REPO_CONFIG_FILE), (0, import_yaml2.stringify)(config), "utf8");
1100
1174
  const entry = {
1101
1175
  name,
1102
1176
  type: "repo",
@@ -1107,15 +1181,15 @@ async function initRepoWiki(opts) {
1107
1181
  return entry;
1108
1182
  }
1109
1183
  async function addExternalCollection(opts) {
1110
- const cwd = import_node_path5.default.resolve(opts.cwd);
1184
+ const cwd = import_node_path6.default.resolve(opts.cwd);
1111
1185
  const root = await ensureGitRepo(cwd);
1112
1186
  if (await isLinkedWorktree(cwd)) throw new WorktreeNotAllowedError(root);
1113
1187
  const id = await repoId(root) ?? `path:${root}`;
1114
1188
  const already = await findCollectionById(id);
1115
1189
  if (already) throw new CollectionAlreadyAddedError(already.name);
1116
1190
  const docsRel = opts.docsDir ?? "docs";
1117
- const contentDir = import_node_path5.default.join(root, docsRel);
1118
- let name = await repoRemoteName(root) ?? import_node_path5.default.basename(root);
1191
+ const contentDir = import_node_path6.default.join(root, docsRel);
1192
+ let name = await repoRemoteName(root) ?? import_node_path6.default.basename(root);
1119
1193
  if (await findCollection(name)) {
1120
1194
  let n = 2;
1121
1195
  while (await findCollection(`${name}-${n}`)) n += 1;
@@ -1134,14 +1208,14 @@ async function addExternalCollection(opts) {
1134
1208
  return entry;
1135
1209
  }
1136
1210
  async function resolveCwdCollection(cwd) {
1137
- const abs = import_node_path5.default.resolve(cwd);
1211
+ const abs = import_node_path6.default.resolve(cwd);
1138
1212
  const root = await gitToplevel(abs);
1139
1213
  if (root) {
1140
1214
  const id = await repoId(root) ?? `path:${root}`;
1141
1215
  const byId = await findCollectionById(id);
1142
1216
  if (byId) return byId;
1143
1217
  }
1144
- const within = (base) => abs === base || abs.startsWith(base + import_node_path5.default.sep);
1218
+ const within = (base) => abs === base || abs.startsWith(base + import_node_path6.default.sep);
1145
1219
  const all = await listCollections();
1146
1220
  let best;
1147
1221
  let bestLen = -1;
@@ -1165,9 +1239,9 @@ async function collectionGitStatus(entry) {
1165
1239
  }
1166
1240
 
1167
1241
  // src/project.ts
1168
- var import_promises8 = __toESM(require("fs/promises"), 1);
1242
+ var import_promises9 = __toESM(require("fs/promises"), 1);
1169
1243
  var import_node_os2 = __toESM(require("os"), 1);
1170
- var import_node_path6 = __toESM(require("path"), 1);
1244
+ var import_node_path7 = __toESM(require("path"), 1);
1171
1245
  var import_yaml3 = require("yaml");
1172
1246
  var PROJECT_CONFIG_FILE = "diffwiki.yaml";
1173
1247
  function normaliseSite(raw) {
@@ -1222,7 +1296,7 @@ function normaliseContext(raw) {
1222
1296
  async function readProjectConfig(dir) {
1223
1297
  let raw;
1224
1298
  try {
1225
- raw = await import_promises8.default.readFile(import_node_path6.default.join(dir, PROJECT_CONFIG_FILE), "utf8");
1299
+ raw = await import_promises9.default.readFile(import_node_path7.default.join(dir, PROJECT_CONFIG_FILE), "utf8");
1226
1300
  } catch (err) {
1227
1301
  if (err.code === "ENOENT") return void 0;
1228
1302
  throw err;
@@ -1243,7 +1317,7 @@ async function readProjectConfig(dir) {
1243
1317
  }
1244
1318
  collections.sort((a, b) => (a.order ?? Number.MAX_SAFE_INTEGER) - (b.order ?? Number.MAX_SAFE_INTEGER));
1245
1319
  } else if (typeof parsed.path === "string") {
1246
- const name = typeof parsed.collection === "string" ? parsed.collection : import_node_path6.default.basename(import_node_path6.default.resolve(dir));
1320
+ const name = typeof parsed.collection === "string" ? parsed.collection : import_node_path7.default.basename(import_node_path7.default.resolve(dir));
1247
1321
  collections.push({ name, path: parsed.path });
1248
1322
  }
1249
1323
  const site = normaliseSite(parsed.site);
@@ -1253,15 +1327,28 @@ async function readProjectConfig(dir) {
1253
1327
  async function readContextCollections(dir) {
1254
1328
  return (await readProjectConfig(dir))?.context?.collections ?? [];
1255
1329
  }
1330
+ async function setContextCollections(dir, collections) {
1331
+ const file = import_node_path7.default.join(dir, PROJECT_CONFIG_FILE);
1332
+ let raw = "";
1333
+ try {
1334
+ raw = await import_promises9.default.readFile(file, "utf8");
1335
+ } catch (err) {
1336
+ if (err.code !== "ENOENT") throw err;
1337
+ }
1338
+ const doc = (0, import_yaml3.parseDocument)(raw);
1339
+ doc.setIn(["context", "collections"], collections);
1340
+ await import_promises9.default.writeFile(file, doc.toString(), "utf8");
1341
+ return file;
1342
+ }
1256
1343
  async function resolveProjectCollections(dir) {
1257
1344
  const config = await readProjectConfig(dir);
1258
1345
  if (!config) return [];
1259
- const root = import_node_path6.default.resolve(dir);
1346
+ const root = import_node_path7.default.resolve(dir);
1260
1347
  const createdAt = (/* @__PURE__ */ new Date()).toISOString();
1261
1348
  return config.collections.map((c) => ({
1262
1349
  name: c.name,
1263
1350
  type: "repo",
1264
- path: import_node_path6.default.resolve(root, c.path),
1351
+ path: import_node_path7.default.resolve(root, c.path),
1265
1352
  createdAt
1266
1353
  }));
1267
1354
  }
@@ -1271,11 +1358,11 @@ async function readSiteConfig(dir) {
1271
1358
  async function seedProjectHome(dir) {
1272
1359
  const entries = await resolveProjectCollections(dir);
1273
1360
  if (entries.length === 0) {
1274
- throw new Error(`No collections to render from diffwiki.yaml in ${import_node_path6.default.resolve(dir)}`);
1361
+ throw new Error(`No collections to render from diffwiki.yaml in ${import_node_path7.default.resolve(dir)}`);
1275
1362
  }
1276
- const home = await import_promises8.default.mkdtemp(import_node_path6.default.join(import_node_os2.default.tmpdir(), "diffwiki-project-"));
1277
- await import_promises8.default.writeFile(
1278
- import_node_path6.default.join(home, "registry.json"),
1363
+ const home = await import_promises9.default.mkdtemp(import_node_path7.default.join(import_node_os2.default.tmpdir(), "diffwiki-project-"));
1364
+ await import_promises9.default.writeFile(
1365
+ import_node_path7.default.join(home, "registry.json"),
1279
1366
  `${JSON.stringify({ version: 1, collections: entries }, null, 2)}
1280
1367
  `,
1281
1368
  "utf8"
@@ -1287,8 +1374,8 @@ async function seedProjectHome(dir) {
1287
1374
  init_collections();
1288
1375
 
1289
1376
  // src/browse.ts
1290
- var import_promises9 = __toESM(require("fs/promises"), 1);
1291
- var import_node_path7 = __toESM(require("path"), 1);
1377
+ var import_promises10 = __toESM(require("fs/promises"), 1);
1378
+ var import_node_path8 = __toESM(require("path"), 1);
1292
1379
  init_collections();
1293
1380
  init_errors();
1294
1381
  var INDEX_RE = /^(index|readme)\.mdx?$/i;
@@ -1334,7 +1421,7 @@ function buildArticleTree(relPaths) {
1334
1421
  async function collectRelPaths(dir, rel = "") {
1335
1422
  let dirents;
1336
1423
  try {
1337
- dirents = await import_promises9.default.readdir(dir, { withFileTypes: true });
1424
+ dirents = await import_promises10.default.readdir(dir, { withFileTypes: true });
1338
1425
  } catch {
1339
1426
  return [];
1340
1427
  }
@@ -1343,7 +1430,7 @@ async function collectRelPaths(dir, rel = "") {
1343
1430
  if (d.name.startsWith(".")) continue;
1344
1431
  const childRel = rel ? `${rel}/${d.name}` : d.name;
1345
1432
  if (d.isDirectory()) {
1346
- out.push(...await collectRelPaths(import_node_path7.default.join(dir, d.name), childRel));
1433
+ out.push(...await collectRelPaths(import_node_path8.default.join(dir, d.name), childRel));
1347
1434
  } else if (/\.(mdx?)$/.test(d.name)) {
1348
1435
  out.push(childRel);
1349
1436
  }
@@ -1352,7 +1439,7 @@ async function collectRelPaths(dir, rel = "") {
1352
1439
  }
1353
1440
  async function readdirSafe(dir) {
1354
1441
  try {
1355
- return await import_promises9.default.readdir(dir);
1442
+ return await import_promises10.default.readdir(dir);
1356
1443
  } catch {
1357
1444
  return [];
1358
1445
  }
@@ -1370,7 +1457,7 @@ var CHEAP_META_BYTES = 8192;
1370
1457
  async function cheapMeta(absPath) {
1371
1458
  let handle;
1372
1459
  try {
1373
- handle = await import_promises9.default.open(absPath, "r");
1460
+ handle = await import_promises10.default.open(absPath, "r");
1374
1461
  const buf = Buffer.alloc(CHEAP_META_BYTES);
1375
1462
  const { bytesRead } = await handle.read(buf, 0, CHEAP_META_BYTES, 0);
1376
1463
  const snippet = buf.subarray(0, bytesRead).toString("utf8");
@@ -1404,10 +1491,10 @@ async function enrichTitles(nodes, collDir) {
1404
1491
  let status = node.status;
1405
1492
  let order = node.order;
1406
1493
  if (node.slug) {
1407
- const dir = import_node_path7.default.join(collDir, node.slug);
1494
+ const dir = import_node_path8.default.join(collDir, node.slug);
1408
1495
  const idx = pickIndexFile(await readdirSafe(dir));
1409
1496
  if (idx) {
1410
- const meta = await cheapMeta(import_node_path7.default.join(dir, idx));
1497
+ const meta = await cheapMeta(import_node_path8.default.join(dir, idx));
1411
1498
  title = meta.title ?? title;
1412
1499
  audience = meta.audience ?? audience;
1413
1500
  status = meta.status ?? status;
@@ -1417,7 +1504,7 @@ async function enrichTitles(nodes, collDir) {
1417
1504
  return { ...node, title, audience, status, order, children };
1418
1505
  }
1419
1506
  for (const ext of [".mdx", ".md"]) {
1420
- const meta = await cheapMeta(import_node_path7.default.join(collDir, `${node.slug}${ext}`));
1507
+ const meta = await cheapMeta(import_node_path8.default.join(collDir, `${node.slug}${ext}`));
1421
1508
  if (meta.title || meta.audience || meta.status || meta.order != null) {
1422
1509
  return {
1423
1510
  ...node,
@@ -1436,49 +1523,49 @@ async function enrichTitles(nodes, collDir) {
1436
1523
  async function collectionMeta(collDir) {
1437
1524
  const idx = pickIndexFile(await readdirSafe(collDir));
1438
1525
  if (!idx) return {};
1439
- const meta = await cheapMeta(import_node_path7.default.join(collDir, idx));
1526
+ const meta = await cheapMeta(import_node_path8.default.join(collDir, idx));
1440
1527
  return { title: meta.title, audience: meta.audience, status: meta.status };
1441
1528
  }
1442
1529
  async function listArticleTree(coll) {
1443
1530
  const entry = await findCollection(coll);
1444
1531
  if (!entry) throw new CollectionNotFoundError(coll);
1445
1532
  const rels = await collectRelPaths(entry.path);
1446
- return enrichTitles(buildArticleTree(rels), import_node_path7.default.resolve(entry.path));
1533
+ return enrichTitles(buildArticleTree(rels), import_node_path8.default.resolve(entry.path));
1447
1534
  }
1448
1535
  async function readArticle(coll, slug) {
1449
1536
  const entry = await findCollection(coll);
1450
1537
  if (!entry) throw new CollectionNotFoundError(coll);
1451
- const collDir = import_node_path7.default.resolve(entry.path);
1538
+ const collDir = import_node_path8.default.resolve(entry.path);
1452
1539
  const normalized = slug.replace(/\\/g, "/");
1453
1540
  if (normalized.split("/").some((s) => s === "..")) {
1454
1541
  throw new InvalidTargetError(slug, "path traversal detected");
1455
1542
  }
1456
1543
  const safeSlug = normalized.split("/").filter((s) => s !== "" && s !== ".").join("/");
1457
- const inJail = (p) => p === collDir || p.startsWith(collDir + import_node_path7.default.sep);
1544
+ const inJail = (p) => p === collDir || p.startsWith(collDir + import_node_path8.default.sep);
1458
1545
  let abs;
1459
1546
  let isIndex = false;
1460
1547
  for (const rel of safeSlug ? [`${safeSlug}.mdx`, `${safeSlug}.md`] : []) {
1461
- const candidate = import_node_path7.default.resolve(collDir, rel);
1548
+ const candidate = import_node_path8.default.resolve(collDir, rel);
1462
1549
  if (!inJail(candidate)) throw new InvalidTargetError(slug, "path traversal detected");
1463
1550
  try {
1464
- await import_promises9.default.access(candidate);
1551
+ await import_promises10.default.access(candidate);
1465
1552
  abs = candidate;
1466
1553
  break;
1467
1554
  } catch {
1468
1555
  }
1469
1556
  }
1470
1557
  if (!abs) {
1471
- const dir = import_node_path7.default.resolve(collDir, safeSlug);
1558
+ const dir = import_node_path8.default.resolve(collDir, safeSlug);
1472
1559
  if (inJail(dir)) {
1473
1560
  const idx = pickIndexFile(await readdirSafe(dir));
1474
1561
  if (idx) {
1475
- abs = import_node_path7.default.join(dir, idx);
1562
+ abs = import_node_path8.default.join(dir, idx);
1476
1563
  isIndex = true;
1477
1564
  }
1478
1565
  }
1479
1566
  }
1480
1567
  if (!abs) throw new ArticleNotFoundError(`${coll}/${safeSlug}`);
1481
- const parsed = parseArticle(await import_promises9.default.readFile(abs, "utf8"));
1568
+ const parsed = parseArticle(await import_promises10.default.readFile(abs, "utf8"));
1482
1569
  const title = parsed.title?.trim() || (safeSlug ? safeSlug.split("/").pop() ?? safeSlug : coll);
1483
1570
  return {
1484
1571
  coll,
@@ -1494,7 +1581,7 @@ async function readArticle(coll, slug) {
1494
1581
  }
1495
1582
 
1496
1583
  // src/render.ts
1497
- var import_node_path8 = __toESM(require("path"), 1);
1584
+ var import_node_path9 = __toESM(require("path"), 1);
1498
1585
  var import_unified = require("unified");
1499
1586
  var import_remark_parse = __toESM(require("remark-parse"), 1);
1500
1587
  var import_remark_gfm = __toESM(require("remark-gfm"), 1);
@@ -1511,7 +1598,7 @@ function isNonRelativeHref(href) {
1511
1598
  return /^[a-z][a-z0-9+.-]*:/i.test(href);
1512
1599
  }
1513
1600
  function rehypeRewriteLinks(ctx) {
1514
- const slugDir = ctx.isIndex ? ctx.slug || "." : import_node_path8.default.posix.dirname(ctx.slug || ".");
1601
+ const slugDir = ctx.isIndex ? ctx.slug || "." : import_node_path9.default.posix.dirname(ctx.slug || ".");
1515
1602
  return (tree) => {
1516
1603
  (0, import_unist_util_visit.visit)(tree, "element", (node) => {
1517
1604
  if (node.tagName !== "a") return;
@@ -1521,8 +1608,8 @@ function rehypeRewriteLinks(ctx) {
1521
1608
  const suffix = suffixMatch ? suffixMatch[0] : "";
1522
1609
  const pathPart = suffix ? raw.slice(0, raw.length - suffix.length) : raw;
1523
1610
  if (pathPart === "") return;
1524
- const joined = import_node_path8.default.posix.join(slugDir === "." ? "" : slugDir, pathPart);
1525
- const normalized = import_node_path8.default.posix.normalize(joined);
1611
+ const joined = import_node_path9.default.posix.join(slugDir === "." ? "" : slugDir, pathPart);
1612
+ const normalized = import_node_path9.default.posix.normalize(joined);
1526
1613
  const toRoute = (p) => p.replace(/\.(mdx?)$/i, "").replace(/(^|\/)(index|readme)$/i, "$1").replace(/\/+$/, "").replace(/^\/+/, "");
1527
1614
  if (normalized.startsWith("../")) {
1528
1615
  const sibling = normalized.slice(3);
@@ -1925,8 +2012,8 @@ function resolveCollectionSelection(all, sel = {}) {
1925
2012
  }
1926
2013
 
1927
2014
  // src/query.ts
1928
- var import_promises10 = __toESM(require("fs/promises"), 1);
1929
- var import_node_path9 = __toESM(require("path"), 1);
2015
+ var import_promises11 = __toESM(require("fs/promises"), 1);
2016
+ var import_node_path10 = __toESM(require("path"), 1);
1930
2017
  init_collections();
1931
2018
  init_errors();
1932
2019
  init_logger();
@@ -1987,6 +2074,12 @@ function rankDocs(term, docs) {
1987
2074
  if (total > 0) scored.push({ ...d, score: total });
1988
2075
  }
1989
2076
  scored.sort((a, b) => (b.score ?? 0) - (a.score ?? 0));
2077
+ const max = scored[0]?.score ?? 0;
2078
+ if (max > 0) {
2079
+ for (const d of scored) {
2080
+ if (typeof d.score === "number") d.score = Number((d.score / max).toFixed(4));
2081
+ }
2082
+ }
1990
2083
  return scored;
1991
2084
  }
1992
2085
 
@@ -1995,13 +2088,13 @@ var log2 = getLogger("query");
1995
2088
  async function collectMarkdownFiles(dir) {
1996
2089
  let entries;
1997
2090
  try {
1998
- entries = await import_promises10.default.readdir(dir, { withFileTypes: true });
2091
+ entries = await import_promises11.default.readdir(dir, { withFileTypes: true });
1999
2092
  } catch {
2000
2093
  return [];
2001
2094
  }
2002
2095
  const files = [];
2003
2096
  for (const entry of entries) {
2004
- const full = import_node_path9.default.join(dir, entry.name);
2097
+ const full = import_node_path10.default.join(dir, entry.name);
2005
2098
  if (entry.isDirectory()) {
2006
2099
  files.push(...await collectMarkdownFiles(full));
2007
2100
  } else if (entry.isFile() && /\.mdx?$/.test(entry.name)) {
@@ -2028,12 +2121,12 @@ async function nativeSearch(term, collections, collection, hideSystem = false) {
2028
2121
  const docs = [];
2029
2122
  for (const entry of targets) {
2030
2123
  for (const full of await collectMarkdownFiles(entry.path)) {
2031
- const fallbackTitle = import_node_path9.default.basename(full).replace(/\.mdx?$/, "");
2124
+ const fallbackTitle = import_node_path10.default.basename(full).replace(/\.mdx?$/, "");
2032
2125
  let title = fallbackTitle;
2033
2126
  let tags = [];
2034
2127
  let body = "";
2035
2128
  try {
2036
- const parsed = parseArticle(await import_promises10.default.readFile(full, "utf8"));
2129
+ const parsed = parseArticle(await import_promises11.default.readFile(full, "utf8"));
2037
2130
  if (hideSystem && parsed.audience === "system") continue;
2038
2131
  if (parsed.title) title = parsed.title;
2039
2132
  tags = parsed.tags;
@@ -2091,7 +2184,7 @@ async function dropSystemHits(hits) {
2091
2184
  const kept = [];
2092
2185
  for (const hit of hits) {
2093
2186
  try {
2094
- if (parseArticle(await import_promises10.default.readFile(hit.path, "utf8")).audience === "system") continue;
2187
+ if (parseArticle(await import_promises11.default.readFile(hit.path, "utf8")).audience === "system") continue;
2095
2188
  } catch {
2096
2189
  }
2097
2190
  kept.push(hit);
@@ -2107,6 +2200,7 @@ async function search(term, opts) {
2107
2200
  const showSystem = opts?.showSystem ?? await showSystemEnabled();
2108
2201
  const hideSystem = !showSystem && wanted.length === 0;
2109
2202
  const searchable = hideSystem ? collections.filter((c) => c.audience !== "system") : collections;
2203
+ const systemCollections = new Set(collections.filter((c) => c.audience === "system").map((c) => c.name));
2110
2204
  const filterHits = (hits) => wanted.length > 0 ? hits.filter((h) => wanted.includes(h.collection)) : hits;
2111
2205
  const engine = opts?.engine ?? await resolveDefaultEngine();
2112
2206
  log2.debug(`resolved search engine "${engine}"`);
@@ -2124,7 +2218,7 @@ async function search(term, opts) {
2124
2218
  try {
2125
2219
  const providerOpts = { ...opts, collection: wanted.length === 1 ? wanted[0] : void 0 };
2126
2220
  const raw = filterHits(await provider.handleSearch(term, searchable, providerOpts));
2127
- const hits = hideSystem ? await dropSystemHits(raw) : raw;
2221
+ const hits = hideSystem ? await dropSystemHits(raw.filter((h) => !systemCollections.has(h.collection))) : raw;
2128
2222
  return { hits, engine, fellBackToNative: false };
2129
2223
  } catch (err) {
2130
2224
  const message = err instanceof Error ? err.message : String(err);
@@ -2138,14 +2232,14 @@ async function query(term, opts) {
2138
2232
  }
2139
2233
 
2140
2234
  // src/search.ts
2141
- var import_promises11 = __toESM(require("fs/promises"), 1);
2142
- var import_node_path10 = __toESM(require("path"), 1);
2235
+ var import_promises12 = __toESM(require("fs/promises"), 1);
2236
+ var import_node_path11 = __toESM(require("path"), 1);
2143
2237
  init_collections();
2144
2238
  var isStatic2 = () => process.env.DIFFWIKI_STATIC === "1";
2145
2239
  async function readArticleParts(collDir, slug) {
2146
2240
  for (const ext of [".mdx", ".md"]) {
2147
2241
  try {
2148
- const parsed = parseArticle(await import_promises11.default.readFile(import_node_path10.default.join(collDir, `${slug}${ext}`), "utf8"));
2242
+ const parsed = parseArticle(await import_promises12.default.readFile(import_node_path11.default.join(collDir, `${slug}${ext}`), "utf8"));
2149
2243
  return { tags: parsed.tags, body: parsed.body };
2150
2244
  } catch {
2151
2245
  }
@@ -2181,7 +2275,7 @@ async function buildSearchIndex(collection) {
2181
2275
  for (const e of entries) {
2182
2276
  try {
2183
2277
  const collEntry = await findCollection(e.name);
2184
- const collDir = import_node_path10.default.resolve(collEntry?.path ?? e.path);
2278
+ const collDir = import_node_path11.default.resolve(collEntry?.path ?? e.path);
2185
2279
  if (collEntry?.audience === "system") continue;
2186
2280
  if (isStatic2()) {
2187
2281
  const cm = await collectionMeta(collDir);
@@ -2195,15 +2289,15 @@ async function buildSearchIndex(collection) {
2195
2289
  }
2196
2290
 
2197
2291
  // src/doctor.ts
2198
- var import_promises13 = __toESM(require("fs/promises"), 1);
2292
+ var import_promises14 = __toESM(require("fs/promises"), 1);
2199
2293
  init_paths();
2200
2294
  init_registry();
2201
2295
  init_collections();
2202
2296
 
2203
2297
  // src/plugins/manager.ts
2204
- var import_promises12 = __toESM(require("fs/promises"), 1);
2298
+ var import_promises13 = __toESM(require("fs/promises"), 1);
2205
2299
  var import_node_os3 = __toESM(require("os"), 1);
2206
- var import_node_path12 = __toESM(require("path"), 1);
2300
+ var import_node_path13 = __toESM(require("path"), 1);
2207
2301
  init_paths();
2208
2302
  init_errors();
2209
2303
  init_collections();
@@ -2214,7 +2308,7 @@ init_host();
2214
2308
  var import_node_child_process4 = require("child_process");
2215
2309
  var import_node_util3 = require("util");
2216
2310
  var import_node_fs = require("fs");
2217
- var import_node_path11 = require("path");
2311
+ var import_node_path12 = require("path");
2218
2312
  var import_node_url = require("url");
2219
2313
  var execFileAsync3 = (0, import_node_util3.promisify)(import_node_child_process4.execFile);
2220
2314
  async function runProcess(file, args, opts) {
@@ -2230,7 +2324,7 @@ async function runProcess(file, args, opts) {
2230
2324
  // src/plugins/manager.ts
2231
2325
  async function readPackageJson(file) {
2232
2326
  try {
2233
- return JSON.parse(await import_promises12.default.readFile(file, "utf8"));
2327
+ return JSON.parse(await import_promises13.default.readFile(file, "utf8"));
2234
2328
  } catch {
2235
2329
  return null;
2236
2330
  }
@@ -2246,7 +2340,7 @@ function binOf(manifest) {
2246
2340
  async function pluginManifestDirs(nodeModules) {
2247
2341
  let entries;
2248
2342
  try {
2249
- entries = await import_promises12.default.readdir(nodeModules);
2343
+ entries = await import_promises13.default.readdir(nodeModules);
2250
2344
  } catch {
2251
2345
  return [];
2252
2346
  }
@@ -2256,44 +2350,44 @@ async function pluginManifestDirs(nodeModules) {
2256
2350
  if (entry.startsWith("@")) {
2257
2351
  let scoped;
2258
2352
  try {
2259
- scoped = await import_promises12.default.readdir(import_node_path12.default.join(nodeModules, entry));
2353
+ scoped = await import_promises13.default.readdir(import_node_path13.default.join(nodeModules, entry));
2260
2354
  } catch {
2261
2355
  continue;
2262
2356
  }
2263
2357
  for (const pkg of scoped) {
2264
2358
  if (pkg.startsWith(".")) continue;
2265
- dirs.push(import_node_path12.default.join(nodeModules, entry, pkg));
2359
+ dirs.push(import_node_path13.default.join(nodeModules, entry, pkg));
2266
2360
  }
2267
2361
  } else {
2268
- dirs.push(import_node_path12.default.join(nodeModules, entry));
2362
+ dirs.push(import_node_path13.default.join(nodeModules, entry));
2269
2363
  }
2270
2364
  }
2271
2365
  return dirs;
2272
2366
  }
2273
2367
  async function ensurePluginRoot() {
2274
2368
  const dir = pluginsDir();
2275
- await import_promises12.default.mkdir(dir, { recursive: true });
2276
- const pkgPath = import_node_path12.default.join(dir, "package.json");
2369
+ await import_promises13.default.mkdir(dir, { recursive: true });
2370
+ const pkgPath = import_node_path13.default.join(dir, "package.json");
2277
2371
  if (await readPackageJson(pkgPath) === null) {
2278
2372
  const pkg = { name: "diffwiki-plugins", private: true, version: "0.0.0" };
2279
- await import_promises12.default.writeFile(pkgPath, `${JSON.stringify(pkg, null, 2)}
2373
+ await import_promises13.default.writeFile(pkgPath, `${JSON.stringify(pkg, null, 2)}
2280
2374
  `, "utf8");
2281
2375
  }
2282
2376
  return dir;
2283
2377
  }
2284
2378
  async function asSearchPackage(pkgDir) {
2285
- const manifest = await readPackageJson(import_node_path12.default.join(pkgDir, "package.json"));
2379
+ const manifest = await readPackageJson(import_node_path13.default.join(pkgDir, "package.json"));
2286
2380
  if (!manifest?.name || manifest.diffwiki?.kind !== "search") return null;
2287
2381
  const bin = binOf(manifest);
2288
2382
  if (!bin) return null;
2289
- return { name: manifest.name, version: manifest.version, binAbs: import_node_path12.default.resolve(pkgDir, bin) };
2383
+ return { name: manifest.name, version: manifest.version, binAbs: import_node_path13.default.resolve(pkgDir, bin) };
2290
2384
  }
2291
2385
  async function packageNameFromSpec(spec) {
2292
2386
  const looksLikePath = spec.startsWith(".") || spec.startsWith("/") || spec.startsWith("~") || spec.startsWith("file:");
2293
2387
  if (looksLikePath) {
2294
2388
  const raw = spec.startsWith("file:") ? spec.slice("file:".length) : spec;
2295
- const abs = import_node_path12.default.resolve(raw.replace(/^~(?=$|\/)/, import_node_os3.default.homedir()));
2296
- return (await readPackageJson(import_node_path12.default.join(abs, "package.json")))?.name ?? null;
2389
+ const abs = import_node_path13.default.resolve(raw.replace(/^~(?=$|\/)/, import_node_os3.default.homedir()));
2390
+ return (await readPackageJson(import_node_path13.default.join(abs, "package.json")))?.name ?? null;
2297
2391
  }
2298
2392
  if (/^(git\+|git:|https?:|ssh:)/.test(spec)) return null;
2299
2393
  if (spec.startsWith("@")) {
@@ -2304,10 +2398,10 @@ async function packageNameFromSpec(spec) {
2304
2398
  return at <= 0 ? spec : spec.slice(0, at);
2305
2399
  }
2306
2400
  async function resolveSearchPackage(dir, name) {
2307
- return asSearchPackage(import_node_path12.default.join(dir, "node_modules", ...name.split("/")));
2401
+ return asSearchPackage(import_node_path13.default.join(dir, "node_modules", ...name.split("/")));
2308
2402
  }
2309
2403
  async function discoverSearchPackage(dir) {
2310
- const nodeModules = import_node_path12.default.join(dir, "node_modules");
2404
+ const nodeModules = import_node_path13.default.join(dir, "node_modules");
2311
2405
  for (const pkgDir of await pluginManifestDirs(nodeModules)) {
2312
2406
  const found = await asSearchPackage(pkgDir);
2313
2407
  if (found) return found;
@@ -2404,6 +2498,14 @@ async function registerPlugin(name, command) {
2404
2498
  await runSetup(command, name);
2405
2499
  return record;
2406
2500
  }
2501
+ async function repairPlugins() {
2502
+ const enabled = (await readPluginRegistry()).plugins.filter((p) => p.enabled && p.kind === "search");
2503
+ const out = [];
2504
+ for (const p of enabled) {
2505
+ out.push({ name: p.name, setup: await runSetup(p.command, p.name) });
2506
+ }
2507
+ return out;
2508
+ }
2407
2509
  async function removePlugin(name) {
2408
2510
  const record = (await readPluginRegistry()).plugins.find((p) => p.name === name);
2409
2511
  if (!record) throw new PluginError(`plugin "${name}" is not registered`);
@@ -2439,7 +2541,7 @@ init_host();
2439
2541
  init_types();
2440
2542
  async function exists(target) {
2441
2543
  try {
2442
- await import_promises13.default.access(target);
2544
+ await import_promises14.default.access(target);
2443
2545
  return true;
2444
2546
  } catch {
2445
2547
  return false;
@@ -2545,17 +2647,16 @@ async function appendPluginDiagnostics(out, config) {
2545
2647
  }
2546
2648
 
2547
2649
  // src/lint.ts
2548
- var import_promises14 = __toESM(require("fs/promises"), 1);
2549
- var import_node_path13 = __toESM(require("path"), 1);
2650
+ var import_promises15 = __toESM(require("fs/promises"), 1);
2651
+ var import_node_path14 = __toESM(require("path"), 1);
2550
2652
  init_collections();
2551
2653
  init_errors();
2552
2654
  var BUDGETS = {
2553
2655
  recipe: { min: 300, max: 1e3 },
2554
2656
  "recipe-steps": { min: 500, max: 1500 },
2555
2657
  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.
2658
+ reference: { min: 300, max: 2e3 },
2659
+ // Generic fallback budget for a bare `architecture` tag (no dedicated template).
2559
2660
  architecture: { min: 1e3, max: 6e3 }
2560
2661
  };
2561
2662
  function estimateTokens(body) {
@@ -2564,8 +2665,7 @@ function estimateTokens(body) {
2564
2665
  function inferShape(tags, body) {
2565
2666
  const t = new Set(tags.map((x) => x.toLowerCase()));
2566
2667
  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";
2668
+ if (t.has("reference")) return "reference";
2569
2669
  if (t.has("architecture")) return "architecture";
2570
2670
  if (/^###\s+Step\b/im.test(body)) return "recipe-steps";
2571
2671
  return "recipe";
@@ -2636,13 +2736,13 @@ function lintArticle(article) {
2636
2736
  async function walkMarkdown(dir) {
2637
2737
  let entries;
2638
2738
  try {
2639
- entries = await import_promises14.default.readdir(dir, { withFileTypes: true });
2739
+ entries = await import_promises15.default.readdir(dir, { withFileTypes: true });
2640
2740
  } catch {
2641
2741
  return [];
2642
2742
  }
2643
2743
  const out = [];
2644
2744
  for (const e of entries) {
2645
- const full = import_node_path13.default.join(dir, e.name);
2745
+ const full = import_node_path14.default.join(dir, e.name);
2646
2746
  if (e.isDirectory()) out.push(...await walkMarkdown(full));
2647
2747
  else if (e.isFile() && /\.mdx?$/.test(e.name) && !/-template\.mdx?$/.test(e.name)) out.push(full);
2648
2748
  }
@@ -2672,11 +2772,11 @@ async function lintCollections(scope) {
2672
2772
  let warnCount = 0;
2673
2773
  for (const coll of collections) {
2674
2774
  for (const full of await walkMarkdown(coll.path)) {
2675
- const rel = import_node_path13.default.relative(coll.path, full);
2775
+ const rel = import_node_path14.default.relative(coll.path, full);
2676
2776
  const label = `${coll.name}/${rel}`;
2677
2777
  let findings = [];
2678
2778
  try {
2679
- const parsed = parseArticle(await import_promises14.default.readFile(full, "utf8"));
2779
+ const parsed = parseArticle(await import_promises15.default.readFile(full, "utf8"));
2680
2780
  findings = lintArticle(parsed);
2681
2781
  } catch (err) {
2682
2782
  findings = [{ level: "error", rule: "parse", message: `could not parse: ${err.message}` }];
@@ -2803,11 +2903,14 @@ init_registry2();
2803
2903
  listEnabledPlugins,
2804
2904
  listPlugins,
2805
2905
  listSearchModes,
2906
+ listTemplateShapes,
2907
+ listTemplates,
2806
2908
  loadSearchProvider,
2807
2909
  localGraph,
2808
2910
  nativeSearch,
2809
2911
  parseAddTarget,
2810
2912
  parseArticle,
2913
+ parseFrontmatter,
2811
2914
  parsePathTarget,
2812
2915
  pluginsDir,
2813
2916
  pluginsRegistryPath,
@@ -2829,6 +2932,7 @@ init_registry2();
2829
2932
  removePlugin,
2830
2933
  removeTags,
2831
2934
  renderArticle,
2935
+ repairPlugins,
2832
2936
  repoId,
2833
2937
  repoRemoteName,
2834
2938
  resolveCollectionSelection,
@@ -2842,13 +2946,17 @@ init_registry2();
2842
2946
  resolveProjectCollections,
2843
2947
  search,
2844
2948
  seedProjectHome,
2949
+ seedTemplates,
2845
2950
  serializeArticle,
2846
2951
  setArticleMeta,
2847
2952
  setConfigValue,
2953
+ setContextCollections,
2848
2954
  setTags,
2849
2955
  showSystemEnabled,
2850
2956
  slugify,
2851
2957
  slugifyPath,
2958
+ stripFrontmatter,
2959
+ templatesDir,
2852
2960
  toTreeNodes,
2853
2961
  tokenize,
2854
2962
  unregisterCollection,