@heroiclands/package-build 19.0.0 → 20.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/CHANGELOG.md +624 -0
  2. package/CONTENT.md +79 -8
  3. package/bin/content-build.mjs +7 -1
  4. package/content-config.mjs +10 -1
  5. package/docs/content-format.md +394 -72
  6. package/engine/actor-compiler.mjs +197 -7
  7. package/engine/address-charset.mjs +23 -5
  8. package/engine/base-compiler.mjs +63 -2
  9. package/engine/bundles.mjs +9 -0
  10. package/engine/content-address.mjs +92 -1
  11. package/engine/content-format.mjs +102 -0
  12. package/engine/content-index.mjs +11 -8
  13. package/engine/content-links.mjs +37 -21
  14. package/engine/field-reference.mjs +57 -5
  15. package/engine/field-spec.mjs +214 -7
  16. package/engine/folder-notes.mjs +24 -1
  17. package/engine/foreign-catalog.mjs +4 -1
  18. package/engine/foundry-entries.mjs +14 -0
  19. package/engine/frontmatter-lint.mjs +186 -27
  20. package/engine/frontmatter.mjs +11 -11
  21. package/engine/generate.mjs +67 -10
  22. package/engine/helpers.mjs +86 -9
  23. package/engine/index.mjs +3 -0
  24. package/engine/item-compiler.mjs +37 -0
  25. package/engine/journals.mjs +21 -4
  26. package/engine/macros.mjs +8 -0
  27. package/engine/map-notes.mjs +7 -7
  28. package/engine/note-ids.mjs +25 -1
  29. package/engine/note-vocabulary.mjs +76 -9
  30. package/engine/retired-fields.mjs +57 -16
  31. package/engine/runtime-only-fields.mjs +204 -0
  32. package/engine/scenes.mjs +12 -19
  33. package/engine/schema-check.mjs +23 -1
  34. package/engine/site-index.mjs +17 -0
  35. package/engine/subtype-registry.mjs +30 -0
  36. package/engine/system-block.mjs +81 -3
  37. package/engine/web-wikilinks.mjs +33 -27
  38. package/engine/wikilink-syntax.mjs +7 -0
  39. package/engine/wikilinks.mjs +74 -16
  40. package/hm3/actors.mjs +63 -13
  41. package/package.json +2 -2
  42. package/sohl/actors.mjs +106 -7
  43. package/sohl/item-fields.mjs +203 -0
  44. package/sohl/note-schemas.mjs +6 -3
  45. package/types/engine/actor-compiler.d.mts +83 -3
  46. package/types/engine/address-charset.d.mts +22 -4
  47. package/types/engine/base-compiler.d.mts +54 -3
  48. package/types/engine/content-address.d.mts +64 -0
  49. package/types/engine/content-format.d.mts +9 -0
  50. package/types/engine/field-spec.d.mts +271 -3
  51. package/types/engine/folder-notes.d.mts +20 -0
  52. package/types/engine/foundry-entries.d.mts +6 -0
  53. package/types/engine/frontmatter-lint.d.mts +18 -2
  54. package/types/engine/frontmatter.d.mts +11 -11
  55. package/types/engine/generate.d.mts +27 -0
  56. package/types/engine/helpers.d.mts +37 -9
  57. package/types/engine/index.d.mts +1 -0
  58. package/types/engine/map-notes.d.mts +2 -2
  59. package/types/engine/note-ids.d.mts +14 -0
  60. package/types/engine/retired-fields.d.mts +29 -13
  61. package/types/engine/runtime-only-fields.d.mts +102 -0
  62. package/types/engine/schema-check.d.mts +10 -1
  63. package/types/engine/subtype-registry.d.mts +21 -0
  64. package/types/engine/system-block.d.mts +28 -2
  65. package/types/sohl/actors.d.mts +3 -3
@@ -10,6 +10,7 @@ export * as folderNotes from "./folder-notes.mjs";
10
10
  export * as contentPackage from "./content-package.mjs";
11
11
  export * as notePackage from "./note-package.mjs";
12
12
  export * as retiredFields from "./retired-fields.mjs";
13
+ export * as runtimeOnlyFields from "./runtime-only-fields.mjs";
13
14
  export * as homepage from "./homepage.mjs";
14
15
  export * as noteSchemas from "./note-schemas.mjs";
15
16
  export * as noteVocabulary from "./note-vocabulary.mjs";
@@ -158,8 +158,8 @@ export function buildScene(fm: object, ctx: object): object;
158
158
  * @param {string} sceneId - The owning scene's `_id`.
