@heroiclands/package-build 23.5.0 → 23.6.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 (54) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/bin/content-commands.mjs +23 -5
  3. package/bin/package-build.mjs +1 -74
  4. package/docs/api.md +9 -0
  5. package/docs/authoring/links-and-markup.md +77 -12
  6. package/docs/commands.md +4 -48
  7. package/docs/content-format.md +16 -0
  8. package/docs/diagnostics.md +1 -2
  9. package/docs/guides/module-project.md +1 -1
  10. package/docs/guides/reference-pages.md +89 -0
  11. package/docs/guides/release-and-deploy.md +3 -3
  12. package/docs/project-setup.md +16 -59
  13. package/docs/reference/format-details.md +533 -84
  14. package/docs/reference/note-types.md +191 -149
  15. package/engine/base-compiler.mjs +21 -0
  16. package/engine/cdn-publish.mjs +30 -5
  17. package/engine/content-format.yaml +9 -0
  18. package/engine/content-index.mjs +19 -5
  19. package/engine/culture-reach.mjs +330 -0
  20. package/engine/dictionary-layout.mjs +340 -0
  21. package/engine/dictionary-rows.mjs +608 -0
  22. package/engine/glossary-entries.mjs +182 -0
  23. package/engine/glossary-language.mjs +99 -0
  24. package/engine/index.mjs +1 -0
  25. package/engine/infobox.mjs +8 -0
  26. package/engine/language-words.mjs +360 -0
  27. package/engine/literature-notes.mjs +10 -8
  28. package/engine/note-addresses.mjs +25 -0
  29. package/engine/note-vocabulary.mjs +161 -1
  30. package/engine/path-charset.mjs +120 -0
  31. package/engine/project-init.mjs +4 -29
  32. package/engine/sql-tables.mjs +311 -21
  33. package/engine/word-classes.mjs +167 -0
  34. package/engine/word-rows.mjs +233 -0
  35. package/engine/word-terms.mjs +306 -0
  36. package/package.json +2 -4
  37. package/types/engine/cdn-publish.d.mts +26 -25
  38. package/types/engine/content-index.d.mts +15 -5
  39. package/types/engine/culture-reach.d.mts +66 -0
  40. package/types/engine/dictionary-layout.d.mts +69 -0
  41. package/types/engine/dictionary-rows.d.mts +39 -0
  42. package/types/engine/glossary-entries.d.mts +12 -0
  43. package/types/engine/glossary-language.d.mts +42 -0
  44. package/types/engine/index.d.mts +1 -0
  45. package/types/engine/language-words.d.mts +33 -0
  46. package/types/engine/literature-notes.d.mts +2 -2
  47. package/types/engine/note-addresses.d.mts +17 -0
  48. package/types/engine/path-charset.d.mts +38 -0
  49. package/types/engine/sql-tables.d.mts +7 -2
  50. package/types/engine/word-classes.d.mts +87 -0
  51. package/types/engine/word-rows.d.mts +64 -0
  52. package/types/engine/word-terms.d.mts +68 -0
  53. package/labels.mjs +0 -191
  54. package/types/labels.d.mts +0 -40
package/CHANGELOG.md CHANGED
@@ -1,5 +1,38 @@
1
1
  # @heroiclands/package-build
2
2
 
