@heroiclands/package-build 22.15.0 → 22.16.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 (223) hide show
  1. package/CHANGELOG.md +103 -0
  2. package/README.md +39 -678
  3. package/bin/{content-build.mjs → content-commands.mjs} +197 -118
  4. package/bin/package-build.mjs +303 -42
  5. package/bin/report.mjs +1 -1
  6. package/ci/ci-docker.mjs +6 -3
  7. package/ci/ci-steps.mjs +6 -4
  8. package/ci/content-format-reference.mjs +106 -0
  9. package/config.mjs +22 -2
  10. package/content-config.mjs +74 -134
  11. package/docs/api.md +294 -234
  12. package/docs/authoring/assets.md +31 -0
  13. package/docs/authoring/dates-and-calendars.md +33 -0
  14. package/docs/authoring/first-note.md +48 -0
  15. package/docs/authoring/frontmatter.md +40 -0
  16. package/docs/authoring/links-and-markup.md +137 -0
  17. package/docs/commands.md +631 -162
  18. package/docs/configuration.md +176 -140
  19. package/docs/content-format.md +50 -4229
  20. package/docs/design.md +32 -0
  21. package/docs/diagnostics.md +29 -22
  22. package/docs/getting-started.md +21 -877
  23. package/docs/guides/book.md +153 -0
  24. package/docs/guides/e2e.md +143 -0
  25. package/docs/guides/icons.md +126 -0
  26. package/docs/guides/localization.md +120 -0
  27. package/docs/guides/module-project.md +915 -0
  28. package/docs/guides/packs.md +167 -0
  29. package/docs/guides/reference-pages.md +148 -0
  30. package/docs/guides/release-and-deploy.md +114 -0
  31. package/docs/guides/site.md +152 -0
  32. package/docs/guides/system-project.md +45 -0
  33. package/docs/index.md +16 -0
  34. package/docs/project-setup.md +54 -35
  35. package/docs/reference/format-details.md +4420 -0
  36. package/docs/reference/note-types.md +531 -0
  37. package/engine/address-render.mjs +26 -0
  38. package/engine/address.mjs +3 -20
  39. package/engine/anchors.mjs +15 -6
  40. package/engine/asset-bindings.mjs +6 -2
  41. package/engine/base-compiler.mjs +47 -11
  42. package/engine/being-measurements.mjs +70 -0
  43. package/engine/calendar-artifacts.mjs +1 -1
  44. package/engine/calendar-choice.mjs +32 -0
  45. package/engine/calendar-human.mjs +71 -0
  46. package/engine/calendar-notes.mjs +41 -3
  47. package/engine/calendars.mjs +8 -1
  48. package/engine/changelog-lint.mjs +2 -2
  49. package/engine/code-fences.mjs +23 -97
  50. package/engine/compile-corpus.mjs +6 -7
  51. package/engine/content-admonitions.mjs +114 -0
  52. package/engine/content-captions.mjs +181 -0
  53. package/engine/content-embeds.mjs +2 -2
  54. package/engine/content-footnotes.mjs +34 -0
  55. package/engine/content-format-check.mjs +2 -2
  56. package/engine/content-format.mjs +127 -73
  57. package/engine/content-format.yaml +745 -0
  58. package/engine/content-html.mjs +1 -1
  59. package/engine/content-icons.mjs +45 -92
  60. package/engine/content-images.mjs +22 -74
  61. package/engine/content-index.mjs +12 -3
  62. package/engine/content-links.mjs +21 -26
  63. package/engine/content-lint.mjs +1 -5
  64. package/engine/content-secrets.mjs +3 -1
  65. package/engine/content-tables.mjs +47 -1257
  66. package/engine/culture-choice.mjs +32 -0
  67. package/engine/date-conversion.mjs +47 -0
  68. package/engine/declaration-check.mjs +87 -0
  69. package/engine/docs-checks.mjs +140 -0
  70. package/engine/documentation-links.mjs +31 -0
  71. package/engine/exported-scene.mjs +1 -1
  72. package/engine/extension-attributes.mjs +74 -0
  73. package/engine/field-reference.mjs +1 -1
  74. package/engine/field-spec.mjs +4 -0
  75. package/engine/foreign-catalog.mjs +5 -5
  76. package/engine/format-generated.mjs +15 -0
  77. package/engine/foundry-entries.mjs +3 -1
  78. package/engine/frontmatter-lint.mjs +5 -1
  79. package/engine/frontmatter.mjs +10 -0
  80. package/engine/generate.mjs +2 -2
  81. package/engine/helpers.mjs +71 -117
  82. package/engine/homepage.mjs +2 -3
  83. package/engine/index-records.mjs +1 -0
  84. package/engine/index.mjs +18 -3
  85. package/engine/infobox-registry.mjs +3 -1
  86. package/engine/infobox.mjs +38 -20
  87. package/engine/item-compiler.mjs +10 -8
  88. package/engine/item-docs.mjs +7 -6
  89. package/engine/itinerary-scenes.mjs +4 -2
  90. package/engine/journals.mjs +51 -10
  91. package/engine/macros.mjs +1 -1
  92. package/engine/map-build.mjs +1 -1
  93. package/engine/map-dot.mjs +1 -1
  94. package/engine/map-raster.mjs +4 -2
  95. package/engine/markdown-expressions.mjs +117 -0
  96. package/engine/metadata-index.mjs +7 -3
  97. package/engine/note-addresses.mjs +1 -0
  98. package/engine/note-dates.mjs +283 -61
  99. package/engine/note-package.mjs +5 -25
  100. package/engine/note-state.mjs +6 -2
  101. package/engine/note-vocabulary.mjs +74 -19
  102. package/engine/number-words.mjs +75 -0
  103. package/engine/office-holders.mjs +188 -0
  104. package/engine/pack-config.mjs +2 -15
  105. package/engine/pack-router.mjs +24 -5
  106. package/engine/packages.mjs +3 -2
  107. package/engine/pathnames.mjs +21 -24
  108. package/engine/pdf-build.mjs +217 -55
  109. package/engine/pdf-fonts.mjs +2 -2
  110. package/engine/pdf-images.mjs +82 -0
  111. package/engine/pdf-render.mjs +174 -22
  112. package/engine/populations.mjs +3 -2
  113. package/engine/project-init.mjs +499 -0
  114. package/engine/prose-config.mjs +1 -1
  115. package/engine/prose-lint.mjs +7 -6
  116. package/engine/readability-lint.mjs +86 -0
  117. package/engine/reckoning-markers.mjs +205 -93
  118. package/engine/scenes.mjs +5 -2
  119. package/engine/schema-check.mjs +3 -3
  120. package/engine/site-build.mjs +87 -37
  121. package/engine/site-config.mjs +8 -8
  122. package/engine/site-index.mjs +4 -0
  123. package/engine/site-maps.mjs +1 -1
  124. package/engine/site-preview.mjs +19 -8
  125. package/engine/social-ties.mjs +5 -1
  126. package/engine/sql-tables.mjs +92 -22
  127. package/engine/web-wikilinks.mjs +15 -31
  128. package/engine/wikilink-syntax.mjs +4 -1
  129. package/engine/wikilinks.mjs +12 -14
  130. package/hm3/item-fields.mjs +1 -1
  131. package/hm3/items.mjs +12 -45
  132. package/index.mjs +2 -14
  133. package/manifest.mjs +31 -4
  134. package/markdownlint-config.mjs +1 -1
  135. package/package.json +32 -11
  136. package/prettier-config.mjs +1 -1
  137. package/sohl/being-info.mjs +3 -145
  138. package/sohl/index.mjs +1 -1
  139. package/sohl/infobox.mjs +2 -1
  140. package/sohl/item-builders.mjs +3 -27
  141. package/sohl/item-fields.mjs +61 -22
  142. package/sohl/kb-passes.mjs +2 -2
  143. package/sohl/note-schemas.mjs +0 -8
  144. package/stage.mjs +19 -0
  145. package/types/config.d.mts +12 -3
  146. package/types/content-config.d.mts +15 -81
  147. package/types/engine/address-render.d.mts +13 -0
  148. package/types/engine/address.d.mts +2 -11
  149. package/types/engine/anchors.d.mts +3 -4
  150. package/types/engine/asset-bindings.d.mts +9 -2
  151. package/types/engine/base-compiler.d.mts +1 -2
  152. package/types/engine/being-measurements.d.mts +16 -0
  153. package/types/engine/calendar-choice.d.mts +14 -0
  154. package/types/engine/calendar-human.d.mts +4 -0
  155. package/types/engine/calendar-notes.d.mts +2 -0
  156. package/types/engine/calendars.d.mts +5 -3
  157. package/types/engine/code-fences.d.mts +6 -72
  158. package/types/engine/compile-corpus.d.mts +1 -3
  159. package/types/engine/content-admonitions.d.mts +24 -0
  160. package/types/engine/content-captions.d.mts +45 -0
  161. package/types/engine/content-footnotes.d.mts +10 -0
  162. package/types/engine/content-format-check.d.mts +17 -4
  163. package/types/engine/content-format.d.mts +24 -11
  164. package/types/engine/content-icons.d.mts +14 -23
  165. package/types/engine/content-images.d.mts +2 -2
  166. package/types/engine/content-index.d.mts +9 -2
  167. package/types/engine/content-links.d.mts +3 -5
  168. package/types/engine/content-tables.d.mts +11 -140
  169. package/types/engine/culture-choice.d.mts +14 -0
  170. package/types/engine/date-conversion.d.mts +4 -0
  171. package/types/engine/declaration-check.d.mts +24 -0
  172. package/types/engine/docs-checks.d.mts +28 -0
  173. package/types/engine/documentation-links.d.mts +7 -0
  174. package/types/engine/exported-scene.d.mts +6 -2
  175. package/types/engine/extension-attributes.d.mts +14 -0
  176. package/types/engine/field-spec.d.mts +9 -0
  177. package/types/engine/format-generated.d.mts +7 -0
  178. package/types/engine/frontmatter.d.mts +14 -4
  179. package/types/engine/generate.d.mts +1 -1
  180. package/types/engine/helpers.d.mts +37 -92
  181. package/types/engine/homepage.d.mts +2 -3
  182. package/types/engine/index.d.mts +5 -0
  183. package/types/engine/infobox-registry.d.mts +3 -1
  184. package/types/engine/infobox.d.mts +17 -6
  185. package/types/engine/item-compiler.d.mts +4 -4
  186. package/types/engine/item-docs.d.mts +4 -3
  187. package/types/engine/journals.d.mts +10 -7
  188. package/types/engine/map-graphviz.d.mts +1 -1
  189. package/types/engine/markdown-expressions.d.mts +17 -0
  190. package/types/engine/note-dates.d.mts +47 -1
  191. package/types/engine/note-package.d.mts +3 -19
  192. package/types/engine/note-state.d.mts +2 -1
  193. package/types/engine/note-vocabulary.d.mts +9 -0
  194. package/types/engine/number-words.d.mts +4 -0
  195. package/types/engine/office-holders.d.mts +14 -0
  196. package/types/engine/pack-config.d.mts +1 -1
  197. package/types/engine/packages.d.mts +3 -2
  198. package/types/engine/pdf-images.d.mts +38 -0
  199. package/types/engine/populations.d.mts +2 -2
  200. package/types/engine/project-init.d.mts +17 -0
  201. package/types/engine/prose-config.d.mts +1 -1
  202. package/types/engine/prose-lint.d.mts +3 -2
  203. package/types/engine/readability-lint.d.mts +22 -0
  204. package/types/engine/reckoning-markers.d.mts +4 -2
  205. package/types/engine/schema-check.d.mts +3 -3
  206. package/types/engine/site-build.d.mts +38 -28
  207. package/types/engine/site-config.d.mts +2 -2
  208. package/types/engine/site-index.d.mts +4 -0
  209. package/types/engine/site-preview.d.mts +1 -1
  210. package/types/engine/social-ties.d.mts +8 -2
  211. package/types/engine/sql-tables.d.mts +13 -2
  212. package/types/engine/web-wikilinks.d.mts +2 -1
  213. package/types/engine/wikilinks.d.mts +7 -1
  214. package/types/hm3/items.d.mts +5 -6
  215. package/types/labels.d.mts +40 -0
  216. package/types/manifest.d.mts +3 -3
  217. package/types/sohl/actors.d.mts +2 -2
  218. package/types/sohl/being-info.d.mts +3 -43
  219. package/types/sohl/index.d.mts +1 -1
  220. package/types/sohl/infobox.d.mts +5 -2
  221. package/types/sohl/item-fields.d.mts +4 -13
  222. package/types/stage.d.mts +9 -0
  223. package/CONTENT.md +0 -3239
