@heroiclands/package-build 22.4.2 → 22.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,37 @@
1
1
  # @heroiclands/package-build
2
2
 
3
+ ## 22.4.4
4
+
5
+ ### Patch Changes
6
+
7
+ Every page a site build publishes carries a `related` block in its front matter — the pages that link to it and the pages it links to, each named once with its title, address and type — which the theme renders as the Related card at the foot of the page. A page nothing connects to carries no block, and the homepage is counted on both sides like any other page.
8
+
9
+ `changelog group` keeps a hard-wrapped paragraph as one block instead of fragmenting it one block per line, and no longer inserts a blank line under a bold sentence a paragraph continues directly beneath.
10
+
11
+ ## 22.4.3
12
+
13
+ ### Patch Changes
14
+
15
+ **A new `changelog group` command folds a release's changeset blocks together by their bold label.**
16
+ `@heroiclands/package-build/changelog` writes each changeset's summary into `CHANGELOG.md` as its
17
+ own block, so three pull requests each touching compendium content leave three separate
18
+ `**Compendiums**` blocks instead of one. `changelog group` rewrites the newest release section in
19
+ place: every block sharing one label is merged into a single block, bullets kept in their original
20
+ order with an exact duplicate kept once, and the merged blocks are ordered by a new
21
+ `changelog.labels` configuration key — the unlabelled lead paragraph first, then every declared
22
+ label in order, then any undeclared label last with a warning naming it. `changelog check` warns on
23
+ a label absent from a declared `changelog.labels` too, so a drifted spelling is caught before a
24
+ release ships it.
25
+
26
+ **A shipped changelog generator writes a release note without a commit hash.**
27
+ `@heroiclands/package-build/changelog` is a Changesets changelog generator
28
+ whose `getReleaseLine` renders a changeset's summary as a block, verbatim —
29
+ no bullet, no indentation, separated from its neighbours by one blank line —
30
+ and whose `getDependencyReleaseLine` writes nothing. Point
31
+ `.changeset/config.json` at it
32
+ (`"changelog": "@heroiclands/package-build/changelog"`) to stop every release
33
+ note from carrying a commit-hash prefix.
34
+
3
35
  ## 22.4.2
4
36
 
5
37
  ### Patch Changes
package/CONTENT.md CHANGED
@@ -2333,6 +2333,53 @@ notes it lists, and link it from the homepage. Nothing is generated between
2333
2333
  the homepage and the pages, so delete the key.
2334
2334
  ````
2335
2335
 
