@heroiclands/package-build 25.1.3 → 25.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/assets/styles/content-base.css +0 -9
  3. package/bin/content-commands.mjs +22 -16
  4. package/bin/package-build.mjs +24 -25
  5. package/content-config.mjs +7 -34
  6. package/docs/api.md +38 -21
  7. package/docs/authoring/first-note.md +3 -3
  8. package/docs/authoring/frontmatter.md +30 -2
  9. package/docs/authoring/links-and-markup.md +92 -31
  10. package/docs/commands.md +103 -59
  11. package/docs/configuration.md +10 -45
  12. package/docs/content-format.md +7 -5
  13. package/docs/diagnostics.md +75 -0
  14. package/docs/guides/book.md +3 -3
  15. package/docs/guides/module-project.md +1 -1
  16. package/docs/guides/reference-pages.md +7 -3
  17. package/docs/project-setup.md +3 -0
  18. package/docs/reference/format-details.md +299 -185
  19. package/docs/reference/note-types.md +16 -17
  20. package/engine/changelog-group.mjs +84 -173
  21. package/engine/changelog-labels.mjs +73 -0
  22. package/engine/changelog-lint.mjs +168 -44
  23. package/engine/content-expand.mjs +58 -38
  24. package/engine/content-format.yaml +0 -2
  25. package/engine/content-index.mjs +49 -23
  26. package/engine/content-links.mjs +4 -0
  27. package/engine/content-lint.mjs +2 -3
  28. package/engine/content-render.mjs +6 -2
  29. package/engine/culture-parents.mjs +1 -1
  30. package/engine/culture-reach.mjs +10 -1
  31. package/engine/diagnostics.mjs +3 -2
  32. package/engine/dictionary-rows.mjs +3 -1
  33. package/engine/first-use.mjs +301 -87
  34. package/engine/frontmatter-lint.mjs +40 -14
  35. package/engine/glossary-entries.mjs +5 -5
  36. package/engine/glossary-language.mjs +21 -44
  37. package/engine/homepage.mjs +0 -4
  38. package/engine/index-records.mjs +3 -1
  39. package/engine/infobox.mjs +6 -3
  40. package/engine/language-names.mjs +3 -2
  41. package/engine/language-rows.mjs +6 -7
  42. package/engine/lexical-sections.mjs +3 -1
  43. package/engine/literature-notes.mjs +4 -76
  44. package/engine/metadata-index.mjs +15 -8
  45. package/engine/name-forms.mjs +99 -0
  46. package/engine/name-refs.mjs +143 -0
  47. package/engine/name-rows.mjs +11 -1
  48. package/engine/names.mjs +27 -22
  49. package/engine/note-addresses.mjs +0 -3
  50. package/engine/note-frontmatter.mjs +3 -5
  51. package/engine/note-language.mjs +348 -0
  52. package/engine/note-vocabulary.mjs +13 -11
  53. package/engine/pdf-index.mjs +3 -2
  54. package/engine/pdf-render.mjs +2 -4
  55. package/engine/settlement-tables.mjs +4 -3
  56. package/engine/site-build.mjs +2 -7
  57. package/engine/site-search.mjs +3 -5
  58. package/engine/site-theme.mjs +1 -1
  59. package/engine/sql-tables.mjs +29 -14
  60. package/engine/wikilink-syntax.mjs +2 -0
  61. package/engine/word-citations.mjs +3 -3
  62. package/engine/word-rows.mjs +3 -1
  63. package/engine/word-terms.mjs +2 -2
  64. package/package.json +1 -1
  65. package/site-theme/static/css/style.css +0 -4
  66. package/types/engine/changelog-group.d.mts +6 -10
  67. package/types/engine/changelog-labels.d.mts +10 -0
  68. package/types/engine/changelog-lint.d.mts +11 -6
  69. package/types/engine/content-index.d.mts +16 -16
  70. package/types/engine/diagnostics.d.mts +1 -1
  71. package/types/engine/first-use.d.mts +97 -1
  72. package/types/engine/frontmatter-lint.d.mts +9 -2
  73. package/types/engine/glossary-language.d.mts +13 -6
  74. package/types/engine/homepage.d.mts +0 -3
  75. package/types/engine/literature-notes.d.mts +2 -27
  76. package/types/engine/name-forms.d.mts +61 -0
  77. package/types/engine/name-refs.d.mts +51 -0
  78. package/types/engine/name-rows.d.mts +4 -0
  79. package/types/engine/names.d.mts +12 -92
  80. package/types/engine/note-frontmatter.d.mts +1 -4
  81. package/types/engine/note-language.d.mts +47 -0
  82. package/types/engine/settlement-tables.d.mts +1 -1
  83. package/types/engine/site-build.d.mts +2 -6
package/CHANGELOG.md CHANGED
@@ -1,5 +1,34 @@
1
1
  # @heroiclands/package-build
