@heroiclands/package-build 23.4.1 → 23.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.md +29 -25
  3. package/assets/images/provenance.yaml +1 -0
  4. package/bin/package-build.mjs +259 -101
  5. package/ci/prettier.mjs +73 -0
  6. package/config.mjs +10 -0
  7. package/container.mjs +232 -1
  8. package/content-config.mjs +5 -1
  9. package/docs/api.md +100 -52
  10. package/docs/authoring/assets.md +16 -1
  11. package/docs/commands.md +154 -51
  12. package/docs/configuration.md +39 -2
  13. package/docs/content-format.md +30 -6
  14. package/docs/diagnostics.md +1 -2
  15. package/docs/guides/e2e.md +38 -0
  16. package/docs/guides/icons.md +5 -0
  17. package/docs/guides/module-project.md +1 -1
  18. package/docs/guides/release-and-deploy.md +36 -1
  19. package/docs/project-setup.md +16 -59
  20. package/docs/reference/format-details.md +180 -34
  21. package/docs/reference/note-types.md +69 -69
  22. package/engine/asset-index.mjs +63 -22
  23. package/engine/asset-references.mjs +355 -0
  24. package/engine/asset-shipping.mjs +363 -0
  25. package/engine/asset-types.mjs +70 -0
  26. package/engine/base-compiler.mjs +24 -1
  27. package/engine/cdn-publish.mjs +643 -0
  28. package/engine/index.mjs +6 -0
  29. package/engine/note-vocabulary.mjs +4 -1
  30. package/engine/pathnames.mjs +21 -10
  31. package/engine/project-init.mjs +4 -29
  32. package/engine/rank-ladder.mjs +40 -16
  33. package/engine/scenes.mjs +1 -1
  34. package/engine/standing-terms.mjs +3 -1
  35. package/package.json +5 -6
  36. package/packagebuild-metadata.jsonl +13 -13
  37. package/release.mjs +69 -7
  38. package/sohl/actors.mjs +12 -4
  39. package/sohl/default-item-art.mjs +23 -19
  40. package/types/config.d.mts +7 -0
  41. package/types/container.d.mts +137 -1
  42. package/types/engine/asset-index.d.mts +3 -2
  43. package/types/engine/asset-references.d.mts +136 -0
  44. package/types/engine/asset-shipping.d.mts +240 -0
  45. package/types/engine/asset-types.d.mts +36 -0
  46. package/types/engine/base-compiler.d.mts +7 -1
  47. package/types/engine/cdn-publish.d.mts +312 -0
  48. package/types/engine/index.d.mts +2 -0
  49. package/types/engine/pathnames.d.mts +2 -1
  50. package/types/engine/rank-ladder.d.mts +25 -6
  51. package/types/release.d.mts +5 -3
  52. package/types/sohl/default-item-art.d.mts +2 -1
  53. package/labels.mjs +0 -191
  54. package/types/labels.d.mts +0 -40
@@ -26,16 +26,31 @@ The embed's label is alternative text. `![[icon-harbor|]]` is decorative. `size=
26
26
 
27
27
  ## Ownership and source records
28
28
 
