@heroiclands/package-build 22.2.0 → 22.3.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.
@@ -33,13 +33,12 @@
33
33
  * a function.
34
34
  *
35
35
  * **Every gate reports; none exits.** The integrity checks a site build needs —
36
- * a wikilink authored in frontmatter, a name that yields no slug, two pages
37
- * claiming one URL, an unusable or unaddressable foreign manifest, an address
38
- * two packages both claim, a table directive that cannot be honoured, a dead
39
- * wikilink were inline `process.exit` calls in both scripts, with no test
40
- * between them. Here each returns its findings and the command decides. That is
41
- * the rule `engine/site-index.mjs` already follows, and it is the only reason
42
- * these cases can be tested at all.
36
+ * a wikilink authored in frontmatter, a name that yields no slug, an unusable
37
+ * or unaddressable foreign manifest, a table directive that cannot be
38
+ * honoured, a dead wikilink were inline `process.exit` calls in both
39
+ * scripts, with no test between them. Here each returns its findings and the
40
+ * command decides. That is the rule `engine/site-index.mjs` already follows,
41
+ * and it is the only reason these cases can be tested at all.
43
42
  *
44
43
  * @module
45
44
  */
@@ -49,7 +48,6 @@ import path from "node:path";
49
48
  import { createRequire } from "node:module";
50
49
  import matter from "gray-matter";
51
50
 
52
- import { slugify } from "./content-slug.mjs";
53
51
  import { addressSlug } from "./content-address.mjs";
54
52
  import { protectCode } from "./code-fences.mjs";
55
53
  import { expandContentTables } from "./content-tables.mjs";
@@ -81,37 +79,6 @@ import { HUGO_CONTENT } from "./site-config.mjs";
81
79
 
82
80
  const require = createRequire(import.meta.url);
83
81
 
