@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.
Files changed (153) hide show
  1. package/CHANGELOG.md +1773 -0
  2. package/CONTENT.md +288 -72
  3. package/README.md +1 -1
  4. package/bin/content-build.mjs +218 -137
  5. package/bin/package-build.mjs +43 -0
  6. package/content-config.mjs +62 -67
  7. package/docs/content-format.md +768 -58
  8. package/engine/actor-compiler.mjs +586 -0
  9. package/engine/address-charset.mjs +11 -8
  10. package/engine/address-diff.mjs +266 -22
  11. package/engine/anchored-sections.mjs +83 -0
  12. package/engine/anchors.mjs +83 -0
  13. package/engine/base-compiler.mjs +148 -18
  14. package/engine/bundle-notes.mjs +276 -0
  15. package/engine/bundles.mjs +307 -0
  16. package/engine/code-fences.mjs +103 -0
  17. package/engine/compile-corpus.mjs +89 -0
  18. package/engine/content-address.mjs +267 -5
  19. package/engine/content-format-check.mjs +54 -11
  20. package/engine/content-format.mjs +37 -5
  21. package/engine/content-index.mjs +394 -111
  22. package/engine/content-links.mjs +185 -77
  23. package/engine/content-lint.mjs +232 -14
  24. package/engine/content-package.mjs +2 -1
  25. package/engine/content-tables.mjs +99 -3
  26. package/engine/document-subtypes.mjs +49 -6
  27. package/engine/field-reference.mjs +1 -1
  28. package/engine/field-spec.mjs +56 -6
  29. package/engine/folder-notes.mjs +470 -0
  30. package/engine/foreign-catalog.mjs +195 -4
  31. package/engine/{manifest-emit.mjs → foundry-entries.mjs} +78 -81
  32. package/engine/frontmatter-lint.mjs +245 -19
  33. package/engine/frontmatter.mjs +88 -8
  34. package/engine/generate.mjs +304 -13
  35. package/engine/helpers.mjs +293 -64
  36. package/engine/ids.mjs +137 -2
  37. package/engine/index-records.mjs +126 -0
  38. package/engine/index.mjs +23 -5
  39. package/engine/item-compiler.mjs +349 -0
  40. package/engine/item-docs.mjs +7 -1
  41. package/engine/item-registry.mjs +6 -0
  42. package/engine/journals.mjs +93 -27
  43. package/engine/macros.mjs +4 -2
  44. package/engine/metadata-index.mjs +495 -0
  45. package/engine/note-claims.mjs +146 -25
  46. package/engine/note-ids.mjs +112 -0
  47. package/engine/note-renames.mjs +134 -0
  48. package/engine/note-schemas.mjs +25 -0
  49. package/engine/note-vocabulary.mjs +110 -27
  50. package/engine/pack-config.mjs +4 -0
  51. package/engine/pack-router.mjs +23 -0
  52. package/engine/retired-fields.mjs +137 -3
  53. package/engine/scenes.mjs +66 -34
  54. package/engine/schema-check.mjs +58 -17
  55. package/engine/site-build.mjs +71 -12
  56. package/engine/site-index.mjs +20 -7
  57. package/engine/sql-tables.mjs +485 -0
  58. package/engine/subtype-registry.mjs +102 -0
  59. package/engine/system-block.mjs +96 -12
  60. package/engine/systems.mjs +201 -0
  61. package/engine/web-wikilinks.mjs +50 -20
  62. package/engine/wikilink-syntax.mjs +17 -8
  63. package/engine/wikilinks.mjs +124 -51
  64. package/engine/yaml-lint.mjs +282 -0
  65. package/hm3/actors.mjs +352 -0
  66. package/hm3/default-item-art.mjs +75 -0
  67. package/hm3/document-subtypes.mjs +134 -0
  68. package/hm3/index.mjs +56 -0
  69. package/hm3/item-builders.mjs +84 -0
  70. package/hm3/item-fields.mjs +180 -0
  71. package/hm3/items.mjs +96 -0
  72. package/hm3/template-priority.mjs +85 -0
  73. package/labels.mjs +191 -0
  74. package/manifest.mjs +43 -2
  75. package/package.json +19 -4
  76. package/release.mjs +62 -7
  77. package/sohl/actors.mjs +33 -487
  78. package/sohl/being-info.mjs +16 -7
  79. package/sohl/default-item-art.mjs +14 -3
  80. package/sohl/document-subtypes.mjs +16 -10
  81. package/sohl/item-builders.mjs +14 -5
  82. package/sohl/item-fields.mjs +68 -7
  83. package/sohl/items.mjs +44 -258
  84. package/sohl/note-schemas.mjs +2 -2
  85. package/types/content-config.d.mts +21 -42
  86. package/types/engine/actor-compiler.d.mts +204 -0
  87. package/types/engine/address-charset.d.mts +11 -8
  88. package/types/engine/address-diff.d.mts +53 -5
  89. package/types/engine/anchored-sections.d.mts +21 -0
  90. package/types/engine/anchors.d.mts +20 -0
  91. package/types/engine/base-compiler.d.mts +17 -17
  92. package/types/engine/bundle-notes.d.mts +173 -0
  93. package/types/engine/bundles.d.mts +60 -0
  94. package/types/engine/code-fences.d.mts +43 -0
  95. package/types/engine/compile-corpus.d.mts +32 -0
  96. package/types/engine/content-address.d.mts +205 -5
  97. package/types/engine/content-format-check.d.mts +6 -2
  98. package/types/engine/content-format.d.mts +57 -1
  99. package/types/engine/content-index.d.mts +64 -80
  100. package/types/engine/content-links.d.mts +52 -7
  101. package/types/engine/content-lint.d.mts +10 -1
  102. package/types/engine/content-package.d.mts +2 -1
  103. package/types/engine/content-tables.d.mts +21 -39
  104. package/types/engine/document-subtypes.d.mts +37 -3
  105. package/types/engine/field-spec.d.mts +76 -5
  106. package/types/engine/folder-notes.d.mts +159 -0
  107. package/types/engine/foreign-catalog.d.mts +53 -0
  108. package/types/engine/{manifest-emit.d.mts → foundry-entries.d.mts} +16 -38
  109. package/types/engine/frontmatter-lint.d.mts +10 -2
  110. package/types/engine/frontmatter.d.mts +64 -0
  111. package/types/engine/generate.d.mts +38 -0
  112. package/types/engine/helpers.d.mts +94 -30
  113. package/types/engine/ids.d.mts +96 -0
  114. package/types/engine/index-records.d.mts +68 -0
  115. package/types/engine/index.d.mts +9 -3
  116. package/types/engine/item-compiler.d.mts +131 -0
  117. package/types/engine/journals.d.mts +47 -9
  118. package/types/engine/metadata-index.d.mts +226 -0
  119. package/types/engine/note-claims.d.mts +57 -11
  120. package/types/engine/note-ids.d.mts +38 -0
  121. package/types/engine/note-renames.d.mts +102 -0
  122. package/types/engine/note-vocabulary.d.mts +44 -8
  123. package/types/engine/retired-fields.d.mts +75 -0
  124. package/types/engine/scenes.d.mts +3 -2
  125. package/types/engine/schema-check.d.mts +25 -4
  126. package/types/engine/site-build.d.mts +4 -4
  127. package/types/engine/site-index.d.mts +1 -1
  128. package/types/engine/sql-tables.d.mts +185 -0
  129. package/types/engine/subtype-registry.d.mts +49 -0
  130. package/types/engine/system-block.d.mts +40 -1
  131. package/types/engine/systems.d.mts +106 -0
  132. package/types/engine/web-wikilinks.d.mts +4 -2
  133. package/types/engine/wikilink-syntax.d.mts +10 -3
  134. package/types/engine/wikilinks.d.mts +41 -13
  135. package/types/engine/yaml-lint.d.mts +107 -0
  136. package/types/hm3/actors.d.mts +48 -0
  137. package/types/hm3/default-item-art.d.mts +42 -0
  138. package/types/hm3/document-subtypes.d.mts +24 -0
  139. package/types/hm3/index.d.mts +7 -0
  140. package/types/hm3/item-builders.d.mts +11 -0
  141. package/types/hm3/item-fields.d.mts +12 -0
  142. package/types/hm3/items.d.mts +23 -0
  143. package/types/hm3/template-priority.d.mts +21 -0
  144. package/types/manifest.d.mts +22 -1
  145. package/types/release.d.mts +9 -4
  146. package/types/sohl/actors.d.mts +7 -74
  147. package/types/sohl/being-info.d.mts +8 -27
  148. package/types/sohl/default-item-art.d.mts +5 -3
  149. package/types/sohl/items.d.mts +17 -32
  150. package/engine/foreign-manifests.mjs +0 -126
  151. package/engine/kb-manifest.mjs +0 -490
  152. package/types/engine/foreign-manifests.d.mts +0 -43
  153. package/types/engine/kb-manifest.d.mts +0 -241
