@heroiclands/package-build 10.0.1 → 11.0.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.
Files changed (44) hide show
  1. package/CHANGELOG.md +279 -0
  2. package/CONTENT.md +218 -71
  3. package/MIGRATING.md +64 -0
  4. package/bin/content-build.mjs +59 -75
  5. package/docs/content-format.md +90 -67
  6. package/engine/base-compiler.mjs +7 -1
  7. package/engine/content-address.mjs +71 -18
  8. package/engine/content-format-check.mjs +1 -1
  9. package/engine/content-links.mjs +93 -112
  10. package/engine/content-lint.mjs +14 -10
  11. package/engine/content-slug.mjs +39 -105
  12. package/engine/diagnostics.mjs +16 -2
  13. package/engine/frontmatter-lint.mjs +26 -13
  14. package/engine/helpers.mjs +31 -68
  15. package/engine/homepage.mjs +131 -86
  16. package/engine/index.mjs +2 -5
  17. package/engine/manifest-emit.mjs +23 -4
  18. package/engine/note-vocabulary.mjs +58 -1
  19. package/engine/retired-fields.mjs +117 -6
  20. package/engine/site-build.mjs +182 -59
  21. package/engine/site-index.mjs +57 -102
  22. package/engine/web-wikilinks.mjs +183 -127
  23. package/engine/wikilink-syntax.mjs +174 -34
  24. package/engine/wikilinks.mjs +159 -117
  25. package/package.json +1 -1
  26. package/types/engine/base-compiler.d.mts +1 -1
  27. package/types/engine/content-address.d.mts +46 -14
  28. package/types/engine/content-links.d.mts +13 -17
  29. package/types/engine/content-slug.d.mts +11 -48
  30. package/types/engine/diagnostics.d.mts +14 -1
  31. package/types/engine/helpers.d.mts +4 -3
  32. package/types/engine/homepage.d.mts +96 -60
  33. package/types/engine/index.d.mts +0 -1
  34. package/types/engine/note-vocabulary.d.mts +43 -0
  35. package/types/engine/retired-fields.d.mts +78 -1
  36. package/types/engine/site-build.d.mts +70 -17
  37. package/types/engine/site-index.d.mts +19 -21
  38. package/types/engine/web-wikilinks.d.mts +29 -28
  39. package/types/engine/wikilink-syntax.d.mts +126 -40
  40. package/types/engine/wikilinks.d.mts +29 -24
  41. package/engine/abbreviations.mjs +0 -0
  42. package/engine/alias-index.mjs +0 -153
  43. package/types/engine/abbreviations.d.mts +0 -44
  44. package/types/engine/alias-index.d.mts +0 -122
@@ -2,10 +2,12 @@
2
2
  * Every `.md` file under `dir`, depth-first in directory order.
3
3
  *
4
4
  * Deliberately *not* {@link walkMarkdownTree}, whose stack-based walk yields a
