@heroiclands/package-build 17.1.0 → 18.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.
- package/CHANGELOG.md +1773 -0
- package/CONTENT.md +288 -72
- package/README.md +1 -1
- package/bin/content-build.mjs +218 -137
- package/bin/package-build.mjs +43 -0
- package/content-config.mjs +62 -67
- package/docs/content-format.md +768 -58
- package/engine/actor-compiler.mjs +586 -0
- package/engine/address-charset.mjs +11 -8
- package/engine/address-diff.mjs +266 -22
- package/engine/anchored-sections.mjs +83 -0
- package/engine/anchors.mjs +83 -0
- package/engine/base-compiler.mjs +148 -18
- package/engine/bundle-notes.mjs +276 -0
- package/engine/bundles.mjs +307 -0
- package/engine/code-fences.mjs +103 -0
- package/engine/compile-corpus.mjs +89 -0
- package/engine/content-address.mjs +267 -5
- package/engine/content-format-check.mjs +54 -11
- package/engine/content-format.mjs +37 -5
- package/engine/content-index.mjs +394 -111
- package/engine/content-links.mjs +185 -77
- package/engine/content-lint.mjs +232 -14
- package/engine/content-package.mjs +2 -1
- package/engine/content-tables.mjs +99 -3
- package/engine/document-subtypes.mjs +49 -6
- package/engine/field-reference.mjs +1 -1
- package/engine/field-spec.mjs +56 -6
- package/engine/folder-notes.mjs +470 -0
- package/engine/foreign-catalog.mjs +195 -4
- package/engine/{manifest-emit.mjs → foundry-entries.mjs} +78 -81
- package/engine/frontmatter-lint.mjs +245 -19
- package/engine/frontmatter.mjs +88 -8
- package/engine/generate.mjs +304 -13
- package/engine/helpers.mjs +293 -64
- package/engine/ids.mjs +137 -2
- package/engine/index-records.mjs +126 -0
- package/engine/index.mjs +23 -5
- package/engine/item-compiler.mjs +349 -0
- package/engine/item-docs.mjs +7 -1
- package/engine/item-registry.mjs +6 -0
- package/engine/journals.mjs +93 -27
- package/engine/macros.mjs +4 -2
- package/engine/metadata-index.mjs +495 -0
- package/engine/note-claims.mjs +146 -25
- package/engine/note-ids.mjs +112 -0
- package/engine/note-renames.mjs +134 -0
- package/engine/note-schemas.mjs +25 -0
- package/engine/note-vocabulary.mjs +110 -27
- package/engine/pack-config.mjs +4 -0
- package/engine/pack-router.mjs +23 -0
- package/engine/retired-fields.mjs +137 -3
- package/engine/scenes.mjs +66 -34
- package/engine/schema-check.mjs +58 -17
- package/engine/site-build.mjs +71 -12
- package/engine/site-index.mjs +20 -7
- package/engine/sql-tables.mjs +485 -0
- package/engine/subtype-registry.mjs +102 -0
- package/engine/system-block.mjs +96 -12
- package/engine/systems.mjs +201 -0
- package/engine/web-wikilinks.mjs +50 -20
- package/engine/wikilink-syntax.mjs +17 -8
- package/engine/wikilinks.mjs +124 -51
- package/engine/yaml-lint.mjs +282 -0
- package/hm3/actors.mjs +352 -0
- package/hm3/default-item-art.mjs +75 -0
- package/hm3/document-subtypes.mjs +134 -0
- package/hm3/index.mjs +56 -0
- package/hm3/item-builders.mjs +84 -0
- package/hm3/item-fields.mjs +180 -0
- package/hm3/items.mjs +96 -0
- package/hm3/template-priority.mjs +85 -0
- package/labels.mjs +191 -0
- package/manifest.mjs +43 -2
- package/package.json +19 -4
- package/release.mjs +62 -7
- package/sohl/actors.mjs +33 -487
- package/sohl/being-info.mjs +16 -7
- package/sohl/default-item-art.mjs +14 -3
- package/sohl/document-subtypes.mjs +16 -10
- package/sohl/item-builders.mjs +14 -5
- package/sohl/item-fields.mjs +68 -7
- package/sohl/items.mjs +44 -258
- package/sohl/note-schemas.mjs +2 -2
- package/types/content-config.d.mts +21 -42
- package/types/engine/actor-compiler.d.mts +204 -0
- package/types/engine/address-charset.d.mts +11 -8
- package/types/engine/address-diff.d.mts +53 -5
- package/types/engine/anchored-sections.d.mts +21 -0
- package/types/engine/anchors.d.mts +20 -0
- package/types/engine/base-compiler.d.mts +17 -17
- package/types/engine/bundle-notes.d.mts +173 -0
- package/types/engine/bundles.d.mts +60 -0
- package/types/engine/code-fences.d.mts +43 -0
- package/types/engine/compile-corpus.d.mts +32 -0
- package/types/engine/content-address.d.mts +205 -5
- package/types/engine/content-format-check.d.mts +6 -2
- package/types/engine/content-format.d.mts +57 -1
- package/types/engine/content-index.d.mts +64 -80
- package/types/engine/content-links.d.mts +52 -7
- package/types/engine/content-lint.d.mts +10 -1
- package/types/engine/content-package.d.mts +2 -1
- package/types/engine/content-tables.d.mts +21 -39
- package/types/engine/document-subtypes.d.mts +37 -3
- package/types/engine/field-spec.d.mts +76 -5
- package/types/engine/folder-notes.d.mts +159 -0
- package/types/engine/foreign-catalog.d.mts +53 -0
- package/types/engine/{manifest-emit.d.mts → foundry-entries.d.mts} +16 -38
- package/types/engine/frontmatter-lint.d.mts +10 -2
- package/types/engine/frontmatter.d.mts +64 -0
- package/types/engine/generate.d.mts +38 -0
- package/types/engine/helpers.d.mts +94 -30
- package/types/engine/ids.d.mts +96 -0
- package/types/engine/index-records.d.mts +68 -0
- package/types/engine/index.d.mts +9 -3
- package/types/engine/item-compiler.d.mts +131 -0
- package/types/engine/journals.d.mts +47 -9
- package/types/engine/metadata-index.d.mts +226 -0
- package/types/engine/note-claims.d.mts +57 -11
- package/types/engine/note-ids.d.mts +38 -0
- package/types/engine/note-renames.d.mts +102 -0
- package/types/engine/note-vocabulary.d.mts +44 -8
- package/types/engine/retired-fields.d.mts +75 -0
- package/types/engine/scenes.d.mts +3 -2
- package/types/engine/schema-check.d.mts +25 -4
- package/types/engine/site-build.d.mts +4 -4
- package/types/engine/site-index.d.mts +1 -1
- package/types/engine/sql-tables.d.mts +185 -0
- package/types/engine/subtype-registry.d.mts +49 -0
- package/types/engine/system-block.d.mts +40 -1
- package/types/engine/systems.d.mts +106 -0
- package/types/engine/web-wikilinks.d.mts +4 -2
- package/types/engine/wikilink-syntax.d.mts +10 -3
- package/types/engine/wikilinks.d.mts +41 -13
- package/types/engine/yaml-lint.d.mts +107 -0
- package/types/hm3/actors.d.mts +48 -0
- package/types/hm3/default-item-art.d.mts +42 -0
- package/types/hm3/document-subtypes.d.mts +24 -0
- package/types/hm3/index.d.mts +7 -0
- package/types/hm3/item-builders.d.mts +11 -0
- package/types/hm3/item-fields.d.mts +12 -0
- package/types/hm3/items.d.mts +23 -0
- package/types/hm3/template-priority.d.mts +21 -0
- package/types/manifest.d.mts +22 -1
- package/types/release.d.mts +9 -4
- package/types/sohl/actors.d.mts +7 -74
- package/types/sohl/being-info.d.mts +8 -27
- package/types/sohl/default-item-art.d.mts +5 -3
- package/types/sohl/items.d.mts +17 -32
- package/engine/foreign-manifests.mjs +0 -126
- package/engine/kb-manifest.mjs +0 -490
- package/types/engine/foreign-manifests.d.mts +0 -43
- package/types/engine/kb-manifest.d.mts +0 -241
package/engine/note-claims.mjs
CHANGED
|
@@ -61,15 +61,21 @@
|
|
|
61
61
|
* @module
|
|
62
62
|
*/
|
|
63
63
|
|
|
64
|
-
import {
|
|
65
|
-
|
|
64
|
+
import { assertSuppliedCorpus } from "./helpers.mjs";
|
|
65
|
+
// The record accessors only: this module is imported by the content index, so
|
|
66
|
+
// importing the index back would close a cycle (#243).
|
|
67
|
+
import { authoredFrontmatter, isNoteRecord, noteFile } from "./index-records.mjs";
|
|
68
|
+
import { JOURNAL_TYPES, MAP_TYPES, PACK_BY_TYPE, RETIRED_TYPES, currentType } from "./ids.mjs";
|
|
66
69
|
import { itemTypes } from "./item-registry.mjs";
|
|
67
70
|
import { docEntryTypes } from "./item-docs.mjs";
|
|
68
71
|
import { loadPackConfig } from "./pack-config.mjs";
|
|
69
72
|
import { locateFrontmatterKey } from "./retired-fields.mjs";
|
|
70
73
|
import { noteTypesFor, subtypeRow } from "./document-subtypes.mjs";
|
|
74
|
+
import { KNOWN_DOCUMENT_SUBTYPE_MAPS } from "./subtype-registry.mjs";
|
|
71
75
|
import { HOMEPAGE_TYPE } from "./homepage.mjs";
|
|
72
|
-
import {
|
|
76
|
+
import { FOLDER_TYPE } from "./folder-notes.mjs";
|
|
77
|
+
import { BUNDLE_TYPE } from "./bundle-notes.mjs";
|
|
78
|
+
import { NOTE_VOCABULARY } from "./note-vocabulary.mjs";
|
|
73
79
|
|
|
74
80
|
/**
|
|
75
81
|
* Note types that compile into **no compendium document, by design**.
|
|
@@ -84,19 +90,54 @@ import { SOHL_DOCUMENT_SUBTYPES } from "../sohl/document-subtypes.mjs";
|
|
|
84
90
|
export const NEVER_PACKED_TYPES = Object.freeze(new Set([HOMEPAGE_TYPE]));
|
|
85
91
|
|
|
86
92
|
/**
|
|
87
|
-
*
|
|
93
|
+
* Content types the specification states and this toolchain does not yet
|
|
94
|
+
* compile.
|
|
95
|
+
*
|
|
96
|
+
* **Stated, never inferred, and that distinction is the whole point.** An
|
|
97
|
+
* unimplemented type and a type somebody forgot to route look identical from
|
|
98
|
+
* the outside: both are documented, both validate, and neither reaches a pass.
|
|
99
|
+
* The only thing separating them is intent, so intent is written down here.
|
|
100
|
+
*
|
|
101
|
+
* Inferring it — "declared, but absent from the configured vocabulary" — reads
|
|
102
|
+
* correctly and is worthless, because the configured vocabulary is *derived
|
|
103
|
+
* from the routing*. Take a type's route away and it leaves the vocabulary too,
|
|
104
|
+
* so the inference excuses precisely the mistake it was meant to catch. That is
|
|
105
|
+
* not hypothetical: it is #241, where `place`, `lore` and `scenario` were
|
|
106
|
+
* declared, validated and unrouted, and every gate reported success until a
|
|
107
|
+
* downstream repository failed on 450 notes.
|
|
108
|
+
*
|
|
109
|
+
* A type leaves this set when it is implemented, the way `bundle` did in #259.
|
|
110
|
+
* The membership is asserted, so it cannot be forgotten in either direction.
|
|
88
111
|
*
|
|
89
|
-
*
|
|
90
|
-
|
|
112
|
+
* @type {ReadonlySet<string>}
|
|
113
|
+
*/
|
|
114
|
+
export const UNIMPLEMENTED_TYPES = Object.freeze(new Set(["vehicle"]));
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Note types that reach a pack by a route **other than the pack router**.
|
|
118
|
+
*
|
|
119
|
+
* A folder is the only one, and it is not unclaimed: it compiles to a real
|
|
120
|
+
* `Folder` document. What it has no answer to is *which* pack claims it, because
|
|
121
|
+
* that is not a property of the note — a folder materialises in every pack
|
|
122
|
+
* holding a document that references it, and its ancestors with it (#257). So
|
|
123
|
+
* it is exempt from the claim check for the opposite reason a homepage is:
|
|
124
|
+
* a homepage is in no pack, and a folder may be in several.
|
|
125
|
+
*
|
|
126
|
+
* @type {ReadonlySet<string>}
|
|
127
|
+
*/
|
|
128
|
+
export const DERIVED_PACKED_TYPES = Object.freeze(new Set([FOLDER_TYPE]));
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* The note-type → document-subtype maps this toolchain ships.
|
|
91
132
|
*
|
|
92
|
-
*
|
|
93
|
-
* has
|
|
94
|
-
*
|
|
95
|
-
*
|
|
133
|
+
* Declared in {@link module:engine/subtype-registry} and re-exported here,
|
|
134
|
+
* where it has always been read from. It moved to a leaf in #270 so that
|
|
135
|
+
* `helpers.mjs` could reach it: this module imports `walkMarkdownTree` from
|
|
136
|
+
* there, so a dependency the other way would have closed a cycle.
|
|
96
137
|
*
|
|
97
138
|
* @type {readonly import("./document-subtypes.mjs").DocumentSubtypeMap[]}
|
|
98
139
|
*/
|
|
99
|
-
export
|
|
140
|
+
export { KNOWN_DOCUMENT_SUBTYPE_MAPS } from "./subtype-registry.mjs";
|
|
100
141
|
|
|
101
142
|
/**
|
|
102
143
|
* What a claim question is asked against.
|
|
@@ -166,7 +207,10 @@ function mappingSystems(maps, type) {
|
|
|
166
207
|
* pack from appearing to answer for any note.
|
|
167
208
|
*
|
|
168
209
|
* @param {string} docType - The Foundry document type a pack holds.
|
|
169
|
-
* @param {ClaimSources} [sources] - What to answer from.
|
|
210
|
+
* @param {ClaimSources} [sources] - What to answer from.
|
|
211
|
+
* @param {object} [opts] - Options.
|
|
212
|
+
* @param {readonly object[]} [opts.records] - The corpus, derived once by the
|
|
213
|
+
* compile and handed in — required, for the reason above (#243). Defaults to the
|
|
170
214
|
* configured registries and the systems this toolchain ships.
|
|
171
215
|
* @returns {ReadonlySet<string>} The note types such a pass would claim.
|
|
172
216
|
*/
|
|
@@ -188,15 +232,21 @@ export function noteTypesClaimedBy(docType, sources) {
|
|
|
188
232
|
// The actors pass: every note type a system sends to an `Actor`.
|
|
189
233
|
case "Actor":
|
|
190
234
|
return Object.freeze(new Set(maps.flatMap((map) => noteTypesFor(map, "Actor"))));
|
|
191
|
-
// The journals pass:
|
|
192
|
-
// item's, a macro's and a map note's
|
|
193
|
-
// JournalEntry of their own.
|
|
235
|
+
// The journals pass: every type whose whole document is a journal,
|
|
236
|
+
// plus every doc-carrying type — an item's, a macro's and a map note's
|
|
237
|
+
// description each compile into a JournalEntry of their own.
|
|
194
238
|
case "JournalEntry":
|
|
195
|
-
return Object.freeze(new Set([
|
|
239
|
+
return Object.freeze(new Set([...JOURNAL_TYPES, ...docs]));
|
|
196
240
|
case "Macro":
|
|
197
241
|
return Object.freeze(new Set(["macro"]));
|
|
198
242
|
case "Scene":
|
|
199
243
|
return Object.freeze(new Set(MAP_TYPES));
|
|
244
|
+
// The bundles pass: an Adventure is what a `bundle` note compiles into
|
|
245
|
+
// (#259). A **prebuilt** Adventure pack still claims nothing —
|
|
246
|
+
// {@link claimedNoteTypes} passes over it, because no note is routed
|
|
247
|
+
// into a pack whose JSON is checked in rather than compiled.
|
|
248
|
+
case "Adventure":
|
|
249
|
+
return Object.freeze(new Set([BUNDLE_TYPE]));
|
|
200
250
|
default:
|
|
201
251
|
return Object.freeze(new Set());
|
|
202
252
|
}
|
|
@@ -209,14 +259,27 @@ export function noteTypesClaimedBy(docType, sources) {
|
|
|
209
259
|
* is claimed — which is what keeps a type deliberately unmapped for one system,
|
|
210
260
|
* and claimed for another, silent (#79).
|
|
211
261
|
*
|
|
262
|
+
* **A prebuilt pack claims nothing.** Its per-document JSON is checked in
|
|
263
|
+
* rather than compiled, so it has no pass and no note is routed into one —
|
|
264
|
+
* which `content-config.mjs` already states by refusing `default: true`
|
|
265
|
+
* alongside `prebuilt`. Counting it would tell an author their note is claimed
|
|
266
|
+
* by a pack that will never look at it. Before #259 the point could not arise:
|
|
267
|
+
* the only prebuilt pack in the wild is `harn-adventures`'s Adventure pack, and
|
|
268
|
+
* no compiler was registered for that document type, so the row answered for
|
|
269
|
+
* nothing whatever it was asked. Now one is.
|
|
270
|
+
*
|
|
212
271
|
* @param {object} [config] - The resolved build configuration. Defaults to this
|
|
213
272
|
* repository's.
|
|
214
273
|
* @param {ClaimSources} [sources] - What to answer from.
|
|
274
|
+
* @param {object} [opts] - Options.
|
|
275
|
+
* @param {readonly object[]} [opts.records] - The corpus, derived once by the
|
|
276
|
+
* compile and handed in — required, for the reason above (#243).
|
|
215
277
|
* @returns {ReadonlySet<string>} The claimed note types.
|
|
216
278
|
*/
|
|
217
279
|
export function claimedNoteTypes(config = loadPackConfig(), sources) {
|
|
218
280
|
const claimed = new Set();
|
|
219
281
|
for (const pack of config.packs ?? []) {
|
|
282
|
+
if (pack.prebuilt) continue;
|
|
220
283
|
for (const type of noteTypesClaimedBy(pack.type, sources)) claimed.add(type);
|
|
221
284
|
}
|
|
222
285
|
return Object.freeze(claimed);
|
|
@@ -232,6 +295,9 @@ export function claimedNoteTypes(config = loadPackConfig(), sources) {
|
|
|
232
295
|
* declare on top.
|
|
233
296
|
*
|
|
234
297
|
* @param {ClaimSources} [sources] - What to answer from.
|
|
298
|
+
* @param {object} [opts] - Options.
|
|
299
|
+
* @param {readonly object[]} [opts.records] - The corpus, derived once by the
|
|
300
|
+
* compile and handed in — required, for the reason above (#243).
|
|
235
301
|
* @returns {ReadonlySet<string>} The vocabulary.
|
|
236
302
|
*/
|
|
237
303
|
export function noteTypeVocabulary(sources) {
|
|
@@ -284,14 +350,18 @@ function article(word) {
|
|
|
284
350
|
* @returns {string} The message.
|
|
285
351
|
*/
|
|
286
352
|
function configurationMessage(type, config, sources) {
|
|
353
|
+
// Every table below is keyed by the current spelling of a note type; the
|
|
354
|
+
// message quotes the authored one, which is what the reader has in front of
|
|
355
|
+
// them (#78).
|
|
356
|
+
const current = currentType(type);
|
|
287
357
|
const documents = mappedDocuments(sources.maps, type);
|
|
288
|
-
if (!documents.length && sources.itemTypes.has(
|
|
289
|
-
if (!documents.length && PACK_BY_TYPE[
|
|
358
|
+
if (!documents.length && sources.itemTypes.has(current)) documents.push("Item");
|
|
359
|
+
if (!documents.length && PACK_BY_TYPE[current]) documents.push(PACK_BY_TYPE[current].docType);
|
|
290
360
|
|
|
291
361
|
const systems = mappingSystems(sources.maps, type);
|
|
292
362
|
const configured = new Set((config.packs ?? []).map((pack) => pack.type));
|
|
293
363
|
const packless = documents.filter((document) => !configured.has(document));
|
|
294
|
-
const needsBuilder = documents.includes("Item") && !sources.itemTypes.has(
|
|
364
|
+
const needsBuilder = documents.includes("Item") && !sources.itemTypes.has(current);
|
|
295
365
|
|
|
296
366
|
const into = documents.map((document) => `${article(document)} ${document}`).join(" or ");
|
|
297
367
|
const becomes =
|
|
@@ -322,6 +392,32 @@ function configurationMessage(type, config, sources) {
|
|
|
322
392
|
);
|
|
323
393
|
}
|
|
324
394
|
|
|
395
|
+
/**
|
|
396
|
+
* The **specification** finding: the format states the type, nothing compiles it.
|
|
397
|
+
*
|
|
398
|
+
* A third thing that can be wrong, and the only one that is not the author's
|
|
399
|
+
* fault. `docs/content-format.md` documents the type and the vocabulary declares
|
|
400
|
+
* its properties, so a note written against the published specification is
|
|
401
|
+
* correct — this toolchain simply has not implemented it yet.
|
|
402
|
+
*
|
|
403
|
+
* It earns its own message because the other two would both mislead here.
|
|
404
|
+
* Naming a missing pack or registry sends an author to
|
|
405
|
+
* `package-build.config.yaml`, where nothing they can write will help; saying
|
|
406
|
+
* the type is unknown flatly contradicts the specification they read it in.
|
|
407
|
+
*
|
|
408
|
+
* @param {string} type - The note's declared `type`.
|
|
409
|
+
* @returns {string} The message.
|
|
410
|
+
*/
|
|
411
|
+
function specifiedMessage(type) {
|
|
412
|
+
return (
|
|
413
|
+
`no configured pack claims a note of type "${type}", so it compiles ` +
|
|
414
|
+
`into nothing. The content format specifies "${type}", so the note is ` +
|
|
415
|
+
`not wrong — this toolchain has not implemented the type yet. Nothing ` +
|
|
416
|
+
`in this repository's configuration will change that; do not author ` +
|
|
417
|
+
`the type until a release compiles it.`
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
|
|
325
421
|
/**
|
|
326
422
|
* The **authoring** finding: nothing anywhere knows the type.
|
|
327
423
|
*
|
|
@@ -351,22 +447,41 @@ function authoringMessage(type) {
|
|
|
351
447
|
* @param {object} [config] - The resolved build configuration. Defaults to this
|
|
352
448
|
* repository's.
|
|
353
449
|
* @param {ClaimSources} [sources] - What to answer from.
|
|
450
|
+
* @param {object} [opts] - Options.
|
|
451
|
+
* @param {readonly object[]} [opts.records] - The corpus, derived once by the
|
|
452
|
+
* compile and handed in — required, for the reason above (#243).
|
|
354
453
|
* @returns {Array<{file: string, line?: number, column?: number,
|
|
355
454
|
* severity: "error", message: string, type: string}>} One finding per note.
|
|
356
455
|
*/
|
|
357
|
-
export function unclaimedNoteFindings(config = loadPackConfig(), sources) {
|
|
456
|
+
export function unclaimedNoteFindings(config = loadPackConfig(), sources, { records } = {}) {
|
|
358
457
|
const resolved = resolveSources(sources);
|
|
359
458
|
const claimed = claimedNoteTypes(config, resolved);
|
|
360
459
|
const vocabulary = noteTypeVocabulary(resolved);
|
|
361
460
|
const findings = [];
|
|
362
461
|
|
|
363
|
-
|
|
462
|
+
// The corpus this compile derived once (#243), required rather than
|
|
463
|
+
// derived here: this module is imported *by* the content index, so it
|
|
464
|
+
// could not derive one without closing a cycle — and the caller that wants
|
|
465
|
+
// this answer is running a compile and already holds it.
|
|
466
|
+
assertSuppliedCorpus(records, "unclaimedNoteFindings");
|
|
467
|
+
|
|
468
|
+
for (const record of records) {
|
|
469
|
+
if (!isNoteRecord(record)) continue;
|
|
470
|
+
const fm = authoredFrontmatter(record);
|
|
471
|
+
const absPath = noteFile(config.paths.content, record);
|
|
364
472
|
if (!fm) continue;
|
|
365
473
|
const type = typeof fm.type === "string" ? fm.type.trim() : "";
|
|
366
474
|
if (!type) continue;
|
|
367
475
|
if (NEVER_PACKED_TYPES.has(type)) continue;
|
|
476
|
+
if (DERIVED_PACKED_TYPES.has(type)) continue;
|
|
368
477
|
if (RETIRED_TYPES[type]) continue;
|
|
369
|
-
|
|
478
|
+
// A **renamed** spelling is a live type, not an unknown one: it resolves
|
|
479
|
+
// to the same row, the same registry entry and the same pack. So the
|
|
480
|
+
// claim is asked of the current spelling while the finding quotes the
|
|
481
|
+
// authored one (#78). The rename itself is reported by the frontmatter
|
|
482
|
+
// lint, which can say what to write instead.
|
|
483
|
+
const current = currentType(type);
|
|
484
|
+
if (claimed.has(current)) continue;
|
|
370
485
|
|
|
371
486
|
findings.push({
|
|
372
487
|
file: absPath,
|
|
@@ -374,9 +489,15 @@ export function unclaimedNoteFindings(config = loadPackConfig(), sources) {
|
|
|
374
489
|
severity: /** @type {"error"} */ ("error"),
|
|
375
490
|
type,
|
|
376
491
|
message:
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
492
|
+
// The unimplemented set is asked *first*: such a type is
|
|
493
|
+
// absent from the configured vocabulary precisely because
|
|
494
|
+
// nothing routes it, so the `vocabulary.has` branch would never
|
|
495
|
+
// reach it — and reading its absence as the reason is the
|
|
496
|
+
// inference {@link UNIMPLEMENTED_TYPES} exists to replace.
|
|
497
|
+
UNIMPLEMENTED_TYPES.has(current) ? specifiedMessage(type)
|
|
498
|
+
: vocabulary.has(current) ? configurationMessage(type, config, resolved)
|
|
499
|
+
: Object.hasOwn(NOTE_VOCABULARY, current) ? specifiedMessage(type)
|
|
500
|
+
: authoringMessage(type),
|
|
380
501
|
});
|
|
381
502
|
}
|
|
382
503
|
return findings;
|
|
@@ -0,0 +1,112 @@
|
|
|
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 id a note's document is filed under, and the one rule that decides it.
|
|
16
|
+
*
|
|
17
|
+
* A note used to author its `id` — an opaque 16-character string, 6,343 of them
|
|
18
|
+
* across the four content trees, each a *second* identity for a thing that
|
|
19
|
+
* already had one. The note's canonical address (`sohl-none-miscgear-bowlcer`)
|
|
20
|
+
* says everything the id said, is readable, and is the identity `content-lint`
|
|
21
|
+
* already guards: a duplicate address is a build error, while a duplicate `id`
|
|
22
|
+
* was checked nowhere. So the derived id inherits a guarantee, where the
|
|
23
|
+
* authored one had none (#270).
|
|
24
|
+
*
|
|
25
|
+
* **An authored `id` always wins.** That is the pattern the map compiler
|
|
26
|
+
* already uses and documents — `regionDocId(sceneId, key, pinned)` returns
|
|
27
|
+
* `pinned || makeId(…)` — and it is the correction lever an author needs when a
|
|
28
|
+
* document must keep its identity across a shortcode rename.
|
|
29
|
+
*
|
|
30
|
+
* **This is one function because every pass must agree.** Half a dozen corpus
|
|
31
|
+
* readers ask what a note's document id is — the pack passes, the wikilink
|
|
32
|
+
* index, the content index, the Foundry-address pass, the address diff — and
|
|
33
|
+
* they never see each other's answer. They agreed before because they all read
|
|
34
|
+
* one authored field; they agree now because they all call this.
|
|
35
|
+
*
|
|
36
|
+
* `maps` and `pkg` are parameters with defaults rather than values resolved
|
|
37
|
+
* inside, so a test can pose a package and a registry this toolchain does not
|
|
38
|
+
* ship without a configuration on disk.
|
|
39
|
+
*
|
|
40
|
+
* @module
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
import { documentId } from "./content-address.mjs";
|
|
44
|
+
import { systemOf } from "./document-subtypes.mjs";
|
|
45
|
+
import { contentPackage } from "./content-package.mjs";
|
|
46
|
+
import { KNOWN_DOCUMENT_SUBTYPE_MAPS } from "./subtype-registry.mjs";
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* A frontmatter value read as a non-blank string, or `undefined`.
|
|
50
|
+
*
|
|
51
|
+
* A blank `id:` is not a pin. It is an author who deleted the value and left
|
|
52
|
+
* the key, and treating `""` as pinned would file the document under the empty
|
|
53
|
+
* string — which the LevelDB packer reports only as an opaque key collision.
|
|
54
|
+
*
|
|
55
|
+
* @param {unknown} value - The authored value.
|
|
56
|
+
* @returns {string|undefined} The trimmed value, or `undefined` when blank.
|
|
57
|
+
*/
|
|
58
|
+
function text(value) {
|
|
59
|
+
if (typeof value !== "string") return undefined;
|
|
60
|
+
const trimmed = value.trim();
|
|
61
|
+
return trimmed || undefined;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The document id a note compiles under: its pin, or its address.
|
|
66
|
+
*
|
|
67
|
+
* Returns `undefined` for a file with **no address** — no `type`, or no
|
|
68
|
+
* `shortcode`. Such a file is not an addressable note, so it has no document
|
|
69
|
+
* and inventing an id for one would file it under nothing. Every caller already
|
|
70
|
+
* had to handle an absent id (that is what the authored field's absence meant),
|
|
71
|
+
* so this reports the same thing rather than throwing where a walk used to
|
|
72
|
+
* skip; whether an id is *required* stays each pass's own decision
|
|
73
|
+
* (`BasePackCompiler.requiresId`).
|
|
74
|
+
*
|
|
75
|
+
* @param {object|null|undefined} fm - Parsed frontmatter.
|
|
76
|
+
* @param {object} [opts]
|
|
77
|
+
* @param {string} [opts.pkg] - The owning content package. Defaults to the
|
|
78
|
+
* configured one, which is the only package a tree's notes belong to (#56).
|
|
79
|
+
* @param {readonly object[]} [opts.maps] - The document-subtype maps, which
|
|
80
|
+
* decide the address's `<system>` segment.
|
|
81
|
+
* @returns {string|undefined} The document's `_id`, or `undefined` when the
|
|
82
|
+
* note has no address to derive one from.
|
|
83
|
+
*/
|
|
84
|
+
export function noteDocId(fm, { pkg, maps = KNOWN_DOCUMENT_SUBTYPE_MAPS } = {}) {
|
|
85
|
+
if (!fm || typeof fm !== "object") return undefined;
|
|
86
|
+
const pinned = text(fm.id);
|
|
87
|
+
if (pinned) return pinned;
|
|
88
|
+
const type = text(fm.type);
|
|
89
|
+
const shortcode = text(fm.shortcode);
|
|
90
|
+
if (!type || !shortcode) return undefined;
|
|
91
|
+
return documentId(pkg ?? contentPackage(), systemOf(type, maps), type, shortcode);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Fill a note's `id` in place, so everything downstream reads one value.
|
|
96
|
+
*
|
|
97
|
+
* The corpus readers each hold their own parsed frontmatter and each ask for
|
|
98
|
+
* `fm.id` in several places; normalising the field once, where the note is
|
|
99
|
+
* read, is what makes "the id is derived" true for all of them rather than for
|
|
100
|
+
* whichever ones remembered to derive it. Idempotent, and a no-op for a note
|
|
101
|
+
* that authored an id or has no address.
|
|
102
|
+
*
|
|
103
|
+
* @param {object|null|undefined} fm - Parsed frontmatter, mutated in place.
|
|
104
|
+
* @param {object} [opts] - As {@link noteDocId}.
|
|
105
|
+
* @returns {object|null|undefined} `fm`, for chaining.
|
|
106
|
+
*/
|
|
107
|
+
export function resolveNoteId(fm, opts) {
|
|
108
|
+
if (!fm || typeof fm !== "object") return fm;
|
|
109
|
+
const id = noteDocId(fm, opts);
|
|
110
|
+
if (id) fm.id = id;
|
|
111
|
+
return fm;
|
|
112
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
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 shortcodes a note declares it used to be published under (#278).
|
|
16
|
+
*
|
|
17
|
+
* A package's `(type, shortcode)` addresses are a published interface, and
|
|
18
|
+
* `addresses diff` reports what a build stopped publishing — telling a
|
|
19
|
+
* **rename** from a **withdrawal** by matching document ids across two
|
|
20
|
+
* releases. #270 removed the property that rested on. An id is now derived from
|
|
21
|
+
* the canonical address, which carries the shortcode, so renaming a shortcode
|
|
22
|
+
* moves the id too: both sides of the join move together, the match finds
|
|
23
|
+
* nothing, and a rename is reported as a withdrawal with no successor named.
|
|
24
|
+
*
|
|
25
|
+
* **The remaining lever needed foresight, which is the wrong thing to ask for.**
|
|
26
|
+
* An authored `id:` still wins, and pinning one does keep a document's identity
|
|
27
|
+
* across a rename — but it has to be written *before* the rename, by an author
|
|
28
|
+
* who does not yet know they will make one. An author who has just renamed a
|
|
29
|
+
* shortcode knows exactly what the old one was, and that is the only moment
|
|
30
|
+
* anyone does.
|
|
31
|
+
*
|
|
32
|
+
* So a note states it:
|
|
33
|
+
*
|
|
34
|
+
* ```yaml
|
|
35
|
+
* type: weapongear
|
|
36
|
+
* shortcode: Taburi
|
|
37
|
+
* renamedFrom: Tabri
|
|
38
|
+
* ```
|
|
39
|
+
*
|
|
40
|
+
* **A declaration is an assertion, not a match, and the diagnostic says which
|
|
41
|
+
* it had.** That distinction is the whole ethos of `address-diff.mjs`: it
|
|
42
|
+
* refuses to guess a successor from a similar-looking string, because a wrong
|
|
43
|
+
* one sends the reader to the wrong fix. An author's declaration is neither a
|
|
44
|
+
* guess nor an identity match — it is testimony from the one party that knows —
|
|
45
|
+
* so it is used, attributed, and never silently blended with an id match.
|
|
46
|
+
*
|
|
47
|
+
* **It is a list, because renames chain.** The diff is release-to-release and a
|
|
48
|
+
* shortcode may be renamed more than once between two releases; a baseline
|
|
49
|
+
* several releases back published a name two renames ago. A single value would
|
|
50
|
+
* report that as a withdrawal again, which is the bug this closes.
|
|
51
|
+
*
|
|
52
|
+
* **It is transient.** Once every baseline a build is compared against post-dates
|
|
53
|
+
* the rename, the declaration has nothing left to say and may be deleted. That
|
|
54
|
+
* is the difference from an `id:` pin, which is permanent, opaque, and a second
|
|
55
|
+
* identity for a thing that already has one (#270).
|
|
56
|
+
*
|
|
57
|
+
* **It is one key per note, at the top level, however many systems the note
|
|
58
|
+
* compiles into.** A shortcode is the note's, not a system block's: a note
|
|
59
|
+
* carrying `sohl:` and `hm3:` blocks compiles into two documents that share one
|
|
60
|
+
* shortcode, so a rename moves both and one declaration covers both.
|
|
61
|
+
*
|
|
62
|
+
* This module reads the key and says nothing about whether the values are
|
|
63
|
+
* *sound* — that a value is a well-formed shortcode, is not the note's own, and
|
|
64
|
+
* does not name an address some live note still publishes are cross-note
|
|
65
|
+
* questions, and `engine/content-lint.mjs` owns them. Composing a value into an
|
|
66
|
+
* address key is likewise not here: the diff defines that space and spells the
|
|
67
|
+
* key once, in `engine/address-diff.mjs`.
|
|
68
|
+
*
|
|
69
|
+
* @module
|
|
70
|
+
*/
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The `renamedFrom:` entries a note authors, exactly as authored.
|
|
74
|
+
*
|
|
75
|
+
* Scalar or list, because one predecessor is the overwhelmingly common case and
|
|
76
|
+
* requiring `- ` on it would be friction with nothing behind it; both normalize
|
|
77
|
+
* here so no reader has to ask which form it got.
|
|
78
|
+
*
|
|
79
|
+
* Entries are returned **unvalidated** — a number, a nested list, a blank
|
|
80
|
+
* string all come back as they were written. The lint needs to see them to
|
|
81
|
+
* report them, and a reader that quietly dropped them would report a correct
|
|
82
|
+
* tree clean while a typo silently did nothing.
|
|
83
|
+
*
|
|
84
|
+
* @param {object|null|undefined} fm - Parsed frontmatter.
|
|
85
|
+
* @returns {readonly unknown[]} The authored entries, in authored order; empty
|
|
86
|
+
* when the note declares none.
|
|
87
|
+
*/
|
|
88
|
+
export function renamedFromEntries(fm) {
|
|
89
|
+
if (!fm || typeof fm !== "object") return [];
|
|
90
|
+
const raw = /** @type {{renamedFrom?: unknown}} */ (fm).renamedFrom;
|
|
91
|
+
if (raw == null) return [];
|
|
92
|
+
return Array.isArray(raw) ? raw : [raw];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* The well-formed shortcodes among a note's `renamedFrom:` entries.
|
|
97
|
+
*
|
|
98
|
+
* Trimmed, de-duplicated, and in authored order. Anything that is not a
|
|
99
|
+
* non-blank string is skipped rather than coerced: it is reported by the lint,
|
|
100
|
+
* and a diff that guessed at what a number meant would claim a rename nobody
|
|
101
|
+
* declared.
|
|
102
|
+
*
|
|
103
|
+
* De-duplicating here rather than leaving it to callers is not tidiness — the
|
|
104
|
+
* predecessor index is a map, so a repeated entry would otherwise be indexed
|
|
105
|
+
* twice and the second write would look like a conflicting claim.
|
|
106
|
+
*
|
|
107
|
+
* @param {object|null|undefined} fm - Parsed frontmatter.
|
|
108
|
+
* @returns {string[]} The declared predecessor shortcodes.
|
|
109
|
+
*/
|
|
110
|
+
export function renamedFrom(fm) {
|
|
111
|
+
const seen = new Set();
|
|
112
|
+
for (const entry of renamedFromEntries(fm)) {
|
|
113
|
+
if (typeof entry !== "string") continue;
|
|
114
|
+
const trimmed = entry.trim();
|
|
115
|
+
if (trimmed) seen.add(trimmed);
|
|
116
|
+
}
|
|
117
|
+
return [...seen];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Whether a note declares the key at all, however malformed its value.
|
|
122
|
+
*
|
|
123
|
+
* Separate from {@link renamedFrom} returning nothing, because the two mean
|
|
124
|
+
* opposite things to a lint: a note declaring no key is silent and correct,
|
|
125
|
+
* while one declaring `renamedFrom: []` — or a single blank string — has said
|
|
126
|
+
* something that does nothing, which is worth a word.
|
|
127
|
+
*
|
|
128
|
+
* @param {object|null|undefined} fm - Parsed frontmatter.
|
|
129
|
+
* @returns {boolean} `true` when the key is present and not null.
|
|
130
|
+
*/
|
|
131
|
+
export function declaresRenamedFrom(fm) {
|
|
132
|
+
if (!fm || typeof fm !== "object") return false;
|
|
133
|
+
return /** @type {{renamedFrom?: unknown}} */ (fm).renamedFrom != null;
|
|
134
|
+
}
|
package/engine/note-schemas.mjs
CHANGED
|
@@ -73,6 +73,29 @@ const JOURNAL_ONLY_FIELDS = Object.freeze([]);
|
|
|
73
73
|
*/
|
|
74
74
|
const ARMORLOCATION_FIELDS = Object.freeze([]);
|
|
75
75
|
|
|
76
|
+
/**
|
|
77
|
+
* A `bundle` note — a set of documents taken as a unit, Foundry's `Adventure`.
|
|
78
|
+
*
|
|
79
|
+
* Like a folder, it declares no system-block fields: everything it says is a
|
|
80
|
+
* `data` property. Its `system` names which system's documents it may hold
|
|
81
|
+
* rather than which system it belongs to, so the note itself is system-neutral.
|
|
82
|
+
*
|
|
83
|
+
* @type {readonly import("./field-spec.mjs").FieldSpec[]}
|
|
84
|
+
*/
|
|
85
|
+
const BUNDLE_FIELDS = Object.freeze([]);
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* A `folder` note — Foundry's `Folder`, the grouping documents are filed in.
|
|
89
|
+
*
|
|
90
|
+
* Empty for the same reason a bundle's is: a folder declares no system-block
|
|
91
|
+
* fields, because a `Folder` is a core Foundry document rather than any
|
|
92
|
+
* system's. Its two properties — `parent` and `color` — are `data:` keys, held
|
|
93
|
+
* closed by `engine/note-vocabulary.mjs` (#256).
|
|
94
|
+
*
|
|
95
|
+
* @type {readonly import("./field-spec.mjs").FieldSpec[]}
|
|
96
|
+
*/
|
|
97
|
+
const FOLDER_FIELDS = Object.freeze([]);
|
|
98
|
+
|
|
76
99
|
/**
|
|
77
100
|
* Every engine-level content type, and what a note of that type may write.
|
|
78
101
|
*
|
|
@@ -84,4 +107,6 @@ export const ENGINE_NOTE_SCHEMAS = Object.freeze({
|
|
|
84
107
|
lore: JOURNAL_ONLY_FIELDS,
|
|
85
108
|
scenario: JOURNAL_ONLY_FIELDS,
|
|
86
109
|
armorlocation: ARMORLOCATION_FIELDS,
|
|
110
|
+
bundle: BUNDLE_FIELDS,
|
|
111
|
+
folder: FOLDER_FIELDS,
|
|
87
112
|
});
|