@@ -16,6 +16,39 @@ export function makeId(namespace: string, value: string): string;
16
16
  * @throws {Error} Naming the replacement type.
17
17
  */
18
18
  export function assertTypeNotRetired(type: string, where?: string): void;
19
+ /**
20
+ * The current spelling of a content type: itself, or what it was renamed to.
21
+ *
22
+ * Every table keyed by note type is keyed by the **current** name, and every
23
+ * lookup goes through this — which is the whole of the window's behaviour, in
24
+ * one function, so no two readers can disagree about which vocabulary a note
25
+ * is held to.
26
+ *
27
+ * A non-string passes through untouched: callers hand this whatever the
28
+ * frontmatter carried, and inventing a type for a number would hide the
29
+ * missing-`type:` finding that belongs to the linter.
30
+ *
31
+ * @param {any} type - The type as authored, or as a link spells it.
32
+ * @returns {any} The current spelling.
33
+ */
34
+ export function currentType(type: any): any;
35
+ /**
36
+ * What a note declaring a renamed type is told, in one place.
37
+ *
38
+ * Shared by every reporter, because an author meets whichever runs first and
39
+ * they should read the same. It names the type to write rather than a value to
40
+ * correct — no value makes the retired spelling right — and it says the note
41
+ * compiles either way, so a reader knows this is a rename to schedule rather
42
+ * than a build to unbreak.
43
+ *
44
+ * @param {string} retired - The spelling the note used.
45
+ * @param {string} current - What to write instead.
46
+ * @param {string} [where] - What carries it — a file path, a link target —
47
+ * appended to the message. Omit it where the caller emits through a
48
+ * diagnostic, whose locator already starts the line.
49
+ * @returns {string} The message, unpunctuated at the end as a finding is.
50
+ */
51
+ export function renamedTypeMessage(retired: string, current: string, where?: string): string;
19
52
  /**
20
53
  * The pack a type's documents live in, in the conventional one-pack-per-type
21
54
  * layout.
@@ -94,6 +127,27 @@ export const MAP_TYPES: ReadonlySet<string>;
94
127
  * @type {readonly string[]}
95
128
  */