5
- * tree in reverse. Order is load-bearing here and nowhere else: the address
6
- * index resolves a bare `[[Name]]` on a first-writer-wins basis, so reversing
7
- * the walk silently changes which page an ambiguous name resolves to. A pack
8
- * compile has no such dependency, which is why the two walks can differ.
5
+ * tree in reverse. Order was load-bearing here when the address index carried
6
+ * first-writer-wins fallbacks for a page's name, filename and slug reversing
7
+ * the walk silently changed which page an ambiguous name resolved to. Those
8
+ * fallbacks are gone with the bare `[[Name]]` form (#180), so this is now
9
+ * ordinary reading order rather than a dependency; it is kept because a site's
10
+ * emitted pages should not reorder for no reason.
9
11
  *
10
12
  * @param {string} dir - Directory to walk.
11
13
  * @param {readonly string[]} skip - Directory names to ignore at any depth.
@@ -16,14 +18,15 @@ export function walkSiteTree(dir: string, skip?: readonly string[]): string[];
16
18
  * The content tree's pages, and what could not be addressed.
17
19
  *
18
20
  * @param {string} contentBase - Absolute path to the content tree.
19
- * @param {object} ctx - `{ packages, contentPackage, skipDirectories, mount,
20
- * scheme }`. `contentPackage` is the package a note that declares none
21
- * belongs to.
22
- * @returns {{pages: object[], slugFindings: object[], fmLinkFindings: object[]}}
21
+ * @param {object} ctx - `{ packages, contentPackage, skipDirectories, base,
22
+ * mount, scheme }`. `contentPackage` is the package a note that declares none
23
+ * belongs to; `base` is where the package is served and `mount` is where its
24
+ * content tree sits inside it.
25
+ * @returns {{pages: object[], addressFindings: object[], fmLinkFindings: object[]}}
23
26
  */
24
27
  export function collectContentPages(contentBase: string, ctx: object): {
25
28
  pages: object[];
26
- slugFindings: object[];
29
+ addressFindings: object[];
27
30
  fmLinkFindings: object[];
28
31
  };
29
32
  /**
@@ -54,15 +57,27 @@ export function collectTreePages(tree: object, ctx: object): {
54
57
  * count is what {@link checkHomepageCount} judges (#52) — this walk reports
55
58
  * what it found, and {@link buildSite} decides whether that is one.
56
59
  *
60
+ * A homepage that declares no `shortcode` has no address (#182), and is
61
+ * reported rather than written: it is the same finding a content page's missing
62
+ * shortcode produces, and it has to be available in homepage-only mode, where
63
+ * no other gate runs.
64
+ *
65
+ * **It is still counted.** An unaddressable homepage is a homepage — dropping
66
+ * it from the list would make {@link checkHomepageCount} report a tree with one
67
+ * as having none, sending its author to write a second front page instead of a
68
+ * line of frontmatter.
69
+ *
57
70
  * @param {string} contentBase - Absolute path to the content tree.
58
71
  * @param {object} ctx - `{ skipDirectories }`.
59
- * @returns {{pages: object[]}} The homepage notes, in walk order.
72
+ * @returns {{pages: object[], addressFindings: object[]}} The homepage notes,
73
+ * in walk order, and the ones among them that could not be addressed.
60
74
  */
61
75
  export function collectHomepages(contentBase: string, ctx: object): {
62
76
  pages: object[];
77
+ addressFindings: object[];
63
78
  };
64
79
  /**
65
- * Writes each homepage at the package's own root.
80
+ * Writes each homepage at its address, below the package's own root.
66
81
  *
67
82
  * Its own writer, deliberately small. A homepage is authored markdown published
68
83
  * verbatim — no table expansion, no section landing and no link resolution — so
@@ -76,14 +91,25 @@ export function collectHomepages(contentBase: string, ctx: object): {
76
91
  * {@link auditHomepageLinks} reads the `landing:` addresses and the body's
77
92
  * markdown links, and reports a wikilink on the page rather than resolving one.
78
93
  *
94
+ * **Its destination is no longer fixed** (#182). The file is written at the
95
+ * note's address, flat at the package's site root, and the page states that
96
+ * address as its `url` — the same separation of file from URL every other page
97
+ * has. Nothing is written at `/<package>/` itself: that becomes a redirect the
98
+ * package's own repository authors, which is a routing fact rather than a page.
99
+ *
79
100
  * @param {string} outRoot - The package's site root — the configured `site.out`,
80
101
  * one level above the content mount.
81
102
  * @param {readonly object[]} pages - From {@link collectHomepages}.
82
103
  * @param {object} config - The resolved configuration, for the package name and
83
104
  * the default title.
105
+ * @param {object} [options] - Options.
106
+ * @param {string} [options.base] - Where the package is served; defaults to the
107
+ * configured `site.base`, and to `/<contentPackage>/` below that.
84
108
  * @returns {number} How many pages were written.
85
109
  */
86
- export function writeHomepages(outRoot: string, pages: readonly object[], config: object): number;
110
+ export function writeHomepages(outRoot: string, pages: readonly object[], config: object, { base }?: {
111
+ base?: string | undefined;
112
+ }): number;
87
113
  /**
88
114
  * The integrity gates a site build runs before it writes anything.
89
115
  *
@@ -93,15 +119,19 @@ export function writeHomepages(outRoot: string, pages: readonly object[], config
93
119
  *
94
120
  * - **Frontmatter wikilinks** first, because frontmatter is copied to the page
95
121
  * verbatim and a link written in one reaches the reader as literal `[[…]]`.
96
- * - **Slugs and collisions** next: a note that derives no URL, or two that
97
- * derive the same one, would silently drop or overwrite a page.
122
+ * - **Addresses** next: a note that has no address no shortcode to be
123
+ * addressed by, or no section to be filed under — would silently drop a page.
124
+ * There is no collision gate beside it: an address is `(type, shortcode)`,
125
+ * which is unique within a package by rule, so two pages cannot claim one URL
126
+ * (#181).
98
127
  * - **Foreign manifests** last, in two steps. *Unusable* is a file this build
99
128
  * cannot read; *unaddressable* is one it can read but cannot look anything up
100
129
  * in — a distinction worth keeping, because the second surfaces as a pile of
101
130
  * dead links blaming the notes that cite them rather than the file at fault.
102
131
  *
103
132
  * @param {object[]} pages - Every page, from both walks.
104
- * @param {object} findings - `{ slugFindings, fmLinkFindings }` from collection.
133
+ * @param {object} findings - `{ addressFindings, fmLinkFindings }` from
134
+ * collection.
105
135
  * @param {object} options - `{ manifestDir }`.
106
136
  * @returns {object} The gate results and, when they pass, the built index.
107
137
  */
@@ -157,12 +187,22 @@ export function sectionFrontmatter(meta: object): object;
157
187
  /**
158
188
  * The frontmatter a page publishes with.
159
189
  *
160
- * An authored `aliases` is Obsidian's a list of *names* a reader might call
190
+ * An authored `aliases` is retired (#180) and refused before a build reaches
191
+ * here, which makes this a guard rather than a working path. It was Obsidian's
192
+ * — a list of *names* a reader might call
161
193
  * the note, which is vault addressing and stays in the vault. Hugo reads
162
194
  * `aliases` as **URL redirects**, so passing them through would publish a
163
195
  * redirect stub at each name. They are dropped, and this build emits no
164
196
  * redirects of its own.
165
197
  *
198
+ * A content page states its own **`url`**, which is its address rather than its
199
+ * path (#181). Hugo would otherwise publish it where the file sits — under the
200
+ * mount, inside its section directory — and the file sits there for a reason:
201
+ * Hugo derives a page's section from its directory, which is what gives the
202
+ * section its landing page, `.CurrentSection` and its per-section layout
203
+ * lookup. So the directory stays and the address is stated, and the two are
204
+ * free to differ.
205
+ *
166
206
  * A content page carries the package the build **derived** (#65). No note
167
207
  * declares one — `package:` is retired (#56) — so the note's frontmatter alone
168
208
  * would publish a page that does not say which package it belongs to. The
@@ -178,7 +218,20 @@ export function sectionFrontmatter(meta: object): object;
178
218
  * @returns {object} The frontmatter to write.
179
219
  */
180
220
  export function pageFrontmatter(page: object, { readmeSections, decorate }: object): object;
181
- /** Where a page is written, relative to the output root. */
221
+ /**
222
+ * Where a page is written, relative to the output root.
223
+ *
224
+ * **Into its section directory, which is not where it publishes** (#181). A
225
+ * content page's URL is its address — `/<package>/<type>-<shortcode>/` — and it
226
+ * is stated in the front matter; the file still goes to `<section>/`, because
227
+ * Hugo reads a page's section from its path and nothing else. Flattening the
228
+ * tree to match the URL would take the section landings, `.CurrentSection` and
229
+ * every per-section layout with it.
230
+ *
231
+ * The filename is the address rather than the section-relative half of it, so
232
+ * two sections cannot fight over one file: a `doc` note routes by its `subType`,
233
+ * which may be spelled the same as another note's `type`.
234
+ */
182
235
  export function pageDestination(page: any): string;
183
236
  /**
184
237
  * Renders and writes every page.
@@ -27,22 +27,27 @@ export function buildSiteIndex(entries: readonly SiteEntry[], { foreignIndex }?:
27
27
  * @param {object} options - Per-page inputs.
28
28
  * @param {string} options.src - Source path of the page being resolved, for
29
29
  * diagnostics.
30
- * @param {string|null} [options.type] - The citing note's type, which scopes a
31
- * bare alias lookup.
30
+ * @param {string|null} [options.type] - The citing note's type, carried for a
31
+ * consumer's own diagnostics.
32
32
  * @param {object[]} options.errors - Collector the resolver appends to.
33
+ * @param {string} [options.file] - The page's source file, which a link
34
+ * diagnostic names. Absent, `src` stands in.
33
35
  * @param {Map<string, object>} [options.foreignIndex] - The foreign index, for
34
36
  * resolvers that distinguish a foreign hit from a local one.
35
- * @param {boolean} [options.manifestsComplete] - Whether every package this
36
- * build links into supplied a manifest. When false, a resolver may soften an
37
- * unresolved cross-package link rather than fail.
38
37
  * @returns {object} The resolver context.
38
+ *
39
+ * There is deliberately **no `manifestsComplete`**. It used to let a resolver
40
+ * soften an unresolved cross-package address while any package's manifest was
41
+ * missing; #184 retired the softening, since the pack compilers and the link
42
+ * checker never had it and one authored link must not get two verdicts. A
43
+ * caller still passing it is ignored rather than obeyed.
39
44
  */
40
- export function wikiContext(built: SiteIndex, { src, type, errors, foreignIndex, manifestsComplete }: {
45
+ export function wikiContext(built: SiteIndex, { src, file, type, errors, foreignIndex }: {
41
46
  src: string;
42
47
  type?: string | null | undefined;
43
48
  errors: object[];
49
+ file?: string | undefined;
44
50
  foreignIndex?: Map<string, object> | undefined;
45
- manifestsComplete?: boolean | undefined;
46
51
  }): object;
47
52
  /**
48
53
  * One page the site will publish, as the index needs to see it.
@@ -89,28 +94,21 @@ export type SiteEntry = {
89
94
  */
90
95
  export type SiteIndex = {
91
96
  /**
92
- * Address → page.
97
+ * Address → page. `draft` says the page
98
+ * carries the `draft` tag, which marks a
99
+ * link *into* it (#183).
93
100
  */
94
101
  index: Map<string, {
95
102
  url: string;
96
103
  name?: string;
104
+ draft?: boolean;
97
105
  }>;
98
106
  /**
99
- * Keys claimed by two pages, and so
100
- * deliberately absent from `index`.
107
+ * Short addresses claimed by two
108
+ * packages, and so deliberately absent
109
+ * from `index`.
101
110
  */
102
111
  ambiguous: Set<string>;
103
- /**
104
- * `type|alias`.
105
- */
106
- typeAlias: Map<string, {
107
- url: string;
108
- name?: string;
109
- }>;
110
- /**
111
- * Type-scoped aliases claimed twice.
112
- */
113
- typeCollide: Set<string>;
114
112
  /**
115
113
  * Every type the resolver should read as
116
114
  * an address qualifier, local and foreign.
@@ -30,33 +30,34 @@ export function frontmatterWikilinks(fm: unknown): Array<{
30
30
  /**
31
31
  * Rewrites the wikilinks in a markdown body as KB-local markdown links.
32
32
  *
33
- * A target is looked up case-insensitively in **one** of two namespaces, and
34
- * the pipe chooses which (#131):
33
+ * **Every target is an address**, parsed by {@link readQualifier} and looked up
34
+ * case-insensitively in the KB-wide `ctx.index` (the canonical
35
+ * `package-type-shortcode`, `type/shortcode`, and the site's own
36
+ * `section/slug`), then in the vendored `ctx.foreign` manifests. A link written
37
+ * without a label addresses nothing at all and is reported as such (#180) —
38
+ * there is no second namespace left for it to name.
35
39
  *
36
- * - **Unpiped** an alias scoped to the source's own **type**
37
- * (`ctx.typeAlias`, keyed `type|alias`). A note's directory and `category`
38
- * play no part.
39
- * - **Piped** — an address, parsed by {@link readQualifier} and looked up in
40
- * the KB-wide `ctx.index` (the canonical `package-type-shortcode`,
41
- * `type/shortcode`, and the site's own `section/slug`), then in the vendored
42
- * `ctx.foreign` manifests.
40
+ * Only a slash-qualified target reaches the raw key, which is what keeps
41
+ * `section/slug` addressable without a page's own slug answering for it.
43
42
  *
44
- * Neither falls back to the other, so the name/basename/slug fallbacks that
45
- * share `ctx.index` no longer answer for an address: only a slash-qualified
46
- * target reaches the raw key, which is what keeps `section/slug` addressable.
43
+ * **Every target that resolves nowhere fails the build** (#184), and is
44
+ * classified into the vocabulary all three resolvers share `unlabelled`,
45
+ * `not-an-address`, `unknown-type`, `ambiguous`, `unresolved`. Failures are
46
+ * collected in `ctx.errors`, each carrying the authored `link` and its
47
+ * `occurrence` so a caller can report the line and column it sits on.
47
48
  *
48
- * An unresolved target fails the build when it is a genuine intra-KB problem —
49
- * an ambiguous alias, a qualified `prefix/key` whose prefix is a real KB
50
- * section or content directory, or a **piped** target that is not an address
51
- * at all. Anything else is treated as an external reference until every
52
- * package's manifest is present, after which any address resolving nowhere
53
- * fails too. Failures are collected in `ctx.errors`.
49
+ * There used to be one exception: a hyphen-form address was let through while
50
+ * any linkable package had no vendored manifest, since a real cross-package
51
+ * reference and a typo look identical from here. The pack compilers and the
52
+ * link checker never made that allowance, so its only surviving effect was to
53
+ * give one authored link two verdicts. The advice moved into the message
54
+ * instead.
54
55
  *
55
- * Whether or not it fails the build, a target that resolves nowhere renders
56
- * through {@link unresolvedLink} rather than as bare prose (#1665): the author's
57
- * text is kept, marked so a reader can see a link was intended. Not failing the
58
- * build is a statement that the link *may* be legitimate prose it was never a
59
- * reason to make a dead link indistinguishable from the sentence around it.
56
+ * A target that resolves nowhere still renders through {@link unresolvedLink}
57
+ * rather than as bare prose (#1665): the author's text is kept, marked so a
58
+ * reader can see a link was intended. The marking and the failure are separate
59
+ * jobs and always were the mark is for whoever reads the page a *previous*
60
+ * build emitted, the failure is for the author of this one.
60
61
  *
61
62
  * A target that **resolved** to an entry with no page is not this case and is
62
63
  * not marked: a pack-only package (#1516) publishes Foundry addresses and no
@@ -64,13 +65,13 @@ export function frontmatterWikilinks(fm: unknown): Array<{
64
65
  * link to.
65
66
  *
66
67
  * @param {string} body - The markdown body.
67
- * @param {object} ctx - `{ index, typeAlias, collide, typeCollide, sections,
68
- * contentTypes, packages, foreign, manifestsComplete, type, errors, src }`.
68
+ * @param {object} ctx - `{ index, collide, sections, contentTypes, packages,
69
+ * foreign, type, errors, src, file }`.
69
70
  * `packages` is every package an address may name, without which the leading
70
71
  * package segment of a canonical address reads as an unknown type; `foreign`
71
- * is the cross-package manifest index (#1446); `manifestsComplete` says
72
- * whether every linkable package is accounted for. Together they decide
73
- * whether an unresolved address is a typo or a package merely absent.
72
+ * is the cross-package manifest index (#1446). `src` is the page's display
73
+ * path and `file` the source file a diagnostic should name — absent, `src`
74
+ * stands in.
74
75
  * @returns {string} The body with wikilinks rewritten.
75
76
  */
76
77
  export function resolveWebWikilinks(body: string, ctx: object): string;
@@ -4,14 +4,14 @@
4
4
  * @typedef {object} ParsedWikilink
5
5
  * @property {string} inner - The whole interior, unescaped and trimmed. What an
6
6
  * unlabelled link displays, anchor and all.
7
- * @property {string} target - What is linked to: an address, an alias, or `""`
8
- * for a link to a section of the same page.
7
+ * @property {string} target - What is linked to: an address, or `""` for a
8
+ * link to a section of the same page.
9
9
  * @property {string} anchor - The `#section` slug, `""` when there is none.
10
10
  * @property {string|null} display - The text after `|`, or `null` when the link
11
11
  * is unlabelled. `null` and `""` differ: an author may write `[[x|]]`.
12
- * @property {boolean} labelled - Whether a `|` was present at all. What an
13
- * unlabelled link shows depends on whether its target read as an address
14
- * (SoHL#1409), so the caller needs to know.
12
+ * @property {boolean} labelled - Whether a `|` was present at all. A link
13
+ * without one addresses nothing and is a finding (#180) see
14
+ * {@link unlabelledLinkMessage} — so every reader has to be able to ask.
15
15
  */
16
16
  /**
17
17
  * Split a wikilink's interior into its parts.
@@ -30,8 +30,9 @@ export function parseWikilink(rawInner: string): ParsedWikilink;
30
30
  * means "address this target, and show the target's own name" — so `display:
31
31
  * ""` has to read as *absent* everywhere a fallback is chosen, exactly as
32
32
  * `display: null` does. The two are still distinguishable through
33
- * {@link ParsedWikilink.labelled}, which is the thing that genuinely differs
34
- * and which #1409 depends on.
33
+ * {@link ParsedWikilink.labelled}, which is the thing that genuinely differs:
34
+ * `[[x|]]` is labelled and `[[x]]` is not, and only the first addresses
35
+ * anything (#180).
35
36
  *
36
37
  * Stated here because the two resolvers had already drawn the line in two
37
38
  * places and drawn it differently: the packs tested falsiness and were right,
@@ -48,34 +49,88 @@ export function authoredLabel({ display }: {
48
49
  display: string | null;
49
50
  }): string | null;
50
51
  /**
51
- * Which namespace a link resolves in: the **address** space, or the **alias**
52
- * space.
53
- *
54
- * **The pipe decides, and nothing else does** (#131). `[[x|…]]` is an address,
55
- * parsed by the address grammar; `[[x]]` is an alias, looked up within the
56
- * source note's own type. Neither falls back to the other.
57
- *
58
- * Both resolvers used to decide by *shape* instead try the address, fall
59
- * back to the aliaswhich had three costs. An author could not say which
60
- * they meant, so a note whose **name** looked like an address (`Grukar-ahk`)
61
- * was read as one, and a genuine address that resolved nowhere silently became
62
- * a name lookup and reported nothing. And a positional address grammar could
63
- * not split a target confidently until it had first ruled out every note name
64
- * in the corpus.
65
- *
66
- * An **empty** label is still a pipe: `[[x|]]` is an address that renders the
67
- * target's *current* name, so a rename shows at every citation with no link
68
- * edited. That is why this reads {@link ParsedWikilink.labelled} and not
69
- * {@link authoredLabel} — the two answer different questions, and only one of
70
- * them is about namespaces.
71
- *
72
- * @param {{labelled: boolean}} parsed - A parsed wikilink, or anything
73
- * carrying its `labelled`.
74
- * @returns {boolean} True when the target is an address.
52
+ * What an author writing an unlabelled link is told, in one place.
53
+ *
54
+ * Shared by the link checker, the pack compilers and the web resolver, because
55
+ * an author meets whichever runs first and they should read the same. It names
56
+ * the form to write rather than a value to correct: there is no value that
57
+ * makes an unlabelled link resolve.
58
+ *
59
+ * **Why there is nothing left for a bare link to mean** (#180). The pipe used
60
+ * to select between two namespaces address and alias and the alias one was
61
+ * empty in practice: across 8,305 wikilinks in three content trees, not one
62
+ * bare `[[Alias]]` resolved to a note. What the index it looked up in *did* do
63
+ * was fold every note's `name.full` into itself, which forbade two notes of a
64
+ * type from sharing a display name (#179). So the namespace is gone, every
65
+ * link is an address, and an address needs the pipe that says so.
66
+ *
67
+ * The **link part may still be an anchor**: `[[#slug|Text]]` addresses a
68
+ * section of the page it is written on. It is the label that is required, not
69
+ * a target.
70
+ *
71
+ * @param {string} target - The target as authored, named in the message.
72
+ * @returns {string} The message, unpunctuated at the end as a finding is.
75
73
  */
76
- export function resolvesAsAddress({ labelled }: {
77
- labelled: boolean;
78
- }): boolean;
74
+ export function unlabelledLinkMessage(target: string): string;
75
+ /**
76
+ * What an author writing an address that resolves to nothing is told.
77
+ *
78
+ * **Both corrections, because the author cannot tell which applies.** An
79
+ * address lands nowhere either because the shortcode is wrong or because the
80
+ * package publishing it has no manifest vendored here, and the link itself
81
+ * looks identical in the two cases.
82
+ *
83
+ * This used to be a **warning** in the checker and, in the site build, nothing
84
+ * at all until every linkable package had vendored a manifest — on the
85
+ * reasoning that a bare `[[Name]]` might be a placeholder for a note nobody had
86
+ * written yet. That reasoning was a property of the bare form, which is retired
87
+ * (#180); the intent behind it now has a real spelling, a `draft`-tagged note
88
+ * that exists and resolves and renders marked (#183). So an address naming no
89
+ * note is a typo or an omission, both want fixing, and all three builds say so.
90
+ *
91
+ * @param {string} target - The address as authored, named in the message.
92
+ * @returns {string} The message, unpunctuated at the end as a finding is.
93
+ */
94
+ export function unresolvedAddressMessage(target: string): string;
95
+ /**
96
+ * What an author writing a short address more than one package publishes is
97
+ * told.
98
+ *
99
+ * There is no defensible way to pick one, and the correction is mechanical:
100
+ * write the package-qualified form. So it fails rather than warning, and the
101
+ * message names the claimants so the author can choose between them without
102
+ * going looking.
103
+ *
104
+ * @param {string} target - The address as authored.
105
+ * @param {Iterable<string>} [packages] - The packages that publish it.
106
+ * @returns {string} The message.
107
+ */
108
+ export function ambiguousAddressMessage(target: string, packages?: Iterable<string>): string;
109
+ /**
110
+ * The message for one link finding, whichever resolver found it.
111
+ *
112
+ * The single table the checker, the pack compilers and the web resolver all
113
+ * report through, so one authored link cannot get three different explanations
114
+ * of the same mistake depending on which build the author ran first. Callers
115
+ * add their own context around it — the note's name, the file it sits in — and
116
+ * never their own wording for the defect.
117
+ *
118
+ * @param {object} finding
119
+ * @param {string} finding.reason - One of {@link LINK_FINDING_REASONS}.
120
+ * @param {string} finding.target - The link target as authored.
121
+ * @param {Iterable<string>} [finding.packages] - For `ambiguous`, the
122
+ * claimants.
123
+ * @param {string} [finding.anchor] - For `unknown-anchor`, the section named.
124
+ * @returns {string} The message.
125
+ * @throws {Error} On a reason outside the closed set — a resolver inventing one
126
+ * would otherwise report a link with no explanation at all.
127
+ */
128
+ export function linkFindingMessage({ reason, target, packages, anchor }: {
129
+ reason: string;
130
+ target: string;
131
+ packages?: Iterable<string> | undefined;
132
+ anchor?: string | undefined;
133
+ }): string;
79
134
  /**
80
135
  * Whether a parsed link addresses a section of the page it is written on.
81
136
  *
@@ -109,7 +164,18 @@ export function isSamePage({ target, anchor }: ParsedWikilink): boolean;
109
164
  * not.
110
165
  *
111
166
  * So the parse lives here, once. A resolver receives the parts and decides only
112
- * what it is actually for: which address space the target belongs to.
167
+ * what it is actually for: which document the address names.
168
+ *
169
+ * The one rule about a link's *shape* that both resolvers share also lives here
170
+ * — **a link carries a label** ({@link unlabelledLinkMessage}) — because the
171
+ * two used to state it in their own words and the author met whichever ran
172
+ * first.
173
+ *
174
+ * For the same reason, so does the **vocabulary of link findings**
175
+ * ({@link LINK_FINDING_REASONS}) and the message each one reports through
176
+ * ({@link linkFindingMessage}). Three builds read one authored link; an author
177
+ * meets whichever ran first, and a consumer switching on a `reason` should not
178
+ * be switching on which build produced it (#184).
113
179
  *
114
180
  * @module
115
181
  */
@@ -122,6 +188,26 @@ export function isSamePage({ target, anchor }: ParsedWikilink): boolean;
122
188
  * text as written, which is the safe direction for a rewriter.
123
189
  */
124
190
  export const WIKILINK: RegExp;
191
+ /**
192
+ * Every way a link can fail, named once for all three resolvers (#184).
193
+ *
194
+ * A link is read in three places — the checker (`content-links.mjs`), the pack
195
+ * compilers (`wikilinks.mjs`) and the web resolver (`web-wikilinks.mjs`) — and
196
+ * each used to name the failures in its own words. `unknown` in one was
197
+ * `unresolved` in another and `broken type/shortcode` in the third, so a
198
+ * consumer switching on a `reason` was switching on which build had produced
199
+ * it. The set is closed and lives here, beside the syntax the three share.
200
+ *
201
+ * - `unlabelled` — no `|`, so the link addresses nothing (#180).
202
+ * - `not-an-address` — labelled, but the target does not parse as an address.
203
+ * - `unknown-type` — definitely qualified, but names no type this build knows.
204
+ * - `unresolved` — parses as an address, and nothing publishes it.
205
+ * - `ambiguous` — more than one package publishes the short address.
206
+ * - `unknown-anchor` — the address resolved, the `#section` it names did not.
207
+ *
208
+ * @type {ReadonlySet<string>}
209
+ */
210
+ export const LINK_FINDING_REASONS: ReadonlySet<string>;
125
211
  /**
126
212
  * The parts of a wikilink's interior.
127
213
  */
@@ -132,8 +218,8 @@ export type ParsedWikilink = {
132
218
  */
133
219
  inner: string;
134
220
  /**
135
- * - What is linked to: an address, an alias, or `""`
136
- * for a link to a section of the same page.
221
+ * - What is linked to: an address, or `""` for a
222
+ * link to a section of the same page.
137
223
  */
138
224
  target: string;
139
225
  /**
@@ -146,9 +232,9 @@ export type ParsedWikilink = {
146
232
  */
147
233
  display: string | null;
148
234
  /**
149
- * - Whether a `|` was present at all. What an
150
- * unlabelled link shows depends on whether its target read as an address
151
- * (SoHL#1409), so the caller needs to know.
235
+ * - Whether a `|` was present at all. A link
236
+ * without one addresses nothing and is a finding (#180) see
237
+ * {@link unlabelledLinkMessage} — so every reader has to be able to ask.
152
238
  */
153
239
  labelled: boolean;
154
240
  };