@heroiclands/package-build 23.0.0 → 23.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/README.md +1 -1
  3. package/bin/content-commands.mjs +1 -1
  4. package/ci/content-format-reference.mjs +46 -0
  5. package/docs/api.md +124 -141
  6. package/docs/authoring/assets.md +1 -1
  7. package/docs/authoring/dates-and-calendars.md +12 -6
  8. package/docs/authoring/frontmatter.md +2 -2
  9. package/docs/authoring/links-and-markup.md +145 -29
  10. package/docs/commands.md +8 -10
  11. package/docs/configuration.md +13 -11
  12. package/docs/content-format.md +2 -2
  13. package/docs/guides/book.md +59 -4
  14. package/docs/guides/icons.md +4 -5
  15. package/docs/guides/packs.md +6 -0
  16. package/docs/guides/reference-pages.md +21 -1
  17. package/docs/guides/release-and-deploy.md +39 -7
  18. package/docs/guides/site.md +17 -18
  19. package/docs/reference/format-details.md +877 -276
  20. package/docs/reference/note-types.md +278 -91
  21. package/docs/reference/theme-pages.md +58 -97
  22. package/engine/address-values.mjs +124 -4
  23. package/engine/address.mjs +44 -3
  24. package/engine/anchors.mjs +103 -3
  25. package/engine/art-slots.mjs +2 -9
  26. package/engine/base-compiler.mjs +25 -6
  27. package/engine/calendar-artifacts.mjs +4 -1
  28. package/engine/calendar-notes.mjs +166 -102
  29. package/engine/content-format-check.mjs +2 -3
  30. package/engine/content-format.mjs +44 -1
  31. package/engine/content-format.yaml +79 -16
  32. package/engine/content-images.mjs +10 -1
  33. package/engine/content-index.mjs +37 -9
  34. package/engine/content-links.mjs +24 -5
  35. package/engine/content-tables.mjs +24 -3
  36. package/engine/data-keys.mjs +347 -0
  37. package/engine/derived-sections.mjs +322 -0
  38. package/engine/diagnostics.mjs +28 -0
  39. package/engine/event-fields.mjs +187 -0
  40. package/engine/event-rows.mjs +126 -0
  41. package/engine/event-views.mjs +305 -0
  42. package/engine/exported-scene.mjs +29 -53
  43. package/engine/frontmatter-lint.mjs +103 -77
  44. package/engine/frontmatter.mjs +7 -2
  45. package/engine/generate.mjs +53 -0
  46. package/engine/generated-sections.mjs +223 -0
  47. package/engine/heading-attributes.mjs +4 -2
  48. package/engine/helpers.mjs +12 -1
  49. package/engine/holdings.mjs +12 -33
  50. package/engine/ids.mjs +17 -11
  51. package/engine/index.mjs +4 -4
  52. package/engine/infobox-render.mjs +21 -6
  53. package/engine/infobox.mjs +217 -99
  54. package/engine/journals.mjs +104 -47
  55. package/engine/literature-notes.mjs +4 -1
  56. package/engine/literature-works.mjs +21 -13
  57. package/engine/map-graphviz.mjs +4 -18
  58. package/engine/markdown-expressions.mjs +116 -11
  59. package/engine/metadata-index.mjs +26 -0
  60. package/engine/near-miss.mjs +90 -0
  61. package/engine/note-addresses.mjs +75 -19
  62. package/engine/note-dates.mjs +5 -3
  63. package/engine/note-event-terms.mjs +364 -0
  64. package/engine/note-events.mjs +823 -0
  65. package/engine/note-vocabulary.mjs +184 -88
  66. package/engine/office-holders.mjs +20 -54
  67. package/engine/packagebuild-index.mjs +16 -1
  68. package/engine/pdf-build.mjs +97 -60
  69. package/engine/pdf-index.mjs +263 -0
  70. package/engine/pdf-render.mjs +47 -5
  71. package/engine/place-relation-terms.mjs +174 -0
  72. package/engine/place-relations.mjs +72 -160
  73. package/engine/rank-ladder.mjs +14 -41
  74. package/engine/reckoning-markers.mjs +2 -0
  75. package/engine/region-events.mjs +6 -12
  76. package/engine/scenes.mjs +73 -468
  77. package/engine/site-build.mjs +75 -101
  78. package/engine/site-index.mjs +3 -1
  79. package/engine/site-maps.mjs +43 -29
  80. package/engine/site-preview.mjs +40 -31
  81. package/engine/social-ties.mjs +2 -1
  82. package/engine/sql-tables.mjs +212 -9
  83. package/engine/standing-terms.mjs +145 -6
  84. package/engine/standings.mjs +37 -49
  85. package/engine/web-wikilinks.mjs +21 -1
  86. package/engine/wikilink-syntax.mjs +34 -4
  87. package/engine/wikilinks.mjs +27 -2
  88. package/engine/yaml-lint.mjs +7 -4
  89. package/hugo-theme/layouts/partials/infobox/entry.html +4 -1
  90. package/hugo-theme/layouts/partials/page-body.html +0 -3
  91. package/hugo-theme/static/css/style.css +25 -55
  92. package/package.json +1 -2
  93. package/packagebuild-metadata.jsonl +13 -13
  94. package/release.mjs +62 -4
  95. package/sohl/note-schemas.mjs +8 -73
  96. package/types/engine/address-values.d.mts +50 -3
  97. package/types/engine/address.d.mts +21 -0
  98. package/types/engine/anchors.d.mts +49 -2
  99. package/types/engine/art-slots.d.mts +1 -1
  100. package/types/engine/base-compiler.d.mts +13 -4
  101. package/types/engine/calendar-notes.d.mts +7 -0
  102. package/types/engine/content-format.d.mts +52 -0
  103. package/types/engine/content-images.d.mts +5 -1
  104. package/types/engine/content-tables.d.mts +9 -1
  105. package/types/engine/data-keys.d.mts +145 -0
  106. package/types/engine/derived-sections.d.mts +73 -0
  107. package/types/engine/diagnostics.d.mts +15 -0
  108. package/types/engine/event-fields.d.mts +99 -0
  109. package/types/engine/event-rows.d.mts +12 -0
  110. package/types/engine/event-views.d.mts +67 -0
  111. package/types/engine/exported-scene.d.mts +5 -2
  112. package/types/engine/frontmatter-lint.d.mts +3 -3
  113. package/types/engine/generated-sections.d.mts +143 -0
  114. package/types/engine/holdings.d.mts +6 -20
  115. package/types/engine/ids.d.mts +15 -12
  116. package/types/engine/index.d.mts +1 -1
  117. package/types/engine/infobox.d.mts +13 -3
  118. package/types/engine/journals.d.mts +24 -11
  119. package/types/engine/literature-works.d.mts +1 -2
  120. package/types/engine/map-graphviz.d.mts +2 -10
  121. package/types/engine/markdown-expressions.d.mts +12 -3
  122. package/types/engine/near-miss.d.mts +42 -0
  123. package/types/engine/note-addresses.d.mts +7 -1
  124. package/types/engine/note-event-terms.d.mts +68 -0
  125. package/types/engine/note-events.d.mts +71 -0
  126. package/types/engine/note-vocabulary.d.mts +37 -8
  127. package/types/engine/office-holders.d.mts +3 -0
  128. package/types/engine/pdf-index.d.mts +35 -0
  129. package/types/engine/pdf-render.d.mts +8 -2
  130. package/types/engine/place-relation-terms.d.mts +83 -0
  131. package/types/engine/place-relations.d.mts +11 -66
  132. package/types/engine/rank-ladder.d.mts +2 -0
  133. package/types/engine/region-events.d.mts +6 -12
  134. package/types/engine/scenes.d.mts +6 -32
  135. package/types/engine/site-build.d.mts +8 -22
  136. package/types/engine/site-maps.d.mts +15 -7
  137. package/types/engine/sql-tables.d.mts +77 -2
  138. package/types/engine/standing-terms.d.mts +37 -4
  139. package/types/engine/standings.d.mts +14 -9
  140. package/types/engine/wikilink-syntax.d.mts +20 -0
  141. package/types/engine/wikilinks.d.mts +3 -2
  142. package/types/engine/yaml-lint.d.mts +5 -4
  143. package/types/release.d.mts +4 -0
  144. package/docs/guides/government-migration.md +0 -52
  145. package/engine/itinerary-scenes.mjs +0 -166
  146. package/engine/lore-events.mjs +0 -197
  147. package/engine/map-notes.mjs +0 -1309
  148. package/engine/map-raster.mjs +0 -51
  149. package/hugo-theme/layouts/partials/holdings/entry.html +0 -21
  150. package/hugo-theme/layouts/partials/holdings/kind-groups.html +0 -71
  151. package/hugo-theme/layouts/partials/holdings.html +0 -61
  152. package/hugo-theme/layouts/partials/literature-works.html +0 -29
  153. package/hugo-theme/layouts/partials/place-map.html +0 -9
  154. package/types/engine/itinerary-scenes.d.mts +0 -29
  155. package/types/engine/lore-events.d.mts +0 -14
  156. package/types/engine/map-notes.d.mts +0 -297
  157. package/types/engine/map-raster.d.mts +0 -8