96
129
  export const MAP_SUBTYPES: readonly string[];
130
+ /**
131
+ * Content types whose whole document **is** a JournalEntry.
132
+ *
133
+ * Prose, and nothing else: each compiles into one journal entry of its own,
134
+ * with no second document to point at. That is what separates them from the
135
+ * doc-carrying types in `item-docs.mjs`, whose prose becomes a journal *beside*
136
+ * an item, a macro or a scene — those are two documents, and the pair is
137
+ * addressed as `<type>` and `doc<type>`. These are one, so there is no
138
+ * `docplace` and nothing synthesizes one.
139
+ *
140
+ * `doc` was the only member until #241. `place`, `lore` and `scenario` are in
141
+ * the published content format and were declared for validation in #233, but
142
+ * nothing routed them: a note of one lint-ed clean and then compiled into
143
+ * nothing, because {@link PACK_BY_TYPE} did not name it and the open-set
144
+ * default sent it to the items pack. `sohl-thalorna` could not compile a single
145
+ * pack for exactly this reason — 450 notes, and the same 450 the linter had
146
+ * reported before it learned the types.
147
+ *
148
+ * @type {ReadonlySet<string>}
149
+ */
150
+ export const JOURNAL_TYPES: ReadonlySet<string>;
97
151
  /**
98
152
  * Content type → the pack its documents compile into, and the document type
99
153
  * that pack holds.
@@ -129,6 +183,48 @@ export const PACK_BY_TYPE: Readonly<Record<string, {
129
183
  * @type {Readonly<Record<string, string>>}
130
184
  */
131
185
  export const RETIRED_TYPES: Readonly<Record<string, string>>;
186
+ /**
187
+ * Content types that were **renamed**, and what each is called now.
188
+ *
189
+ * A retired type and a renamed one are different cases, and only the first can
190
+ * be refused. `character` above was retired *outright* — nothing a note wrote
191
+ * made it right, so {@link assertTypeNotRetired} throws. A renamed type has a
192
+ * replacement, the two spellings mean the same thing, and a note carrying the
193
+ * old one compiles into exactly the document it always did: refusing it would
194
+ * fail a build over a note that is not wrong.
195
+ *
196
+ * So these retire in the three steps `package:` took (#56), and this table is
197
+ * the **first**: both spellings resolve, the current one is canonical, and the
198
+ * retired one is *reported* — never refused. The sweep of the content trees and
199
+ * the refusal come after, once no tree writes the old name. That is the same
200
+ * rule `RETIRED_FIELD_ALIASES` states for a renamed *field*, and for the same
201
+ * reason: a consumer must be able to adopt the new toolchain before its content
202
+ * moves, and there are some 31,000 references to move.
203
+ *
204
+ * **Why these three (#78).** `armorgear`, `concoctiongear` and `projectilegear`
205
+ * named the *SoHL document subtype* a note happened to compile into rather than
206
+ * the thing the note is about. A note's `type` sits outside the `sohl:` and
207
+ * `hm3:` blocks precisely because it belongs to no system, and HM3 already
208
+ * compiles a `projectile` into a `missilegear` — so the suffix was never a fact
209
+ * about the note. `weapongear` keeps its name: SoHL and HM3 both call the
210
+ * document that, and #78's table has no row for it.
211
+ *
212
+ * **Keyed by the retired spelling**, which is the opposite of
213
+ * `RETIRED_FIELD_ALIASES`. The direction follows the operation: a field alias
214
+ * is scoped by the schema that declares the *current* name, so that is what a
215
+ * reader asks with; a type alias is applied to a value read off a note, so what
216
+ * a reader has in hand is the retired spelling and what it needs is the
217
+ * replacement.
218
+ *
219
+ * **The address is not normalised.** A note's canonical address — and therefore
220
+ * its document id — carries its `type` as authored, so both spellings keep the
221
+ * addresses they already publish and a tree that has not swept compiles
222
+ * byte-identically. Renaming the type in a note *is* an address move, which is
223
+ * the sweep's business and not this table's.
224
+ *
225
+ * @type {Readonly<Record<string, string>>}
226
+ */
227
+ export const RENAMED_TYPES: Readonly<Record<string, string>>;
132
228
  /** Where every other content type compiles: the items pack. */
