@heroiclands/package-build 22.15.1 → 22.17.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 +110 -0
  2. package/README.md +39 -678
  3. package/bin/{content-build.mjs → content-commands.mjs} +197 -118
  4. package/bin/package-build.mjs +315 -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 +296 -236
  12. package/docs/authoring/assets.md +31 -0
  13. package/docs/authoring/dates-and-calendars.md +85 -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 +633 -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 +4362 -0
  36. package/docs/reference/note-types.md +532 -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-format.mjs +441 -0
  46. package/engine/calendar-human.mjs +133 -0
  47. package/engine/calendar-notes.mjs +307 -88
  48. package/engine/calendars.mjs +8 -1
  49. package/engine/changelog-lint.mjs +2 -2
  50. package/engine/code-fences.mjs +23 -97
  51. package/engine/compile-corpus.mjs +6 -7
  52. package/engine/content-admonitions.mjs +114 -0
  53. package/engine/content-captions.mjs +181 -0
  54. package/engine/content-embeds.mjs +2 -2
  55. package/engine/content-footnotes.mjs +34 -0
  56. package/engine/content-format-check.mjs +2 -2
  57. package/engine/content-format.mjs +127 -73
  58. package/engine/content-format.yaml +746 -0
  59. package/engine/content-html.mjs +1 -1
  60. package/engine/content-icons.mjs +45 -92
  61. package/engine/content-images.mjs +22 -74
  62. package/engine/content-index.mjs +12 -3
  63. package/engine/content-links.mjs +21 -26
  64. package/engine/content-lint.mjs +1 -5
  65. package/engine/content-secrets.mjs +3 -1
  66. package/engine/content-tables.mjs +47 -1257
  67. package/engine/culture-choice.mjs +32 -0
  68. package/engine/date-conversion.mjs +47 -0
  69. package/engine/declaration-check.mjs +87 -0
  70. package/engine/docs-checks.mjs +140 -0
  71. package/engine/documentation-links.mjs +31 -0
  72. package/engine/exported-scene.mjs +1 -1
  73. package/engine/extension-attributes.mjs +74 -0
  74. package/engine/field-reference.mjs +1 -1
  75. package/engine/field-spec.mjs +4 -0
  76. package/engine/foreign-catalog.mjs +5 -5
  77. package/engine/format-generated.mjs +15 -0
  78. package/engine/foundry-entries.mjs +3 -1
  79. package/engine/frontmatter-lint.mjs +5 -1
  80. package/engine/frontmatter.mjs +10 -0
  81. package/engine/generate.mjs +2 -2
  82. package/engine/helpers.mjs +71 -117
  83. package/engine/homepage.mjs +2 -3
  84. package/engine/index-records.mjs +1 -0
  85. package/engine/index.mjs +18 -3
  86. package/engine/infobox-registry.mjs +3 -1
  87. package/engine/infobox.mjs +38 -20
  88. package/engine/item-compiler.mjs +10 -8
  89. package/engine/item-docs.mjs +7 -6
  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/markdown-expressions.mjs +118 -0
  95. package/engine/metadata-index.mjs +7 -3
  96. package/engine/note-addresses.mjs +1 -0
  97. package/engine/note-dates.mjs +314 -64
  98. package/engine/note-package.mjs +5 -25
  99. package/engine/note-state.mjs +6 -2
  100. package/engine/note-vocabulary.mjs +74 -19
  101. package/engine/number-words.mjs +75 -0
  102. package/engine/office-holders.mjs +188 -0
  103. package/engine/pack-config.mjs +2 -15
  104. package/engine/pack-router.mjs +24 -5
  105. package/engine/packages.mjs +3 -2
  106. package/engine/pathnames.mjs +21 -24
  107. package/engine/pdf-build.mjs +217 -55
  108. package/engine/pdf-fonts.mjs +2 -2
  109. package/engine/pdf-images.mjs +82 -0
  110. package/engine/pdf-render.mjs +174 -22
  111. package/engine/populations.mjs +3 -2
  112. package/engine/project-init.mjs +499 -0
  113. package/engine/prose-config.mjs +1 -1
  114. package/engine/prose-lint.mjs +7 -6
  115. package/engine/readability-lint.mjs +86 -0
  116. package/engine/reckoning-markers.mjs +176 -166
  117. package/engine/scenes.mjs +5 -2
  118. package/engine/schema-check.mjs +3 -3
  119. package/engine/site-build.mjs +87 -37
  120. package/engine/site-config.mjs +8 -8
  121. package/engine/site-index.mjs +4 -0
  122. package/engine/site-maps.mjs +1 -1
  123. package/engine/site-preview.mjs +19 -8
  124. package/engine/social-ties.mjs +5 -1
  125. package/engine/sql-tables.mjs +92 -22
  126. package/engine/web-wikilinks.mjs +15 -31
  127. package/engine/wikilink-syntax.mjs +4 -1
  128. package/engine/wikilinks.mjs +12 -14
  129. package/hm3/item-fields.mjs +1 -1
  130. package/hm3/items.mjs +12 -45
  131. package/index.mjs +2 -14
  132. package/manifest.mjs +31 -4
  133. package/markdownlint-config.mjs +1 -1
  134. package/package.json +32 -11
  135. package/prettier-config.mjs +1 -1
  136. package/sohl/being-info.mjs +3 -145
  137. package/sohl/index.mjs +1 -1
  138. package/sohl/infobox.mjs +2 -1
  139. package/sohl/item-builders.mjs +3 -27
  140. package/sohl/item-fields.mjs +61 -22
  141. package/sohl/kb-passes.mjs +2 -2
  142. package/sohl/note-schemas.mjs +0 -8
  143. package/stage.mjs +19 -0
  144. package/types/config.d.mts +12 -3
  145. package/types/content-config.d.mts +15 -81
  146. package/types/engine/address-render.d.mts +13 -0
  147. package/types/engine/address.d.mts +2 -11
  148. package/types/engine/anchors.d.mts +3 -4
  149. package/types/engine/asset-bindings.d.mts +9 -2
  150. package/types/engine/base-compiler.d.mts +1 -2
  151. package/types/engine/being-measurements.d.mts +16 -0
  152. package/types/engine/calendar-choice.d.mts +14 -0
  153. package/types/engine/calendar-format.d.mts +33 -0
  154. package/types/engine/calendar-human.d.mts +4 -0
  155. package/types/engine/calendar-notes.d.mts +5 -3
  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 +13 -26
  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
