@heroiclands/package-build 23.7.1 → 23.9.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 (96) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/README.md +32 -0
  3. package/bin/content-commands.mjs +32 -10
  4. package/ci/check-banners.mjs +37 -19
  5. package/container.mjs +48 -10
  6. package/docs/api.md +402 -58
  7. package/docs/authoring/first-note.md +1 -1
  8. package/docs/authoring/frontmatter.md +30 -1
  9. package/docs/authoring/links-and-markup.md +154 -56
  10. package/docs/commands.md +31 -12
  11. package/docs/content-format.md +25 -2
  12. package/docs/design.md +31 -0
  13. package/docs/diagnostics.md +1 -1
  14. package/docs/guides/e2e.md +37 -1
  15. package/docs/guides/packs.md +2 -1
  16. package/docs/guides/reference-pages.md +86 -63
  17. package/docs/project-setup.md +30 -15
  18. package/docs/reference/format-details.md +313 -132
  19. package/docs/reference/note-types.md +191 -174
  20. package/e2e.mjs +89 -2
  21. package/engine/anchors.mjs +7 -20
  22. package/engine/base-compiler.mjs +11 -18
  23. package/engine/compile-corpus.mjs +5 -3
  24. package/engine/content-alerts.mjs +4 -2
  25. package/engine/content-blocks.mjs +18 -36
  26. package/engine/content-format.yaml +1 -0
  27. package/engine/content-index.mjs +447 -33
  28. package/engine/content-links.mjs +43 -15
  29. package/engine/content-poetry.mjs +9 -0
  30. package/engine/content-tables.mjs +8 -4
  31. package/engine/culture-reach.mjs +61 -34
  32. package/engine/data-keys.mjs +3 -1
  33. package/engine/diagnostics.mjs +24 -11
  34. package/engine/dictionary-layout.mjs +151 -19
  35. package/engine/dictionary-rows.mjs +129 -79
  36. package/engine/foundry-entries.mjs +21 -3
  37. package/engine/frontmatter-lint.mjs +1 -1
  38. package/engine/generate.mjs +12 -22
  39. package/engine/glossary-entries.mjs +11 -11
  40. package/engine/heading-attributes.mjs +21 -28
  41. package/engine/helpers.mjs +20 -12
  42. package/engine/infobox.mjs +3 -0
  43. package/engine/journals.mjs +15 -2
  44. package/engine/language-data.mjs +53 -0
  45. package/engine/language-terms.mjs +86 -0
  46. package/engine/language-words.mjs +2 -2
  47. package/engine/lexical-sections.mjs +247 -0
  48. package/engine/markdownlint-run.mjs +12 -2
  49. package/engine/markup-validation.mjs +34 -0
  50. package/engine/note-addresses.mjs +1 -0
  51. package/engine/note-claims.mjs +21 -10
  52. package/engine/note-package.mjs +3 -5
  53. package/engine/note-state.mjs +9 -3
  54. package/engine/note-vocabulary.mjs +36 -7
  55. package/engine/pdf-build.mjs +17 -10
  56. package/engine/prose-lint.mjs +12 -6
  57. package/engine/related-pages.mjs +22 -3
  58. package/engine/see-also.mjs +150 -0
  59. package/engine/site-build.mjs +21 -3
  60. package/engine/site-preview.mjs +4 -2
  61. package/engine/sql-tables.mjs +287 -53
  62. package/engine/standings.mjs +14 -12
  63. package/engine/stub-lint.mjs +6 -2
  64. package/engine/web-wikilinks.mjs +8 -6
  65. package/engine/word-rows.mjs +20 -12
  66. package/engine/word-terms.mjs +15 -6
  67. package/hugo-theme/layouts/partials/related.html +25 -8
  68. package/package.json +2 -1
  69. package/types/container.d.mts +4 -3
  70. package/types/e2e.d.mts +28 -0
  71. package/types/engine/base-compiler.d.mts +2 -2
  72. package/types/engine/compile-corpus.d.mts +2 -1
  73. package/types/engine/content-index.d.mts +85 -8
  74. package/types/engine/content-tables.d.mts +2 -1
  75. package/types/engine/culture-reach.d.mts +22 -2
  76. package/types/engine/data-keys.d.mts +7 -0
  77. package/types/engine/diagnostics.d.mts +7 -6
  78. package/types/engine/dictionary-layout.d.mts +35 -4
  79. package/types/engine/dictionary-rows.d.mts +9 -0
  80. package/types/engine/foundry-entries.d.mts +3 -1
  81. package/types/engine/heading-attributes.d.mts +2 -2
  82. package/types/engine/helpers.d.mts +11 -5
  83. package/types/engine/language-terms.d.mts +7 -0
  84. package/types/engine/lexical-sections.d.mts +95 -0
  85. package/types/engine/markdownlint-run.d.mts +3 -1
  86. package/types/engine/markup-validation.d.mts +14 -0
  87. package/types/engine/note-addresses.d.mts +10 -0
  88. package/types/engine/note-claims.d.mts +7 -1
  89. package/types/engine/note-package.d.mts +3 -1
  90. package/types/engine/note-state.d.mts +2 -1
  91. package/types/engine/note-vocabulary.d.mts +1 -0
  92. package/types/engine/related-pages.d.mts +10 -1
  93. package/types/engine/see-also.d.mts +55 -0
  94. package/types/engine/site-build.d.mts +4 -1
  95. package/types/engine/sql-tables.d.mts +40 -6
  96. package/types/engine/word-rows.d.mts +9 -7
@@ -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
- 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).
48
+ Nonempty `data.words` on a language note, or `data.glossary` on a culture or affiliation, supplies content without an authored body. Every other typed note must have a nonempty body after its frontmatter, including folder and homepage notes. An empty or whitespace-only body is otherwise an error, even with `tags: [draft]` or a description. 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).
@@ -31,7 +31,7 @@ The formatter keeps a collection on one line when the full line is under 100 cha
31
31
 
32
32
  ## Shared facts and game mechanics
33
33
 
