@heroiclands/package-build 22.27.0 → 23.0.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # @heroiclands/package-build
2
2
 
3
+ ## 23.0.0
4
+
5
+ ### Major Changes
6
+
7
+ Every page a work of literature names in its subjects lists that work under **In song and story**, with its form beside the title, including works from a package this one depends on.
8
+
9
+ ### Patch Changes
10
+
11
+ Affiliations, skills, attributes, mysteries, mystical abilities and miscellaneous gear without their own art show SoHL's default icons again instead of a broken image.
12
+
13
+ Folder notes need only a nonempty body: a short line or placeholder no longer warns or fails, with or without `draft`. An empty folder body is still an error.
14
+
15
+ Lore notes can be typed `literature` for epics, sagas, poems, legends and other works a people tells, sings or writes. A literature note may name its people, its form, its subjects and its language, and each appears in the note's infobox, with the people, subjects and language as links.
16
+
17
+ ## 22.28.0
18
+
19
+ ### Minor Changes
20
+
21
+ Notes now require a nonempty body, including drafts, folders and homepages. Short written notes still warn unless tagged as drafts.
22
+
3
23
  ## 22.27.0
4
24
 
5
25
  ### Minor Changes
@@ -226,7 +226,12 @@ const SHIPPED_ITEM_FIELDS = { sohl: ITEM_FIELDS, hm3: HM3_ITEM_FIELDS };
226
226
  /** Register the content operations on the package command line. */