@@ -50,6 +50,8 @@
50
50
  * npx package-build clean [--distclean]
51
51
  * npx package-build assets
52
52
  * npx package-build calendars --calendaria-version <version>
53
+ * npx package-build datefrom <calendar> <date>
54
+ * npx package-build dateto <calendar> <canonical-date>
53
55
  * npx package-build manifest
54
56
  * npx package-build lang check
55
57
  * npx package-build lang coverage [--unused]
@@ -76,6 +78,9 @@
76
78
  import fs from "node:fs";
77
79
  import path from "node:path";
78
80
  import process from "node:process";
81
+ import { spawnSync } from "node:child_process";
82
+ import { createInterface } from "node:readline/promises";
83
+ import { fileURLToPath } from "node:url";
79
84
  import { globSync } from "glob";
80
85
  import yargs from "yargs";
81
86
  import { hideBin } from "yargs/helpers";
@@ -87,9 +92,12 @@ import { writeSiteRoot } from "../engine/site-root.mjs";
87
92
  import { DEPLOY_ROOT } from "../engine/site-config.mjs";
88
93
  import { compilesFoundryDocuments } from "../content-config.mjs";
89
94
  import { loadPackConfig, packConfigPath, resolveConfigFile } from "../engine/pack-config.mjs";
90
- import { cleanBuildArtifacts, stageAssets } from "../stage.mjs";
95
+ import { cleanBuildArtifacts, resetStage, stageAssets } from "../stage.mjs";
91
96
  import { buildSchemaArtifact } from "../engine/schema-extract.mjs";
92
97
  import { emitCalendarArtifacts } from "../engine/calendar-artifacts.mjs";
98
+ import { dateFromCalendar, dateToCalendar } from "../engine/date-conversion.mjs";
99
+ import { walkMarkdownTree } from "../engine/helpers.mjs";
100
+ import { reckoningContext } from "../engine/reckoning-markers.mjs";
93
101
  import { SCHEMA_ARTIFACT_FILE } from "../engine/foreign-catalog.mjs";
94
102
  import { validateLangSource } from "../lang.mjs";
95
103
  import { checkLabelRegistry } from "../labels.mjs";
@@ -112,6 +120,14 @@ import { deployStage } from "../deploy.mjs";
112
120
  import { CONTAINER_ACTIONS, containerAction } from "../container.mjs";
113
121
  import { E2E_MODES, e2eFast, e2eRun, e2eSweep, seedTestWorld } from "../e2e.mjs";