159
159
  * @param {string} [img] - The background art, already resolved from the note.
160
160
  * Passed by {@link buildScene}, which reads it from the note rather than from
161
- * the block; defaults to whichever spelling the block itself carries, so a
162
- * direct two-argument call still works (#142).
161
+ * the block; defaults to the block's own `img`, so a direct two-argument call
162
+ * still works.
163
163
  * @returns {object} The Level document, keyed for the pack.
164
164
  */
165
165
  export function buildLevel(sohl: object, sceneId: string, img?: string): object;
@@ -1,6 +1,20 @@
1
1
  /**
2
2
  * The document id a note compiles under: its pin, or its address.
3
3
  *
4
+ * **One type hashes its address differently, and that is not an exception to
5
+ * the rule but an application of it.** A `Folder` is a document of its own
6
+ * class, and its id is hashed under the `folder` namespace so that a folder and
7
+ * an item sharing a shortcode cannot derive one id — a collision Foundry would
8
+ * not report, since it keys folders and documents in separate collections
9
+ * (#258). So the answer for a folder comes from
10
+ * {@link module:engine/folder-notes.folderDocId}, the pass that emits those
11
+ * documents, rather than from a second derivation here.
12
+ *
13
+ * That this function ever answered differently was invisible from inside a
14
+ * build — no pass reads a folder's id from here — and surfaced only in the
15
+ * content index, which is read from outside and had no way to be checked
16
+ * against what shipped (#310).
17
+ *
4
18
  * Returns `undefined` for a file with **no address** — no `type`, or no
5
19
  * `shortcode`. Such a file is not an addressable note, so it has no document
6
20
  * and inventing an id for one would file it under nothing. Every caller already
@@ -258,6 +258,30 @@ export function legacyKeyMessage(block: string, field: {
258
258
  name?: string;
259
259
  legacyKey?: string;
260
260
  }, file?: string): string;
261
+ /**
262
+ * What a note writing a field at the **top-level key `data:` gathered it off**
263
+ * is told (#332).
264
+ *
265
+ * {@link legacyKeyMessage}'s counterpart for the other retiring position. #128
266
+ * did not invent the facts `data:` holds — it collected them out of the note's
267
+ * open top level — so `portrait:` beside `img:` is the *pre-`data:`* spelling
268
+ * of `data.portrait`, and both are read for the same reason both in-block
269
+ * spellings are: a package moves its corpus when it is ready, not on a flag day.
270
+ *
271
+ * It says nothing about which value is emitted, because that is not what an
272
+ * author needs from it. The value is the same either way; what the finding
273
+ * counts is one more note still on the old position.
274
+ *
275
+ * @param {{name?: string}} field - The declaration, which names the current
276
+ * position; the retiring one is derived from it.
277
+ * @param {string} [file] - The note's path, named in the message. Omit it where
278
+ * the caller emits through a diagnostic, whose locator already starts the
279
+ * line — repeating it prints the path twice.
280
+ * @returns {string} The message, unpunctuated at the end as a finding is.
281
+ */
282
+ export function retiredTopLevelMessage(field: {
283
+ name?: string;
284
+ }, file?: string): string;
261
285
  /**
262
286
  * Whether a note writes the retired spelling of a field, wherever it put it.
263
287
  *
@@ -279,9 +303,9 @@ export function declaresRetiredAlias(fm: object | null | undefined, current: str
279
303
  * first, then the note's top level — so a renamed field keeps working wherever
280
304
  * it was already written while the canonical home is the top level.
281
305
  *
282
- * A blank value counts as absent: `img:` cleared in an editor means the note
283
- * names no art there, and falling through to the retired spelling is what an
284
- * author part-way through the rename means by it.
306
+ * A blank value counts as absent: `relations:` cleared in an editor means the
307
+ * note records no standings there, and falling through to the retired spelling
308
+ * is what an author part-way through the rename means by it.
285
309
  *
286
310
  * @param {object|null|undefined} fm - Parsed frontmatter.
287
311
  * @param {string} current - The field's current name.
@@ -295,8 +319,8 @@ export function readAliasedField(fm: object | null | undefined, current: string)
295
319
  * and what every reader asks for; the value is the spelling still honoured.
296
320
  * The table is therefore scoped by the schema without saying so twice: an alias
297
321
  * applies to a note only where that note's type declares the current field, so
298
- * `image` is retired on a map — which declares `img` — and remains an unknown
299
- * key anywhere else.
322
+ * `relation` is retired on an affiliation — which declares `relations` — and
323
+ * remains an unknown key anywhere else.
300
324
  *
301
325
  * **`templatePriority` (#266).** The number that decides which of several
302
326
  * competing templates the Create dialog offers was called `archetype`, and
@@ -311,14 +335,6 @@ export function readAliasedField(fm: object | null | undefined, current: string)
311
335
  * read past. Only `affiliation` declares the field, so the alias is reported
312
336
  * there and the old spelling stays an ordinary unknown key everywhere else.
313
337
  *
314
- * **`img` (#142).** Every note type names its artwork `img`, at the note's top
315
- * level, and resolves it the same way. A map alone named its background art
316
- * `image` and read it out of the `sohl:` block — two spellings for one idea,
317
- * with nothing to reconcile them, and a specification that had to hedge rather
318
- * than state a rule. Art is not system-specific: a Scene is a core Foundry
319
- * document and HM3 would want the identical one, so the field belongs beside
320
- * every other note's `img`, not inside a system block.
321
- *
322
338
  * @type {Readonly<Record<string, string>>}
323
339
  */
324
340
  export const RETIRED_FIELD_ALIASES: Readonly<Record<string, string>>;
@@ -0,0 +1,102 @@
1
+ /**
2
+ * What a note authoring a runtime-only field is told, in one place.
3
+ *
4
+ * Shared by every caller that can meet one, because an author meets whichever
5
+ * runs first and they should read the same. It says what the field holds and
6
+ * that deleting the key is the whole fix — there is no value to correct, which
7
+ * is what separates this from an out-of-range one.
8
+ *
9
+ * The reason comes from the declaration rather than from here: this module
10
+ * knows no field names, and a message written per field would be a second
11
+ * statement of the fact the declaration already carries.
12
+ *
13
+ * @param {string} key - The **whole key the note wrote**, from the region it
14
+ * sits in down to the field: `sohl.system.onsetDate` on the item's own note,
15
+ * `sohl.items[2].system.contractDate` on an actor's embedded entry. Composed
16
+ * by the caller, because only it knows where it found the value — and a
17
+ * message naming the leaf alone leaves an author a note to search.
18
+ * @param {import("./field-spec.mjs").FieldSpec} field - The declaration, which
19
+ * carries the reason.
20
+ * @param {string} [file] - The note's path, named in the message. Omit it where
21
+ * the caller emits through a diagnostic, whose locator already starts the
22
+ * line — repeating it prints the path twice.
23
+ * @returns {string} The message, unpunctuated at the end as a finding is.
24
+ */
25
+ export function runtimeOnlyMessage(key: string, field: import("./field-spec.mjs").FieldSpec, file?: string): string;
26
+ /**
27
+ * The runtime-only fields a note actually writes, in declaration order.
28
+ *
29
+ * **Presence is the whole test**, as it is for every retired field: an authored
30
+ * `onsetDate: null` is as much a claim about play state as a number is, and it
31
+ * is exactly the belief the message exists to correct. So the question is
32
+ * whether the path resolves to anything at all, never whether the value is a
33
+ * usable one.
34
+ *
35
+ * Only `<block>.system.<to>` is searched, because it is the only position a
36
+ * runtime-only field is reachable at. Such a declaration carries no `name`, so
37
+ * it has neither a legacy in-block key nor a shared top-level source — and a
38
+ * bare `<block>.onsetDate` is an unrecognized block key, which
39
+ * {@link module:engine/system-block.unknownBlockKeys} already reports as an
40
+ * error naming the note and the line.
41
+ *
42
+ * @param {object|null|undefined} fm - Parsed frontmatter.
43
+ * @param {readonly import("./field-spec.mjs").FieldSpec[]} [fields] - The
44
+ * type's field declaration.
45
+ * @param {object} options - Options.
46
+ * @param {string} options.block - The system block to look in.
47
+ * @returns {import("./field-spec.mjs").FieldSpec[]} The offending declarations.
48
+ */
49
+ export function authoredRuntimeOnlyFields(fm: object | null | undefined, fields?: readonly import("./field-spec.mjs").FieldSpec[], { block }?: {
50
+ block: string;
51
+ }): import("./field-spec.mjs").FieldSpec[];
52
+ /**
53
+ * The same question asked of a `system` block directly.
54
+ *
55
+ * A note's own block is reached through {@link authoredRuntimeOnlyFields}, but
56
+ * it is not the only place an author writes one: an actor note's `items:`
57
+ * entries carry a `system:` overlay that is deep-merged onto the template
58
+ * verbatim, with no field declaration in the path at all. That overlay is a
59
+ * `system` block by every meaning except where it sits, and a `contractDate`
60
+ * written there ships exactly as one written on the trauma's own note.
61
+ *
62
+ * @param {Record<string, unknown>|null|undefined} data - The authored `system`
63
+ * data.
64
+ * @param {readonly import("./field-spec.mjs").FieldSpec[]} [fields] - The
65
+ * type's field declaration.
66
+ * @returns {import("./field-spec.mjs").FieldSpec[]} The offending declarations.
67
+ */
68
+ export function runtimeOnlyIn(data: Record<string, unknown> | null | undefined, fields?: readonly import("./field-spec.mjs").FieldSpec[]): import("./field-spec.mjs").FieldSpec[];
69
+ /**
70
+ * Refuse a note that authors any of its type's runtime-only fields.
71
+ *
72
+ * Refused rather than reported: the note is not compiled, so nothing it would
73
+ * have emitted reaches a pack. What that costs is the caller's to decide — each
74
+ * of them counts the refused note and emits a located diagnostic, so a refusal
75
+ * is never a silent skip.
76
+ *
77
+ * The **first** offending field is thrown on. A note authoring a whole phase
78
+ * triplet would otherwise produce three findings that are one mistake, and the
79
+ * build stops on this note either way; the fix for the first is the fix for all
80
+ * of them.
81
+ *
82
+ * @param {object|null|undefined} fm - Parsed frontmatter, or nothing when it
83
+ * could not be parsed.
84
+ * @param {readonly import("./field-spec.mjs").FieldSpec[]} [fields] - The
85
+ * type's field declaration. A type that declares none — or declares no
86
+ * runtime-only field — passes.
87
+ * @param {object} options - Options.
88
+ * @param {string} options.block - The system block to look in.
89
+ * @param {string} [options.file] - The note's path, named in the message. Omit
90
+ * it where the caller emits through a diagnostic, which puts the locator at
91
+ * the start of the line already — repeating it prints the path twice.
92
+ * @param {string} [options.absPath] - The note's file on disk, read only on the
93
+ * failing path to locate the offending line and column. The position rides on
94
+ * the thrown error as `position`, for a caller that emits a diagnostic.
95
+ * @returns {void}
96
+ * @throws {Error} When the note authors one.
97
+ */
98
+ export function assertNoRuntimeOnlyFields(fm: object | null | undefined, fields?: readonly import("./field-spec.mjs").FieldSpec[], { block, file, absPath }?: {
99
+ block: string;
100
+ file?: string | undefined;
101
+ absPath?: string | undefined;
102
+ }): void;
@@ -46,11 +46,20 @@ export function declaredFields(artifact: SchemaArtifact, documentType: string, s
46
46
  * the path beneath it separately, so a comparison that knew only the leaf would
47
47
  * report the container as unemitted and the leaf as undeclared.
48
48
  *
49
- * @param {readonly {to: string}[]} fields - A type's field declaration.
49
+ * **A runtime-only field is not in it** (#330). It declares a `to` in order to
50
+ * *claim* the path — so the verbatim passthrough leaves it alone and the
51
+ * refusal has something to name — and `buildFromFields` deliberately skips it,
52
+ * because the document writes that field in play. Counting it here would make
53
+ * the check assert the builder writes a key it never writes; the *unemitted*
54
+ * direction handles it instead, in {@link compareFields}.
55
+ *
56
+ * @param {readonly {to: string, runtimeOnly?: string}[]} fields - A type's
57
+ * field declaration.
50
58
  * @returns {Set<string>} The paths, parents included.
51
59
  */
52
60
  export function emittedFields(fields: readonly {
53
61
  to: string;
62
+ runtimeOnly?: string;
54
63
  }[]): Set<string>;
55
64
  /**
56
65
  * Compare one system's builders against one system's published schemas.
@@ -47,3 +47,24 @@ export function schemaSubtypeOf(system: string | undefined, type: string): strin
47
47
  * @type {readonly import("./document-subtypes.mjs").DocumentSubtypeMap[]}
48
48
  */
49
49
  export const KNOWN_DOCUMENT_SUBTYPE_MAPS: readonly import("./document-subtypes.mjs").DocumentSubtypeMap[];
50
+ /**
51
+ * Every note type any shipped map compiles into an **Actor**.
52
+ *
53
+ * Derived from the maps rather than written out, so a system that adds an actor
54
+ * type is covered without a second list to keep in step — the same reason
55
+ * {@link KNOWN_DOCUMENT_SUBTYPE_MAPS} exists rather than a hand-kept table.
56
+ *
57
+ * It exists because an actor note publishes documentation like every other
58
+ * system-bearing note (#337). `docEntryTypes` was `itemTypes` plus `macro` and
59
+ * the map types, which left a being as the one system-bearing note with no
60
+ * `none` address — nothing a prose link could land on, since its only address
61
+ * named the Actor. Composing that set needs to know which types are actors, and
62
+ * this is where the maps that know already live.
63
+ *
64
+ * **Not an item type.** This widens what carries *documentation*; it must never
65
+ * widen what the items pass compiles, or a being note would be compiled into an
66
+ * Item beside its Actor.
67
+ *
68
+ * @type {ReadonlySet<string>}
69
+ */
70
+ export const ACTOR_TYPES: ReadonlySet<string>;
@@ -114,6 +114,32 @@ export function legacyKeyOf(field: {
114
114
  name?: string;
115
115
  legacyKey?: string;
116
116
  }): string | undefined;
117
+ /**
118
+ * The bare top-level key a `data:`-sourced field is being swept off — step 3b.
119
+ *
120
+ * `data:` (#128) did not invent the facts it holds; it *gathered* them, out of
121
+ * the note's open top level where each was a sibling of `img` and `shortcode`.
122
+ * So the retiring spelling of `data.portrait` is not a second declaration
123
+ * anyone has to write — it is `portrait`, mechanically, and the same holds for
124
+ * every other key that move relocated. Deriving it is what keeps the two
125
+ * spellings of one field from disagreeing the way two declarations would.
126
+ *
127
+ * **Only a `data.` source has one.** `protection.blunt` and `impact.die` are
128
+ * paths into containers a note has always written at the top level; they were
129
+ * never `blunt:` or `die:`, and reading those would invent a position rather
130
+ * than remember one.
131
+ *
132
+ * A field declaring {@link module:engine/field-spec.FieldSpec `topLevelMeans`}
133
+ * has no shared position at all, retiring or otherwise — the resolver checks
134
+ * that before asking.
135
+ *
136
+ * @param {{name?: string}} field - The declaration.
137
+ * @returns {string|undefined} The retiring top-level path, or `undefined` for a
138
+ * field whose shared source never lived there.
139
+ */
140
+ export function retiredTopLevelKey(field: {
141
+ name?: string;
142
+ }): string | undefined;
117
143
  /**
118
144
  * Where a declared field's value came from.
119
145
  *
@@ -121,7 +147,7 @@ export function legacyKeyOf(field: {
121
147
  * can distinguish a value an author wrote from one a default supplied, which
122
148
  * the value alone never says.
123
149
  *
124
- * @typedef {"system"|"block"|"shared"|"default"|"value"} FieldSource
150
+ * @typedef {"system"|"block"|"shared"|"topLevel"|"default"|"value"} FieldSource
125
151
  */
126
152
  /**
127
153
  * Resolve one declared field against a note, in the declared order.
@@ -317,4 +343,4 @@ export const SYSTEM_BLOCK_KEYS: ReadonlySet<string>;
317
343
  * can distinguish a value an author wrote from one a default supplied, which
318
344
  * the value alone never says.
319
345
  */
320
- export type FieldSource = "system" | "block" | "shared" | "default" | "value";
346
+ export type FieldSource = "system" | "block" | "shared" | "topLevel" | "default" | "value";
@@ -12,7 +12,7 @@ export class Actors extends SystemActorCompiler {
12
12
  * Build all embedded items for an actor: one per `sohl.attributes`
13
13
  * entry plus one per `sohl.items` entry. `sohl.skills` is ignored.
14
14
  */
15
- buildEmbeddedItems(itemsMap: any, actorId: any, fm: any, ctx: any): object[];
15
+ buildEmbeddedItems(itemsMap: any, actorId: any, fm: any, ctx: any): any[];
16
16
  /**
17
17
  * Bake each unopened skill's opening mastery level into the document (#46).
18
18
  *
@@ -48,11 +48,11 @@ export class Actors extends SystemActorCompiler {
48
48
  system: {
49
49
  shortcode: any;
50
50
  templatePriority: number | null;
51
- portrait: string;
51
+ portrait: any;
52
52
  appearance: string;
53
53
  dossier: string;
54
54
  };
55
- items: object[];
55
+ items: any[];
56
56
  prototypeToken: {
57
57
  name: any;
58
58
  displayName: number;