29
- Record provenance for the assets you ship. Put `provenance.yaml` in an asset directory to describe files there and below, or put a YAML sidecar beside one asset to give that file its own record. A sidecar takes precedence over a directory record for that asset. An asset with no record still enters the index with blank provenance fields. Keep attribution with the authored asset so movement through the tree does not lose its source or license. See the [asset record reference](../reference/format-details.md#the-asset-record) for the accepted keys, inheritance, and examples.
29
+ Every asset is covered by a provenance record. Put `provenance.yaml` in an asset directory to describe files there and below, or put a YAML sidecar named for the whole filename — `anvil.svg.yaml` — beside one asset to give that file its own record. A sidecar replaces the directory record for that asset; otherwise the nearest `provenance.yaml` in the file's directory or one above it, up to the type root, answers. Each file resolves on its own, so a directory may hold files its `provenance.yaml` covers beside files a sidecar answers for. A file no record covers is an error at the file. Keep attribution with the authored asset so movement through the tree does not lose its source or license. See the [asset record reference](../reference/format-details.md#the-asset-record) for the accepted keys, inheritance, and examples.
30
+
31
+ Every record states `attribution`, `license` and `ai` — whether a machine made the file, as the YAML boolean `true` or `false`. A record that leaves `ai` out is an error at the record; it is never read as `false`.
32
+
33
+ ```yaml
34
+ attribution: Tom Rodriguez
35
+ license: CC-BY-SA-4.0
36
+ ai: false
37
+ ```
30
38
 
31
39
  A directory of portraits also states what the pictures in it are for:
32
40
 
33
41
  ```yaml
34
42
  attribution: Tom Rodriguez
35
43
  license: CC-BY-SA-4.0
44
+ ai: false
36
45
  role: portrait
37
46
  ```
38
47
 
48
+ ## What the Foundry package ships
49
+
50
+ The repository keeps every picture; the Foundry package carries a filtered copy, laid out flat. A picture ships when its record says `ai: false` and its licence is one of `LicenseRef-PublicDomain`, `CC0-1.0`, `CC0`, `CC-BY-3.0`, `CC-BY-4.0`, `CC-BY-SA-4.0` or `LicenseRef-NounProject-Pro`, together with any licence the package adds under `packageBuild.shippingLicenses`. A shipped picture sits at `icons/<basename>` or `images/<basename>` at the package root, whatever directories it is kept in here, and the package's asset set, `asset-sets/default.json`, lists what it ships.
51
+
52
+ A note may still name a picture the package does not ship. Its compiled document draws the package's stand-in instead — `icons/missing.<ext>` or `images/missing.<ext>`, which you provide as an ordinary human-made picture licensed to ship, addressed `<package>-none-icon-missing` and `<package>-none-image-missing` — and keeps the real picture's address, so a world that holds the real file can point back at it. A package that leaves out a picture a note names, and ships no stand-in of that type, fails the build at the note. See [what a package ships](../reference/format-details.md#what-a-package-ships).
53
+
39
54
  **A hero image is 1792×768.** `data.banner` fills one fixed strip wherever it is drawn, so every banner is cut to that size and a picture of any other size is a finding naming both sizes — resize the picture to match. That is the one size any art field requires.
40
55
 
41
56
  `data.banner` reads in one of three states:
package/docs/commands.md CHANGED
@@ -58,7 +58,7 @@ The command entries below list only their own options.
58
58
  the walk up from the working directory the CLI otherwise does — the only
59
59
  environment variable the toolchain itself reads for configuration
60
60
  resolution. `deploy`, `container` and `e2e` additionally read `FOUNDRYVTT_*`,
61
- documented where each applies.
61
+ and `cdn publish` reads `CDN_*`, documented where each applies.
62
62
 
63
63
  An option name recurring across commands is not a recurring meaning —
64
64
  `--out`, `--check`, `--from` and `--strict` each carry their own default and
@@ -300,6 +300,16 @@ names a module in `packageBuild.assetTransform`, exporting
300
300
  anything else replaces its content. Reads every `from` path; writes into
301
301
  `packageBuild.stageDir`.
302
302
 
303
+ Then stages the package's pictures itself. Every file under `assets/icons/`
304
+ and `assets/images/` whose provenance says `ai: false` and whose licence is on
305
+ the shipping list (the default list plus `packageBuild.shippingLicenses`) is copied flat to `icons/<basename>` or `images/<basename>`
306
+ at the package root, through the same transform, and the package's asset set,
307
+ `asset-sets/default.json`, lists what was staged. Everything else stays in the repository. See
308
+ [what a package ships](reference/format-details.md#what-a-package-ships).
309
+ A `packageBuild.assets` row that copies a picture root, a directory inside
310
+ one, or a directory holding one is refused, because it would ship every file
311
+ unfiltered.
312
+
303
313
  **OPTIONS**
304
314
 
305
315
  None.
@@ -307,15 +317,20 @@ None.
307
317
  **EXIT STATUS**
308
318
 
309
319
  1 if `packageBuild.assetTransform` is declared but cannot be loaded, or
310
- exports no `transform` function. 1 on any other thrown error. Otherwise 0 —
311
- including when `packageBuild.assets` is empty, which logs and stages nothing
312
- rather than failing.
320
+ exports no `transform` function. 1 when a row copies a picture root, and 1 on
321
+ any asset finding — a file no provenance record covers, a record missing
322
+ `attribution`, `license` or `ai`, two files claiming one address — with
323
+ nothing staged. 1 on any other thrown error. Otherwise 0.
313
324
 
314
325
  **EXAMPLES**
315
326
 
316
327
  ```
317
328
  $ package-build assets
318
- ✅ Static assets staged (1 entries, 1 files).
329
+ Pictures: 4 staged, 2 left out by provenance.
330
+ ✅ Static assets staged (2 entries, 9 files).
331
+
332
+ $ package-build assets
333
+ package-build.config.yaml:14:17: error: `packageBuild.assets[6]` copies `assets/images`, which reaches assets/images/ — the stage copies the icon and image roots itself, flat and filtered by each file's provenance, so a row copying them would ship every file unfiltered. Remove the row
319
334
  ```
320
335
 
321
336
  **SEE ALSO**
@@ -774,50 +789,6 @@ package-build: 1 template(s) fully localized and compiling.
774
789
  `package-build lang check`, `package-build lang coverage`,
775
790
  [Diagnostics](diagnostics.md), [Configuration](configuration.md).
776
791
 
777
- ### `package-build labels check`
778
-
779
- **NAME**
780
-
781
- Compare the machine label registry against the documented reference table
782
- (`check` is the only action).
783
-
784
- **SYNOPSIS**
785
-
786
- ```
787
- package-build labels check [--registry <path>] [--doc <path>]
788
- ```
789
-
790
- **DESCRIPTION**
791
-
792
- Compares the machine label registry synced to GitHub against the documented
793
- reference table, and reports where they disagree — a label the registry has
794
- that the table does not, or the reverse. Neither derives from the other, so
795
- nothing else notices when they drift. Reads both files named below; writes
796
- nothing.
797
-
798
- **OPTIONS**
799
-
800
- | Option | Type | Default | Description |
801
- | ------------ | ------ | ---------------------------- | -------------------------------------------------------------- |
802
- | `--registry` | string | `.github/labels.yml` | The machine registry synced to GitHub. |
803
- | `--doc` | string | `.github/ISSUE_REPORTING.md` | The documented reference whose §3 table lists the same labels. |
804
-
805
- **EXIT STATUS**
806
-
807
- 1 if either file does not exist. 1 if the registry and the table disagree on
808
- any label. Otherwise 0.
809
-
810
- **EXAMPLES**
811
-
812
- ```
813
- $ package-build labels check --registry .github/labels.yml --doc .github/ISSUE_REPORTING.md
814
- package-build: registry and §3 agree (11 labels).
815
- ```
816
-
817
- **SEE ALSO**
818
-
819
- None.
820
-
821
792
  ### `package-build bump [packages..]`
822
793
 
823
794
  **NAME**
@@ -1138,7 +1109,9 @@ states it a second time. It also publishes the content index the manifest
1138
1109
  advertises (`flags.metadataUrl`) and, when the stage carries one,
1139
1110
  `schema.json` — a repository that names `build/schema.json` in
1140
1111
  `packageBuild.assets` gets it released beside the archive; one that does not
1141
- gets none. When the content tree holds notes beyond its homepage, it
1112
+ gets none. When the stage carries `asset-sets/default.json`, it writes
1113
+ `icons.zip` and `images.zip` too, one per picture root the stage holds, each
1114
+ with that set. When the content tree holds notes beyond its homepage, it
1142
1115
  also builds the content-tree book (see `package-build pdf`) and reports it
1143
1116
  alongside the archive; `--no-pdf` skips that step for a release that has a
1144
1117
  tree but does not want the book this time. A book that fails to build is
@@ -1246,6 +1219,134 @@ Deployed stage 'test' successfully.
1246
1219
  `package-build container <stage> <action>`, `package-build e2e <action>`,
1247
1220
  [Configuration](configuration.md).
1248
1221
 
1222
+ ### `package-build cdn publish`
1223
+
1224
+ **NAME**
1225
+
1226
+ Upload every picture the package has to the CDN, and publish the art the
1227
+ package leaves out as a named asset set.
1228
+
1229
+ **SYNOPSIS**
1230
+
1231
+ ```
1232
+ package-build cdn publish --set <file> --name <display name> [--dry-run]
1233
+ ```
1234
+
1235
+ **DESCRIPTION**
1236
+
1237
+ The one writer to `<site.assets>/<contentPackage>/`. It is run by hand, or by
1238
+ a manually dispatched workflow; no build or release runs it.
1239
+
1240
+ Every icon and image under the package's picture roots is uploaded flat, as
1241
+ `icons/<basename>` or `images/<basename>` — shipped and left-out pictures
1242
+ alike, because the website draws every one of them from the same address the
1243
+ site build gives it. `packageBuild.assetTransform` runs on each file exactly
1244
+ as `package-build assets` runs it, so a picture on the CDN holds the bytes the
1245
+ staged package holds. SVG is served as `image/svg+xml`.
1246
+
1247
+ The bucket is served at `site.assets`, so an object's key is its path below
1248
+ that address: `thalorna/images/thorn.webp` is
1249
+ `https://cdn.heroiclands.org/thalorna/images/thorn.webp`.
1250
+
1251
+ `--set` names the set file, written at `<site.assets>/<contentPackage>/<file>`.
1252
+ It lists only the pictures the package leaves out — an address a set does not
1253
+ list resolves to the shipped copy — chosen by the same rule the stage applies,
1254
+ `packageBuild.shippingLicenses` included. Its shape is the shipped set's, with
1255
+ each `source` the file's absolute CDN address:
1256
+
1257
+ ```json
1258
+ {
1259
+ "name": "Thalorna — illustrated",
1260
+ "package": "thalorna",
1261
+ "assets": [
1262
+ {
1263
+ "address": "thalorna-none-image-thorn",
1264
+ "source": "https://cdn.heroiclands.org/thalorna/images/thorn.webp",
1265
+ "target": "images/thorn.webp",
1266
+ "size": 48213
1267
+ }
1268
+ ]
1269
+ }
1270
+ ```
1271
+
1272
+ Several sets may sit side by side under one package, each with its own file
1273
+ name. `default.json` is refused: it is the shipped set's name.
1274
+
1275
+ A picture whose object already exists at the same size is skipped, and the set
1276
+ file is skipped when its bytes are unchanged, so a second run uploads nothing.
1277
+ A picture that fails to upload is reported and the rest carry on; the set file
1278
+ is then withheld, since it would list a file the CDN does not hold.
1279
+
1280
+ **Refusals and reports.** A file under `icons/` or `images/` whose name is no
1281
+ address — a basename that is not a shortcode, or two files claiming one — and
1282
+ a provenance record that cannot be read stop the run before anything is
1283
+ uploaded, each reported against its file. A note naming a picture the package
1284
+ does not have, and a note naming left-out art that is not on the CDN when the
1285
+ run ends, are warnings at the note's line and column: an art slot under
1286
+ `data:`, an embed, or a body image.
1287
+
1288
+ **Credentials.** The store is an S3-compatible bucket — Cloudflare R2 for
1289
+ `cdn.heroiclands.org` — signed with AWS Signature Version 4. Its settings are
1290
+ read from the environment, after `.env.local` and `.env` are loaded:
1291
+
1292
+ | Variable | What it is |
1293
+ | ----------------------- | ------------------------------------------------------------------------- |
1294
+ | `CDN_ENDPOINT` | The S3 endpoint; for R2, `https://<account id>.r2.cloudflarestorage.com`. |
1295
+ | `CDN_BUCKET` | The bucket served at `site.assets`. |
1296
+ | `CDN_ACCESS_KEY_ID` | The access key id of a token that can write the bucket. |
1297
+ | `CDN_SECRET_ACCESS_KEY` | That token's secret. Never printed. |
1298
+
1299
+ Setting some but not all of them is an error naming the missing ones. With
1300
+ none set, `--dry-run` compares against the public CDN at `site.assets`
1301
+ instead, read-only, and an upload is refused.
1302
+
1303
+ A workflow calls the command the same way, passing the four values as named
1304
+ secrets in the step's environment:
1305
+
1306
+ ```yaml
1307
+ - run: npx package-build cdn publish --set alt-art.json --name "Thalorna — illustrated"
1308
+ env:
1309
+ CDN_ENDPOINT: ${{ secrets.CDN_ENDPOINT }}
1310
+ CDN_BUCKET: ${{ secrets.CDN_BUCKET }}
1311
+ CDN_ACCESS_KEY_ID: ${{ secrets.CDN_ACCESS_KEY_ID }}
1312
+ CDN_SECRET_ACCESS_KEY: ${{ secrets.CDN_SECRET_ACCESS_KEY }}
1313
+ ```
1314
+
1315
+ **OPTIONS**
1316
+
1317
+ | Option | Type | Default | Description |
1318
+ | ----------- | ------- | ------- | --------------------------------------------------------------------------- |
1319
+ | `--set` | string | — | Required. The set file's name under the package's CDN root, a `.json` file. |
1320
+ | `--name` | string | — | Required. The set's display name. |
1321
+ | `--dry-run` | boolean | `false` | List what would upload and what is skipped; write nothing. |
1322
+
1323
+ **EXIT STATUS**
1324
+
1325
+ 1 when a picture's file has no address, when a picture fails to upload, when
1326
+ `--set` is `default.json` or a path, when `site.assets` is undeclared, and
1327
+ when an upload has no store configured. Warnings about notes do not change
1328
+ it. Otherwise 0.
1329
+
1330
+ **EXAMPLES**
1331
+
1332
+ ```
1333
+ $ package-build cdn publish --set alt-art.json --name "Thalorna — illustrated" --dry-run
1334
+ Dry run: comparing 582 pictures against https://cdn.heroiclands.org, read-only.
1335
+ would upload thalorna/icons/anvilbdg.svg (969 bytes)
1336
+ …
1337
+ would upload thalorna/alt-art.json (582 assets)
1338
+ Would upload 582, skipped 0, failed 0; https://cdn.heroiclands.org/thalorna/alt-art.json lists 582 left-out pictures (would change).
1339
+
1340
+ $ package-build cdn publish --set default.json --name Shipped
1341
+ package-build: `--set default.json` is refused: `default.json` is the shipped set's name, the fallback every other set is read against. Name the set for its art
1342
+ ```
1343
+
1344
+ **SEE ALSO**
1345
+
1346
+ `package-build assets`, `package-build release`,
1347
+ [Configuration](configuration.md#site),
1348
+ [Release and deploy](guides/release-and-deploy.md#publish-pictures-to-the-cdn).
1349
+
1249
1350
  ### `package-build container <stage> <action>`
1250
1351
 
1251
1352
  **NAME**
@@ -1306,7 +1407,9 @@ exact pin takes the major tag of its pin or of `compatibility.minimum`
1306
1407
  mount), `FOUNDRYVTT_CONTAINER_USER` (passed to `docker run --user`; `0:0`
1307
1408
  under rootless Docker makes the data root's files belong to the host user,
1308
1409
  because the image otherwise runs Foundry as user 1000 and ignores
1309
- `FOUNDRY_UID` and `FOUNDRY_GID`).
1410
+ `FOUNDRY_UID` and `FOUNDRY_GID`), `FOUNDRYVTT_CONTAINER_S3` (`1` or `true`
1411
+ runs an S3-compatible server beside the container; see
1412
+ [an S3 server for the container](guides/e2e.md#an-s3-server-for-the-container)).
1310
1413
 
1311
1414
  **EXIT STATUS**
1312
1415
 
@@ -206,6 +206,9 @@ against — the one reader of it is `resolveImg`
206
206
  `packageKind` and `foundryPackage`.
207
207
 
208
208
  - For a `systems` or `modules` package, `<packageKind>/<foundryPackage>/assets`.
209
+ A picture under `icons/` or `images/` is served flat from
210
+ `<packageKind>/<foundryPackage>/<icons|images>/<basename>` instead, the
211
+ layout the stage gives it; every other file keeps its path under `assets/`.
209
212
  - For a `documentation` package, `null` — Foundry serves no files for a
210
213
  package of this kind, and `documentation/null/assets` would be an address
211
214
  that resolves nowhere. `resolveImg` refuses rather than rooting a path
@@ -901,6 +904,12 @@ resolves every relative asset against it:
901
904
 
902
905
  > ``package-build config: `site.assets` is not declared, and a site build needs one — it is the host every package's imagery is served from, and the theme resolves every relative asset against it.``
903
906
 
907
+ `package-build cdn publish` uploads the package's pictures under the same
908
+ host, at `<site.assets>/<contentPackage>/icons/<basename>` and
909
+ `…/images/<basename>`, which is the address the website gives each one. The
910
+ bucket behind the CDN is served at `site.assets`, so an object's key is its
911
+ path below this value.
912
+
904
913
  The generated Hugo configuration carries the same host as
905
914
  `params.cdnBaseURL`, which the theme resolves a relative asset path against.
906
915
  The two are one value read by two readers: the toolchain emits it into a
@@ -1685,7 +1694,7 @@ validator.
1685
1694
  packageBuild:
1686
1695
  assets:
1687
1696
  - { from: lang, to: lang }
1688
- - { from: assets/icons, to: assets/icons }
1697
+ - { from: assets/audio, to: assets/audio }
1689
1698
  assetTransform: ./utils/svg-theme.mjs
1690
1699
  stageDir: build/stage
1691
1700
  clean:
@@ -1701,6 +1710,7 @@ packageBuild:
1701
1710
  | [`packageBuild.stageDir`](#packagebuildstagedir-and-packagebuildassets) | string | no | `build/stage` |
1702
1711
  | [`packageBuild.assets`](#packagebuildstagedir-and-packagebuildassets) | array | no | `[]` |
1703
1712
  | [`packageBuild.assetTransform`](#packagebuildassettransform-and-packagebuildmanifestflags) | string (path to a module) | no | `null` |
1713
+ | [`packageBuild.shippingLicenses`](#packagebuildshippinglicenses) | array of strings | no | `[]` |
1704
1714
  | [`packageBuild.baseStyles`](#packagebuildbasestyles) | boolean | no | `true` |
1705
1715
  | [`packageBuild.manifest`](#packagebuildmanifest) | object, pass-through | no | `{}` |
1706
1716
  | [`packageBuild.manifestFlags`](#packagebuildassettransform-and-packagebuildmanifestflags) | string (path to a module) | no | `null` |
@@ -1715,7 +1725,7 @@ packageBuild:
1715
1725
  | [`packageBuild.proseLint`](#packagebuildproselint) | object | no | `{age: 21, threshold: 5, minWords: 8, rules: readability}` |
1716
1726
  | [`packageBuild.proseScore`](#packagebuildprosescore) | object | no | `{minWords: 80, bands: {}}` |
1717
1727
 
1718
- > ``package-build config: `packageBuild.<key>` is not a recognised key (expected one of: stageDir, assets, assetTransform, baseStyles, manifest, manifestFlags, schema, clean, lang, deploy, release, bundle, container, e2e, proseLint, proseScore).``
1728
+ > ``package-build config: `packageBuild.<key>` is not a recognised key (expected one of: stageDir, assets, assetTransform, shippingLicenses, baseStyles, manifest, manifestFlags, schema, clean, lang, deploy, release, bundle, container, e2e, proseLint, proseScore).``
1719
1729
 
1720
1730
  ### `packageBuild.proseLint`
1721
1731
 
@@ -1765,6 +1775,15 @@ table says `lang`, not `build/stage/lang`.
1765
1775
  `assets` is the table of staging copies: source path in the repository,
1766
1776
  destination under the staged package root.
1767
1777
 
1778
+ **The picture roots are not in it.** `package-build assets` stages
1779
+ `assets/icons/` and `assets/images/` itself — flat at the package root, and
1780
+ only the files whose provenance allows them to ship (see
1781
+ [what a package ships](reference/format-details.md#what-a-package-ships)). A
1782
+ row whose `from` is a picture root, a directory inside one, or a directory
1783
+ holding one is refused at the row:
1784
+
1785
+ > ``package-build.config.yaml:<line>:<column>: error: `packageBuild.assets[<index>]` copies `<from>`, which reaches assets/images/ — the stage copies the icon and image roots itself, flat and filtered by each file's provenance, so a row copying them would ship every file unfiltered. Remove the row``
1786
+
1768
1787
  | Key | Type | Required |
1769
1788
  | ------------------------------- | -------------------------- | -------- |
1770
1789
  | `packageBuild.assets[].from` | string | yes |
@@ -1801,6 +1820,24 @@ repository has to compute. Neither declares anything, both default to
1801
1820
 
1802
1821
  > ``package-build config: `packageBuild.manifestFlags` must be a non-empty string.``
1803
1822
 
1823
+ ### `packageBuild.shippingLicenses`
1824
+
1825
+ Licences this package adds to the default shipping list. A picture ships in the
1826
+ Foundry package when its provenance says `ai: false` and its `license` is on the
1827
+ default list or on this one; the default list is never replaced, and `ai: true`
1828
+ never ships. Each entry is spelled exactly as provenance spells the licence.
1829
+
1830
+ ```yaml
1831
+ packageBuild:
1832
+ shippingLicenses: [CC-BY-NC-SA-4.0]
1833
+ ```
1834
+
1835
+ > ``package-build config: `packageBuild.shippingLicenses` must be a list of licence identifiers.``
1836
+
1837
+ > ``package-build config: `packageBuild.shippingLicenses` entry <index> must be a non-empty string.``
1838
+
1839
+ > ``package-build config: `packageBuild.shippingLicenses` lists `<licence>` more than once.``
1840
+
1804
1841
  ### `packageBuild.baseStyles`
1805
1842
 
1806
1843
  Whether the package takes the shared base stylesheet. It defaults to `true`,
@@ -53,12 +53,36 @@ explains routing for documents and their prose journals.
53
53
 
54
54
  A note can also produce a system document: an Actor, Item, Scene, or Macro according to its type and system blocks. The `sohl:` and `hm3:` blocks supply game-specific mechanics and overrides; they do not create separate prose. The JournalEntry is shared between systems.
55
55
 
56
- An `affiliation` note declares `data.governance.ranks` with at least one rung at
57
- `level: 1`. Each rung states `level`, `title`, and `description`. A being's
58
- `data.affiliations` entry names a rank on that body's ladder; level 1 is the
59
- ordinary standing when no higher rank applies. A single level-1 rung is enough
60
- when the body expresses its hierarchy through `governance.offices` instead.
61
- Missing ranks, an empty list, or a ladder without level 1 are build errors.
56
+ An `affiliation` note declares `data.governance.ranks` with at least two rungs:
57
+ `level: 0`, those the body excludes, and `level: 1`, its ordinary members. Every
58
+ body that can name its members can name those it shuts out, whether or not it
59
+ ever does, so both are required. Each rung states `level`, `title`, and
60
+ `description`, and the level-0 title is the body's own word for the excluded
61
+ rather than a generic "Excluded". A being's `data.affiliations` entry names a
62
+ rank on that body's ladder; level 1 is the ordinary standing when no higher
63
+ rank applies.
64
+
65
+ Add rungs above level 1 only where the body truly has them, generally no more
66
+ than five. A rung is a **standing**, what a member is; an entry in
67
+ `governance.offices` is a **post**, what a member does. Leadership is normally
68
+ an office held by the top rank rather than a rank above it, and a rank never
69
+ falls when an office is left: a bishop stays a bishop after being Presiding
70
+ Bishop.
71
+
72
+ ```yaml
73
+ data:
74
+ governance:
75
+ ranks:
76
+ - { level: 0, title: Apostate, description: Put out of the communion. }
77
+ - { level: 1, title: Layman, description: A baptised member of the faith. }
78
+ - { level: 2, title: Priest, description: Ordained to serve a parish. }
79
+ - { level: 3, title: Bishop, description: Consecrated to oversee a diocese. }
80
+ offices:
81
+ Presiding Bishop: Chairs the synod of bishops for a term.
82
+ ```
83
+
84
+ Missing ranks, an empty list, or a ladder without level 0 or level 1 are build
85
+ errors, reported at the `ranks` key.
62
86
 
63
87
  | Authored section | SoHL Actor | HM3 Actor |
64
88
  | ---------------- | ------------------- | -------------------- |
@@ -284,7 +284,7 @@ when it reports at least one `error`-severity diagnostic. A `warning` alone
284
284
  never fails a run.** `bin/report.mjs`'s `reportFindings` draws this line once
285
285
  for the commands built from the package's pure rule functions (`package-build
286
286
  lint`, `package-build lang check`, `package-build lang coverage`,
287
- `package-build yaml`, `package-build labels check`): it emits every finding
287
+ `package-build yaml`): it emits every finding
288
288
  and returns only the count whose `severity` is `error`, and the command sets
289
289
  `process.exitCode = 1` exactly when that count is nonzero. Commands whose
290
290
  findings are hand-assembled — `package-build links`, `package-build
@@ -335,7 +335,6 @@ over one would teach people to stop reading the report.
335
335
  | `package-build lang check` | any localization finding is `error`-severity |
336
336
  | `package-build lang coverage` | a referenced key is missing (`error`-severity); an unreferenced key is always advisory and never fails the run |
337
337
  | `package-build yaml [paths..]` | any YAML finding is `error`-severity |
338
- | `package-build labels check` | the label registry and the documented table disagree on any label |
339
338
 
340
339
  Every command also fails on a thrown error unrelated to a
341
340
  diagnostic — a missing configuration, an unreadable file, a network failure —
@@ -159,3 +159,41 @@ major version or moving tag is refused, because the result must name the
159
159
  build actually tested. A passing sweep provides evidence for choosing a
160
160
  new `compatibility.verified` value; a routine run exercises the pinned
161
161
  minimum build.
162
+
163
+ ## An S3 server for the container
164
+
165
+ A suite that exercises S3 storage needs an S3-compatible server the browser and
166
+ Foundry both reach. Setting `FOUNDRYVTT_CONTAINER_S3=1` when a container is
167
+ created (`start`, `recreate`; through `e2e:fast --recreate` as well) provides one.
168
+ Without it nothing changes: no network, no server, no extra environment.
169
+
170
+ With it, before Foundry starts:
171
+
172
+ - A user-defined network named `<container>-net` is created, and the Foundry
173
+ container is created on it.
174
+ - A SeaweedFS server (`chrislusf/seaweedfs:3.80`) named `<container>-s3` joins
175
+ the network under the alias `s3.localhost` and publishes port 8333. Chromium
176
+ resolves every `*.localhost` name to loopback, so `http://s3.localhost:8333`
177
+ reaches the server from the browser and, through the alias, from the
178
+ container. Its CORS allows every origin.
179
+ - The bucket `sohl-e2e` is created. Anonymous read and the test credentials come
180
+ from the identities file the server is started with.
181
+ - The repository's `cypress/s3/aws-e2e.json` is copied into the data root's
182
+ `Config/`, and the container is created with `FOUNDRY_AWS_CONFIG=aws-e2e.json`.
183
+
184
+ The repository supplies both files: `cypress/s3/s3-identities.json` (the
185
+ SeaweedFS identities, including an `anonymous` identity with `Read`) and
186
+ `cypress/s3/aws-e2e.json` (Foundry's `awsConfig`, pointing at
187
+ `http://s3.localhost:8333`). The credentials are test-only. A missing file stops
188
+ the start and names it.
189
+
190
+ ```bash
191
+ FOUNDRYVTT_CONTAINER_S3=1 npm run e2e:fast -- --recreate
192
+ ```
193
+
194
+ The container's environment is fixed when it is created, so an existing
195
+ container started with the variable set joins the network but keeps the
196
+ environment it was created with; `--recreate` applies `FOUNDRY_AWS_CONFIG`.
197
+ `package-build container <stage> rm` with the variable set removes the server
198
+ and the network along with the container. The server is otherwise left running
199
+ between runs, bucket intact.
@@ -68,6 +68,11 @@ defaults point into the system's `assets/images/` and `assets/icons/` trees,
68
68
  introduced by [HM3's asset-path migration](https://git.pupluppy.org/HeroicLands/HarnMaster-3-FoundryVTT/pulls/60).
69
69
  Use these defaults with an HM3 release that includes those assets.
70
70
 
71
+ A compiled document reaches each icon in the deployed layout — the type root
72
+ and the basename, `systems/sohl/icons/person.svg` — whatever directories hold
73
+ it in the repository, and states its address in `flags.<system>.assets`. See
74
+ [what a package ships](../reference/format-details.md#what-a-package-ships).
75
+
71
76
  ## Write an inline icon in prose
72
77
 
73
78
  Declare the glyph vocabulary in `package-build.config.yaml` or a separate YAML
@@ -861,7 +861,7 @@ everything that reads the tree runs before everything that writes the stage.
861
861
 
862
862
  [`project-setup.md`](../project-setup.md) turns this into npm scripts, and covers
863
863
  everything a repository carries beyond the build itself: the git hooks, the
864
- changeset directory, the label registry, and what each script in the chain is
864
+ changeset directory, and what each script in the chain is
865
865
  for.
866
866
 
867
867
  ## What comes next
@@ -47,7 +47,10 @@ manifest under `build/dist/`. The ZIP contains the staged tree at its root,
47
47
  which is the shape Foundry installs. When the manifest advertises a content
48
48
  index, the corresponding metadata JSONL is also placed beside the ZIP. A
49
49
  manifest that advertises an index the build has not produced fails the
50
- release. An opted-in staged `schema.json` is published beside it too.
50
+ release. An opted-in staged `schema.json` is published beside it too. A
51
+ package whose stage holds pictures also gets `icons.zip` and `images.zip`:
52
+ each holds one picture root of the stage, flat, with the package's asset
53
+ set `asset-sets/default.json`, so a GM can place the files by hand.
51
54
 
52
55
  When the package publishes content and has a book configuration, the release
53
56
  also builds a PDF beside the archive. Book findings are reported without
@@ -128,6 +131,38 @@ the same stage;
128
131
  [container command](../commands.md#package-build-container-stage-action) for
129
132
  the other actions and environment variables.
130
133
 
134
+ ## Publish pictures to the CDN
135
+
136
+ The website draws every picture a package has from
137
+ `<site.assets>/<contentPackage>/`, and the art a Foundry package leaves out
138
+ reaches a world from there as a named asset set. `package-build cdn publish`
139
+ is the one command that writes there:
140
+
141
+ ```bash
142
+ package-build cdn publish --set alt-art.json --name "Thalorna — illustrated" --dry-run
143
+ package-build cdn publish --set alt-art.json --name "Thalorna — illustrated"
144
+ ```
145
+
146
+ It uploads every icon and image flat, transformed as the stage transforms
147
+ them, skips what the CDN already holds at the same size, and writes
148
+ `<site.assets>/<contentPackage>/alt-art.json` listing the left-out pictures.
149
+ Run it after the pictures or their provenance change; it is not part of a
150
+ build or a release.
151
+
152
+ The bucket's credentials go in `.env.local` beside the stage paths:
153
+
154
+ ```dotenv
155
+ CDN_ENDPOINT=https://<account id>.r2.cloudflarestorage.com
156
+ CDN_BUCKET=<bucket>
157
+ CDN_ACCESS_KEY_ID=<access key id>
158
+ CDN_SECRET_ACCESS_KEY=<secret access key>
159
+ ```
160
+
161
+ A dry run with none of them set compares against the public CDN instead. A
162
+ dispatched workflow passes the same four values as named secrets. See the
163
+ [command reference](../commands.md#package-build-cdn-publish) for the set
164
+ file's shape, the refusals and the exit status.
165
+
131
166
  ## Publish a package-build npm version
132
167
 
133
168
  For this toolchain's own npm release, a pull request with a change installers