114
122
  import { reportFindings } from "./report.mjs";
123
+ import { formatGenerated } from "../engine/format-generated.mjs";
124
+ import {
125
+ checkProject,
126
+ initializeProject,
127
+ INIT_KINDS,
128
+ INIT_LICENSES,
129
+ } from "../engine/project-init.mjs";
130
+ import { registerContentCommands } from "./content-commands.mjs";
115
131
 
116
132
  /**
117
133
  * How many unreferenced keys a run prints before it summarizes the rest.
@@ -121,6 +137,143 @@ import { reportFindings } from "./report.mjs";
121
137
  */
122
138
  const ADVISORY_PREVIEW = 20;
123
139
 
140
+ /** Replay pull-request workflow commands in Docker or on the host. */
141
+ function ciCommand() {
142
+ return {
143
+ command: "ci",
144
+ describe: "Replay pull-request workflow commands",
145
+ builder: (y) =>
146
+ y.option("native", {
147
+ type: "boolean",
148
+ default: false,
149
+ describe: "Run in the current working tree without Docker",
150
+ }),
151
+ handler: handler(async (args) => {
152
+ const script = fileURLToPath(
153
+ new URL(
154
+ args.native ? "../ci/ci-steps.mjs" : "../ci/ci-docker.mjs",
155
+ import.meta.url,
156
+ ),
157
+ );
158
+ const top = spawnSync("git", ["rev-parse", "--show-toplevel"], {
159
+ encoding: "utf8",
160
+ });
161
+ const root = top.status === 0 ? top.stdout.trim() : process.cwd();
162
+ const result = spawnSync(process.execPath, [script], {
163
+ cwd: root,
164
+ stdio: "inherit",
165
+ });
166
+ if (result.error) throw result.error;
167
+ if (result.status !== 0) process.exitCode = 1;
168
+ }),
169
+ };
170
+ }
171
+
172
+ /** Initialize a repository from anywhere, including outside a Git checkout. */
173
+ function initCommand() {
174
+ return {
175
+ command: "init [directory]",
176
+ describe: "Create or check a content package with site and book targets",
177
+ builder: (y) =>
178
+ y
179
+ .positional("directory", {
180
+ type: "string",
181
+ describe: "Directory to initialize or check (default: current directory)",
182
+ })
183
+ .option("check", {
184
+ type: "boolean",
185
+ default: false,
186
+ describe: "Check an existing package without writing files",
187
+ })
188
+ .option("kind", {
189
+ type: "string",
190
+ choices: INIT_KINDS,
191
+ describe: "Package kind",
192
+ })
193
+ .option("name", { type: "string", describe: "npm and Foundry package name" })
194
+ .option("title", { type: "string", describe: "Human-readable package title" })
195
+ .option("description", { type: "string", describe: "Package and site description" })
196
+ .option("author", { type: "string", describe: "Author credited in the package" })
197
+ .option("license", {
198
+ type: "string",
199
+ choices: INIT_LICENSES,
200
+ describe: "Original or unofficial fan-material license template",
201
+ })
202
+ .option("core-minimum", {
203
+ type: "string",
204
+ default: "14.359",
205
+ describe: "Minimum Foundry core version for a Foundry package",
206
+ })
207
+ .option("core-verified", {
208
+ type: "string",
209
+ default: "14.364",
210
+ describe: "Verified Foundry core version for a Foundry package",
211
+ }),
212
+ handler: handler(async (args) => {
213
+ const root = path.resolve(args.directory ?? process.cwd());
214
+ if (args.check) {
215
+ const findings = checkProject(root);
216
+ for (const { file, message } of findings) {
217
+ console.error(`${path.join(root, file)}: error: ${message}`);
218
+ }
219
+ if (findings.length) process.exitCode = 1;
220
+ else console.log(`package-build: ${root} is ready to build.`);
221
+ return;
222
+ }
223
+ const defaults = {
224
+ name: path.basename(root),
225
+ title: path
226
+ .basename(root)
227
+ .replaceAll("-", " ")
228
+ .split(" ")
229
+ .map((word) => word[0]?.toUpperCase() + word.slice(1))
230
+ .join(" "),
231
+ kind: "modules",
232
+ license: "original",
233
+ };
234
+ const answers = {
235
+ name: args.name,
236
+ title: args.title,
237
+ description: args.description,
238
+ author: args.author,
239
+ kind: args.kind,
240
+ license: args.license,
241
+ };
242
+ const questions = ["name", "title", "description", "author", "kind", "license"];
243
+ if (questions.some((key) => !answers[key])) {
244
+ if (!process.stdin.isTTY) {
245
+ const missing = questions.filter((key) => !answers[key]);
246
+ throw new Error(
247
+ `init: supply ${missing.map((key) => `--${key}`).join(", ")} when input is not interactive`,
248
+ );
249
+ }
250
+ const input = createInterface({ input: process.stdin, output: process.stdout });
251
+ try {
252
+ for (const key of questions) {
253
+ if (answers[key]) continue;
254
+ const fallback = defaults[key];
255
+ const value = await input.question(
256
+ `${key}${fallback ? ` [${fallback}]` : ""}: `,
257
+ );
258
+ answers[key] = value.trim() || fallback;
259
+ }
260
+ } finally {
261
+ input.close();
262
+ }
263
+ }
264
+ const files = await initializeProject(root, {
265
+ ...answers,
266
+ version: ownVersion(),
267
+ coreMinimum: args.coreMinimum,
268
+ coreVerified: args.coreVerified,
269
+ });
270
+ console.log(`package-build: created ${files.length} files in ${root}.`);
271
+ console.log("Run `npm install`, then `npm run lint`.");
272
+ console.log("Use `npm run build:site`, `npm run serve:site`, or `npm run build:book`.");
273
+ }),
274
+ };
275
+ }
276
+
124
277
  /**
125
278
  * This package's own version, for `--version`.
126
279
  *
@@ -318,6 +471,24 @@ function assetsCommand() {
318
471
  };
319
472
  }
320
473
 
474
+ /** Clear the package stage before a complete build writes to it. */
475
+ function stageCommand() {
476
+ return {
477
+ command: "stage <action>",
478
+ describe: "Manage the assembled Foundry package stage",
479
+ builder: (y) =>
480
+ y.positional("action", {
481
+ choices: ["reset"],
482
+ describe: "Remove the configured stage directory",
483
+ }),
484
+ handler: handler(() => {
485
+ const config = loadPackageBuildConfig();
486
+ const removed = resetStage(config.rootDir, config.stageDir);
487
+ console.log(removed ? `Removed ${config.stageDir}` : "Stage is already empty.");
488
+ }),
489
+ };
490
+ }
491
+
321
492
  /** Emit the package's calendar definitions from its content index. */