@@ -10,7 +10,9 @@
10
10
 
11
11
  import { positionOfFrontmatterPath } from "./diagnostics.mjs";
12
12
  import { canonicalDayOffset, daysInMonth } from "./calendars.mjs";
13
- import { parseNoteDate } from "./note-dates.mjs";
13
+ import { DATEFROM_PATTERN, parseNoteDate } from "./note-dates.mjs";
14
+ import { eraNames } from "./calendar-human.mjs";
15
+ import { addressSuffixes, renderAddress } from "./address-render.mjs";
14
16
 
15
17
  /**
16
18
  * Resolve every declared reckoning marker against its calendar and era start.
@@ -18,126 +20,205 @@ import { parseNoteDate } from "./note-dates.mjs";
18
20
  *
19
21
  * @param {{notes?: readonly object[]}} index - The corpus link index.
20
22
  * @param {number} daysPerYear - The world's year length.
21
- * @returns {{markers: Map<string, object>, findings: object[]}} Resolved markers and errors.
23
+ * @returns {{eras: Map<string, object>, markers: Map<string, object>, findings: object[]}} Resolved eras, markers, and errors.
22
24
  */
23
25
  export function resolveReckoningMarkers(index, daysPerYear) {
24
26
  const declared = new Map();
27
+ const qualifiers = new Map();
28
+ const markerKeys = new Map();
29
+ const eras = new Map();
25
30
  const markers = new Map();
26
31
  const findings = [];
32
+ const failed = new Set();
33
+ const visiting = [];
34
+
35
+ const location = (row, key) => ({
36
+ ...(row.note.file === undefined ? {} : { file: row.note.file }),
37
+ ...(row.note.raw === undefined ?
38
+ {}
39
+ : positionOfFrontmatterPath(row.note.raw, ["data", "eras", row.position, key])),
40
+ });
41
+ const finding = (row, key, message) =>
42
+ findings.push({
43
+ ...location(row, key),
44
+ severity: "error",
45
+ message,
46
+ });
47
+ const alias = (qualifier, key) => {
48
+ if (qualifiers.has(qualifier) && qualifiers.get(qualifier) !== key)
49
+ qualifiers.set(qualifier, null);
50
+ else qualifiers.set(qualifier, key);
51
+ };
27
52
 
28
53
  for (const note of index?.notes ?? []) {
29
54
  const fm = note?.fm ?? note?.frontmatter ?? note;
30
55
  if (fm?.type !== "lore" || fm?.subType !== "calendar") continue;
31
- const eras = fm.data?.eras;
32
- if (!Array.isArray(eras)) continue;
33
- eras.forEach((row, position) => {
34
- if (row?.marker === undefined) return;
35
- const marker = row.marker;
36
- const at = {
37
- ...(note.file === undefined ? {} : { file: note.file }),
38
- ...(note.raw === undefined ?
39
- {}
40
- : positionOfFrontmatterPath(note.raw, ["data", "eras", position, "marker"])),
56
+ const rows = fm.data?.eras;
57
+ if (!Array.isArray(rows)) continue;
58
+ rows.forEach((era, position) => {
59
+ if (
60
+ !era ||
61
+ typeof era.shortcode !== "string" ||
62
+ !era.shortcode ||
63
+ typeof fm.shortcode !== "string" ||
64
+ !fm.shortcode
65
+ )
66
+ return;
67
+ const pkg = fm.package ?? note.package ?? index?.contentPackage;
68
+ const short = `${fm.shortcode}.${era.shortcode}`;
69
+ const key =
70
+ pkg ?
71
+ renderAddress({ package: pkg, system: "note", type: "lore", shortcode: short })
72
+ : short;
73
+ const row = {
74
+ key,
75
+ era: short,
76
+ calendar: fm.data,
77
+ marker: era.marker,
78
+ calendarShortcode: fm.shortcode,
79
+ name: era.name,
80
+ abbreviation: era.abbreviation,
81
+ label: era.label,
82
+ start: era.start,
83
+ end: era.end,
84
+ note,
85
+ position,
41
86
  };
42
- if (typeof marker !== "string" || !/^[A-Z][A-Z0-9]*$/.test(marker)) {
43
- findings.push({
44
- ...at,
45
- severity: "error",
46
- message:
47
- "an era marker is uppercase letters and digits, starting with a letter",
48
- });
87
+ if (declared.has(key)) {
49
88
  return;
50
89
  }
51
- if (declared.has(marker)) {
52
- findings.push({
53
- ...at,
54
- severity: "error",
55
- message: `reckoning marker ${marker} is declared more than once`,
56
- });
90
+ declared.set(key, row);
91
+ alias(short, key);
92
+ if (pkg) {
93
+ alias(
94
+ renderAddress({ package: pkg, system: "note", type: "lore", shortcode: short }),
95
+ key,
96
+ );
97
+ alias(`note-lore-${short}`, key);
98
+ alias(`lore-${short}`, key);
99
+ }
100
+ if (era.marker === undefined) return;
101
+ if (typeof era.marker !== "string" || !/^[A-Z][A-Z0-9]*$/.test(era.marker)) {
102
+ finding(
103
+ row,
104
+ "marker",
105
+ "an era marker is uppercase letters and digits, starting with a letter",
106
+ );
57
107
  return;
58
108
  }
59
- declared.set(marker, {
60
- marker,
61
- era: `${fm.shortcode}.${row.shortcode}`,
62
- calendar: fm.data,
63
- start: row.start,
64
- end: row.end,
65
- note,
66
- position,
67
- });
109
+ if (markerKeys.has(era.marker)) {
110
+ finding(row, "marker", `reckoning marker ${era.marker} is declared more than once`);
111
+ return;
112
+ }
113
+ markerKeys.set(era.marker, key);
68
114
  });
69
115
  }
70
116
 
71
- const visiting = new Set();
72
- const failed = new Set();
73
- function resolve(marker) {
74
- if (markers.has(marker)) return markers.get(marker);
75
- if (failed.has(marker)) return null;
76
- const row = declared.get(marker);
77
- if (!row) return null;
78
- if (visiting.has(marker)) {
79
- findings.push({
80
- ...(row.note.file === undefined ? {} : { file: row.note.file }),
81
- severity: "error",
82
- message: `reckoning marker cycle includes ${[...visiting, marker].join(" → ")}`,
83
- });
84
- failed.add(marker);
117
+ for (const [qualifier, key] of qualifiers)
118
+ if (key === null) eras.set(qualifier, { ambiguous: true });
119
+
120
+ function resolve(key) {
121
+ if (key === null || !declared.has(key) || failed.has(key)) return null;
122
+ const row = declared.get(key);
123
+ if (eras.has(key)) return eras.get(key);
124
+ if (visiting.includes(key)) {
125
+ const members = [...visiting.slice(visiting.indexOf(key)), key].map(
126
+ (member) => declared.get(member).era,
127
+ );
128
+ finding(row, "start", `calendar era cycle includes ${members.join(" → ")}`);
129
+ for (const member of visiting.slice(visiting.indexOf(key))) failed.add(member);
85
130
  return null;
86
131
  }
87
- visiting.add(marker);
132
+ visiting.push(key);
88
133
  if (row.start === undefined || row.start === null) {
89
- findings.push({
90
- ...(row.note.file === undefined ? {} : { file: row.note.file }),
91
- severity: "error",
92
- message: `reckoning marker ${marker} needs an era start`,
93
- });
94
- failed.add(marker);
134
+ finding(row, "start", `calendar era ${row.era} needs a start`);
135
+ failed.add(key);
95
136
  }
137
+ const source = typeof row.start === "string" ? row.start.trim() : "";
138
+ const marker = /^([A-Z][A-Z0-9]*)\(/.exec(source)?.[1];
139
+ const qualifier = /\s+([^\s]+\.[^\s]+)$/.exec(source)?.[1];
140
+ const conversion = DATEFROM_PATTERN.exec(source);
141
+ const convertedRef = conversion?.[1];
142
+ const convertedDate = conversion?.[2];
143
+ const convertedRows =
144
+ convertedRef ?
145
+ [...declared.values()].filter((candidate) => {
146
+ const fm = candidate.note.fm ?? candidate.note.frontmatter ?? candidate.note;
147
+ const pkg = fm.package ?? candidate.note.package ?? index?.contentPackage;
148
+ if (!pkg) return convertedRef === candidate.calendarShortcode;
149
+ return addressSuffixes({
150
+ package: pkg,
151
+ system: "note",
152
+ type: "lore",
153
+ shortcode: candidate.calendarShortcode,
154
+ }).includes(convertedRef);
155
+ })
156
+ : [];
157
+ const convertedTargets = convertedRows.filter((candidate) =>
158
+ eraNames(candidate).some((name) =>
159
+ convertedDate?.toLowerCase().endsWith(` ${name.toLowerCase()}`),
160
+ ),
161
+ );
162
+ if (convertedTargets.length === 0 && convertedRows.length === 1)
163
+ convertedTargets.push(convertedRows[0]);
96
164
  const dependency =
97
- typeof row.start === "string" ? /^([A-Z][A-Z0-9]*)\(/.exec(row.start)?.[1] : null;
98
- if (dependency && !failed.has(marker)) resolve(dependency);
99
- if (!failed.has(marker)) {
165
+ marker ? markerKeys.get(marker)
166
+ : qualifier ? qualifiers.get(qualifier)
167
+ : convertedTargets.length === 1 ? convertedTargets[0].key
168
+ : null;
169
+ if (dependency && !failed.has(key)) resolve(dependency);
170
+ if (!failed.has(key)) {
100
171
  const parsed = parseNoteDate(row.start, {
101
172
  markers,
173
+ eras,
102
174
  daysPerYear,
103
175
  field: "data.eras.start",
104
176
  allowUnknown: false,
105
- file: row.note.file,
177
+ ...location(row, "start"),
106
178
  raw: row.note.raw,
107
179
  keyPath: ["data", "eras", row.position, "start"],
108
180
  });
109
181
  findings.push(...parsed.findings);
110
182
  if (
111
- parsed.date?.known &&
112
- Number.isSafeInteger(parsed.date.canonicalYear) &&
113
- parsed.findings.every((f) => f.severity !== "error")
183
+ Number.isSafeInteger(parsed.date?.canonicalYear) &&
184
+ parsed.findings.every((item) => item.severity !== "error")
114
185
  ) {
115
186
  const entry = {
116
187
  era: row.era,
188
+ qualifier: row.key,
189
+ marker: row.marker,
190
+ firstEra: row.position === 0,
117
191
  calendar: row.calendar,
192
+ calendarShortcode: row.calendarShortcode,
193
+ name: row.name,
194
+ abbreviation: row.abbreviation,
195
+ label: row.label,
118
196
  epochYear: parsed.date.canonicalYear,
119
197
  epochDay: parsed.date.canonicalDay ?? 1,
120
198
  };
121
- markers.set(marker, entry);
122
- } else {
123
- if (parsed.date && parsed.findings.length === 0)
124
- findings.push({
125
- ...(row.note.file === undefined ? {} : { file: row.note.file }),
126
- severity: "error",
127
- message: `reckoning marker ${marker} needs a canonically resolved era start`,
128
- });
129
- failed.add(marker);
130
- }
199
+ if (Number.isSafeInteger(daysPerYear) && daysPerYear > 0)
200
+ entry.startOffset = canonicalDayOffset(
201
+ entry.epochYear,
202
+ entry.epochDay,
203
+ 1,
204
+ daysPerYear,
205
+ );
206
+ eras.set(key, entry);
207
+ for (const [name, owner] of qualifiers) if (owner === key) eras.set(name, entry);
208
+ if (row.marker && markerKeys.get(row.marker) === key)
209
+ markers.set(row.marker, entry);
210
+ } else failed.add(key);
131
211
  }
132
- visiting.delete(marker);
133
- return markers.get(marker) ?? null;
212
+ visiting.pop();
213
+ return eras.get(key) ?? null;
134
214
  }
135
- for (const marker of declared.keys()) resolve(marker);
136
- for (const [marker, row] of declared) {
137
- const entry = markers.get(marker);
215
+ for (const key of declared.keys()) resolve(key);
216
+ for (const row of declared.values()) {
217
+ const entry = eras.get(row.key);
138
218
  if (!entry || row.end === undefined || row.end === null) continue;
139
219
  const parsed = parseNoteDate(row.end, {
140
220
  markers,
221
+ eras,
141
222
  daysPerYear,
142
223
  field: "data.eras.end",
143
224
  allowUnknown: false,
@@ -146,13 +227,13 @@ export function resolveReckoningMarkers(index, daysPerYear) {
146
227
  keyPath: ["data", "eras", row.position, "end"],
147
228
  });
148
229
  findings.push(...parsed.findings);
149
- if (!Number.isSafeInteger(parsed.date?.canonicalYear)) {
150
- if (parsed.date && parsed.findings.length === 0)
151
- findings.push({
152
- ...(row.note.file === undefined ? {} : { file: row.note.file }),
153
- severity: "error",
154
- message: `reckoning marker ${marker} needs a canonically resolved era end`,
155
- });
230
+ if (!Number.isSafeInteger(parsed.date?.canonicalYear)) continue;
231
+ if (!Number.isSafeInteger(daysPerYear) || daysPerYear < 1) {
232
+ finding(
233
+ row,
234
+ "end",
235
+ `calendar era ${row.era} needs the world's year length to bound its end`,
236
+ );
156
237
  continue;
157
238
  }
158
239
  const firstEndDay = canonicalDayOffset(
@@ -164,21 +245,52 @@ export function resolveReckoningMarkers(index, daysPerYear) {
164
245
  const extraDays =
165
246
  parsed.date.precision === "year" ? daysPerYear - 1
166
247
  : parsed.date.precision === "month" ?
167
- (daysInMonth(markers.get(parsed.date.marker)?.calendar, parsed.date.month) ?? 1) - 1
248
+ (daysInMonth(
249
+ markers.get(parsed.date.marker)?.calendar ??
250
+ eras.get(parsed.date.qualifier)?.calendar,
251
+ parsed.date.month,
252
+ ) ?? 1) - 1
168
253
  : 0;
169
254
  const endOffset = firstEndDay + extraDays;
170
255
  const startOffset = canonicalDayOffset(entry.epochYear, entry.epochDay, 1, daysPerYear);
171
256
  if (endOffset < startOffset) {
172
- findings.push({
173
- ...(row.note.file === undefined ? {} : { file: row.note.file }),
174
- severity: "error",
175
- message: `reckoning marker ${marker} ends before its era starts`,
176
- });
257
+ finding(row, "end", `calendar era ${row.era} ends before its start`);
177
258
  continue;
178
259
  }
179
260
  entry.endOffset = endOffset;
180
261
  }
181
- return { markers, findings };
262
+ const calendars = new Map();
263
+ for (const row of declared.values()) {
264
+ const rows = calendars.get(row.note) ?? [];
265
+ rows.push(row);
266
+ calendars.set(row.note, rows);
267
+ }
268
+ for (const rows of calendars.values()) {
269
+ for (let position = 0; position < rows.length; position++) {
270
+ const row = rows[position];
271
+ const entry = eras.get(row.key);
272
+ if (!entry || !Number.isSafeInteger(entry.startOffset)) continue;
273
+ entry.firstEra = position === 0;
274
+ const next = rows[position + 1];
275
+ if (!next) continue;
276
+ const following = eras.get(next.key);
277
+ if (!following || !Number.isSafeInteger(following.startOffset)) continue;
278
+ if (following.startOffset <= entry.startOffset) {
279
+ finding(
280
+ next,
281
+ "start",
282
+ `calendar eras ${row.era} and ${next.era} must begin in increasing order`,
283
+ );
284
+ continue;
285
+ }
286
+ if (entry.endOffset !== undefined && entry.endOffset >= following.startOffset) {
287
+ finding(next, "start", `calendar eras ${row.era} and ${next.era} overlap`);
288
+ continue;
289
+ }
290
+ if (entry.endOffset === undefined) entry.endOffset = following.startOffset - 1;
291
+ }
292
+ }
293
+ return { eras, markers, findings };
182
294
  }
183
295
 
184
296
  /** One resolved date context per corpus index, shared by its note checks. */
@@ -188,11 +300,11 @@ const contexts = new WeakMap();
188
300
  * Read the world's year length and its era markers from one corpus index.
189
301
  *
190
302
  * @param {{notes?: readonly object[]}|undefined} index - The corpus index.
191
- * @returns {{markers: Map<string, object>, daysPerYear: number|undefined, findings: object[]}}
303
+ * @returns {{eras: Map<string, object>, markers: Map<string, object>, daysPerYear: number|undefined, findings: object[]}}
192
304
  */
193
305
  export function reckoningContext(index) {
194
306
  if (!index || typeof index !== "object")
195
- return { markers: new Map(), daysPerYear: undefined, findings: [] };
307
+ return { eras: new Map(), markers: new Map(), daysPerYear: undefined, findings: [] };
196
308
  const cached = contexts.get(index);
197
309
  if (cached) return cached;
198
310
  const world = (index.notes ?? []).find((note) => {
package/engine/scenes.mjs CHANGED
@@ -39,7 +39,7 @@
39
39
  * rather than stored. It is the same vocabulary the link manifest uses.
40
40
  *
41
41
  * Not a standalone script — exports the `Scenes` compiler class, imported and
42
- * driven by `packages/content-build/engine/generate.mjs` (via `npm run build:compiledb`).
42
+ * driven by `packages/package-build/engine/generate.mjs` (via `npm run build:compiledb`).
43
43
  *
44
44
  * The walk itself — filtering by type, expanding tables, converting
45
45
  * wikilinks, writing the JSON and counting errors — belongs to {@link sohl.utils.packs.BasePackCompiler}; this module
@@ -499,7 +499,10 @@ export class Scenes extends BasePackCompiler {
499
499
  // default JournalEntry pack, not in whichever Scene pack the map
500
500
  // itself was routed to.
501
501
  // This compile's router, as everywhere else in this pass.
502
- journalPack: this.#packRouter.defaultOf("JournalEntry"),
502
+ journalPack:
503
+ hasBody ?
504
+ this.#packRouter.resolve(fm, "JournalEntry")
505
+ : this.#packRouter.defaultOf("JournalEntry"),
503
506
  pageIds: hasBody ? this.#pageIds(markdown, entryId, name) : new Map(),
504
507
  knownActions: this.knownActions,
505
508
  warnings,
@@ -468,7 +468,7 @@ export function compareEmittedSystem({
468
468
  *
469
469
  * - **A system**: its own `schema.json`, generated from its `src/` into
470
470
  * `build/` by `package-build schema`.
471
- * - **A module**: the copy cached by `content-build deps fetch`, from the
471
+ * - **A module**: the copy cached by `package-build deps fetch`, from the
472
472
  * archive of the version it pins — which is what makes the comparison happen
473
473
  * at `verified` rather than against whatever the system's `main` holds today.
474
474
  * That distinction is the whole of the `affiliation.subType` case.
@@ -640,7 +640,7 @@ function schemaFor(config, system = undefined) {
640
640
  * version released before the artifact existed, or a subtype the artifact does
641
641
  * not name, produces no findings — the same stance `compareFields` takes, where
642
642
  * an unknown subtype is a routing question rather than a field one. The
643
- * whole-build check in `content-build lint` is where a missing artifact is said
643
+ * whole-build check in `package-build lint` is where a missing artifact is said
644
644
  * out loud, once, instead of per note.
645
645
  *
646
646
  * @param {object} fm - The note's frontmatter.
@@ -690,7 +690,7 @@ export function checkAuthoredSystemData(
690
690
  * **Silent where there is nothing to check against**, exactly as its two
691
691
  * siblings are: a module pinning a system version released before the artifact
692
692
  * existed, or a subtype the artifact does not name, produces no findings.
693
- * `content-build lint` is where a missing artifact is said out loud, once.
693
+ * `package-build lint` is where a missing artifact is said out loud, once.
694
694
  *
695
695
  * @param {object} emitted - The `system` block the compiler produced.
696
696
  * @param {object} opts