@heroiclands/package-build 22.4.7 → 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.
@@ -15,8 +15,11 @@ export * as derivedFields from "./derived-fields.mjs";
15
15
  export * as homepage from "./homepage.mjs";
16
16
  export * as noteSchemas from "./note-schemas.mjs";
17
17
  export * as noteVocabulary from "./note-vocabulary.mjs";
18
+ export * as noteState from "./note-state.mjs";
19
+ export * as stubLint from "./stub-lint.mjs";
18
20
  export * as placeRelations from "./place-relations.mjs";
19
21
  export * as holdings from "./holdings.mjs";
22
+ export * as populations from "./populations.mjs";
20
23
  export * as marketClass from "./market-class.mjs";
21
24
  export * as mapPlaces from "./map-places.mjs";
22
25
  export * as mapLayout from "./map-layout.mjs";
@@ -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,6 +354,11 @@ 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;
347
362
  /**
348
363
  * - A
349
364
  * check of the whole note, run by the frontmatter lint beside the field
@@ -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
+ };
@@ -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