322
493
  function calendarsCommand() {
323
494
  return {
@@ -340,32 +511,122 @@ function calendarsCommand() {
340
511
  };
341
512
  }
342
513
 
343
- /**
344
- * Format generated text the way the repository formats everything else.
345
- *
346
- * Not cosmetic. A generated file that Prettier would reformat leaves
347
- * `lint:format` and the generator's own `--check` each demanding what the other
348
- * forbids, and the repository cannot be made green. Resolving the config from
349
- * the *output path* is what makes one implementation here serve repositories
350
- * with different Prettier settings.
351
- *
352
- * Imported on use, as `prose-lint.mjs` does, so that commands which never
353
- * format do not pay to load it.
354
- *
355
- * @param {string} text - The unformatted content.
356
- * @param {string} filepath - Where it will be written.
357
- * @returns {Promise<string>} The formatted content.
358
- */
359
- async function formatGenerated(text, filepath) {
360
- const prettier = await import("prettier");
361
- const config = await prettier.resolveConfig(filepath);
362
- return prettier.format(text, { ...config, filepath });
514
+ /** Read the current content tree so conversion uses its authored calendars. */
515
+ function calendarConversionContext() {
516
+ const config = loadPackConfig();
517
+ if (!fs.existsSync(config.paths.content))
518
+ throw new Error(`no content tree at ${config.paths.content}`);
519
+ const notes = [
520
+ ...walkMarkdownTree(config.paths.content, {
521
+ skipDirectories: config.skipDirectories,
522
+ }),
523
+ ].map(({ frontmatter, absPath }) => ({ fm: frontmatter, file: absPath }));
524
+ const context = reckoningContext({ notes });
525
+ const error = context.findings.find((finding) => finding.severity === "error");
526
+ if (error) throw Object.assign(new Error(formatDiagnostic(error)), { located: true });
527
+ if (!Number.isSafeInteger(context.daysPerYear) || context.daysPerYear < 1)
528
+ throw new Error("the content tree needs a world with data.year.days");
529
+ return context;
530
+ }
531
+
532
+ /** Convert a calendar day to the world's canonical year and day. */
533
+ function datefromCommand() {
534
+ return {
535
+ command: "datefrom <calendar> <date>",
536
+ describe: "Convert a calendar date to its canonical value",
537
+ builder: (y) =>
538
+ y
539
+ .positional("calendar", {
540
+ type: "string",
541
+ describe: "Calendar Address or shortcode",
542
+ })
543
+ .positional("date", {
544
+ type: "string",
545
+ describe: "Day written in that calendar's std format (or first named format)",
546
+ }),
547
+ handler: handler((args) => {
548
+ console.log(dateFromCalendar(args.calendar, args.date, calendarConversionContext()));
549
+ }),
550
+ };
551
+ }
552
+
553
+ /** Express a canonical day in the covering era of a calendar. */
554
+ function datetoCommand() {
555
+ return {
556
+ command: "dateto <calendar> <canonical-date>",
557
+ describe: "Convert a canonical day to a calendar date",
558
+ builder: (y) =>
559
+ y
560
+ .positional("calendar", {
561
+ type: "string",
562
+ describe: "Calendar Address or shortcode",
563
+ })
564
+ .positional("canonical-date", {
565
+ type: "string",
566
+ describe: "Canonical <year>.<day>[:HHMMSS]",
567
+ })
568
+ .option("format", {
569
+ type: "string",
570
+ describe: "Named calendar format; defaults to std or the first declared format",
571
+ }),
572
+ handler: handler((args) => {
573
+ console.log(
574
+ dateToCalendar(
575
+ args.calendar,
576
+ args.canonicalDate,
577
+ calendarConversionContext(),
578
+ args.format,
579
+ ),
580
+ );
581
+ }),
582
+ };
583
+ }
584
+
585
+ /** Check a project's declarations with library checks enabled. */
586
+ function typesCheckCommand() {
587
+ return {
588
+ command: "check",
589
+ describe: "Check project declaration files with TypeScript library checks enabled",
590
+ builder: (yargs) =>
591
+ yargs
592
+ .option("project", {
593
+ describe: "TypeScript project file.",
594
+ type: "string",
595
+ default: "tsconfig.json",
596
+ })
597
+ .option("exports", {
598
+ describe: "Include every declaration entry point in package.json exports.",
599
+ type: "boolean",
600
+ default: false,
601
+ }),
602
+ handler: handler(async (args) => {
603
+ const { checkDeclarations } = await import("../engine/declaration-check.mjs");
604
+ const findings = checkDeclarations(args.project, { exports: args.exports }).map(
605
+ (finding) => ({
606
+ ...finding,
607
+ file: path.relative(process.cwd(), finding.file),
608
+ }),
609
+ );
610
+ if (reportFindings(findings, {}) > 0) process.exitCode = 1;
611
+ else console.log("Package declarations are valid.");
612
+ }),
613
+ };
614
+ }
615
+
616
+ /** Check project declaration files. */
617
+ function typesCommand() {
618
+ return {
619
+ command: "types <action>",
620
+ describe: "Check TypeScript declarations",
621
+ builder: (yargs) => yargs.command(typesCheckCommand()).demandCommand(1).strict(),
622
+ handler: () => {},
623
+ };
363
624
  }
364
625
 
365
626
  /**
366
627
  * `schema` — publish this package's DataModel field sets as `schema.json`.
367
628
  *
368
- * The producing half of the check `content-build lint` runs: Foundry discards
629
+ * The producing half of the check `package-build lint` runs: Foundry discards
369
630
  * an unknown `system` key at construction and says nothing, so a content build
370
631
  * needs to know what a document will actually receive. It cannot ask a
371
632
  * running Foundry, and it cannot read `defineSchema()` from a sibling checkout,
@@ -447,7 +708,7 @@ function manifestCommand() {
447
708
  die(
448
709
  `\`packageKind: ${shared.packageKind}\` ships no Foundry ` +
449
710
  `package, so there is no manifest to generate. The site ` +
450
- `and the book are built by \`content-build\`.`,
711
+ `and the book are built by \`package-build\`.`,
451
712
  );
452
713
  }
453
714
  const packageJson = readPackageJson(config);
@@ -1406,27 +1667,39 @@ function bumpCommand() {
1406
1667
  };
1407
1668
  }
1408
1669
 
1409
- yargs(hideBin(process.argv))
1410
- .scriptName("package-build")
1411
- .command(cleanCommand())
1412
- .command(assetsCommand())
1413
- .command(calendarsCommand())
1414
- .command(manifestCommand())
1415
- .command(siteRootCommand())
1416
- .command(schemaCommand())
1417
- .command(langCommand())
1418
- .command(labelsCommand())
1419
- .command(bumpCommand())
1420
- .command(yamlCommand())
1421
- .command(changelogCommand())
1422
- .command(bundleCommand())
1423
- .command(releaseCommand())
1424
- .command(deployCommand())
1425
- .command(containerCommand())
1426
- .command(e2eCommand())
1670
+ registerContentCommands(
1671
+ yargs(hideBin(process.argv))
1672
+ .scriptName("package-build")
1673
+ .command(initCommand())
1674
+ .command(ciCommand())
1675
+ .command(cleanCommand())
1676
+ .command(assetsCommand())
1677
+ .command(stageCommand())
1678
+ .command(calendarsCommand())
1679
+ .command(datefromCommand())
1680
+ .command(datetoCommand())
1681
+ .command(manifestCommand())
1682
+ .command(siteRootCommand())
1683
+ .command(typesCommand())
1684
+ .command(schemaCommand())
1685
+ .command(langCommand())
1686
+ .command(labelsCommand())
1687
+ .command(bumpCommand())
1688
+ .command(yamlCommand())
1689
+ .command(changelogCommand())
1690
+ .command(bundleCommand())
1691
+ .command(releaseCommand())
1692
+ .command(deployCommand())
1693
+ .command(containerCommand())
1694
+ .command(e2eCommand()),
1695
+ )
1427
1696
  .demandCommand(1, "Name a command.")
1428
1697
  .strict()
1429
1698
  .version(ownVersion())
1430
1699
  .help()
1431
1700
  .alias("help", "h")
1432
- .fail((message, err) => die(err ?? message)).argv;
1701
+ .epilog("Run package-build <command> --help for that command's actions and options.")
1702
+ .fail((message, err, cli) => {
1703
+ if (!err) cli.showHelp("error");
1704
+ die(err ?? message);
1705
+ }).argv;
package/bin/report.mjs CHANGED
@@ -104,7 +104,7 @@ export function toDiagnostics(findings, { file }) {
104
104
  * Optional only when every finding carries its own.
105
105
  * @param {(d: object) => void} [opts.emit] - How to emit one diagnostic.
106
106
  * Injectable so a test can capture the emitted shape without reaching for
107
- * the console; defaults to content-build's `emitDiagnostic`, which writes
107
+ * the console; defaults to package-build's `emitDiagnostic`, which writes
108
108
  * both severities to stderr.
109
109
  * @returns {number} How many diagnostics were errors — the count a command
110
110
  * turns into its exit code.
package/ci/ci-docker.mjs CHANGED
@@ -194,7 +194,7 @@ function main() {
194
194
  "--rm",
195
195
  ...(native ? [] : ["--platform", "linux/amd64"]),
196
196
  "-v",
197
- `${dir}:/work`,
197
+ `${dir}:/source:ro`,
198
198
  // The step runner comes from this package, not from the export: the
199
199
  // repository under test does not carry it, and should not have to.
200
200
  "-v",
@@ -202,8 +202,11 @@ function main() {
202
202
  "-w",
203
203
  "/work",
204
204
  image,
205
- "node",
206
- "/ci/ci-steps.mjs",
205
+ "sh",
206
+ "-c",
207
+ // Run from the container's own filesystem. Some generators create
208
+ // files that Docker Desktop cannot read back through a macOS mount.
209
+ "mkdir -p /work && cp -a /source/. /work && exec node /ci/ci-steps.mjs",
207
210
  ]);
208
211
 
209
212
  fs.rmSync(dir, { recursive: true, force: true });
package/ci/ci-steps.mjs CHANGED
@@ -69,8 +69,8 @@ const WORKFLOW_DIR = path.join(ROOT, ".github/workflows");
69
69
  /**
70
70
  * The workflow's steps, split into what can be run here and what cannot.
71
71
  *
72
- * @returns {{run: Array<{name: string, run: string}>, skipped: string[]}} The
73
- * `run:` steps in workflow order, and the names of the `uses:` steps.
72
+ * @returns {{run: Array<{name: string, run: string}>, skipped: string[], files: string[]}}
73
+ * Workflow files, their `run:` steps in order, and the `uses:` step names.
74
74
  * @throws {Error} When the workflow cannot be read or declares no `run:` step.
75
75
  */
76
76
  export function ciSteps() {
@@ -213,8 +213,10 @@ function main() {
213
213
  return 1;
214
214
  }
215
215
 
216
- const { run, skipped } = steps;
217
- console.log(`ci-steps: running ${run.length} step(s) from .github/workflows/build.yml`);
216
+ const { run, skipped, files } = steps;
217
+ console.log(
218
+ `ci-steps: running ${run.length} step(s) from ${files.map((file) => path.relative(ROOT, file)).join(", ")}`,
219
+ );
218
220
  if (skipped.length) {
219
221
  console.log(`ci-steps: not runnable here (published actions): ${skipped.join(", ")}`);
220
222
  }
@@ -0,0 +1,106 @@
1
+ /*
2
+ * SPDX-License-Identifier: GPL-3.0-or-later
3
+ */
4
+
5
+ import fs from "node:fs";
6
+ import path from "node:path";
7
+ import { fileURLToPath } from "node:url";
8
+ import { formatGenerated } from "../engine/format-generated.mjs";
9
+ import { loadContentFormat } from "../engine/content-format.mjs";
10
+ import { NOTE_VOCABULARY, SHARED_DATA_FIELDS } from "../engine/note-vocabulary.mjs";
11
+
12
+ const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
13
+ const output = path.join(root, "docs/reference/note-types.md");
14
+ const contract = loadContentFormat();
15
+ const cell = (value) =>
16
+ String(value ?? "")
17
+ .replaceAll("|", "\\|")
18
+ .replaceAll("\n", " ");
19
+ const fields = (list) =>
20
+ list.map(
21
+ (field) =>
22
+ `| \`data.${cell(field.name)}\` | ${cell(field.shape ?? field.kind ?? "value")} | ${cell(field.describe)} |`,
23
+ );
24
+ const rows = [
25
+ "---",
26
+ "shortcode: referencenotetypes",
27
+ 'name: { full: "Note types and fields" }',
28
+ "type: doc",
29
+ "subType: reference",
30
+ "---",
31
+ "",
32
+ "# Note types and fields",
33
+ "",
34
+ "Choose a note's `type` for the subject it describes. Use `subType` when its type declares a more specific genre. Field rules are declared in `engine/note-vocabulary.mjs`; mapping claims and closed vocabularies are recorded in `engine/content-format.yaml`. This page is checked against both declarations.",
35
+ "",
36
+ "The [first-note guide](../authoring/first-note.md) teaches the shape of a note. [Frontmatter](../authoring/frontmatter.md) explains shared and system-specific values. The [detailed reference](format-details.md) gives the full behavior and examples.",
37
+ "",
38
+ "## Shared `data` fields",
39
+ "",
40
+ "These fields are accepted by every note type. A field's value can still be irrelevant to a particular output; the build reports that where it can.",
41
+ "",
42
+ "| Field | Shape | Meaning |",
43
+ "| --- | --- | --- |",
44
+ ...fields(SHARED_DATA_FIELDS),
45
+ "",
46
+ "## Type-specific fields",
47
+ "",
48
+ ];
49
+ for (const [type, spec] of contract.types) {
50
+ const declared = NOTE_VOCABULARY[type];
51
+ if (!declared) throw new Error(`No vocabulary for ${type}`);
52
+ rows.push(`### ${type}`, "");
53
+ rows.push(
54
+ `**Subtypes:** ${
55
+ declared.subTypes === null ? "Open vocabulary"
56
+ : spec.subTypes.length ? spec.subTypes.map((value) => `\`${value}\``).join(", ")
57
+ : "None"
58
+ }.`,
59
+ "",
60
+ );
61
+ if (type === "lore") {
62
+ rows.push(
63
+ "Lore records in-world knowledge. A `culture` describes a people; a `custom` describes how they practice a rite, observance, or usage. A `material` describes a physical constituent and its qualities, which may vary by region. See the [lore subtype definitions](format-details.md#type-lore) for the complete vocabulary.",
64
+ "",
65
+ );
66
+ }
67
+ rows.push("| Field | Shape | Meaning |", "| --- | --- | --- |");
68
+ rows.push(
69
+ ...(declared.data?.length ?
70
+ fields(declared.data)
71
+ : ["| — | — | No type-specific `data` fields. |"]),
72
+ );
73
+ rows.push("");
74
+ const claims = contract.claims.filter((claim) => !claim.shared && claim.noteType === type);
75
+ if (claims.length) {
76
+ rows.push("**System mappings**", "", "| Source | System | Target |", "| --- | --- | --- |");
77
+ rows.push(
78
+ ...claims.map(
79
+ (claim) =>
80
+ `| \`${cell(claim.source)}\` | ${cell(claim.system)} | \`${cell(claim.target)}\` |`,
81
+ ),
82
+ );
83
+ rows.push("");
84
+ }
85
+ }
86
+ rows.push("## Shared system mappings", "", "| Source | System | Target |", "| --- | --- | --- |");
87
+ rows.push(
88
+ ...contract.claims
89
+ .filter((claim) => claim.shared)
90
+ .map(
91
+ (claim) =>
92
+ `| \`${cell(claim.source)}\` | ${cell(claim.system)} | \`${cell(claim.target)}\` |`,
93
+ ),
94
+ );
95
+ rows.push("");
96
+ const result = await formatGenerated(`${rows.join("\n")}\n`, output);
97
+ if (process.argv.includes("--check")) {
98
+ if (!fs.existsSync(output) || fs.readFileSync(output, "utf8") !== result) {
99
+ console.error(
100
+ `${path.relative(root, output)}: error: run node ci/content-format-reference.mjs to refresh the note-type reference`,
101
+ );
102
+ process.exitCode = 1;
103
+ }
104
+ } else {
105
+ process.stdout.write(result);
106
+ }
package/config.mjs CHANGED
@@ -75,6 +75,7 @@ const SECTION_KEYS = [
75
75
  "bundle",
76
76
  "container",
77
77
  "e2e",
78
+ "proseLint",
78
79
  ];
79
80
 
80
81
  /**
@@ -526,7 +527,8 @@ function normalizeExceptions(value, field, where) {
526
527
  * The resolved `packageBuild` section, every optional half filled in.
527
528
  *
528
529
  * @typedef {object} PackageBuildConfig
529
- * @property {string} rootDir The repository root, from content-build.
530
+ * @property {string} rootDir The repository root, from package-build.
531
+ * @property {{age: number, threshold: number, minWords: number}} proseLint Optional prose analysis settings.
530
532
  * @property {string} packageKind `systems` or `modules`.
531
533
  * @property {string} packageId The Foundry package id.
532
534
  * @property {string} artifact Derived: `system` or `module`.
@@ -689,6 +691,23 @@ export function resolvePackageBuildConfig(shared) {
689
691
  const section = /** @type {Record<string, unknown>} */ (shared.packageBuild ?? {});
690
692
  rejectUnknownKeys(section, SECTION_KEYS, "packageBuild.");
691
693
 
694
+ const proseLint = section.proseLint ?? {};
695
+ if (!isMapping(proseLint)) fail("packageBuild.proseLint", "must be a mapping");
696
+ rejectUnknownKeys(proseLint, ["age", "threshold", "minWords"], "packageBuild.proseLint.");
697
+ const proseOptions = { age: 21, threshold: 5, minWords: 8 };
698
+ for (const [key, value] of Object.entries(proseLint)) {
699
+ const upper = key === "threshold" ? 7 : Infinity;
700
+ if (!Number.isInteger(value) || value < 1 || value > upper) {
701
+ fail(
702
+ `packageBuild.proseLint.${key}`,
703
+ upper === Infinity ?
704
+ "must be a positive integer"
705
+ : "must be an integer from 1 to 7",
706
+ );
707
+ }
708
+ proseOptions[key] = value;
709
+ }
710
+
692
711
  if (section.assets !== undefined && !Array.isArray(section.assets)) {
693
712
  fail("packageBuild.assets", "must be a list");
694
713
  }
@@ -789,6 +808,7 @@ export function resolvePackageBuildConfig(shared) {
789
808
 
790
809
  return Object.freeze({
791
810
  rootDir: shared.rootDir,
811
+ proseLint: Object.freeze(proseOptions),
792
812
  // Where the package is assembled before it is zipped or deployed. Every
793
813
  // asset destination is relative to it, so a repository's table says
794
814
  // `lang`, not `build/stage/lang` — the latter is what each consumer's
@@ -921,7 +941,7 @@ export function resolvePackageBuildConfig(shared) {
921
941
  /**
922
942
  * The repository's resolved package-build configuration.
923
943
  *
924
- * Read on call rather than at import, exactly as content-build resolves its
944
+ * Read on call rather than at import, exactly as package-build resolves its
925
945
  * own: importing a module of this package must not require a configuration to
926
946
  * exist anywhere above it.
927
947
  *