@@ -2,8 +2,7 @@
2
2
  * A drawing as the page writer receives it.
3
3
  *
4
4
  * @typedef {object} SiteMap
5
- * @property {string} name - The file's name in the page's bundle, and the
6
- * value of the page's `map` key: `from-<shortcode>.svg`.
5
+ * @property {string} name - The file's name: `from-<shortcode>.svg`.
7
6
  * @property {string} file - Where the drawing is, absolute.
8
7
  */
9
8
  /**
@@ -24,7 +23,7 @@
24
23
  */
25
24
  export function inlineSvg(svg: string): string;
26
25
  /**
27
- * Draw the map from every related place of this package.
26
+ * Draw the map from every place of this package that is given one.
28
27
  *
29
28
  * @param {object} opts
30
29
  * @param {Array<Record<string, any>>} opts.records - The package's index
@@ -36,8 +35,8 @@ export function inlineSvg(svg: string): string;
36
35
  * ending in a slash: what every name in a drawing links through.
37
36
  * @returns {{maps: Map<string, SiteMap>, findings: Array<{file: string,
38
37
  * line?: number, column?: number, severity: "error"|"warning",
39
- * message: string}>}} The drawings, keyed by the URL of the page each
40
- * belongs to, and warnings for relations that name no place.
38
+ * message: string}>}} The drawings, keyed by the file name a **From here**
39
+ * section names, and warnings for relations that name no place.
41
40
  */
