@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
@@ -1,59 +1,22 @@
1
1
  /**
2
- * The URL segment for one content note.
2
+ * The URL-safe token a piece of prose reduces to.
3
3
  *
4
- * The name is **transliterated** before it is reduced, so an accented character
5
- * is carried across rather than dropped dropping is what turned `Nüsvōrroth`
6
- * into `n-sv-rroth` and forced a hand-written slug. Ligatures expand the way a
7
- * reader would spell them out: `þ`→`th`, `æ`→`ae`, `œ`→`oe`, `ß`→`ss`, `ij`→`ij`,
8
- * `fi`→`fi`, and eth (`ð`) follows the Icelandic convention of a bare `d`.
4
+ * The text is **transliterated** before it is reduced, so an accented character
5
+ * is carried across rather than dropped. Ligatures expand the way a reader would
6
+ * spell them out: `þ`→`th`, `æ`→`ae`, `œ`→`oe`, `ß`→`ss`, `ij`→`ij`, `fi`→`fi`,
7
+ * and eth (`ð`) follows the Icelandic convention of a bare `d`.
9
8
  *
10
9
  * Two reductions are ours rather than the transliterator's:
11
10
  *
12
11
  * - **apostrophes are removed**, not treated as separators (`Armorer's Kit` →
13
- * `armorers-kit`), matching the URLs these pages already publish at;
12
+ * `armorers-kit`);
14
13
  * - **a fraction keeps its digits together** — a vulgar fraction expands to
15
14
  * `3/4`, and the solidus would otherwise split it into `3-4`, so a slash
16
15
  * *between digits* is closed up (`Kûrbúl ¾-Helm` → `kurbul-34-helm`).
17
16
  *
18
- * @param {string | undefined} name - The note's display name (`name.full`),
19
- * which a malformed note may not have at all.
20
- * @returns {string} The URL segment (never empty).
21
- * @throws {Error} When there is no name, or the name carries no URL-safe
22
- * characters — either way the note cannot be addressed, which is a content
23
- * error rather than something to paper over with a fallback.
17
+ * @param {string | undefined} text - The prose to reduce.
18
+ * @returns {string} The token, or `""` when the text carries nothing URL-safe.
19
+ * Empty is an ordinary answer here: nothing is addressed by a slug any more,
20
+ * so an anchor that reduces to nothing is the caller's to judge.
24
21
  */