2
2
 
3
+ ## 25.2.0
4
+
5
+ **Content and notes**
6
+
7
+ - A note lists its other names under the code of their language, beside the English `full`: `name: {full: Johnathan Lewis, en: [John], vyla: [juan juis]}`.
8
+ - A note states its language as `data.lang`, a code `languages:` declares or `en`; a note that states none takes its culture's, then its parents', then `en`.
9
+ - A culture that states no language, and inherits none from a parent culture, is an error wherever a note names it.
10
+ - Names match whatever their case, so `[arden vale]{.name}` names the note _Arden Vale_.
11
+ - A `.name` span can carry `ref=` to tie each spelling of a person, place or thing to its note without linking it: `[Tom]{.name ref=being-tomr}`.
12
+ - A `ref` may be a page-local key such as `ref="#jane"` for a subject with no note, and a `ref` that matches no note is an error.
13
+
14
+ **Rendering and sites**
15
+
16
+ - The first use of each name in a page is bold on the website, in Foundry journals and in the book, and its later uses are plain text.
17
+ - Every spelling tied by one `ref`, every link to one note and every case of one label count as one name.
18
+ - A wikilink counts as a name when it carries `.name`, such as `[[place-harbor|the harbor]]{.name}`; names in headings, tables, captions and other-language runs take no bold.
19
+
20
+ **Builds and commands**
21
+
22
+ - `changelog check` now refuses a changeset that is not labelled blocks of bullets: a lead paragraph, an unlabelled bullet, prose between blocks and a label that is not declared are errors.
23
+ - A label must be in `labels` or be `Miscellaneous`, which every repository has and never declares; with no list declared, only `Miscellaneous` is accepted.
24
+ - `changelog check --release` holds a Version Packages section to the same shape.
25
+ - `changelog group` removes the `### … Changes` headings, merges blocks that share a label, orders them as `labels` declares and lists `Miscellaneous` last.
26
+
27
+ **Upgrading**
28
+
29
+ - Declare `labels` in the changelog options of `.changeset/config.json`, and rewrite pending changesets as labelled blocks of bullets before upgrading.
30
+ - Move each `aliases` entry under its language's code, or `en`, and write `data.lang: <code>` in place of `data.language: skill-…`; both old keys are refused.
31
+
3
32
  ## 25.1.3
4
33
 
5
34
  **Spelling checks**
@@ -584,15 +584,6 @@
584
584
  outline-offset: 2px;
585
585
  }
586
586
 
