@heroiclands/package-build 8.1.0 → 10.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 (122) hide show
  1. package/CHANGELOG.md +748 -0
  2. package/CONTENT.md +273 -13
  3. package/bin/content-build.mjs +479 -123
  4. package/bin/package-build.mjs +27 -69
  5. package/bin/report.mjs +1 -2
  6. package/bundle.mjs +2 -10
  7. package/config.mjs +31 -106
  8. package/container.mjs +13 -57
  9. package/content-config.mjs +300 -188
  10. package/coverage.mjs +14 -55
  11. package/deploy.mjs +4 -13
  12. package/docs/content-format.md +1418 -0
  13. package/e2e.mjs +16 -55
  14. package/engine/address-charset.mjs +62 -0
  15. package/engine/address-diff.mjs +1 -4
  16. package/engine/alias-index.mjs +153 -0
  17. package/engine/base-compiler.mjs +203 -31
  18. package/engine/code-fences.mjs +4 -13
  19. package/engine/compendiums.mjs +13 -37
  20. package/engine/content-address.mjs +6 -10
  21. package/engine/content-format-check.mjs +570 -0
  22. package/engine/content-format.mjs +253 -0
  23. package/engine/content-links.mjs +144 -99
  24. package/engine/content-lint.mjs +12 -16
  25. package/engine/content-slug.mjs +2 -6
  26. package/engine/content-tables.mjs +26 -79
  27. package/engine/diagnostics.mjs +37 -15
  28. package/engine/document-subtypes.mjs +440 -0
  29. package/engine/field-reference.mjs +6 -20
  30. package/engine/field-spec.mjs +49 -45
  31. package/engine/foreign-catalog.mjs +7 -22
  32. package/engine/foreign-manifests.mjs +1 -4
  33. package/engine/frontmatter-lint.mjs +347 -43
  34. package/engine/frontmatter.mjs +3 -8
  35. package/engine/generate.mjs +36 -20
  36. package/engine/helpers.mjs +54 -81
  37. package/engine/homepage.mjs +4 -15
  38. package/engine/ids.mjs +21 -12
  39. package/engine/index.mjs +15 -0
  40. package/engine/item-registry.mjs +72 -9
  41. package/engine/journals.mjs +4 -14
  42. package/engine/kb-manifest.mjs +41 -24
  43. package/engine/macros.mjs +2 -10
  44. package/engine/manifest-emit.mjs +6 -17
  45. package/engine/map-notes.mjs +53 -87
  46. package/engine/note-claims.mjs +383 -0
  47. package/engine/note-package.mjs +1 -4
  48. package/engine/note-vocabulary.mjs +678 -0
  49. package/engine/pack-config.mjs +56 -60
  50. package/engine/pack-router.mjs +18 -8
  51. package/engine/prose-config.mjs +20 -4
  52. package/engine/prose-lint.mjs +61 -17
  53. package/engine/region-events.mjs +1 -3
  54. package/engine/retired-fields.mjs +117 -3
  55. package/engine/scene-levels.mjs +8 -22
  56. package/engine/scenes.mjs +31 -47
  57. package/engine/schema-check.mjs +348 -7
  58. package/engine/schema-extract.mjs +11 -39
  59. package/engine/site-build.mjs +13 -38
  60. package/engine/site-index.mjs +40 -35
  61. package/engine/system-block.mjs +513 -0
  62. package/engine/web-wikilinks.mjs +115 -92
  63. package/engine/wikilink-syntax.mjs +30 -0
  64. package/engine/wikilinks.mjs +147 -183
  65. package/index.mjs +1 -5
  66. package/lang.mjs +1 -3
  67. package/manifest.mjs +10 -37
  68. package/markdownlint-config.mjs +1 -5
  69. package/package.json +6 -2
  70. package/sohl/actors.mjs +251 -68
  71. package/sohl/being-info.mjs +3 -6
  72. package/sohl/document-subtypes.mjs +82 -0
  73. package/sohl/index.mjs +4 -6
  74. package/sohl/item-builders.mjs +1 -3
  75. package/sohl/item-fields.mjs +16 -34
  76. package/sohl/items.mjs +111 -17
  77. package/sohl/kb-passes.mjs +29 -39
  78. package/sohl/note-schemas.mjs +11 -7
  79. package/sohl/skill-base.mjs +7 -23
  80. package/stage.mjs +3 -13
  81. package/templates.mjs +4 -15
  82. package/types/bundle.d.mts +1 -1
  83. package/types/container.d.mts +2 -2
  84. package/types/content-config.d.mts +48 -4
  85. package/types/coverage.d.mts +1 -1
  86. package/types/e2e.d.mts +4 -4
  87. package/types/engine/address-charset.d.mts +45 -0
  88. package/types/engine/alias-index.d.mts +122 -0
  89. package/types/engine/base-compiler.d.mts +132 -4
  90. package/types/engine/content-address.d.mts +2 -2
  91. package/types/engine/content-format-check.d.mts +163 -0
  92. package/types/engine/content-format.d.mts +101 -0
  93. package/types/engine/content-links.d.mts +16 -1
  94. package/types/engine/content-lint.d.mts +6 -0
  95. package/types/engine/diagnostics.d.mts +29 -0
  96. package/types/engine/document-subtypes.d.mts +233 -0
  97. package/types/engine/field-spec.d.mts +76 -23
  98. package/types/engine/frontmatter-lint.d.mts +47 -2
  99. package/types/engine/generate.d.mts +15 -2
  100. package/types/engine/helpers.d.mts +22 -14
  101. package/types/engine/ids.d.mts +10 -0
  102. package/types/engine/index.d.mts +5 -0
  103. package/types/engine/item-registry.d.mts +21 -2
  104. package/types/engine/kb-manifest.d.mts +35 -8
  105. package/types/engine/map-notes.d.mts +21 -11
  106. package/types/engine/note-claims.d.mts +113 -0
  107. package/types/engine/note-vocabulary.d.mts +251 -0
  108. package/types/engine/pack-config.d.mts +4 -3
  109. package/types/engine/pack-router.d.mts +4 -4
  110. package/types/engine/prose-lint.d.mts +6 -2
  111. package/types/engine/retired-fields.d.mts +73 -2
  112. package/types/engine/schema-check.d.mts +182 -0
  113. package/types/engine/schema-extract.d.mts +1 -1
  114. package/types/engine/site-index.d.mts +1 -1
  115. package/types/engine/system-block.d.mts +281 -0
  116. package/types/engine/web-wikilinks.d.mts +23 -12
  117. package/types/engine/wikilink-syntax.d.mts +29 -0
  118. package/types/manifest.d.mts +1 -1
  119. package/types/sohl/actors.d.mts +62 -6
  120. package/types/sohl/document-subtypes.d.mts +14 -0
  121. package/types/sohl/index.d.mts +1 -0
  122. package/types/sohl/items.d.mts +21 -0