34
- The [note-type reference](../reference/note-types.md) lists every field accepted under `data`, including the shared `id`, `pack`, `packFolder`, `harnworld`, and `banner` fields. A type such as `being` also has its own fields, including `homes` and `affiliations`. `culture` is shared by `being`, `place`, `affiliation`, `lore` and `doc`: it names a `lore` note of `subType: culture`, is required on a `doc` of `subType: settingguide`, and is inherited down `data.parents` by places and affiliations. See [culture in the index](../reference/format-details.md#culture-in-the-index). A field absent from a type's vocabulary is a lint error; an unrelated field is not silently repurposed.
34
+ The [note-type reference](../reference/note-types.md) lists every field accepted under `data`, including the shared `id`, `pack`, `packFolder`, `harnworld`, `banner`, and `seeAlso` fields. A type such as `being` also has its own fields, including `homes` and `affiliations`. `culture` is shared by `being`, `place`, `affiliation`, `lore` and `doc`: it names a `lore` note of `subType: culture`, is required on a `doc` of `subType: settingguide`, and is inherited down `data.parents` by places and affiliations. See [culture in the index](../reference/format-details.md#culture-in-the-index). A field absent from a type's vocabulary is a lint error; an unrelated field is not silently repurposed.
35
35
 
36
36
  A being's `data.archetypes` names the roles it can fill in an adventure. Choose every specific role that fits, such as `entertainer` for a performer or `guildsperson` for someone whose professional training or connections matter. Use `[commoner]` when no specific archetype fits. `commoner` cannot share the list with another value. The [archetype reference](../reference/format-details.md#actors) defines the available roles.
37
37
 
@@ -41,6 +41,35 @@ A `sohl:`, `hm3:` or `dnd5e:` block holds values for that Foundry system. Its `s
41
41
 
42
42
  One body describes the subject for both systems. A being's `{#appearance}` and `{#dossier}` sections feed SoHL Actor `system.appearance` and `system.dossier`, and HM3 Actor `system.description` and `system.biography`. An Item's `{#description}` page becomes a shared JournalEntryPage. Its resolved `@UUID[...]` pointer is stored in SoHL Item `system.docHtml` and HM3 Item `system.description`. An Item with no `{#description}` anchor uses its first page. Other H1 pages remain linkable.
43
43
 
44
+ ## Suggested reading
45
+
46
+ Use `data.seeAlso` for deliberate cross-references. Each list entry has a required
47
+ `address` naming another note and an optional `gloss`: a short phrase explaining
48
+ why to follow it. The list keeps the order you write.
49
+
50
+ ```yaml
51
+ data:
52
+ seeAlso:
53
+ - address: lore-harborhistory
54
+ gloss: The founding of the port
55
+ - address: place-breakwater
56
+ - address: thalorna-note-lore-searoutes
57
+ gloss: Routes beyond the bay
58
+ ```
59
+
60
+ Here the harbor history comes first, the breakwater needs no explanation, and
61
+ the sea routes belong to the declared `thalorna` dependency. Write bare Addresses,
62
+ without wikilink brackets or anchors; include the note type, since this field
63
+ has no default type. Local references default to this package and the `note`
64
+ system. A reference must resolve in this package or a declared dependency.
65
+ An unresolved address, duplicate destination, or reference to the note itself
66
+ is an error at that entry's address.
67
+
68
+ The website places these links and glosses in a **See also** group ahead of
69
+ inferred connections in **Related**. A destination also found among backlinks
70
+ or mentions appears only in See also. Foundry journals and books append a
71
+ **See also** section. An omitted or empty list adds no section or group.
72
+
44
73
  ## Place governments
45
74
 
46
75
  A place may state its governing affiliation in `data.government`. The field type is `Address`, with `affiliation` as the default target type when the type is omitted. Use either a local shortcode or a fully qualified affiliation Address from this package or a fetched dependency:
@@ -178,6 +178,20 @@ The harbor master is working for the smugglers.
178
178
  :::
179
179
  ```
180
180
 
181
+ Leave a blank line between a blockquote or alert and the closing `:::`. Without it, Markdown treats the fence as part of the quote and leaves the enclosing block unclosed:
182
+
183
+ ```markdown
184
+ :::secret {title="For the GM"}
185
+ Hidden information.
186
+
187
+ > [!TIP]
188
+ > Advice for the GM.
189
+
190
+ :::
191
+ ```
192
+
193
+ `lint`, `markdown` and `format` refuse malformed block and alert syntax, including unclosed divs and unknown alert types. `format` leaves a malformed file unchanged so you can correct the reported opening line before formatting it.
194
+
181
195
  A secret block can contain alerts and general divs. It cannot contain another secret block. A heading that starts its own journal page cannot split a secret block or general div; use a lower heading without an anchor within the section.
182
196
 
183
197
  Foundry renders a secret section; the website renders an expandable spoiler, and the book renders a labeled box. An untitled secret takes its normal default title, and a Foundry secret without an id receives an id derived from its body. The presentation does not make published HTML private: deliberately inspecting generated source can reveal its content. Use YAML comments for information that must never be published.
@@ -320,7 +334,7 @@ A column's heading is its SQL alias, so `AS "Name"` is what a reader sees. A cel
320
334
  | `glossary` | One row per entry a culture or an affiliation writes in `data.glossary`, with the `note` it belongs to and the `language` it resolves to. |
321
335
  | `language_lineage` | One row per language note and each language it inherits words from through `data.family`, itself included at `depth` 0. Join it to `words` for a language's words with inherited ones. |
322
336
  | `dictionary` | One row per word per language it is listed under, inherited words included, with its `depth`. See [dictionaries and glossaries](#dictionaries-and-glossaries). |
323
- | `terms` | One row per glossary entry, rank or office per note that owns it, with the `cultures` it reaches. See [dictionaries and glossaries](#dictionaries-and-glossaries). |
337
+ | `terms` | One row per glossary entry, rank or office per note that owns it, with its `cultures` associations. See [dictionaries and glossaries](#dictionaries-and-glossaries). |
324
338
  | `register` | One row per name a language note writes in `data.register`, with its `tongue`, `kind`, `gloss` and `why`, its pieces as `built_text`, and the `notes` carrying the name, derived from the index. |
325
339
  | `tongues` | One row per tag a language note writes in `data.tongues`: `tag`, `meaning` and `tongue_language`. |
326
340
  | `retired` | One row per form a language note writes in `data.retired`: `form`, `instead` as a list, and `why`. |
@@ -826,12 +840,12 @@ See [a language's register, sounds and history](../reference/format-details.md#a
826
840
 
827
841
  A braced list may follow the language on the opening fence. Four attributes are accepted, and anything else is reported by name.
828
842
 
829
- | Attribute | Value | Default | What it does |
830
- | --------------- | ----------------------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
831
- | `allow-empty` | `true` or `false` | `false` | Whether a query selecting no rows is allowed. A zero-row result is a finding otherwise, because a table that silently prints nothing is the commonest way a query goes wrong unnoticed. |
832
- | `section-level` | an integer from 1 through 6 | `2` | The heading level given to each group a `_section` column creates, and to each letter of a dictionary or a glossary. |
833
- | `key` | `true`, `false` or `only` | `true` | Whether a dictionary or a glossary prints its key. `false` leaves it out; `only` prints the key for the fence's rows and no letter or entry, so a page with one `key=false` fence per heading states its key once. Refused on a table. |
834
- | `layout` | `table`, `dictionary` or `glossary` | `table` | How the result is set: as a table, as a dictionary of words, or as a glossary of terms — see [dictionaries and glossaries](#dictionaries-and-glossaries). |
843
+ | Attribute | Value | Default | What it does |
844
+ | --------------- | ----------------------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
845
+ | `allow-empty` | `true` or `false` | `false` | Whether a query selecting no rows is allowed. A zero-row result is a finding otherwise, because a table that silently prints nothing is the commonest way a query goes wrong unnoticed. |
846
+ | `section-level` | an integer from 1 through 6 | `2` | The heading level given to each group a `_section` column creates, and to each letter of a dictionary or a glossary. |
847
+ | `key` | `true`, `false` or `only` | `true` | Whether a dictionary or a glossary prints its key. `false` omits the key and any dictionary verb table; `only` prints the key for the fence's rows and no table, letter or entry. Refused on a table. |
848
+ | `layout` | `table`, `dictionary` or `glossary` | `table` | How the result is set: as a table, as a dictionary of words, or as a glossary of terms — see [dictionaries and glossaries](#dictionaries-and-glossaries). |
835
849
 
836
850
  ````markdown
837
851
  ```sql {allow-empty=true section-level=3}
@@ -859,81 +873,165 @@ See [SQL details](../reference/format-details.md#content-tables) for query optio
859
873
 
860
874
  ### Dictionaries and glossaries
861
875
 
862
- Two layouts set a result as paragraphs under letter headings rather than as a table. **`layout=dictionary` sets words**, read from the `dictionary` relation: each with its class, its etymology, its senses, numbered where a word's `gloss` lists several, and the notes it is cited from — every note whose prose marks the word in italics. **`layout=glossary` sets terms**, read from the `terms` relation: a culture's or an affiliation's own glossary entries and an affiliation's ranks and offices, each sense tagged with the body or culture it comes from. Each needs the `name`, `senses` and `usage` columns, and a query that leaves one out is reported, naming the column.
863
-
864
- **The query decides what appears**, and any note may carry either fence. These are the common ones:
876
+ The build supplies a note's dictionary or glossary from its frontmatter on
877
+ every surface: the site, the Foundry journal and the book.
865
878
 
866
- | What | Layout | Query |
867
- | ----------------------------------------- | ------------ | --------------------------------------------------------------------------------- |
868
- | A language's dictionary | `dictionary` | `SELECT * FROM dictionary WHERE language = '<the language's Address>'` |
869
- | A language's common words, on any page | `dictionary` | `SELECT * FROM dictionary WHERE language = '<the language's Address>' AND common` |
870
- | A culture's glossary | `glossary` | `SELECT * FROM terms WHERE list_contains(cultures, '<the culture's Address>')` |
871
- | An affiliation's glossary | `glossary` | `SELECT * FROM terms WHERE owner = '<the affiliation's Address>'` |
872
- | The setting's _Dictionary of the Tongues_ | `dictionary` | `SELECT * EXCLUDE (common) FROM dictionary WHERE depth = 0` |
873
- | The setting's _Glossary_ | `glossary` | `SELECT * EXCLUDE (common) FROM terms` |
879
+ - **A language note with nonempty `data.words` receives a Dictionary section.** It
880
+ contains the language's full dictionary as one alphabetical list, with
881
+ letter headings, each word's field beside it and the key printed once. A
882
+ language with absent or empty `data.words` receives no Dictionary section.
883
+ - **A culture or affiliation with nonempty `data.glossary` receives a Glossary
884
+ section.** It contains only the entries that owner declares. Ranks and
885
+ offices stand in the affiliation's own sections; they enter its glossary
886
+ only when an explicit glossary entry names them. A note with absent or
887
+ empty `data.glossary` receives no Glossary section.
874
888
 
875
- A language's dictionary lists the words it inherits through `data.family` beside its own; `depth = 0` lists each word once, under the language that states it. A culture's glossary holds the culture's own terms and the ranks, offices and glossary terms of every affiliation that reaches it. The two setting pages leave out `common`, so they mark no entry: which words are common is a culture's judgement.
889
+ The sections follow the note's authored text. An existing heading with
890
+ `{#dictionary}` or `{#lexicon}` receives the dictionary after its authored
891
+ text; `{#glossary}` does the same for a glossary. The heading and its
892
+ introduction stay in place, with one generated list under it:
876
893
 
877
- The most common Vylari words, on the Vylarian culture's page:
894
+ ```markdown
895
+ ## Lexicon {#lexicon}
896
+
897
+ The tongue of Provenzia, whose _solmère_ is its own word.
898
+ ```
899
+
900
+ A dictionary belongs to a language; a glossary belongs to one culture or
901
+ affiliation. A glossary entry belongs for one of three reasons: a common word
902
+ in the owner's core vocabulary, a dictionary word with a context meaning,
903
+ or a phrase, compound or term of art outside the dictionary with its own
904
+ gloss. The [worked culture glossary](../reference/format-details.md#glossary-terms)
905
+ selects `solmère` as a common word, gives `amère` a context meaning and glosses
906
+ `blood-field` as a term outside the dictionary. Its frontmatter supplies the
907
+ whole default Glossary section. There are no setting-wide dictionary or
908
+ glossary pages.
909
+
910
+ A dictionary noun prints its gender from its existing class and its plural
911
+ where one is available: **marendos** _n. m., pl. marendoi_. An explicit
912
+ word plural wins; otherwise the language's optional `grammar.plural` rule
913
+ replaces the first matching literal ending. The language's optional
914
+ `grammar.verbs` pattern prints once as a small head table, after the key and
915
+ before the letters. Empty verb endings print as "unchanged". Grammar is
916
+ never inherited through `family`, and absent grammar leaves the dictionary
917
+ unchanged. The [worked noun and verb example](../reference/format-details.md#optional-noun-and-verb-forms)
918
+ shows both declarations. Only supplied shapes are checked; words are never
919
+ checked against the rule, and no language must state grammar.
920
+
921
+ An entry naming a word of its resolved language without a `definition`
922
+ carries the word's dictionary senses and class, with a link to the word's
923
+ entry on its language page. An entry with a `definition` carries that meaning
924
+ instead, retaining its word link and class when it names a dictionary word.
925
+ Ranks, offices and definitions carry their source body or culture beside
926
+ their senses. Common choices belong to each owner: a culture and an
927
+ affiliation using the same language choose independently, and a word may be
928
+ common in one glossary and absent from another. Dictionaries mark nothing
929
+ common.
930
+
931
+ Public dictionaries omit citations to GM-only notes. `links` checks generated
932
+ tables in the source note's audience: public notes use public rows, and GM
933
+ notes retain GM citations. An authored link from a public note to a GM-only
934
+ note is still reported.
935
+
936
+ #### Custom views
937
+
938
+ An `sql` fence with `layout=dictionary` or `layout=glossary` sets a custom view
939
+ of the same relations. A dictionary view reads `dictionary`; a glossary view
940
+ reads `terms`. Each needs the `name`, `senses` and `usage` columns; a query
941
+ that leaves one out is reported, naming the column.
942
+
943
+ The optional `plural` and `verbs` columns supply noun plurals and the head
944
+ table in a custom dictionary view; `SELECT *` includes both. A projection
945
+ that omits these columns omits that presentation, as omitting `class` omits
946
+ word classes. With the default or `key=true`, each selected language's pattern
947
+ appears once per dictionary result, even when no abbreviation key is needed.
948
+ `key=false` omits both the key and the verb table from a fragment;
949
+ `key=only` prints only the abbreviation key. Separate head-enabled fences
950
+ each print their own tables.
951
+
952
+ For a custom list of one culture's common entries:
878
953
 
879
954
  ````markdown
880
955
  ## Words Everyone Knows {#common-words}
881
956
 
882
- ```sql {layout=dictionary section-level=3}
883
- SELECT * FROM dictionary WHERE language = 'thalorna-note-skill-vylarilng' AND common
884
- ```
885
- ````
886
-
887
- A culture's glossary:
888
-
889
- ````markdown
890
- ## Glossary {#glossary}
891
-
892
957
  ```sql {layout=glossary section-level=3}
893
- SELECT * FROM terms WHERE list_contains(cultures, 'thalorna-note-lore-provenzianclt')
958
+ SELECT * FROM terms WHERE owner = 'thalorna-note-lore-provenzianclt' AND common
894
959
  ```
895
960
  ````
896
961
 
897
- A page that sets several fences, one per heading, states its key once: a `key=only` fence at the head over all the rows the page sets, then `key=false` on each fence below it, so the abbreviations are not repeated under every heading. The `key=only` fence prints the key for its rows and no letter heading and no entry:
962
+ A custom dictionary view can select one field from the language's dictionary:
898
963
 
899
964
  ````markdown
900
- ## Words {#words}
901
-
902
- ```sql {layout=dictionary key=only}
903
- SELECT * FROM dictionary WHERE language = 'thalorna-note-skill-vylarilng' AND depth = 0
904
- ```
905
-
906
- ### Love and its conduct
965
+ ## Love and its conduct
907
966
 
908
- ```sql {layout=dictionary key=false section-level=4}
909
- SELECT d.* FROM dictionary d JOIN words w ON w.language = d.language AND w.form = d.name
910
- WHERE d.language = 'thalorna-note-skill-vylarilng' AND w.field = 'Love and its conduct'
967
+ ```sql {layout=dictionary section-level=3}
968
+ SELECT * FROM dictionary
969
+ WHERE language = 'thalorna-note-skill-provnzlng' AND field = 'Love and its conduct'
911
970
  ```
912
971
  ````
913
972
 
914
- `key`, whatever its value, sets a dictionary's or a glossary's key, so it is refused on a fence set as a table.
973
+ A custom view with several fences can print its key once: a `key=only` fence
974
+ over all selected rows, then `key=false` on each fence below it. The
975
+ `key=only` fence prints only the abbreviation key. `key`, whatever its value,
976
+ is refused on a fence set as a table. The automatic Dictionary section
977
+ prints its own key once for the full alphabetical list.
915
978
 
916
979
  Either layout prints, in order:
917
980
 
918
- - **a key**, once, unless the fence sets `key=false`, and nothing else where it sets `key=only`, listing only the abbreviations the result uses — a language's tag, and for words a class or a modifier — each with what it stands for, and the common mark where an entry carries it;
919
- - **a heading per letter**, at `section-level`, accents taken off and apostrophes skipped, so `'ostal` files under O and `Écuyer` under E;
920
- - **one paragraph per entry**: `•` for a common entry, the headword in bold, its pronunciation between slashes, its language tag in italics, the senses, and after a dash the notes it is cited from, as links.
981
+ - **a key**, once, unless the fence sets `key=false`, and nothing else where
982
+ it sets `key=only`, listing the abbreviations the result uses — a language's
983
+ tag, a word's class or a modifier — each with its expansion; a glossary also
984
+ explains the common mark where an entry carries it;
985
+ - **a heading per letter**, at `section-level`, accents taken off and
986
+ apostrophes skipped, so `'ostal` files under O and `Écuyer` under E;
987
+ - **one paragraph per entry**: a glossary's `•` for a common entry, the
988
+ headword in bold, its pronunciation between slashes, its language tag and
989
+ word class in italics where present, its senses, and after a dash the notes
990
+ it is cited from, as links.
921
991
 
922
- A word also carries its class beside the tag and its etymology in brackets:
992
+ A dictionary entry gives its field in italic parentheses and its etymology
993
+ in brackets:
923
994
 
924
995
  ```markdown
925
- • **solmère** /sol-MEH-reh/ _n. m._ [fr. Élav. _sólmerë_] Longing; the ache for a time that is gone. — [[thalorna-note-lore-lastshrine|The Song of the Last Shrine]]
996
+ **solmère** /sol-MEH-reh/ _n. m._ (_Love and its conduct_) [fr. Élav. _sólmerë_] Longing; the ache for a person, a place or a time that is gone. — [[thalorna-note-lore-lastshrine|The Song of the Last Shrine]]
926
997
  ```
927
998
 
928
- A term carries no class, and each of its senses names its source. A glossary sets the rows sharing a headword and a language as one entry, numbering their senses in the order the rows arrive and printing a sense several bodies give once, with all of them:
999
+ A glossary entry naming a dictionary word uses the word's senses when
1000
+ `definition` is absent; a list of dictionary glosses supplies every sense.
1001
+ Those senses carry no body label. An entry with a `definition` uses that
1002
+ meaning and labels it with the owner, retaining its word link and class
1003
+ where it names a dictionary word. On the owner's own page, a sense attributed
1004
+ only to that owner omits the repeated label; other bodies and shared senses
1005
+ keep their labels. A glossary sets the rows sharing a headword
1006
+ and a language as one entry, numbering their senses in query order and
1007
+ printing a sense several bodies give once, with all of them:
929
1008
 
930
1009
  ```markdown
931
- **Sworn Knight** **1** (_Duchy of Aubrac_) Holds land of the duke. **2** (_Kingdom of Provenzia_) Holds land of the crown. **3** (_Provenzia_) A knight bound by oath to a lord. — [[thalorna-note-affiliation-duchy|Duchy of Aubrac]], [[thalorna-note-affiliation-kingdom|Kingdom of Provenzia]], [[thalorna-note-lore-provenzianclt|Provenzia]]
932
- ```
933
-
934
- The entries are ordered by the layout, not by the query: by letter, then by `name_sort`. **The language tag prints where the entry's language is not the page's own**: a language note's own language, a culture's `data.language`, or the language an affiliation's glossary resolves to. It is the language note's `data.abbreviation`, or its full name where it states none. The etymology never repeats the entry's own language: a borrowed word prints `[fr. Élav. _sólmerë_]`, a built word its pieces each with its gloss, `[_am-_ "love" + _-ère_ "one who"]`, an inherited word its older form, `[*_hur_]` or `[Hel. *_thalass_]`, and a root word its root and pattern. A citation of a note with no published page prints as its name.
935
-
936
- A culture's or an affiliation's `data.glossary` shapes both: an entry without a definition marks a word or a rank common, gives it a sort key or a respelling, and lets in a rank whose title is a single word; an entry with a definition replaces the senses of the owner's own rank or office, or is a term of the owner's own. The [format reference](../reference/format-details.md#dictionaries-and-glossaries) gives every column and rule.
1010
+ **Sworn Knight** **1** (_Duchy of Aubrac_) Holds land of the duke. **2** (_Kingdom of Provenzia_) Holds land of the crown. **3** A knight bound by oath to a lord. — [[thalorna-note-affiliation-duchy|Duchy of Aubrac]], [[thalorna-note-affiliation-kingdom|Kingdom of Provenzia]], [[thalorna-note-lore-provenzianclt|Provenzia]]
1011
+ ```
1012
+
1013
+ The layout orders entries by letter, then by `name_sort`. **The language tag
1014
+ prints where the entry's language is not the page's own**: a language note's
1015
+ own language, a culture's `data.language`, or the language an affiliation's
1016
+ glossary resolves to. It is the language note's `data.abbreviation`, or its
1017
+ full name where unstated. Etymology never repeats the entry's own language:
1018
+ a borrowed word prints `[fr. Élav. _sólmerë_]`, a built word its pieces each
1019
+ with its gloss, `[_am-_ "love" + _-ère_ "one who"]`, an inherited word its
1020
+ older form, `[*_hur_]` or `[Hel. *_thalass_]`, and a root word its root and
1021
+ pattern. A citation of a note with no published page prints as its name.
1022
+
1023
+ A built piece can name a language element by its `form` or one of its
1024
+ `variants`; its `sense` prints beside the piece. An affix can also be named by
1025
+ its `front` form. Words and affixes take precedence over elements in the same
1026
+ language, and the language is searched before its ancestors. Exact spellings
1027
+ are checked before ignoring joining hyphens; case and accents are preserved.
1028
+
1029
+ A glossary entry may give its own sort key or respelling. A reference to a
1030
+ rank or office inherits its descriptions and admits a title of one word; its
1031
+ `common` value belongs to the entry's owner. A definition replaces the senses
1032
+ of the owner's own rank or office, or supplies an entry of the owner's own.
1033
+ The [format reference](../reference/format-details.md#dictionaries-and-glossaries)
1034
+ gives every column and rule.
937
1035
 
938
1036
  ## Page lists
939
1037
 
@@ -973,7 +1071,7 @@ That list names the `lore` notes tagged `key-concept`, grouped by type and alpha
973
1071
 
974
1072
  **A tag no page carries is a finding** at the fence's own line, naming the tag it looked for. A misspelled tag is the whole of what goes wrong here, and a heading with an empty list under it is how one goes unnoticed. Write `{allow-empty=true}` where a group that is genuinely empty is intended; the directive then leaves nothing behind rather than an empty list.
975
1073
 
976
- A page list names this package's own pages. A **stub** — a note with an empty body — publishes no page, so it joins no list. A note tagged `gm` is listed in Foundry and left out of the website and the book, exactly as it is left out of a content table's relations.
1074
+ A page list names this package's own pages. A **stub** — a note with neither an authored body nor automatic lexical content — publishes no page, so it joins no list. A note tagged `gm` is listed in Foundry and left out of the website and the book, exactly as it is left out of a content table's relations.
977
1075
 
978
1076
  See [page-list details](../reference/format-details.md#page-lists) for the directive's full behaviour.
979
1077
 
package/docs/commands.md CHANGED
@@ -1995,8 +1995,8 @@ package-build lint [root] [--no-references]
1995
1995
  **DESCRIPTION**
1996
1996
 
1997
1997
  Checks a content tree's addresses and frontmatter without compiling anything
1998
- — no LevelDB is opened and no Foundry manifest is needed, so it runs in
1999
- under a second and can gate a commit. Beyond address well-formedness, it
1998
+ — no LevelDB is opened and no Foundry manifest is needed. Beyond address
1999
+ well-formedness, it
2000
2000
  checks frontmatter against the declared vocabulary and each system's `sohl:`
2001
2001
  / `hm3:` block, the package's charset (so a book can pick a font that covers
2002
2002
  it), that an icon a note writes is one the registry declares, and that
@@ -2012,6 +2012,16 @@ dependencies; exact Item UUID validation remains part of compilation. Reads
2012
2012
  the content tree named by `root`, defaulting to
2013
2013
  `paths.content`; writes nothing.
2014
2014
 
2015
+ Content lint computes source positions when it emits findings and shares each
2016
+ note's YAML position document across consecutive lookups.
2017
+
2018
+ The final summary reports wall time in milliseconds for `index` (collecting
2019
+ content records), `SQL` (opening relations and preparing tables, inline queries
2020
+ and generated sections), and `validation` (building the link index and running
2021
+ the checks). These are elapsed durations from a monotonic clock, including any
2022
+ waits within each stage. They are prose summary output, separate from positioned
2023
+ findings, and appear whether completed validation passes or reports errors.
2024
+
2015
2025
  **OPTIONS**
2016
2026
 
2017
2027
  | Positional | Type | Default | Description |
@@ -2172,6 +2182,9 @@ wikilink at all. Also reports a vendored foreign-package content index that
2172
2182
  has drifted out of reach, naming `package-build deps fetch` as the fix. Resolves
2173
2183
  address-valued SoHL system fields declared by the installed schema against this
2174
2184
  package and its declared dependencies.
2185
+ An anchored reference to a dependency's readable note must name a prose anchor
2186
+ in its fetched content index. A missing section is an error even when the page
2187
+ address resolves. Event ids are not prose sections and cannot be wikilink targets.
2175
2188
  Reads the content tree named by `root`, defaulting to `paths.content`, and
2176
2189
  the cached content indexes of any declared dependency; writes nothing.
2177
2190
 
@@ -2332,16 +2345,22 @@ package-build content-index [root] [--out <dir>]
2332
2345
 
2333
2346
  **DESCRIPTION**
2334
2347
 
2335
- Emits this package's note index as JSON Lines — one record per note,
2336
- carrying its full frontmatter plus its place in the tree. Every build
2337
- already walks the tree and parses every note's frontmatter and then throws
2338
- the result away; this publishes that walk so anything outside a build can
2339
- query the content without paying for a second walk. Its own command rather
2340
- than only a build step, because the point is that anyone can regenerate it
2341
- at will — the artifact costs a frontmatter parse, not a build, which is
2342
- also what lets it stay uncommitted. Reads the content tree named by `root`,
2343
- defaulting to `paths.content`; writes `<contentPackage>-metadata.jsonl`
2344
- into `--out`, defaulting to the configured `paths.contentIndex`.
2348
+ Emits this package's note index as JSON Lines, carrying full frontmatter,
2349
+ tree positions, prose and event anchors, and document targets. Reads the
2350
+ content tree named by `root`, defaulting to `paths.content`; writes
2351
+ `<contentPackage>-metadata.jsonl`, `<contentPackage>-dictionary.jsonl` and
2352
+ `<contentPackage>-terms.jsonl` into `--out`, defaulting to the configured
2353
+ `paths.contentIndex`.
2354
+
2355
+ The command prepares SQL tables, page lists, automatic sections and scalar
2356
+ expressions before publishing their prose targets. Each source uses its own
2357
+ audience for prose declarations: public notes use public providers and usage,
2358
+ and GM notes use the complete corpus. Native target maps use the compendium's
2359
+ complete SQL corpus and expose only declared prose slugs present there, with
2360
+ their actual native page targets. Readable item documentation targets its
2361
+ documentation journal. An empty published prose-anchor list declares no prose targets,
2362
+ including for pages without a Foundry document. Preparation errors leave all
2363
+ three output files untouched.
2345
2364
 
2346
2365
  **OPTIONS**
2347
2366
 
@@ -9,7 +9,7 @@ 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`.
12
+ Nonempty `data.words` on a language note (`type: skill`, `subType: language`), or `data.glossary` on a culture or affiliation, supplies page content without an authored body. Every other typed note requires a nonempty body, including drafts, folders and homepages. An empty or whitespace-only body is otherwise 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
13
 
14
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.
15
15
 
@@ -91,6 +91,29 @@ errors, reported at the `ranks` key.
91
91
 
92
92
  An Item's `{#description}` page is its readable description. The note Address `<package>-note-<type>-<shortcode>#description` resolves to a JournalEntryPage UUID. The build writes the same `@UUID[...]` pointer to SoHL Item `system.docHtml` and HM3 Item `system.description`. An Item note without that anchor uses its first page as the description. Other pages and anchors remain available for links.
93
93
 
94
+ ## Suggesting another note
95
+
96
+ State deliberate cross-references in `data.seeAlso`, in reading order:
97
+
98
+ ```yaml
99
+ data:
100
+ seeAlso:
101
+ - address: lore-harborhistory
102
+ gloss: The founding of the port
103
+ - address: place-breakwater
104
+ - address: thalorna-note-lore-searoutes
105
+ gloss: Routes beyond the bay
106
+ ```
107
+
108
+ Each `address` names a whole note; `gloss` optionally explains why to read it.
109
+ The first entry points to the port's history, the second to its breakwater,
110
+ and the third to a note in the declared `thalorna` dependency. An address must
111
+ resolve, and neither a duplicate destination nor the note itself is allowed.
112
+ The site shows the list ahead of other Related links, without repeating their
113
+ destinations. Journals and books append See also; an omitted or empty list adds
114
+ nothing. See [suggested reading](authoring/frontmatter.md#suggested-reading)
115
+ for the Address rules.
116
+
94
117
  ## File and folder names
95
118
 
96
119
  Every character in the path of a content file or a picture is printable 7-bit ASCII, U+0020 to U+007E. The rule covers each file and folder name under the content tree and under the package's `icons/` and `images/` roots. Letters, digits, spaces, hyphens, underscores, dots and apostrophes are all inside it, so `Knight's_Shield.md` is a valid name. An accented letter, any other non-ASCII character, and a control character are outside it.
@@ -143,6 +166,6 @@ Places, affiliations, lore and beings may name their culture in `data.culture`,
143
166
 
144
167
  An affiliation may name the trade, temple or order tongue it keeps in its own `data.language`. An affiliation that reaches no culture is a warning, in a tree holding a culture note. A language needs no culture and no affiliation. See [type: affiliation](reference/format-details.md#type-affiliation).
145
168
 
146
- A language note (`type: skill`, `subType: language`) states its words in frontmatter: `data.words` states each word as a map of its form, class, field and gloss — one sense or a list of them, which a dictionary numbers — with its source, its variant forms, the pieces it is built from and a note, beside `data.classModifiers`, `data.fields` and `data.affixes`, each affix with the `kind` it makes, for a given name's ending who the name is `given` to, and the stem closes it is barred after; a zero affix states `zero: true` and no form. The notes a word is cited from are derived, not written: a note cites a word by marking it in italics in its prose, matched by form or a variant with diacritics and case set aside. The language of a word is the note it sits on, and none of it reaches the compiled Item. A daughter language names its parent in `data.family` and reads the parent's words as its own; the `language_lineage` relation joins a language to its ancestors. Classes come from the [master word classes](reference/format-details.md#word-classes). A culture or an affiliation states its own glossary terms in `data.glossary`, and every entry resolves to a language — its own `language`, else the affiliation's, its parents' or its culture's, else the culture's — or is an error at the entry. The `words`, `affixes`, `fields`, `glossary` and `language_lineage` relations hold them for `sql` fences, the `dictionary` relation lists each word under every language that has it, inherited words included, and the `terms` relation each glossary entry, rank and office with the cultures it reaches; a fence sets them with `layout=dictionary` or `layout=glossary`. A language note names the tag a dictionary prints for it in `data.abbreviation`. Every fact a check reads about a language's names, sounds and history is frontmatter too: `data.tongues` and `data.register` (every name the setting gives in the language's reach, the notes carrying each derived from the index), `data.retired`, `data.scope`, `data.sounds` (letters with their place, word edges from a closed list, the cluster limit and the groups of clusters inside a word, the stress rule, syllable finals, syllable rules and vowel harmony with each vowel's front partner, the joining of parts, written marks, a root's skeleton vowel and frames, the spellings read past, and named mutations such as wearing and voicing), `data.soundChanges` (named sets of ordered steps), `data.elements` (each standing `opening`, `closing`, `either` or `link`, for one kind of name, with the word it is the stem of and its genitive), `data.roots`, `data.cognates` (with a reconstructed `proto` form), `data.callingNames`, `data.older` (the forms older than the rules), `data.negation` (the negative particle and the mutation it brings) and `data.naming` (how long each class of name runs, the edges names keep that words do not, the element kinds each kind of name draws on, what a place name takes from what it is held from, and the near name), each a relation — `sounds` as `letters`, `edges`, `clusters`, `cluster_groups`, `stress`, `finals`, `syllables`, `joining`, `joins`, `marks`, `skeleton`, `frames`, `read_past` and `mutations`, `soundChanges` as `sound_changes`, `callingNames` as `calling_names` — that a fence sets back on the page. A word's citations also come from a note whose name, alias or rank or office title spells it whole. See [a language's words](reference/format-details.md#a-languages-words), [a language's register, sounds and history](reference/format-details.md#a-languages-register-sounds-and-history), [glossary terms](reference/format-details.md#glossary-terms) and [dictionaries and glossaries](reference/format-details.md#dictionaries-and-glossaries).
169
+ A language note (`type: skill`, `subType: language`) states its words in frontmatter: `data.words` states each word as a map of its form, class, field and gloss — one sense or a list of them, which a dictionary numbers — with its source, its variant forms, the pieces it is built from and a note, beside `data.classModifiers`, `data.fields` and `data.affixes`, each affix with the `kind` it makes, for a given name's ending who the name is `given` to, and the stem closes it is barred after; a zero affix states `zero: true` and no form. The notes a word is cited from are derived, not written: a note cites a word by marking it in italics in its prose, matched by form or a variant with diacritics and case set aside. The language of a word is the note it sits on, and none of it reaches the compiled Item. A daughter language names its parent in `data.family` and reads the parent's words as its own; the `language_lineage` relation joins a language to its ancestors. Classes come from the [master word classes](reference/format-details.md#word-classes). A culture or an affiliation states its own glossary entries in `data.glossary`: common words in its core vocabulary, dictionary words with a context meaning, or phrases, compounds and terms of art outside the dictionary with their own gloss. An entry naming a word of its resolved language takes the word's senses and class and links to its language entry; a `definition` supplies the entry's meaning instead. Common marks belong to the glossary's owner. A language note with nonempty `data.words` receives its full Dictionary section automatically: one alphabetical list with field tags and one key. A culture or affiliation with nonempty `data.glossary` receives only its explicitly declared entries in its Glossary section; ranks and offices stand in the affiliation's own sections. A note without the relevant frontmatter receives no such section. All surfaces show these sections after the authored text, or under an existing `{#dictionary}`, `{#lexicon}` or `{#glossary}` heading after its introduction. There are no setting-wide dictionary or glossary pages. Every entry resolves to a language — its own `language`, else the affiliation's, its parents' or its culture's, else the culture's — or is an error at the entry. The `words`, `affixes`, `fields`, `glossary` and `language_lineage` relations hold them for `sql` fences, the `dictionary` relation lists each word under every language that has it, inherited words included, and the `terms` relation each glossary entry, rank and office with its culture associations; a fence sets custom views with `layout=dictionary` or `layout=glossary`. A language note names the tag a dictionary prints for it in `data.abbreviation`. Every fact a check reads about a language's names, sounds and history is frontmatter too: `data.tongues` and `data.register` (every name the setting gives in the language's reach, the notes carrying each derived from the index), `data.retired`, `data.scope`, `data.sounds` (letters with their place, word edges from a closed list, the cluster limit and the groups of clusters inside a word, the stress rule, syllable finals, syllable rules and vowel harmony with each vowel's front partner, the joining of parts, written marks, a root's skeleton vowel and frames, the spellings read past, and named mutations such as wearing and voicing), `data.soundChanges` (named sets of ordered steps), `data.elements` (each standing `opening`, `closing`, `either` or `link`, for one kind of name, with the word it is the stem of and its genitive), `data.roots`, `data.cognates` (with a reconstructed `proto` form), `data.callingNames`, `data.older` (the forms older than the rules), `data.negation` (the negative particle and the mutation it brings) and `data.naming` (how long each class of name runs, the edges names keep that words do not, the element kinds each kind of name draws on, what a place name takes from what it is held from, and the near name), each a relation — `sounds` as `letters`, `edges`, `clusters`, `cluster_groups`, `stress`, `finals`, `syllables`, `joining`, `joins`, `marks`, `skeleton`, `frames`, `read_past` and `mutations`, `soundChanges` as `sound_changes`, `callingNames` as `calling_names` — that a fence sets back on the page. A word's citations also come from a note whose name, alias or rank or office title spells it whole. See [a language's words](reference/format-details.md#a-languages-words), [a language's register, sounds and history](reference/format-details.md#a-languages-register-sounds-and-history), [glossary terms](reference/format-details.md#glossary-terms) and [dictionaries and glossaries](reference/format-details.md#dictionaries-and-glossaries).
147
170
 
148
171
  Blockquote alerts use `[!NOTE]`, `[!TIP]`, `[!IMPORTANT]`, `[!WARNING]`, or `[!]`, with optional attributes after the marker. They replace the former info and warn div fences; secret blocks remain supported. See [alerts](authoring/links-and-markup.md#alerts).
package/docs/design.md CHANGED
@@ -30,3 +30,34 @@ Passes declare their inputs and outputs so compile order follows dependencies wh
30
30
  ## Verification
31
31
 
32
32
  The command and API references have tests that derive their surfaces from source. The note-format contract lives in structured data and code declarations so editorial changes to Markdown do not change validation. Generated field references are checked against those declarations. Tests check emitted documents as well as input acceptance: a build that exits successfully but drops a value is still wrong. A consumer regression run compares `build/packs-json` across toolchain versions against a real content tree.
33
+
34
+ The SQL scaling guard runs `prepareTreeSqlTables` over two sizes of a synthetic
35
+ linked note tree, with rewritten shortcodes and references. It profiles each
36
+ executed query through the supplied database, including local and dependency
37
+ relations, inline expressions and generated event sections. DuckDB's JSON
38
+ `EXPLAIN ANALYZE` reports the actual cardinality of file-reader operators before
39
+ downstream filters; these source rows must stay at zero after database creation.
40
+ A direct JSON scan is the positive control for that measurement. In-memory
41
+ scans may grow with the corpus, and elapsed time is not an assertion. Schema
42
+ inspection through `DESCRIBE` has no execution profile and is excluded.
43
+
44
+ Run the guard with `npm test -- tests/sql-scaling.test.ts`. A controlled local
45
+ change from `CREATE TABLE` to `CREATE VIEW` in `createTableFromFile` must make
46
+ it fail as source rows read per query increase with the larger tree; restore
47
+ the implementation after the probe. The companion relation tests check that
48
+ queries survive source-file removal and enumerate file-reading views.
49
+
50
+ A compile retains parsed frontmatter, body, raw source and body coordinates in
51
+ its corpus. SQL preparation, document passes, folder pins, event anchors,
52
+ culture inheritance and prose-presence checks consume that shared result.
53
+ The compile guard counts actual Markdown reads and YAML parses for every file
54
+ in its fixture, including documentation and folder notes, and requires one of
55
+ each. Diagnostic positions consume retained source text when supplied.
56
+
57
+ Standing checks locate only findings they emit. YAML path lookups retain the
58
+ most recently located text's document and line counter; another text replaces
59
+ that single cache entry, so an edited note cannot inherit stale coordinates.
60
+ SQL preparation caches authored answers and stub-inclusive comparisons in
61
+ separate fields per query. Generated event fences read `entries` and `events`
62
+ and skip the comparison; their authored/generated boundary is recorded when
63
+ the sections are appended, independently of query text.
@@ -170,7 +170,7 @@ package-build.config.yaml:382:64: error: package-build config: `site.sections.be
170
170
 
171
171
  ## A worked case: the three states of a note
172
172
 
173
- A note with an empty body is a **stub**, and the diagnostics about one show each
173
+ A place note with an empty body is a **stub**, and the diagnostics about one show each
174
174
  rule above in turn.
175
175
 
176
176
  An empty body must be deliberate, so a stub that says nothing about itself is an
@@ -128,6 +128,33 @@ for interactive testing. Arguments after `--` are passed to the suite, for
128
128
  example `package-build e2e run -- --browser chrome`. A port answering is not
129
129
  enough: the harness waits until Foundry's join page shows the world is active.
130
130
 
131
+ ## Choose a browser
132
+
133
+ For a Cypress suite, `e2e fast`, `e2e run` and `e2e open` use pinned
134
+ Chrome for Testing **155.0.8059.84** on macOS arm64 and Linux x64. The first run
135
+ downloads it; later runs reuse the cached executable. Each run prints the
136
+ resolved browser and version, and Cypress reports the browser in its header.
137
+ Other suite commands run as declared.
138
+
139
+ An explicit `--browser` in the configured suite or its extra arguments wins,
140
+ including `--browser=electron`. For example:
141
+
142
+ ```bash
143
+ package-build e2e fast -- --browser electron
144
+ package-build e2e run -- --browser /path/to/chrome
145
+ package-build e2e open -- --browser=chrome
146
+ ```
147
+
148
+ A platform without a configured build requires an explicit installed browser.
149
+ The automatic download needs network access on first use. The cache location is,
150
+ in order: `HL_BROWSER_CACHE`, `browser-cache` beside `FOUNDRYVTT_CACHE`,
151
+ `HL_E2E_ROOT/browser-cache`, or `heroiclands/chrome-for-testing` under the user's
152
+ cache directory (`XDG_CACHE_HOME`, macOS `~/Library/Caches`, otherwise
153
+ `~/.cache`). CI keeps it inside the job's persistent `HL_E2E_ROOT`; point
154
+ `FOUNDRYVTT_CACHE` there too, or set `HL_BROWSER_CACHE` explicitly. Reseeding
155
+ and container teardown leave the browser cache intact. Renovate tracks the
156
+ exact Chrome pin in `e2e.mjs` through the repository's custom datasource.
157
+
131
158
  ## Iterate without reseeding
132
159
 
133
160
  `fast` rebuilds declared targets, redeploys the full stage, cycles the world,
@@ -196,4 +223,13 @@ container started with the variable set joins the network but keeps the
196
223
  environment it was created with; `--recreate` applies `FOUNDRY_AWS_CONFIG`.
197
224
  `package-build container <stage> rm` with the variable set removes the server
198
225
  and the network along with the container. The server is otherwise left running
199
- between runs, bucket intact.
226
+ between runs, bucket intact. New and reused servers must answer the S3 HTTP
227
+ health check before Foundry starts; a ready filer alone is insufficient.
228
+
229
+ Both configuration paths must be readable regular files. When changing
230
+ worktrees, an existing server must bind the identities file from the current
231
+ checkout (symlinks to the same file are accepted). An incompatible or
232
+ uninspectable bind stops the start and names the server and expected path.
233
+ Restore the original checkout to keep its bucket data, or remove only the named
234
+ disposable S3 server with `docker rm -f <container>-s3` and rerun. Removing that
235
+ server discards its bucket data; the tool never removes it automatically.
@@ -20,7 +20,8 @@ Start with one pack for each document class the notes produce. An Item, Actor,
20
20
  Macro, Scene, or Adventure note can also produce a JournalEntry for its prose,
21
21
  so a package with those notes generally needs a JournalEntry pack. A missing
22
22
  pack for a document a note actually produces is a finding on that note. An
23
- empty body produces no prose journal and needs no pack for it.
23
+ empty body without automatic Dictionary or Glossary content produces no prose
24
+ journal and needs no pack for it.
24
25
 
25
26
  ```yaml
26
27
  packs: