@heroiclands/package-build 23.1.0 → 23.2.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 (248) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/bin/content-commands.mjs +68 -149
  3. package/bin/package-build.mjs +56 -10
  4. package/ci/check-banners.mjs +42 -6
  5. package/ci/confirm-npm-version.mjs +135 -0
  6. package/ci/content-format-reference.mjs +14 -5
  7. package/config.mjs +1 -0
  8. package/container.mjs +185 -4
  9. package/content-config.mjs +333 -210
  10. package/deploy.mjs +116 -10
  11. package/docs/api.md +238 -246
  12. package/docs/authoring/assets.md +11 -5
  13. package/docs/authoring/frontmatter.md +1 -1
  14. package/docs/authoring/links-and-markup.md +87 -0
  15. package/docs/commands.md +138 -99
  16. package/docs/configuration.md +277 -125
  17. package/docs/content-format.md +2 -0
  18. package/docs/diagnostics.md +48 -13
  19. package/docs/guides/book.md +341 -21
  20. package/docs/guides/e2e.md +13 -2
  21. package/docs/guides/icons.md +14 -4
  22. package/docs/guides/module-project.md +18 -8
  23. package/docs/guides/release-and-deploy.md +54 -6
  24. package/docs/guides/site.md +5 -2
  25. package/docs/project-setup.md +30 -10
  26. package/docs/reference/format-details.md +373 -170
  27. package/docs/reference/note-types.md +128 -109
  28. package/docs/reference/theme-pages.md +31 -6
  29. package/e2e.mjs +140 -28
  30. package/engine/actor-compiler.mjs +22 -31
  31. package/engine/address-charset.mjs +4 -17
  32. package/engine/address-fields.mjs +1 -1
  33. package/engine/address-render.mjs +12 -1
  34. package/engine/address-values.mjs +6 -0
  35. package/engine/address.mjs +46 -33
  36. package/engine/art-fields.mjs +30 -5
  37. package/engine/art-slots.mjs +10 -1
  38. package/engine/asset-bindings.mjs +12 -1
  39. package/engine/base-compiler.mjs +34 -65
  40. package/engine/being-age.mjs +2 -7
  41. package/engine/being-measurements.mjs +12 -1
  42. package/engine/calendar-artifacts.mjs +6 -0
  43. package/engine/calendar-choice.mjs +12 -1
  44. package/engine/calendar-format.mjs +12 -1
  45. package/engine/calendar-human.mjs +12 -1
  46. package/engine/compile-corpus.mjs +3 -1
  47. package/engine/content-address.mjs +9 -12
  48. package/engine/content-alerts.mjs +13 -1
  49. package/engine/content-blocks.mjs +14 -3
  50. package/engine/content-embeds.mjs +11 -3
  51. package/engine/content-figures.mjs +9 -0
  52. package/engine/content-footnotes.mjs +12 -1
  53. package/engine/content-format-check.mjs +11 -262
  54. package/engine/content-format.yaml +5 -2
  55. package/engine/content-images.mjs +3 -0
  56. package/engine/content-index.mjs +443 -20
  57. package/engine/content-links.mjs +29 -35
  58. package/engine/content-lint.mjs +2 -3
  59. package/engine/content-poetry.mjs +12 -1
  60. package/engine/content-slug.mjs +2 -14
  61. package/engine/content-spans.mjs +13 -1
  62. package/engine/content-tables.mjs +12 -1
  63. package/engine/culture-choice.mjs +210 -13
  64. package/engine/data-keys.mjs +19 -16
  65. package/engine/date-conversion.mjs +12 -1
  66. package/engine/declaration-check.mjs +9 -0
  67. package/engine/derived-fields.mjs +6 -7
  68. package/engine/diagnostics.mjs +34 -4
  69. package/engine/docs-checks.mjs +9 -0
  70. package/engine/document-subtypes.mjs +47 -34
  71. package/engine/event-views.mjs +139 -14
  72. package/engine/exported-scene.mjs +92 -28
  73. package/engine/extension-attributes.mjs +12 -1
  74. package/engine/field-spec.mjs +11 -102
  75. package/engine/folder-notes.mjs +0 -64
  76. package/engine/foreign-catalog.mjs +2 -1
  77. package/engine/format-generated.mjs +9 -0
  78. package/engine/foundry-entries.mjs +10 -28
  79. package/engine/frontmatter-lint.mjs +83 -392
  80. package/engine/frontmatter.mjs +20 -70
  81. package/engine/generate.mjs +10 -15
  82. package/engine/heading-attributes.mjs +12 -1
  83. package/engine/helpers.mjs +41 -102
  84. package/engine/holdings.mjs +1 -1
  85. package/engine/homepage.mjs +3 -10
  86. package/engine/ids.mjs +1 -167
  87. package/engine/index-records.mjs +6 -4
  88. package/engine/index.mjs +0 -3
  89. package/engine/infobox-render.mjs +15 -3
  90. package/engine/infobox.mjs +13 -14
  91. package/engine/item-compiler.mjs +4 -10
  92. package/engine/item-docs.mjs +3 -5
  93. package/engine/item-registry.mjs +2 -10
  94. package/engine/journals.mjs +1 -5
  95. package/engine/literature-notes.mjs +4 -12
  96. package/engine/macros.mjs +1 -2
  97. package/engine/map-graphviz.mjs +12 -1
  98. package/engine/markdown-expressions.mjs +12 -1
  99. package/engine/markdownlint-run.mjs +567 -0
  100. package/engine/metadata-index.mjs +42 -12
  101. package/engine/note-addresses.mjs +143 -35
  102. package/engine/note-claims.mjs +14 -28
  103. package/engine/note-dates.mjs +3 -3
  104. package/engine/note-frontmatter.mjs +6 -0
  105. package/engine/note-ids.mjs +8 -14
  106. package/engine/note-package.mjs +8 -25
  107. package/engine/note-state.mjs +1 -3
  108. package/engine/note-vocabulary.mjs +148 -84
  109. package/engine/number-words.mjs +12 -1
  110. package/engine/pack-config.mjs +15 -25
  111. package/engine/pack-router.mjs +10 -16
  112. package/engine/pathnames.mjs +8 -22
  113. package/engine/pdf-build.mjs +773 -121
  114. package/engine/pdf-coverage.mjs +188 -0
  115. package/engine/pdf-index.mjs +54 -7
  116. package/engine/pdf-pages.mjs +247 -0
  117. package/engine/pdf-render.mjs +114 -4
  118. package/engine/pdf-toc.mjs +29 -32
  119. package/engine/project-init.mjs +46 -6
  120. package/engine/prose-config.mjs +1 -1
  121. package/engine/prose-lint.mjs +52 -88
  122. package/engine/readability-lint.mjs +9 -0
  123. package/engine/reckoning-markers.mjs +12 -1
  124. package/engine/reference-targets.mjs +7 -0
  125. package/engine/runtime-only-fields.mjs +5 -8
  126. package/engine/scene-levels.mjs +2 -2
  127. package/engine/scenes.mjs +1 -1
  128. package/engine/shortcode-references.mjs +7 -0
  129. package/engine/site-build.mjs +30 -16
  130. package/engine/site-index.mjs +10 -16
  131. package/engine/site-preview.mjs +6 -0
  132. package/engine/site-root.mjs +8 -1
  133. package/engine/social-tie-terms.mjs +12 -1
  134. package/engine/social-ties.mjs +3 -0
  135. package/engine/sql-tables.mjs +56 -20
  136. package/engine/standing-terms.mjs +11 -1
  137. package/engine/standings.mjs +11 -27
  138. package/engine/system-block.mjs +39 -141
  139. package/engine/system-vocabulary.mjs +6 -0
  140. package/engine/walk-files.mjs +69 -0
  141. package/engine/web-wikilinks.mjs +23 -3
  142. package/engine/wikilink-syntax.mjs +32 -21
  143. package/engine/wikilinks.mjs +27 -16
  144. package/engine/yaml-lint.mjs +7 -4
  145. package/githooks/attribution.mjs +8 -0
  146. package/githooks/commit-message.mjs +9 -0
  147. package/hm3/actors.mjs +9 -48
  148. package/hm3/default-item-art.mjs +9 -9
  149. package/hm3/item-fields.mjs +2 -9
  150. package/hm3/template-priority.mjs +6 -7
  151. package/hugo-theme/data/banner-kinds.yaml +78 -0
  152. package/hugo-theme/data/banners.yaml +2 -3
  153. package/hugo-theme/layouts/404.html +3 -3
  154. package/hugo-theme/layouts/partials/banner-url.html +31 -7
  155. package/hugo-theme/layouts/partials/hero-banner.html +13 -14
  156. package/hugo-theme/layouts/partials/related.html +1 -1
  157. package/hugo-theme/layouts/partials/warn-missing-banner.html +1 -1
  158. package/hugo-theme/static/css/style.css +1 -1
  159. package/manifest.mjs +11 -4
  160. package/markdownlint-config.mjs +1 -1
  161. package/package.json +6 -3
  162. package/release.mjs +83 -18
  163. package/sohl/actors.mjs +16 -27
  164. package/sohl/default-item-art.mjs +3 -3
  165. package/sohl/document-subtypes.mjs +1 -16
  166. package/sohl/infobox.mjs +2 -3
  167. package/sohl/item-builders.mjs +5 -8
  168. package/sohl/item-fields.mjs +5 -35
  169. package/sohl/items.mjs +0 -4
  170. package/sohl/kb-passes.mjs +3 -3
  171. package/types/container.d.mts +116 -5
  172. package/types/content-config.d.mts +83 -35
  173. package/types/deploy.d.mts +42 -6
  174. package/types/e2e.d.mts +35 -3
  175. package/types/engine/address-charset.d.mts +4 -17
  176. package/types/engine/address.d.mts +17 -8
  177. package/types/engine/art-fields.d.mts +7 -4
  178. package/types/engine/art-slots.d.mts +6 -0
  179. package/types/engine/base-compiler.d.mts +15 -17
  180. package/types/engine/content-address.d.mts +9 -12
  181. package/types/engine/content-embeds.d.mts +4 -2
  182. package/types/engine/content-format-check.d.mts +0 -64
  183. package/types/engine/content-index.d.mts +82 -3
  184. package/types/engine/content-links.d.mts +2 -4
  185. package/types/engine/culture-choice.d.mts +51 -8
  186. package/types/engine/data-keys.d.mts +2 -2
  187. package/types/engine/derived-fields.d.mts +1 -2
  188. package/types/engine/diagnostics.d.mts +27 -4
  189. package/types/engine/document-subtypes.d.mts +12 -31
  190. package/types/engine/event-views.d.mts +14 -2
  191. package/types/engine/exported-scene.d.mts +3 -2
  192. package/types/engine/field-spec.d.mts +13 -122
  193. package/types/engine/folder-notes.d.mts +0 -39
  194. package/types/engine/foreign-catalog.d.mts +2 -1
  195. package/types/engine/foundry-entries.d.mts +4 -5
  196. package/types/engine/frontmatter-lint.d.mts +2 -2
  197. package/types/engine/frontmatter.d.mts +19 -45
  198. package/types/engine/generate.d.mts +1 -1
  199. package/types/engine/helpers.d.mts +25 -42
  200. package/types/engine/homepage.d.mts +3 -10
  201. package/types/engine/ids.d.mts +0 -114
  202. package/types/engine/index-records.d.mts +6 -4
  203. package/types/engine/index.d.mts +0 -1
  204. package/types/engine/infobox-render.d.mts +7 -1
  205. package/types/engine/infobox.d.mts +3 -2
  206. package/types/engine/item-docs.d.mts +1 -1
  207. package/types/engine/item-registry.d.mts +2 -4
  208. package/types/engine/literature-notes.d.mts +1 -1
  209. package/types/engine/markdownlint-run.d.mts +40 -0
  210. package/types/engine/metadata-index.d.mts +9 -1
  211. package/types/engine/note-addresses.d.mts +27 -2
  212. package/types/engine/note-claims.d.mts +3 -5
  213. package/types/engine/note-ids.d.mts +2 -4
  214. package/types/engine/note-package.d.mts +1 -1
  215. package/types/engine/note-state.d.mts +1 -3
  216. package/types/engine/note-vocabulary.d.mts +28 -21
  217. package/types/engine/pack-config.d.mts +2 -6
  218. package/types/engine/pack-router.d.mts +4 -4
  219. package/types/engine/pathnames.d.mts +3 -3
  220. package/types/engine/pdf-build.d.mts +98 -12
  221. package/types/engine/pdf-coverage.d.mts +60 -0
  222. package/types/engine/pdf-index.d.mts +34 -5
  223. package/types/engine/pdf-pages.d.mts +143 -0
  224. package/types/engine/pdf-render.d.mts +45 -1
  225. package/types/engine/pdf-toc.d.mts +12 -14
  226. package/types/engine/prose-config.d.mts +1 -1
  227. package/types/engine/prose-lint.d.mts +11 -10
  228. package/types/engine/runtime-only-fields.d.mts +2 -2
  229. package/types/engine/site-build.d.mts +9 -5
  230. package/types/engine/site-index.d.mts +4 -5
  231. package/types/engine/site-root.d.mts +1 -1
  232. package/types/engine/sql-tables.d.mts +31 -2
  233. package/types/engine/standing-terms.d.mts +2 -1
  234. package/types/engine/standings.d.mts +7 -9
  235. package/types/engine/system-block.d.mts +10 -44
  236. package/types/engine/walk-files.d.mts +15 -0
  237. package/types/engine/wikilink-syntax.d.mts +18 -19
  238. package/types/engine/yaml-lint.d.mts +5 -4
  239. package/types/hm3/actors.d.mts +1 -2
  240. package/types/hm3/item-fields.d.mts +2 -4
  241. package/types/hm3/template-priority.d.mts +6 -7
  242. package/types/manifest.d.mts +3 -2
  243. package/types/release.d.mts +12 -7
  244. package/types/sohl/actors.d.mts +2 -1
  245. package/types/sohl/document-subtypes.d.mts +1 -2
  246. package/types/sohl/item-fields.d.mts +3 -7
  247. package/engine/retired-fields.mjs +0 -631
  248. package/types/engine/retired-fields.d.mts +0 -354