25
- export function slugify(text: any): string;
26
- /**
27
- * The URL segment a content note publishes at.
28
- *
29
- * {@link slugify} with the rule that a document *must* be addressable: a note
30
- * that yields no slug is a content error, not something to paper over with a
31
- * fallback, because the alternative is a page nobody can reach.
32
- *
33
- * @param {string | undefined} name - The note's display name (`name.full`),
34
- * which a malformed note may not have at all.
35
- * @returns {string} The URL segment (never empty).
36
- * @throws {Error} When there is no name, or the name carries no URL-safe
37
- * characters.
38
- */
39
- export function contentSlug(name: string | undefined): string;
40
- /**
41
- * Find pages that would publish to the same URL.
42
- *
43
- * Nothing constrains two notes in one section from sharing a name, and a
44
- * collision silently overwrites one page with the other. This turns it into a
45
- * build failure that names every claimant, so the fix is a more specific title.
46
- * (The content tree has no collisions today.)
47
- *
48
- * @param {Array<{sec: string, slug: string, src: string}>} pages
49
- * @returns {Array<{url: string, sources: string[]}>} One entry per collision, in
50
- * first-claim order; empty when every URL is unique.
51
- */
52
- export function findSlugCollisions(pages: Array<{
53
- sec: string;
54
- slug: string;
55
- src: string;
56
- }>): Array<{
57
- url: string;
58
- sources: string[];
59
- }>;
22
+ export function slugify(text: string | undefined): string;
@@ -111,16 +111,29 @@ export function positionInBody(body: string, offset: number, { bodyLine, bodyCol
111
111
  * a line of prose — sending the reader to a position that is not the problem,
112
112
  * which is the one thing the located form exists to prevent.
113
113
  *
114
+ * The key match tolerates leading whitespace by default, so a nested key of the
115
+ * same name answers when no top-level one is present — which is usually what a
116
+ * reader wants, the key being unique in nearly every note. Pass `topLevel` where
117
+ * it is not: `aliases` is both a retired top-level field and a **permitted**
118
+ * `name.aliases` (#180), and a finding about the first must never open on the
119
+ * second, which would tell an author to delete a field they are allowed to
120
+ * write.
121
+ *
114
122
  * @param {string} raw - The file's full contents, frontmatter included.
115
123
  * @param {string} key - The top-level frontmatter key.
116
124
  * @param {string} [value] - When given, prefer the occurrence whose line also
117
125
  * carries this text. A list-valued key (`aliases`) is reported at the entry
118
126
  * that is wrong, not at the key that introduces it.
127
+ * @param {object} [options] - Options.
128
+ * @param {boolean} [options.topLevel=false] - Require the key at column 1, so
129
+ * an identically named nested key cannot answer for it.
119
130
  * @returns {{line?: number, column?: number}} Spreadable position fields, empty
120
131
  * when the key cannot be located — dropped rather than guessed, as
121
132
  * {@link formatDiagnostic} requires.
122
133
  */
123
- export function positionInFrontmatter(raw: string, key: string, value?: string): {
134
+ export function positionInFrontmatter(raw: string, key: string, value?: string, { topLevel }?: {
135
+ topLevel?: boolean | undefined;
136
+ }): {
124
137
  line?: number;
125
138
  column?: number;
126
139
  };
@@ -256,11 +256,11 @@ export function defaultStats(): object;
256
256
  * @param {object} [router] - The pack router. Supplied by the calling pass so
257
257
  * the index and the compile agree about where each note landed; defaults to
258
258
  * this repository's own.
259
- * @returns {{byShortcode: Map, byAlias: Map}} From `buildWikilinkIndex`.
259
+ * @returns {{byShortcode: Map, types: Set}} From `buildWikilinkIndex`.
260
260
  */
261
261
  export function buildContentLinkIndex(contentBase: string, router?: object): {
262
262
  byShortcode: Map<any, any>;
263
- byAlias: Map<any, any>;
263
+ types: Set<any>;
264
264
  };
265
265
  /**
266
266
  * Converts the wikilinks in one note's markdown, reporting any that have no
@@ -280,7 +280,8 @@ export function buildContentLinkIndex(contentBase: string, router?: object): {
280
280
  * entry. Position is carried by `{ file, bodyLine, bodyColumn, lineMap }`,
281
281
  * the last from {@link expandNoteTables}.
282
282
  * @returns {{markdown: string, unresolved: Array<object>}}
283
- * @throws {Error} On an ambiguous alias or a dead qualified address. The error
283
+ * @throws {Error} On any link that does not resolve an unlabelled one, a
284
+ * target that is not an address, or an address nothing publishes. The error
284
285
  * carries `file` and `position`, so a caller reports it in the same form
285
286
  * rather than re-deriving one.
286
287
  */
@@ -1,3 +1,18 @@
1
+ /**
2
+ * The file a homepage is written to, relative to the package's site root.
3
+ *
4
+ * Its **address**, flat at the package root, and stated in the page's own `url`
5
+ * — the same separation every other page has since #181, where the directory
6
+ * decides the Hugo section and the front matter decides the URL. Flat rather
7
+ * than inside a `homepage/` section directory, because a homepage is not one of
8
+ * a kind: a section holding exactly one page would publish a landing at
9
+ * `/<package>/kb/homepage/` that nothing links to and nobody wrote.
10
+ *
11
+ * @param {object} fm - Parsed frontmatter.
12
+ * @returns {string} The destination filename, e.g. `homepage-root.md`.
13
+ * @throws {Error} When the note declares no shortcode, and so has no address.
14
+ */
15
+ export function homepageDestination(fm: object): string;
1
16
  /**
2
17
  * Whether a note's frontmatter declares the homepage type.
3
18
  *
@@ -6,26 +21,38 @@
6
21
  */
7
22
  export function isHomepage(fm: object | null | undefined): boolean;
8
23
  /**
9
- * The address-bearing fields one note authors, in the order it authored them.
24
+ * What the address rule says about one note's top-level fields.
10
25
  *
11
- * Authoring order rather than declaration order, so a caller emitting one
12
- * diagnostic per finding emits them top to bottom down the file the order a
13
- * reader and a compiler-output parser both expect.
26
+ * Two statements about the same thing, so they are made together: the field a
27
+ * homepage **owes** and the field it may **not** write.
14
28
  *
15
- * Presence is the whole test: `shortcode:` authored empty still says "this page
16
- * has an address of its own", and a value cannot make the claim true.
29
+ * The missing `shortcode` comes first, and is located at `type:` rather than at
30
+ * a key that is not there the `homepage` value is what makes the field
31
+ * required, it is a real position in the file, and inventing a `1:1` for an
32
+ * absent key would put the author on the opening fence. The refused fields
33
+ * follow in the order the note authored them, so a caller emitting one
34
+ * diagnostic per finding walks down the file.
17
35
  *
18
- * Returned without a locator, because the two things that would supply one
19
- * the raw note text and the position helper belong to the caller. This
20
- * mirrors {@link module:engine/retired-fields}, whose retired-field messages
21
- * are likewise positioned by whoever reports them.
36
+ * Presence is the whole test for a refused field, and absence-or-blank for the
37
+ * required one: `shortcode:` authored empty is no address, and a value cannot
38
+ * make `id` mean something on a page that compiles to no document.
39
+ *
40
+ * Each finding carries the `locator` key to position it at, because the two
41
+ * things that would resolve one — the raw note text and the position helper —
42
+ * belong to the caller. This mirrors {@link module:engine/retired-fields},
43
+ * whose retired-field messages are likewise positioned by whoever reports them.
22
44
  *
23
45
  * @param {object|null|undefined} fm - Parsed frontmatter.
24
- * @returns {Array<{key: string, message: string}>} One entry per field the note
25
- * authored, empty for any note that is not a homepage.
46
+ * @returns {Array<{field: string, locator: {key: string, literal?: string},
47
+ * message: string}>} One entry per finding, empty for any note that is not a
48
+ * homepage and declares nothing wrong.
26
49
  */
27
50
  export function checkHomepageAddressFields(fm: object | null | undefined): Array<{
28
- key: string;
51
+ field: string;
52
+ locator: {
53
+ key: string;
54
+ literal?: string;
55
+ };
29
56
  message: string;
30
57
  }>;
31
58
  /**
@@ -37,12 +64,16 @@ export function checkHomepageAddressFields(fm: object | null | undefined): Array
37
64
  * - _None_ and the package serves nothing at `/<package>/`. That is the failure
38
65
  * #50 exists to prevent, and it is silent — the site build reports `wrote 0
39
66
  * homepage(s)` and exits 0.
40
- * - _Two_ and it serves a page nobody chose. Every homepage is written to the
41
- * same {@link HOMEPAGE_DESTINATION}, so the second overwrites the first and
42
- * the package's front page is decided by the order the walk happened to reach
43
- * the files in by *filename*, on a type whose whole point is that it is
44
- * routed by frontmatter. There is no "first wins" convention to fall back on,
45
- * so nothing here can pick the right one.
67
+ * - _Two_ and it serves a page nobody chose. **This is a cardinality rule, and
68
+ * since #182 it is only that.** It used to rest on the fixed destination
69
+ * every homepage shared the second overwrote the first so the address
70
+ * rule enforced it as a side effect. A homepage is written at its own address
71
+ * now, so two of them publish two pages and collide over nothing; the
72
+ * duplicate-address check catches only the pair that happen to share a
73
+ * shortcode, and says nothing at all about a `homepage-root` beside a
74
+ * `homepage-front`. Which of the two the redirect at `/<package>/` should
75
+ * name is a question nothing here can answer, and both being reachable is
76
+ * not an answer to it.
46
77
  *
47
78
  * Neither has a safe default, so neither is a warning. A warning is the right
48
79
  * severity for something a build can proceed past correctly, and a build that
@@ -106,25 +137,36 @@ export function homepageTitle(fm: object | null | undefined, config: object): st
106
137
  /**
107
138
  * The frontmatter a homepage publishes with.
108
139
  *
109
- * The note's own, plus the two derived values every emitted page carries: the
110
- * resolved `title`, and the package the build derived — no note declares one
140
+ * The note's own, plus the derived values every emitted page carries: the
141
+ * resolved `title`, the package the build derived — no note declares one
111
142
  * (`package:` is retired, #56) and the theme's breadcrumb partial reads
112
- * `.Params.package`.
143
+ * `.Params.package` — and its **address**.
144
+ *
145
+ * The address is stated as `url` for the same reason every other page states
146
+ * one (#181): Hugo publishes a page where its file sits unless told otherwise,
147
+ * and a homepage's file sits at the package's site root. `slug` is written
148
+ * beside it because it is the last segment of that address and Hugo's own key
149
+ * for one; it decides nothing while `url` is present, but a page carrying only
150
+ * `url` would report a slug Hugo had inferred from the filename.
113
151
  *
114
152
  * An authored `aliases` is dropped for the same reason it is on every other
115
- * page: Obsidian reads it as names a reader might call the note, Hugo reads it
116
- * as URL redirects, and passing it through would publish a redirect stub at
117
- * each one.
153
+ * page: Hugo reads it as URL redirects, so passing it through would publish a
154
+ * redirect stub at each one. The field is retired (#180) and refused before a
155
+ * build reaches here, which makes this a guard rather than a working path.
118
156
  *
119
157
  * @param {object} fm - The note's frontmatter.
120
158
  * @param {object} options - Options.
121
159
  * @param {string} options.contentPackage - The package this build publishes.
122
160
  * @param {string} options.title - The resolved title.
161
+ * @param {string} options.base - Where the package is served, with both
162
+ * slashes — `/<package>/`.
123
163
  * @returns {object} The frontmatter to write.
164
+ * @throws {Error} When the note declares no shortcode, and so has no address.
124
165
  */
125
- export function homepageFrontmatter(fm: object, { contentPackage, title }: {
166
+ export function homepageFrontmatter(fm: object, { contentPackage, title, base }: {
126
167
  contentPackage: string;
127
168
  title: string;
169
+ base: string;
128
170
  }): object;
129
171
  /**
130
172
  * Every address a homepage carries, wherever it is written.
@@ -172,54 +214,48 @@ export const HOMEPAGE_TYPE: string;
172
214
  *
173
215
  * Empty on purpose, and declared rather than omitted: a type with no vocabulary
174
216
  * and a type that is unknown are different findings, and only the second is an
175
- * authoring error. The whole envelope is the two top-level keys `type` and an
176
- * optional `title`; there is no game-system data on a page that compiles to no
177
- * document.
217
+ * authoring error. The whole envelope is the top-level keys `type` and
218
+ * `shortcode`, plus an optional `title` or `name`; there is no game-system data
219
+ * on a page that compiles to no document.
178
220
  *
179
221
  * @type {readonly import("./field-spec.mjs").FieldSpec[]}
180
222
  */
181
223
  export const HOMEPAGE_FIELDS: readonly import("./field-spec.mjs").FieldSpec[];
182
224
  /**
183
- * Where a homepage is written, relative to the package's site root.
225
+ * The shortcode a package landing conventionally takes.
184
226
  *
185
- * Hugo's section landing, because the page *is* the package's landing: the
186
- * package root is a section and this is its index.
227
+ * A **convention, not a rule.** The address only has to be unique within the
228
+ * package, which `(type, shortcode)` already guarantees, and nothing here knows
229
+ * better than an author what their landing is called. What the constant buys is
230
+ * one spelling shared by the diagnostic, the documentation and the six trees —
231
+ * so `[[homepage-root|…]]` is the same link in every package.
187
232
  *
188
233
  * @type {string}
189
234
  */
190
- export const HOMEPAGE_DESTINATION: string;
235
+ export const HOMEPAGE_SHORTCODE: string;
191
236
  /**
192
- * The top-level fields a homepage refuses, and what each one would decide (#53).
193
- *
194
- * A note's URL derives from `name.full` and its identity from
195
- * `(type, shortcode)`. The homepage is the one page for which neither holds: it
196
- * publishes at `/<package>/`, fixed by the package id. An author fluent in the
197
- * conventions writes them here expecting exactly what they do everywhere else,
198
- * and gets none of it.
199
- *
200
- * **They were never inert, which is why ignoring them was the wrong answer.** A
201
- * `shortcode` puts the note in the address index and in the `dataview` link
202
- * universe, so `[[homepage-<shortcode>]]` resolves *green* to
203
- * `homepage/<slug>/`, an address derived from `name.full` and published by
204
- * nothing, because a homepage is written to {@link HOMEPAGE_DESTINATION} at the
205
- * package root. A build that reports a live link to a 404 is worse than one
206
- * that says nothing. It also inflates `content-build lint`'s address tally, so
207
- * the lint and the link manifest disagree about what the package publishes.
208
- *
209
- * **A named class, not an allow-list, and that boundary is the decision.** The
210
- * documented envelope is `type` plus an optional `title`, and `landing`,
211
- * `description` and `banner` are legitimate beside them — but a homepage's
212
- * frontmatter is *emitted into the published page*
237
+ * The top-level field a homepage refuses, and what it would decide (#53).
238
+ *
239
+ * **One field, where there used to be three.** `name` and `shortcode` were
240
+ * refused because a page's URL derived from `name.full` while a homepage's
241
+ * destination was fixed, so the address a `shortcode` computed named a page the
242
+ * site build never wrote. A page's URL is its address now (#181) and a homepage
243
+ * publishes at its own, so both fields decide exactly what they decide
244
+ * everywhere else and are permitted (#182).
245
+ *
246
+ * `id` is untouched by that, and stays: it is the Foundry document id a
247
+ * compendium UUID is built from, and a homepage compiles into no document.
248
+ *
249
+ * **A named class, not an allow-list, and that boundary is the decision.** A
250
+ * homepage's frontmatter is *emitted into the published page*
213
251
  * ({@link homepageFrontmatter}), so an unrecognised key is a Hugo or theme
214
252
  * parameter this build has never heard of and has no standing to refuse.
215
253
  * Rejecting unknown keys would make every new theme parameter wait on a
216
- * package-build release. What is refused is the specific class that makes a
217
- * false claim about *where this page is*.
254
+ * package-build release.
218
255
  *
219
- * `aliases` is deliberately not in the class: {@link homepageFrontmatter}
220
- * already drops it from every emitted page, with a reason of its own, so
221
- * authoring one is the same no-op it is on any other page rather than a wrong
222
- * belief about this one's address.
256
+ * `aliases` is deliberately not in the class: it is a **retired** field, refused
257
+ * on every note whatever its type (#180), so it is answered there rather than
258
+ * here.
223
259
  *
224
260
  * @type {ReadonlyMap<string, string>}
225
261
  */
@@ -28,7 +28,6 @@ export * as documentSubtypes from "./document-subtypes.mjs";
28
28
  export * as itemDocs from "./item-docs.mjs";
29
29
  export * as wikilinks from "./wikilinks.mjs";
30
30
  export * as wikilinkSyntax from "./wikilink-syntax.mjs";
31
- export * as aliasIndex from "./alias-index.mjs";
32
31
  export * as siteIndex from "./site-index.mjs";
33
32
  export * as baseCompiler from "./base-compiler.mjs";
34
33
  export * as journals from "./journals.mjs";
@@ -6,6 +6,35 @@
6
6
  * @returns {readonly string[]} The tags, in declaration order.
7
7
  */
8
8
  export function declaredTags(type: string, groups?: object): readonly string[];
9
+ /**
10
+ * Whether a note carries a given tag, however the author wrote it.
11
+ *
12
+ * `tags:` is authored by hand and Obsidian is permissive about it: a single tag
13
+ * may be a scalar rather than a list, a value may carry the leading `#` it is
14
+ * written with in prose, and case and surrounding space are not significant.
15
+ * The spelling of the tag *itself* still is — a near miss is a near miss, and
16
+ * the frontmatter lint is what reports it; nothing here guesses.
17
+ *
18
+ * Reads `tags` and, as Dataview does, `tag` — the singular spelling Obsidian
19
+ * also accepts.
20
+ *
21
+ * @param {object|null|undefined} fm - Parsed frontmatter.
22
+ * @param {string} tag - The tag to look for, in its declared spelling.
23
+ * @returns {boolean} Whether the note carries it.
24
+ */
25
+ export function hasTag(fm: object | null | undefined, tag: string): boolean;
26
+ /**
27
+ * Whether a note is tagged as an unfinished **draft** (#183).
28
+ *
29
+ * The one reader of {@link DRAFT_TAG}, so both builds ask the same question of
30
+ * the same field. Presentation only: a draft note is in the packs, in the
31
+ * manifest and on the site exactly as any other, and this decides nothing but
32
+ * whether a link into it renders marked.
33
+ *
34
+ * @param {object|null|undefined} fm - Parsed frontmatter.
35
+ * @returns {boolean} Whether the note carries the `draft` tag.
36
+ */
37
+ export function isDraftNote(fm: object | null | undefined): boolean;
9
38
  /**
10
39
  * The `data:` keys a note type may carry.
11
40
  *
@@ -66,6 +95,20 @@ export function subTypes(type: string, vocabulary?: Readonly<Record<string, Type
66
95
  * fishing village is a `village` that is `fishing`, and the single-valued field
67
96
  * this replaced had to spell it `Fishing Village` as a value of its own.
68
97
  */
98
+ /**
99
+ * The declared tag that marks a note as **unfinished** (#183).
100
+ *
101
+ * Named once and referenced from the declaration below, because a second
102
+ * spelling is how the two come apart: rename the tag in `DECLARED_TAGS` and a
103
+ * private copy elsewhere keeps matching the old word, silently.
104
+ *
105
+ * It is a **presentation** fact and nothing more. A draft note compiles,
106
+ * validates, publishes and resolves like any other; only a link *into* it
107
+ * renders marked. What it emphatically is not is the retired `draft:` field,
108
+ * whose entire effect was to move a note from published to unresolvable — see
109
+ * {@link draftRetiredMessage}.
110
+ */
111
+ export const DRAFT_TAG: "draft";
69
112
  export const DECLARED_TAGS: Readonly<{
70
113
  /** What a place *is*. */
71
114
  placeKind: Readonly<{
@@ -35,6 +35,77 @@ export function assertNoDraftField(fm: object | null | undefined, { file, absPat
35
35
  file?: string | undefined;
36
36
  absPath?: string | undefined;
37
37
  }): void;
38
+ /**
39
+ * What a note declaring a top-level `aliases:` is told, in one place.
40
+ *
41
+ * Shared by the compile-time refusal and the frontmatter lint, because an
42
+ * author meets whichever of the two runs first and they should read the same.
43
+ * It says what the field fed and what to write instead, rather than which value
44
+ * to correct: no value makes declaring it right.
45
+ *
46
+ * **What it did (#180).** It was the authored half of the alias index — the
47
+ * namespace a bare `[[Alias]]` was looked up in. Across the three content trees
48
+ * not one bare link resolved through it, while the collision rule that kept it
49
+ * unambiguous folded in every note's `name.full` and so dictated what a note
50
+ * could be named (#179). The form is retired, so the list has no reader.
51
+ *
52
+ * **`name.aliases` is a different field and is not retired.** It fed the same
53
+ * index, but unlike the top-level list it is being kept — reserved, unread,
54
+ * and deliberately unmentioned by this message, which would otherwise tell an
55
+ * author to delete a field they are allowed to write. See
56
+ * {@link assertNoAliasesField}.
57
+ *
58
+ * @param {string} [file] - The note's path, named in the message. Omit it where
59
+ * the caller emits through a diagnostic, whose locator already starts the
60
+ * line — repeating it prints the path twice.
61
+ * @returns {string} The message, unpunctuated at the end as a finding is.
62
+ */
63
+ export function aliasesRetiredMessage(file?: string): string;
64
+ /**
65
+ * Refuse a note that declares a top-level `aliases:`.
66
+ *
67
+ * Presence is the whole test. `aliases: []` is as retired as a populated one —
68
+ * it reads as "this note claims no other names", a statement about a namespace
69
+ * that no longer exists.
70
+ *
71
+ * **The nested `name.aliases` is deliberately not refused.** Both spellings fed
72
+ * the retired alias index, and both lost their reader with it, but only the
73
+ * top-level one is retired: `name.aliases` is **reserved**, held for a use that
74
+ * does not exist yet. So it is neither refused nor read — no index consults it,
75
+ * no rule validates its contents, nothing derives from it, and nothing emits
76
+ * it. It rides in the note as inert data, and a note carrying one compiles,
77
+ * resolves and emits exactly as if it were absent.
78
+ *
79
+ * That distinction is the reason this checks `Object.hasOwn(fm, "aliases")`
80
+ * rather than resolving a dotted key: the top-level field is the whole subject,
81
+ * and reaching into `name` at all is the thing being avoided.
82
+ *
83
+ * @param {object|null|undefined} fm - Parsed frontmatter, or nothing when it
84
+ * could not be parsed.
85
+ * @param {object} [options] - Options.
86
+ * @param {string} [options.file] - The note's path, named in the message. Omit
87
+ * it where the caller emits through a diagnostic, which puts the locator at
88
+ * the start of the line already — repeating it prints the path twice.
89
+ * @param {string} [options.absPath] - The note's file on disk, read only on the
90
+ * failing path to locate the offending line and column. The position rides on
91
+ * the thrown error as `position`, for a caller that emits a diagnostic.
92
+ * @returns {void}
93
+ * @throws {Error} When the note declares a top-level `aliases`.
94
+ */
95
+ export function assertNoAliasesField(fm: object | null | undefined, { file, absPath }?: {
96
+ file?: string | undefined;
97
+ absPath?: string | undefined;
98
+ }): void;
99
+ /**
100
+ * Whether a note declares the retired top-level `aliases:`.
101
+ *
102
+ * A nested `name.aliases` is **not** this field and never answers true here —
103
+ * see {@link assertNoAliasesField} for why the two part company.
104
+ *
105
+ * @param {object|null|undefined} fm - Parsed frontmatter.
106
+ * @returns {boolean} Whether the retired field is declared.
107
+ */
108
+ export function declaresRetiredAliasesField(fm: object | null | undefined): boolean;
38
109
  /**
39
110
  * A frontmatter key's position in a note's file, or nothing.
40
111
  *
@@ -47,11 +118,17 @@ export function assertNoDraftField(fm: object | null | undefined, { file, absPat
47
118
  * @param {string} [value] - When given, prefer the occurrence whose line also
48
119
  * carries this text — so a finding about one entry of a block opens on that
49
120
  * entry rather than on the key that introduces it.
121
+ * @param {object} [options] - Options, forwarded to
122
+ * {@link positionInFrontmatter}.
123
+ * @param {boolean} [options.topLevel=false] - Require the key at column 1, so
124
+ * an identically named nested key cannot answer for it.
50
125
  * @returns {{line?: number, column?: number}|undefined} Spreadable position
51
126
  * fields, dropped rather than guessed when the file cannot be read or the key
52
127
  * cannot be found — as `formatDiagnostic` requires.
53
128
  */
54
- export function locateFrontmatterKey(absPath: string | undefined, key: string, value?: string): {
129
+ export function locateFrontmatterKey(absPath: string | undefined, key: string, value?: string, { topLevel }?: {
130
+ topLevel?: boolean | undefined;
131
+ }): {
55
132
  line?: number;
56
133
  column?: number;
57
134
  } | undefined;