@heroiclands/package-build 0.6.1 → 3.0.1

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 (127) hide show
  1. package/CHANGELOG-content-build.md +965 -0
  2. package/CHANGELOG.md +68 -0
  3. package/CONTENT.md +824 -0
  4. package/MIGRATING.md +127 -0
  5. package/README.md +29 -17
  6. package/bin/content-build.mjs +1082 -0
  7. package/bin/package-build.mjs +2 -2
  8. package/bin/report.mjs +2 -2
  9. package/config.mjs +21 -18
  10. package/content-config.mjs +1424 -0
  11. package/coverage.mjs +1 -1
  12. package/e2e.mjs +1 -1
  13. package/engine/abbreviations.mjs +0 -0
  14. package/engine/base-compiler.mjs +608 -0
  15. package/engine/code-fences.mjs +311 -0
  16. package/engine/compendiums.mjs +353 -0
  17. package/engine/content-address.mjs +141 -0
  18. package/engine/content-links.mjs +438 -0
  19. package/engine/content-lint.mjs +204 -0
  20. package/engine/content-package.mjs +67 -0
  21. package/engine/content-slug.mjs +169 -0
  22. package/engine/content-tables.mjs +1216 -0
  23. package/engine/content-tree.mjs +56 -0
  24. package/engine/diagnostics.mjs +270 -0
  25. package/engine/field-reference.mjs +242 -0
  26. package/engine/field-spec.mjs +218 -0
  27. package/engine/foreign-catalog.mjs +450 -0
  28. package/engine/foreign-manifests.mjs +129 -0
  29. package/engine/frontmatter-lint.mjs +343 -0
  30. package/engine/frontmatter.mjs +258 -0
  31. package/engine/generate.mjs +305 -0
  32. package/engine/helpers.mjs +809 -0
  33. package/engine/ids.mjs +192 -0
  34. package/engine/index.mjs +142 -0
  35. package/engine/item-docs.mjs +153 -0
  36. package/engine/item-registry.mjs +129 -0
  37. package/engine/journals.mjs +374 -0
  38. package/engine/kb-manifest.mjs +473 -0
  39. package/engine/macros.mjs +333 -0
  40. package/engine/manifest-emit.mjs +339 -0
  41. package/engine/map-notes.mjs +1302 -0
  42. package/engine/pack-config.mjs +479 -0
  43. package/engine/pack-router.mjs +254 -0
  44. package/engine/prose-config.mjs +238 -0
  45. package/engine/prose-lint.mjs +294 -0
  46. package/engine/region-events.mjs +79 -0
  47. package/engine/scene-levels.mjs +420 -0
  48. package/engine/scenes.mjs +528 -0
  49. package/engine/site-build.mjs +778 -0
  50. package/engine/site-index.mjs +332 -0
  51. package/engine/web-wikilinks.mjs +330 -0
  52. package/engine/wikilink-syntax.mjs +105 -0
  53. package/engine/wikilinks.mjs +645 -0
  54. package/index.mjs +42 -25
  55. package/lang.mjs +1 -1
  56. package/manifest.mjs +79 -5
  57. package/markdownlint-config.mjs +45 -0
  58. package/package.json +78 -35
  59. package/prettier-config.mjs +34 -0
  60. package/sohl/actors.mjs +581 -0
  61. package/sohl/affiliation-standings.mjs +44 -0
  62. package/sohl/being-info.mjs +173 -0
  63. package/sohl/default-item-art.mjs +82 -0
  64. package/sohl/index.mjs +55 -0
  65. package/sohl/item-builders.mjs +114 -0
  66. package/sohl/item-fields.mjs +867 -0
  67. package/sohl/items.mjs +197 -0
  68. package/sohl/kb-passes.mjs +207 -0
  69. package/sohl/note-schemas.mjs +322 -0
  70. package/templates.mjs +1 -1
  71. package/types/config.d.mts +1 -1
  72. package/types/content-config.d.mts +571 -0
  73. package/types/engine/abbreviations.d.mts +44 -0
  74. package/types/engine/base-compiler.d.mts +320 -0
  75. package/types/engine/code-fences.d.mts +102 -0
  76. package/types/engine/compendiums.d.mts +77 -0
  77. package/types/engine/content-address.d.mts +61 -0
  78. package/types/engine/content-links.d.mts +87 -0
  79. package/types/engine/content-lint.d.mts +47 -0
  80. package/types/engine/content-package.d.mts +36 -0
  81. package/types/engine/content-slug.d.mts +59 -0
  82. package/types/engine/content-tables.d.mts +131 -0
  83. package/types/engine/content-tree.d.mts +12 -0
  84. package/types/engine/diagnostics.d.mts +152 -0
  85. package/types/engine/field-reference.d.mts +20 -0
  86. package/types/engine/field-spec.d.mts +181 -0
  87. package/types/engine/foreign-catalog.d.mts +103 -0
  88. package/types/engine/foreign-manifests.d.mts +43 -0
  89. package/types/engine/frontmatter-lint.d.mts +74 -0
  90. package/types/engine/frontmatter.d.mts +99 -0
  91. package/types/engine/generate.d.mts +56 -0
  92. package/types/engine/helpers.d.mts +348 -0
  93. package/types/engine/ids.d.mts +126 -0
  94. package/types/engine/index.d.mts +33 -0
  95. package/types/engine/item-docs.d.mts +73 -0
  96. package/types/engine/item-registry.d.mts +57 -0
  97. package/types/engine/journals.d.mts +121 -0
  98. package/types/engine/kb-manifest.d.mts +214 -0
  99. package/types/engine/macros.d.mts +201 -0
  100. package/types/engine/manifest-emit.d.mts +137 -0
  101. package/types/engine/map-notes.d.mts +283 -0
  102. package/types/engine/pack-config.d.mts +77 -0
  103. package/types/engine/pack-router.d.mts +53 -0
  104. package/types/engine/prose-config.d.mts +121 -0
  105. package/types/engine/prose-lint.d.mts +59 -0
  106. package/types/engine/region-events.d.mts +43 -0
  107. package/types/engine/scene-levels.d.mts +36 -0
  108. package/types/engine/scenes.d.mts +48 -0
  109. package/types/engine/site-build.d.mts +214 -0
  110. package/types/engine/site-index.d.mts +141 -0
  111. package/types/engine/web-wikilinks.d.mts +67 -0
  112. package/types/engine/wikilink-syntax.d.mts +101 -0
  113. package/types/engine/wikilinks.d.mts +172 -0
  114. package/types/index.d.mts +3 -0
  115. package/types/manifest.d.mts +52 -4
  116. package/types/markdownlint-config.d.mts +8 -0
  117. package/types/prettier-config.d.mts +1 -0
  118. package/types/sohl/actors.d.mts +65 -0
  119. package/types/sohl/affiliation-standings.d.mts +22 -0
  120. package/types/sohl/being-info.d.mts +78 -0
  121. package/types/sohl/default-item-art.d.mts +28 -0
  122. package/types/sohl/index.d.mts +7 -0
  123. package/types/sohl/item-builders.d.mts +11 -0
  124. package/types/sohl/item-fields.d.mts +20 -0
  125. package/types/sohl/items.d.mts +19 -0
  126. package/types/sohl/kb-passes.d.mts +71 -0
  127. package/types/sohl/note-schemas.d.mts +7 -0
