@heroiclands/package-build 22.4.1 → 22.4.3

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,59 @@
1
1
  # @heroiclands/package-build
2
2
 
3
+ ## 22.4.3
4
+
5
+ ### Patch Changes
6
+
7
+ `@heroiclands/package-build/changelog` writes each changeset's summary into `CHANGELOG.md` as its
8
+
9
+ own block, so three pull requests each touching compendium content leave three separate
10
+
11
+ `**Compendiums**` blocks instead of one. `changelog group` rewrites the newest release section in
12
+
13
+ place: every block sharing one label is merged into a single block, bullets kept in their original
14
+
15
+ order with an exact duplicate kept once, and the merged blocks are ordered by a new
16
+
17
+ `changelog.labels` configuration key — the unlabelled lead paragraph first, then every declared
18
+
19
+ label in order, then any undeclared label last with a warning naming it. `changelog check` warns on
20
+
21
+ a label absent from a declared `changelog.labels` too, so a drifted spelling is caught before a
22
+
23
+ release ships it.
24
+
25
+ `@heroiclands/package-build/changelog` is a Changesets changelog generator
26
+
27
+ whose `getReleaseLine` renders a changeset's summary as a block, verbatim —
28
+
29
+ no bullet, no indentation, separated from its neighbours by one blank line —
30
+
31
+ and whose `getDependencyReleaseLine` writes nothing. Point
32
+
33
+ `.changeset/config.json` at it
34
+
35
+ (`"changelog": "@heroiclands/package-build/changelog"`) to stop every release
36
+
37
+ note from carrying a commit-hash prefix.
38
+
39
+ **A new `changelog group` command folds a release's changeset blocks together by their bold label.**
40
+
41
+ **A shipped changelog generator writes a release note without a commit hash.**
42
+
43
+ ## 22.4.2
44
+
45
+ ### Patch Changes
46
+
47
+ - 8dedb23: **A module embedding HM3 spells, invocations or psionics compiles.** A being's
48
+ embedded `spell`, `invocation` or `psionic` reference that names no shortcode
49
+ template and no `data.icon` of its own now takes that subtype's own default
50
+ art — the icon HM3 itself assigns a freshly created item of that kind —
51
+ instead of the build refusing the actor outright.
52
+ - 54109cf: **A module embedding HârnMaster 3 weapons compiles.** A being's `(type,
53
+ shortcode)` reference into HM3's one-to-many `weapongear` row resolves when
54
+ its `type` names one of the row's own subtypes — `weapongear` or
55
+ `missilegear` — instead of being refused as ambiguous.
56
+
3
57
  ## 22.4.1
4
58
 
5
59
  ### Patch Changes
package/CONTENT.md CHANGED
@@ -2830,12 +2830,12 @@ zero differences, so only "which file was read" separates the outcomes.
2830
2830
  Releasing is not a command anyone runs. It is a consequence of merging, in two
2831
2831
  steps, and each step is visible while it is pending.
2832
2832
 
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.
2833
+ **A pull request declares its bump when a consumer will notice.** Run
2834
+ `npx changeset` and pick major/minor/patch; the summary you write becomes the
2835
+ changelog entry and the release note. Nothing a consumer meets means no
2836
+ changeset at all CI carries no gate demanding one. What CI does check is the
2837
+ quality of a changeset that exists: `node bin/package-build.mjs changelog check`
2838
+ lints every pending one against the rules a changeset is held to.
2839
2839
 
2840
2840
  **Merging to `main` opens a Version Packages pull request** carrying the version
2841
2841
  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 towho
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
@@ -82,12 +82,13 @@ carries no hook files of its own and cannot drift from the others. `|| true` so
82
82
  that an install outside a git checkout — a Docker build, a CI cache step —
83
83
  does not fail on it.
84
84
 
85
- Three hooks arrive with it, each with its own switch, read with git's normal
85
+ Four hooks arrive with it, each with its own switch, read with git's normal
86
86
  precedence: a plain `git config` sets one clone, `--global` sets a machine.
87
87
 
88
88
  | Hook | What it does | Key | Default |
89
89
  | -------------------------------- | --------------------------------------------------------------- | ------------------------------------ | ------- |
90
90
  | `pre-commit`, `pre-merge-commit` | Refuse a commit on a protected branch. | `hooks.allowCommitOnMain` (inverted) | on |
91
+ | `pre-commit` | Lint any `.changeset/*.md` the commit stages. | `hooks.changelogCheck` | on |
91
92
  | `commit-msg` | Refuse AI attribution in a commit message. | `hooks.noAttribution` | on |
92
93
  | `pre-push` | Run this repository's own Build & Test workflow in a container. | `hooks.prePushCi` | off |
93
94
 
@@ -130,19 +131,20 @@ anything to remove.
130
131
 
131
132
  ### `lint:*` — the checks, one per question
132
133
 
133
- Six checks, each answering one question and blind to what the others see. They
134
- are separate scripts rather than one because a failing chain stops at its first
135
- failure, and knowing _which_ question failed is most of the diagnosis.
134
+ Seven checks, each answering one question and blind to what the others see.
135
+ They are separate scripts rather than one because a failing chain stops at its
136
+ first failure, and knowing _which_ question failed is most of the diagnosis.
136
137
 
137
138
  ```json
138
- "lint": "run-s lint:format lint:markdown lint:addresses lint:content-links lint:lang lint:labels",
139
+ "lint": "run-s lint:format lint:markdown lint:addresses lint:content-links lint:lang lint:labels lint:changelog",
139
140
  "lint:format": "content-build format",
140
141
  "lint:markdown": "content-build markdown",
141
142
  "lint:markdown:fix": "content-build markdown --fix",
142
143
  "lint:addresses": "content-build lint",
143
144
  "lint:content-links": "content-build links",
144
145
  "lint:lang": "package-build lang check",
145
- "lint:labels": "package-build labels check"
146
+ "lint:labels": "package-build labels check",
147
+ "lint:changelog": "package-build changelog check"
146
148
  ```
147
149
 
148
150
  - **`lint:format`** — is every file formatted to the shared Prettier options?
@@ -165,6 +167,10 @@ failure, and knowing _which_ question failed is most of the diagnosis.
165
167
  - **`lint:labels`** — do `.github/labels.yml` and §3 of
166
168
  `.github/ISSUE_REPORTING.md` still list the same labels? Neither derives from
167
169
  the other, so nothing else notices when they drift.
170
+ - **`lint:changelog`** — does every pending `.changeset/*.md` read like a
171
+ release note rather than a pasted pull-request description? Passes
172
+ trivially when nothing is pending: no pull request is required to carry a
173
+ changeset, only to write one that reads well when it does.
168
174
 
169
175
  `run-s` comes from `npm-run-all`, a devDependency. It runs scripts in sequence
170
176
  and stops at the first failure, which is what makes a named chain readable.
@@ -227,16 +233,19 @@ reads better with one and a CI workflow reads better with the other.
227
233
 
228
234
  ```json
229
235
  "changeset": "changeset",
230
- "changeset:check": "changeset status --since=origin/main",
231
236
  "changeset:version": "changeset version && npm install --package-lock-only"
232
237
  ```
233
238
 
234
- `changeset` adds one. `changeset:check` reports what is pending against `main`,
235
- which is what a CI job gates on so that a behaviour change cannot merge without
236
- declaring its bump. `changeset:version` consumes the pending changesets, writes
237
- `CHANGELOG.md` and bumps `package.json` and the `npm install --package-lock-only`
238
- after it is what keeps `package-lock.json`'s recorded version in step, since
239
- changesets does not touch the lockfile.
239
+ `changeset` adds one, written only when a consumer will notice the change —
240
+ nothing gates a pull request on carrying one. `changeset:version` consumes the
241
+ pending changesets, writes `CHANGELOG.md` and bumps `package.json` and the
242
+ `npm install --package-lock-only` after it is what keeps `package-lock.json`'s
243
+ recorded version in step, since changesets does not touch the lockfile.
244
+
245
+ What a pending changeset is held to is quality, not existence: `lint:changelog`
246
+ and the `pre-commit` hook `prepare` installs both run
247
+ `package-build changelog check` against it, in CI and at commit time
248
+ respectively, and both pass trivially when there is none.
240
249
 
241
250
  ### The site scripts
242
251