@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
@@ -0,0 +1,173 @@
1
+ /**
2
+ * Strip the LevelDB keys from a document tree.
3
+ *
4
+ * An Adventure's members are **inline source data** in a `SetField`, not
5
+ * sublevel documents, so they carry no `_key`: the compendium CLI's hierarchy
6
+ * does not recurse into an adventure, and Foundry's schema has no such field to
7
+ * hold one. A member that kept its key would ship a property the data model
8
+ * refuses.
9
+ *
10
+ * Shared with the scenes pass, which bundles the Adventures that make a pinned
11
+ * map's ids resolve — one rule about what an Adventure member may carry, stated
12
+ * once.
13
+ *
14
+ * @param {*} value - A document, array, or scalar.
15
+ * @returns {*} The same shape with every `_key` removed.
16
+ */
17
+ export function stripAdventureKeys(value: any): any;
18
+ /**
19
+ * An authored address with any wikilink brackets and label stripped.
20
+ *
21
+ * The specification types `contents` as a `WikiLink[]`, and a frontmatter link
22
+ * is written as a bare address — but `[[address]]` is what an author reaches
23
+ * for, and it is what a folder note's `parent` already accepts. Accepting both
24
+ * costs one regex and removes a failure whose message would have to explain the
25
+ * difference.
26
+ *
27
+ * @param {unknown} value - As authored.
28
+ * @returns {string|null} The bare address, or `null` for a blank entry.
29
+ */
30
+ export function bareAddress(value: unknown): string | null;
31
+ /**
32
+ * The addresses a bundle note names, in the order it names them.
33
+ *
34
+ * `contents` is a `data:` property, which is where the specification's
35
+ * `### type: bundle` table puts it — the closed container, so a misspelled
36
+ * `content` is a finding rather than a silently empty Adventure. It is accepted
37
+ * at the top level too, exactly as a folder note's `parent` is: an author
38
+ * following #259's own example rather than the specification should get a
39
+ * bundle, not a silent default.
40
+ *
41
+ * **Order is the author's**, and it is kept: an Adventure's `SetField` attaches
42
+ * no meaning to member order, but the emitted JSON is compared between runs, so
43
+ * a stable order is what keeps the same tree compiling to the same bytes.
44
+ *
45
+ * @param {object} fm - Parsed frontmatter.
46
+ * @returns {string[]} The bare addresses. Empty when the note states none.
47
+ * @throws {Error} When `contents` is neither absent nor a list — a scalar is a
48
+ * bundle of one written wrongly, and reading it as such would accept a shape
49
+ * the specification does not admit.
50
+ */
51
+ export function bundleContents(fm: object): string[];
52
+ /**
53
+ * What a pass should do about a member its sources do not hold.
54
+ *
55
+ * The whole of the system rule, in one predicate, because it is a rule and not
56
+ * an implementation detail. An `Adventure` has no `system` field, so a bundle
57
+ * spanning two systems cannot be one document that knows it spans them: it is
58
+ * one Adventure per system, and **the pack each is written to is what carries
59
+ * the system**.
60
+ *
61
+ * The constraint is not computed from the member's *type* — that would be wrong
62
+ * for the types both systems map, which is most of them: a `miscgear` is a SoHL
63
+ * item **and** an HM3 item, so "which system does this type belong to" has no
64
+ * single answer. It is read instead from what the pack can actually see. A pack
65
+ * declaring `system: hm3` reads the HM3 packs and the system-neutral ones, so a
66
+ * note that publishes no HM3 document is simply not in its sources — which is
67
+ * the same fact, established where it is already true.
68
+ *
69
+ * - **The pack declares a system.** A member it cannot see is another system's,
70
+ * and is left **out** — reported, never silently, because an installer that
71
+ * quietly ships half its contents is worse than one that fails.
72
+ * - **The pack declares none.** Nothing is scoped away, so there is no other
73
+ * system for the member to have gone to: its absence is a **failure**.
74
+ *
75
+ * @param {string|null|undefined} packSystem - The pack's declared `system:`.
76
+ * @returns {"omit"|"fail"} What to do about a member the sources lack.
77
+ */
78
+ export function missingMemberVerdict(packSystem: string | null | undefined): "omit" | "fail";
79
+ /**
80
+ * Assemble one `Adventure` from a set of already-compiled documents.
81
+ *
82
+ * The members are **copies**, and they arrive compiled: this takes the JSON a
83
+ * previous pass wrote and files each document under the content field its class
84
+ * maps to. Nothing is derived from the content tree here, which is what keeps
85
+ * this half framework-free and testable without one.
86
+ *
87
+ * @param {object} params
88
+ * @param {string} params.id - The Adventure's `_id`.
89
+ * @param {string} params.name - Its name.
90
+ * @param {string|null} [params.img] - Its artwork, or `null`.
91
+ * @param {string} [params.description] - The note's prose, already rendered.
92
+ * @param {string} [params.caption] - The short caption Foundry shows on the
93
+ * import card. Blank unless a caller has one.
94
+ * @param {string|null} [params.folder] - The folder id it is filed under.
95
+ * @param {object} [params.flags] - Document flags.
96
+ * @param {object} params.stats - The `_stats` block to stamp.
97
+ * @param {ReadonlyArray<{docType: string, document: object}>} params.contents -
98
+ * The compiled documents it holds, each with the Foundry class it is.
99
+ * @returns {object} The Adventure document, keyed for the pack.
100
+ * @throws {Error} When a member's document class is not one an Adventure can
101
+ * hold — a defect in the caller rather than in the note.
102
+ */
103
+ export function buildAdventure({ id, name, img, description, caption, folder, flags, stats, contents, }: {
104
+ id: string;
105
+ name: string;
106
+ img?: string | null | undefined;
107
+ description?: string | undefined;
108
+ caption?: string | undefined;
109
+ folder?: string | null | undefined;
110
+ flags?: object | undefined;
111
+ stats: object;
112
+ contents: ReadonlyArray<{
113
+ docType: string;
114
+ document: object;
115
+ }>;
116
+ }): object;
117
+ /**
118
+ * Bundles, as notes — the Foundry `Adventure` a `type: bundle` note compiles
119
+ * into.
120
+ *
121
+ * An `Adventure` is badly named, and the name misled the first design: it is
122
+ * not a story. `Adventure.contentFields` maps each `SetField` on the schema to
123
+ * a document class, and importing one partitions its members by whether the
124
+ * world's collection already holds that `_id`, then creates or updates each.
125
+ * Afterwards the documents live independently and the Adventure has no further
126
+ * role. **It is an installer** — a set of document *copies* packaged for
127
+ * one-shot import (#259).
128
+ *
129
+ * That is the whole difference from a folder (#256), which is a live grouping
130
+ * **by reference** that persists in the pack. A bundle carries copies and
131
+ * exists to be imported once, so the two are not variations on one idea.
132
+ *
133
+ * The note type is `bundle` rather than `adventure` because the format prefers
134
+ * the domain word wherever Foundry's misleads — a `Scene` is authored as a
135
+ * `map` — and `collection` was refused because `DocumentCollection` is a real
136
+ * Foundry class meaning very nearly the opposite. See `docs/content-format.md`
137
+ * § `type: bundle`.
138
+ *
139
+ * This module is the framework-free half: what a bundle note *says*, and how a
140
+ * set of already-compiled documents becomes an Adventure. The pass that walks
141
+ * the tree and resolves what one note says about another is
142
+ * {@link module:engine/bundles}.
143
+ *
144
+ * @module
145
+ */
146
+ /**
147
+ * The note type a bundle is authored as.
148
+ *
149
+ * @type {string}
150
+ */
151
+ export const BUNDLE_TYPE: string;
152
+ /**
153
+ * Foundry's `Adventure.contentFields`, keyed by the document class each holds.
154
+ *
155
+ * Restated here rather than derived, because this build never loads Foundry:
156
+ * the schema is read from `common/documents/adventure.mjs`, where every
157
+ * `SetField` of an `EmbeddedDataField` is a content field and its name is the
158
+ * key an importer partitions on. The inverse direction is the one a compiler
159
+ * wants — it has a document type in hand and needs the field to file it under.
160
+ *
161
+ * The full set is listed, not only the five this build compiles, because the
162
+ * map is a statement about Foundry rather than about this toolchain: a consumer
163
+ * registering a compiler for `RollTable` needs no edit here.
164
+ *
165
+ * @type {Readonly<Record<string, string>>}
166
+ */
167
+ export const ADVENTURE_CONTENT_FIELD: Readonly<Record<string, string>>;
168
+ /**
169
+ * The `data:` key a bundle lists its members under.
170
+ *
171
+ * @type {string}
172
+ */
173
+ export const CONTENTS_FIELD: string;
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Load every compiled document a bundle may hold, keyed `<docType>/<id>`.
3
+ *
4
+ * Keyed by id rather than by address because that is what the note resolves to:
5
+ * an address names a note, the note's id is derived once by `resolveNoteId`,
6
+ * and every pass files its document under it. Matching on the id is therefore
7
+ * an identity check rather than a second derivation that could disagree with
8
+ * the first.
9
+ *
10
+ * A **folder** document is skipped. It is emitted into every pack that holds
11
+ * something filed in it (#257), so it is not one pack's document and has no
12
+ * single note behind it; a bundle that wants folders is a question this pass
13
+ * refuses rather than guesses at — see {@link Bundles#resolveAddress}.
14
+ *
15
+ * @param {Readonly<Record<string, readonly string[]>>} sourceDirs - The JSON
16
+ * directories of every pack whose output may be bundled, by document type.
17
+ * @returns {Map<string, object>} The compiled documents.
18
+ * @throws {Error} When a declared directory does not exist — the generator
19
+ * orders this pass last, so what reaches this is a run restricted to one
20
+ * pack, which reordering cannot fix.
21
+ */
22
+ export function loadBundleSources(sourceDirs: Readonly<Record<string, readonly string[]>>): Map<string, object>;
23
+ export class Bundles extends BasePackCompiler {
24
+ /**
25
+ * @param {object} options - As {@link BasePackCompiler}, plus:
26
+ * @param {Record<string, readonly string[]>} [options.bundleSourceDirs] -
27
+ * Each bundleable pack's JSON tree, by document type. Supplied by the
28
+ * generator from the configured pack list, so the dependency is stated
29
+ * rather than assumed from a sibling directory.
30
+ */
31
+ constructor({ bundleSourceDirs, ...options }: {
32
+ bundleSourceDirs?: Record<string, readonly string[]> | undefined;
33
+ });
34
+ /**
35
+ * The JSON directories this pass reads its members from, by document type.
36
+ *
37
+ * @type {Readonly<Record<string, readonly string[]>>}
38
+ */
39
+ bundleSourceDirs: Readonly<Record<string, readonly string[]>>;
40
+ members: Map<string, object> | undefined;
41
+ /**
42
+ * The document one address in `contents` names.
43
+ *
44
+ * The address alone, resolved against the content tree — whether this pack
45
+ * actually *holds* that document is the caller's question, and the answer
46
+ * to it is what the system rule turns on.
47
+ *
48
+ * @param {string} address - One bare address from `contents`.
49
+ * @param {string} bundleName - The bundle, for the message.
50
+ * @returns {{docType: string, id: string, name: string}} The document the
51
+ * address names.
52
+ * @throws {Error} When the address names nothing this build compiles.
53
+ */
54
+ resolveAddress(address: string, bundleName: string): {
55
+ docType: string;
56
+ id: string;
57
+ name: string;
58
+ };
59
+ }
60
+ import { BasePackCompiler } from "./base-compiler.mjs";
@@ -1,3 +1,46 @@
1
+ /**
2
+ * A fence's info string, read as **org-babel header arguments**.
3
+ *
4
+ * A directive fence carries statements *about the directive* that are no part of
5
+ * the query it holds — whether an empty result is intended, what heading level a
6
+ * section takes, later a caption. Those started as an ad-hoc bare word
7
+ * (`allow-empty`) and a `key=value` (`section-level=3`), each matched by its own
8
+ * regex: a grammar only in the sense that two regexes are one, and with no room
9
+ * to add a third property without adding a third spelling.
10
+ *
11
+ * Org-mode settled this long ago. A babel source block writes them after the
12
+ * language as `:key value`, which is a real grammar with a specification, a
13
+ * parser, and an editor that already completes it.
14
+ *
15
+ * ```text
16
+ * ```sql :section-level 3 :allow-empty
17
+ * ```
18
+ *
19
+ * **The language word stays first and stays plain.** `sql` is what GitHub,
20
+ * Prettier and every other markdown reader match on to syntax-highlight the
21
+ * block, so it leads and the header args follow; a reader that does not know
22
+ * them sees an ordinary SQL block.
23
+ *
24
+ * The grammar, matching org's:
25
+ *
26
+ * - a key is `:name` **starting a word** — a colon inside or ending a word is
27
+ * text, so `:caption Gear: the tables` is one argument;
28
+ * - a value runs to the next key or the end of the string, spaces included, and
29
+ * is trimmed;
30
+ * - a key with no value is `true`, which is what a statement like
31
+ * `:allow-empty` is;
32
+ * - a value may be `"quoted"` to hold a word that would otherwise read as a
33
+ * key — the one ambiguity org has too;
34
+ * - a repeated key takes its last value.
35
+ *
36
+ * @param {string} info - The text after the fence marker.
37
+ * @returns {{language: string, args: Record<string, string|true>}} The language,
38
+ * lowercased, and the header arguments in written order.
39
+ */
40
+ export function parseHeaderArgs(info: string): {
41
+ language: string;
42
+ args: Record<string, string | true>;
43
+ };
1
44
  /**
2
45
  * Every code region in a markdown body, as character offsets into it.
3
46
  *
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Derive the corpus a compile runs over, and the three indexes built over it.
3
+ *
4
+ * @param {object} opts - Options.
5
+ * @param {string} opts.contentBase - Root of the content tree.
6
+ * @param {readonly string[]} opts.skipDirectories - The scope, stated by the
7
+ * caller as every corpus read requires (#243).
8
+ * @param {object} opts.router - The pack router this compile resolved. Shared
9
+ * by every pass, which is what makes one link index correct for all of them.
10
+ * @param {object} [opts.config] - The resolved configuration.
11
+ * @param {object[]} [opts.problems] - Collects the notes the index cannot
12
+ * record, so one of them does not abort the compile before it reports. One is
13
+ * created when none is passed, and returned on the corpus either way — a note
14
+ * the index refuses is a note the compile must still *report*, exactly as the
15
+ * compile loop reported it when the loop was the first to see it.
16
+ * @returns {Promise<{records: object[], linkIndex: object, contentDocs: object[],
17
+ * sqlTables: Map<string, object[]>|undefined, problems: object[]}>} The corpus,
18
+ * its indexes, and the notes it could not record.
19
+ */
20
+ export function buildCompileCorpus({ contentBase, skipDirectories, router, config, problems, }: {
21
+ contentBase: string;
22
+ skipDirectories: readonly string[];
23
+ router: object;
24
+ config?: object | undefined;
25
+ problems?: object[] | undefined;
26
+ }): Promise<{
27
+ records: object[];
28
+ linkIndex: object;
29
+ contentDocs: object[];
30
+ sqlTables: Map<string, object[]> | undefined;
31
+ problems: object[];
32
+ }>;
@@ -1,10 +1,18 @@
1
1
  /**
2
2
  * The single path segment a note is addressed by: `type-shortcode`.
3
3
  *
4
- * Lowercased, so it is exactly the tail of the note's canonical key
5
- * (`canonicalKey` in `engine/kb-manifest.mjs` lowercases too) which is what
6
- * makes a manifest entry's `path` derivable from the key it is filed under
7
- * rather than transported beside it.
4
+ * Lowercased and hyphen-joined by the same rule as the note's canonical key
5
+ * ({@link canonicalKey}, below, lowercases too), so it is that
6
+ * key's **last two segments** — which is what makes a manifest entry's `path`
7
+ * derivable from the key it is filed under rather than transported beside it.
8
+ *
9
+ * It was once the key's whole tail, and #59 ended that: the key gained a
10
+ * `<system>` segment, so its tail is now `system-type-shortcode` and a slug is
11
+ * the tail with that segment dropped. The behaviour here is unchanged, and
12
+ * deliberately — a page has no system to name (see the module note above), so
13
+ * the two forms diverge rather than one having fallen behind the other. A
14
+ * consumer deriving a `path` from a key drops the *package and the system*, not
15
+ * the package alone.
8
16
  *
9
17
  * The hyphen is a separator and never occurs inside a segment: a shortcode is
10
18
  * `^[A-Za-z0-9]+$` (`ADDRESS_SEGMENT_PATTERN`, enforced by `content-lint.mjs`)
@@ -56,7 +64,199 @@ export function addressSlug(fm: object): string;
56
64
  * dead entry in the manifest.
57
65
  */
58
66
  export function packageAddress(fm: object): string;
59
- export { DEFAULT_ADDRESS_SCHEME };
67
+ /**
68
+ * The **canonical** address of a note: fully qualified, one spelling per
69
+ * document, and globally unique.
70
+ *
71
+ * The written form of a link is a **partial** address: it may omit leading
72
+ * segments, and each one it omits is filled in by rule rather than left
73
+ * unconstrained. An omitted package (`[[skill-lang]]`) defaults to the citing
74
+ * note's own, so an unqualified link resolves locally and only locally, and a
75
+ * link into another package must name it. An omitted **system** is a
76
+ * *wildcard*, not a default — most links target items, which belong to a
77
+ * system — and the resolver requires exactly one match: none is a dead link,
78
+ * more than one is an ambiguity reported with every candidate named.
79
+ * Everything internal — index keys, cache keys, every lookup — uses this fully
80
+ * qualified form instead, so no consumer has to know what a short form
81
+ * defaulted to or matched.
82
+ *
83
+ * Global uniqueness is what lets a dependency's index merge straight into a
84
+ * local one: the keys cannot collide by accident, so a key already present on
85
+ * merge is a real conflict rather than an artefact of two packages sharing a
86
+ * namespace. `(type, shortcode)` alone is unique only *within* a package, and
87
+ * two independently authored packages reaching for the same short string is a
88
+ * matter of time (#1499).
89
+ *
90
+ * **The system segment (#59).** A package may ship content for more than one
91
+ * system, and one note then compiles into a document per system — an actor in
92
+ * `actors-sohl` *and* an actor in `actors-hm3`. Without a system segment both
93
+ * land on one key, so the address cannot name either of them. `harn-ensemble`
94
+ * carries 2,497 such notes.
95
+ *
96
+ * The value is a system id, or the literal **`none`** for a document no game
97
+ * system defines: a journal, a macro, a scene, and an item's documentation
98
+ * journal — which is `none` however many systems the item itself declares,
99
+ * because it is one journal.
100
+ *
101
+ * `none` rather than `any`: every segment of an address is an exact literal,
102
+ * and `any` reads as a wildcard — "matches under any system" — which is not
103
+ * what it does. A resolver written to that misreading would fail silently,
104
+ * since a lookup miss already returns nothing rather than erroring. And not
105
+ * `null` or `~`, both of which are YAML nulls that parse to an absent value and
106
+ * drop the segment entirely.
107
+ *
108
+ * @param {string} pkg - The owning **content** package (`sohl`, `thalorna`) —
109
+ * not the Foundry package, which varies per compilation target.
110
+ * @param {string} system - The system whose document this addresses, or `none`.
111
+ * @param {string} type - The note's `type`.
112
+ * @param {string} shortcode - The note's `shortcode`.
113
+ * @returns {string} `package-system-type-shortcode`, lowercased.
114
+ */
115
+ export function canonicalKey(pkg: string, system: string, type: string, shortcode: string): string;
116
+ /**
117
+ * Reads a canonical key back into its parts.
118
+ *
119
+ * Parsing is plain positional counting: split on the separator, require
120
+ * {@link CANONICAL_KEY_SEGMENTS} of them, and assign each position its field.
121
+ * **The charset rule is what makes that sound** — every segment is
122
+ * `^[A-Za-z0-9]+$` (`ADDRESS_SEGMENT_PATTERN` in `engine/address-charset.mjs`),
123
+ * so the hyphen is purely a separator and the count alone determines every
124
+ * field. That is enforced at each of the three sources rather than assumed of
125
+ * the data: shortcodes by `content-lint.mjs` (#1397), `contentPackage` by
126
+ * `defineConfig` (#59), and types are bare words. Were any of them free to
127
+ * carry a hyphen, no amount of counting would recover the fields and the reader
128
+ * would need a vocabulary to match against instead.
129
+ *
130
+ * **Nothing to read and nothing readable are different answers.** A key that
131
+ * cannot be canonical — `harn-adventures-sohl-skill-melee`, five segments,
132
+ * because the package name carries the separator — yields `null`, while an
133
+ * absent or blank input yields `undefined`. Both are falsy, so
134
+ * every call site (all of which test the result for truthiness) is unaffected;
135
+ * the distinction is there so a caller reporting "this key is unreadable" can
136
+ * tell that it has a key to report about.
137
+ *
138
+ * @param {unknown} key - A canonical key, or nothing.
139
+ * @returns {{package: string, system: string, type: string, shortcode: string}
140
+ * |null|undefined}
141
+ * The parts; `null` when there is a string that is not in canonical form;
142
+ * `undefined` when there is no key at all.
143
+ */
144
+ export function readCanonicalKey(key: unknown): {
145
+ package: string;
146
+ system: string;
147
+ type: string;
148
+ shortcode: string;
149
+ } | null | undefined;
150
+ /**
151
+ * Asserts a base is usable as a prefix and returns it.
152
+ *
153
+ * Exported for the URL helpers' own callers: a build that composes a base
154
+ * before resolving against it should reject a malformed one at the point it
155
+ * is chosen, not at the point a link is emitted.
156
+ *
157
+ * @param {string} base - The package base.
158
+ * @param {string} what - What is being resolved, for the error message.
159
+ * @returns {string} The base.
160
+ */
161
+ export function checkBase(base: string, what: string): string;
162
+ /**
163
+ * The package-relative address a site-absolute URL records as.
164
+ *
165
+ * Strips the emitting package's own base, so what is recorded says *where in
166
+ * the package* a page is and nothing about where the package itself is
167
+ * mounted. A URL outside the base is an error rather than a best effort: it
168
+ * would record an address that silently resolves to the wrong place once a
169
+ * consumer prefixes its own base.
170
+ *
171
+ * @param {string} url - The site-absolute URL the emitting build publishes at.
172
+ * @param {string} base - That build's base for the package, e.g. `"/thalorna/"`.
173
+ * @returns {string} The address relative to `base`, with no leading slash.
174
+ */
175
+ export function packageRelative(url: string, base: string): string;
176
+ /**
177
+ * The URL a package-relative address resolves to in this build.
178
+ *
179
+ * Plain concatenation, which is what makes an absolute-origin base work: a base
180
+ * of `"https://thalorna.example.org/"` yields an absolute link, and one of
181
+ * `"/thalorna/"` a root-relative one, with no other rule to keep in step.
182
+ *
183
+ * @param {string} rel - The package-relative address from a fetched index.
184
+ * @param {string} base - This build's base for that package.
185
+ * @returns {string} The resolved URL.
186
+ */
187
+ export function resolvePackageUrl(rel: string, base: string): string;
188
+ /**
189
+ * The Foundry `_id` of the document a note compiles into, derived from its
190
+ * canonical address (#270).
191
+ *
192
+ * A note used to author this — an opaque 16-character string, one per note,
193
+ * that said nothing its address did not and that no check guaranteed. The
194
+ * address is the identity that *is* guaranteed: `content-lint` refuses a
195
+ * duplicate `(type, shortcode)` across every pack of a document type, which is
196
+ * exactly the scope a primary document's id must be unique within. So the
197
+ * derived id inherits a guard that already exists, where the authored one had
198
+ * none.
199
+ *
200
+ * **The coupling this creates, stated plainly.** The address carries the
201
+ * shortcode, so renaming a shortcode moves the document's id — where an
202
+ * authored id survived one. That is a real trade rather than a free win, and
203
+ * two things make it acceptable: a rename already breaks every wikilink to the
204
+ * note, so it is a breaking change either way; and a note that must keep its
205
+ * identity across a rename pins an `id`, which is what the pin is for. One
206
+ * thing genuinely degrades — `engine/address-diff.mjs` tells a rename from a
207
+ * withdrawal by matching document ids, and can no longer do so for a note that
208
+ * authors none. Its module note records that.
209
+ *
210
+ * @param {string} pkg - The owning **content** package.
211
+ * @param {string} system - The system whose document this is, or `none`.
212
+ * @param {string} type - The note's `type`.
213
+ * @param {string} shortcode - The note's `shortcode`.
214
+ * @returns {string} A 16-character Foundry id.
215
+ */
216
+ export function documentId(pkg: string, system: string, type: string, shortcode: string): string;
60
217
  /** The knowledgebase's mount within this package's site (#1470). */
61
218
  export const KB_PREFIX: "kb/";
219
+ /**
220
+ * How many segments a canonical key has, and therefore how many the reader
221
+ * below counts.
222
+ *
223
+ * Named rather than written as a literal because it is the *grammar*, not an
224
+ * implementation detail of one function: it is the number a change to the
225
+ * address form would move, and the thing a reader of that change has to find.
226
+ *
227
+ * @type {number}
228
+ */
229
+ export const CANONICAL_KEY_SEGMENTS: number;
230
+ /**
231
+ * Where this build serves each package, keyed by package name.
232
+ *
233
+ * One line per package, and the only edit a relocation requires: point a
234
+ * package at another path (`"/setting/thalorna/"`) or another origin
235
+ * (`"https://thalorna.example.org/"`) and every inbound link into it follows.
236
+ * A base is a prefix, so it must end in `/`.
237
+ *
238
+ * Only *foreign* packages are consulted — a package this build publishes is
239
+ * authoritative in its own entries and never resolves through a fetched
240
+ * index — but every linkable package is listed, because which are foreign
241
+ * depends on the consuming repository.
242
+ */
243
+ export const PACKAGE_BASE: Readonly<{
244
+ sohl: "/sohl/";
245
+ thalorna: "/thalorna/";
246
+ }>;
247
+ /**
248
+ * The namespace {@link documentId} hashes a canonical address under.
249
+ *
250
+ * Named rather than written as a literal at the one call site, because it is
251
+ * part of the published derivation: a consumer holding a content-index entry
252
+ * recomputes the document's id — and therefore its compendium UUID — as
253
+ * `makeId(DOCUMENT_ID_NAMESPACE, entry.canonical)`, so the string is a fact
254
+ * about the format rather than an implementation detail. Changing it moves
255
+ * every id this toolchain has ever emitted.
256
+ *
257
+ * @type {string}
258
+ */
259
+ export const DOCUMENT_ID_NAMESPACE: string;
62
260
  import { DEFAULT_ADDRESS_SCHEME } from "../content-config.mjs";
261
+ import { NO_SYSTEM } from "./systems.mjs";
262
+ export { DEFAULT_ADDRESS_SCHEME, NO_SYSTEM };
@@ -20,10 +20,14 @@ export function declaredPaths(artifact: object): Set<string>;
20
20
  * {@link undeclaredMessage} draws, and the difference between "the
21
21
  * specification is wrong" and "the schema has not caught up".
22
22
  *
23
- * @param {object} finding - `{system, systemVersion, noteType, source, target}`.
23
+ * A shared row names no type, so it is placed rather than typed: "in the shared
24
+ * mappings" instead of "on a `weapongear`". Reading `on a \`the shared
25
+ * mappings\`` would be the alternative, and a diagnostic is prose (#275).
26
+ *
27
+ * @param {object} finding - `{system, systemVersion, noteType, source, target, shared}`.
24
28
  * @returns {string} The message.
25
29
  */
26
- export function undeclaredTargetMessage({ system, systemVersion, noteType, source, target }: object): string;
30
+ export function undeclaredTargetMessage({ system, systemVersion, noteType, source, target, shared, }: object): string;
27
31
  /**
28
32
  * Check every `system.*` target the specification names.
29
33
  *
@@ -30,6 +30,53 @@ export function loadContentFormat(file?: string): ContentFormat;
30
30
  * @type {string}
31
31
  */
32
32
  export const CONTENT_FORMAT_PATH: string;
33
+ /**
34
+ * What one note type's section declares.
35
+ *
36
+ * @typedef {object} TypeSpec
37
+ * @property {string} name - The note type, as the `### type:` heading spells it.
38
+ * @property {number} line - 1-based line of that heading.
39
+ * @property {Set<string>} dataKeys - The head segment of each declared `data`
40
+ * property — what a note actually writes. `appearance.eye_color` is authored
41
+ * as `appearance`, so that is the key recorded.
42
+ * @property {Set<string>} dataPaths - The declared paths, whole.
43
+ */
44
+ /**
45
+ * One `system.*` target the specification names for one note type.
46
+ *
47
+ * @typedef {object} MappingClaim
48
+ * @property {string} noteType - The type whose section makes the claim, or
49
+ * `the shared mappings` for a row of the shared tables — see `shared`.
50
+ * @property {boolean} [shared] - Whether the row came from a **shared** mapping
51
+ * table, which stands before the first `### type:` heading and states what
52
+ * every type maps identically (#275). Absent on a per-type row, so the two
53
+ * never mix: only a per-type row has a field declaration to be checked
54
+ * against.
55
+ * @property {string} system - The system column it sits under, from the header.
56
+ * @property {string} source - The shared source cell, stripped of its backticks.
57
+ * @property {string} target - The dotted path, `system.` prefix included.
58
+ * @property {number} line - 1-based line of the row.
59
+ * @property {number} column - 1-based column of the cell's first character.
60
+ */
61
+ /**
62
+ * The specification, as data.
63
+ *
64
+ * @typedef {object} ContentFormat
65
+ * @property {string} file - Where it was read from, for diagnostics.
66
+ * @property {Map<string, TypeSpec>} types - Note type → what its section declares.
67
+ * @property {MappingClaim[]} claims - Every `system.*` target, in document order.
68
+ */
69
+ /**
70
+ * What a shared row's `noteType` reads, in place of a type name.
71
+ *
72
+ * Phrased to be substituted into a diagnostic sentence — "the format maps `x`
73
+ * in the shared mappings to `y`" — because that is the only place it is ever
74
+ * read. Both shared tables use it: the second states what the actor types add,
75
+ * and a row of it is no more a `being`'s than a row of the first is.
76
+ *
77
+ * @type {string}
78
+ */
79
+ export const SHARED_SCOPE: string;
33
80
  /**
34
81
  * What one note type's section declares.
35
82
  */
@@ -58,9 +105,18 @@ export type TypeSpec = {
58
105
  */
59
106
  export type MappingClaim = {
60
107
  /**
61
- * - The type whose section makes the claim.
108
+ * - The type whose section makes the claim, or
109
+ * `the shared mappings` for a row of the shared tables — see `shared`.
62
110
  */
63
111
  noteType: string;
112
+ /**
113
+ * - Whether the row came from a **shared** mapping
114
+ * table, which stands before the first `### type:` heading and states what
115
+ * every type maps identically (#275). Absent on a per-type row, so the two
116
+ * never mix: only a per-type row has a field declaration to be checked
117
+ * against.
118
+ */
119
+ shared?: boolean | undefined;
64
120
  /**
65
121
  * - The system column it sits under, from the header.
66
122
  */