227
227
  export function registerContentCommands(cli) {
228
228
  return cli
229
- .command(withIndexPreflight(packageCommand(), (_config, argv) => argv.action === "compile"))
229
+ .command(
230
+ withIndexPreflight(
231
+ withBodyPreflight(packageCommand(), (_config, argv) => argv.action === "compile"),
232
+ (_config, argv) => argv.action === "compile",
233
+ ),
234
+ )
230
235
  .command(depsCommand())
231
236
  .command(docsCommand())
232
237
  .command(withIndexPreflight(lintCommand()))
@@ -235,11 +240,17 @@ export function registerContentCommands(cli) {
235
240
  .command(withIndexPreflight(linksCommand()))
236
241
  .command(formatCommand())
237
242
  .command(markdownCommand())
238
- .command(contentIndexCommand())
239
- .command(withIndexPreflight(siteCommand(), publishesContentPages))
243
+ .command(
244
+ withBodyPreflight(
245
+ contentIndexCommand(),
246
+ () => true,
247
+ (config, argv) => argv.root || config.paths.content,
248
+ ),
249
+ )
250
+ .command(withIndexPreflight(withBodyPreflight(siteCommand()), publishesContentPages))
240
251
  .command(
241
252
  withIndexPreflight(
242
- pdfCommand(),
253
+ withBodyPreflight(pdfCommand(), (config) => Boolean(config.pdf)),
243
254
  (config) => config.pdf && publishesContentPages(config),
244
255
  ),
245
256
  )
@@ -380,6 +391,43 @@ function proseCommand() {
380
391
  };
381
392
  }
382
393
 
394
+ /** Refuse invalid note bodies before emitting metadata, packs, pages or a book. */
395
+ function withBodyPreflight(
396
+ command,
397
+ shouldCheck = () => true,
398
+ contentRoot = (config) => config.paths.content,
399
+ ) {
400
+ const handler = command.handler;
401
+ return {
402
+ ...command,
403
+ handler: async (argv) => {
404
+ try {
405
+ const config = loadPackConfig();
406
+ if (shouldCheck(config, argv)) {
407
+ const contentBase = contentRoot(config, argv);
408
+ if (!fs.existsSync(contentBase)) return handler(argv);
409
+ const records = indexRecordsFor({ config, contentBase });
410
+ const { findings } = lintNoteStates(contentBase, {
411
+ records,
412
+ contentPackage: config.contentPackage,
413
+ });
414
+ const errors = findings.filter((finding) => finding.severity === "error");
415
+ if (errors.length) {
416
+ for (const finding of errors) emitDiagnostic(finding);
417
+ process.exitCode = 1;
418
+ return;
419
+ }
420
+ }
421
+ } catch (err) {
422
+ reportFailure(err);
423
+ process.exitCode = 1;
424
+ return;
425
+ }
426
+ return handler(argv);
427
+ },
428
+ };
429
+ }
430
+
383
431
  /** Check declared dependency indexes before an index-consuming command does any work. */
384
432
  function withIndexPreflight(command, shouldCheck = () => true) {
385
433
  const handler = command.handler;
@@ -1167,7 +1215,7 @@ function lintCommand() {
1167
1215
  skipDirectories: config.skipDirectories,
1168
1216
  });
1169
1217
 
1170
- // Whether every empty body is a stub on purpose, and how
1218
+ // Whether every note has a body, and how
1171
1219
  // finished the tree is. The counts and the oldest drafts are
1172
1220
  // prose rather than findings: a note nobody has finished is not
1173
1221
  // wrong, and 464 warnings a reader cannot clear is how a report
@@ -0,0 +1,49 @@
1
+ /*
2
+ * This file is part of the Song of Heroic Lands (SoHL) system for Foundry VTT.
3
+ * Copyright (c) 2024-2026 Tom Rodriguez ("Toasty") — <toasty@heroiclands.org>
4
+ *
5
+ * This work is licensed under the GNU General Public License v3.0 (GPLv3).
6
+ * You may copy, modify, and distribute it under the terms of that license.
7
+ *
8
+ * For full terms, see the LICENSE.md file in the project root or visit:
9
+ * https://www.gnu.org/licenses/gpl-3.0.html
10
+ *
11
+ * SPDX-License-Identifier: GPL-3.0-or-later
12
+ */
13
+
14
+ /**
15
+ * Publish this package to npm, with provenance wherever it can be generated.
16
+ *
17
+ * `publishConfig.provenance` is `true`, and npm refuses a publish with
18
+ * `Automatic provenance generation not supported for provider: null` anywhere
19
+ * that cannot mint a Sigstore identity. That is a CI runner holding an OIDC
20
+ * token: GitHub Actions with `id-token: write`, which exposes
21
+ * `ACTIONS_ID_TOKEN_REQUEST_URL`, or GitLab CI with `SIGSTORE_ID_TOKEN`.
22
+ * Everywhere else — a desk — this passes `--provenance=false`, so the publish
23
+ * goes ahead unattested rather than failing.
24
+ *
25
+ * Arguments after `--` reach `npm publish` unchanged, so
26
+ * `npm run deploy:npm -- --dry-run` shows the tarball without publishing it.
27
+ * `prepack` builds the declarations and the asset index on the way.
28
+ *
29
+ * Not a `package-build` subcommand: it publishes this package and no other.
30
+ */
31
+
32
+ import { spawnSync } from "node:child_process";
33
+
34
+ const env = process.env;
35
+ const provenance =
36
+ (env.GITHUB_ACTIONS === "true" && Boolean(env.ACTIONS_ID_TOKEN_REQUEST_URL)) ||
37
+ (env.GITLAB_CI === "true" && Boolean(env.SIGSTORE_ID_TOKEN));
38
+
39
+ if (!provenance) {
40
+ console.log("deploy:npm: no OIDC identity here; publishing without provenance.");
41
+ }
42
+
43
+ const args = ["publish", ...(provenance ? ["--provenance"] : ["--provenance=false"])];
44
+ const result = spawnSync("npm", [...args, ...process.argv.slice(2)], {
45
+ stdio: "inherit",
46
+ shell: process.platform === "win32",
47
+ });
48
+ if (result.error) throw result.error;
49
+ process.exit(result.status ?? 1);
package/docs/api.md CHANGED
@@ -441,16 +441,16 @@ Drawing a declared infobox: HTML for separate Foundry pages appended after autho
441
441
 
442
442
  The three states of a note — **stub**, **draft**, **full** — and the one observable fact they rest on. A note whose body is empty is a stub: it carries its facts into the index and is queryable there, and it has no page, no address and no link target. Nothing is stored: the ladder is derived once in the SQL `entries` view and no note can assert its own state.
443
443
 
444
- | Export | Signature | Returns | Use it when |
445
- | --------------------- | ------------------------------------------- | --------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
446
- | `NOTE_STATES` | `const NOTE_STATES` | `ReadonlyArray<string>` | naming the three states in ladder order |
447
- | `isEmptyBody` | `isEmptyBody(body)` | `boolean` | asking the exact question: is everything after the closing frontmatter fence whitespace? |
448
- | `isStubbableType` | `isStubbableType(type, vocabulary)` | `boolean` | asking whether an empty body suppresses this type's page; `folder` and `homepage` are structural and exempt |
449
- | `isStubNote` | `isStubNote(frontmatter, body, vocabulary)` | `boolean` | deciding whether one note is a stub, which is what the index and the site build both ask |
450
- | `journalHasContent` | `journalHasContent(body)` | `boolean` | asking whether the JournalEntry a body compiles into would hold anything, which is what decides whether one is made at all |
451
- | `PLACEHOLDER_PHRASES` | `const PLACEHOLDER_PHRASES` | `ReadonlyArray<string>` | reading the phrases that stand in for prose nobody has written |
452
- | `placeholderBody` | `placeholderBody(body)` | `Array<{phrase: string, line: number}>` | finding a body that reduces to nothing but placeholders, which is an abandoned draft rather than a stub |
453
- | `bodyWordCount` | `bodyWordCount(body)` | `number` | counting rendered prose, with wikilinks as one word and closed dashes, slashes, and ellipses separating adjacent words |
444
+ | Export | Signature | Returns | Use it when |
445
+ | --------------------- | ------------------------------------------- | --------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
446
+ | `NOTE_STATES` | `const NOTE_STATES` | `ReadonlyArray<string>` | naming the three states in ladder order |
447
+ | `isEmptyBody` | `isEmptyBody(body)` | `boolean` | asking the exact question: is everything after the closing frontmatter fence whitespace? |
448
+ | `isStubbableType` | `isStubbableType(type, vocabulary)` | `boolean` | asking whether an empty body suppresses this type's page; `folder` and `homepage` retain their addresses in this classification, but still require bodies |
449
+ | `isStubNote` | `isStubNote(frontmatter, body, vocabulary)` | `boolean` | deciding whether one note is a stub, which is what the index and the site build both ask |
450
+ | `journalHasContent` | `journalHasContent(body)` | `boolean` | asking whether the JournalEntry a body compiles into would hold anything, which is what decides whether one is made at all |
451
+ | `PLACEHOLDER_PHRASES` | `const PLACEHOLDER_PHRASES` | `ReadonlyArray<string>` | reading the phrases that stand in for prose nobody has written |
452
+ | `placeholderBody` | `placeholderBody(body)` | `Array<{phrase: string, line: number}>` | finding a body that reduces to nothing but placeholders, which is an abandoned draft rather than a stub |
453
+ | `bodyWordCount` | `bodyWordCount(body)` | `number` | counting rendered prose, with wikilinks as one word and closed dashes, slashes, and ellipses separating adjacent words |
454
454
 
455
455
  ### `engine.draftNotice`
456
456
 
@@ -471,7 +471,7 @@ What a draft note says about itself, in one sentence, on every surface it reache
471
471
 
472
472
  ### `engine.stubLint`
473
473
 
474
- An empty body must be deliberate. Four rules hold the state honest — a stub states what it is, a stub is not tagged `draft`, a body that renders to nothing is an abandoned draft, and a short unmarked body is asked whether it forgot its marker — and two reports say what the rules cannot decide.
474
+ Every typed note must have a nonempty body. Empty and whitespace-only bodies are errors regardless of description or `draft` tag, including structural types such as `folder` and `homepage`. Placeholder-only bodies also produce errors. Nonempty bodies with fewer than 25 prose words produce warnings unless tagged `draft`. A `folder` note is held only to the nonempty rule: any body, placeholder or short, passes with or without `draft`. Untyped vault scaffolding is excluded. The lint, content-index, package compile, site and PDF commands enforce body errors before publishing. The state classification helpers still describe incomplete records for inspection; their classifications do not exempt notes from validation.
475
475
 
476
476
  | Export | Signature | Returns | Use it when |
477
477
  | ------------------ | ---------------------------------------------------------- | --------------------------------------------------- | --------------------------------------------------------------------------------------------- |
@@ -45,4 +45,4 @@ The full Address is `<package>-note-lore-harbor`; `<package>` comes from `conten
45
45
 
46
46
  Add `description` for a short page summary, `tags: [draft]` to mark unfinished prose, or `data` for fields the note's type permits. A system block such as `sohl:` or `hm3:` adds mechanics and can produce a Foundry Actor or Item. It does not replace the shared prose or make another JournalEntry. [Frontmatter](frontmatter.md) explains the allowed keys, and the [note-type reference](../reference/note-types.md) lists every type-specific field.
47
47
 
48
- A note with an empty body is a stub. It remains addressable as data, while a document that would have no content is omitted. See [note states](../reference/format-details.md#three-states-of-a-note) for the precise emission rules.
48
+ Every typed note must have a nonempty body after its frontmatter. An empty or whitespace-only body is an error, even with `tags: [draft]` or a description. This includes folder and homepage notes. A nonempty body with fewer than 25 prose words produces a warning unless the note has a `draft` tag. A folder note needs only a nonempty body: any content passes, with or without the tag. See [note states](../reference/format-details.md#three-states-of-a-note).
@@ -9,6 +9,8 @@ subType: userguide
9
9
 
10
10
  A content package begins with Markdown notes under `assets/content/`. Each note has YAML frontmatter describing its identity and data, followed by prose. Folder names beneath `assets/content/` are for authors; `type`, `subType`, and Addresses determine what the build creates.
11
11
 
12
+ Every typed note requires a nonempty body, including drafts, folders and homepages. An empty or whitespace-only body is an error. A nonempty body with fewer than 25 prose words warns unless tagged `draft`; the tag never exempts an empty body. A `folder` note needs only a nonempty body: any content passes, with or without `draft`.
13
+
12
14
  Start with [your first note](authoring/first-note.md). It explains the smallest valid note and shows the JournalEntry, web page, book entry, and content-index record made from it. Then use the chapters below in the order your note needs them.
13
15
 
14
16
  | Need | Read |
@@ -144,12 +144,12 @@ The build always writes `title`, `slug`, `url`, `kbfolder`, `package` and
144
144
  note. `infoboxes` is assembled from the note's own fields, so no note authors
145
145
  it and nothing in the note format accepts it.
146
146
 
147
- The build writes `resolvedDates`, `related`, `contains`, `governed_by`, `governed_places`
148
- and `map` only when it has something to write. A page with no links either way
147
+ The build writes `resolvedDates`, `related`, `contains`, `governed_by`, `governed_places`,
148
+ `works` and `map` only when it has something to write. A page with no links either way
149
149
  carries no `related`, and a place with no drawing carries no `map`.
150
150
 
151
- An authored `aliases`, `related`, `contains`, `governed_by`, `governed_places` or `map` is
152
- dropped. Each states a fact about the whole tree rather than about one note, so
151
+ An authored `aliases`, `related`, `contains`, `governed_by`, `governed_places`, `works` or
152
+ `map` is dropped. Each states a fact about the whole tree rather than about one note, so
153
153
  the build replaces whatever a note wrote.
154
154
 
155
155
  Artwork addresses are rewritten in place under `data`. A page's `data.icon`,
@@ -837,7 +837,7 @@ and the whole of it is the suffix.
837
837
 
838
838
  ```markdown
839
839
  ![A map of the Vale](images/maps/valeofthorns.webp) <!-- this package's -->
840
- ![A shield](sohl/assets/icons/noun/shield.svg) <!-- the sohl package's -->
840
+ ![A shield](sohl/assets/icons/noun/shieldnn.svg) <!-- the sohl package's -->
841
841
  ```
842
842
 
843
843
  The four forms, for a `harnensemble` note (the `harnensemble` content package
@@ -850,14 +850,14 @@ ships as the Foundry module `harn-ensemble`) writing `images/map.webp`:
850
850
  | **Web** | `https://cdn.heroiclands.org/harnensemble/images/map.webp` |
851
851
  | **Book** | `assets/images/map.webp`, staged beside the Typst source |
852
852
 
853
- And for the same note writing `sohl/assets/icons/noun/shield.svg`, a file the
853
+ And for the same note writing `sohl/assets/icons/noun/shieldnn.svg`, a file the
854
854
  system ships and this repository does not hold:
855
855
 
856
856
  | Surface | Address |
857
857
  | ----------- | ------------------------------------------------------------- |
858
- | **Foundry** | `systems/sohl/assets/icons/noun/shield.svg` |
859
- | **Local** | `assets/icons/noun/shield.svg`, in the `sohl` repository |
860
- | **Web** | `https://cdn.heroiclands.org/sohl/icons/noun/shield.svg` |
858
+ | **Foundry** | `systems/sohl/assets/icons/noun/shieldnn.svg` |
859
+ | **Local** | `assets/icons/noun/shieldnn.svg`, in the `sohl` repository |
860
+ | **Web** | `https://cdn.heroiclands.org/sohl/icons/noun/shieldnn.svg` |
861
861
  | **Book** | not carried, unless a declared replacement carries it instead |
862
862
 
863
863
  **The content package name and Foundry id are separate values.**
@@ -2006,8 +2006,20 @@ convention rather than declared in a field — see
2006
2006
 
2007
2007
  ### Three states of a note
2008
2008
 
2009
- A note has three states, and two of them are nothing but conventions the format
2010
- already carried.
2009
+ Every typed note requires a nonempty body after its closing frontmatter fence.
2010
+ An empty or whitespace-only body is an error, regardless of description or
2011
+ `tags: [draft]`. This includes `folder` and `homepage` notes. Untyped vault
2012
+ scaffolding is excluded. A nonempty body with fewer than 25 prose words warns
2013
+ unless tagged `draft`; the tag never exempts an empty body. A `folder` note is
2014
+ held only to the nonempty rule: any body, a placeholder or a single line
2015
+ included, passes with or without `draft`.
2016
+
2017
+ The `lint`, `content-index`, `package compile`, `site` and `pdf` commands enforce
2018
+ body errors. Write a short beginning and tag it `draft` when work is unfinished.
2019
+
2020
+ The internal classification below describes records, including incomplete notes
2021
+ that inspection APIs can still read. A **stub** is invalid authored content and
2022
+ must receive a body before these commands can publish it.
2011
2023
 
2012
2024
  | State | Body | Marker | Page | Document | Link target |
2013
2025
  | --------- | ------- | --------------- | ------------ | --------------------------------- | ----------- |
@@ -2039,13 +2051,13 @@ same.
2039
2051
 
2040
2052
  | After the fence | Empty? | What happens |
2041
2053
  | ----------------------- | ------ | --------------------------------------------------------------- |
2042
- | Nothing, or blank lines | yes | A stub |
2054
+ | Nothing, or blank lines | yes | An error, with or without `draft` |
2043
2055
  | An HTML comment | no | A note with a body that renders to nothing. The lint reports it |
2044
2056
  | A horizontal rule | no | The same |
2045
2057
  | A lone heading | no | The same |
2046
2058
  | `_To be written._` | no | The same |
2047
2059
 
2048
- An HTML comment is deliberately not empty. An invisible marker does not make a valid stub; use an empty body when the note has no prose.
2060
+ An HTML comment is deliberately not empty, but an invisible marker does not supply prose. Replace placeholder-only content with written text. A `folder` note is the exception: any of these bodies satisfies it, and the lint reports nothing.
2049
2061
 
2050
2062
  #### What a stub emits
2051
2063
 
@@ -2090,12 +2102,12 @@ What is suppressed with the page is the web address and the link target: a
2090
2102
  wikilink into a stub is refused, and a table cell listing one prints its name as
2091
2103
  plain text rather than as a link.
2092
2104
 
2093
- #### Two types are structural and exempt
2105
+ #### Structural classification does not exempt validation
2094
2106
 
2095
- `folder` and `homepage` have no body by design — a folder note's page is a
2096
- generated section index, and the homepage's is the site's front door. An empty
2097
- body on either keeps its address and resolves as **full**, because such a note is
2098
- complete.
2107
+ A `folder` note's page is a generated section index, and the homepage is the
2108
+ site's front door. The classification helpers retain their addresses and report
2109
+ **full** even when their bodies are empty. Validation still rejects an empty
2110
+ body for both types.
2099
2111
 
2100
2112
  Every type in the vocabulary declares whether an empty body suppresses its page,
2101
2113
  so adding a type means answering the question rather than inheriting a default.
@@ -2784,6 +2796,15 @@ An Item can mark its readable description with `# ... {#description}`. That head
2784
2796
 
2785
2797
  Generates a living (or undead, or spirit) being.
2786
2798
 
2799
+ **subType**:
2800
+
2801
+ - npc: A character meant to be encountered. It may describe a reusable template and need not be
2802
+ a singular individual.
2803
+ - character: A detailed, playable, singular individual.
2804
+ - creature: Any being that is neither a character nor an NPC.
2805
+
2806
+ See [a being's subtype describes the subject](#a-beings-subtype-describes-the-subject).
2807
+
2787
2808
  | `data` property | Values | Description |
2788
2809
  | --------------------------- | ---------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
2789
2810
  | `tokenIcon` | `Address` | What a token on the canvas wears — an `icon` address; defaults to `icon` |
@@ -3916,22 +3937,39 @@ the setting itself, rather than instructions or other apparatus for the GM.
3916
3937
  which covers the _reckoning_ — a festival's date is calendar and the festival is not, and a
3917
3938
  tournament is not a matter of time-reckoning at all — and from `culture`, which is a grouping
3918
3939
  of people rather than an occasion they attend.
3919
-
3920
- | `data` property | Values | Description |
3921
- | ----------------------- | ----------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- |
3922
- | `epoch` | canonical `<year>.<day>` | The canonical day that equals calendar year 1, day 1 |
3923
- | `months` | `{ name, abbreviation?, days }[]` | Ordered months; their days sum to the world's year |
3924
- | `weekdays` | `{ name, abbreviation? }[]` | Ordered week days, indexed from zero; weeks run continuously |
3925
- | `seasons` | `{ name, abbreviation?, start }[]` | Seasons starting on one-based days of year |
3926
- | `namedDays` | `{ name, abbreviation?, day }[]` | Names assigned to one-based days of year |
3927
- | `eras` | `{ shortcode, name, abbreviation?, marker?, proclaimedBy?, start, label? }[]` | Year counts; `start` is `null` or an in-calendar year |
3928
- | `formats` | map of names to Calendaria patterns | Named date formats; `std` is the preferred default |
3929
- | `events` | `{ when, until?, recurs? }[]` | This note's dated occurrences, each with its own relationships and chronicle fields |
3930
- | `events[].when` | a date, or `"0.<day>"` | The occurrence's anchor and first instance — required |
3931
- | `events[].until` | a date | Where the occurrence ran to, or where a recurring series stopped |
3932
- | `events[].recurs` | `{ every }` or `{ on }` | How further occurrences are found, absent for a one-time occurrence |
3933
- | `events[].recurs.every` | whole number of years, 1 or more | A period counted on the canonical axis from `when` |
3934
- | `events[].recurs.on` | list of dates, strictly increasing, each later than `when` | Recorded occurrences beyond the first, in place of a period |
3940
+ - literature: A work a people composes and keeps — an epic, saga, poem, legend, hymn or
3941
+ chronicle, told, sung or written. Held apart from `history`, which states what happened: a
3942
+ legend is literature whether or not its events took place, and a chronicle is literature as a
3943
+ work even where its events are also history.
3944
+
3945
+ Each note a work names in `data.subjects` lists that work on its own site page, under **In
3946
+ song and story**, with the work's `form` beside its title; the subject note writes nothing.
3947
+ A work in a fetched index lists on this package's pages when that index carries its
3948
+ `subjects`.
3949
+
3950
+ Calendar fields are written only on a `calendar` note, and `culture`, `form`, `subjects` and
3951
+ `language` only on a `literature` note; written on any other lore subType, each is an error at
3952
+ its own key.
3953
+
3954
+ | `data` property | Values | Description |
3955
+ | ----------------------- | ----------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
3956
+ | `epoch` | canonical `<year>.<day>` | The canonical day that equals calendar year 1, day 1 |
3957
+ | `months` | `{ name, abbreviation?, days }[]` | Ordered months; their days sum to the world's year |
3958
+ | `weekdays` | `{ name, abbreviation? }[]` | Ordered week days, indexed from zero; weeks run continuously |
3959
+ | `seasons` | `{ name, abbreviation?, start }[]` | Seasons starting on one-based days of year |
3960
+ | `namedDays` | `{ name, abbreviation?, day }[]` | Names assigned to one-based days of year |
3961
+ | `eras` | `{ shortcode, name, abbreviation?, marker?, proclaimedBy?, start, label? }[]` | Year counts; `start` is `null` or an in-calendar year |
3962
+ | `formats` | map of names to Calendaria patterns | Named date formats; `std` is the preferred default |
3963
+ | `culture` | `Address` | `literature` only: the people whose work it is, naming a lore note with `subType: culture` |
3964
+ | `form` | `string` | `literature` only: the kind of work in its people's own terms — `epic`, `saga`, `praise-song`, `elegy`. Free text |
3965
+ | `subjects` | `Address[]` | `literature` only: the beings, places, gods, events and other notes the work concerns |
3966
+ | `language` | `Address` | `literature` only: the tongue it is composed in, naming a skill note with `subType: language` |
3967
+ | `events` | `{ when, until?, recurs? }[]` | This note's dated occurrences, each with its own relationships and chronicle fields |
3968
+ | `events[].when` | a date, or `"0.<day>"` | The occurrence's anchor and first instance — required |
3969
+ | `events[].until` | a date | Where the occurrence ran to, or where a recurring series stopped |
3970
+ | `events[].recurs` | `{ every }` or `{ on }` | How further occurrences are found, absent for a one-time occurrence |
3971
+ | `events[].recurs.every` | whole number of years, 1 or more | A period counted on the canonical axis from `when` |
3972
+ | `events[].recurs.on` | list of dates, strictly increasing, each later than `when` | Recorded occurrences beyond the first, in place of a period |
3935
3973
 
3936
3974
  `data.events` is available on every `lore` subType. Each entry's `kind`, `depth`, `summary`, `sources`, `standing`, `accounts`, `who`, `where`, `unresolved`, `follows` and `names` hold its chronicle detail; their shapes belong to whatever design governs chronicle records, and this format only asks that they sit inside an entry. The shared format checks `when`, `until` and `recurs`; a content package can check the rest. `events` itself does not appear as an infobox row, but a recurring entry's computed next occurrence does.
3937
3975
 
@@ -4735,8 +4773,8 @@ the map for the pack it is writing and falls back to `default` when there is no
4735
4773
  such key, so a mistyped `journal:` files the folder wherever the default puts it
4736
4774
  — exactly the hierarchy the key was written to override, and silently.
4737
4775
 
4738
- **A folder note carries no prose.** It is structure, not content, so it produces
4739
- no documentation journal and takes no part in `docEntryTypes`.
4776
+ **A folder note requires a body**, like every typed note. Its body does not
4777
+ produce a documentation journal or take part in `docEntryTypes`.
4740
4778
  In a compiled Foundry journal, a wikilink to a folder prints its label as text:
4741
4779
  the folder can materialise in several packs and has no single compendium UUID.
4742
4780
 
@@ -417,7 +417,7 @@ These fields are accepted by every note type. A field's value can still be irrel
417
417
 
418
418
  ### lore
419
419
 
420
- **Subtypes:** `cosmology`, `deity`, `theology`, `arcana`, `spirit`, `economy`, `law`, `calendar`, `history`, `material`, `folk`, `culture`, `custom`, `bestiary`, `gathering`.
420
+ **Subtypes:** `cosmology`, `deity`, `theology`, `arcana`, `spirit`, `economy`, `law`, `calendar`, `history`, `material`, `folk`, `culture`, `custom`, `bestiary`, `gathering`, `literature`.
421
421
 
422
422
  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.
423
423
 
@@ -430,6 +430,10 @@ Lore records in-world knowledge. A `culture` describes a people; a `custom` desc
430
430
  | `data.namedDays` | list of `{ name, abbreviation?, day }` | Names assigned to particular days of the year. |
431
431
  | `data.eras` | list of `{ shortcode, name, marker?, abbreviation?, proclaimedBy?, start, label? }` | The year-counts kept in this calendar. A marker names one era and uses these months. |
432
432
  | `data.formats` | map of named Calendaria format strings | Named patterns for reading and writing this calendar's dates. |
433
+ | `data.culture` | an Address | The people whose work this is, as a culture lore note. |
434
+ | `data.form` | string | The kind of work in its people's own terms — an epic, a saga, a praise-song, an elegy. Free text. |
435
+ | `data.subjects` | list of Addresses | The beings, places, gods, events and other notes the work concerns. |
436
+ | `data.language` | an Address | The tongue the work is composed in, as a language skill note. |
433
437
  | `data.events` | list of `{ when, until?, recurs? }` | This note's dated occurrences — a founding once, an anniversary that recurs, or a list of recorded happenings. |
434
438
 
435
439
  ### map
@@ -14,7 +14,7 @@ map, search, and the markup an image or a figure renders as — and names the
14
14
  classes a consumer styles against.
15
15
 
16
16
  **None of the sections below is an authoring instruction.** `infoboxes`,
17
- `related`, `contains`, `governed_by`, `governed_places`, `map`, `package`, `slug`, `url`
17
+ `related`, `contains`, `governed_by`, `governed_places`, `works`, `map`, `package`, `slug`, `url`
18
18
  and `title` cannot be written by a note at all: a note's top-level region is
19
19
  closed and does not admit them, so each arrives already resolved by the time
20
20
  the theme sees it. Read this page when a panel is missing or a class needs
@@ -215,6 +215,37 @@ uses, so a region with forty settlements stays scannable.
215
215
  A page whose front matter declares none of `contains`, `governed_by` or
216
216
  `governed_places` renders no card at all.
217
217
 
218
+ ## The "In song and story" card
219
+
220
+ `partials/literature-works.html` renders a second card directly below the
221
+ government card, listing the works of literature that name the page's subject.
222
+ The build derives `works` from every `literature` lore note's `data.subjects`,
223
+ including works a fetched index carries, so no note writes it:
224
+
225
+ ```yaml
226
+ ---
227
+ works: [{ title: …, url: …, form: … }] # works naming this page, sorted by title
228
+ ---
229
+ ```
230
+
231
+ `form` is the work's `data.form`, printed in parentheses beside its title, and is
232
+ absent when the work states none. A work naming one subject twice lists once, and
233
+ a work naming itself is not listed on its own page. An entry's `url` is absent
234
+ for a work that publishes no page, which renders as plain text through
235
+ `partials/holdings/entry.html`. A page no work names carries no `works` and
236
+ renders no card.
237
+
238
+ The card reuses the government card's classes, so a consumer styling one styles
239
+ both; `.literature-works` on the `<section>` tells them apart.
240
+
241
+ | class | drawn as |
242
+ | ----------------------- | --------------------------------- |
243
+ | `.literature-works` | the card, beside `.holdings` |
244
+ | `.holdings-table-label` | "In song and story" |
245
+ | `.holdings-table-grid` | the grid of works |
246
+ | `.holdings-entry-name` | a work's title, linked or plain |
247
+ | `.holdings-entry-kind` | the work's form, beside its title |
248
+
218
249
  ## The place map
219
250
 
220
251
  A place page with `map: from-<shortcode>.svg` displays a **From here** panel
@@ -241,6 +241,10 @@ types:
241
241
  - namedDays
242
242
  - eras
243
243
  - formats
244
+ - culture
245
+ - form
246
+ - subjects
247
+ - language
244
248
  - events
245
249
  subTypes:
246
250
  - cosmology
@@ -258,6 +262,7 @@ types:
258
262
  - custom
259
263
  - bestiary
260
264
  - gathering
265
+ - literature
261
266
  map:
262
267
  data:
263
268
  - scene
@@ -42,9 +42,8 @@
42
42
  * because a `Folder` is a core Foundry document like a `JournalEntry` or a
43
43
  * `Scene`, not a system's.
44
44
  *
45
- * **A folder note carries no prose.** It is structure, not content, so it wants
46
- * no documentation journal and takes no part in `docEntryTypes` — the one
47
- * open decision, settled the way it recommended.
45
+ * **A folder note requires a body** under author validation. That body creates
46
+ * no documentation journal and takes no part in `docEntryTypes`.
48
47
  *
49
48
  * @module
50
49
  */
package/engine/index.mjs CHANGED
@@ -93,7 +93,7 @@ export * as noteState from "./note-state.mjs";
93
93
  /** What a draft note says about itself, in one sentence, on every surface. */
94
94
  export * as draftNotice from "./draft-notice.mjs";
95
95
 
96
- /** Whether every empty body is a stub on purpose, and how finished a tree is. */
96
+ /** Require written bodies on typed notes and report how finished a tree is. */
97
97
  export * as stubLint from "./stub-lint.mjs";
98
98
 
99
99
  /** What a place is next to and reachable from: the closed sets, and the checks. */