@@ -0,0 +1,254 @@
1
+ /*
2
+ * This file is part of the Song of Heroic Lands (SoHL) system for Foundry VTT.
3
+ * Copyright (c) 2024-2026 Tom Rodriguez ("Toasty") — <toasty@heroiclands.org>
4
+ *
5
+ * This work is licensed under the GNU General Public License v3.0 (GPLv3).
6
+ * You may copy, modify, and distribute it under the terms of that license.
7
+ *
8
+ * For full terms, see the LICENSE.md file in the project root or visit:
9
+ * https://www.gnu.org/licenses/gpl-3.0.html
10
+ *
11
+ * SPDX-License-Identifier: GPL-3.0-or-later
12
+ */
13
+
14
+ /**
15
+ * Which pack a note's document lands in, when a document type has more than
16
+ * one (#1566).
17
+ *
18
+ * A repository used to be able to ship exactly one pack per document type: the
19
+ * pipeline ran one compile pass per type and routed every note of that type
20
+ * into that pass's pack. Editorial grouping of same-type documents into
21
+ * separate compendiums is ordinary Foundry practice — "Core Spells" and
22
+ * "Expanded Spells" are two Item packs — and `sohl-kethira-basic` shipped
23
+ * three. Collapsing them is a breaking change for every existing world,
24
+ * because a compendium UUID carries its pack name
25
+ * (`Compendium.<pkg>.characteristics.Item.<id>`).
26
+ *
27
+ * **A note declares its pack; the configuration declares the default.** The
28
+ * routing key is one frontmatter field, `pack:`, naming a pack from the
29
+ * configured list — declarative and inspectable, so a maintainer reads where a
30
+ * note lands rather than tracing a predicate. Its `type:` still selects the
31
+ * *compiler*; the declaration selects *which pack of that type* receives the
32
+ * document, and the two are orthogonal.
33
+ *
34
+ * The rules, in full:
35
+ *
36
+ * - A note that declares nothing lands in the **default** pack of its document
37
+ * type. A type with exactly one pack is that type's default implicitly, which
38
+ * is what keeps every existing one-pack-per-type configuration — and every
39
+ * note in it — behaving identically.
40
+ * - A type with several packs designates its default with `default: true`.
41
+ * Where none does, a declaration is **mandatory** for every note of that type:
42
+ * an undeclared note routes nowhere and fails the build.
43
+ * - A declared name that no pack answers to — or that names a pack holding
44
+ * another document type, or a companion pack, which no note may address —
45
+ * fails the build, naming the note and what it asked for. A silent fall-back
46
+ * to the default would be #1502 in a new costume.
47
+ * - A note's **derived** documents are routed by the default of *their* type,
48
+ * not by the note's declaration: an item note's prose compiles into a
49
+ * JournalEntry, and `pack:` names where the *item* goes.
50
+ *
51
+ * @module
52
+ */
53
+
54
+ import { packForType } from "./ids.mjs";
55
+ import { loadPackConfig } from "./pack-config.mjs";
56
+
57
+ /**
58
+ * A note that cannot be routed to a pack. Thrown rather than returned so no
59
+ * caller can carry on with a plausible-looking default.
60
+ */
61
+ export class PackRoutingError extends Error {
62
+ /** @param {string} message */
63
+ constructor(message) {
64
+ super(message);
65
+ this.name = "PackRoutingError";
66
+ }
67
+ }
68
+
69
+ /**
70
+ * The frontmatter field a note declares its pack in.
71
+ *
72
+ * Deliberately close to `package:` and deliberately not the same word: a note's
73
+ * `package:` says which *distribution* owns it, `pack:` which *compendium*
74
+ * receives its document.
75
+ */
76
+ export const PACK_FIELD = "pack";
77
+
78
+ /**
79
+ * Build the router for one configured pack list.
80
+ *
81
+ * Pure — it reads the list and nothing else, so a consumer's routing can be
82
+ * tested without a content tree or a config file on disk.
83
+ *
84
+ * @param {readonly object[]} packs - The resolved `packs` list from
85
+ * `defineConfig`.
86
+ * @returns {{resolve: (fm: object, docType: string) => string,
87
+ * resolveOrNull: (fm: object, docType: string) => string|undefined,
88
+ * packsOfType: (docType: string) => string[],
89
+ * defaultOf: (docType: string) => string|undefined}} The router.
90
+ */
91
+ export function createPackRouter(packs) {
92
+ /** Routable packs — a companion is written by its parent's pass. */
93
+ const byName = new Map();
94
+ /** Companion names, so addressing one can be refused by name. */
95
+ const companions = new Set();
96
+ /** @type {Map<string, string[]>} */
97
+ const byType = new Map();
98
+ /** @type {Map<string, string>} */
99
+ const defaults = new Map();
100
+
101
+ for (const pack of packs ?? []) {
102
+ byName.set(pack.name, pack);
103
+ const names = byType.get(pack.type) ?? [];
104
+ names.push(pack.name);
105
+ byType.set(pack.type, names);
106
+ if (pack.default) defaults.set(pack.type, pack.name);
107
+ for (const companion of pack.companions ?? []) {
108
+ companions.add(companion.name);
109
+ }
110
+ }
111
+ // A type with exactly one pack needs no `default: true` to have one: the
112
+ // single-pack layout is the common case, and requiring the flag there would
113
+ // make every existing configuration invalid for no gain.
114
+ for (const [type, names] of byType) {
115
+ if (names.length === 1) defaults.set(type, names[0]);
116
+ }
117
+
118
+ /** @param {object} fm */
119
+ const noteLabel = (fm) =>
120
+ fm?.name?.full ?? fm?.shortcode ?? fm?.id ?? "a note";
121
+
122
+ /**
123
+ * The pack one pass should write this note's document to.
124
+ *
125
+ * @param {object} fm - The note's frontmatter.
126
+ * @param {string} docType - The document type the calling pass writes.
127
+ * @returns {string} The pack name.
128
+ * @throws {PackRoutingError} When the note routes nowhere.
129
+ */
130
+ function resolve(fm, docType) {
131
+ const declared = fm?.[PACK_FIELD];
132
+ // The declaration names where the note's *own* document goes. A pass
133
+ // writing a document derived from it — an item's prose becoming a
134
+ // JournalEntry — is not what the author was addressing.
135
+ const ownDocType = packForType(fm?.type).docType;
136
+
137
+ if (declared != null && declared !== "" && docType === ownDocType) {
138
+ if (companions.has(declared)) {
139
+ throw new PackRoutingError(
140
+ `${noteLabel(fm)} declares \`pack: ${declared}\`, which is a ` +
141
+ `companion pack. A companion is written by another pack's ` +
142
+ `pass, so no note may be routed into one.`,
143
+ );
144
+ }
145
+ const pack = byName.get(declared);
146
+ if (!pack) {
147
+ throw new PackRoutingError(
148
+ `${noteLabel(fm)} declares \`pack: ${declared}\`, which no ` +
149
+ `configured pack answers to. Declare it in ` +
150
+ `package-build.config.yaml, or correct the note. ` +
151
+ `Packs of type ${docType}: ` +
152
+ `${(byType.get(docType) ?? []).join(", ") || "(none)"}.`,
153
+ );
154
+ }
155
+ if (pack.type !== docType) {
156
+ throw new PackRoutingError(
157
+ `${noteLabel(fm)} is a ${docType} but declares ` +
158
+ `\`pack: ${declared}\`, which holds ${pack.type} ` +
159
+ `documents. A note's \`pack:\` names a pack of its own ` +
160
+ `document type.`,
161
+ );
162
+ }
163
+ return declared;
164
+ }
165
+
166
+ const fallback = defaults.get(docType);
167
+ if (!fallback) {
168
+ const candidates = byType.get(docType) ?? [];
169
+ throw new PackRoutingError(
170
+ candidates.length ?
171
+ `${noteLabel(fm)} declares no \`pack:\`, and no ${docType} ` +
172
+ `pack is marked \`default: true\` — so it routes ` +
173
+ `nowhere. Mark one of ${candidates.join(", ")} as the ` +
174
+ `default, or declare the pack on the note.`
175
+ : `${noteLabel(fm)} compiles into a ${docType}, but no pack ` +
176
+ `of that type is configured.`,
177
+ );
178
+ }
179
+ return fallback;
180
+ }
181
+
182
+ return {
183
+ resolve,
184
+
185
+ /**
186
+ * {@link resolve} for a caller that must not fail the build — the link
187
+ * index, which addresses every note it can and leaves the compilers to
188
+ * report the ones it cannot.
189
+ *
190
+ * @param {object} fm - The note's frontmatter.
191
+ * @param {string} docType - The document type being addressed.
192
+ * @returns {string|undefined} The pack name, or `undefined`.
193
+ */
194
+ resolveOrNull(fm, docType) {
195
+ try {
196
+ return resolve(fm, docType);
197
+ } catch {
198
+ return undefined;
199
+ }
200
+ },
201
+
202
+ /**
203
+ * Every routable pack of a document type, in configured order.
204
+ *
205
+ * @param {string} docType - The Foundry document type.
206
+ * @returns {string[]} The pack names.
207
+ */
208
+ packsOfType(docType) {
209
+ return [...(byType.get(docType) ?? [])];
210
+ },
211
+
212
+ /**
213
+ * The pack of a type that receives notes declaring none.
214
+ *
215
+ * @param {string} docType - The Foundry document type.
216
+ * @returns {string|undefined} The pack name, or `undefined` when the
217
+ * type has several packs and none is marked default.
218
+ */
219
+ defaultOf(docType) {
220
+ return defaults.get(docType);
221
+ },
222
+ };
223
+ }
224
+
225
+ const routers = new WeakMap();
226
+
227
+ /**
228
+ * The router for a resolved configuration, built once per configuration.
229
+ *
230
+ * @param {object} config - A configuration from `defineConfig`.
231
+ * @returns {ReturnType<typeof createPackRouter>} Its router.
232
+ */
233
+ export function routerFor(config) {
234
+ let router = routers.get(config);
235
+ if (!router) {
236
+ router = createPackRouter(config.packs);
237
+ routers.set(config, router);
238
+ }
239
+ return router;
240
+ }
241
+
242
+ /**
243
+ * The consuming repository's own router — what every module that emits a UUID
244
+ * asks where a note's document lives.
245
+ *
246
+ * An accessor rather than a hoisted constant, so that importing this module
247
+ * needs no configuration (#2). {@link routerFor} keeps one router per
248
+ * configuration object, so repeated calls return the same instance.
249
+ *
250
+ * @returns {ReturnType<typeof createPackRouter>} This repository's router.
251
+ */
252
+ export function packRouter() {
253
+ return routerFor(loadPackConfig());
254
+ }
@@ -0,0 +1,238 @@
1
+ /*
2
+ * This file is part of the Song of Heroic Lands (SoHL) system for Foundry VTT.
3
+ * Copyright (c) 2024-2026 Tom Rodriguez ("Toasty") — <toasty@heroiclands.org>
4
+ *
5
+ * This work is licensed under the GNU General Public License v3.0 (GPLv3).
6
+ * You may copy, modify, and distribute it under the terms of that license.
7
+ *
8
+ * For full terms, see the LICENSE.md file in the project root or visit:
9
+ * https://www.gnu.org/licenses/gpl-3.0.html
10
+ *
11
+ * SPDX-License-Identifier: GPL-3.0-or-later
12
+ */
13
+
14
+ /**
15
+ * The prose conventions every content repository writes to — one Prettier
16
+ * configuration and one markdownlint rule set, declared here so a note
17
+ * formatted in one repository is formatted the same way in the next (#69).
18
+ *
19
+ * These used to exist in exactly one consumer. The SoHL repository carried
20
+ * both; `sohl-thalorna` had Prettier but never ran it from `lint`; and
21
+ * `sohl-kethira-basic` had neither, so the package least likely to have been
22
+ * proofread was checked for addresses and nothing else. A rule set copied into
23
+ * three repositories is one rule with three implementations, which is the drift
24
+ * #20 exists to remove — so it is declared once, here, and every consumer
25
+ * invokes it.
26
+ *
27
+ * **Neither of these is an override.** A consumer that declares its own
28
+ * Prettier config or its own `.markdownlint-cli2.jsonc` wins; what ships here
29
+ * is the default a repository gets for declaring nothing. Repository *layout*
30
+ * knowledge — which paths to skip — stays with the repository that has the
31
+ * layout, in its own ignore files.
32
+ *
33
+ * @module
34
+ */
35
+
36
+ /**
37
+ * The Prettier options every content repository shares, before any per-language
38
+ * adjustment.
39
+ *
40
+ * The values are not arbitrary: they are the ones the SoHL repository has
41
+ * always used, kept identical here so a module or a note moving between
42
+ * repositories does not reformat on arrival. Changing one of these reformats
43
+ * every consumer, so treat it as a breaking change to the shared tree rather
44
+ * than a preference.
45
+ *
46
+ * @type {Readonly<object>}
47
+ */
48
+ export const PRETTIER_BASE = Object.freeze({
49
+ printWidth: 80,
50
+ tabWidth: 4,
51
+ useTabs: false,
52
+ semi: true,
53
+ singleQuote: false,
54
+ quoteProps: "as-needed",
55
+ trailingComma: "all",
56
+ bracketSpacing: true,
57
+ bracketSameLine: true,
58
+ arrowParens: "always",
59
+ endOfLine: "lf",
60
+ experimentalTernaries: true,
61
+ });
62
+
63
+ /**
64
+ * What markdown gets on top of {@link PRETTIER_BASE}.
65
+ *
66
+ * Markdown indents at 2, not the global 4. Notes are the thing several
67
+ * repositories exchange, so their indentation is the one value that most needs
68
+ * to be the same everywhere — a note's YAML frontmatter is nested lists, and at
69
+ * 4 every note reindents away from the form it was written in.
70
+ *
71
+ * **Declared apart from the `overrides` block, not derived from it.** Prettier
72
+ * applies `overrides` only while resolving a config *file*; options handed to
73
+ * it directly keep the global values, so a consumer with no config of its own
74
+ * silently got markdown at 4 (#76). The runner needs the adjustment as data it
75
+ * can apply itself, and {@link PRETTIER_CONFIG} composes the same values into
76
+ * the shape a config file wants — one source, two presentations.
77
+ *
78
+ * @type {Readonly<object>}
79
+ */
80
+ export const PRETTIER_MARKDOWN = Object.freeze({ tabWidth: 2 });
81
+
82
+ /**
83
+ * The shared configuration in the shape a Prettier **config file** takes.
84
+ *
85
+ * This is what a consumer's `prettier.config.mjs` re-exports, and it is the
86
+ * form in which the markdown adjustment works: resolved from the consumer's own
87
+ * root, `**\/*.md` matches that repository's markdown. Shipped from inside
88
+ * `node_modules` it would match nothing, because Prettier resolves an
89
+ * override's glob relative to the config file's own directory — which is why
90
+ * the runner applies {@link PRETTIER_MARKDOWN} itself rather than pointing
91
+ * Prettier at this file.
92
+ *
93
+ * @type {Readonly<object>}
94
+ */
95
+ export const PRETTIER_CONFIG = Object.freeze({
96
+ ...PRETTIER_BASE,
97
+ overrides: Object.freeze([
98
+ Object.freeze({
99
+ files: "**/*.md",
100
+ options: PRETTIER_MARKDOWN,
101
+ }),
102
+ ]),
103
+ });
104
+
105
+ /**
106
+ * The shared options for one file, with the per-language adjustment applied.
107
+ *
108
+ * What a consumer's own Prettier config would have produced, for a repository
109
+ * that declares none.
110
+ *
111
+ * @param {string} file - Path of the file about to be formatted.
112
+ * @returns {object} Options to hand Prettier directly. Never carries
113
+ * `overrides`: passing that inline is what silently did nothing (#76).
114
+ */
115
+ export function sharedPrettierOptionsFor(file) {
116
+ return /\.md$/i.test(file) ?
117
+ { ...PRETTIER_BASE, ...PRETTIER_MARKDOWN }
118
+ : { ...PRETTIER_BASE };
119
+ }
120
+
121
+ /**
122
+ * The markdownlint rules — the structural checks Prettier cannot make.
123
+ *
124
+ * Prettier already formats every hand-written `.md` file, and it is indifferent
125
+ * to structure: it will happily reformat a document whose heading levels skip
126
+ * from h3 to h5, whose two sibling sections claim the same anchor, or whose
127
+ * link is `(text)[url]`. Those are the defects this set is for.
128
+ *
129
+ * **THE RULE SET IS DELIBERATELY NARROW, and stays that way on purpose.**
130
+ * Turning on markdownlint's defaults over a content tree produces tens of
131
+ * thousands of findings, almost all of them line length, list indentation and
132
+ * blank-line placement — Prettier's territory, or a second formatter's taste
133
+ * imposed on prose it already owns. So `default` is off and each rule below is
134
+ * enabled by name, with the reason it earns its place. Add a rule only if it
135
+ * can report that a page is *wrong*.
136
+ *
137
+ * @type {Readonly<object>}
138
+ */
139
+ export const MARKDOWNLINT_CONFIG = Object.freeze({
140
+ default: false,
141
+
142
+ /* ── Document structure ─────────────────────────────────────────────── */
143
+
144
+ // MD001 — heading levels increment by one. A skipped level (h3 → h5) breaks
145
+ // the outline every consumer derives from it: a knowledgebase's on-page
146
+ // table of contents, a section nav, and the reading order of a page nobody
147
+ // re-reads top to bottom.
148
+ MD001: true,
149
+
150
+ // MD024 — no duplicate sibling headings. Two identical headings under one
151
+ // parent generate two identical anchors and the second is unreachable, so a
152
+ // `#fragment` link silently lands on the wrong section. Content notes link
153
+ // by anchor everywhere and this package validates those links, which cannot
154
+ // help when the anchor exists but points at the wrong one of two.
155
+ //
156
+ // `siblings_only` is what makes the rule usable: repeating "## Notes" under
157
+ // each of several parents is a normal shape for a reference page, and only
158
+ // a repeat within one parent is ambiguous.
159
+ MD024: Object.freeze({ siblings_only: true }),
160
+
161
+ // MD056 — every table row has the column count its header declares. A row
162
+ // with too few or too many cells renders as a broken table, and the failure
163
+ // is invisible in the source.
164
+ MD056: true,
165
+
166
+ /* ── Links that do not link ─────────────────────────────────────────── */
167
+
168
+ // MD011 — reversed link syntax: `(text)[url]` renders as literal text with
169
+ // no link at all. A pure typo class, and easy to miss in review.
170
+ MD011: true,
171
+
172
+ // MD034 — no bare URLs. Whether a bare URL becomes a link depends on the
173
+ // renderer, and this content is rendered by three of them (Hugo/goldmark
174
+ // for a knowledgebase, Foundry's own markdown for compendium journals, and
175
+ // GitHub for the repository pages). Writing the link explicitly is the only
176
+ // form that renders the same everywhere.
177
+ MD034: true,
178
+
179
+ // MD039 — no spaces inside link text. `[ Skills ](url)` carries the spaces
180
+ // into the rendered anchor, which shows up as a stray underline and as a
181
+ // mismatched link title in any index built from it.
182
+ MD039: true,
183
+
184
+ // MD042 — no empty links. `[Skills]()` renders as a dead anchor that looks
185
+ // exactly like a working one.
186
+ MD042: true,
187
+
188
+ // MD052 / MD053 — reference links resolve, and definitions are used. An
189
+ // undefined reference renders as literal `[text][ref]` brackets; an orphaned
190
+ // definition is a link someone meant to make and did not.
191
+ MD052: true,
192
+ MD053: true,
193
+
194
+ /* ── Emphasis markers ───────────────────────────────────────────────── */
195
+
196
+ // MD049 / MD050 — one marker for each kind of emphasis: `_emphasis_` and
197
+ // `**strong**`. Both are style rules rather than correctness ones, and both
198
+ // are already satisfied wherever Prettier runs, because Prettier normalises
199
+ // to exactly this pair. That is the point of enabling them: the convention
200
+ // otherwise holds as a *side effect* of the formatter's default, so it would
201
+ // lapse silently if that default changed or a path were added to an ignore
202
+ // file. Stated here, it is a decision rather than an accident — and it is
203
+ // the one rule a repository with no Prettier in its lint chain was missing
204
+ // entirely.
205
+ //
206
+ // Note for anyone reading rendered output: neither marker is an underline.
207
+ // Markdown has no underline; `_x_` and `*x*` both mean emphasis, and the
208
+ // choice between them is purely which one these repositories write.
209
+ MD049: Object.freeze({ style: "underscore" }),
210
+ MD050: Object.freeze({ style: "asterisk" }),
211
+ });
212
+
213
+ /**
214
+ * The globs `content-build markdown` checks when a consumer names no paths.
215
+ *
216
+ * Every markdown file the repository tracks, which is what a consumer means by
217
+ * "lint my markdown". What to *skip* is the repository's own business and comes
218
+ * from its `.gitignore` (honoured by default) and its own configuration.
219
+ *
220
+ * @type {readonly string[]}
221
+ */
222
+ export const MARKDOWN_GLOBS = Object.freeze(["**/*.md"]);
223
+
224
+ /**
225
+ * Markdown every consumer has and nobody hand-writes.
226
+ *
227
+ * `CHANGELOG.md` is assembled by `changeset version` from the `.changeset/*.md`
228
+ * bodies on every release. It is not in anyone's `.gitignore` — it is
229
+ * committed — so nothing else excludes it, and linting it reports on the
230
+ * generator: the findings are real (a heading level changesets chose, an
231
+ * asterisk it emitted) and unfixable, because the next release rewrites the
232
+ * file. Every repository here releases through changesets, so this is a fact
233
+ * about the shared toolchain rather than any one repository's layout, and it
234
+ * belongs in the default instead of being rediscovered three times.
235
+ *
236
+ * @type {readonly string[]}
237
+ */
238
+ export const MARKDOWN_IGNORES = Object.freeze(["CHANGELOG.md"]);