84
- /**
85
- * Every `.md` file under `dir`, depth-first in directory order.
86
- *
87
- * Deliberately *not* {@link walkMarkdownTree}, whose stack-based walk yields a
88
- * tree in reverse. Order was load-bearing here when the address index carried
89
- * first-writer-wins fallbacks for a page's name, filename and slug — reversing
90
- * the walk silently changed which page an ambiguous name resolved to. Those
91
- * fallbacks are gone with the bare `[[Name]]` form, so this is now
92
- * ordinary reading order rather than a dependency; it is kept because a site's
93
- * emitted pages should not reorder for no reason.
94
- *
95
- * @param {string} dir - Directory to walk.
96
- * @param {readonly string[]} skip - Directory names to ignore at any depth.
97
- * @returns {string[]} Absolute paths.
98
- */
99
- export function walkSiteTree(dir, skip = []) {
100
- const out = [];
101
- if (!fs.existsSync(dir)) return out;
102
- const skipped = new Set(skip);
103
- for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
104
- const full = path.join(dir, e.name);
105
- if (e.isDirectory()) {
106
- if (skipped.has(e.name)) continue;
107
- out.push(...walkSiteTree(full, skip));
108
- } else if (e.isFile() && e.name.endsWith(".md")) {
109
- out.push(full);
110
- }
111
- }
112
- return out;
113
- }
114
-
115
82
  /**
116
83
  * The content-tree files this build publishes from, in the order it emits them.
117
84
  *
@@ -135,10 +102,6 @@ export function walkSiteTree(dir, skip = []) {
135
102
  * is a fact about the filesystem, not about the content, so it can differ
136
103
  * between two checkouts of one tree; this order cannot.
137
104
  *
138
- * `collectTreePages` is deliberately **not** converted: it walks an auxiliary
139
- * tree (`site.trees`, the developer docs), which is not the content tree and
140
- * appears in no record.
141
- *
142
105
  * @param {string} contentBase - Root of the content tree.
143
106
  * @param {object} ctx - The build context. `ctx.records` is the corpus when the
144
107
  * caller already derived it — the site build derives one and hands it to both
@@ -267,73 +230,6 @@ export function collectContentPages(contentBase, ctx) {
267
230
  return { pages, addressFindings, fmLinkFindings };
268
231
  }
269
232
 
270
- /**
271
- * An extra tree's pages — a documentation tree published alongside the content.
272
- *
273
- * These preserve their **source layout** below the section rather than being
274
- * addressed by type and slug: they are a book with chapters, and a reader
275
- * follows their paths. A `README` is its directory's landing.
276
- *
277
- * **The section is the tree's, never the note's.** `tree.section` is the
278
- * mount point a `trees` entry configures — fixed, physical, and the same
279
- * value `site-index.mjs` indexes a tree page's address under. A note's own
280
- * `subType` is a genre and reaches no address, the same contract
281
- * `packageAddress()` holds for a content page: reading it here would move a
282
- * page's URL, its file destination (`pageDestination`) and the address a
283
- * wikilink cites it by, every time an author classified it.
284
- *
285
- * @param {object} tree - `{ from, rel, section, route }`.
286
- * @param {object} ctx - `{ mount }`.
287
- * @returns {{pages: object[], fmLinkFindings: object[]}}
288
- */
289
- export function collectTreePages(tree, ctx) {
290
- const pages = [];
291
- const fmLinkFindings = [];
292
-
293
- for (const file of walkSiteTree(tree.from)) {
294
- const note = readNote(file);
295
- if (!note) continue;
296
- const { fm, body } = note;
297
-
298
- for (const hit of frontmatterWikilinks(fm)) {
299
- fmLinkFindings.push({ file, ...hit });
300
- }
301
-
302
- const rel = path.relative(tree.from, file).replace(/\\/g, "/");
303
- const base = path.basename(rel);
304
- const isReadme = base.toLowerCase() === "readme.md";
305
- const sec = tree.section;
306
- const h1 = /^#\s+(.+?)\s*$/m.exec(body);
307
- const h1Title = h1 ? h1[1].replace(/\{@link\s+[^}]*\}/g, "").trim() : null;
308
- const name = fm.name?.full ?? fm.title ?? h1Title ?? path.basename(base, ".md");
309
- const slug = fm.slug ?? slugify(path.basename(base, ".md"));
310
- const relNoExt = rel.slice(0, -3).toLowerCase();
311
- const dir = path.posix.dirname(relNoExt);
312
- pages.push({
313
- kind: "tree",
314
- tree,
315
- fm,
316
- // As above: the source file, for a located link diagnostic.
317
- file,
318
- // The H1 is stripped: the page title renders it.
319
- body: body.replace(/^\s*#\s+.*$\r?\n?/m, ""),
320
- name,
321
- slug,
322
- base,
323
- rel,
324
- sec,
325
- url:
326
- isReadme ?
327
- dir === "." ?
328
- `${ctx.mount}${sec}/`
329
- : `${ctx.mount}${sec}/${dir}/`
330
- : `${ctx.mount}${sec}/${relNoExt}/`,
331
- isReadme,
332
- });
333
- }
334
- return { pages, fmLinkFindings };
335
- }
336
-
337
233
  /**
338
234
  * The package's homepage notes — the authored page at `/<contentPackage>/`.
339
235
  *
@@ -454,7 +350,6 @@ export function siteGates(pages, findings, { config }) {
454
350
  addressErrors: findings.addressFindings ?? [],
455
351
  staleManifests: [],
456
352
  unaddressable: [],
457
- conflicts: [],
458
353
  index: null,
459
354
  foreign: null,
460
355
  manifests: null,
@@ -477,14 +372,10 @@ export function siteGates(pages, findings, { config }) {
477
372
  out.unaddressable = unaddressableForeignPackages(foreign.index);
478
373
  if (out.unaddressable.length) return out;
479
374
 
480
- const index = buildSiteIndex(pages, {
375
+ out.index = buildSiteIndex(pages, {
481
376
  foreignIndex: foreign.index,
482
377
  noIndexPackages: noContentIndexPackages(config),
483
378
  });
484
- out.conflicts = index.conflicts;
485
- if (out.conflicts.length) return out;
486
-
487
- out.index = index;
488
379
  return out;
489
380
  }
490
381
 
@@ -505,7 +396,6 @@ export function emptyGates() {
505
396
  addressErrors: [],
506
397
  staleManifests: [],
507
398
  unaddressable: [],
508
- conflicts: [],
509
399
  index: null,
510
400
  foreign: null,
511
401
  manifests: null,
@@ -519,8 +409,7 @@ export function gatesFailed(gates) {
519
409
  gates.frontmatterLinks.length ||
520
410
  gates.addressErrors.length ||
521
411
  gates.staleManifests.length ||
522
- gates.unaddressable.length ||
523
- gates.conflicts.length,
412
+ gates.unaddressable.length,
524
413
  );
525
414
  }
526
415
 
@@ -622,15 +511,8 @@ export function sectionFrontmatter(meta) {
622
511
  * self-describing and makes sweeping the field out of a content tree
623
512
  * output-preserving for a site as it already is for the packs.
624
513
  *
625
- * A **tree** page is the one that still reads `readmeSections`: a `trees` entry
626
- * keeps its source layout below a named section, so its own `README` is that
627
- * section's landing and takes the title and hero the section declares.
628
- *
629
514
  * @param {object} page - The page.
630
515
  * @param {object} options
631
- * @param {Record<string, object>} [options.readmeSections] - The sections a
632
- * published tree declares, which a tree page's own `README` is the landing
633
- * for.
634
516
  * @param {(data: object, page: object) => void} [options.decorate] - Called
635
517
  * with each page's frontmatter, for whatever a consumer's own pass adds.
636
518
  * @param {(value: unknown, type: string) => string|null} [options.artSrc] -
@@ -642,37 +524,25 @@ export function sectionFrontmatter(meta) {
642
524
  * same way.
643
525
  * @returns {object} The frontmatter to write.
644
526
  */