3
+ ## 23.6.0
4
+
5
+ ### Minor Changes
6
+
7
+ - `package-build lint` refuses a content or picture path holding a character outside printable ASCII, reporting it as an error at the path. Rename the file using ASCII only; addresses come from the shortcode, so no address changes.
8
+
9
+ ### Patch Changes
10
+
11
+ **Dictionaries and glossaries**
12
+
13
+ - A language note lists its words in `data.words`, with the class modifiers, fields and affixes they use; the compiled Item is unchanged.
14
+ - A language names its parent in `data.family` and reads the parent's words as its own; lint reports a parent that is no language or a family that loops.
15
+ - A language states the tag a dictionary prints for it in `data.abbreviation`, such as `Prov.`.
16
+ - Cultures and affiliations list their own glossary terms in `data.glossary`. These terms mark words and ranks common, give respellings, let in single-word titles, and add or replace definitions.
17
+ - A glossary term that names no language takes its affiliation's, a parent affiliation's or its culture's; lint reports a term that ends up with none.
18
+ - Lint warns about an affiliation that belongs to no culture, since its ranks and offices reach no glossary.
19
+ - An `sql` fence sets its rows as a dictionary of words with `layout=dictionary`, or as a glossary of terms with `layout=glossary`: bold headwords under letter headings, a key of only the abbreviations used, numbered senses and citations.
20
+ - One query lists a language's words, including inherited ones, or its common words on any page. One query lists a culture's or an affiliation's glossary with its ranks and offices, or the whole setting's dictionary and glossary.
21
+ - `package-build content-index` also writes the dictionary and the glossary beside the index, for reading with `jq` or an editor.
22
+
23
+ **Checks and compile**
24
+
25
+ - The content checks and the compile run several times faster on a large tree.
26
+ - Two notes that pin the same `id` are reported at the second note, naming the first, instead of as dozens of items that duplicate themselves.
27
+
28
+ ## 23.5.1
29
+
30
+ ### Patch Changes
31
+
32
+ `cdn publish` skips SVGs the CDN already holds, so a re-run uploads only what changed.
33
+
34
+ `package-build labels check` is gone, because issue labels are defined once for the whole organisation; remove any `lint:labels` script and `.github/labels.yml`. A project created by `package-build init` no longer gets a label registry.
35
+
3
36
  ## 23.5.0
4
37
 
5
38
  ### Minor Changes
@@ -69,6 +69,7 @@ import { checkArtSlotSizes } from "../engine/art-fields.mjs";
69
69
  import { lintContentTree } from "../engine/content-lint.mjs";
70
70
  import { lintNoteStates } from "../engine/stub-lint.mjs";
71
71
  import { lintContentCharset } from "../engine/content-charset.mjs";
72
+ import { lintPathCharset } from "../engine/path-charset.mjs";
72
73
  import { lintContentHtml } from "../engine/content-html.mjs";
73
74
  import { lintContentIcons } from "../engine/content-icons.mjs";
74
75
  import { lintContentImages } from "../engine/content-images.mjs";
@@ -1093,6 +1094,16 @@ function lintCommand() {
1093
1094
  skipDirectories: config.skipDirectories,
1094
1095
  });
1095
1096
 
1097
+ // The spelling of every content and picture path, held to
1098
+ // printable ASCII. Walked from the directory entries rather
1099
+ // than the parsed notes, so a picture, which has no note,
1100
+ // is covered by the same rule.
1101
+ const pathCharset = lintPathCharset({
1102
+ contentBase: root,
1103
+ assetsBase: config.paths.assets,
1104
+ skipDirectories: config.skipDirectories,
1105
+ });
1106
+
1096
1107
  // The names the charset check leaves room for. An icon
1097
1108
  // the registry does not declare renders as its own literal
1098
1109
  // text, which is visible but easy to publish, so it is reported