42
41
  export function drawSiteMaps({ records, foreignIndex, config, base }: {
43
42
  records: Array<Record<string, any>>;
@@ -54,6 +53,16 @@ export function drawSiteMaps({ records, foreignIndex, config, base }: {
54
53
  message: string;
55
54
  }>;
56
55
  };
56
+ /**
57
+ * The figure the website sets a drawing in: the SVG inline, so its place names
58
+ * stay links, with no blank line inside it — a blank line would end the HTML
59
+ * block and hand the rest of the drawing to the Markdown renderer.
60
+ *
61
+ * @param {string} svg - The drawing, as {@link inlineSvg} wrote it.
62
+ * @param {string} alt - What the drawing shows, for a screen reader.
63
+ * @returns {string} One HTML block.
64
+ */
65
+ export function inlineDrawingFigure(svg: string, alt: string): string;
57
66
  /**
58
67
  * Where the site build draws, below the package root.
59
68
  *
@@ -65,8 +74,7 @@ export const SITE_MAP_DIR: string;
65
74
  */
66
75
  export type SiteMap = {
67
76
  /**
68
- * - The file's name in the page's bundle, and the
69
- * value of the page's `map` key: `from-<shortcode>.svg`.
77
+ * - The file's name: `from-<shortcode>.svg`.
70
78
  */
71
79
  name: string;
72
80
  /**
@@ -164,6 +164,77 @@ export function prepareSqlTables(db: object, sources: Array<{
164
164
  }): Promise<Map<string, object[]>>;
165
165
  /** Evaluate scalar SQL calls before synchronous Markdown renderers run. */
166
166
  export function prepareInlineSqlExpressions(db: any, sources: any): Promise<Map<any, any>>;
167
+ /**
168
+ * Each note's generated sections — the derived lists and the event views — see
169
+ * {@link module:engine/generated-sections}.
170
+ *
171
+ * @param {object|undefined} db - From {@link openNotesDatabase}, over
172
+ * `records`; absent, no event view is asked for.
173
+ * @param {object[]} records - Content-index records, of the audience the
174
+ * surface publishes to.
175
+ * @param {string} contentBase - Root of the content tree.
176
+ * @param {object} [opts]
177
+ * @param {string} [opts.only] - One note file to give sections to; absent,
178
+ * every note.
179
+ * @param {Map<string, object>} [opts.foreignIndex] - The fetched indexes, whose
180
+ * places, affiliations and works the lists take in.
181
+ * @param {object} [opts.config] - The resolved configuration.
182
+ * @param {Map<string, Map<string, string>>} [opts.derived] - The derived
183
+ * lists, from an earlier call over the same records, which are not derived
184
+ * again.
185
+ * @returns {Promise<{sections: Map<string, string>, views: Set<string>,
186
+ * derived: Map<string, Map<string, string>>}>} Note file to its sections,
187
+ * joined in order; the files whose sections hold an event view's fence; and
188
+ * the derived lists alone.
189
+ */
190
+ export function generatedSectionsFor(db: object | undefined, records: object[], contentBase: string, { only, foreignIndex, config, derived: given }?: {
191
+ only?: string | undefined;
192
+ foreignIndex?: Map<string, object> | undefined;
193
+ config?: object | undefined;
194
+ derived?: Map<string, Map<string, string>> | undefined;
195
+ }): Promise<{
196
+ sections: Map<string, string>;
197
+ views: Set<string>;
198
+ derived: Map<string, Map<string, string>>;
199
+ }>;
200
+ /**
201
+ * Join each note's generated sections to its body among the sources a
202
+ * preparation answers, so the fences the event views hold are answered with
203
+ * the author's own, in order after them. A note whose sections hold a fence
204
+ * and whose body holds no directive of its own joins the sources, its body
205
+ * read as {@link prepareTreeSqlTables} reads one.
206
+ *
207
+ * The expansion every surface runs appends the same text again, from the
208
+ * `generated` {@link attachGeneratedSections} sets on the note's results —
209
+ * see {@link module:engine/content-tables.expandContentTables}.
210
+ *
211
+ * @param {{sections: Map<string, string>, views: Set<string>}} generated -
212
+ * From {@link generatedSectionsFor}.
213
+ * @param {Array<{source: string, markdown: string, frontmatter?: object}>} sources -
214
+ * The bodies to answer, extended in place.
215
+ * @returns {void}
216
+ */
217
+ export function joinGeneratedSections(generated: {
218
+ sections: Map<string, string>;
219
+ views: Set<string>;
220
+ }, sources: Array<{
221
+ source: string;
222
+ markdown: string;
223
+ frontmatter?: object;
224
+ }>): void;
225
+ /**
226
+ * Record each note's generated sections on its prepared results, where the
227
+ * table expansion reads them. A note with no results of its own is given an
228
+ * empty set carrying them.
229
+ *
230
+ * @param {Map<string, object[]>} prepared - From {@link prepareSqlTables}.
231
+ * @param {{sections: Map<string, string>}} generated - From
232
+ * {@link generatedSectionsFor}.
233
+ * @returns {void}
234
+ */
235
+ export function attachGeneratedSections(prepared: Map<string, object[]>, generated: {
236
+ sections: Map<string, string>;
237
+ }): void;
167
238
  /**
168
239
  * Answer every `sql` directive in a content tree.
169
240
  *
@@ -189,15 +260,19 @@ export function prepareInlineSqlExpressions(db: any, sources: any): Promise<Map<
189
260
  * A command that also builds a link index holds them already, and deriving
190
261
  * them twice is the duplicated-corpus failure this closes.
191
262
  * @param {"all"|"public"} [opts.audience] - Whether to exclude GM notes.
263
+ * @param {Map<string, object>} [opts.foreignIndex] - The fetched indexes the
264
+ * generated sections take in, where the caller holds them already; absent,
265
+ * they are read from the cache.
192
266
  * @returns {Promise<Map<string, object[]>|undefined>} Results by note path,
193
267
  * carrying `inline` and `pageLists` beside them, or nothing when the tree
194
- * holds no such directive.
268
+ * holds no such directive and no note is given a generated section.
195
269
  */
196
- export function prepareTreeSqlTables(contentBase: string, { config, skipDirectories, records, audience }?: {
270
+ export function prepareTreeSqlTables(contentBase: string, { config, skipDirectories, records, audience, foreignIndex }?: {
197
271
  config?: object | undefined;
198
272
  skipDirectories?: readonly string[] | undefined;
199
273
  records?: object[] | undefined;
200
274
  audience?: "all" | "public" | undefined;
275
+ foreignIndex?: Map<string, object> | undefined;
201
276
  }): Promise<Map<string, object[]> | undefined>;
202
277
  /**
203
278
  * Aliases the renderer reads rather than printing.
@@ -1,3 +1,13 @@
1
+ /**
2
+ * The terms of a body's standing — what a membership carries, and the rungs
3
+ * and posts a body confers — as inner-key declarations.
4
+ *
5
+ * A leaf module, importing nothing: the note vocabulary reads these
6
+ * declarations as it loads, and the checks that judge what the values mean
7
+ * reach the vocabulary through the Address reader.
8
+ *
9
+ * @module
10
+ */
1
11
  /**
2
12
  * The note type a standing is held in.
3
13
  *
@@ -7,10 +17,33 @@
7
17
  */
8
18
  export const STANDING_BODY_TYPES: readonly string[];
9
19
  /**
10
- * The two keys one standing carries.
20
+ * The keys one standing carries, as inner-key declarations.
11
21
  *
12
- * `rank` is the level on the body's own `governance.ranks` ladder, as a number.
13
- * `office` is a key of its `governance.offices` map, as a string. Both are
14
- * optional — a membership with neither is the ordinary shape.
22
+ * `rank` is the level on the body's own `governance.ranks` ladder, and every
23
+ * membership states one. `office` is a key of its `governance.offices` map, and
24
+ * is stated only where the being holds a post.
25
+ *
26
+ * @type {readonly import("./data-keys.mjs").InnerKeySpec[]}
15
27
  */
28
+ export const STANDING_FIELDS: readonly import("./data-keys.mjs").InnerKeySpec[];
29
+ /** The keys of {@link STANDING_FIELDS}, in declared order. */
16
30
  export const STANDING_KEYS: readonly string[];
31
+ /**
32
+ * The keys a rung carries, as inner-key declarations.
33
+ *
34
+ * @type {readonly import("./data-keys.mjs").InnerKeySpec[]}
35
+ */
36
+ export const RUNG_FIELDS: readonly import("./data-keys.mjs").InnerKeySpec[];
37
+ /**
38
+ * The keys one row of an office's `holders` carries, as inner-key declarations.
39
+ *
40
+ * @type {readonly import("./data-keys.mjs").InnerKeySpec[]}
41
+ */
42
+ export const HOLDER_FIELDS: readonly import("./data-keys.mjs").InnerKeySpec[];
43
+ /**
44
+ * The keys an office written as a map carries. An office may instead be its
45
+ * description alone, as a string.
46
+ *
47
+ * @type {readonly import("./data-keys.mjs").InnerKeySpec[]}
48
+ */
49
+ export const OFFICE_FIELDS: readonly import("./data-keys.mjs").InnerKeySpec[];
@@ -101,22 +101,25 @@ export function standingsDigest(fm: object): {
101
101
  offices?: Record<string, string>;
102
102
  } | undefined;
103
103
  /**
104
- * How a standing reads beside the body that confers it.
104
+ * What a standing says before the body that confers it — `War Chief, Hárár
105
+ * (5), of ` — or `""` where no standing is held.
105
106
  *
106
107
  * The office leads, because it is the more particular of the two: a reader
107
- * meeting "War Chief" has been told the rung as well. The body closes the
108
- * phrase, which is the whole point of keying the entry by it — a standing with
109
- * no body named is the lossy form.
108
+ * meeting "War Chief" has been told the rung as well. The rung follows as its
109
+ * title with its level in parentheses, read from the body's own ladder; where
110
+ * the ladder names no rung at that level, or none is available, it reads as
111
+ * `Rank 5` and the number is not repeated. The body closes the phrase, which is
112
+ * the whole point of keying the entry by it — a standing with no body named is
113
+ * the lossy form.
110
114
  *
111
115
  * No article is supplied. One body's name reads with a "the" in front of it and
112
116
  * the next does not, and inventing one is this build editing the corpus.
113
117
  *
114
118
  * @param {object} standing - `{rank, office}`, either or both absent.
115
- * @param {string} body - What the body is called.
116
119
  * @param {{ranks?: Record<string, string>}} [digest] - The body's ladder.
117
- * @returns {string} The phrase, or the body's name where no standing is held.
120
+ * @returns {string} The words before the body's name, ending `of `.
118
121
  */
119
- export function standingPhrase(standing: object, body: string, digest?: {
122
+ export function standingLead(standing: object, digest?: {
120
123
  ranks?: Record<string, string>;
121
124
  }): string;
122
125
  /**
@@ -127,7 +130,8 @@ export function standingPhrase(standing: object, body: string, digest?: {
127
130
  *
128
131
  * 1. the value is a map keyed by Address (or the list this window still takes);
129
132
  * 2. every key names an affiliation that resolves, once;
130
- * 3. the entry states a `rank`;
133
+ * 3. the entry states a standing at all — what a stated standing holds is the
134
+ * inner-key check's, read from {@link STANDING_FIELDS};
131
135
  * 4. a `rank` is a level the named body's `governance.ranks` declares;
132
136
  * 5. an `office` is a key of that body's `governance.offices`.
133
137
  *
@@ -143,5 +147,6 @@ export function checkStandings(note: object, { index }?: {
143
147
  index?: object;
144
148
  }): object[];
145
149
  import { STANDING_BODY_TYPES } from "./standing-terms.mjs";
150
+ import { STANDING_FIELDS } from "./standing-terms.mjs";
146
151
  import { STANDING_KEYS } from "./standing-terms.mjs";
147
- export { STANDING_BODY_TYPES, STANDING_KEYS };
152
+ export { STANDING_BODY_TYPES, STANDING_FIELDS, STANDING_KEYS };
@@ -72,6 +72,26 @@ export function authoredLabel({ display }: {
72
72
  * @returns {string} The message, unpunctuated at the end as a finding is.
73
73
  */
74
74
  export function unlabelledLinkMessage(target: string): string;
75
+ /**
76
+ * Whether an anchor a link names is one of its target's events.
77
+ *
78
+ * A wikilink names a `prose` anchor; an `event` anchor is named only by the
79
+ * frontmatter keys that accept one. A local target carries its event ids as
80
+ * `eventAnchors`, a target another package publishes carries its kinded
81
+ * anchors as `noteAnchors`.
82
+ *
83
+ * @param {{eventAnchors?: Set<string>, noteAnchors?: Array<{slug: string,
84
+ * kind?: string}>}|undefined} hit - The link's resolved target.
85
+ * @param {string} anchor - The anchor the link names.
86
+ * @returns {boolean} Whether it is an event.
87
+ */
88
+ export function isEventAnchor(hit: {
89
+ eventAnchors?: Set<string>;
90
+ noteAnchors?: Array<{
91
+ slug: string;
92
+ kind?: string;
93
+ }>;
94
+ } | undefined, anchor: string): boolean;
75
95
  /**
76
96
  * What an author writing an address that resolves to nothing is told.
77
97
  *
@@ -18,8 +18,8 @@ export function anchorPageId(noteId: string, anchorSlug: string): string;
18
18
  *
19
19
  * @param {Array<{type: string, id: string, shortcode?: string|null,
20
20
  * name?: string, pack?: string, docPack?: string, none?: boolean,
21
- * draft?: boolean, anchors?: Set<string>, anchorUuids?: Record<string,string>,
22
- * docAnchorUuids?: Record<string,string>}>} docs -
21
+ * draft?: boolean, anchors?: Set<string>, eventAnchors?: Set<string>,
22
+ * anchorUuids?: Record<string,string>, docAnchorUuids?: Record<string,string>}>} docs -
23
23
  * One entry per content note. `pack` / `docPack` name the packs the note's
24
24
  * document and its documentation entry landed in; omitted, the conventional
25
25
  * one-pack-per-type names stand in. `none` says the note declares
@@ -56,6 +56,7 @@ export function buildWikilinkIndex(docs: Array<{
56
56
  none?: boolean;
57
57
  draft?: boolean;
58
58
  anchors?: Set<string>;
59
+ eventAnchors?: Set<string>;
59
60
  anchorUuids?: Record<string, string>;
60
61
  docAnchorUuids?: Record<string, string>;
61
62
  }>, packageId: string, foreign?: Map<string, object>, contentPackage?: string, { assets, noIndexPackages, referenceTargets, foreignReferences }?: {
@@ -22,10 +22,11 @@
22
22
  * - `no-empty-key`, `no-empty-document` — a fence or a file that parses to
23
23
  * nothing at all.
24
24
  *
25
- * **GitHub workflows are exempt from `no-empty-mapping-value`**, because an
26
- * empty value is the language there: `on:` `push:` and `workflow_dispatch:`
27
- * carry their meaning by being present, and writing `push: null` to satisfy a
28
- * linter would be worse YAML, not better.
25
+ * **Workflows are exempt from `no-empty-mapping-value`**, under
26
+ * `.github/workflows/` and Gitea's `.gitea/workflows/` alike, because an empty
27
+ * value is the language there: `on:` `push:` and `workflow_dispatch:` carry
28
+ * their meaning by being present, and writing `push: null` to satisfy a linter
29
+ * would be worse YAML, not better.
29
30
  *
30
31
  * @param {object} plugin - The `eslint-plugin-yml` module.
31
32
  * @returns {object[]} A complete flat config, for `overrideConfig`.
@@ -8,6 +8,10 @@
8
8
  * failure is timing-dependent, so it survives every run that happens to be
9
9
  * fast enough, which is what makes it worth being explicit about.
10
10
  *
11
+ * **The archive is reproducible.** Entries are written in path order with one
12
+ * date (`SOURCE_DATE_EPOCH` when set, otherwise 1980-01-01) and fixed
13
+ * permissions, so a rebuild of one staged tree is byte-identical.
14
+ *
11
15
  * @param {object} [opts]
12
16
  * @param {string} [opts.stageDir] - The staged package tree.
13
17
  * @param {string} [opts.outDir] - Where the release assets are written.
@@ -1,52 +0,0 @@
1
- ---
2
- shortcode: governmentmigration
3
- name: { full: "Migrating place governments" }
4
- type: doc
5
- subType: howto
6
- ---
7
-
8
- # Migrating place governments
9
-
10
- A place's `data.government` is the sole declaration of government. Its type is `Address`, with default target type `affiliation`. Name one governing affiliation, or write explicit `null` for complete anarchy. Omission means unknown. The website derives `governed_by` on a place and `governed_places` on its governing affiliation from resolved references across local and fetched indexes. Neither containment nor organizational parents supplies a government.
11
-
12
- ```yaml
13
- type: place
14
- data:
15
- government: temple
16
- ```
17
-
18
- A temple can be a government when it actually governs the place; so can a company, house or polity. Naming land in an old affiliation `data.domains` list never makes that body a government automatically. When old lists overlap, choose the one body that actually governs each place. Do not choose from subtype or list order. Leave government absent if the answer is unknown; do not write null merely to silence a warning.
19
-
20
- ## Legacy input and output changes
21
-
22
- Legacy affiliation `data.domains` is temporarily accepted without a finding so existing trees can migrate. It is ignored: it no longer creates affiliation Domains infobox rows, metadata-index `domains`, SoHL `system.domain`, or website `held_by` and `holdings`. New notes should omit it. A fetched older system schema can still declare `system.domain`; the schema check then reports an advisory that the field receives its system default. No new government is inferred from that default. There is no new authored holdings or tenure field. Preserve distinct ownership, subordinate tenure, property rights or influence in the note's prose, with links where useful, before deleting the old list.
23
-
24
- The tenure-based `over-held land` population advisory is retired. Geographical `contains` and the remaining population checks continue to use place parents unchanged. Explicit null remains null in published metadata and displays Complete anarchy; omission stays absent. Neither appears in a governing body's reverse list.
25
-
26
- ## Inventory behind the migration
27
-
28
- The baseline at commit `ada039d` contains 15 production files, six documentation files and nine test files mentioning `domains`, `held_by` or `holdings` (137, 70 and 99 occurrences respectively). Domains alone appears in 20 files across 127 matching lines. The active consumers are author declarations/schema and infoboxes, metadata indexes, SoHL `system.domain`, direct and reverse website tenure lists, site previews and the polity population advisory. Public holdings APIs and generated reference documentation describe these outputs.
29
-
30
- The migrated Thalorna baseline has 3,171 notes: 637 places, of which 191 declare government, 446 omit it and none declare null. Its 133 affiliations still carry 196 legacy domains claims covering 191 distinct places: 125 polities account for 184 claims, seven faith traditions for 11 and one order for one. Two places have overlapping claims (`pssshrines`: two; `aukhelathrgq`: five). These are reasons to review actual governance rather than infer it from legacy claims.
31
-
32
- Those consumers combined governance with independently meaningful direct tenure: houses holding manors, temples and companies claiming property, overlapping provincial and imperial claims. This migration deliberately replaces structured tenure displays with government derived from places, rather than converting every old claim into government. Independent historical facts remain supported in prose. Acceptance of legacy input is transitional; enforced removal will require separate author guidance.
33
-
34
- ## SQL queries
35
-
36
- Queries that read affiliation `data.domains` must instead join each place's explicit government to the affiliation's address. For example, to list the governing bodies of places directly within a region:
37
-
38
- ```sql
39
- SELECT DISTINCT affiliation.address.slug AS _ref,
40
- affiliation.name.full AS "Government"
41
- FROM notes affiliation
42
- JOIN notes place
43
- ON place.data.government = concat(affiliation.package, '-note-affiliation-', affiliation.shortcode)
44
- WHERE affiliation.type = 'affiliation'
45
- AND place.type = 'place'
46
- AND list_contains(place.data.parents, 'thalorna-note-place-aelwyth')
47
- ORDER BY affiliation.name.full COLLATE NOCASE
48
- ```
49
-
50
- The equality above uses the canonical journal addresses in the local `notes` table. Explicit Item addresses may use a system segment such as `sohl`; include that address when joining those references. Government lists generated by the site already handle Item and journal addresses for the same page. Null and omitted government do not match the join. A query that can legitimately select no places should declare `{allow-empty=true}` on its SQL fence.
51
-
52
- The real consumer regression migrated fourteen SQL query sites, including ten Government columns that formerly read reverse domains and four polity queries. Keeping the source field accepted does not preserve queries that depend on its retired semantics.
@@ -1,166 +0,0 @@
1
- /*
2
- * SPDX-License-Identifier: GPL-3.0-or-later
3
- */
4
-
5
- /** Gridless Foundry Scenes drawn from place relations. */
6
-
7
- import fs from "node:fs";
8
- import path from "node:path";
9
- import { resolvePackageBuildConfig } from "../config.mjs";
10
-
11
- import { buildMaps, relatedPlaces } from "./map-build.mjs";
12
- import { renderDot } from "./map-graphviz.mjs";
13
- import { mapWorld } from "./map-places.mjs";
14
- import { loadForeignIndexes } from "./metadata-index.mjs";
15
- import { authoredFrontmatter, isNoteRecord } from "./index-records.mjs";
16
- import { buildScene } from "./map-notes.mjs";
17
- import { buildJournalEntry } from "./journals.mjs";
18
- import { imageDimensions } from "./asset-types.mjs";
19
- import { makeId } from "./ids.mjs";
20
- import { stripAdventureKeys } from "./bundle-notes.mjs";
21
-
22
- /** GraphViz's plain coordinates, converted to pixel positions on its PNG. */
23
- export function pinPositions(plain, width, height) {
24
- const graph = /^graph\s+\S+\s+(\S+)\s+(\S+)/m.exec(plain);
25
- if (!graph) throw new Error("GraphViz emitted no graph size for itinerary pins");
26
- const graphWidth = Number(graph[1]);
27
- const graphHeight = Number(graph[2]);
28
- if (!(graphWidth > 0 && graphHeight > 0)) {
29
- throw new Error("GraphViz emitted an invalid graph size for itinerary pins");
30
- }
31
- const positions = new Map();
32
- for (const line of plain.split("\n")) {
33
- const match = /^node\s+(?:"(p_[^"]+)"|(p_\S+))\s+(\S+)\s+(\S+)/.exec(line);
34
- if (!match) continue;
35
- const shortcode = (match[1] ?? match[2]).slice(2);
36
- positions.set(shortcode, {
37
- x: Math.round((Number(match[3]) / graphWidth) * width),
38
- y: Math.round((1 - Number(match[4]) / graphHeight) * height),
39
- });
40
- }
41
- return positions;
42
- }
43
-
44
- /** The compiled local journals a generated Adventure can import with its Scenes. */
45
- function localJournals(dirs) {
46
- const byId = new Map();
47
- for (const dir of dirs) {
48
- if (!fs.existsSync(dir)) continue;
49
- for (const file of fs.readdirSync(dir)) {
50
- if (!file.endsWith(".json")) continue;
51
- const doc = JSON.parse(fs.readFileSync(path.join(dir, file), "utf8"));
52
- if (String(doc._key ?? "").startsWith("!journal!")) byId.set(doc._id, doc);
53
- }
54
- }
55
- return byId;
56
- }
57
-
58
- /**
59
- * Emit backgrounds into the staged package and return Scenes and the journals
60
- * their pins need. The caller writes them into its Scene and Adventure packs.
61
- */
62
- export function buildItineraryScenes({
63
- config,
64
- records,
65
- contentBase,
66
- journalSourceDirs = [],
67
- stats,
68
- foreign = loadForeignIndexes(config, [config.contentPackage]),
69
- }) {
70
- const buildConfig = resolvePackageBuildConfig(config);
71
- const world = mapWorld({ records, foreignIndex: foreign.index, contentBase, config });
72
- const centres = relatedPlaces(world.places).filter((sc) => world.places.get(sc)?.local);
73
- const findings = foreign.stale.map((stale) => ({
74
- file: contentBase,
75
- severity: "warning",
76
- message: `the ${stale.package} content index is unavailable, so itinerary maps omit its places: ${stale.reason}`,
77
- }));
78
- const assetDir = path.resolve(
79
- config.rootDir,
80
- buildConfig.stageDir,
81
- "assets/maps/generated-itineraries",
82
- );
83
- fs.rmSync(assetDir, { recursive: true, force: true });
84
- if (!centres.length) return { scenes: [], journal: [], findings };
85
-
86
- const outDir = path.resolve(config.rootDir, "build/map/foundry");
87
- fs.rmSync(outDir, { recursive: true, force: true });
88
- const drawn = buildMaps({ world, outDir, from: centres });
89
- findings.push(...drawn.findings);
90
- fs.mkdirSync(assetDir, { recursive: true });
91
-
92
- const localRecords = new Map();
93
- for (const record of records) {
94
- if (!isNoteRecord(record) || record.type !== "place") continue;
95
- localRecords.set(String(record.shortcode).toLowerCase(), authoredFrontmatter(record));
96
- }
97
- const compiledJournals = localJournals(journalSourceDirs);
98
- const imported = new Map();
99
- const scenes = [];
100
- for (const centre of centres) {
101
- const dot = path.join(outDir, `from-${centre}.dot`);
102
- const png = path.join(assetDir, `from-${centre}.png`);
103
- const plain = path.join(outDir, `from-${centre}.plain`);
104
- renderDot(dot, png, { engine: "neato", format: "png", nop: 2, dpi: 150 });
105
- renderDot(dot, plain, { engine: "neato", format: "plain", nop: 2 });
106
- const size = imageDimensions(png, ".png");
107
- if (!size) throw new Error(`${png} carries no readable PNG size`);
108
- const { width, height } = size;
109
- const positions = pinPositions(fs.readFileSync(plain, "utf8"), width, height);
110
- const sceneId = makeId("itinerary-scene", `${config.contentPackage}:${centre}`);
111
- const src = `${buildConfig.packageKind}/${buildConfig.packageId}/assets/maps/generated-itineraries/from-${centre}.png`;
112
- const scene = buildScene(
113
- {
114
- id: sceneId,
115
- name: { full: `From ${world.places.get(centre).name}` },
116
- subType: "regionalmap",
117
- sohl: { dimensions: [width, height], pxPerGrid: 100 },
118
- data: { bgImage: src },
119
- },
120
- { art: () => src, stats },
121
- );
122
- scene.grid.distance = 1;
123
- scene.grid.units = "";
124
- for (const [shortcode, at] of positions) {
125
- const place = world.places.get(shortcode);
126
- if (!place) continue;
127
- let journal;
128
- if (place.local) {
129
- journal = compiledJournals.get(localRecords.get(shortcode)?.id);
130
- } else {
131
- const ref = foreign.index.get(place.address);
132
- if (ref?.uuid && /\.JournalEntry\./.test(ref.uuid)) {
133
- const id = makeId("itinerary-foreign-journal", place.address);
134
- journal = buildJournalEntry({
135
- id,
136
- name: place.name,
137
- markdown: `@UUID[${ref.uuid}]{Open ${place.name}}`,
138
- stats,
139
- });
140
- }
141
- }
142
- if (!journal) {
143
- findings.push({
144
- file: place.file ? path.join(contentBase, place.file) : contentBase,
145
- severity: "warning",
146
- message: `place "${shortcode}" has no addressable JournalEntry, so its itinerary label has no Foundry pin`,
147
- });
148
- continue;
149
- }
150
- imported.set(journal._id, stripAdventureKeys(journal));
151
- const id = makeId("itinerary-note", `${sceneId}:${shortcode}`);
152
- scene.notes.push({
153
- _id: id,
154
- entryId: journal._id,
155
- pageId: null,
156
- x: at.x,
157
- y: at.y,
158
- text: place.name,
159
- iconSize: 32,
160
- _key: `!scenes.notes!${sceneId}.${id}`,
161
- });
162
- }
163
- scenes.push(scene);
164
- }
165
- return { scenes, journal: [...imported.values()], findings };
166
- }