587
- /*
588
- * A name the text marks is bold, as on the site, in the preview and in the book.
589
- * Foundry's interface and a system's sheets use `.name` for their own elements,
590
- * which the content roots leave untouched.
591
- */
592
- :is(.journal-entry-page, .journal-entry-content, .editor-content, .prosemirror) .name {
593
- font-weight: var(--pb-name-weight, 700);
594
- }
595
-
596
587
  /*
597
588
  * A wikilink whose target no package publishes keeps its text and is marked, so
598
589
  * the sentence still reads and a reader can tell a link was meant. Each value
@@ -139,6 +139,7 @@ import { stagePackageBuildAssets } from "../engine/build-assets.mjs";
139
139
  import { DEPLOYED_ASSET_TYPES } from "../engine/asset-types.mjs";
140
140
  import { createContentSession, CONTENT_TARGETS } from "../engine/content-session.mjs";
141
141
  import { createFirstUseChecker } from "../engine/first-use.mjs";
142
+ import { createNameRefChecker } from "../engine/name-refs.mjs";
142
143
  import { createNameCatalog, createNamesReport, namesReportFinding } from "../engine/names.mjs";
143
144
  // The one place a link finding is worded, shared with both builds.
144
145
  import { linkFindingMessage } from "../engine/wikilink-syntax.mjs";
@@ -1037,6 +1038,11 @@ function lintCommand() {
1037
1038
  syntax: session.syntax,
1038
1039
  catalog,
1039
1040
  });
1041
+ const nameRefChecker = createNameRefChecker({
1042
+ catalog,
1043
+ parsed: session.parsed,
1044
+ syntax: session.syntax,
1045
+ });
1040
1046
  const names = namesReportFinding(
1041
1047
  createNamesReport({ package: config.contentPackage, records, catalog }),
1042
1048
  { contentBase: root },
@@ -1075,6 +1081,8 @@ function lintCommand() {
1075
1081
  }
1076
1082
  const frontmatter = lintFrontmatter(index, {
1077
1083
  currentDate: config.currentDate,
1084
+ // The codes a note's `name` lists its forms under.
1085
+ languages: Object.keys(config.languages ?? {}),
1078
1086
  schemas: { ...ENGINE_NOTE_SCHEMAS, ...NOTE_SCHEMAS },
1079
1087
  // The closed frontmatter regions. Passed in rather
1080
1088
  // than reached for, so the linter stays a checker of
@@ -1300,6 +1308,7 @@ function lintCommand() {
1300
1308
  ...(runs.encodingFindings ?? []),
1301
1309
  ...languageNoteFindings({ ...config, configFile: packConfigPath() }, records),
1302
1310
  ...index.notes.flatMap(firstUseChecker),
1311
+ ...index.notes.flatMap(nameRefChecker),
1303
1312
  ...(names ? [names] : []),
1304
1313
  ...addresses.findings,
1305
1314
  ...frontmatter.findings,
@@ -3400,16 +3409,16 @@ function newNpc(config, argv) {
3400
3409
  throw new Error(`${key}'s appearance profile has ${profileErrors.length} problem(s)`);
3401
3410
  }
3402
3411
 
3403
- const tongue = culture.data?.language;
3404
- if (tongue === undefined || tongue === null)
3412
+ const code = hit.lang;
3413
+ const tongue = typeof code === "string" ? config.languages?.[code]?.note : undefined;
3414
+ if (tongue === undefined)
3405
3415
  throw noteError(
3406
3416
  cultureNote,
3407
- ["data"],
3408
- `${key} states no \`data.language\`, so \`npc new\` has no name lists to draw ` +
3409
- "from; name the language skill note its people speak",
3417
+ Object.hasOwn(culture.data ?? {}, "lang") ? ["data", "lang"] : ["data"],
3418
+ `${key} is in ${code ?? "no language"}, which \`languages:\` names no language ` +
3419
+ "note for, so `npc new` has no name lists to draw from; state the culture's " +
3420
+ "`data.lang` and its language's `note`",
3410
3421
  );
3411
- const languageSource = cultureFieldSource(records, key, "language") ?? hit;
3412
- const languageSourceNote = located(languageSource);
3413
3422
  const tuple = parseAddress(tongue, {
3414
3423
  package: index.contentPackage,
3415
3424
  system: "note",
@@ -3420,18 +3429,15 @@ function newNpc(config, argv) {
3420
3429
  const languageKey = tuple.reason ? undefined : renderAddress(tuple);
3421
3430
  const languageHit = languageKey ? index.addressHit(languageKey) : undefined;
3422
3431
  if (!languageHit)
3423
- throw noteError(
3424
- languageSourceNote,
3425
- ["data", "language"],
3426
- `${key}'s \`data.language\` names no note in this package or its dependencies`,
3432
+ throw new Error(
3433
+ `\`languages.${code}.note\` ${tongue}, ${key}'s language, names no note in this ` +
3434
+ "package or its dependencies",
3427
3435
  );
3428
3436
  const language = languageHit.fm ?? languageHit;
3429
3437
  if (String(language.type ?? languageHit.type) !== "skill" || language.subType !== "language")
3430
- throw noteError(
3431
- languageSourceNote,
3432
- ["data", "language"],
3433
- `${key}'s \`data.language\` ${languageKey} is not a language: it names a ` +
3434
- `${language.type ?? languageHit.type ?? "note"} of subType ` +
3438
+ throw new Error(
3439
+ `\`languages.${code}.note\` ${languageKey}, ${key}'s language, is not a language: ` +
3440
+ `it names a ${language.type ?? languageHit.type ?? "note"} of subType ` +
3435
3441
  `${language.subType ?? "none"}`,
3436
3442
  );
3437
3443
  const languageNote = located(languageHit);
@@ -92,7 +92,7 @@ import { emitDiagnostic, formatDiagnostic } from "../engine/diagnostics.mjs";
92
92
  import { writeSiteRoot } from "../engine/site-root.mjs";
93
93
  import { DEPLOY_ROOT } from "../engine/site-config.mjs";
94
94
  import { compilesFoundryDocuments, siteBase } from "../content-config.mjs";
95
- import { loadPackConfig, packConfigPath, resolveConfigFile } from "../engine/pack-config.mjs";
95
+ import { loadPackConfig, packConfigPath } from "../engine/pack-config.mjs";
96
96
  import { cleanBuildArtifacts, resetStage } from "../stage.mjs";
97
97
  import {
98
98
  CDN_ENV,
@@ -115,6 +115,7 @@ import { validateLangSource } from "../lang.mjs";
115
115
  import { lintYaml } from "../engine/yaml-lint.mjs";
116
116
  import { lintChangesetText, lintReleaseText } from "../engine/changelog-lint.mjs";
117
117
  import { groupChangelogText } from "../engine/changelog-group.mjs";
118
+ import { readChangelogLabels } from "../engine/changelog-labels.mjs";
118
119
  import { bumpDependencies } from "../engine/dependency-bump.mjs";
119
120
  import {
120
121
  analyzeCoverage,
@@ -1066,35 +1067,33 @@ function yamlCommand() {
1066
1067
  }
1067
1068
 
1068
1069
  /**
1069
- * `changelog check` / `changelog group` — release-prose checks, and folding
1070
- * a release's changeset blocks together by their bold label.
1071
- *
1072
- * `check` answers one question a changeset otherwise goes unheld to — who
1073
- * notices, and what do they see — so a pull-request description pasted into
1074
- * one does not ship verbatim as a release note. Default reads every pending
1075
- * changeset; `--release` reads the first `## <version>` section of
1076
- * `CHANGELOG.md` instead, for the **Version Packages** branch a merge to
1077
- * `main` opens.
1078
- *
1079
- * `group` rewrites that same first `## <version>` section in place: every
1080
- * changeset in it writes its own `**Compendiums**` block, and nothing merges
1081
- * the three a release with three compendium fixes ends up with. It reads
1082
- * `changelog.labels` for the vocabulary and display order, when the
1083
- * repository declares one.
1070
+ * `changelog check` / `changelog group` — the changeset shape check, and the
1071
+ * final grouping of a release.
1072
+ *
1073
+ * `check` holds a changeset to the house shape — labelled blocks of
1074
+ * bullets, labels from `.changeset/config.json` or `Miscellaneous` — and to the
1075
+ * rules that keep a pull-request description out of a release note. Default
1076
+ * reads every pending changeset; `--release` reads the first `## <version>`
1077
+ * section of `CHANGELOG.md` instead, for the **Version Packages** branch a
1078
+ * merge to `main` opens.
1079
+ *
1080
+ * `group` rewrites that same first `## <version>` section in place: it removes
1081
+ * the `### … Changes` headings, merges blocks sharing a label and orders them
1082
+ * as the labels in `.changeset/config.json` declare, `Miscellaneous` last.
1084
1083
  *
1085
1084
  * @returns {object} The yargs command module.
1086
1085
  */