package/CHANGELOG.md CHANGED
@@ -1,5 +1,76 @@
1
1
  # @heroiclands/package-build
2
2
 
3
+ ## 23.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ **Books**
8
+
9
+ - `pdf:` declares its books in a `books:` list; a configuration that still writes `pdf.title` or `pdf.document` fails. To move, put `title`, `subtitle`, `document` and `front` into one `pdf.books` entry with an `id`; `fonts`, `iconFonts`, `binary` and `out` stay directly under `pdf:`.
10
+ - `package-build pdf` builds every declared book, and `--book <id>` builds one. Each book's PDF is named from its `out` stem (default `<package>-<id>`) and the version; write `out: <package>` to keep a single book's file name.
11
+
12
+ **Authors**
13
+
14
+ - Authors are listed once, in a top-level `authors` list, each with an optional role; the Foundry manifest's authors and the site's byline come from it. Move `packageBuild.manifest.authors` up to `authors`; the manifest block refuses it.
15
+
16
+ **Cultures**
17
+
18
+ - A setting guide must name the culture it introduces in `data.culture`. Places, affiliations and every kind of lore may name theirs; other `doc` notes may not, and a culture note may state it only as itself.
19
+
20
+ **Closed note fields**
21
+
22
+ - `icon` is accepted only on beings, Items, macros and bundles; a document, map, lore, place or scenario that writes it is refused at the key. An empty `icon`, `tokenIcon` or `banner` is an error at its line.
23
+ - A `data.banner` naming no image is an error at its line, and `data.affiliations` written as a list is an error at its line.
24
+ - A note or a being's `items:` entry with an undeclared `type` is refused at its `type:` line, including `character`, `creature`, `armor`, `concoction`, `projectile` and the map types `battlemap`, `localmap` and `regionalmap`.
25
+ - A `data:` property written at the top level, or in a `sohl:` or `hm3:` block, is an error at its own line; `archetype`, `relation`, `birthday`, a being's `stations`, and top-level `draft`, `aliases`, `section`, `traits` and `folder` are unrecognized keys.
26
+ - Address shortcodes must be lowercase; a reference that spells one with capitals is an error.
27
+ - Undeclared configuration keys, such as `packs[].folders` and `site.trees`, are refused as unrecognized options, and `package-build content-format notes` is not a command.
28
+
29
+ ### Patch Changes
30
+
31
+ **Books**
32
+
33
+ - A run reports every printable note no book selects, as a warning by default; `pdf.coverage` makes it an error or silences it, and `pdf.unprinted` names notes that belong in no book.
34
+ - A note several books select prints in each, and the summary names its home book, the first in the list; a cross-reference to a note in another book names that book and its page.
35
+ - Each book's index lists its own notes.
36
+ - A section whose queries select nothing is left out of the book; a book that selects nothing at all is an error.
37
+ - A book can open with a full-page cover image under its title, and its title page lists the authors, the copyright and the licence.
38
+ - `package-build pdf --list-assets` prints the output path of each declared book as JSON, without building anything.
39
+
40
+ **Cultures**
41
+
42
+ - A place takes the culture of the places it sits within, and an affiliation that of the bodies it answers to; a polity with neither takes that of its seat and the places it governs. Content tables, book queries and place and affiliation pages use that inherited culture.
43
+ - A note whose parents belong to different cultures is an error until it names its own.
44
+
45
+ **Pages and maps**
46
+
47
+ - A page declines its hero image with `data.banner: none`, and a `data.banner` image resolves to the picture it names on homepages and content pages alike.
48
+ - A map note can name its thumbnail with `data.thumbnail`, an image Address, and the compiled Scene shows that picture in Foundry's Scene lists without changing the map's background art.
49
+
50
+ **Beings**
51
+
52
+ - A membership in `data.affiliations` can list the societies it is held in with `societies`; on SoHL, the first society named becomes the membership's society on the character sheet.
53
+ - Compiled actors and items keep their default artwork under the HM3 system's asset layout.
54
+ - `package-build lint` resolves a being's embedded `model:` addresses against a `recommends` system that declares `itemCatalog: true`, as the compile does.
55
+
56
+ **Dependencies**
57
+
58
+ - A dependency whose cached index comes from an older package-build fails naming that dependency and the fix: update it, then run `package-build deps fetch`.
59
+
60
+ **Foundry test containers and deployment**
61
+
62
+ - An e2e run seeds its test world as the container's user and launches it, even after Foundry has repaired a database in it; when the world does not activate, the hint names the stage, so `package-build container <stage> logs` can be pasted as written.
63
+ - A container pinned to an exact Foundry build runs that build's own image, such as `felddy/foundryvtt:14.359`, and with the build cached and a licence present it starts without signing in to foundryvtt.com.
64
+ - A deployment removes its previous package tree even when Foundry's container owns the files in it, and sweeps away the `<id>.old-<pid>` trees earlier deployments left; a tree that still cannot be removed is a warning naming the command that removes it.
65
+
66
+ ## 23.1.1
67
+
68
+ ### Patch Changes
69
+
70
+ `package-build yaml` accepts an empty trigger such as `workflow_dispatch:` in a Gitea workflow under `.gitea/workflows/`, as it already does under `.github/workflows/`.
71
+
72
+ Rebuilding a release of the same commit produces an identical `module.zip` or `system.zip`, so a rebuilt archive can be compared with the one already published. Setting `SOURCE_DATE_EPOCH` stamps the archive's files with that time.
73
+
3
74
  ## 23.1.0