2336
+ ### Every page carries what links to it, and what it links to
2337
+
2338
+ The build resolves every wikilink on every page through the address index, so
2339
+ at the moment it writes a page it holds the whole link graph. It writes the
2340
+ part that concerns each page into that page's front matter:
2341
+
2342
+ ```yaml
2343
+ related:
2344
+ backlinks: # pages that link to this one
2345
+ - title: Afzandah Parnâzar
2346
+ url: /thalorna/being-afzndhprnzr/
2347
+ type: being
2348
+ mentions: # pages this one links to
2349
+ - title: Kethramír
2350
+ url: /thalorna/place-kthrmr/
2351
+ type: place
2352
+ ```
2353
+
2354
+ The theme's `related.html` partial reads both lists and renders them as one
2355
+ "Related" card, grouped by `type`. What the build guarantees about them:
2356
+
2357
+ - **A page appears once per list**, however many times it is linked, and
2358
+ whichever address the link was written to: `docbeing-grod` and `being-grod`
2359
+ are one page, and a page that cites it under both lists it once. A link from
2360
+ a page to itself is not a connection and is dropped.
2361
+ - **Each list is sorted by `type`, then `title`**, so the card's grouping is
2362
+ the same on every build whatever order the notes were walked in.
2363
+ - **Both lists are present whenever either has an entry**, so a theme reads one
2364
+ shape; a page with no links either way carries **no `related` key at all**,
2365
+ which is the partial's silent-disappear convention kept at the source.
2366
+ - **`url` composes `<base><slug>/`**, like every other href this build renders
2367
+ — see [A page's URL is its address](#a-pages-url-is-its-address). The page
2368
+ states its own address as `/<slug>/`; everything pointing _at_ it, this
2369
+ included, carries the base.
2370
+ - **The homepage is a page like any other on both sides.** A content page
2371
+ reaches it through `[[homepage-root|Text]]` and is listed in its backlinks;
2372
+ its own body links are ordinary markdown, resolved against the package root
2373
+ the way a browser resolves them, so `being-grod/`, `/thalorna/being-grod/`
2374
+ and `./being-grod/#top` each count as a mention of that page. A link that
2375
+ names a host leaves the site and counts as nothing.
2376
+ - **Only a page of this site is an endpoint.** A link into another package
2377
+ resolves to an href the build renders but no page it writes, so it is listed
2378
+ on neither side; a backlink can be counted only on a page this build emits.
2379
+
2380
+ `related` is derived, never authored: what links to a page is a fact about
2381
+ every other page in the tree. A note that writes one has it replaced.
2382
+
2336
2383
  ### Why the output location is fixed
2337
2384
 
2338
2385
  The content mount is a build artifact and is **deleted on every run**, so that a
@@ -2830,12 +2877,12 @@ zero differences, so only "which file was read" separates the outcomes.
2830
2877
  Releasing is not a command anyone runs. It is a consequence of merging, in two
2831
2878
  steps, and each step is visible while it is pending.
2832
2879
 
2833
- **Every pull request declares its bump.** Run `npx changeset` and pick
2834
- major/minor/patch; the summary you write becomes the changelog entry and the
2835
- release note. If the change ships nothing a consumer can see, say so explicitly
2836
- with `npx changeset add --empty`. CI's **Changeset declared** job fails a pull
2837
- request that declares neither — `npm run changeset:check` is the same check,
2838
- locally.
2880
+ **A pull request declares its bump when a consumer will notice.** Run
2881
+ `npx changeset` and pick major/minor/patch; the summary you write becomes the
2882
+ changelog entry and the release note. Nothing a consumer meets means no
2883
+ changeset at all — CI carries no gate demanding one. What CI does check is the
2884
+ quality of a changeset that exists: `node bin/package-build.mjs changelog check`
2885
+ lints every pending one against the rules a changeset is held to.
2839
2886
 
2840
2887
  **Merging to `main` opens a Version Packages pull request** carrying the version
2841
2888
  bump and the rewritten `CHANGELOG.md`. That pull request _is_ the pending
package/README.md CHANGED
@@ -638,12 +638,12 @@ the first import.
638
638
  Releasing is not a command anyone runs. It is a consequence of merging, in two
639
639
  steps, and each step is visible while it is pending.
640
640
 
641
- **Every pull request declares its bump.** Run `npx changeset` and pick
642
- major/minor/patch; the summary you write becomes the changelog entry and the
643
- release note. If the change ships nothing a consumer can see, say so explicitly
644
- with `npx changeset add --empty`. CI's **Changeset declared** job fails a pull
645
- request that declares neither — `npm run changeset:check` is the same check,
646
- locally.
641
+ **A pull request declares its bump when a consumer will notice.** Run
642
+ `npx changeset` and pick major/minor/patch; the summary you write becomes the
643
+ changelog entry and the release note. Nothing a consumer meets means no
644
+ changeset at all — CI carries no gate demanding one. What CI does check is the
645
+ quality of a changeset that exists: `node bin/package-build.mjs changelog check`
646
+ lints every pending one against the rules a changeset is held to.
647
647
 
648
648
  **Merging to `main` opens a Version Packages pull request** carrying the version
649
649
  bump and the rewritten `CHANGELOG.md`. That pull request _is_ the pending
@@ -54,6 +54,7 @@
54
54
  * npx package-build lang coverage [--unused]
55
55
  * npx package-build lang hardcoded
56
56
  * npx package-build changelog check [--release] [paths..]
57
+ * npx package-build changelog group [paths..]
57
58
  * npx package-build bundle check
58
59
  * npx package-build release
59
60
  * npx package-build deploy <stage>
@@ -82,7 +83,7 @@ import { loadPackageBuildConfig } from "../config.mjs";
82
83
  import { writeSiteRoot } from "../engine/site-root.mjs";
83
84
  import { DEPLOY_ROOT } from "../engine/site-config.mjs";
84
85
  import { compilesFoundryDocuments } from "../content-config.mjs";
85
- import { loadPackConfig, packConfigPath } from "../engine/pack-config.mjs";
86
+ import { loadPackConfig, packConfigPath, resolveConfigFile } from "../engine/pack-config.mjs";
86
87
  import { cleanBuildArtifacts, stageAssets } from "../stage.mjs";
87
88
  import { buildSchemaArtifact } from "../engine/schema-extract.mjs";
88
89
  import { SCHEMA_ARTIFACT_FILE } from "../engine/foreign-catalog.mjs";
@@ -90,6 +91,7 @@ import { validateLangSource } from "../lang.mjs";
90
91
  import { checkLabelRegistry } from "../labels.mjs";
91
92
  import { lintYaml } from "../engine/yaml-lint.mjs";
92
93
  import { lintChangesetText, lintReleaseText } from "../engine/changelog-lint.mjs";
94
+ import { groupChangelogText } from "../engine/changelog-group.mjs";
93
95
  import { bumpDependencies } from "../engine/dependency-bump.mjs";
94
96
  import {
95
97
  analyzeCoverage,
@@ -779,41 +781,53 @@ function yamlCommand() {
779
781
  }
780
782
 
781
783
  /**
782
- * `changelog check` — lint release prose against the rules a changeset is
783
- * actually held to (`check` is the only action).
784
- *
785
- * A changeset answers one question — who notices, and what do they see — and
786
- * nothing enforced it, so a pull-request description pasted into one ships
787
- * verbatim as a release note. Default reads every pending changeset;
788
- * `--release` reads the first `## <version>` section of `CHANGELOG.md`
789
- * instead, for the **Version Packages** branch a merge to `main` opens.
784
+ * `changelog check` / `changelog group` — release-prose checks, and folding
785
+ * a release's changeset blocks together by their bold label.
786
+ *
787
+ * `check` answers one question a changeset otherwise goes unheld to — who
788
+ * notices, and what do they see — so a pull-request description pasted into
789
+ * one does not ship verbatim as a release note. Default reads every pending
790
+ * changeset; `--release` reads the first `## <version>` section of
791
+ * `CHANGELOG.md` instead, for the **Version Packages** branch a merge to
792
+ * `main` opens.
793
+ *
794
+ * `group` rewrites that same first `## <version>` section in place: every
795
+ * changeset in it writes its own `**Compendiums**` block, and nothing merges
796
+ * the three a release with three compendium fixes ends up with. It reads
797
+ * `changelog.labels` for the vocabulary and display order, when the
798
+ * repository declares one.
790
799
  *
791
800
  * @returns {object} The yargs command module.
792
801
  */
793
802
  function changelogCommand() {
794
803
  return {
795
804
  command: "changelog <action> [paths..]",
796
- describe: "Release-prose checks",
805
+ describe: "Release-prose checks, and folding a release's blocks by label",
797
806
  builder: (y) =>
798
807
  y
799
808
  .positional("action", {
800
- choices: ["check"],
801
- describe: "check: lint pending changesets, or a release section",
809
+ choices: ["check", "group"],
810
+ describe:
811
+ "check: lint pending changesets, or a release section. " +
812
+ "group: fold a release section's blocks together by label",
802
813
  })
803
814
  .positional("paths", {
804
815
  describe:
805
- "Files to check. Defaults to `.changeset/*.md` (config.json and " +
806
- "README.md excluded), or `CHANGELOG.md` with --release.",
816
+ "Files to act on. Defaults to `.changeset/*.md` (config.json and " +
817
+ "README.md excluded) for check, or `CHANGELOG.md` for check --release " +
818
+ "and for group.",
807
819
  type: "string",
808
820
  })
809
821
  .option("release", {
810
822
  type: "boolean",
811
823
  default: false,
812
824
  describe:
813
- "Check the first `## <version>` section of CHANGELOG.md instead of " +
814
- "pending changesets",
825
+ "check only: check the first `## <version>` section of CHANGELOG.md " +
826
+ "instead of pending changesets",
815
827
  }),
816
- handler: handler(async (args) => changelogCheck(args)),
828
+ handler: handler(async (args) =>
829
+ args.action === "group" ? changelogGroup(args) : changelogCheck(args),
830
+ ),
817
831
  };
818
832
  }
819
833
 
@@ -835,19 +849,35 @@ function changelogFiles(args) {
835
849
  );
836
850
  }
837
851
 
852
+ /**
853
+ * `changelog.labels`, when this repository declares a configuration — `null`
854
+ * otherwise, so `check` and `group` both work in a tree with no
855
+ * `package-build.config.yaml` at all, exactly as `check` already does for
856
+ * pending changesets with no configuration to read.
857
+ *
858
+ * @returns {readonly string[]|null}
859
+ */
860
+ function loadChangelogLabels() {
861
+ const found = resolveConfigFile();
862
+ if (!found.path) return null;
863
+ return loadPackConfig().changelog.labels;
864
+ }
865
+
838
866
  /**
839
867
  * Run `changelog check` over every resolved file and report the result.
840
868
  *
841
869
  * @param {object} args - Parsed CLI arguments.
842
870
  */
843
871
  function changelogCheck(args) {
872
+ const labels = loadChangelogLabels();
844
873
  const files = changelogFiles(args);
845
874
  let errors = 0;
846
875
  let total = 0;
847
876
  for (const file of files) {
848
877
  if (!fs.existsSync(file)) die(`changelog check: ${file} does not exist.`);
849
878
  const text = fs.readFileSync(file, "utf8");
850
- const { findings } = args.release ? lintReleaseText(text) : lintChangesetText(text);
879
+ const { findings } =
880
+ args.release ? lintReleaseText(text, { labels }) : lintChangesetText(text, { labels });
851
881
  errors += reportFindings(findings, { file });
852
882
  total += findings.length;
853
883
  }
@@ -858,6 +888,26 @@ function changelogCheck(args) {
858
888
  if (errors) process.exitCode = 1;
859
889
  }
860
890
 
891
+ /**
892
+ * Run `changelog group` over every resolved file, writing each back in place.
893
+ *
894
+ * @param {object} args - Parsed CLI arguments.
895
+ */
896
+ function changelogGroup(args) {
897
+ const labels = loadChangelogLabels();
898
+ const files = args.paths?.length ? args.paths : ["CHANGELOG.md"];
899
+ let warnings = 0;
900
+ for (const file of files) {
901
+ if (!fs.existsSync(file)) die(`changelog group: ${file} does not exist.`);
902
+ const original = fs.readFileSync(file, "utf8");
903
+ const { text, findings } = groupChangelogText(original, { labels });
904
+ reportFindings(findings, { file });
905
+ warnings += findings.length;
906
+ if (text !== original) fs.writeFileSync(file, text, "utf8");
907
+ }
908
+ console.log(`package-build: ${files.length} file(s) grouped · ${warnings} warning(s)`);
909
+ }
910
+
861
911
  /**
862
912
  * `labels check` — do the machine registry and the documented table agree?
863
913
  *
package/changelog.cjs ADDED
@@ -0,0 +1,55 @@
1
+ /*
2
+ * This file is part of the Song of Heroic Lands (SoHL) system for Foundry VTT.
3
+ * Copyright (c) 2024-2026 Tom Rodriguez ("Toasty") — <toasty@heroiclands.org>
4
+ *
5
+ * This work is licensed under the GNU General Public License v3.0 (GPLv3).
6
+ * You may copy, modify, and distribute it under the terms of that license.
7
+ *
8
+ * For full terms, see the LICENSE.md file in the project root or visit:
9
+ * https://www.gnu.org/licenses/gpl-3.0.html
10
+ *
11
+ * SPDX-License-Identifier: GPL-3.0-or-later
12
+ */
13
+
14
+ /**
15
+ * The Changesets changelog generator every HeroicLands repository configures
16
+ * as `"changelog": "@heroiclands/package-build/changelog"`.
17
+ *
18
+ * Every built-in generator writes a commit hash into the release line
19
+ * whenever it knows the commit, which it always does in this org's
20
+ * squash-merge workflow — `@changesets/cli/changelog` writes `- <hash>: `,
21
+ * and `@changesets/changelog-git` adds a link on top of it. A commit hash is
22
+ * a commit-log artefact, not something the person installing the package
23
+ * needs, and the writing conventions keep tracker and commit references out
24
+ * of the tree. `getReleaseLine` writes the changeset's summary and nothing
25
+ * else; `getDependencyReleaseLine` writes nothing, because none of these
26
+ * packages narrate an internal dependency bump.
27
+ *
28
+ * Changesets loads a changelog generator with `require`, so this is
29
+ * CommonJS in a package that is otherwise `"type": "module"`.
30
+ *
31
+ * @module
32
+ */
33
+
34
+ /**
35
+ * @param {{summary: string}} changeset - The pending changeset. Only
36
+ * `summary` is read; a known `commit` is deliberately ignored.
37
+ * @returns {string} The summary rendered as a block, verbatim — no leading
38
+ * `- `, no indentation of the following lines — wrapped in a single
39
+ * newline on each side so Changesets' own joiner (which counts, rather
40
+ * than reads, the newlines bracketing a release line) always separates
41
+ * this block from its neighbours by one blank line.
42
+ */
43
+ function getReleaseLine(changeset) {
44
+ return `\n${changeset.summary}\n`;
45
+ }
46
+
47
+ /**
48
+ * @returns {string} Always empty — these packages have no internal
49
+ * dependency bumps to narrate.
50
+ */
51
+ function getDependencyReleaseLine() {
52
+ return "";
53
+ }
54
+
55
+ module.exports = { getReleaseLine, getDependencyReleaseLine };
@@ -756,10 +756,12 @@ const CONFIG_KEYS = [
756
756
  "requiresSystem",
757
757
  "packageBuild",
758
758
  "publish",
759
+ "changelog",
759
760
  ];
760
761
  const SYSTEM_KEYS = ["manifest", "compatibility"];
761
762
  const COMPATIBILITY_KEYS = ["minimum", "verified"];
762
763
  const DOCS_KEYS = ["itemFields"];
764
+ const CHANGELOG_KEYS = ["labels"];
763
765
  const SITE_KEYS = [
764
766
  "base",
765
767
  "assets",
@@ -1458,6 +1460,37 @@ function normalizeDocs(value) {
1458
1460
  });
1459
1461
  }
1460
1462
 
1463
+ /**
1464
+ * `changelog.labels` — the vocabulary and display order `changelog check`
1465
+ * and `changelog group` read a release block's bold label against.
1466
+ *
1467
+ * Optional: a repository that groups nothing declares none, and `group`
1468
+ * falls back to ordering every label by first appearance. Declared, it is a
1469
+ * list of non-empty strings with no duplicate — a repeated label could never
1470
+ * be told apart from a genuine drift (`Compendiums` twice would silently
1471
+ * swallow the `Character data` beside `Characters` the list exists to catch).
1472
+ *
1473
+ * @param {unknown} value - The configured value, or `undefined`.
1474
+ * @returns {Readonly<{labels: readonly string[]|null}>}
1475
+ */
1476
+ function normalizeChangelog(value) {
1477
+ if (value === undefined) return Object.freeze({ labels: null });
1478
+ if (!isPlainObject(value)) fail("changelog", "must be a mapping");
1479
+ const input = /** @type {Record<string, unknown>} */ (value);
1480
+ rejectUnknownKeys(input, CHANGELOG_KEYS, "changelog.");
1481
+ if (input.labels === undefined) return Object.freeze({ labels: null });
1482
+ if (!Array.isArray(input.labels)) fail("changelog.labels", "must be an array");
1483
+ const labels = input.labels.map((label, index) =>
1484
+ requireNonEmptyString(label, `changelog.labels[${index}]`),
1485
+ );
1486
+ const seen = new Set();
1487
+ for (const label of labels) {
1488
+ if (seen.has(label)) fail("changelog.labels", `declares \`${label}\` more than once`);
1489
+ seen.add(label);
1490
+ }
1491
+ return Object.freeze({ labels: Object.freeze(labels) });
1492
+ }
1493
+
1461
1494
  /**
1462
1495
  * The asset host the website resolves a pathname against.
1463
1496
  *
@@ -2758,5 +2791,6 @@ export function defineConfig(config) {
2758
2791
  requiresSystem,
2759
2792
  packageBuild: normalizePackageBuild(input.packageBuild),
2760
2793
  publish: normalizePublish(input.publish),
2794
+ changelog: normalizeChangelog(input.changelog),
2761
2795
  });
2762
2796
  }
package/docs/api.md CHANGED
@@ -1555,6 +1555,22 @@ console.log(VISIBLE_ATTRIBUTES);
1555
1555
  | `findHardcodedText` | `findHardcodedText(source, { allow = [] } = {})` | `TemplateFinding[]` | finding every user-visible literal a template leaves untranslated; `allow` is the explicit, reasoned escape hatch for a literal that is deliberately not a key |
1556
1556
  | `findTemplateSyntaxErrors` | `findTemplateSyntaxErrors(source)` | `TemplateFinding[]` — one finding when it does not parse, else none | checking whether Handlebars can parse a template at all, without needing any helper it calls to exist |
1557
1557
 
1558
+ ## `./changelog`
1559
+
1560
+ The Changesets changelog generator, loaded with `require` — a CommonJS module in a package that is otherwise `"type": "module"`. Every HeroicLands repository sets `"changelog": "@heroiclands/package-build/changelog"` in its `.changeset/config.json`, because every built-in generator writes a commit hash into the release line whenever it knows the commit, which it always does in this org's squash-merge workflow, and a commit hash is a commit-log artefact rather than something the person installing the package needs.
1561
+
1562
+ `getReleaseLine` renders a changeset's summary as a block, verbatim — no leading `- `, no indentation of the following lines — so a summary that opens with a paragraph, a bold label, or its own bullet list lands in `CHANGELOG.md` exactly as written. It wraps the summary in a single newline on each side, which costs nothing in the rendered file — Changesets' own joiner trims a release line's edges and only counts the newlines it finds there — but guarantees the block is separated from its neighbours, heading or sibling entry alike, by one blank line.
1563
+
1564
+ ```json
1565
+ // .changeset/config.json
1566
+ { "changelog": "@heroiclands/package-build/changelog" }
1567
+ ```
1568
+
1569
+ | Export | Signature | Returns | Use it when |
1570
+ | -------------------------- | -------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- |
1571
+ | `getReleaseLine` | `getReleaseLine(changeset, type, changelogOpts)` | `string` — the summary, verbatim, separated from its neighbours by a blank line | writing one changeset's release line with no commit-hash prefix and no bullet |
1572
+ | `getDependencyReleaseLine` | `getDependencyReleaseLine(changesets, dependenciesUpdated, changelogOpts)` | `string` — always `""` | satisfying the `ChangelogFunctions` interface; these packages narrate no internal dependency bumps |
1573
+
1558
1574
  ## `./package.json`
1559
1575
 
1560
1576
  Not a JavaScript module — this subpath entry exists so tooling (bundlers, `import.meta.resolve`, a script reading the installed version) can resolve the package's own `package.json` through Node's package-exports resolution instead of reaching outside the declared export map.
package/docs/commands.md CHANGED
@@ -646,8 +646,11 @@ from the heading rule, since neither is authored.
646
646
  A token that reads as code — `camelCase()`, a `path/with/slashes.ext`,
647
647
  `SCREAMING_SNAKE` — outside any code span is a warning, not a failure: a
648
648
  user-facing note sometimes needs one (`Compendium.hm3.items.Item.<id>`), but
649
- rarely. Every other finding is an error. Reads the files given, or resolves
650
- its own defaults; writes nothing.
649
+ rarely. A block's bold label absent from a declared `changelog.labels` is
650
+ also a warning — the drift `**Character data**` beside `**Characters**`
651
+ produces — and checks nothing when the repository declares no
652
+ `changelog.labels`. Every other finding is an error. Reads the files given,
653
+ or resolves its own defaults; writes nothing.
651
654
 
652
655
  **OPTIONS**
653
656
 
@@ -678,6 +681,71 @@ package-build: 1 file(s) checked · 1 error(s) · 0 warning(s)
678
681
 
679
682
  [Diagnostics](diagnostics.md).
680
683
 
684
+ ### `package-build changelog group`
685
+
686
+ **NAME**
687
+
688
+ Fold a release section's changeset blocks together by their bold label
689
+ (`group` is a sibling of `check`, not one of its actions).
690
+
691
+ **SYNOPSIS**
692
+
693
+ ```
694
+ package-build changelog group [paths..]
695
+ ```
696
+
697
+ **DESCRIPTION**
698
+
699
+ `@heroiclands/package-build/changelog` writes each changeset's summary into
700
+ `CHANGELOG.md` as its own block, verbatim, under `### <Bump> Changes` — one
701
+ per changeset, in whatever order Changesets read the files. Three pull
702
+ requests each touching compendium content each write their own
703
+ `**Compendiums**` block, and nothing merges them: the release reads as three
704
+ scattered entries instead of one.
705
+
706
+ `group` rewrites the first `## <version>` section of `CHANGELOG.md` in
707
+ place. Within each `### <Bump> Changes` body, every block sharing one bold
708
+ label is merged into a single block for that label — its bullets kept in
709
+ the order they were originally written, an exact-duplicate bullet kept
710
+ once — and the merged blocks are reordered: the unlabelled lead paragraph a
711
+ changeset writes with no category first, then every label
712
+ [`changelog.labels`](configuration.md#changelog) declares, in the order
713
+ given, then any label absent from that list last, in the order it first
714
+ appeared — each reported as a warning naming it. With no `changelog.labels`
715
+ declared, every group instead keeps the order its label first appeared in,
716
+ lead paragraph first, and nothing is reported as unknown. Every earlier
717
+ release section is untouched, byte for byte. Running `group` on its own
718
+ output is a no-op.
719
+
720
+ **OPTIONS**
721
+
722
+ | Positional | Type | Default | Description |
723
+ | ---------- | --------- | -------------- | ----------------------------------- |
724
+ | `paths` | string(s) | `CHANGELOG.md` | Files to group, rewritten in place. |
725
+
726
+ **EXIT STATUS**
727
+
728
+ 1 if a named file does not exist. 0 otherwise — an undeclared label is a
729
+ warning, never a failure.
730
+
731
+ **EXAMPLES**
732
+
733
+ ```
734
+ $ package-build changelog group
735
+ package-build: 1 file(s) grouped · 0 warning(s)
736
+ ```
737
+
738
+ ```
739
+ $ package-build changelog group
740
+ CHANGELOG.md:12: warning: changelog-group/unknown-label "Scenery" is not declared in `changelog.labels` — filed last, in order of first appearance
741
+ package-build: 1 file(s) grouped · 1 warning(s)
742
+ ```
743
+
744
+ **SEE ALSO**
745
+
746
+ [`changelog check`](#package-build-changelog-check),
747
+ [`changelog.labels`](configuration.md#changelog).
748
+
681
749
  ### `package-build bundle check`
682
750
 
683
751
  **NAME**
@@ -48,7 +48,7 @@ around its own evaluation.
48
48
 
49
49
  ### Quick reference
50
50
 
51
- 20 top-level keys. `rootDir` is not one of them — a data configuration never
51
+ 21 top-level keys. `rootDir` is not one of them — a data configuration never
52
52
  writes it — and is documented under [Derived values](#derived-values) instead,
53
53
  alongside `foundryPackage`, `homepage`, `author` and `itemBuilders`, whose
54
54
  data-configuration behaviour is also derivation rather than ordinary
@@ -76,10 +76,11 @@ authoring.
76
76
  | [`requiresSystem`](#requiressystem) | string | no; refused in a `documentation` package | `null` |
77
77
  | [`packageBuild`](#the-packagebuild-section) | object | no | `{}` |
78
78
  | [`publish`](#publish) | object | no; **required**, with `site: content`, in a `documentation` package | `{site: "homepage", address: {prefix: ""}}` |
79
+ | [`changelog`](#changelog) | object | no | `{labels: null}` |
79
80
 
80
81
  Any key outside this list is refused:
81
82
 
82
- > `` `<key>` is not a recognized option (expected one of: rootDir, contentPackage, foundryPackage, homepage, author, packageKind, stats, itemBuilders, paths, skipDirectories, icons, packs, docs, site, pdf, compatibility, relationships, systems, requiresSystem, packageBuild, publish). ``
83
+ > `` `<key>` is not a recognized option (expected one of: rootDir, contentPackage, foundryPackage, homepage, author, packageKind, stats, itemBuilders, paths, skipDirectories, icons, packs, docs, site, pdf, compatibility, relationships, systems, requiresSystem, packageBuild, publish, changelog). ``
83
84
 
84
85
  (`rootDir` appears in that list because it is a key `defineConfig` itself
85
86
  accepts — an `.mjs` configuration authors it directly. A YAML configuration
@@ -289,7 +290,7 @@ Unlike the first three, authoring `itemBuilders` is not an error — it is
289
290
 
290
291
  ---
291
292
 
292
- ## The 20 keys
293
+ ## The 21 keys
293
294
 
294
295
  ### `contentPackage`
295
296
 
@@ -1334,6 +1335,45 @@ Any other key under `publish.address` is refused:
1334
1335
 
1335
1336
  > ``package-build config: `publish.address.<key>` is not a recognized option (expected one of: prefix).``
1336
1337
 
1338
+ ### `changelog`
1339
+
1340
+ **Type:** object · **Optional**, default `{labels: null}`.
1341
+
1342
+ `changelog check` and `changelog group` both read a release block's bold
1343
+ label — `**Compendiums**` — against `changelog.labels`: the vocabulary and
1344
+ display order a repository declares for the categories its changesets group
1345
+ under. Declaring none is ordinary — a repository that writes plain,
1346
+ unlabelled changesets needs nothing here.
1347
+
1348
+ | Key | Type | Required | Default |
1349
+ | ------------------ | -------- | -------- | ------- |
1350
+ | `changelog.labels` | string[] | no | `null` |
1351
+
1352
+ > ``package-build config: `changelog` must be a mapping.``
1353
+
1354
+ > ``package-build config: `changelog.<key>` is not a recognized option (expected one of: labels).``
1355
+
1356
+ > ``package-build config: `changelog.labels` must be an array.``
1357
+
1358
+ > ``package-build config: `changelog.labels[<n>]` must be a non-empty string.``
1359
+
1360
+ A repeated label is refused rather than silently deduplicated — it could
1361
+ never be told apart from the drift the list exists to catch (`Compendiums`
1362
+ written twice swallows a genuine `Character data` beside `Characters`):
1363
+
1364
+ > ``package-build config: `changelog.labels` declares `Compendiums` more than once.``
1365
+
1366
+ With `changelog.labels` declared, `changelog group` orders a release
1367
+ section's blocks by that list — the unlabelled lead paragraph first, then
1368
+ every declared label in the order given, then any undeclared label last, in
1369
+ the order it first appears, each reported as a warning naming it. With no
1370
+ `changelog.labels` declared, `group` orders every block by first
1371
+ appearance, lead paragraph first, and warns about nothing. `changelog check`
1372
+ warns on a block whose label is not in a declared `changelog.labels`; with
1373
+ none declared, it checks nothing. See
1374
+ [`package-build changelog check`](commands.md#package-build-changelog-check)
1375
+ and [`package-build changelog group`](commands.md#package-build-changelog-group).
1376
+
1337
1377
  ---
1338
1378
 
1339
1379
  ## The `packageBuild` section