133
229
  export const ITEM_PACK: Readonly<{
134
230
  pack: "items";
@@ -0,0 +1,68 @@
1
+ /**
2
+ * The file a record was read from, as an absolute path.
3
+ *
4
+ * **The one composition, because there were four.** `file.path` is recorded
5
+ * *relative* to the content root deliberately — an absolute one is a fact about
6
+ * the machine that built the index, would differ between two checkouts of the
7
+ * same tree, and would put someone's home directory in a published artifact. So
8
+ * every pass that reads the index and then needs to open a note has to compose
9
+ * the absolute form, and each of the readers converted for #243 had written its
10
+ * own `path.join(base, ...record.file.path.split("/"))`. Four copies of one
11
+ * rule is what #243 exists to remove, so here it is once.
12
+ *
13
+ * The split is on `"/"` rather than `path.sep` because the recorded path is
14
+ * always POSIX — that is what makes the index identical on every platform.
15
+ *
16
+ * @param {string} contentBase - Root of the content tree the index was built from.
17
+ * @param {object} record - An index record.
18
+ * @returns {string} The note's absolute path.
19
+ */
20
+ export function noteFile(contentBase: string, record: object): string;
21
+ /**
22
+ * The note's own frontmatter, as authored, from an index record.
23
+ *
24
+ * The inverse of the record's spread, and exact rather than best-effort: a
25
+ * record is the note's frontmatter plus {@link DERIVED_KEYS}, and a note that
26
+ * authors one of those keys fails the walk — so removing them cannot remove
27
+ * anything the note wrote. That enforced pairing is what lets a pass read the
28
+ * corpus from the index and still lint, route or compile what the *author*
29
+ * typed, rather than reasoning about `address:` and `anchors:` as though
30
+ * someone had written them.
31
+ *
32
+ * Lives beside the list it is the inverse of, so the two cannot drift.
33
+ *
34
+ * @param {Record<string, any>} record - An index record.
35
+ * @returns {Record<string, any>} The frontmatter, without the derived keys.
36
+ */
37
+ export function authoredFrontmatter(record: Record<string, any>): Record<string, any>;
38
+ /**
39
+ * Whether a record is a note's, rather than a documentation journal's.
40
+ *
41
+ * An item note yields two records — itself and the JournalEntry its prose
42
+ * compiles into — and the second is a document, not a note: it has no file of
43
+ * its own to read, no frontmatter an author wrote, and its `type` is the
44
+ * virtual `doc<type>` that `readQualifier` resolves rather than a type any tree
45
+ * declares. A reader enumerating the corpus wants the notes; one resolving an
46
+ * address wants both.
47
+ *
48
+ * @param {Record<string, any>} record - An index record.
49
+ * @returns {boolean} True for a note's own record.
50
+ */
51
+ export function isNoteRecord(record: Record<string, any>): boolean;
52
+ /**
53
+ * The keys the content index adds to a record, which a note therefore may not
54
+ * carry itself.
55
+ *
56
+ * `package` is the note's distribution unit — the configured `contentPackage`,
57
+ * since a note declaring its own is a hard error (#56) — and it matches what
58
+ * the content-table expander puts on the same field, so a query reads the same
59
+ * value from either. `file` namespaces the note's place in the tree, again
60
+ * matching the expander's `file.*`.
61
+ *
62
+ * Both are checked rather than assumed: `folder` is real frontmatter on most
63
+ * notes, so the neighbouring names are close enough to a real key that a silent
64
+ * overwrite is a plausible future rather than a hypothetical one.
65
+ *
66
+ * @type {ReadonlyArray<string>}
67
+ */
68
+ export const DERIVED_KEYS: ReadonlyArray<string>;
@@ -6,17 +6,21 @@ export * as contentTree from "./content-tree.mjs";
6
6
  export * as packConfig from "./pack-config.mjs";
7
7
  export * as packRouter from "./pack-router.mjs";
8
8
  export * as noteClaims from "./note-claims.mjs";
9
+ export * as folderNotes from "./folder-notes.mjs";
9
10
  export * as contentPackage from "./content-package.mjs";
10
11
  export * as notePackage from "./note-package.mjs";
11
12
  export * as retiredFields from "./retired-fields.mjs";
12
13
  export * as homepage from "./homepage.mjs";
13
14
  export * as noteSchemas from "./note-schemas.mjs";
14
15
  export * as noteVocabulary from "./note-vocabulary.mjs";
16
+ export * as systems from "./systems.mjs";
15
17
  export * as contentSlug from "./content-slug.mjs";
16
18
  export * as contentAddress from "./content-address.mjs";
17
- export * as foreignManifests from "./foreign-manifests.mjs";
18
- export * as kbManifest from "./kb-manifest.mjs";
19
- export * as manifestEmit from "./manifest-emit.mjs";
19
+ export * as subtypeRegistry from "./subtype-registry.mjs";
20
+ export * as noteIds from "./note-ids.mjs";
21
+ export * as noteRenames from "./note-renames.mjs";
22
+ export * as metadataIndex from "./metadata-index.mjs";
23
+ export * as foundryEntries from "./foundry-entries.mjs";
20
24
  export * as contentIndex from "./content-index.mjs";
21
25
  export * as siteBuild from "./site-build.mjs";
22
26
  export * as contentLint from "./content-lint.mjs";
@@ -35,6 +39,8 @@ export * as journals from "./journals.mjs";
35
39
  export * as macros from "./macros.mjs";
36
40
  export * as mapNotes from "./map-notes.mjs";
37
41
  export * as scenes from "./scenes.mjs";
42
+ export * as bundleNotes from "./bundle-notes.mjs";
43
+ export * as bundles from "./bundles.mjs";
38
44
  export * as sceneLevels from "./scene-levels.mjs";
39
45
  export * as generate from "./generate.mjs";
40
46
  export * as compendiums from "./compendiums.mjs";
@@ -0,0 +1,131 @@
1
+ /**
2
+ * The description an item carries: a pointer to its **item doc**, the
3
+ * JournalEntry the journals pass compiles this same body into (#1348).
4
+ *
5
+ * The prose is not rendered into the item at all. Carrying it would duplicate
6
+ * it onto every actor holding the item — 7.59 MB of copies across the actors
7
+ * pack, of which 133 KB was distinct — where a link is 60 bytes and always
8
+ * current. The two passes derive the target from the note's own id, so neither
9
+ * has to see the other's output; both split the *converted* markdown, so an H1
10
+ * carrying a wikilink names the same page on both sides.
11
+ *
12
+ * An item with no prose points at nothing, exactly as the journals pass writes
13
+ * no entry for it.
14
+ *
15
+ * @param {string} markdown - The note body, tables expanded and wikilinks
16
+ * resolved.
17
+ * @param {object} fm - The note's frontmatter.
18
+ * @param {string} name - The item's name.
19
+ * @returns {string} The pointer, or "" for a note with no body.
20
+ */
21
+ export function itemDescription(markdown: string, fm: object, name: string): string;
22
+ /**
23
+ * The Item compile pass of one game system.
24
+ *
25
+ * A subclass declares its {@link SystemItemCompiler.documentSubtypes} and, if
26
+ * its system writes any, the {@link SystemItemCompiler#commonSystem} keys. It
27
+ * declares nothing else: the class is abstract only in the sense that a map is
28
+ * required, and instantiating it without one is a programming error rather than
29
+ * a configuration one.
30
+ */
31
+ export class SystemItemCompiler extends BasePackCompiler {
32
+ /**
33
+ * The note-type → document-subtype map this pass compiles against.
34
+ *
35
+ * Stated by the class rather than reached for through a module import, so
36
+ * every decision the pass makes — which notes it claims, which subtype each
37
+ * becomes, which registry builds it, which block it reads — reads one
38
+ * declaration. A second system replaces that declaration and nothing else.
39
+ *
40
+ * @type {import("./document-subtypes.mjs").DocumentSubtypeMap|undefined}
41
+ */
42
+ static documentSubtypes: import("./document-subtypes.mjs").DocumentSubtypeMap | undefined;
43
+ /**
44
+ * The frontmatter block this pass reads, and the registry it addresses.
45
+ *
46
+ * @returns {string} The system's block name.
47
+ */
48
+ get system(): string;
49
+ /**
50
+ * How many of each item type this pass wrote, for the summary. Every type
51
+ * is present from the start so the tally reads as a census of the
52
+ * whitelist rather than of what happened to compile.
53
+ *
54
+ * @type {Record<string, number>}
55
+ */
56
+ counts: Record<string, number>;
57
+ /**
58
+ * The Foundry Item subtype a note compiles into.
59
+ *
60
+ * **Looked up, not inferred.** For every type this system declares, the
61
+ * emitted subtype is the map's, so the note vocabulary and the document
62
+ * vocabulary are two separately-stated things rather than one string
63
+ * written twice (#79).
64
+ *
65
+ * **A type the map does not name belongs to the consumer**, and its
66
+ * registry entry is the declaration: a repository shipping an item type of
67
+ * its own writes it once, in the `itemBuilders` table of its
68
+ * `package-build.config.yaml`, and that key is what the document is a
69
+ * subtype of. That is an authored statement in the consumer's own
70
+ * configuration, not a coincidence inside this package's source — and
71
+ * refusing it here would silently drop every document of a type this system
72
+ * has no opinion about (#7/#1563).
73
+ *
74
+ * @param {object} fm - The note's frontmatter.
75
+ * @returns {string} The document's `type`.
76
+ */
77
+ itemSubtype(fm: object): string;
78
+ /** An item is named by its own type in the log, not by "item". */
79
+ noteLabel(fm: any): any;
80
+ /**
81
+ * The `system` keys this system's compiler writes on **every** item, beside
82
+ * whatever the type's declared fields emit.
83
+ *
84
+ * Nothing by default, which is the honest position for a system that has
85
+ * not said otherwise: a key written here lands on every document of every
86
+ * type, so inventing one that the receiving DataModel does not declare
87
+ * would be a finding on the whole pack (#155).
88
+ *
89
+ * @param {object} fm - The note's frontmatter.
90
+ * @param {object} at - What the pass already knows about this note.
91
+ * @param {string} at.description - The pointer to the note's item doc.
92
+ * @param {string} at.markdown - The note body, tables expanded and
93
+ * wikilinks resolved, for a system that reads an anchored section out of
94
+ * it.
95
+ * @param {string} at.label - Human-readable context for error messages.
96
+ * @returns {object} The shared `system` fields.
97
+ */
98
+ commonSystem(fm: object, { description, markdown, label }: {
99
+ description: string;
100
+ markdown: string;
101
+ label: string;
102
+ }): object;
103
+ /**
104
+ * The document's `flags` — whatever the note authors, and whatever this
105
+ * system writes there of its own accord.
106
+ *
107
+ * The authored flags alone by default, which is the honest position for a
108
+ * system that has not said otherwise. A system whose data model has nowhere
109
+ * to record a shared fact keeps it here instead: HM3 writes the template
110
+ * priority as `flags.hm3.templatePriority`, because it declares no `system`
111
+ * field for it and an undeclared `system` key is discarded at load without
112
+ * a word.
113
+ *
114
+ * **This is the one emitted key nothing else can check** (#283). A `system`
115
+ * key this pass invents is caught by the emitted-`system` check against the
116
+ * receiving schema, but a flag is declared by no schema — so an omission
117
+ * here is silent, and was: the Actor pass wrote the priority and this one
118
+ * did not, for as long as there had been two passes.
119
+ *
120
+ * @param {object} fm - The note's frontmatter.
121
+ * @returns {object} The flags to emit.
122
+ */
123
+ commonFlags(fm: object): object;
124
+ /** @inheritdoc */
125
+ onCompiled(fm: any): void;
126
+ /** @inheritdoc */
127
+ reportCompiled(stats: any): void;
128
+ /** @inheritdoc */
129
+ reportDetail(stats: any): void;
130
+ }
131
+ import { BasePackCompiler } from "./base-compiler.mjs";
@@ -21,35 +21,73 @@ export function splitPages(body: any, leadName?: string): {
21
21
  markdown: string;
22
22
  }[];
23
23
  /**
24
- * Two headings in one note sharing an `{#anchor}` derive the same page id, which
25
- * the LevelDB packer reports only as an opaque duplicate-key collision. Catch it
26
- * here, where the note and the slug can be named.
24
+ * Two pages in one note that would derive the same id, which the LevelDB packer
25
+ * reports only as an opaque duplicate-key collision. Catch it here, where the
26
+ * note and the page can be named.
27
27
  *
28
- * @param {Array<{anchorSlug: string|null}>} rawPages - From {@link splitPages}.
28
+ * Both halves of {@link journalPageId} are checked, because each is now keyed
29
+ * on an identity alone:
30
+ *
31
+ * - **An anchor**, declared twice, has always collided.
32
+ * - **A name**, repeated among the unanchored pages, collides since #268 took
33
+ * the index out of the key. `MD024` with `siblings_only` already makes two
34
+ * sibling headings with the same text a lint error, so this is the same rule
35
+ * restated where the build can enforce it — a lint is a separate command, and
36
+ * the compile must not depend on someone having run it.
37
+ *
38
+ * The two are counted separately: an anchored page takes its id from the slug
39
+ * and an unanchored one from the name, so a page named for another's anchor is
40
+ * not a collision.
41
+ *
42
+ * @param {Array<{anchorSlug: string|null, name: string}>} rawPages - From
43
+ * {@link splitPages}.
44
+ * @param {string} noteName - The note, for the error message.
45
+ * @throws {Error} When two pages in one note share an anchor or a name.
46
+ */
47
+ export function assertUniquePages(rawPages: Array<{
48
+ anchorSlug: string | null;
49
+ name: string;
50
+ }>, noteName: string): void;
51
+ /**
52
+ * The anchor half of {@link assertUniquePages}, under its former name.
53
+ *
54
+ * @deprecated Call {@link assertUniquePages}, which checks page names too.
55
+ * @param {Array<{anchorSlug: string|null, name: string}>} rawPages - From
56
+ * {@link splitPages}.
29
57
  * @param {string} noteName - The note, for the error message.
30
- * @throws {Error} When an anchor is declared twice in the same note.
31
58
  */
32
59
  export function assertUniqueAnchors(rawPages: Array<{
33
60
  anchorSlug: string | null;
61
+ name: string;
34
62
  }>, noteName: string): void;
35
63
  /**
36
64
  * The id of one page within its entry.
37
65
  *
38
66
  * An anchored page takes the id its inbound links compute from the note id and
39
67
  * the slug, so link and page agree without shared state. Every other page is
40
- * keyed by its position and name, which is what lets the items pass address an
41
- * item doc's first page without having compiled it (see
68
+ * keyed by its **name**, which is what lets the items pass address an item
69
+ * doc's first page without having compiled it (see
42
70
  * {@link sohl.utils.packs.itemDocPointer}).
43
71
  *
72
+ * **It takes no index** (#268). A page used to be keyed by position *and* name,
73
+ * so inserting a heading renumbered every page after it and a re-import created
74
+ * new pages beside the old ones — while nothing about those pages had changed.
75
+ * The anchored case above never took one, and is the shape this now shares.
76
+ *
77
+ * The name is a sound identity here in a way it is not for an embedded item: a
78
+ * page's name is its heading, and `MD024` with `siblings_only` is in the shared
79
+ * markdownlint rule set, so two sibling headings with the same text are already
80
+ * a lint error. {@link assertUniquePages} states the same thing at compile
81
+ * time, where the packer would otherwise report only an opaque duplicate key.
82
+ *
44
83
  * @param {string} entryId - The owning JournalEntry's `_id`.
45
84
  * @param {{anchorSlug: string|null, name: string}} page - From {@link splitPages}.
46
- * @param {number} index - The page's position in the entry.
47
85
  * @returns {string} A 16-character Foundry id.
48
86
  */
49
87
  export function journalPageId(entryId: string, page: {
50
88
  anchorSlug: string | null;
51
89
  name: string;
52
- }, index: number): string;
90
+ }): string;
53
91
  /**
54
92
  * Compile split pages into JournalEntryPage documents.
55
93
  *
@@ -0,0 +1,226 @@
1
+ /**
2
+ * What a package's content index is called, wherever it is written or fetched.
3
+ *
4
+ * **The local index and the published artifact are one file.** A package emits
5
+ * this, ships it as a release asset, and advertises it as `flags.metadataUrl`;
6
+ * a consumer fetches that same file into its cache and reads it. Naming it in
7
+ * one function is what keeps the emitter, the release and the fetcher from
8
+ * drifting into three spellings of one artifact.
9
+ *
10
+ * The `-metadata` suffix earns its place: a bare `<package>.jsonl` says nothing
11
+ * about what it holds, and these files land in a cache directory beside other
12
+ * packages' artifacts where the name is all a reader has.
13
+ *
14
+ * @param {string} pkg - The content package name.
15
+ * @returns {string} The file name, e.g. `sohl-metadata.jsonl`.
16
+ */
17
+ export function metadataFileName(pkg: string): string;
18
+ /**
19
+ * Every dependency whose published index this build resolves addresses through.
20
+ *
21
+ * **Every declared dependency, not only those supplying an item catalogue.**
22
+ * `itemCatalog: true` says a dependency supplies *items*; citing its
23
+ * *addresses* is a separate edge, and a package may have either without the
24
+ * other. `harn-ensemble` cites no foreign address and carries 324,016 embedded
25
+ * item references (`HeroicLands/harn-ensemble#42`); a package citing addresses
26
+ * and needing no items is the mirror of it. Gating the index on the catalogue
27
+ * flag would serve neither.
28
+ *
29
+ * The declaration is the one already in the emitted `system.json` /
30
+ * `module.json`, so it cannot drift from what Foundry itself installs, and
31
+ * there is no new configuration key to keep in step. Each entry carries the
32
+ * producer's own `manifest` URL, so the fetcher needs no address of its own:
33
+ * it reads that manifest and takes the `flags.metadataUrl` it advertises.
34
+ *
35
+ * @param {object} config - The resolved build configuration.
36
+ * @returns {Array<{id: string, manifest: string, kind: string,
37
+ * verified: string|undefined}>} The dependencies, in declaration order.
38
+ */
39
+ export function metadataRelationships(config: object): Array<{
40
+ id: string;
41
+ manifest: string;
42
+ kind: string;
43
+ verified: string | undefined;
44
+ }>;
45
+ /**
46
+ * The cache directory for one dependency's index at one version.
47
+ *
48
+ * Keyed by version so that changing the pinned version is a different cache
49
+ * rather than a silent overwrite, and so a second build costs nothing — the
50
+ * same rule the item catalogue's cache follows, for the same reason.
51
+ *
52
+ * @param {object} config - The resolved build configuration.
53
+ * @param {string} id - The dependency's package id.
54
+ * @param {string} version - Its resolved version.
55
+ * @returns {string} The directory.
56
+ */
57
+ export function metadataCacheDir(config: object, id: string, version: string): string;
58
+ /**
59
+ * Mark a dependency's cache complete.
60
+ *
61
+ * @param {string} dir - The dependency's cache directory.
62
+ * @returns {void}
63
+ */
64
+ export function markComplete(dir: string): void;
65
+ /**
66
+ * The fetched index files this build resolves foreign addresses against.
67
+ *
68
+ * **Reads the cache only.** A cold cache is an error naming the command that
69
+ * fills it, rather than a download nobody asked for: a compile that reaches the
70
+ * network is not reproducible and fails strangely offline. That is the item
71
+ * catalogue's rule, and it holds here for the same reason.
72
+ *
73
+ * A half-finished fetch counts as cold. A partial index resolves some addresses
74
+ * and fails others with nothing to distinguish the two, which is worse than
75
+ * resolving none — the failure would read as a typo in whichever note happened
76
+ * to cite the missing half.
77
+ *
78
+ * @param {object} config - The resolved build configuration.
79
+ * @returns {string[]} One index file per declared dependency.
80
+ * @throws {Error} When a declared dependency has not been fetched.
81
+ */
82
+ export function cachedMetadataFiles(config: object): string[];
83
+ /**
84
+ * The same fetched indexes, each paired with the package that published it.
85
+ *
86
+ * The id is what a SQL content table addresses a dependency's notes by
87
+ * (`FROM sohl.notes`, #246), so the pairing has to survive the lookup —
88
+ * {@link cachedMetadataFiles} drops it, and a caller reconstructing the id from
89
+ * the file name would be parsing a path to recover something the declaration
90
+ * already stated.
91
+ *
92
+ * @param {object} config - The resolved build configuration.
93
+ * @returns {Array<{id: string, file: string}>} One entry per declared
94
+ * dependency.
95
+ * @throws {Error} When a declared dependency has not been fetched.
96
+ */
97
+ export function cachedMetadataIndexes(config: object): Array<{
98
+ id: string;
99
+ file: string;
100
+ }>;
101
+ /**
102
+ * The newest cached version among several version-keyed cache directories.
103
+ *
104
+ * Versions are compared **numerically per segment**, not as strings: a plain
105
+ * sort puts `0.8.10` before `0.8.2`, so a build that had cached both would
106
+ * silently resolve against the older one. A fetch always writes the currently
107
+ * declared version, so several present at once means an earlier pin was left
108
+ * behind rather than that a choice is genuinely open.
109
+ *
110
+ * **Both version-keyed caches under `build/cache` choose this way** — the
111
+ * content index here and the item catalogue in
112
+ * {@link module:engine/foreign-catalog} — so the comparison lives in one place
113
+ * rather than being written once per cache. Two copies would be two chances to
114
+ * get it wrong, and the wrong answer is invisible: every cached version is a
115
+ * complete, stamped, perfectly valid artifact, so picking the older one reports
116
+ * nothing and simply resolves against stale data (#272).
117
+ *
118
+ * @param {string[]} dirs - Complete cache directories, named `<id>@<version>`.
119
+ * @returns {string} The newest one.
120
+ */
121
+ export function newestVersionDir(dirs: string[]): string;
122
+ /**
123
+ * Resolve every foreign address this build can cite, from the fetched indexes.
124
+ *
125
+ * The replacement for the vendored link manifest, and deliberately the same
126
+ * return shape — a `Map` from canonical address to `{ url, name, uuid, … }` —
127
+ * so a foreign entry and a local one stay interchangeable at the point of use.
128
+ * What changed is where the data comes from: a file the producer published,
129
+ * not a copy a consumer committed.
130
+ *
131
+ * **A package this build publishes is skipped**, however it got into the cache.
132
+ * A build is authoritative in its own addresses, and reading them back from a
133
+ * fetched artifact would let a stale copy overrule the tree that is being
134
+ * compiled right now. It is also what stops a cycle forming: the mutual
135
+ * vendoring this replaces deadlocked because each package had to read the
136
+ * other's file before it could publish its own.
137
+ *
138
+ * **Nothing here can be stale.** The version gate the manifest needed existed
139
+ * because a vendored copy could sit at any age; a fetched index is pinned to
140
+ * the version the relationship declares, so `stale` now reports only what is
141
+ * genuinely unusable — an unreadable file, or a package with pages and no base
142
+ * to serve them from.
143
+ *
144
+ * @param {object} config - The resolved build configuration.
145
+ * @param {Iterable<string>} localPackages - Packages this build publishes.
146
+ * @param {Record<string, string>} [bases] - Where each package is served.
147
+ * @returns {{index: Map<string, object>, packages: Set<string>,
148
+ * stale: Array<{package: string, reason: string}>}} The resolved addresses,
149
+ * which packages contributed, and what could not be read.
150
+ */
151
+ export function loadForeignIndexes(config: object, localPackages: Iterable<string>, bases?: Record<string, string>): {
152
+ index: Map<string, object>;
153
+ packages: Set<string>;
154
+ stale: Array<{
155
+ package: string;
156
+ reason: string;
157
+ }>;
158
+ };
159
+ /**
160
+ * Where a dependency's fetched index sits, for naming it in a diagnostic.
161
+ *
162
+ * Best effort: the newest complete cache for that package, or the directory it
163
+ * would occupy. A finding has to name *a* file even when the cache is in the
164
+ * state the finding is about.
165
+ *
166
+ * @param {object} config - The resolved build configuration.
167
+ * @param {string} pkg - The dependency's package id.
168
+ * @returns {string} A path to name in a diagnostic.
169
+ */
170
+ export function cachedIndexPath(config: object, pkg: string): string;
171
+ /**
172
+ * Whether a fetched index can still be *addressed*, as distinct from read.
173
+ *
174
+ * A consumer resolves cross-package links by canonical key, so it needs both
175
+ * sides to agree on the key's shape. When they drift the lookup cannot match on
176
+ * *any* input — and because a miss is indistinguishable from a typo, the
177
+ * symptom is a pile of dead addresses blamed on the notes that cite them rather
178
+ * than on the index at fault. A package whose every key is unreadable is
179
+ * therefore reported against the index, once, instead of once per citing note.
180
+ *
181
+ * The realistic cause is a version skew: a dependency released before the
182
+ * address grammar gained its `<system>` segment (#59) ships three-segment keys.
183
+ * Re-fetching after that dependency releases is the fix.
184
+ *
185
+ * @param {Map<string, object>} foreignIndex - The resolved foreign index.
186
+ * @returns {Array<{package: string, entries: number, sampleKey: string}>} One
187
+ * finding per drifted package, in the order the index first names each.
188
+ */
189
+ export function unaddressableForeignPackages(foreignIndex: Map<string, object>): Array<{
190
+ package: string;
191
+ entries: number;
192
+ sampleKey: string;
193
+ }>;
194
+ /**
195
+ * One finding, in the standard `file:line:column: severity: message` form.
196
+ *
197
+ * The position is recovered by locating the offending key in the index text:
198
+ * the finding is about a literal the reader can see in the file, so its
199
+ * position is implicit rather than absent. When the file cannot be read, or the
200
+ * key is not in it, the locator degrades to the file alone — a dropped field,
201
+ * never a guessed `1:1` that would send the reader to the top of a large file
202
+ * for a finding that is not there.
203
+ *
204
+ * @param {{package: string, entries: number, sampleKey: string}} finding - One
205
+ * finding from {@link unaddressableForeignPackages}.
206
+ * @param {object} config - The resolved build configuration.
207
+ * @returns {string} The formatted diagnostic, path first on the line.
208
+ */
209
+ export function formatUnaddressableFinding(finding: {
210
+ package: string;
211
+ entries: number;
212
+ sampleKey: string;
213
+ }, config: object): string;
214
+ /**
215
+ * The relationship kinds that are dependencies, and therefore citable.
216
+ *
217
+ * A package may cite what it depends on and nothing else. `recommends` and
218
+ * `conflicts` are declarations *about* other packages rather than dependencies
219
+ * on them, so an address resolved through one would emit a link into a package
220
+ * the consumer does not require — a defect in the citing note, not a lookup to
221
+ * satisfy.
222
+ *
223
+ * @type {readonly string[]}
224
+ */
225
+ export const METADATA_RELATIONSHIP_KINDS: readonly string[];
226
+ export function isComplete(dir: string): boolean;