4
75
 
5
76
  ### Minor Changes
@@ -24,7 +24,6 @@
24
24
  * npx package-build lint [root] [--no-references]
25
25
  * npx package-build content-format schema --schema <system>=<path>
26
26
  * npx package-build content-format fields [--fields <system>]
27
- * npx package-build content-format notes [root] [--strict]
28
27
  * npx package-build links [root] [--manifests <dir>]
29
28
  * npx package-build format [paths..] [--write]
30
29
  * npx package-build markdown [paths..] [--fix]
@@ -46,7 +45,8 @@ import prefix from "loglevel-plugin-prefix";
46
45
  import { compilePacks, cleanPacks, unpackPacks } from "../engine/compendiums.mjs";
47
46
  import { compilesFoundryDocuments, publishesContentPages } from "../content-config.mjs";
48
47
  import { loadPackConfig, packConfigPath } from "../engine/pack-config.mjs";
49
- import { buildPdf } from "../engine/pdf-build.mjs";
48
+ import { buildPdf, listBookAssets } from "../engine/pdf-build.mjs";
49
+ import { summaryLines } from "../engine/pdf-coverage.mjs";
50
50
  import {
51
51
  fetchAllCatalogs,
52
52
  fetchCatalogFromPath,
@@ -79,11 +79,7 @@ import {
79
79
  systemBlocksFor,
80
80
  } from "../engine/frontmatter-lint.mjs";
81
81
  import { loadContentFormat } from "../engine/content-format.mjs";
82
- import {
83
- checkDeclaredFields,
84
- checkSchemaTargets,
85
- measureCorpus,
86
- } from "../engine/content-format-check.mjs";
82
+ import { checkDeclaredFields, checkSchemaTargets } from "../engine/content-format-check.mjs";
87
83
  import {
88
84
  compareFields,
89
85
  resolveSchemaArtifact,
@@ -106,14 +102,7 @@ import { NOTE_VOCABULARY } from "../engine/note-vocabulary.mjs";
106
102
  import { checkFormatting, checkPrettierConventions, lintMarkdown } from "../engine/prose-lint.mjs";
107
103
  import { lintProse, scoreProseTree } from "../engine/readability-lint.mjs";
108
104
  import { loadPackageBuildConfig } from "../config.mjs";
109
- import {
110
- authoredFrontmatter,
111
- emitContentIndex,
112
- indexRecordsFor,
113
- isAssetRecord,
114
- isNoteRecord,
115
- noteFile,
116
- } from "../engine/content-index.mjs";
105
+ import { emitContentIndex, indexRecordsFor, isAssetRecord } from "../engine/content-index.mjs";
117
106
  import {
118
107
  buildSite,
119
108
  gatesFailed,
@@ -123,11 +112,7 @@ import { auditLinks, buildLinkIndex, walkReachability } from "../engine/content-
123
112
  import { prepareTreeSqlTables } from "../engine/sql-tables.mjs";
124
113
  // The one place a link finding is worded, shared with both builds.
125
114
  import { linkFindingMessage } from "../engine/wikilink-syntax.mjs";
126
- import {
127
- emitDiagnostic,
128
- positionInFrontmatter,
129
- positionOfLiteral,
130
- } from "../engine/diagnostics.mjs";
115
+ import { emitDiagnostic, positionOfLiteral } from "../engine/diagnostics.mjs";
131
116
  import { reportFindings } from "./report.mjs";
132
117
  import {
133
118
  readItemAddresses,
@@ -199,8 +184,8 @@ function reportFailure(err) {
199
184
  /**
200
185
  * A note's source text, or `""` when it cannot be read.
201
186
  *
202
- * Used only to turn a link finding into a position. A path that no longer
203
- * resolves — a page a build generated, a tree walked from somewhere else —
187
+ * Used only to turn a link finding into a position. A path that does not
188
+ * resolve — a page a build generated, a tree walked from somewhere else —
204
189
  * yields `""`, and {@link positionOfLiteral} then reports nothing, so the
205
190
  * diagnostic drops the line and column rather than guessing them.
206
191
  *
@@ -637,7 +622,6 @@ function docsCommand() {
637
622
  * naming system's published `schema.json`. A failure means the specification
638
623
  * and the system disagree, which is a defect in one of the two.
639
624
  * - `fields` compares declared field mappings with compiler declarations.
640
- * - `notes` measures authored notes against the declared vocabulary.
641
625
  *
642
626
  * The shipped contract is `engine/content-format.yaml`; guide layout does not
643
627
  * participate in validation.
@@ -648,12 +632,11 @@ function docsCommand() {
648
632
  function contentFormatCommand() {
649
633
  return {
650
634
  command: "content-format <action>",
651
- describe: "Check the content format specification against schemas and notes",
635
+ describe: "Check the content format specification against schemas and field declarations",
652
636
  builder: (yargs) =>
653
637
  yargs
654
638
  .command(contentFormatSchemaCommand())
655
639
  .command(contentFormatFieldsCommand())
656
- .command(contentFormatNotesCommand())
657
640
  .demandCommand(1, "Name an action.")
658
641
  .strict(),
659
642
  handler: () => {},
@@ -865,95 +848,6 @@ function contentFormatFieldsCommand() {
865
848
  };
866
849
  }
867
850
 
868
- /**
869
- * `content-format notes` — a content tree, against the declared vocabulary.
870
- *
871
- * **A report, not a gate.** Every authored note predates the format, so a
872
- * failing check would be red in every repository from the day it lands and
873
- * would stay red for the length of the migration — which is a check nobody
874
- * can act on. `--strict` raises the findings to errors, turned on slice by
875
- * slice as each class of finding reaches zero.
876
- *
877
- * @returns {object} The yargs command module.
878
- */
879
- function contentFormatNotesCommand() {
880
- return {
881
- command: "notes [root]",
882
- describe: "Measure a content tree against the vocabulary the format declares (a report)",
883
- builder: (yargs) => {
884
- yargs.positional("root", {
885
- describe: "Content tree to measure. Defaults to the configured contentBase.",
886
- type: "string",
887
- });
888
- yargs.option("spec", {
889
- describe:
890
- "The format contract to read. Defaults to the engine/content-format.yaml this package ships.",
891
- type: "string",
892
- });
893
- yargs.option("strict", {
894
- describe:
895
- "Fail on the findings instead of reporting them. Turned on per slice, as each class reaches zero.",
896
- type: "boolean",
897
- default: false,
898
- });
899
- },
900
- handler: (argv) => {
901
- try {
902
- const config = loadPackConfig();
903
- const root = argv.root ?? config.paths.content;
904
- const format = specFrom(argv);
905
-
906
- // The corpus from the index, like every other check.
907
- // A report measuring the tree against the declared vocabulary
908
- // has to be looking at the same tree the compile will, or its
909
- // counts describe a corpus nobody builds.
910
- const corpusProblems = [];
911
- const records = indexRecordsFor({
912
- contentBase: root,
913
- config,
914
- skipDirectories: config.skipDirectories,
915
- problems: corpusProblems,
916
- });
917
- for (const problem of corpusProblems) emitDiagnostic(problem);
918
- if (corpusProblems.length) process.exitCode = 1;
919
-
920
- const notes = [];
921
- for (const record of records) {
922
- // A documentation journal is a document this tree emits,
923
- // not a note in it, and has no authored frontmatter to
924
- // measure.
925
- if (!isNoteRecord(record) || typeof record.type !== "string") continue;
926
- const absPath = noteFile(root, record);
927
- notes.push({
928
- file: absPath,
929
- // What the author wrote, never the keys the index
930
- // derived — this measures a note against a vocabulary,
931
- // and `address:` is in no vocabulary.
932
- fm: authoredFrontmatter(record),
933
- raw: fs.readFileSync(absPath, "utf8"),
934
- });
935
- }
936
-
937
- const { findings, byClass } = measureCorpus(notes, format, {
938
- strict: argv.strict,
939
- });
940
- for (const finding of findings) emitDiagnostic(finding);
941
-
942
- const counts = Object.entries(byClass).sort(([a], [b]) => (a < b ? -1 : 1));
943
- for (const [cls, count] of counts) log.info(`${cls}: ${count}`);
944
- log.info(
945
- `${findings.length} finding(s) across ${notes.length} note(s) ` +
946
- `measured against ${format.file}.`,
947
- );
948
- if (argv.strict && findings.length) process.exitCode = 1;
949
- } catch (err) {
950
- reportFailure(err);
951
- process.exitCode = 1;
952
- }
953
- },
954
- };
955
- }
956
-
957
851
  /**
958
852
  * `package-build lint` — check a content tree's addresses.
959
853
  *
@@ -999,11 +893,13 @@ function lintCommand() {
999
893
  // not take every other finding in the tree with it, and the
1000
894
  // reader needs a line to open, not a stack.
1001
895
  const corpusProblems = [];
896
+ const parsed = new Map();
1002
897
  const records = indexRecordsFor({
1003
898
  contentBase: root,
1004
899
  config,
1005
900
  skipDirectories: config.skipDirectories,
1006
901
  problems: corpusProblems,
902
+ parsed,
1007
903
  });
1008
904
  for (const problem of corpusProblems) emitDiagnostic(problem);
1009
905
  // An error whatever the command's own strictness: the note is
@@ -1029,6 +925,7 @@ function lintCommand() {
1029
925
  sqlTables: await prepareTreeSqlTables(root, {
1030
926
  config,
1031
927
  records,
928
+ parsed,
1032
929
  skipDirectories: config.skipDirectories,
1033
930
  }),
1034
931
  });
@@ -1142,9 +1039,8 @@ function lintCommand() {
1142
1039
  builders: fieldSpecs,
1143
1040
  artifact: schema.artifact,
1144
1041
  // A schema is keyed by document subtype and a field
1145
- // declaration by note type. Those were one string until
1146
- // Three are renamed, and joining them by name
1147
- // after that would drop `armorgear`'s findings without
1042
+ // declaration by note type, and the two can differ, so
1043
+ // joining them by name would drop findings without
1148
1044
  // saying so — the seam exists for exactly this.
1149
1045
  subtypeOf: (type) => schemaSubtypeOf(config.stats?.systemId, type),
1150
1046
  });
@@ -1265,13 +1161,8 @@ function lintCommand() {
1265
1161
  ...generatedArt,
1266
1162
  ...artSizes,
1267
1163
  ];
1268
- // Only an **error** fails the run. Every finding was an error
1269
- // by then, so this changes nothing on its own —
1270
- // but a field retired in favour of another is reported while
1271
- // both spellings still compile, and failing a build over a note
1272
- // that produces the correct document would red a tree that has
1273
- // done nothing wrong. `reportFindings` already draws exactly
1274
- // that line, so the rule is the shared one rather than a second
1164
+ // Only an **error** fails the run. `reportFindings` draws that
1165
+ // line, so the rule is the shared one rather than a second
1275
1166
  // copy here. Each finding names its own file.
1276
1167
  const errors = reportFindings(findings, {});
1277
1168
  if (errors) {
@@ -1377,8 +1268,8 @@ function formatCommand() {
1377
1268
  );
1378
1269
  // `--write` collects findings too — a file Prettier cannot
1379
1270
  // parse, or one that will not format to a fixpoint — and
1380
- // used to discard them, so a run that had left files
1381
- // unformatted still reported success and exited 0.
1271
+ // a run that leaves files unformatted must not report
1272
+ // success.
1382
1273
  for (const finding of findings) emitDiagnostic(finding);
1383
1274
  if (findings.length) {
1384
1275
  log.error(
@@ -1482,11 +1373,13 @@ function linksCommand() {
1482
1373
  // Enumerated once and shared, as in `lint`, and a note
1483
1374
  // it cannot record is reported rather than thrown.
1484
1375
  const corpusProblems = [];
1376
+ const parsed = new Map();
1485
1377
  const records = indexRecordsFor({
1486
1378
  contentBase,
1487
1379
  config,
1488
1380
  ...scope,
1489
1381
  problems: corpusProblems,
1382
+ parsed,
1490
1383
  });
1491
1384
  for (const problem of corpusProblems) emitDiagnostic(problem);
1492
1385
  // An error whatever the command's own strictness: the note is
@@ -1500,6 +1393,7 @@ function linksCommand() {
1500
1393
  sqlTables: await prepareTreeSqlTables(contentBase, {
1501
1394
  config,
1502
1395
  records,
1396
+ parsed,
1503
1397
  ...scope,
1504
1398
  }),
1505
1399
  });
@@ -1690,9 +1584,10 @@ function contentIndexCommand() {
1690
1584
  }
1691
1585
 
1692
1586
  /**
1693
- * `package-build pdf` — build the book the content tree publishes as.
1587
+ * `package-build pdf` — build the books the content tree publishes as.
1694
1588
  *
1695
- * The third surface, beside `package compile` and `site`. It takes the same
1589
+ * The third surface, beside `package compile` and `site`. It builds every book
1590
+ * `pdf.books` declares, or the one `--book <id>` names, and takes the same
1696
1591
  * `--out` override the site command does, and reports what it found in the same
1697
1592
  * `file:line:column: severity: message` shape every other pass here uses — so
1698
1593
  * one editor, one CI annotator and one `grep` read all three.
@@ -1707,17 +1602,27 @@ function contentIndexCommand() {
1707
1602
  function pdfCommand() {
1708
1603
  return {
1709
1604
  command: "pdf",
1710
- describe: "Build the content tree as a PDF",
1605
+ describe: "Build the content tree as one PDF per declared book",
1711
1606
  builder: (yargs) => {
1712
1607
  yargs
1608
+ .option("book", {
1609
+ describe: "Build only the book with this `id`. Every declared book by default.",
1610
+ type: "string",
1611
+ })
1713
1612
  .option("out", {
1714
- describe: "Write the book here instead of the configured `pdf.out`.",
1613
+ describe: "Write the books here instead of the configured `pdf.out`.",
1715
1614
  type: "string",
1716
1615
  })
1717
1616
  .option("book-version", {
1718
1617
  describe: "Stamp this version on the title page and in the file name.",
1719
1618
  type: "string",
1720
1619
  })
1620
+ .option("list-assets", {
1621
+ describe:
1622
+ "Print each book's output path as JSON, in configuration order, and build nothing.",
1623
+ type: "boolean",
1624
+ default: false,
1625
+ })
1721
1626
  // Declared positively so yargs derives `--no-compile` from it;
1722
1627
  // an option literally named `no-compile` is rejected as unknown
1723
1628
  // when it is actually passed.
@@ -1729,7 +1634,16 @@ function pdfCommand() {
1729
1634
  },
1730
1635
  handler: async (argv) => {
1731
1636
  try {
1637
+ if (argv.listAssets) {
1638
+ const assets = listBookAssets({
1639
+ ...(argv.out ? { out: argv.out } : {}),
1640
+ ...(argv.bookVersion ? { version: String(argv.bookVersion) } : {}),
1641
+ });
1642
+ console.log(JSON.stringify(assets, null, 2));
1643
+ return;
1644
+ }
1732
1645
  const result = await buildPdf({
1646
+ ...(argv.book !== undefined ? { book: String(argv.book) } : {}),
1733
1647
  ...(argv.out ? { out: argv.out } : {}),
1734
1648
  ...(argv.bookVersion ? { version: String(argv.bookVersion) } : {}),
1735
1649
  compile: argv.compile !== false,
@@ -1746,28 +1660,30 @@ function pdfCommand() {
1746
1660
  log.info(`${report.file}: ${report.message}`);
1747
1661
  }
1748
1662
 
1749
- if (!result.built) {
1750
- // A reason is a deliberate no-op — the fence, an absent
1751
- // block, an absent tree. No reason means the findings above
1752
- // say what went wrong, and those decide the exit code.
1753
- if (result.reason) {
1754
- log.info(`No book built: ${result.reason}`);
1755
- return;
1756
- }
1757
- process.exitCode = 1;
1663
+ // A reason is a deliberate no-op — the fence, an absent block,
1664
+ // an absent tree. No reason means the findings above say what
1665
+ // went wrong, and those decide the exit code.
1666
+ if (result.reason) {
1667
+ log.info(`No book built: ${result.reason}`);
1758
1668
  return;
1759
1669
  }
1760
1670
 
1761
- const { stats } = result;
1762
- log.info(
1763
- `${stats.notes} entr(ies) and ${stats.prose} prose file(s) across ` +
1764
- `${stats.sections} section(s)` +
1765
- (stats.repeated ?
1766
- `, ${stats.repeated} of them printed more than once`
1767
- : ""),
1768
- );
1769
- log.info(`Typst source: ${result.typ}`);
1770
- if (result.pdf) log.info(`Book: ${result.pdf}`);
1671
+ for (const book of result.books) {
1672
+ const { stats } = book;
1673
+ log.info(
1674
+ `${book.id}: ${stats.notes} entr(ies) and ${stats.prose} prose ` +
1675
+ `file(s) across ${stats.sections} section(s)` +
1676
+ (stats.repeated ?
1677
+ `, ${stats.repeated} of them printed more than once`
1678
+ : ""),
1679
+ );
1680
+ log.info(`${book.id}: Typst source: ${book.typ}`);
1681
+ if (book.pdf) log.info(`${book.id}: Book: ${book.pdf}`);
1682
+ }
1683
+ if (result.summary) {
1684
+ for (const line of summaryLines(result.summary)) log.info(line);
1685
+ }
1686
+ if (!result.built) process.exitCode = 1;
1771
1687
  if (errors) process.exitCode = 1;
1772
1688
  } catch (err) {
1773
1689
  reportFailure(err);
@@ -2178,11 +2094,13 @@ function reachabilityCommand() {
2178
2094
  const dir = String(argv.dir).replace(/\/+$/, "");
2179
2095
  const scope = { skipDirectories: config.skipDirectories };
2180
2096
  const corpusProblems = [];
2097
+ const parsed = new Map();
2181
2098
  const records = indexRecordsFor({
2182
2099
  contentBase,
2183
2100
  config,
2184
2101
  ...scope,
2185
2102
  problems: corpusProblems,
2103
+ parsed,
2186
2104
  });
2187
2105
  for (const problem of corpusProblems) emitDiagnostic(problem);
2188
2106
  // An error whatever the command's own strictness: the note is
@@ -2196,6 +2114,7 @@ function reachabilityCommand() {
2196
2114
  sqlTables: await prepareTreeSqlTables(contentBase, {
2197
2115
  config,
2198
2116
  records,
2117
+ parsed,
2199
2118
  ...scope,
2200
2119
  }),
2201
2120
  });
@@ -87,7 +87,7 @@ import { hideBin } from "yargs/helpers";
87
87
 
88
88
  import { loadPackageBuildConfig } from "../config.mjs";
89
89
  import { checkForeignAssetBindings } from "../engine/asset-bindings.mjs";
90
- import { formatDiagnostic } from "../engine/diagnostics.mjs";
90
+ import { emitDiagnostic, formatDiagnostic } from "../engine/diagnostics.mjs";
91
91
  import { writeSiteRoot } from "../engine/site-root.mjs";
92
92
  import { DEPLOY_ROOT } from "../engine/site-config.mjs";
93
93
  import { compilesFoundryDocuments } from "../content-config.mjs";
@@ -117,8 +117,15 @@ import { findHardcodedText, findTemplateSyntaxErrors } from "../templates.mjs";
117
117
  import { checkBundleLoading } from "../bundle.mjs";
118
118
  import { packRelease } from "../release.mjs";
119
119
  import { writeManifest } from "../manifest.mjs";
120
- import { deployStage } from "../deploy.mjs";
121
- import { CONTAINER_ACTIONS, containerAction } from "../container.mjs";
120
+ import { deployStage, resolveStage } from "../deploy.mjs";
121
+ import {
122
+ CONTAINER_ACTIONS,
123
+ containerAction,
124
+ containerRemovalCommand,
125
+ containerTreeRemover,
126
+ dataEnvVar,
127
+ resolveContainer,
128
+ } from "../container.mjs";
122
129
  import { E2E_MODES, e2eFast, e2eRun, e2eSweep, seedTestWorld } from "../e2e.mjs";
123
130
  import { reportFindings } from "./report.mjs";
124
131
  import { formatGenerated } from "../engine/format-generated.mjs";
@@ -214,8 +221,16 @@ function initCommand() {
214
221
  const root = path.resolve(args.directory ?? process.cwd());
215
222
  if (args.check) {
216
223
  const findings = checkProject(root);
217
- for (const { file, message } of findings) {
218
- console.error(`${path.join(root, file)}: error: ${message}`);
224
+ for (const { file, line, column, message } of findings) {
225
+ console.error(
226
+ formatDiagnostic({
227
+ file: path.join(root, file),
228
+ line,
229
+ column,
230
+ severity: "error",
231
+ message,
232
+ }),
233
+ );
219
234
  }
220
235
  if (findings.length) process.exitCode = 1;
221
236
  else console.log(`package-build: ${root} is ready to build.`);
@@ -1440,11 +1455,19 @@ function releaseCommand() {
1440
1455
  // book that would not set is a problem to fix rather than a reason
1441
1456
  // to have published nothing.
1442
1457
  for (const finding of result.pdfFindings ?? []) {
1443
- console.error(`${finding.file ?? "book"}: ${finding.severity}: ${finding.message}`);
1458
+ console.error(
1459
+ formatDiagnostic({
1460
+ ...finding,
1461
+ ...(finding.file ?
1462
+ { file: path.relative(process.cwd(), finding.file) }
1463
+ : {}),
1464
+ }),
1465
+ );
1444
1466
  }
1445
- if (result.pdf) {
1446
- console.log(`📕 Book: ${path.relative(config.rootDir, result.pdf)}`);
1447
- } else if (result.pdfSkipped) {
1467
+ for (const pdf of result.pdfs ?? []) {
1468
+ console.log(`📕 Book: ${path.relative(config.rootDir, pdf)}`);
1469
+ }
1470
+ if (!result.pdfs?.length && result.pdfSkipped) {
1448
1471
  console.log(` No book: ${result.pdfSkipped}`);
1449
1472
  }
1450
1473
  }),
@@ -1475,15 +1498,38 @@ function deployCommand() {
1475
1498
 
1476
1499
  await loadEnvironment(config);
1477
1500
 
1478
- const { stage } = await deployStage({
1501
+ // The container is resolved only once the host user is refused a
1502
+ // removal, so a deploy that meets no refusal needs no container.
1503
+ const containerFor = () =>
1504
+ resolveContainer({ stage: resolveStage(args.stage), config });
1505
+ const { stage, leftovers, remote } = await deployStage({
1479
1506
  stage: args.stage,
1480
1507
  source: path.join(config.rootDir, config.stageDir),
1481
1508
  packageKind: config.packageKind,
1482
1509
  packageId: config.packageId,
1483
1510
  prefix: config.envPrefix,
1484
1511
  log: (message) => console.log(message),
1512
+ removeTree: (tree, where) =>
1513
+ containerTreeRemover({ container: containerFor() })(tree, where),
1485
1514
  });
1486
1515
  console.log(`Deployed stage '${stage}' successfully.`);
1516
+ for (const leftover of leftovers) {
1517
+ const remedy =
1518
+ remote ?
1519
+ "remove it on the host as the user that owns its files"
1520
+ : `remove it with \`${containerRemovalCommand({
1521
+ image: containerFor().image,
1522
+ dataRoot: process.env[dataEnvVar(stage)].trim(),
1523
+ tree: leftover.path,
1524
+ })}\``;
1525
+ emitDiagnostic({
1526
+ file: leftover.path,
1527
+ severity: "warning",
1528
+ message:
1529
+ `the new package is installed, but its previous tree could ` +
1530
+ `not be removed (${leftover.reason}); ${remedy}`,
1531
+ });
1532
+ }
1487
1533
  }),
1488
1534
  };
1489
1535
  }