1087
1086
  function changelogCommand() {
1088
1087
  return {
1089
1088
  command: "changelog <action> [paths..]",
1090
- describe: "Release-prose checks, and folding a release's blocks by label",
1089
+ describe: "Check changesets against the house shape, and group a release's blocks by label",
1091
1090
  builder: (y) =>
1092
1091
  y
1093
1092
  .positional("action", {
1094
1093
  choices: ["check", "group"],
1095
1094
  describe:
1096
1095
  "check: lint pending changesets, or a release section. " +
1097
- "group: fold a release section's blocks together by label",
1096
+ "group: give the newest release its final shape",
1098
1097
  })
1099
1098
  .positional("paths", {
1100
1099
  describe:
@@ -1135,17 +1134,17 @@ function changelogFiles(args) {
1135
1134
  }
1136
1135
 
1137
1136
  /**
1138
- * `changelog.labels`, when this repository declares a configuration — `null`
1139
- * otherwise, so `check` and `group` both work in a tree with no
1140
- * `package-build.config.yaml` at all, exactly as `check` already does for
1141
- * pending changesets with no configuration to read.
1137
+ * The labels in `.changeset/config.json`, or `null` when it declares none.
1138
+ * An invalid declaration stops the command.
1142
1139
  *
1143
1140
  * @returns {readonly string[]|null}
1144
1141
  */
1145
1142
  function loadChangelogLabels() {
1146
- const found = resolveConfigFile();
1147
- if (!found.path) return null;
1148
- return loadPackConfig().changelog.labels;
1143
+ try {
1144
+ return readChangelogLabels();
1145
+ } catch (error) {
1146
+ return die(`changelog: ${error.message}`);
1147
+ }
1149
1148
  }
1150
1149
 
1151
1150
  /**
@@ -71,6 +71,7 @@ import { normalizeCurrentDate } from "./engine/current-date.mjs";
71
71
  // a cycle around a consumer's config file (see `engine/pack-config.mjs`).
72
72
  import { ADDRESS_SEGMENT_PATTERN, isAddressSegment } from "./engine/address-charset.mjs";
73
73
  import { isLanguageTag } from "./engine/language-tag.mjs";
74
+ import { NAME_STRING_KEYS } from "./engine/name-forms.mjs";
74
75
  import { ASSET_TYPE_NAMES, shippingLicensesProblem } from "./engine/asset-types.mjs";
75
76
  import { isReservedPackage } from "./engine/packages.mjs";
76
77
  import { EMPTY_ICON_REGISTRY, checkIconRegistry } from "./engine/content-icons.mjs";
@@ -764,12 +765,10 @@ const CONFIG_KEYS = [
764
765
  "requiresSystem",
765
766
  "packageBuild",
766
767
  "publish",
767
- "changelog",
768
768
  ];
769
769
  const SYSTEM_KEYS = ["manifest", "compatibility"];
770
770
  const COMPATIBILITY_KEYS = ["minimum", "verified"];
771
771
  const DOCS_KEYS = ["itemFields"];
772
- const CHANGELOG_KEYS = ["labels"];
773
772
  const SITE_KEYS = [
774
773
  "base",
775
774
  "title",
@@ -983,6 +982,12 @@ function normalizeLanguages(value) {
983
982
  "is not a name: a name is lowercase letters, digits and single hyphens, " +
984
983
  "starting with a letter",
985
984
  );
985
+ if (NAME_STRING_KEYS.includes(name))
986
+ fail(
987
+ where,
988
+ `is a \`name\` key that holds a single string (${NAME_STRING_KEYS.join(", ")}), ` +
989
+ "so a note's forms cannot be listed under it; choose another name",
990
+ );
986
991
  if (!isPlainObject(entry)) fail(where, "must be a mapping with `tag`");
987
992
  rejectUnknownKeys(entry, ["tag", "note"], `${where}.`);
988
993
  const { tag, note } = /** @type {Record<string, unknown>} */ (entry);
@@ -1627,37 +1632,6 @@ function normalizeDocs(value) {
1627
1632
  });
1628
1633
  }
1629
1634
 
1630
- /**
1631
- * `changelog.labels` — the vocabulary and display order `changelog check`
1632
- * and `changelog group` read a release block's bold label against.
1633
- *
1634
- * Optional: a repository that groups nothing declares none, and `group`
1635
- * falls back to ordering every label by first appearance. Declared, it is a
1636
- * list of non-empty strings with no duplicate — a repeated label could never
1637
- * be told apart from a genuine drift (`Compendiums` twice would silently
1638
- * swallow the `Character data` beside `Characters` the list exists to catch).
1639
- *
1640
- * @param {unknown} value - The configured value, or `undefined`.
1641
- * @returns {Readonly<{labels: readonly string[]|null}>}
1642
- */
1643
- function normalizeChangelog(value) {
1644
- if (value === undefined) return Object.freeze({ labels: null });
1645
- if (!isPlainObject(value)) fail("changelog", "must be a mapping");
1646
- const input = /** @type {Record<string, unknown>} */ (value);
1647
- rejectUnknownKeys(input, CHANGELOG_KEYS, "changelog.");
1648
- if (input.labels === undefined) return Object.freeze({ labels: null });
1649
- if (!Array.isArray(input.labels)) fail("changelog.labels", "must be an array");
1650
- const labels = input.labels.map((label, index) =>
1651
- requireNonEmptyString(label, `changelog.labels[${index}]`),
1652
- );
1653
- const seen = new Set();
1654
- for (const label of labels) {
1655
- if (seen.has(label)) fail("changelog.labels", `declares \`${label}\` more than once`);
1656
- seen.add(label);
1657
- }
1658
- return Object.freeze({ labels: Object.freeze(labels) });
1659
- }
1660
-
1661
1635
  /**
1662
1636
  * The asset host the website resolves a pathname against.
1663
1637
  *
@@ -3228,6 +3202,5 @@ export function defineConfig(config) {
3228
3202
  requiresSystem,
3229
3203
  packageBuild: normalizePackageBuild(input.packageBuild),
3230
3204
  publish: normalizePublish(input.publish),
3231
- changelog: normalizeChangelog(input.changelog),
3232
3205
  });
3233
3206
  }
package/docs/api.md CHANGED
@@ -989,24 +989,40 @@ content and asset scopes preserve their own filtering and order.
989
989
 
990
990
  Import from `@heroiclands/package-build/engine/first-use`.
991
991
 
992
- | Export | Signature | Returns | Use it when |
993
- | ----------------------- | ----------------------------------------------------------------------- | ------------------------ | ------------------------------------------------------------------------------ |
994
- | `createFirstUseChecker` | `createFirstUseChecker({ records, index, catalog?, parsed?, syntax? })` | `checker({ file, raw })` | Checking authored prose against the current corpus's name-marking conventions. |
992
+ | Export | Signature | Returns | Use it when |
993
+ | ----------------------- | ----------------------------------------------------------------------- | ------------------------ | ---------------------------------------------------------------------------------- |
994
+ | `nameUses` | `nameUses(markdown, vocabulary?)` | `NameUse[]` | Listing the uses of names a body's prose makes, each with the key it counts under. |
995
+ | `boldFirstUses` | `boldFirstUses(markdown, vocabulary?)` | `string` | Marking the first use of each key in a body's prose before a target renders it. |
996
+ | `firstUsePlugin` | `md.use(firstUsePlugin)` | — | Rendering the marks as strong text in a markdown-it parser. |
997
+ | `FIRST_USE_OPEN` | constant | `"\uFDD0"` | Recognizing where a first use opens in expanded markdown. |
998
+ | `FIRST_USE_CLOSE` | constant | `"\uFDD1"` | Recognizing where a first use closes in expanded markdown. |
999
+ | `createFirstUseChecker` | `createFirstUseChecker({ records, index, catalog?, parsed?, syntax? })` | `checker({ file, raw })` | Checking a note's marked names against the index. |
1000
+
1001
+ A `NameUse` is `{start, end, key, emphasised}`: the use's UTF-16 range in the
1002
+ markdown, the key it counts under, and whether it sits inside authored strong
1003
+ text. The key is a name wikilink's address, a span's `ref`, or the span's
1004
+ case-folded label with its `lang`. `vocabulary` is `{types, packages,
1005
+ noIndexPackages, contentPackage}`, which reads every spelling of an address as
1006
+ one note; without it an address is keyed as written. Only prose counts:
1007
+ headings, tables, captions, raw HTML, code, verse and runs that state `lang`,
1008
+ `font` or `enc` neither take the bold nor count. `boldFirstUses` wraps each
1009
+ first use in the two marks, except one already inside authored strong text,
1010
+ and `firstUsePlugin` turns each mark in inline text into a `strong` token and
1011
+ drops a mark anywhere else. `expandContent` runs `boldFirstUses` on the
1012
+ author's prose for every target, and `createRenderer` installs the plugin on
1013
+ every target.
995
1014
 
996
1015
  Construct one checker with the session's read-only `records` and
997
1016
  `await session.audit("lint")`. Call it for each existing link-index note.
998
1017
  It performs no I/O and returns `{file, line, column, severity: "warning",
999
- rules, message}` diagnostics. `rules` is a list of applicable rule names, merged
1000
- into one warning for each occurrence; the message names the full name or
1001
- explicit label that needs attention. Only `.name` spans and authored link
1002
- labels enter name tracking; plain text is not searched. Indexed full names and
1003
- declared aliases share an identity, while other labels use their whole text.
1004
- Local and captured dependency names and aliases share the same corpus
1005
- context. The command's lint target constructs
1006
- the checker once, uses exact authored `raw` strings and keeps these findings
1018
+ rules, message}` diagnostics. `rules` is a list of applicable rule names,
1019
+ merged into one warning for each occurrence: `unlinked-name` for a `.name`
1020
+ span without `ref` whose label is an indexed full name or listed form. Plain
1021
+ text is not searched. The command's lint target constructs the
1022
+ checker once, uses exact authored `raw` strings and keeps these findings
1007
1023
  advisory. See the [authoring conventions](authoring/links-and-markup.md#marking-names-in-prose).
1008
1024
 
1009
- Supply the invocation's `catalog` to share its full-name and alias lookup with
1025
+ Supply the invocation's `catalog` to share its full-name and form lookup with
1010
1026
  the package names report. Optional `parsed` maps absolute files to captured
1011
1027
  `{raw, body, bodyLine?, bodyColumn?}`; `syntax` is the session's scanner
1012
1028
  projection. When the captured raw text matches, the checker rebases the body's
@@ -1050,7 +1066,7 @@ Import from `@heroiclands/package-build/engine/name-rows`.
1050
1066
 
1051
1067
  | Export | Signature | Returns | Use it when |
1052
1068
  | -------------- | -------------------- | --------------------------- | ---------------------------------------------------------------------- |
1053
- | `NAME_COLUMNS` | `const NAME_COLUMNS` | Frozen ordered column names | Reading `owner`, `name`, `type`, `subType`, `line`, `column`. |
1069
+ | `NAME_COLUMNS` | `const NAME_COLUMNS` | Frozen ordered column names | Reading `owner`, `name`, `type`, `subType`, `ref`, `line`, `column`. |
1054
1070
  | `namesRows` | `namesRows(records)` | `NameRow[]` | Projecting own-note occurrences into SQL rows without I/O or mutation. |
1055
1071
 
1056
1072
  Rows preserve record and occurrence order. `owner` is the canonical NOTE page
@@ -1067,7 +1083,8 @@ Import from `@heroiclands/package-build/engine/names`.
1067
1083
 
1068
1084
  | Export | Signature | Returns | Use it when |
1069
1085
  | ----------------------- | -------------------------------------------------------------- | --------------------------------- | -------------------------------------------------------------------------------------------- |
1070
- | `normalizeName` | `normalizeName(text)` | `string` | Normalizing NFC, apostrophes and whitespace while preserving case. |
1086
+ | `normalizeName` | `normalizeName(text)` | `string` | Normalizing NFC, apostrophes and whitespace and folding case, as names are matched. |
1087
+ | `displayName` | `displayName(text)` | `string` | Normalizing NFC, apostrophes and whitespace while keeping case, as names are displayed. |
1071
1088
  | `nameForms` | `nameForms(text)` | `string[]` | Enumerating supported whole-label possessives and regular plurals. |
1072
1089
  | `createNameCatalog` | `createNameCatalog({ records, index })` | `NameCatalog` | Sharing indexed full-name, alias and resolved Address matching across checks. |
1073
1090
  | `collectUnwrittenNames` | `collectUnwrittenNames({ records, catalog })` | `{groups, firstUse}` | Grouping every unresolved captured occurrence by supplied classification. |
@@ -1102,8 +1119,8 @@ at the earliest reported actual use, and returns no warning for an empty report.
1102
1119
  Collecting and publishing this package's content index. Records describe note frontmatter, assets, prose and event anchors, and the document targets those notes produce. Dynamic publication prepares the same generated Markdown and scalar expressions as the rendering surfaces, using each source note's audience.
1103
1120
 
1104
1121
  Own-note records carry derived `names` entries in source order as
1105
- `{name, type, subType, line, column}`. Missing classifications are explicit
1106
- `null`; no occurrences gives `[]`. Positions refer to original authored span
1122
+ `{name, type, subType, ref, line, column}`. Missing classifications and an
1123
+ absent `ref` are explicit `null`; no occurrences gives `[]`. Positions refer to original authored span
1107
1124
  openings, before generated content. Metadata JSONL transports these entries
1108
1125
  to dependency SQL without an additional artifact. This field is reserved for
1109
1126
  derived data, and is not an authored frontmatter key.
@@ -1120,7 +1137,7 @@ configuration's `itemTypes`.
1120
1137
  | `noteAddress` | `function noteAddress(frontmatter, contentPackage)` | {{slug: string, canonical: string}\|null} `slug` is what goes inside `[[…]]` within this package; `canonical` is the fully qualified key the manifest files the note under, carrying the package and the system as well. | The address a wikilink writes to reach a note, or `null` when it has none. |
1121
1138
  | `sortKeysDeep` | `function sortKeysDeep(value)` | {unknown} The value with every plain object's keys in sorted order. | Recursively sort an object's keys, so serialization is order-independent. |
1122
1139
  | `asciiName` | `function asciiName(name)` | {string\|null} The ASCII form, or `null` when there is no name, or nothing printable survives. | A note's display name reduced to printable 7-bit ASCII. |
1123
- | `asciiAliases` | `function asciiAliases(aliases)` | {Array<string>} Possibly empty, never null: a note with no aliases has an empty set of them, which is a fact rather than a missing value, and a consumer iterating it should not have to check first. | A note's `name.aliases` reduced to printable 7-bit ASCII, in order. |
1140
+ | `asciiForms` | `function asciiForms(name)` | {Record<string, string[]>} Possibly empty, never null: a note listing no form has an empty map, which a consumer iterating it need not check first. | A note's listed name forms reduced to printable 7-bit ASCII, by language. |
1124
1141
  | `serializeContentIndex` | `function serializeContentIndex(records)` | {string} One compact JSON object per line, newline-terminated. | Serialize records as JSON Lines. |
1125
1142
  | `indexRecordsFor` | `indexRecordsFor({ contentBase, config, skipDirectories, problems, parsed, sources, syntax, foreignIndex, nameTypes? })` | {object[]} One record per note, plus one per documentation entry. | The index records for a content tree, without writing anything. |
1126
1143
  | `indexRecordsForNote` | `indexRecordsForNote({ frontmatter, body, bodyLine, bodyColumn?, nameTypes?, relPath, absPath, raw, contentPackage, manifest, addressContext })` | `object[]` | Derive the note's own record and its documentation record from parsed text. |
@@ -1181,8 +1198,8 @@ scalar expressions, events or other generated sections require
1181
1198
  `indexRecordsFor` and `collectContentIndex` collect authored records and pure
1182
1199
  lexical sections; use preparation and emission to publish dynamic body targets.
1183
1200
 
1184
- On lore and affiliation notes, `data.language` and `data.glossary[].language`
1185
- require a language NOTE Address. Decoding refuses other recognized system
1201
+ On lore and affiliation notes, `data.glossary[].language` requires a
1202
+ language NOTE Address. Decoding refuses other recognized system
1186
1203
  segments at the authored value, naming the complete
1187
1204
  `<pkg>-note-skill-<shortcode>` to write. A short local form such as
1188
1205
  `skill-provnzlng` is also accepted. See the [glossary language
@@ -2494,8 +2511,8 @@ focusable inline spoiler: scoped content CSS redacts its text and descendants
2494
2511
  until pointer hover or keyboard focus reveals them. Its focus outline and
2495
2512
  accessible cue identify the reveal interaction. Native Foundry secret sections
2496
2513
  keep their separate controls. Book rendering discards inline `.secret` classes
2497
- and prints their content normally, retaining emphasis, links, anchors and `.name`
2498
- bold styling. Book secret sections and fences retain their visible labelled
2514
+ and prints their content normally, retaining emphasis, links, anchors and a
2515
+ name's first-use bold. Book secret sections and fences retain their visible labelled
2499
2516
  boxes. The span parser's opaque source tokens retain a `secret` boolean from
2500
2517
  their parsed classes for preview presentation.
2501
2518
  `collectAnchors` includes `secret: true` on secret anchors for destination
@@ -41,9 +41,9 @@ For optional writing suggestions, run `npx package-build prose lint assets/conte
41
41
 
42
42
  The full Address is `<package>-note-lore-harbor`; `<package>` comes from `contentPackage` in `package-build.config.yaml`. In another note, `[[lore-harbor|Harbor]]` links to it. `[[lore-harbor|]]` asks the renderer to use the target's current name. The pipe is required.
43
43
 
44
- Use another subject's full canonical name at its first prose mention, then
45
- plain text for later mentions and aliases. Keep a note's own subject in plain
46
- text. Link indexed subjects even when they are private: the destination's
44
+ Mark another subject's name with a link or a `.name` span; its first use in
45
+ the page is bold when it renders. Keep a note's own
46
+ subject in plain text. Link indexed subjects even when they are private: the destination's
47
47
  audience automatically makes the whole reference a spoiler. Add `.show` only
48
48
  to intentionally reveal the reference; it grants no access to the destination.
49
49
  For an unwritten subject, use a typed span such as
@@ -12,7 +12,7 @@ HeroicLands notes begin with YAML frontmatter. A file is recognized as a note wh
12
12
  ```yaml
13
13
  ---
14
14
  shortcode: harbor
15
- name: { full: Harbor, aliases: [The Harbor] }
15
+ name: { full: Harbor, en: [The Harbor] }
16
16
  type: lore
17
17
  subType: economy
18
18
  description: A sheltered trading port.
@@ -21,7 +21,35 @@ data: {}
21
21
  ---
22
22
  ```
23
23
 
24
- `name` is a map with a required, nonempty `full` display name. Every note may also use `aliases` for an ordered list of other names, and `sort` for the form the name takes in an index, such as `Black Flame, The` or `Hilvan, Doran`. The note infobox lists aliases directly beneath the full name. `sort` files the note wherever notes are ordered by name — the book index, page lists, generated lists of related notes, and the `nameSort` column of the `entries` relation — and is the note's entry text in the book index. Every other list still displays the `full` name. A note with no `sort` files and indexes by its `full` name. For `name: {full: The Black Flame, sort: "Black Flame, The"}`, the book index prints _Black Flame, The_ under **B**, and a page list prints _The Black Flame_ at the same place.
24
+ `name` is a map with a required, nonempty `full` display name: the English name a page, a compendium entry and an index print. Every other name the subject goes by is listed under the code of its language, a code [`languages:`](../configuration.md#languages) declares, or `en`, which needs no declaration. `full` already counts as an English form, so `en` lists only the other English names and never repeats `full`. The first form in each list is that language's standard form. Any other key under `name` is an error at its line. The note infobox lists every form directly beneath the full name.
25
+
26
+ `sort` gives the form the name takes in an index, such as `Black Flame, The` or `Hilvan, Doran`. `sort` files the note wherever notes are ordered by name — the book index, page lists, generated lists of related notes, and the `nameSort` column of the `entries` relation — and is the note's entry text in the book index. Every other list still displays the `full` name. A note with no `sort` files and indexes by its `full` name. For `name: {full: The Black Flame, sort: "Black Flame, The"}`, the book index prints _Black Flame, The_ under **B**, and a page list prints _The Black Flame_ at the same place.
27
+
28
+ A person with nicknames lists them in the language they are said in:
29
+
30
+ ```yaml
31
+ name: { full: Johnathan Lewis, en: [John, Long John], vyla: [juan juis] }
32
+ ```
33
+
34
+ A people with a name of its own gives its English name as `full` and its own name first under its language. Another people's name for it, an exonym, goes under that people's language:
35
+
36
+ ```yaml
37
+ name: { full: Grass People, tenl: [tenlaya], kiic: [kitchek] }
38
+ ```
39
+
40
+ Names match whatever their case: `Harbor`, `harbor` and `HARBOR` all name the note above, and every form is matched the same way. Display keeps the case the note writes. Two notes whose forms differ only by case are one ambiguous name, reported wherever the name is used.
41
+
42
+ `data.lang` states the language the note is in, as a code `languages:` declares or `en`. Any note may state it. A note that does not takes its language from its `data.culture`'s language, then from its `data.parents`, nearest first, then `en`; see [a note's language](../reference/format-details.md#a-notes-language). A culture note's language is how the notes of its people find their tongue:
43
+
44
+ ```yaml
45
+ ---
46
+ shortcode: grassfolk
47
+ name: { full: Grass People, tenl: [tenlaya] }
48
+ type: lore
49
+ subType: culture
50
+ data: { lang: tenl }
51
+ ---
52
+ ```
25
53
 
26
54
  `description` is the short page summary. `tags` hold draft state and descriptive labels. The note type's classification belongs in `subType` where it is declared. `data` holds facts about the subject shared by systems. A YAML comment is the place for an author-only note: commented values remain valid YAML but do not enter the content index or generated documents.
27
55