@heroiclands/package-build 22.4.6 → 22.5.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 (45) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/CONTENT.md +192 -1
  3. package/bin/content-build.mjs +26 -4
  4. package/content-config.mjs +8 -3
  5. package/docs/api.md +99 -26
  6. package/docs/configuration.md +20 -1
  7. package/docs/content-format.md +238 -0
  8. package/docs/diagnostics.md +44 -0
  9. package/engine/content-index.mjs +63 -21
  10. package/engine/content-links.mjs +60 -6
  11. package/engine/content-tables.mjs +29 -2
  12. package/engine/frontmatter-lint.mjs +2 -0
  13. package/engine/holdings.mjs +416 -0
  14. package/engine/index-records.mjs +25 -0
  15. package/engine/index.mjs +16 -0
  16. package/engine/map-graphviz.mjs +12 -3
  17. package/engine/map-places.mjs +33 -3
  18. package/engine/market-class.mjs +178 -0
  19. package/engine/metadata-index.mjs +7 -4
  20. package/engine/note-state.mjs +213 -0
  21. package/engine/note-vocabulary.mjs +59 -2
  22. package/engine/pdf-build.mjs +4 -2
  23. package/engine/populations.mjs +557 -0
  24. package/engine/site-build.mjs +96 -10
  25. package/engine/site-maps.mjs +165 -0
  26. package/engine/sql-tables.mjs +157 -4
  27. package/engine/stub-lint.mjs +237 -0
  28. package/engine/wikilink-syntax.mjs +27 -1
  29. package/package.json +1 -1
  30. package/types/engine/content-index.d.mts +19 -5
  31. package/types/engine/content-links.d.mts +3 -3
  32. package/types/engine/content-tables.d.mts +2 -1
  33. package/types/engine/holdings.d.mts +157 -0
  34. package/types/engine/index-records.d.mts +22 -0
  35. package/types/engine/index.d.mts +6 -0
  36. package/types/engine/map-graphviz.d.mts +8 -1
  37. package/types/engine/map-places.d.mts +26 -0
  38. package/types/engine/market-class.d.mts +62 -0
  39. package/types/engine/note-state.d.mts +110 -0
  40. package/types/engine/note-vocabulary.d.mts +25 -0
  41. package/types/engine/populations.d.mts +121 -0
  42. package/types/engine/site-build.d.mts +37 -6
  43. package/types/engine/site-maps.d.mts +81 -0
  44. package/types/engine/stub-lint.d.mts +51 -0
  45. package/types/engine/wikilink-syntax.d.mts +19 -1
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Whether a value is one of the classes the scale states.
3
+ *
4
+ * A note may write the number quoted, which YAML hands over as a string, so
5
+ * the value is read as a number before it is compared — the same reading
6
+ * `matchesKind` gives a `number` field.
7
+ *
8
+ * @param {unknown} value - The authored value.
9
+ * @returns {boolean} Whether it names a class.
10
+ */
11
+ export function isMarketClass(value: unknown): boolean;
12
+ /**
13
+ * The class a value names, or `undefined` for a value that names none.
14
+ *
15
+ * @param {unknown} value - The authored value.
16
+ * @returns {MarketClass|undefined} The class.
17
+ */
18
+ export function marketClass(value: unknown): MarketClass | undefined;
19
+ /**
20
+ * Check a place note's `data.market`.
21
+ *
22
+ * The value is one of the classes the scale states, or it is a finding located
23
+ * at the key's own line. A value that is not a number at all is left alone:
24
+ * the container's own shape check reports it, and saying so twice would make
25
+ * one slip read as two.
26
+ *
27
+ * @param {object} note - The note, as the link index hands it over.
28
+ * @returns {object[]} Findings.
29
+ */
30
+ export function checkMarket(note: object): object[];
31
+ /**
32
+ * One step of the market scale.
33
+ *
34
+ * @typedef {object} MarketClass
35
+ * @property {number} value - The integer a note writes.
36
+ * @property {string} name - What a settlement of this class is called.
37
+ * @property {string} trade - What can be had there.
38
+ */
39
+ /**
40
+ * The market scale, in order, from the settlement with no market to the one
41
+ * other markets buy from.
42
+ *
43
+ * @type {readonly MarketClass[]}
44
+ */
45
+ export const MARKET_CLASSES: readonly MarketClass[];
46
+ /**
47
+ * One step of the market scale.
48
+ */
49
+ export type MarketClass = {
50
+ /**
51
+ * - The integer a note writes.
52
+ */
53
+ value: number;
54
+ /**
55
+ * - What a settlement of this class is called.
56
+ */
57
+ name: string;
58
+ /**
59
+ * - What can be had there.
60
+ */
61
+ trade: string;
62
+ };
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Whether a note's body is empty.
3
+ *
4
+ * > **The body is empty when everything after the closing frontmatter fence is
5
+ * > whitespace.** Nothing else is empty.
6
+ *
7
+ * The severity is the point: **the test must be one a person can apply by
8
+ * looking at the file.** Every softer rule makes two files that look different
9
+ * behave the same, and turns "why did my page disappear?" into an
10
+ * investigation. So an HTML comment, a horizontal rule, a lone heading and
11
+ * `_To be written._` are all bodies — each renders to little or nothing, and
12
+ * {@link module:engine/stub-lint} reports them rather than treating them as
13
+ * either state.
14
+ *
15
+ * An HTML comment is deliberately not empty. An invisible marker deciding
16
+ * whether a page exists is the failure the retired `draft:` field was removed
17
+ * for, and reintroducing it in comment form would be worse, because nothing
18
+ * would report it.
19
+ *
20
+ * @param {string|null|undefined} body - The note body, frontmatter stripped.
21
+ * @returns {boolean} Whether the body is empty.
22
+ */
23
+ export function isEmptyBody(body: string | null | undefined): boolean;
24
+ /**
25
+ * Whether an empty body makes a note of this type a stub.
26
+ *
27
+ * Two types are **structural** and exempt, and the measurement forced the
28
+ * question: a `folder` note's page is a generated section index and the
29
+ * `homepage`'s is the site's front door, so both have no body by design. If an
30
+ * empty body meant "no page" for them, a tree would lose a page per folder plus
31
+ * its own front door, and the build would report nothing.
32
+ *
33
+ * Read from {@link module:engine/note-vocabulary.NOTE_VOCABULARY}, where every
34
+ * type declares `stubbable` — so a type added without answering the question
35
+ * fails the guard rather than being classified by a default nobody chose. A
36
+ * type the registry does not declare at all is stubbable: the rule is about the
37
+ * body, and an unknown type has no exemption to claim.
38
+ *
39
+ * @param {string|null|undefined} type - The note's `type`.
40
+ * @param {Readonly<Record<string, {stubbable?: boolean}>>} [vocabulary] - The
41
+ * registry to read.
42
+ * @returns {boolean} Whether an empty body suppresses this type's page.
43
+ */
44
+ export function isStubbableType(type: string | null | undefined, vocabulary?: Readonly<Record<string, {
45
+ stubbable?: boolean;
46
+ }>>): boolean;
47
+ /**
48
+ * Whether a note is a stub: an empty body on a type an empty body suppresses.
49
+ *
50
+ * The one place the two halves are put together, so the index, the lint and the
51
+ * guard cannot answer it differently.
52
+ *
53
+ * @param {Record<string, any>|null|undefined} frontmatter - Parsed frontmatter.
54
+ * @param {string|null|undefined} body - The note body.
55
+ * @param {Readonly<Record<string, {stubbable?: boolean}>>} [vocabulary] - The
56
+ * registry to read.
57
+ * @returns {boolean} Whether the note is a stub.
58
+ */
59
+ export function isStubNote(frontmatter: Record<string, any> | null | undefined, body: string | null | undefined, vocabulary?: Readonly<Record<string, {
60
+ stubbable?: boolean;
61
+ }>>): boolean;
62
+ /**
63
+ * The placeholder phrases a body reduces to, or an empty list.
64
+ *
65
+ * A body is reduced by dropping every line that carries no content of its own —
66
+ * a heading, a thematic break, an HTML comment, a blank line — and then asking
67
+ * whether every line that is left is a placeholder. A body with some real prose
68
+ * beside its unwritten sections is **not** reduced: a `TBD` under a heading in
69
+ * a note that is honestly a draft is not a defect, and this fires only when a
70
+ * body reduces to nothing but such phrases.
71
+ *
72
+ * The phrases are returned with the 1-based offset of the line each sat on
73
+ * within the body, so a finding can be located rather than merely stated.
74
+ *
75
+ * @param {string|null|undefined} body - The note body.
76
+ * @returns {Array<{phrase: string, line: number}>} The phrases, in body order.
77
+ * Empty for a body that is empty, or that carries prose of its own.
78
+ */
79
+ export function placeholderBody(body: string | null | undefined): Array<{
80
+ phrase: string;
81
+ line: number;
82
+ }>;
83
+ /**
84
+ * How many words a body holds, counting only the prose a reader meets.
85
+ *
86
+ * Fence blocks, headings, thematic breaks and HTML comments are dropped, and a
87
+ * wikilink counts as the one word it renders — `[[affiliation-meivor|Mëivōr]]`
88
+ * is a word, not three. What is left is split on whitespace.
89
+ *
90
+ * @param {string|null|undefined} body - The note body.
91
+ * @returns {number} The word count.
92
+ */
93
+ export function bodyWordCount(body: string | null | undefined): number;
94
+ /**
95
+ * The three states, in the order they read as a ladder.
96
+ *
97
+ * @type {ReadonlyArray<string>}
98
+ */
99
+ export const NOTE_STATES: ReadonlyArray<string>;
100
+ /**
101
+ * The phrases that stand in for prose nobody has written.
102
+ *
103
+ * A body consisting of nothing but these is an abandoned draft, not a stub:
104
+ * it looks written, publishes a page, and says nothing. The list is matched
105
+ * whole-line, case-insensitively, after trailing punctuation is dropped — a
106
+ * line reading `TBD.` and one reading `tbd` are the same line.
107
+ *
108
+ * @type {ReadonlyArray<string>}
109
+ */
110
+ export const PLACEHOLDER_PHRASES: ReadonlyArray<string>;
@@ -333,6 +333,16 @@ export type DataFieldSpec = {
333
333
  * - **`null`** — the type has a `subType` whose values the specification does
334
334
  * not yet enumerate. Presence is permitted and the value is unchecked.
335
335
  * - **a list** — the closed set of values, and anything else is a finding.
336
+ *
337
+ * `stubbable` says whether an empty body suppresses this type's page. Every
338
+ * type declares it, so adding one means answering the question rather than
339
+ * inheriting a default nobody chose — and a guard derives the check from this
340
+ * registry's own key list, so an omission is a red test.
341
+ *
342
+ * Two types answer **false**, and both are structural: a `folder` note's page
343
+ * is a generated section index and a `homepage`'s is the site's front door, so
344
+ * each is complete with no body at all. An empty body on one of them keeps its
345
+ * address and resolves as `full`.
336
346
  */
337
347
  export type TypeVocabulary = {
338
348
  /**
@@ -344,4 +354,19 @@ export type TypeVocabulary = {
344
354
  * values, as above.
345
355
  */
346
356
  subTypes?: readonly string[] | null | undefined;
357
+ /**
358
+ * - Whether an empty body makes a note of this
359
+ * type a stub — see {@link module :engine/note-state.isStubbableType}.
360
+ */
361
+ stubbable: boolean;
362
+ /**
363
+ * - A
364
+ * check of the whole note, run by the frontmatter lint beside the field
365
+ * checks with the same index. For a rule that is about the note rather than
366
+ * one of its fields — a place's tenure is a fact about every affiliation's
367
+ * `domains`, and is located at the note's `type:` line.
368
+ */
369
+ check?: ((note: object, opts: {
370
+ index?: object;
371
+ }) => object[]) | undefined;
347
372
  };
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Check a place's population against the corpus around it.
3
+ *
4
+ * Declared on the `place` vocabulary beside `data.population`, so the lint
5
+ * runs it with the index every reference check resolves through. Without an
6
+ * index the questions cannot be asked and nothing is reported.
7
+ *
8
+ * Three rules, each reported on the note being linted: `over-held land` and
9
+ * `over-full region` on the place whose figure is exceeded, and
10
+ * `oversized settlement` on the settlement that exceeds one.
11
+ *
12
+ * @param {object} note - The note, as the link index hands it over.
13
+ * @param {object} [opts]
14
+ * @param {object} [opts.index] - The link index, holding the whole corpus.
15
+ * @returns {object[]} Findings.
16
+ */
17
+ export function checkPopulation(note: object, { index }?: {
18
+ index?: object | undefined;
19
+ }): object[];
20
+ /**
21
+ * Check the populations a `doc` note cites against the notes they belong to.
22
+ *
23
+ * A reference page restates figures the place and affiliation notes own, and
24
+ * restated figures drift. Each is located at the figure it wrote, counting
25
+ * repeats so two rows carrying one number land on their own lines.
26
+ *
27
+ * A citation naming a note this package does not hold is skipped: a fetched
28
+ * index carries no figure, so there is nothing to disagree with.
29
+ *
30
+ * Declared as the `doc` vocabulary's type-level check.
31
+ *
32
+ * @param {object} note - The note, as the link index hands it over.
33
+ * @param {object} [opts]
34
+ * @param {object} [opts.index] - The link index, holding the whole corpus.
35
+ * @returns {object[]} Findings.
36
+ */
37
+ export function checkCitedPopulations(note: object, { index }?: {
38
+ index?: object | undefined;
39
+ }): object[];
40
+ /**
41
+ * How far a parent's figure may fall below the sum of what it contains before
42
+ * that is a finding.
43
+ *
44
+ * A figure written to two significant digits carries a half-unit of error in
45
+ * the second digit, and that error is widest at the bottom of a decade — half
46
+ * a unit on `1.0` is five per cent, against half a unit on `9.9` at a twentieth
47
+ * of that. The widest case is the one a tolerance has to admit, so the whole
48
+ * scale is held to five per cent.
49
+ *
50
+ * @type {number}
51
+ */
52
+ export const POPULATION_TOLERANCE: number;
53
+ /**
54
+ * The rules this module enforces, each `name` the prefix its findings carry.
55
+ *
56
+ * The list is the module's own statement of what it checks: a reader learns
57
+ * the whole surface from it, and the suite derives a case per entry from it, so
58
+ * a rule cannot be added without evidence that it fires.
59
+ *
60
+ * @type {ReadonlyArray<{name: string, describe: string}>}
61
+ */
62
+ export const POPULATION_RULES: ReadonlyArray<{
63
+ name: string;
64
+ describe: string;
65
+ }>;
66
+ /**
67
+ * One place or affiliation as the rules read it.
68
+ */
69
+ export type PopulationNode = {
70
+ /**
71
+ * Lower case.
72
+ */
73
+ shortcode: string;
74
+ type: "place" | "affiliation";
75
+ subType: string;
76
+ /**
77
+ * What a finding calls it.
78
+ */
79
+ title: string;
80
+ /**
81
+ * The stated figure, absent where unstated.
82
+ */
83
+ population?: number | undefined;
84
+ /**
85
+ * A place's enclosing places, an affiliation's
86
+ * superiors.
87
+ */
88
+ parents: string[];
89
+ /**
90
+ * What an affiliation holds.
91
+ */
92
+ domains: string[];
93
+ };
94
+ /**
95
+ * The whole tree, indexed the three ways the rules read it.
96
+ */
97
+ export type Corpus = {
98
+ /**
99
+ * By shortcode.
100
+ */
101
+ places: Map<string, PopulationNode>;
102
+ /**
103
+ * By shortcode.
104
+ */
105
+ affiliations: Map<string, PopulationNode>;
106
+ /**
107
+ * Place shortcode → the places
108
+ * whose `parents` name it.
109
+ */
110
+ childrenOf: Map<string, string[]>;
111
+ /**
112
+ * Place shortcode → the polities
113
+ * whose `domains` name it.
114
+ */
115
+ holdersOf: Map<string, string[]>;
116
+ /**
117
+ * `type-shortcode` → the node,
118
+ * for the citation rule.
119
+ */
120
+ byAddress: Map<string, PopulationNode>;
121
+ };
@@ -176,7 +176,10 @@ export function tableUniverse(pages: object[]): Map<string, object[]>;
176
176
  * about every other page, known only once the whole tree has resolved — see
177
177
  * {@link module:engine/related-pages} — so an authored value is dropped the
178
178
  * way `aliases` is, and {@link renderPages} writes the derived block once it
179
- * holds the graph.
179
+ * holds the graph. `contains`, `held_by` and `holdings` are dropped for the
180
+ * same reason — see {@link module:engine/holdings}. **So is `map`**: the
181
+ * file a place page names is the one the build drew beside it — see
182
+ * {@link module:engine/site-maps} — and a page with no drawing names none.
180
183
  *
181
184
  * @param {object} page - The page.
182
185
  * @param {object} options
@@ -210,10 +213,21 @@ export function pageFrontmatter(page: object, { decorate, webSrc, artSrc }: {
210
213
  * `doc` note's `subType` may be spelled the same as another note's `type`, and
211
214
  * `doc-gear.md` and `weapongear-gear.md` are distinct.
212
215
  *
216
+ * **A page that carries a file beside itself is a leaf bundle**, `<slug>/index.md`,
217
+ * because that is the one shape under which Hugo hands a page its own
218
+ * resources — the map from a place, inlined by the theme through
219
+ * `.Resources.Get`. The directory is the page, not a section: it holds no
220
+ * `_index.md`, and the page's stated `url` keeps its address exactly where the
221
+ * flat file's was.
222
+ *
213
223
  * @param {object} page - The page.
224
+ * @param {object} [opts]
225
+ * @param {boolean} [opts.bundle=false] - Whether the page carries a resource.
214
226
  * @returns {string} The file, relative to the mount.
215
227
  */
216
- export function pageDestination(page: object): string;
228
+ export function pageDestination(page: object, { bundle }?: {
229
+ bundle?: boolean | undefined;
230
+ }): string;
217
231
  /**
218
232
  * Renders and writes every page.
219
233
  *
@@ -236,16 +250,25 @@ export function pageDestination(page: object): string;
236
250
  * point the resolver answers, and inverted once between them. Nothing is
237
251
  * resolved twice.
238
252
  *
253
+ * **A map is written with its page.** The drawing from a place is made before
254
+ * this render — see {@link module:engine/site-maps} — and handed in keyed by
255
+ * page URL; the page that owns one is written as a leaf bundle, the file is
256
+ * copied beside it, and the front matter names it as `map`. Every other page
257
+ * is written flat, and carries no such key.
258
+ *
239
259
  * @param {object[]} pages - Every page.
240
260
  * @param {object} options - Everything the render needs. `homepages` is the
241
261
  * homepage as the index knows it — `{ fm, body, url, title }` — which takes
242
262
  * no part in the render and every part in the graph: a content page links
243
- * it by wikilink, and its own markdown links name content pages.
263
+ * it by wikilink, and its own markdown links name content pages. `maps` is
264
+ * each drawing by the URL of the page that carries it.
244
265
  * @returns {{written: number, byKind: Record<string, number>, tableErrors: object[],
245
266
  * wikiErrors: object[], imageErrors: object[],
246
- * related: Map<string, import("./related-pages.mjs").Related>}} `related`
267
+ * related: Map<string, import("./related-pages.mjs").Related>,
268
+ * maps: number}} `related`
247
269
  * is keyed by page URL, and holds the homepage's block beside every content
248
270
  * page's, so the caller can write it on the page this render does not.
271
+ * `maps` counts the pages written with one.
249
272
  */
250
273
  export function renderPages(pages: object[], options: object): {
251
274
  written: number;
@@ -254,6 +277,7 @@ export function renderPages(pages: object[], options: object): {
254
277
  wikiErrors: object[];
255
278
  imageErrors: object[];
256
279
  related: Map<string, import("./related-pages.mjs").Related>;
280
+ maps: number;
257
281
  };
258
282
  /**
259
283
  * Resolves `site.pass` to its bundle.
@@ -281,18 +305,25 @@ export function resolveSitePass(name: string | undefined, options: object): {
281
305
  * {@link module:engine/sql-tables.prepareSqlTables}. A page authoring an
282
306
  * `sql` directive with none prepared is a table error: nothing here runs a
283
307
  * query.
308
+ * @param {(engine: string) => string|undefined} [options.locate] - How a
309
+ * GraphViz engine's binary is found, for the maps; the real lookup by
310
+ * default.
284
311
  * @returns {{gates: object, stats: object|null, tableErrors: object[],
285
- * wikiErrors: object[], imageErrors: object[], manifests: object|null}}
312
+ * wikiErrors: object[], imageErrors: object[], mapFindings: object[],
313
+ * manifests: object|null}} `mapFindings` is what drawing the maps found —
314
+ * warnings, never a reason to fail the build.
286
315
  */
287
- export function buildSite({ config, sqlTables }?: {
316
+ export function buildSite({ config, sqlTables, locate }?: {
288
317
  config?: object | undefined;
289
318
  sqlTables?: Map<string, object[]> | undefined;
319
+ locate?: ((engine: string) => string | undefined) | undefined;
290
320
  }): {
291
321
  gates: object;
292
322
  stats: object | null;
293
323
  tableErrors: object[];
294
324
  wikiErrors: object[];
295
325
  imageErrors: object[];
326
+ mapFindings: object[];
296
327
  manifests: object | null;
297
328
  };
298
329
  export { formatUnaddressableFinding };
@@ -0,0 +1,81 @@
1
+ /**
2
+ * A drawing as the page writer receives it.
3
+ *
4
+ * @typedef {object} SiteMap
5
+ * @property {string} name - The file's name in the page's bundle, and the
6
+ * value of the page's `map` key: `from-<shortcode>.svg`.
7
+ * @property {string} file - Where the drawing is, absolute.
8
+ */
9
+ /**
10
+ * A GraphViz SVG rewritten for inlining in an HTML page.
11
+ *
12
+ * GraphViz writes a standalone document: an XML declaration, a DOCTYPE naming
13
+ * the SVG 1.1 DTD by URL, generator comments, a root sized in points beside
14
+ * its `viewBox`, and links through the `xlink` namespace. Inline in HTML the
15
+ * prologue is invalid, the DTD reference is the one external reference the
16
+ * drawing would carry, the fixed size fights the column it sits in, and
17
+ * `xlink:` is the pre-SVG-2 spelling. So the prologue and the comments go,
18
+ * the root keeps its `viewBox` and loses `width` and `height`, and
19
+ * `xlink:href` and `xlink:title` become `href` and `title`, with the
20
+ * namespace declaration they needed.
21
+ *
22
+ * @param {string} svg - The SVG as GraphViz wrote it.
23
+ * @returns {string} The SVG to inline, starting at `<svg`.
24
+ */
25
+ export function inlineSvg(svg: string): string;
26
+ /**
27
+ * Draw the map from every related place of this package.
28
+ *
29
+ * @param {object} opts
30
+ * @param {Array<Record<string, any>>} opts.records - The package's index
31
+ * records, as the site build read them.
32
+ * @param {Map<string, object>} opts.foreignIndex - The fetched indexes the
33
+ * site build resolved links against.
34
+ * @param {object} opts.config - The resolved configuration.
35
+ * @param {string} opts.base - The site base every page is served under,
36
+ * ending in a slash: what every name in a drawing links through.
37
+ * @param {(engine: string) => string|undefined} [opts.locate] - How the
38
+ * engine's binary is found; the real lookup by default.
39
+ * @returns {{maps: Map<string, SiteMap>, findings: Array<{file: string,
40
+ * line?: number, column?: number, severity: "error"|"warning",
41
+ * message: string}>}} The drawings, keyed by the URL of the page each
42
+ * belongs to, and what the drawing found — one warning naming what to
43
+ * install when GraphViz is absent, and a warning for each relation that
44
+ * names no place.
45
+ */
46
+ export function drawSiteMaps({ records, foreignIndex, config, base, locate }: {
47
+ records: Array<Record<string, any>>;
48
+ foreignIndex: Map<string, object>;
49
+ config: object;
50
+ base: string;
51
+ locate?: ((engine: string) => string | undefined) | undefined;
52
+ }): {
53
+ maps: Map<string, SiteMap>;
54
+ findings: Array<{
55
+ file: string;
56
+ line?: number;
57
+ column?: number;
58
+ severity: "error" | "warning";
59
+ message: string;
60
+ }>;
61
+ };
62
+ /**
63
+ * Where the site build draws, below the package root.
64
+ *
65
+ * @type {string}
66
+ */
67
+ export const SITE_MAP_DIR: string;
68
+ /**
69
+ * A drawing as the page writer receives it.
70
+ */
71
+ export type SiteMap = {
72
+ /**
73
+ * - The file's name in the page's bundle, and the
74
+ * value of the page's `map` key: `from-<shortcode>.svg`.
75
+ */
76
+ name: string;
77
+ /**
78
+ * - Where the drawing is, absolute.
79
+ */
80
+ file: string;
81
+ };
@@ -0,0 +1,51 @@
1
+ /**
2
+ * The state of every note in a tree, checked and counted.
3
+ *
4
+ * Read from the content index rather than from a walk of this pass's own, like
5
+ * every other lint: the command derives the corpus once and hands it to each
6
+ * pass, so no two findings are drawn from different ideas of which files the
7
+ * content is. The **body** is read from the file, because the index carries
8
+ * what is *about* a note and never the note's text.
9
+ *
10
+ * @param {string} contentBase - Root of the content tree.
11
+ * @param {object} opts
12
+ * @param {readonly object[]} opts.records - Index records the caller derived.
13
+ * @param {string} [opts.contentPackage] - Named in the counts line.
14
+ * @returns {{findings: object[], counts: {full: number, draft: number,
15
+ * stub: number}, byType: Array<{type: string, full: number, draft: number,
16
+ * stub: number}>, oldestDrafts: Array<{file: string, modified: Date}>,
17
+ * summary: string[]}} The findings, the ladder's counts, and the lines a
18
+ * reader is shown as prose rather than as findings.
19
+ */
20
+ export function lintNoteStates(contentBase: string, { records, contentPackage }?: {
21
+ records: readonly object[];
22
+ contentPackage?: string | undefined;
23
+ }): {
24
+ findings: object[];
25
+ counts: {
26
+ full: number;
27
+ draft: number;
28
+ stub: number;
29
+ };
30
+ byType: Array<{
31
+ type: string;
32
+ full: number;
33
+ draft: number;
34
+ stub: number;
35
+ }>;
36
+ oldestDrafts: Array<{
37
+ file: string;
38
+ modified: Date;
39
+ }>;
40
+ summary: string[];
41
+ };
42
+ /**
43
+ * The word count below which an unmarked body is asked whether it is a draft.
44
+ *
45
+ * A threshold, so it is a value the rule *is* rather than a census: twenty-five
46
+ * words is about two sentences, which is where a note stops reading as a
47
+ * statement and starts reading as a beginning.
48
+ *
49
+ * @type {number}
50
+ */
51
+ export const SHORT_BODY_WORDS: number;
@@ -92,6 +92,20 @@ export function unlabelledLinkMessage(target: string): string;
92
92
  * @returns {string} The message, unpunctuated at the end as a finding is.
93
93
  */
94
94
  export function unresolvedAddressMessage(target: string): string;
95
+ /**
96
+ * What an author linking to a **stub** is told.
97
+ *
98
+ * Strictly better than "resolves to no note", and available only because a stub
99
+ * is in the index: the note exists, its file can be named, and the reason the
100
+ * link cannot land is a fact about that file rather than a guess about a
101
+ * shortcode. So the message names the file to open and offers both corrections
102
+ * — the link was either premature or the note is owed a body.
103
+ *
104
+ * @param {string} target - The address as authored.
105
+ * @param {string} [file] - Where the stub is, relative to the content root.
106
+ * @returns {string} The message, unpunctuated at the end as a finding is.
107
+ */
108
+ export function stubAddressMessage(target: string, file?: string): string;
95
109
  /**
96
110
  * What an author writing a short address more than one package publishes is
97
111
  * told.
@@ -130,16 +144,18 @@ export function ambiguousAddressMessage(target: string, packages?: Iterable<stri
130
144
  * @param {string} [finding.anchor] - For `unknown-anchor`, the section named.
131
145
  * @param {string} [finding.type] - For `not-an-asset`, the type the address
132
146
  * named.
147
+ * @param {string} [finding.stub] - For `stub`, where the stub is.
133
148
  * @returns {string} The message.
134
149
  * @throws {Error} On a reason outside the closed set — a resolver inventing one
135
150
  * would otherwise report a link with no explanation at all.
136
151
  */
137
- export function linkFindingMessage({ reason, target, packages, anchor, type }: {
152
+ export function linkFindingMessage({ reason, target, packages, anchor, type, stub }: {
138
153
  reason: string;
139
154
  target: string;
140
155
  packages?: Iterable<string> | undefined;
141
156
  anchor?: string | undefined;
142
157
  type?: string | undefined;
158
+ stub?: string | undefined;
143
159
  }): string;
144
160
  /**
145
161
  * Whether a parsed link addresses a section of the page it is written on.
@@ -221,6 +237,8 @@ export const WIKILINK: RegExp;
221
237
  * - `unknown-type` — definitely qualified, but names no type this build knows.
222
238
  * - `not-an-asset` — a real address, embedded where only a file can be drawn.
223
239
  * - `unresolved` — parses as an address, and nothing publishes it.
240
+ * - `stub` — the address names a note this tree holds whose body is empty, so
241
+ * it publishes no page for the link to reach.
224
242
  * - `ambiguous` — more than one package publishes the short address.
225
243
  * - `unknown-anchor` — the address resolved, the `#section` it names did not.
226
244
  * - `no-content-index` — the address names a package declared