@@ -17,10 +17,15 @@
17
17
  * The same authored links the pack compilers turn into Foundry `@UUID` enrichers
18
18
  * (see `./wikilinks.mjs`) become site-local hrefs here:
19
19
  *
20
- * `[[type/shortcode|Text]]` → `[Text](/section/slug/)`
20
+ * `[[type-shortcode|Text]]` → `[Text](/section/slug/)`
21
+ * `[[type-shortcode|]]` → the same, showing the target's own name
21
22
  * `[[Text]]` → the same, via a type-scoped alias
22
- * `[[type/shortcode#slug|Text]]` → `[Text](/section/slug/#slug)`
23
- * `[[#slug|Text]]` → `[Text](#slug)`
23
+ * `[[type-shortcode#slug|Text]]` → `[Text](/section/slug/#slug)`
24
+ * `[[#slug|Text]]` → `[Text](#slug)`
25
+ *
26
+ * **The pipe decides which namespace a target belongs to** (#131), with no
27
+ * fallback either way — see {@link resolvesAsAddress}, which states the rule
28
+ * for this build and the pack build together.
24
29
  *
25
30
  * The KB *section* is not always the type: prose pages (`type: doc`) route by
26
31
  * their `category`, so `doc/quickstart` lands on `/user-guide/sohl-quickstart/`.
@@ -32,12 +37,21 @@
32
37
  * exactly the drift one rule with two implementations produces (#20).
33
38
  */
34
39
 
35
- // Whether a target is an *address* rather than prose is read with the pack
36
- // build's own rule, so the two builds cannot drift apart on it: they disagreed
37
- // once over the unlabelled hyphen form, which the packs showed as a raw
38
- // shortcode and the knowledgebase as a name (#1409).
40
+ // How an address *parses* is the pack build's own rule, so the two builds
41
+ // cannot drift apart on it: they disagreed once over the unlabelled hyphen
42
+ // form, which the packs showed as a raw shortcode and the knowledgebase as a
43
+ // name (#1409).
39
44
  import { readQualifier } from "./wikilinks.mjs";
40
45
  import { replaceOutsideCode } from "./code-fences.mjs";
46
+ // The canonical `package-type-shortcode` key, so a package-qualified address
47
+ // is looked up the way a vendored manifest publishes it.
48
+ import { canonicalKey } from "./kb-manifest.mjs";
49
+ // The alias half of the two namespaces — the key rule, shared with the pack
50
+ // build and the link checker (#131).
51
+ import { aliasKey } from "./alias-index.mjs";
52
+ // Which namespace a target belongs to. The pipe decides, and this is the one
53
+ // place that says so.
54
+ import { resolvesAsAddress } from "./wikilink-syntax.mjs";
41
55
  // One slug rule for the whole build — see `./content-slug.mjs`. This module
42
56
  // carried a copy that dropped non-ASCII letters rather than transliterating
43
57
  // them, so a link to a heading named `Kûrbúl Helm` pointed at `#k-rb-l-helm`.
@@ -46,53 +60,38 @@ import { slugify } from "./content-slug.mjs";
46
60
  // Re-exported so a site build keeps one import path for the whole of link
47
61
  // resolution: the same rule that names a page also names an anchor within it.
48
62
  export { slugify };
49
- import {
50
- authoredLabel,
51
- WIKILINK,
52
- isSamePage,
53
- parseWikilink,
54
- } from "./wikilink-syntax.mjs";
63
+ import { authoredLabel, WIKILINK, isSamePage, parseWikilink } from "./wikilink-syntax.mjs";
55
64
 
56
65
  /** KB heading/anchor slug: lowercase, non-alphanumerics to single hyphens. */
57
66
 
58
67
  /**
59
- * Whether a link target addresses a document as `type-shortcode` (or the legacy
60
- * `type/shortcode`) rather than naming it in prose.
61
- *
62
- * Delegates to the pack build's {@link readQualifier} so one rule serves both
63
- * builds. A `reason` is as much an address as a resolved qualifier is — the
64
- * target is qualified either way, it just names no known type — and the caller
65
- * only ever asks this of a target that already resolved.
66
- *
67
- * @param {string} target - The link target, anchor already removed.
68
- * @param {Set<string>} [contentTypes] - Every content type the KB build saw.
69
- * @returns {boolean} `true` when the target is an address.
70
- */
71
- function isAddress(target, contentTypes) {
72
- return readQualifier(target, contentTypes ?? new Set()) !== null;
73
- }
74
-
75
- /**
76
- * The `type/shortcode` index key a qualified target resolves to, or `null`.
68
+ * The index key a **piped** target resolves to, or `null` when it does not
69
+ * parse as an address at all.
77
70
  *
78
71
  * The KB index is keyed by the canonical `type/shortcode`, so a target written
79
- * in the hyphen separator — which is what the vault authors (#1398) — has to be
80
- * rewritten to it before lookup. Uses the same {@link readQualifier} as
81
- * {@link isAddress}, so recognising an address and resolving one can never
82
- * disagree: the first-hyphen split and the known-type condition that keeps
83
- * `[[Grukar-ahk]]` an alias are stated once, in the pack build.
72
+ * in the hyphen separator — which is what the content tree authors (#1398) —
73
+ * has to be rewritten to it before lookup. Uses the pack build's own
74
+ * {@link readQualifier}, so recognising an address and resolving one can never
75
+ * disagree: the two separators and the optional leading package segment are
76
+ * stated once, there.
84
77
  *
85
78
  * The build indexes an item note under both `skill/climb` and `docskill/climb`,
86
79
  * and `contentTypes` carries both qualifiers, so either form finds the page.
87
80
  *
88
81
  * @param {string} target - The link target, anchor already removed.
89
82
  * @param {Set<string>} [contentTypes] - Every content type the KB build saw.
90
- * @returns {string | null} The index key, or `null` when not qualified.
83
+ * @param {Set<string>} [packages] - Every package an address may name.
84
+ * @returns {string | null} The index key, or `null` when not an address.
91
85
  */
92
- function qualifiedKey(target, contentTypes) {
93
- const read = readQualifier(target, contentTypes ?? new Set());
86
+ function qualifiedKey(target, contentTypes, packages) {
87
+ const read = readQualifier(target, contentTypes ?? new Set(), packages);
94
88
  if (!read || read.reason) return null;
95
- return `${read.type}/${read.shortcode}`.toLowerCase();
89
+ // A package-qualified address keeps its package: the canonical key is what
90
+ // a vendored manifest publishes, and dropping the segment would resolve
91
+ // another package's address against this one's short key.
92
+ return read.package ?
93
+ canonicalKey(read.package, read.type, read.shortcode)
94
+ : `${read.type}/${read.shortcode}`.toLowerCase();
96
95
  }
97
96
 
98
97
  /**
@@ -208,18 +207,27 @@ function isPlainMap(value) {
208
207
  /**
209
208
  * Rewrites the wikilinks in a markdown body as KB-local markdown links.
210
209
  *
211
- * A target is looked up case-insensitively: first as an alias scoped to the
212
- * source's own **type** (`ctx.typeAlias`, keyed `type|alias`) — a note's
213
- * directory and `category` play no part — then in the KB-wide `ctx.index` (keyed
214
- * by the unambiguous `section/slug` and `type/shortcode`, plus name/filename/slug
215
- * fallbacks).
210
+ * A target is looked up case-insensitively in **one** of two namespaces, and
211
+ * the pipe chooses which (#131):
216
212
  *
217
- * An unresolved target fails the build only when it is a genuine intra-KB
218
- * problem an ambiguous alias, or a qualified `prefix/key` whose prefix is a
219
- * real KB section or content directory. Anything else is treated as an external
220
- * referenceuntil every package's manifest is present, after which any
221
- * `type-shortcode` address resolving nowhere fails too. Failures are collected
222
- * in `ctx.errors`.
213
+ * - **Unpiped** an alias scoped to the source's own **type**
214
+ * (`ctx.typeAlias`, keyed `type|alias`). A note's directory and `category`
215
+ * play no part.
216
+ * - **Piped** an address, parsed by {@link readQualifier} and looked up in
217
+ * the KB-wide `ctx.index` (the canonical `package-type-shortcode`,
218
+ * `type/shortcode`, and the site's own `section/slug`), then in the vendored
219
+ * `ctx.foreign` manifests.
220
+ *
221
+ * Neither falls back to the other, so the name/basename/slug fallbacks that
222
+ * share `ctx.index` no longer answer for an address: only a slash-qualified
223
+ * target reaches the raw key, which is what keeps `section/slug` addressable.
224
+ *
225
+ * An unresolved target fails the build when it is a genuine intra-KB problem —
226
+ * an ambiguous alias, a qualified `prefix/key` whose prefix is a real KB
227
+ * section or content directory, or a **piped** target that is not an address
228
+ * at all. Anything else is treated as an external reference — until every
229
+ * package's manifest is present, after which any address resolving nowhere
230
+ * fails too. Failures are collected in `ctx.errors`.
223
231
  *
224
232
  * Whether or not it fails the build, a target that resolves nowhere renders
225
233
  * through {@link unresolvedLink} rather than as bare prose (#1665): the author's
@@ -234,7 +242,9 @@ function isPlainMap(value) {
234
242
  *
235
243
  * @param {string} body - The markdown body.
236
244
  * @param {object} ctx - `{ index, typeAlias, collide, typeCollide, sections,
237
- * contentTypes, foreign, manifestsComplete, type, errors, src }`. `foreign`
245
+ * contentTypes, packages, foreign, manifestsComplete, type, errors, src }`.
246
+ * `packages` is every package an address may name, without which the leading
247
+ * package segment of a canonical address reads as an unknown type; `foreign`
238
248
  * is the cross-package manifest index (#1446); `manifestsComplete` says
239
249
  * whether every linkable package is accounted for. Together they decide
240
250
  * whether an unresolved address is a typo or a package merely absent.
@@ -244,7 +254,8 @@ export function resolveWebWikilinks(body, ctx) {
244
254
  // Code is verbatim: a `[[…]]` inside a code fence, an indented block or an
245
255
  // inline span is source text, not a link (#1505).
246
256
  return replaceOutsideCode(body, WIKILINK, (_m, rawInner) => {
247
- const { target, anchor, display } = parseWikilink(rawInner);
257
+ const parsed = parseWikilink(rawInner);
258
+ const { target, anchor, display } = parsed;
248
259
  // An empty label is not a label: `[[x|]]` addresses the target and
249
260
  // shows its name, so `""` falls through to the same place `null` does
250
261
  // (#113). One reading, from {@link authoredLabel}.
@@ -255,34 +266,41 @@ export function resolveWebWikilinks(body, ctx) {
255
266
  return `[${label ?? anchor}](#${slugify(anchor)})`;
256
267
  }
257
268
 
258
- const key = target.toLowerCase();
259
- const typeKey = ctx.type ? `${ctx.type}|${key}`.toLowerCase() : null;
269
+ // **The pipe chooses the namespace, with no fallback either way**
270
+ // (#131). The two used to be tried in turn, so a note *name* that
271
+ // looked like an address resolved as one and a genuine address that
272
+ // resolved nowhere silently became a name lookup.
273
+ const addressed = resolvesAsAddress(parsed);
274
+ const typeKey = ctx.type ? aliasKey(ctx.type, target) : null;
260
275
  // The canonical separator (#1398) has to be resolved, not merely
261
- // recognised. Without this the form resolved only when source and
262
- // target shared a type, by way of the seeded alias below; every
263
- // *cross-type* link written in it silently lost its href.
264
- const hyphenKey = qualifiedKey(target, ctx.contentTypes);
276
+ // recognised. `null` here means the piped target is not an address at
277
+ // all, which is now a defect rather than a reason to try the aliases.
278
+ const hyphenKey = addressed ? qualifiedKey(target, ctx.contentTypes, ctx.packages) : null;
279
+ const rawKey = target.toLowerCase();
265
280
  const hit =
266
- (typeKey ? ctx.typeAlias.get(typeKey) : undefined) ??
267
- ctx.index.get(key) ??
268
- (hyphenKey ? ctx.index.get(hyphenKey) : undefined) ??
269
- // A manifest entry carries the same `{ url, name }` shape as a
270
- // local one (#1446), so a cross-package hit needs no special case
271
- // below. Local wins: a live build is authoritative and a vendored
272
- // manifest can only be staler.
273
- (hyphenKey ? ctx.foreign?.get(hyphenKey) : undefined);
281
+ addressed ?
282
+ ((hyphenKey ? ctx.index.get(hyphenKey) : undefined) ??
283
+ // `section/slug` is the site's own address for a page, and it
284
+ // is in the same map. Admitted only when the target carries a
285
+ // slash, which is what keeps the *alias* fallbacks sharing
286
+ // that map a page's name, basename and slug — out of the
287
+ // address namespace.
288
+ (rawKey.includes("/") ? ctx.index.get(rawKey) : undefined) ??
289
+ // A manifest entry carries the same `{ url, name }` shape as a
290
+ // local one (#1446), so a cross-package hit needs no special
291
+ // case below. Local wins: a live build is authoritative and a
292
+ // vendored manifest can only be staler.
293
+ (hyphenKey ? ctx.foreign?.get(hyphenKey) : undefined))
294
+ : typeKey ? ctx.typeAlias.get(typeKey)
295
+ : undefined;
274
296
  if (hit) {
275
- // With no explicit label, a *qualified* target has no prose to show
276
- // (a shortcode is not display text), so fall back to the document's
277
- // name. A bare `[[Text]]` is already the prose the author wrote —
278
- // substituting the canonical name there would rewrite the sentence
279
- // ("worsens the [[Shock State]]" must not render as "Shock").
280
- // Both separators qualify: `type-shortcode` is the canonical form
281
- // (#1398), and a hyphen inside a note *name* ("Grukar-ahk") is not
282
- // one, which is why the rule is the packs' own (#1409).
283
- const text =
284
- label ??
285
- (isAddress(target, ctx.contentTypes) ? hit.name : target);
297
+ // An address with no label has no prose to show (a shortcode is
298
+ // not display text), so the document's **current** name stands in
299
+ // and a rename shows at every citation. A bare `[[Text]]` is
300
+ // already the prose the author wrote substituting the canonical
301
+ // name there would rewrite the sentence ("worsens the [[Shock
302
+ // State]]" must not render as "Shock").
303
+ const text = label ?? (addressed ? hit.name : target);
286
304
  // A pack-only package publishes Foundry addresses and no pages
287
305
  // (#1516), so its entries carry no `path` and resolve to no URL.
288
306
  // The address is real — this is not a typo and must not fail the
@@ -295,20 +313,12 @@ export function resolveWebWikilinks(body, ctx) {
295
313
  }
296
314
 
297
315
  const slash = target.indexOf("/");
298
- const prefix =
299
- slash === -1 ? null : target.slice(0, slash).toLowerCase();
300
- // Deliberately *not* extended to the hyphen form, which is also how a
301
- // note addresses content in a package this build does not publish
302
- // (`Rules/Bestiary.md` → `being-grkrahk`, a real note in the `thalorna`
303
- // package). Nothing in the syntax separates that from a typo,
304
- // so failing here would break the build on correct content.
305
- //
306
- // A dead address is caught instead by `lint:content-links` (#1414),
307
- // which holds the reviewed list of cross-package exceptions — and which,
308
- // unlike this build, runs as part of `npm run lint` on every change.
316
+ const prefix = slash === -1 ? null : target.slice(0, slash).toLowerCase();
317
+ // A slash-qualified target whose prefix is a real section or content
318
+ // type is definitely local, so it is a typo whatever the manifest
319
+ // situation.
309
320
  const badQualified =
310
- prefix !== null &&
311
- (ctx.sections.has(prefix) || ctx.contentTypes.has(prefix));
321
+ prefix !== null && (ctx.sections.has(prefix) || ctx.contentTypes.has(prefix));
312
322
  // The hyphen form is the canonical address (#1398) and is what the
313
323
  // authored content writes. It could not be guarded while some packages
314
324
  // were invisible here: `Rules/Bestiary.md` addresses `being-grkrahk`,
@@ -325,7 +335,17 @@ export function resolveWebWikilinks(body, ctx) {
325
335
  // someone has to remember.
326
336
  const badAddress = ctx.manifestsComplete === true && hyphenKey !== null;
327
337
 
328
- if ((typeKey && ctx.typeCollide.has(typeKey)) || ctx.collide.has(key)) {
338
+ if (addressed && hyphenKey === null && !badQualified) {
339
+ // The author wrote a pipe, so they meant an address — and this is
340
+ // not one. Distinct from a dead address, because the fix is
341
+ // different: a name has to become an address, not be corrected
342
+ // (#131).
343
+ ctx.errors.push({ file: ctx.src, target, reason: "not-an-address" });
344
+ } else if (
345
+ addressed ?
346
+ rawKey.includes("/") && ctx.collide.has(rawKey)
347
+ : Boolean(typeKey && ctx.typeCollide.has(typeKey))
348
+ ) {
329
349
  ctx.errors.push({ file: ctx.src, target, reason: "ambiguous" });
330
350
  } else if (badQualified || badAddress) {
331
351
  ctx.errors.push({
@@ -334,6 +354,9 @@ export function resolveWebWikilinks(body, ctx) {
334
354
  reason: "broken type/shortcode",
335
355
  });
336
356
  }
357
+ // An unresolved *alias* stays soft: it may be ordinary prose, or a
358
+ // worldbuilding placeholder for a note not yet written. It still
359
+ // renders marked, so a reader can see a link was intended.
337
360
  return unresolvedLink(label ?? target, target);
338
361
  });
339
362
  }
@@ -116,6 +116,36 @@ export function authoredLabel({ display }) {
116
116
  return display ? display : null;
117
117
  }
118
118
 
119
+ /**
120
+ * Which namespace a link resolves in: the **address** space, or the **alias**
121
+ * space.
122
+ *
123
+ * **The pipe decides, and nothing else does** (#131). `[[x|…]]` is an address,
124
+ * parsed by the address grammar; `[[x]]` is an alias, looked up within the
125
+ * source note's own type. Neither falls back to the other.
126
+ *
127
+ * Both resolvers used to decide by *shape* instead — try the address, fall
128
+ * back to the alias — which had three costs. An author could not say which
129
+ * they meant, so a note whose **name** looked like an address (`Grukar-ahk`)
130
+ * was read as one, and a genuine address that resolved nowhere silently became
131
+ * a name lookup and reported nothing. And a positional address grammar could
132
+ * not split a target confidently until it had first ruled out every note name
133
+ * in the corpus.
134
+ *
135
+ * An **empty** label is still a pipe: `[[x|]]` is an address that renders the
136
+ * target's *current* name, so a rename shows at every citation with no link
137
+ * edited. That is why this reads {@link ParsedWikilink.labelled} and not
138
+ * {@link authoredLabel} — the two answer different questions, and only one of
139
+ * them is about namespaces.
140
+ *
141
+ * @param {{labelled: boolean}} parsed - A parsed wikilink, or anything
142
+ * carrying its `labelled`.
143
+ * @returns {boolean} True when the target is an address.
144
+ */
145
+ export function resolvesAsAddress({ labelled }) {
146
+ return Boolean(labelled);
147
+ }
148
+
119
149
  /**
120
150
  * Whether a parsed link addresses a section of the page it is written on.
121
151
  *