@@ -1175,6 +1186,7 @@ function lintCommand() {
1175
1186
  ...frontmatter.findings,
1176
1187
  ...schemaFindings,
1177
1188
  ...charset.findings,
1189
+ ...pathCharset.findings,
1178
1190
  ...icons.findings,
1179
1191
  ...html.findings,
1180
1192
  ...images.findings,
@@ -1593,16 +1605,22 @@ function contentIndexCommand() {
1593
1605
  handler: (argv) => {
1594
1606
  try {
1595
1607
  const config = loadPackConfig();
1596
- const { file, notes, bytes, full, draft, stub } = emitContentIndex({
1597
- config,
1598
- ...(argv.root ? { contentBase: argv.root } : {}),
1599
- ...(argv.out ? { outDir: argv.out } : {}),
1600
- });
1608
+ const { file, notes, bytes, full, draft, stub, dictionary, terms } =
1609
+ emitContentIndex({
1610
+ config,
1611
+ ...(argv.root ? { contentBase: argv.root } : {}),
1612
+ ...(argv.out ? { outDir: argv.out } : {}),
1613
+ });
1601
1614
  log.info(
1602
1615
  `${config.contentPackage} → ${path.relative(process.cwd(), file)} ` +
1603
1616
  `(${notes} notes — ${full} full, ${draft} draft, ` +
1604
1617
  `${stub} stub — ${Math.round(bytes / 1024)} KiB)`,
1605
1618
  );
1619
+ for (const [relation, written] of Object.entries({ dictionary, terms }))
1620
+ log.info(
1621
+ `${config.contentPackage} → ${path.relative(process.cwd(), written.file)} ` +
1622
+ `(${written.rows} ${relation} rows)`,
1623
+ );
1606
1624
  } catch (err) {
1607
1625
  reportFailure(err);
1608
1626
  process.exitCode = 1;
@@ -114,7 +114,6 @@ import { walkMarkdownTree } from "../engine/helpers.mjs";
114
114
  import { reckoningContext } from "../engine/reckoning-markers.mjs";
115
115
  import { SCHEMA_ARTIFACT_FILE } from "../engine/foreign-catalog.mjs";
116
116
  import { validateLangSource } from "../lang.mjs";
117
- import { checkLabelRegistry } from "../labels.mjs";
118
117
  import { lintYaml } from "../engine/yaml-lint.mjs";
119
118
  import { lintChangesetText, lintReleaseText } from "../engine/changelog-lint.mjs";
120
119
  import { groupChangelogText } from "../engine/changelog-group.mjs";
@@ -1328,77 +1327,6 @@ function changelogGroup(args) {
1328
1327
  console.log(`package-build: ${files.length} file(s) grouped · ${warnings} warning(s)`);
1329
1328
  }
1330
1329
 
1331
- /**
1332
- * `labels check` — do the machine registry and the documented table agree?
1333
- *
1334
- * `.github/labels.yml` is synced to GitHub and the §3 table in
1335
- * `.github/ISSUE_REPORTING.md` is what a person reads. Neither derives from the
1336
- * other, so either can drift, and nothing notices until an issue is filed
1337
- * against a label that does not exist.
1338
- *
1339
- * Every repository wants this and only the paths ever differed, which is why it
1340
- * is here rather than copied into each as a `utils/` script.
1341
- *
1342
- * @returns {object} The yargs command module.
1343
- */
1344
- function labelsCommand() {
1345
- return {
1346
- command: "labels <action>",
1347
- describe: "Issue-label registry checks",
1348
- builder: (y) =>
1349
- y
1350
- .positional("action", {
1351
- choices: ["check"],
1352
- describe: "check: the registry and the documented table name the same labels",
1353
- })
1354
- .option("registry", {
1355
- type: "string",
1356
- default: ".github/labels.yml",
1357
- describe: "The machine registry synced to GitHub",
1358
- })
1359
- .option("doc", {
1360
- type: "string",
1361
- default: ".github/ISSUE_REPORTING.md",
1362
- describe: "The documented reference whose §3 table lists the same labels",
1363
- }),
1364
- handler: handler(async (args) => labelsCheck(args)),
1365
- };
1366
- }
1367
-
1368
- /**
1369
- * Read both faces of the registry and report where they disagree.
1370
- *
1371
- * Findings are reported against the file each belongs to, so a missing row is
1372
- * located in the documentation and a missing entry in the registry, rather than
1373
- * both being attributed to whichever file was read first.
1374
- *
1375
- * @param {object} args - Parsed CLI arguments.
1376
- */
1377
- function labelsCheck(args) {
1378
- for (const target of [args.registry, args.doc]) {
1379
- if (!fs.existsSync(target)) die(`labels check: ${target} does not exist.`);
1380
- }
1381
- const registryText = fs.readFileSync(args.registry, "utf8");
1382
- const docText = fs.readFileSync(args.doc, "utf8");
1383
-
1384
- const { registry, doc, count } = checkLabelRegistry({
1385
- registryText,
1386
- docText,
1387
- docPath: args.doc,
1388
- });
1389
-
1390
- const errors =
1391
- reportFindings(registry, { file: args.registry }) + reportFindings(doc, { file: args.doc });
1392
- if (errors) {
1393
- console.error(
1394
- `\nThe registry and ${args.doc} §3 are edited together — ` +
1395
- `change one and change the other.`,
1396
- );
1397
- process.exit(1);
1398
- }
1399
- console.log(`package-build: registry and §3 agree (${count} labels).`);
1400
- }
1401
-
1402
1330
  /**
1403
1331
  * `lang <action>` — the three localization guards.
1404
1332
  *
@@ -1641,7 +1569,7 @@ function cdnPublishCommand() {
1641
1569
  .option("set", {
1642
1570
  type: "string",
1643
1571
  demandOption: true,
1644
- describe: "The set file's name under the package's CDN root, e.g. ai-art.json",
1572
+ describe: "The set file's name under the package's CDN root, e.g. alt-art.json",
1645
1573
  })
1646
1574
  .option("name", {
1647
1575
  type: "string",
@@ -1972,7 +1900,6 @@ registerContentCommands(
1972
1900
  .command(typesCommand())
1973
1901
  .command(schemaCommand())
1974
1902
  .command(langCommand())
1975
- .command(labelsCommand())
1976
1903
  .command(bumpCommand())
1977
1904
  .command(yamlCommand())
1978
1905
  .command(changelogCommand())
package/docs/api.md CHANGED
@@ -1052,6 +1052,15 @@ The charset authored content is held to, so a book can choose its face. The pack
1052
1052
  | `checkText` | `function checkText(text, file)` | {Array<{file: string, line: number, column: number, severity: "warning", message: string}>} What is wrong, in file order. | Check one file's text against the charset and the normalization rule. |
1053
1053
  | `lintContentCharset` | `function lintContentCharset(contentBase,` | `{{findings: Array<{file: string, line: number, column: number, severity: "warning", message: string}>, files: number}}` The findings, and how many files produced them. | Walk a content tree and check every authored file in it. |
1054
1054
 
1055
+ ### `engine.pathCharset`
1056
+
1057
+ The characters a content or picture path may hold: printable 7-bit ASCII, U+0020 to U+007E. A note's identity comes from its `shortcode`, so a path's spelling changes no address; a non-ASCII name changes only how the file travels between filesystems, shells, archives and CDN keys.
1058
+
1059
+ | Export | Signature | Returns | Use it when |
1060
+ | ------------------------- | --------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
1061
+ | `offendingPathCharacters` | `function offendingPathCharacters(text)` | {string[]} The distinct characters outside U+0020 to U+007E, in order of first appearance. | Whether a path, or one component of it, is clean. |
1062
+ | `lintPathCharset` | `lintPathCharset({ contentBase, assetsBase, skipDirectories })` | `{{findings: Array<{file: string, severity: "error", message: string}>, files: number}}` The findings, and how many entries were checked. | Walk the content tree and the picture roots and report every entry named outside printable ASCII. |
1063
+
1055
1064
  ### `engine.contentIcons`
1056
1065
 
1057
1066
  Naming an interface icon in a note, without drawing it there. The user guide describes Foundry's interface, and it did so by pasting Unicode lookalikes of the icons the sheets actually draw: `☆` for the improve flag, `✎` for the formula editor, `◆` in the success-value table, `★★★` for mastery. The system renders every one of those with **Font Awesome** — a `fa-regular fa-star`, a `fa-solid fa-pen-to-square` — so the note and the screen it describes were drawing different pictures, and drifting apart with every sheet change.
@@ -309,13 +309,18 @@ A column's heading is its SQL alias, so `AS "Name"` is what a reader sees. A cel
309
309
 
310
310
  ### What a query can read
311
311
 
312
- | Relation | What it holds |
313
- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
314
- | `notes` | Every note except a stub. This is the one to use unless a stub is wanted. |
315
- | `entries` | Every note, stubs included. |
316
- | `events` | One row per event a `lore`, `place` or `affiliation` note writes in `data.events`, with `whenSort` to order them by date. |
317
- | `market` | The market scale as three columns — `value`, `name`, `trade` — so a table can print `village` beside the number a note wrote. |
318
- | `<package>.notes` | The same relations — `notes`, `entries` and `events` — for each package this one declares a dependency on, in a schema named for it. |
312
+ | Relation | What it holds |
313
+ | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
314
+ | `notes` | Every note except a stub. This is the one to use unless a stub is wanted. |
315
+ | `entries` | Every note, stubs included. |
316
+ | `events` | One row per event a `lore`, `place` or `affiliation` note writes in `data.events`, with `whenSort` to order them by date. |
317
+ | `words` | One row per word a language note writes in `data.words`, with that note as its `language`, its `position` and what it is built from. |
318
+ | `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. |
319
+ | `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. |
320
+ | `dictionary` | One row per word per language it is listed under, inherited words included, with its `depth`. See [dictionaries and glossaries](#dictionaries-and-glossaries). |
321
+ | `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). |
322
+ | `market` | The market scale as three columns — `value`, `name`, `trade` — so a table can print `village` beside the number a note wrote. |
323
+ | `<package>.notes` | The same relations — `notes`, `entries`, `events`, `words`, `glossary`, `language_lineage`, `dictionary` and `terms` — for each package this one declares a dependency on, in a schema named for it. |
319
324
 
320
325
  Nested frontmatter is addressed exactly as a note writes it: `name.full`, `sohl.weight`, `data.born`. Every Address is a string — the full canonical Address, as in `list_contains(data.parents, 'thalorna-note-place-xerathia')` — whichever package published it. Every note carries a computed `state` column of `full`, `draft` or `stub`. Notes tagged `gm` are absent from the relations on public surfaces.
321
326
 
@@ -414,12 +419,13 @@ An event with no `where.locus` keeps its row with an empty **Where** cell.
414
419
 
415
420
  ### Fence attributes
416
421
 
417
- A braced list may follow the language on the opening fence. Two attributes are accepted, and anything else is reported by name.
422
+ A braced list may follow the language on the opening fence. Three attributes are accepted, and anything else is reported by name.
418
423
 
419
- | Attribute | Value | Default | What it does |
420
- | --------------- | --------------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
421
- | `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. |
422
- | `section-level` | an integer from 1 through 6 | `2` | The heading level given to each group a `_section` column creates. |
424
+ | Attribute | Value | Default | What it does |
425
+ | --------------- | ----------------------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
426
+ | `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. |
427
+ | `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. |
428
+ | `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). |
423
429
 
424
430
  ````markdown
425
431
  ```sql {allow-empty=true section-level=3}
@@ -445,6 +451,65 @@ FROM notes WHERE type IN ('lore', 'place') ORDER BY type, name.full
445
451
 
446
452
  See [SQL details](../reference/format-details.md#content-tables) for query options.
447
453
 
454
+ ### Dictionaries and glossaries
455
+
456
+ 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 and the notes it is cited from. **`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.
457
+
458
+ **The query decides what appears**, and any note may carry either fence. These are the common ones:
459
+
460
+ | What | Layout | Query |
461
+ | ----------------------------------------- | ------------ | --------------------------------------------------------------------------------- |
462
+ | A language's dictionary | `dictionary` | `SELECT * FROM dictionary WHERE language = '<the language's Address>'` |
463
+ | A language's common words, on any page | `dictionary` | `SELECT * FROM dictionary WHERE language = '<the language's Address>' AND common` |
464
+ | A culture's glossary | `glossary` | `SELECT * FROM terms WHERE list_contains(cultures, '<the culture's Address>')` |
465
+ | An affiliation's glossary | `glossary` | `SELECT * FROM terms WHERE owner = '<the affiliation's Address>'` |
466
+ | The setting's _Dictionary of the Tongues_ | `dictionary` | `SELECT * EXCLUDE (common) FROM dictionary WHERE depth = 0` |
467
+ | The setting's _Glossary_ | `glossary` | `SELECT * EXCLUDE (common) FROM terms` |
468
+
469
+ 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.
470
+
471
+ The most common Vylari words, on the Vylarian culture's page:
472
+
473
+ ````markdown
474
+ ## Words Everyone Knows {#common-words}
475
+
476
+ ```sql {layout=dictionary section-level=3}
477
+ SELECT * FROM dictionary WHERE language = 'thalorna-note-skill-vylarilng' AND common
478
+ ```
479
+ ````
480
+
481
+ A culture's glossary:
482
+
483
+ ````markdown
484
+ ## Glossary {#glossary}
485
+
486
+ ```sql {layout=glossary section-level=3}
487
+ SELECT * FROM terms WHERE list_contains(cultures, 'thalorna-note-lore-provenzianclt')
488
+ ```
489
+ ````
490
+
491
+ Either layout prints, in order:
492
+
493
+ - **a key**, once, 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;
494
+ - **a heading per letter**, at `section-level`, accents taken off and apostrophes skipped, so `'ostal` files under O and `Écuyer` under E;
495
+ - **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.
496
+
497
+ A word also carries its class beside the tag and its etymology in brackets:
498
+
499
+ ```markdown
500
+ • **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]]
501
+ ```
502
+
503
+ 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:
504
+
505
+ ```markdown
506
+ **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]]
507
+ ```
508
+
509
+ 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.
510
+
511
+ 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.
512
+
448
513
  ## Page lists
449
514
 
450
515
  A fence marked `pagelist` is replaced by a list of the pages carrying a tag. Where a content table answers "what do these notes say?", a page list answers "which pages belong to this group?" — and the group is expressed by intent rather than enumerated, so a page joins it by being tagged and leaves it by being untagged.
package/docs/commands.md CHANGED
@@ -789,50 +789,6 @@ package-build: 1 template(s) fully localized and compiling.
789
789
  `package-build lang check`, `package-build lang coverage`,
790
790
  [Diagnostics](diagnostics.md), [Configuration](configuration.md).
791
791
 
792
- ### `package-build labels check`
793
-
794
- **NAME**
795
-
796
- Compare the machine label registry against the documented reference table
797
- (`check` is the only action).
798
-
799
- **SYNOPSIS**
800
-
801
- ```
802
- package-build labels check [--registry <path>] [--doc <path>]
803
- ```
804
-
805
- **DESCRIPTION**
806
-
807
- Compares the machine label registry synced to GitHub against the documented
808
- reference table, and reports where they disagree — a label the registry has
809
- that the table does not, or the reverse. Neither derives from the other, so
810
- nothing else notices when they drift. Reads both files named below; writes
811
- nothing.
812
-
813
- **OPTIONS**
814
-
815
- | Option | Type | Default | Description |
816
- | ------------ | ------ | ---------------------------- | -------------------------------------------------------------- |
817
- | `--registry` | string | `.github/labels.yml` | The machine registry synced to GitHub. |
818
- | `--doc` | string | `.github/ISSUE_REPORTING.md` | The documented reference whose §3 table lists the same labels. |
819
-
820
- **EXIT STATUS**
821
-
822
- 1 if either file does not exist. 1 if the registry and the table disagree on
823
- any label. Otherwise 0.
824
-
825
- **EXAMPLES**
826
-
827
- ```
828
- $ package-build labels check --registry .github/labels.yml --doc .github/ISSUE_REPORTING.md
829
- package-build: registry and §3 agree (11 labels).
830
- ```
831
-
832
- **SEE ALSO**
833
-
834
- None.
835
-
836
792
  ### `package-build bump [packages..]`
837
793
 
838
794
  **NAME**
@@ -1348,7 +1304,7 @@ A workflow calls the command the same way, passing the four values as named
1348
1304
  secrets in the step's environment:
1349
1305
 
1350
1306
  ```yaml
1351
- - run: npx package-build cdn publish --set ai-art.json --name "Thalorna — illustrated"
1307
+ - run: npx package-build cdn publish --set alt-art.json --name "Thalorna — illustrated"
1352
1308
  env:
1353
1309
  CDN_ENDPOINT: ${{ secrets.CDN_ENDPOINT }}
1354
1310
  CDN_BUCKET: ${{ secrets.CDN_BUCKET }}
@@ -1374,12 +1330,12 @@ it. Otherwise 0.
1374
1330
  **EXAMPLES**
1375
1331
 
1376
1332
  ```
1377
- $ package-build cdn publish --set ai-art.json --name "Thalorna — illustrated" --dry-run
1333
+ $ package-build cdn publish --set alt-art.json --name "Thalorna — illustrated" --dry-run
1378
1334
  Dry run: comparing 582 pictures against https://cdn.heroiclands.org, read-only.
1379
1335
  would upload thalorna/icons/anvilbdg.svg (969 bytes)
1380
1336
  …
1381
- would upload thalorna/ai-art.json (582 assets)
1382
- Would upload 582, skipped 0, failed 0; https://cdn.heroiclands.org/thalorna/ai-art.json lists 582 left-out pictures (would change).
1337
+ would upload thalorna/alt-art.json (582 assets)
1338
+ Would upload 582, skipped 0, failed 0; https://cdn.heroiclands.org/thalorna/alt-art.json lists 582 left-out pictures (would change).
1383
1339
 
1384
1340
  $ package-build cdn publish --set default.json --name Shipped
1385
1341
  package-build: `--set default.json` is refused: `default.json` is the shipped set's name, the fallback every other set is read against. Name the set for its art
@@ -91,6 +91,18 @@ 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
+ ## File and folder names
95
+
96
+ 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.
97
+
98
+ `package-build lint` reports each offending name as an error located at the path, with no line or column, and names the characters by code point:
99
+
100
+ ```text
101
+ assets/content/Misc_Gear/Scribe/A_Sílhari_Manual.md: error: the path holds U+00ED (í), outside printable ASCII; rename the file using ASCII only
102
+ ```
103
+
104
+ The fix is to rename the file, `A_Sílhari_Manual.md` to `A_Silhari_Manual.md`. A note's identity comes from its `shortcode`, and a picture's from its basename without the extension, so the rename changes no address and breaks no link. The accented spelling belongs in the note's `name`, where it displays as written.
105
+
94
106
  ## A complete minimal note
95
107
 
96
108
  Place this at `assets/content/Places/Harbor.md`:
@@ -129,4 +141,8 @@ for a complete example, parent ordering and findings.
129
141
 
130
142
  Places, affiliations, lore and beings may name their culture in `data.culture`, and a `doc` of `subType: settingguide` must. A place or affiliation that names none inherits the culture of its parents. See [culture in the index](reference/format-details.md#culture-in-the-index).
131
143
 
144
+ 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
+
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, with its source, the pieces it is built from and the notes it is attested in, beside `data.classModifiers`, `data.fields` and `data.affixes`. 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`, `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`. See [a language's words](reference/format-details.md#a-languages-words), [glossary terms](reference/format-details.md#glossary-terms) and [dictionaries and glossaries](reference/format-details.md#dictionaries-and-glossaries).
147
+
132
148
  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).
@@ -284,7 +284,7 @@ when it reports at least one `error`-severity diagnostic. A `warning` alone
284
284
  never fails a run.** `bin/report.mjs`'s `reportFindings` draws this line once
285
285
  for the commands built from the package's pure rule functions (`package-build
286
286
  lint`, `package-build lang check`, `package-build lang coverage`,
287
- `package-build yaml`, `package-build labels check`): it emits every finding
287
+ `package-build yaml`): it emits every finding
288
288
  and returns only the count whose `severity` is `error`, and the command sets
289
289
  `process.exitCode = 1` exactly when that count is nonzero. Commands whose
290
290
  findings are hand-assembled — `package-build links`, `package-build
@@ -335,7 +335,6 @@ over one would teach people to stop reading the report.
335
335
  | `package-build lang check` | any localization finding is `error`-severity |
336
336
  | `package-build lang coverage` | a referenced key is missing (`error`-severity); an unreferenced key is always advisory and never fails the run |
337
337
  | `package-build yaml [paths..]` | any YAML finding is `error`-severity |
338
- | `package-build labels check` | the label registry and the documented table disagree on any label |
339
338
 
340
339
  Every command also fails on a thrown error unrelated to a
341
340
  diagnostic — a missing configuration, an unreadable file, a network failure —
@@ -861,7 +861,7 @@ everything that reads the tree runs before everything that writes the stage.
861
861
 
862
862
  [`project-setup.md`](../project-setup.md) turns this into npm scripts, and covers
863
863
  everything a repository carries beyond the build itself: the git hooks, the
864
- changeset directory, the label registry, and what each script in the chain is
864
+ changeset directory, and what each script in the chain is
865
865
  for.
866
866
 
867
867
  ## What comes next
@@ -163,6 +163,95 @@ frontmatter remains queryable. An Item or Actor note can have both a readable
163
163
  Consumers can use the index's own addresses and file paths rather than
164
164
  reconstructing them from filenames.
165
165
 
166
+ ## Set words as a dictionary and terms as a glossary
167
+
168
+ A language's words and a people's terms are each one `sql` fence, and nothing
169
+ is copied into the note: the words come from the language notes' `data.words`,
170
+ the terms from the `data.glossary` entries of cultures and affiliations and
171
+ from the affiliations' ranks and offices, and a section changes when they do.
172
+ `layout=dictionary` sets rows of the `dictionary` relation and
173
+ `layout=glossary` rows of the `terms` relation. The query decides what
174
+ appears, so any note may carry either.
175
+
176
+ A language note lists its words, with the words it inherits from its parent
177
+ language:
178
+
179
+ ````markdown
180
+ # Dictionary {#dictionary}
181
+
182
+ ```sql {layout=dictionary section-level=2}
183
+ SELECT * FROM dictionary WHERE language = 'thalorna-note-skill-provnzlng'
184
+ ```
185
+ ````
186
+
187
+ A culture note lists the words of its tongue a player is expected to know, and
188
+ its glossary: its own terms and the ranks, offices and terms of every
189
+ affiliation that reaches it.
190
+
191
+ ````markdown
192
+ # Words Everyone Knows {#common-words}
193
+
194
+ ```sql {layout=dictionary section-level=2}
195
+ SELECT * FROM dictionary WHERE language = 'thalorna-note-skill-provnzlng' AND common
196
+ ```
197
+
198
+ # Glossary {#glossary}
199
+
200
+ ```sql {layout=glossary section-level=2}
201
+ SELECT * FROM terms WHERE list_contains(cultures, 'thalorna-note-lore-provenzianclt')
202
+ ```
203
+ ````
204
+
205
+ The glossary renders as a key, a heading per letter and one paragraph per
206
+ term, each sense tagged with the body or culture it comes from and each term
207
+ in another tongue tagged with it:
208
+
209
+ ```markdown
210
+ **Key:** _Lit._ Liturgic Language; • a common word, which a player is expected to know by heart.
211
+
212
+ ## K
213
+
214
+ **Knight-Captain** **1** (_Kingdom of Provenzia_) A knight who leads a company. **2** (_Kingdom of Provenzia_) Commands the guard in the field. — [[thalorna-note-affiliation-kingdom|Kingdom of Provenzia]]
215
+
216
+ ## O
217
+
218
+ **Ordained Priest** _Lit._ (_Church of the Rite_) Ordained to serve a parish. — [[thalorna-note-affiliation-church|Church of the Rite]]
219
+ ```
220
+
221
+ The culture shapes both through its own `data.glossary`.
222
+ `{name: solmère, common: true, pronunciation: sol-MEH-reh}` marks a word of its
223
+ tongue common, so the first fence lists it, and gives it a respelling;
224
+ `{name: Subject, common: true}` marks a rank and lets in a title of one word,
225
+ which no glossary holds unless an entry names it; and
226
+ `{name: blood-field, definition: …}` is a term of the culture's own.
227
+
228
+ An affiliation lists its own terms with `WHERE owner = '<its Address>'`. The
229
+ setting's two pages list everything, each word once and each term under every
230
+ owner that holds it:
231
+
232
+ ````markdown
233
+ ```sql {layout=dictionary}
234
+ SELECT * EXCLUDE (common) FROM dictionary WHERE depth = 0
235
+ ```
236
+ ````
237
+
238
+ ````markdown
239
+ ```sql {layout=glossary}
240
+ SELECT * EXCLUDE (common) FROM terms
241
+ ```
242
+ ````
243
+
244
+ `package-build content-index` also writes both relations beside the index, for
245
+ reading with `jq`:
246
+
247
+ ```bash
248
+ jq -r 'select(.cultures | index("thalorna-note-lore-provenzianclt")) | .name' \
249
+ build/content-index/thalorna-terms.jsonl
250
+ ```
251
+
252
+ The [format reference](../reference/format-details.md#dictionaries-and-glossaries)
253
+ gives every column and every rule.
254
+
166
255
  ## Resolve another package
167
256
 
168
257
  In a Foundry package, declare a content-bearing dependency under
@@ -139,13 +139,13 @@ reaches a world from there as a named asset set. `package-build cdn publish`
139
139
  is the one command that writes there:
140
140
 
141
141
  ```bash
142
- package-build cdn publish --set ai-art.json --name "Thalorna — illustrated" --dry-run
143
- package-build cdn publish --set ai-art.json --name "Thalorna — illustrated"
142
+ package-build cdn publish --set alt-art.json --name "Thalorna — illustrated" --dry-run
143
+ package-build cdn publish --set alt-art.json --name "Thalorna — illustrated"
144
144
  ```
145
145
 
146
146
  It uploads every icon and image flat, transformed as the stage transforms
147
147
  them, skips what the CDN already holds at the same size, and writes
148
- `<site.assets>/<contentPackage>/ai-art.json` listing the left-out pictures.
148
+ `<site.assets>/<contentPackage>/alt-art.json` listing the left-out pictures.
149
149
  Run it after the pictures or their provenance change; it is not part of a
150
150
  build or a release.
151
151