645
- export function pageFrontmatter(page, { readmeSections = {}, decorate, webSrc, artSrc }) {
646
- const { fm, name, slug, sec, isReadme } = page;
647
- let data;
648
- if (page.kind === "content") {
649
- data = {
650
- ...fm,
651
- // Spread after the note's own frontmatter. Guarded because
652
- // `package: undefined` is not a value YAML can carry.
653
- ...(page.pkg ? { package: page.pkg } : {}),
654
- // The address, stated site-root relative, because Hugo prefixes
655
- // the site's own base to it. `slug` is written beside it
656
- // because it is the last segment of that address and Hugo's own key
657
- // for one; it decides nothing while `url` is present, but a page
658
- // that carried only `url` would report a slug Hugo had inferred
659
- // from the filename.
660
- slug,
661
- url: `/${slug}/`,
662
- title: fm.title ?? name,
663
- kbfolder: page.folder,
664
- };
665
- if (decorate) decorate(data, page);
666
- } else {
667
- // A tree's own landing describes the *mount*, and nothing beneath it. A
668
- // nested README is a sub-section's landing, and reading the section's
669
- // entry for it would title every one of them alike and hang the section
670
- // hero on each. Its title comes from its H1, like any other page's.
671
- const isSectionRoot = path.posix.dirname(page.rel) === ".";
672
- const meta = isReadme && isSectionRoot ? readmeSections[sec] : null;
673
- data = { ...fm, title: meta?.title ?? fm.title ?? name };
674
- if (meta) Object.assign(data, sectionFrontmatter(meta));
675
- }
527
+ export function pageFrontmatter(page, { decorate, webSrc, artSrc }) {
528
+ const { fm, name, slug } = page;
529
+ const data = {
530
+ ...fm,
531
+ // Spread after the note's own frontmatter. Guarded because
532
+ // `package: undefined` is not a value YAML can carry.
533
+ ...(page.pkg ? { package: page.pkg } : {}),
534
+ // The address, stated site-root relative, because Hugo prefixes
535
+ // the site's own base to it. `slug` is written beside it
536
+ // because it is the last segment of that address and Hugo's own key
537
+ // for one; it decides nothing while `url` is present, but a page
538
+ // that carried only `url` would report a slug Hugo had inferred
539
+ // from the filename.
540
+ slug,
541
+ url: `/${slug}/`,
542
+ title: fm.title ?? name,
543
+ kbfolder: page.folder,
544
+ };
545
+ if (decorate) decorate(data, page);
676
546
  delete data.aliases;
677
547
  if (webSrc && artSrc) resolveArtFields(data, webSrc, artSrc);
678
548
  return data;
@@ -733,16 +603,11 @@ function isPlainObject(value) {
733
603
  * the same as another note's `type`, and `doc-gear.md` and `weapongear-gear.md`
734
604
  * are distinct whatever the sections.
735
605
  *
736
- * **A `trees` entry is the exception, and always was.** Those pages preserve
737
- * their source layout below a named section — they are a book with chapters,
738
- * addressed by their path — so a `README` there is still its directory's
739
- * `_index.md`.
606
+ * @param {object} page - The page.
607
+ * @returns {string} The file, relative to the mount.
740
608
  */
741
609
  export function pageDestination(page) {
742
- if (page.kind === "content") return `${page.slug}.md`;
743
- const rel =
744
- page.isReadme ? path.posix.join(path.posix.dirname(page.rel), "_index.md") : page.rel;
745
- return path.join(page.sec, rel);
610
+ return `${page.slug}.md`;
746
611
  }
747
612
 
748
613
  /**
@@ -755,10 +620,10 @@ export function pageDestination(page) {
755
620
  * authored as a fenced `dataview` block, which `protectCode` would otherwise
756
621
  * stash away before the expander saw it. Expanding first leaves an ordinary
757
622
  * markdown table to walk, with every other fence still protected.
758
- * 2. **Then, inside protection**: the consumer's `beforeLinks` pass, wikilink
759
- * resolution, and the consumer's `afterLinks` pass. A `{@link}` tag may sit
760
- * in prose a wikilink also touches, so the repository's own rewrites bracket
761
- * the shared one rather than replacing it.
623
+ * 2. **Then, inside protection**: the consumer's `beforeLinks` pass, then
624
+ * wikilink resolution. A `{@link}` tag may sit in prose a wikilink also
625
+ * touches, so the repository's own rewrite runs before the shared one
626
+ * rather than replacing it.
762
627
  *
763
628
  * @param {object[]} pages - Every page.
764
629
  * @param {object} options - Everything the render needs.
@@ -772,7 +637,6 @@ export function renderPages(pages, options) {
772
637
  foreign,
773
638
  universe,
774
639
  pass = {},
775
- readmeSections,
776
640
  decorate,
777
641
  linkable = (d) => Boolean(d.fm.shortcode),
778
642
  sqlTables,
@@ -834,11 +698,10 @@ export function renderPages(pages, options) {
834
698
  };
835
699
 
836
700
  for (const page of pages) {
837
- // The page's path in the tree an author edits: below the content root
838
- // for a content note, below the tree's own root for a `trees` page. It
839
- // is not composed as `<section>/<basename>` for a content note,
840
- // which named a directory that was never the note's.
841
- const src = page.relPath ?? page.rel ?? page.base;
701
+ // The page's path in the tree an author edits, below the content
702
+ // root. It is not composed as `<section>/<basename>`, which named a
703
+ // directory that was never the note's.
704
+ const src = page.relPath ?? page.base;
842
705
  const ctx = wikiContext(index, {
843
706
  src,
844
707
  file: page.file,
@@ -854,7 +717,6 @@ export function renderPages(pages, options) {
854
717
  let t = text;
855
718
  if (pass.beforeLinks) t = pass.beforeLinks(t, page);
856
719
  t = resolveWebWikilinks(t, ctx);
857
- if (pass.afterLinks) t = pass.afterLinks(t, page);
858
720
  // Last, so a consumer's own rewrites see the image as the note
859
721
  // wrote it rather than as a figure. Hugo is handed markdown, not a
860
722
  // rendered page, so a `{…}` directive left in the body would reach
@@ -862,26 +724,22 @@ export function renderPages(pages, options) {
862
724
  return renderImageFigures(t, webSrc);
863
725
  };
864
726
 
865
- let body = page.body;
866
- if (page.kind === "content") {
867
- const { markdown, errors } = expandContentTables(body, {
868
- docs: universe.get(page.pkg) ?? [],
869
- linkable,
870
- source: src,
871
- // Prepared before this render began DuckDB is async and this
872
- // is not. Keyed by the note's own file, absolute here as in
873
- // every other pass, so the three cannot disagree about a note.
874
- sqlTables: sqlTables?.get(page.file),
875
- self: {
876
- fm: searchableFrontmatter(page.fm, page.pkg),
877
- path: page.relPath,
878
- },
879
- });
880
- tableErrors.push(...errors);
881
- body = markdown;
882
- }
727
+ const { markdown: body, errors } = expandContentTables(page.body, {
728
+ docs: universe.get(page.pkg) ?? [],
729
+ linkable,
730
+ source: src,
731
+ // Prepared before this render began — DuckDB is async and this
732
+ // is not. Keyed by the note's own file, absolute here as in
733
+ // every other pass, so the three cannot disagree about a note.
734
+ sqlTables: sqlTables?.get(page.file),
735
+ self: {
736
+ fm: searchableFrontmatter(page.fm, page.pkg),
737
+ path: page.relPath,
738
+ },
739
+ });
740
+ tableErrors.push(...errors);
883
741
 
884
- const data = pageFrontmatter(page, { readmeSections, decorate, webSrc, artSrc });
742
+ const data = pageFrontmatter(page, { decorate, webSrc, artSrc });
885
743
  const dest = path.join(outRoot, pageDestination(page));
886
744
  fs.mkdirSync(path.dirname(dest), { recursive: true });
887
745
  fs.writeFileSync(dest, matter.stringify(protectCode(body, resolve), data));
@@ -915,9 +773,9 @@ export function renderPages(pages, options) {
915
773
  * or its own address publishes nothing. Hugo generates a section page
916
774
  * automatically only for a *top-level* content directory; below that, a
917
775
  * directory without an `_index.md` is not a section, so its URL 404s while its
918
- * children publish normally. With content pages flat, what that reaches is a
919
- * `trees` entry's directory the one thing left below the mount that a note
920
- * creates.
776
+ * children publish normally. With content pages flat, no note creates a
777
+ * directory below the mount, so this reaches only what something else
778
+ * placed there.
921
779
  *
922
780
  * **A section listing is not a page listing any more.** A layout that reads
923
781
  * `.Pages` off a section it declares here will find nothing, because no file is
@@ -1016,7 +874,7 @@ const SITE_PASSES = Object.freeze({
1016
874
  *
1017
875
  * @param {string|undefined} name - The configured name.
1018
876
  * @param {object} options - The configured options, plus `repoRoot`.
1019
- * @returns {{beforeLinks?: Function, afterLinks?: Function}} The bundle.
877
+ * @returns {{beforeLinks?: Function}} The bundle.
1020
878
  */
1021
879
  export function resolveSitePass(name, options) {
1022
880
  if (!name) return {};
@@ -1177,8 +1035,7 @@ export function buildSite({ config, sqlTables } = {}) {
1177
1035
  }
1178
1036
 
1179
1037
  const content = collectContentPages(resolved.paths.content, ctx);
1180
- const pages = [...content.pages];
1181
- const fmLinkFindings = [...content.fmLinkFindings];
1038
+ const { pages } = content;
1182
1039
 
1183
1040
  // The homepage is **indexed but not rendered**. Now that it has an
1184
1041
  // address, `[[homepage-root|Text]]` is an ordinary wikilink and has to
@@ -1197,22 +1054,7 @@ export function buildSite({ config, sqlTables } = {}) {
1197
1054
  url: `${base}${addressSlug(page.fm)}/`,
1198
1055
  }));
1199
1056
 
1200
- const trees = site.trees.map((t) => ({
1201
- ...t,
1202
- from: path.resolve(resolved.rootDir, t.from),
1203
- route: `${mount}${t.section}/`,
1204
- }));
1205
- for (const tree of trees) {
1206
- const got = collectTreePages(tree, ctx);
1207
- pages.push(...got.pages);
1208
- fmLinkFindings.push(...got.fmLinkFindings);
1209
- }
1210
-
1211
- const gates = siteGates(
1212
- [...pages, ...homepageEntries],
1213
- { ...content, fmLinkFindings },
1214
- { config: resolved },
1215
- );
1057
+ const gates = siteGates([...pages, ...homepageEntries], content, { config: resolved });
1216
1058
  if (gatesFailed(gates)) {
1217
1059
  return {
1218
1060
  gates,
@@ -1238,7 +1080,6 @@ export function buildSite({ config, sqlTables } = {}) {
1238
1080
  foreign: gates.foreign,
1239
1081
  universe: tableUniverse(pages),
1240
1082
  pass,
1241
- readmeSections: site.readmeSections,
1242
1083
  // What counts as a being is the toolchain's to say, not a consumer's.
1243
1084
  // Asking in a consumer's script is how one came to still be checking
1244
1085
  // `character` and `creature` months after they were retired, and to
@@ -65,20 +65,14 @@ import { isDraftNote } from "./note-vocabulary.mjs";
65
65
  *
66
66
  * @typedef {object} SiteEntry
67
67
  * @property {string} kind `"content"` for a note compiled from the content
68
- * tree, anything else for a page that carries no
69
- * `type`/`shortcode` (a developer doc, say). Only
70
- * content entries take part in type-scoped indexing.
68
+ * tree. Only content entries take part in
69
+ * type-scoped indexing; anything else is carried
70
+ * through unindexed.
71
71
  * @property {object} fm The note's frontmatter.
72
72
  * @property {string} name Display name.
73
73
  * @property {string} slug URL segment.
74
- * @property {string} [sec] The Hugo section a **tree** page is filed under, and
75
- * the first segment of the `<sec>/<slug>` address it
76
- * is reachable by. A content page has none: it is
77
- * addressed by `(type, shortcode)` and emitted flat.
78
74
  * @property {string} base Source file's basename, e.g. `Climbing.md`.
79
75
  * @property {string} url The page's published address.
80
- * @property {boolean} [isReadme] Whether a tree page is its directory's
81
- * landing.
82
76
  */
83
77
 
84
78
  /**
@@ -94,23 +88,18 @@ import { isDraftNote } from "./note-vocabulary.mjs";
94
88
  * from `index`.
95
89
  * @property {Set<string>} contentTypes Every type the resolver should read as
96
90
  * an address qualifier, local and foreign.
97
- * @property {Set<string>} sections Section names, lowercased.
98
91
  * @property {Map<string, {name: string, url: string, subType?: string}>} refIndex
99
92
  * `type:shortcode` → page, for callers
100
93
  * resolving embedded references (a
101
94
  * being's items, say).
102
- * @property {{key: string, package: string}[]} conflicts Addresses claimed by
103
- * more than one package. Non-empty is a
104
- * build failure; the caller reports it.
105
95
  */
106
96
 
107
97
  /**
108
98
  * Merge the packages this build does not publish into the local index.
109
99
  *
110
100
  * Every canonical key is globally unique, so a foreign manifest merges straight
111
- * in — one map, one lookup, no precedence rule. A key already present is a
112
- * genuine conflict: two packages claiming one address, which is the case the
113
- * canonical form exists to make detectable.
101
+ * in — one map, one lookup, no precedence rule. It runs before the local pass
102
+ * writes a single key, so a local page always ends up owning its own address.
114
103
  *
115
104
  * The short `type/shortcode` form is merged too, because a bare `[[doc-xyz]]`
116
105
  * carries no package and must still find a foreign note when exactly one
@@ -122,22 +111,15 @@ import { isDraftNote } from "./note-vocabulary.mjs";
122
111
  *
123
112
  * @param {Map<string, object>} index - The local index, mutated.
124
113
  * @param {Map<string, {package: string, type?: string}>} foreignIndex - Merged in.
125
- * @returns {{conflicts: {key: string, package: string}[],
126
- * ambiguous: Set<string>}} The addresses two packages both claim outright,
127
- * and the short `type/shortcode` forms two foreign packages claim — those are
128
- * left out of the index, so a resolver can say *ambiguous* rather than
129
- * *nothing answers*.
114
+ * @returns {{ambiguous: Set<string>}} The short `type/shortcode` forms two
115
+ * foreign packages claim those are left out of the index, so a resolver
116
+ * can say *ambiguous* rather than *nothing answers*.
130
117
  */
131
118
  function mergeForeign(index, foreignIndex) {
132
- const conflicts = [];
133
119
  const short = new Map();
134
120
  const ambiguous = new Set();
135
121
 
136
122
  for (const [key, value] of foreignIndex) {
137
- if (index.has(key)) {
138
- conflicts.push({ key, package: value.package });
139
- continue;
140
- }
141
123
  index.set(key, value);
142
124
 
143
125
  const parts = readCanonicalKey(key);
@@ -154,7 +136,7 @@ function mergeForeign(index, foreignIndex) {
154
136
  for (const [key, value] of short) {
155
137
  if (!index.has(key)) index.set(key, value);
156
138
  }
157
- return { conflicts, ambiguous };
139
+ return { ambiguous };
158
140
  }
159
141
 
160
142
  /**
@@ -178,7 +160,6 @@ export function buildSiteIndex(
178
160
  ) {
179
161
  const index = new Map();
180
162
  const contentTypes = new Set();
181
- const sections = new Set();
182
163
  const refIndex = new Map();
183
164
  // Every package an address may name: this build's own, plus every one a
184
165
  // vendored manifest speaks for. Without it `readQualifier` cannot see the
@@ -187,30 +168,12 @@ export function buildSiteIndex(
187
168
  const ownPackage = contentPackage();
188
169
  const packages = new Set(ownPackage ? [ownPackage] : []);
189
170
 
190
- // `section/slug` is unique by construction, and is now a **tree** page's
191
- // address: a `trees` entry keeps its source layout below a named section,
192
- // so `dev-docs/testing` is how one is cited. A content page carries no
193
- // section at all and is addressed by `(type, shortcode)` below
194
- // indexing it here as well would have written `weapongear/weapongear-dagger`,
195
- // a key no author could reasonably write.
196
- //
197
- // A page's name, filename and bare slug were indexed here too, as
198
- // collision-aware fallbacks the bare `[[Name]]` form looked up; that form is
199
- // retired and nothing consults them, so they are gone and with them the rule
200
- // that two pages of a type may not share a name.
201
- for (const e of entries) {
202
- if (typeof e.sec !== "string" || !e.sec) continue;
203
- sections.add(e.sec.toLowerCase());
204
- // `draft` rides on every key a page is addressable by, because a link
205
- // into a draft note renders marked whichever of them the author wrote.
206
- // It decides nothing about resolution: the page is indexed and
207
- // published as any other.
208
- index.set(`${e.sec}/${e.slug}`.toLowerCase(), {
209
- url: e.url,
210
- name: e.name,
211
- draft: isDraftNote(e.fm),
212
- });
213
- }
171
+ // A page is addressed by `(type, shortcode)` and nothing else. Its name,
172
+ // filename and bare slug are not keys: the bare `[[Name]]` form that
173
+ // looked them up is retired, and with it the rule that two pages of a
174
+ // type may not share a name. A page carries no section either a
175
+ // section is a listing the configuration declares, not part of any
176
+ // address.
214
177
 
215
178
  // A foreign package may use a type this build has never seen. Seeding those
216
179
  // is what lets the resolver recognise `polity-xyz` as an address at all —
@@ -226,15 +189,11 @@ export function buildSiteIndex(
226
189
  // the local packages, so a manifest should never carry one — this is what
227
190
  // makes that a belt-and-braces rather than the only thing standing between
228
191
  // a stale vendored manifest and a shadowed local page.
229
- //
230
- // The corollary is that a conflict can only be reported against the keys
231
- // that exist at this point — the addressing ones, `section/slug` and the
232
- // bare fallbacks — which is precisely the overlap worth refusing.
233
- const { conflicts, ambiguous } = mergeForeign(index, foreignIndex);
192
+ const { ambiguous } = mergeForeign(index, foreignIndex);
234
193
 
235
194
  for (const e of entries) {
236
- // A page with no type or shortcode a developer doc — is addressable
237
- // by section and name, and takes no part in type-scoped indexing.
195
+ // Only a content note is addressable; anything else is carried
196
+ // through and takes no part in type-scoped indexing.
238
197
  if (e.kind !== "content") continue;
239
198
  const type = String(e.fm.type).toLowerCase();
240
199
  contentTypes.add(type);
@@ -303,11 +262,9 @@ export function buildSiteIndex(
303
262
  index,
304
263
  ambiguous,
305
264
  contentTypes,
306
- sections,
307
265
  packages,
308
266
  noIndexPackages,
309
267
  refIndex,
310
- conflicts,
311
268
  };
312
269
  }
313
270
 
@@ -352,7 +309,6 @@ export function wikiContext(
352
309
  // a URL and a name, an asset's carries the path to a file.
353
310
  assets,
354
311
  collide: built.ambiguous,
355
- sections: built.sections,
356
312
  contentTypes: built.contentTypes,
357
313
  packages: built.packages,
358
314
  noIndexPackages: built.noIndexPackages,
@@ -312,7 +312,7 @@ function isPlainMap(value) {
312
312
  * for the website, and what the book reads its staging list out of.
313
313
  *
314
314
  * @param {string} body - The markdown body.
315
- * @param {object} ctx - `{ index, assets, collide, sections, contentTypes,
315
+ * @param {object} ctx - `{ index, assets, collide, contentTypes,
316
316
  * packages, noIndexPackages, foreign, type, errors, src, file }`.
317
317
  * `packages` is every package an address may name, without which the leading
318
318
  * package segment of a canonical address reads as an unknown type;
@@ -415,10 +415,6 @@ export function resolveWebWikilinks(body, ctx) {
415
415
  const rawKey = target.toLowerCase();
416
416
  const hit =
417
417
  lookupRead(ctx.index, read, ctx.contentPackage) ??
418
- // `section/slug` is the site's own address for a page, and it is in
419
- // the same map. Admitted only when the target carries a slash, so
420
- // a page's bare slug cannot answer for an address.
421
- (rawKey.includes("/") ? ctx.index.get(rawKey) : undefined) ??
422
418
  // A manifest entry carries the same `{ url, name }` shape as a
423
419
  // local one, so a cross-package hit needs no special case
424
420
  // below. Local wins: a live build is authoritative and a vendored
@@ -449,16 +445,6 @@ export function resolveWebWikilinks(body, ctx) {
449
445
  return hit.draft ? draftLink(link) : link;
450
446
  }
451
447
 
452
- const slash = target.indexOf("/");
453
- const prefix = slash === -1 ? null : target.slice(0, slash).toLowerCase();
454
- // A slash-qualified target whose prefix is a real KB **section** is an
455
- // address in the site's own `section/slug` space, which the lookup
456
- // above already consulted. It parses as no `type/shortcode`, but it did
457
- // address something and nothing answered — so it is unresolved, not
458
- // unaddressable. (A prefix that is a content *type* never reaches here:
459
- // it parses as an address.)
460
- const siteAddress = prefix !== null && ctx.sections.has(prefix);
461
-
462
448
  // **An address resolving nowhere is a failure, unconditionally**.
463
449
  //
464
450
  // It was gated on a manifest-completeness check — while any linkable package was
@@ -485,7 +471,7 @@ export function resolveWebWikilinks(body, ctx) {
485
471
  ctx.collide?.has(collideKey) ? "ambiguous"
486
472
  // "It parsed as an address" is a property of the parse, not of
487
473
  // a key: a partial address has no single key to be non-null.
488
- : (read && !read.reason) || siteAddress ? "unresolved"
474
+ : read && !read.reason ? "unresolved"
489
475
  : read?.reason === "unknown-type" ? "unknown-type"
490
476
  : read?.reason === "no-content-index" ? "no-content-index"
491
477
  // Every link is an address, and this is not one. Distinct from