@heroiclands/package-build 22.4.0 → 22.4.2

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.2
4
+
5
+ ### Patch Changes
6
+
7
+ - 8dedb23: **A module embedding HM3 spells, invocations or psionics compiles.** A being's
8
+ embedded `spell`, `invocation` or `psionic` reference that names no shortcode
9
+ template and no `data.icon` of its own now takes that subtype's own default
10
+ art — the icon HM3 itself assigns a freshly created item of that kind —
11
+ instead of the build refusing the actor outright.
12
+ - 54109cf: **A module embedding HârnMaster 3 weapons compiles.** A being's `(type,
13
+ shortcode)` reference into HM3's one-to-many `weapongear` row resolves when
14
+ its `type` names one of the row's own subtypes — `weapongear` or
15
+ `missilegear` — instead of being refused as ambiguous.
16
+
17
+ ## 22.4.1
18
+
19
+ ### Patch Changes
20
+
21
+ - 37d874d: Adds `package-build changelog check`, which lints a pending changeset or a
22
+ `CHANGELOG.md` release section for the marks a pull-request description
23
+ leaves behind — a commit hash, an issue reference, a code fence, a
24
+ "Verified" paragraph, a byte or test count, an over-long or nested bullet,
25
+ a stray heading, or simply too many bullets or lines for one entry — and
26
+ refuses to let one merge. The shipped `pre-commit` hook runs it on a staged
27
+ changeset automatically.
28
+ - 0007964: **A module that embeds HârnMaster 3 items resolves them from a release that
29
+ stores their shortcodes under the system's own flags.** The actor pass reads
30
+ an item's `(type, shortcode)` address from `system.shortcode` where a system's
31
+ data model declares one, and otherwise from that system's own flag namespace —
32
+ never another system's, so a catalogue extracted for one system resolves
33
+ against its own handles only.
34
+
3
35
  ## 22.4.0
4
36
 
5
37
  ### Minor Changes
package/CONTENT.md CHANGED
@@ -1784,6 +1784,13 @@ fetch time, so a cache filled before this rule existed is treated as incomplete
1784
1784
  and `deps fetch` refills it — the alternative is a lookup that answers with the
1785
1785
  wrong system's document and reports nothing.
1786
1786
 
1787
+ **Within one document, the shortcode itself comes from `system.shortcode`
1788
+ where the data model declares such a field, and otherwise from
1789
+ `flags.<systemId>.shortcode` in that same system's own flag namespace** — a
1790
+ system writes its per-document handle into its own flags and never another
1791
+ system's, so a document carrying its shortcode under a different system's flag
1792
+ namespace is not resolved by it.
1793
+
1787
1794
  **`--from` is for two packages changing together.** It fills the cache from a
1788
1795
  locally built artifact — a package zip or the directory it was built from — so a
1789
1796
  consumer can be built against a dependency that has not shipped. Without it,
@@ -53,6 +53,7 @@
53
53
  * npx package-build lang check
54
54
  * npx package-build lang coverage [--unused]
55
55
  * npx package-build lang hardcoded
56
+ * npx package-build changelog check [--release] [paths..]
56
57
  * npx package-build bundle check
57
58
  * npx package-build release
58
59
  * npx package-build deploy <stage>
@@ -88,6 +89,7 @@ import { SCHEMA_ARTIFACT_FILE } from "../engine/foreign-catalog.mjs";
88
89
  import { validateLangSource } from "../lang.mjs";
89
90
  import { checkLabelRegistry } from "../labels.mjs";
90
91
  import { lintYaml } from "../engine/yaml-lint.mjs";
92
+ import { lintChangesetText, lintReleaseText } from "../engine/changelog-lint.mjs";
91
93
  import { bumpDependencies } from "../engine/dependency-bump.mjs";
92
94
  import {
93
95
  analyzeCoverage,
@@ -776,6 +778,86 @@ function yamlCommand() {
776
778
  };
777
779
  }
778
780
 
781
+ /**
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.
790
+ *
791
+ * @returns {object} The yargs command module.
792
+ */
793
+ function changelogCommand() {
794
+ return {
795
+ command: "changelog <action> [paths..]",
796
+ describe: "Release-prose checks",
797
+ builder: (y) =>
798
+ y
799
+ .positional("action", {
800
+ choices: ["check"],
801
+ describe: "check: lint pending changesets, or a release section",
802
+ })
803
+ .positional("paths", {
804
+ describe:
805
+ "Files to check. Defaults to `.changeset/*.md` (config.json and " +
806
+ "README.md excluded), or `CHANGELOG.md` with --release.",
807
+ type: "string",
808
+ })
809
+ .option("release", {
810
+ type: "boolean",
811
+ default: false,
812
+ describe:
813
+ "Check the first `## <version>` section of CHANGELOG.md instead of " +
814
+ "pending changesets",
815
+ }),
816
+ handler: handler(async (args) => changelogCheck(args)),
817
+ };
818
+ }
819
+
820
+ /**
821
+ * The files `changelog check` reads by default: every pending changeset, or
822
+ * `CHANGELOG.md` alone under `--release`.
823
+ *
824
+ * `config.json` is excluded by the glob itself (it is not `.md`); `README.md`
825
+ * is excluded by name, since it is prose about changesets rather than one.
826
+ *
827
+ * @param {object} args - Parsed CLI arguments.
828
+ * @returns {string[]} Paths, relative to the working directory.
829
+ */
830
+ function changelogFiles(args) {
831
+ if (args.paths?.length) return args.paths;
832
+ if (args.release) return ["CHANGELOG.md"];
833
+ return globSync(".changeset/*.md", { cwd: process.cwd() }).filter(
834
+ (file) => path.basename(file) !== "README.md",
835
+ );
836
+ }
837
+
838
+ /**
839
+ * Run `changelog check` over every resolved file and report the result.
840
+ *
841
+ * @param {object} args - Parsed CLI arguments.
842
+ */
843
+ function changelogCheck(args) {
844
+ const files = changelogFiles(args);
845
+ let errors = 0;
846
+ let total = 0;
847
+ for (const file of files) {
848
+ if (!fs.existsSync(file)) die(`changelog check: ${file} does not exist.`);
849
+ const text = fs.readFileSync(file, "utf8");
850
+ const { findings } = args.release ? lintReleaseText(text) : lintChangesetText(text);
851
+ errors += reportFindings(findings, { file });
852
+ total += findings.length;
853
+ }
854
+ console.log(
855
+ `package-build: ${files.length} file(s) checked · ` +
856
+ `${errors} error(s) · ${total - errors} warning(s)`,
857
+ );
858
+ if (errors) process.exitCode = 1;
859
+ }
860
+
779
861
  /**
780
862
  * `labels check` — do the machine registry and the documented table agree?
781
863
  *
@@ -1237,6 +1319,7 @@ yargs(hideBin(process.argv))
1237
1319
  .command(labelsCommand())
1238
1320
  .command(bumpCommand())
1239
1321
  .command(yamlCommand())
1322
+ .command(changelogCommand())
1240
1323
  .command(bundleCommand())
1241
1324
  .command(releaseCommand())
1242
1325
  .command(deployCommand())
package/docs/commands.md CHANGED
@@ -611,6 +611,73 @@ package-build: 2 file(s) checked · 0 error(s) · 0 warning(s)
611
611
 
612
612
  [Diagnostics](diagnostics.md).
613
613
 
614
+ ### `package-build changelog check`
615
+
616
+ **NAME**
617
+
618
+ Lint release prose — pending changesets, or a `CHANGELOG.md` release section
619
+ — against the rules a changeset is actually held to (`check` is the only
620
+ action).
621
+
622
+ **SYNOPSIS**
623
+
624
+ ```
625
+ package-build changelog check [--release] [paths..]
626
+ ```
627
+
628
+ **DESCRIPTION**
629
+
630
+ A changeset answers one question — who notices, and what do they see — and
631
+ nothing enforced that, so a pull-request description pasted into one ships
632
+ verbatim as a release note: commit hashes, issue references, code fences,
633
+ "Verified" paragraphs, byte counts and test tallies, a paragraph disguised as
634
+ a bullet, a nested checklist, a `#` heading that outranks the version heading
635
+ above it once wrapped into a list item, or simply too many bullets or too
636
+ many lines for one entry. Each finding names the rule it tripped and says
637
+ what to write instead.
638
+
639
+ Default reads every pending changeset (`.changeset/*.md`; `config.json` and
640
+ `README.md` excluded) and checks each one's body, frontmatter fence dropped.
641
+ `--release` reads the first `## <version>` section of `CHANGELOG.md` instead
642
+ — the section a **Version Packages** branch is about to publish — with its
643
+ generated `## <version>` and `### <Bump> Changes` scaffold lines exempted
644
+ from the heading rule, since neither is authored.
645
+
646
+ A token that reads as code — `camelCase()`, a `path/with/slashes.ext`,
647
+ `SCREAMING_SNAKE` — outside any code span is a warning, not a failure: a
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.
651
+
652
+ **OPTIONS**
653
+
654
+ | Positional | Type | Default | Description |
655
+ | ----------- | --------- | ------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- |
656
+ | `paths` | string(s) | `.changeset/*.md` (`config.json`, `README.md` excluded), or `CHANGELOG.md` with `--release` | Files to check. |
657
+ | `--release` | boolean | `false` | Check the first `## <version>` section of `CHANGELOG.md` instead of pending changesets. |
658
+
659
+ **EXIT STATUS**
660
+
661
+ 1 if a named file does not exist. 1 if any finding is an error. A run with
662
+ only warnings — the code-like-token case — exits 0.
663
+
664
+ **EXAMPLES**
665
+
666
+ ```
667
+ $ package-build changelog check
668
+ package-build: 1 file(s) checked · 0 error(s) · 0 warning(s)
669
+ ```
670
+
671
+ ```
672
+ $ package-build changelog check --release
673
+ CHANGELOG.md:5:3: error: changelog-check/commit-hash a commit hash is a commit-log artefact; the changelog generator should not write one — set `changelog` to `@changesets/cli/changelog`
674
+ package-build: 1 file(s) checked · 1 error(s) · 0 warning(s)
675
+ ```
676
+
677
+ **SEE ALSO**
678
+
679
+ [Diagnostics](diagnostics.md).
680
+
614
681
  ### `package-build bundle check`
615
682
 
616
683
  **NAME**
@@ -200,13 +200,39 @@ export function catalogueKey(subType, shortcode, pkg) {
200
200
  return pkg ? packagedItemAddress(pkg, subType, folded) : itemAddress(subType, folded);
201
201
  }
202
202
 
203
+ /**
204
+ * An item's own shortcode, read off its compiled document.
205
+ *
206
+ * **`system.shortcode`**, where a system's data model declares such a field.
207
+ * Where it does not — HM3's has no such field — the handle instead lives in
208
+ * that system's own flag namespace, `flags.<systemId>.shortcode`: a system
209
+ * writes its per-document handle into its own flags and never another
210
+ * system's, so a document extracted from one system's catalogue is read
211
+ * through that system's namespace and no other. `system.shortcode` wins where
212
+ * both are present.
213
+ *
214
+ * @param {object} doc - A compiled Item document, or an embedded item merged
215
+ * from one.
216
+ * @param {string|null} [systemId] - The system whose catalogue `doc` was read
217
+ * from. Omitted or `null`, only `system.shortcode` is read.
218
+ * @returns {string|undefined} The shortcode, or `undefined` when the document
219
+ * states neither.
220
+ */
221
+ export function shortcodeOf(doc, systemId = null) {
222
+ const own = doc?.system?.shortcode;
223
+ if (own) return own;
224
+ if (!systemId) return undefined;
225
+ return doc?.flags?.[systemId]?.shortcode;
226
+ }
227
+
203
228
  /**
204
229
  * What identifies one embedded item on its actor.
205
230
  *
206
- * **Its own `system.shortcode`** — not the entry's top-level `shortcode`, which
207
- * merely *selects* the catalogue template the entry is written from and is
208
- * never written to the document. Two daggers may share a selector; they are two
209
- * embodiments and each must declare its own.
231
+ * **Its own shortcode**read by {@link shortcodeOf} — not the entry's
232
+ * top-level `shortcode`, which merely *selects* the catalogue template the
233
+ * entry is written from and is never written to the document. Two daggers
234
+ * may share a selector; they are two embodiments and each must declare its
235
+ * own.
210
236
  *
211
237
  * The name is a last resort, for a **stand-alone** entry that names no template
212
238
  * and states no shortcode. It is a poor identity — presentation, and free to be
@@ -215,10 +241,13 @@ export function catalogueKey(subType, shortcode, pkg) {
215
241
  * message says to state a `system.shortcode`.
216
242
  *
217
243
  * @param {object} item - The merged embedded item.
244
+ * @param {string|null} [systemId] - The system this item's document was
245
+ * compiled or extracted for, so a document whose own data model carries no
246
+ * `system.shortcode` field is still read by its own flag namespace.
218
247
  * @returns {string} The identity, for {@link embeddedItemId}.
219
248
  */
220
- export function embeddedIdentity(item) {
221
- const own = item?.system?.shortcode;
249
+ export function embeddedIdentity(item, systemId = null) {
250
+ const own = shortcodeOf(item, systemId);
222
251
  if (typeof own === "string" && own.trim()) return own.trim();
223
252
  return typeof item?.name === "string" ? item.name : "";
224
253
  }
@@ -253,9 +282,9 @@ export function embeddedItemId(actorId, subType, identity) {
253
282
  /**
254
283
  * Load every JSON file under each of `itemsSourceDirs`, returning one Map keyed
255
284
  * by {@link itemAddress} — the compiled document's **subtype** and its
256
- * `system.shortcode`. Folder docs and entries without a shortcode are skipped.
257
- * The `_key` field is stripped from each entry — it is not part of the item
258
- * data model.
285
+ * shortcode, read by {@link shortcodeOf}. Folder docs and entries without a
286
+ * shortcode are skipped. The `_key` field is stripped from each entry — it is
287
+ * not part of the item data model.
259
288
  *
260
289
  * The directories are read as one address space, because an actor names an item
261
290
  * by `(type, shortcode)` and never by the pack it happens to ship in. Two local
@@ -269,12 +298,22 @@ export function embeddedItemId(actorId, subType, identity) {
269
298
  * colliding with it. Local directories are therefore read first, and anything
270
299
  * already claimed is left alone.
271
300
  *
301
+ * **Each foreign directory reads its own flag namespace.** A foreign entry's
302
+ * `package` is the system whose catalogue it was extracted from, and that is
303
+ * the only namespace {@link shortcodeOf} is asked to fall back to for it — a
304
+ * document carrying another system's flag, sitting in this system's catalogue,
305
+ * is exactly the defect a system writing outside its own namespace produces,
306
+ * and is silently skipped rather than resolved.
307
+ *
272
308
  * @param {readonly string[]} itemsSourceDirs - Every local Item pack's JSON tree.
273
309
  * @param {readonly string[]} [foreignSourceDirs] - Extracted dependency
274
310
  * catalogues, consulted only for addresses no local pack defines.
311
+ * @param {string|null} [system] - The system `itemsSourceDirs` were compiled
312
+ * for, so a local document whose data model carries no `system.shortcode`
313
+ * field is still read by its own flag namespace.
275
314
  * @returns {Map<string, object>} The predefined items, by address.
276
315
  */
277
- export function loadItemsMap(itemsSourceDirs, foreignSourceDirs = []) {
316
+ export function loadItemsMap(itemsSourceDirs, foreignSourceDirs = [], system = null) {
278
317
  const map = new Map();
279
318
  const source = new Map();
280
319
  const shadowed = [];
@@ -307,7 +346,7 @@ export function loadItemsMap(itemsSourceDirs, foreignSourceDirs = []) {
307
346
  });
308
347
  continue;
309
348
  }
310
- const shortcode = doc?.system?.shortcode;
349
+ const shortcode = shortcodeOf(doc, system);
311
350
  if (!doc?.type || !shortcode) continue;
312
351
  const address = catalogueKey(doc.type, shortcode);
313
352
  const owner = source.get(address);
@@ -349,7 +388,7 @@ export function loadItemsMap(itemsSourceDirs, foreignSourceDirs = []) {
349
388
  });
350
389
  continue;
351
390
  }
352
- const shortcode = doc?.system?.shortcode;
391
+ const shortcode = shortcodeOf(doc, foreignPackage);
353
392
  if (!doc?.type || !shortcode) continue;
354
393
  const address = catalogueKey(doc.type, shortcode);
355
394
  // eslint-disable-next-line no-unused-vars
@@ -574,7 +613,7 @@ export class SystemActorCompiler extends BasePackCompiler {
574
613
  */
575
614
  async prepare() {
576
615
  await super.prepare();
577
- this.itemsMap = loadItemsMap(this.itemsSourceDirs, this.foreignSourceDirs);
616
+ this.itemsMap = loadItemsMap(this.itemsSourceDirs, this.foreignSourceDirs, this.system);
578
617
  log.info(`Loaded ${this.itemsMap.size} predefined items for actor resolution`);
579
618
  }
580
619
 
@@ -607,6 +646,29 @@ export class SystemActorCompiler extends BasePackCompiler {
607
646
  return referencedSubtype(this.documentSubtypes, type, "Item");
608
647
  }
609
648
 
649
+ /**
650
+ * The default art for an embedded item's type, when the entry names none
651
+ * of its own and copies no template that carries one.
652
+ *
653
+ * **The base case is {@link itemArt}**, keyed by the note vocabulary — the
654
+ * same table an item note's own compile defaults from, because most
655
+ * references are written in that vocabulary too (`weapongear`, `skill`,
656
+ * `armorgear`). It is not the whole answer: a reference into a one-to-many
657
+ * row may instead name one of the row's own **subtypes** directly — HM3's
658
+ * `spell`, `invocation` and `psionic` are never a note's own `type`, only
659
+ * `mysticalability`'s `hm3.type` discriminator ever writes them, so no
660
+ * note-type table has a row for them. A system whose one-to-many rows are
661
+ * addressed that way overrides this to answer for those subtypes too; see
662
+ * `hm3/actors.mjs`.
663
+ *
664
+ * @param {string} type - The **note** type the reference names.
665
+ * @param {string} subType - The document subtype it resolved to.
666
+ * @returns {string} The default image path.
667
+ */
668
+ embeddedItemArt(type, subType) {
669
+ return itemArt(type, this.system);
670
+ }
671
+
610
672
  /**
611
673
  * Read an entry's `model:` — the address of the item it is a copy of.
612
674
  *
@@ -786,8 +848,10 @@ export class SystemActorCompiler extends BasePackCompiler {
786
848
  // default answers last. Nullish coalescing throughout, so `icon: ""`
787
849
  // ships blank on purpose rather than collecting a default.
788
850
  merged.img =
789
- this.artPath(overlay ?? {}, "icon") ?? merged.img ?? itemArt(type, this.system);
790
- const identity = embeddedIdentity(merged);
851
+ this.artPath(overlay ?? {}, "icon") ??
852
+ merged.img ??
853
+ this.embeddedItemArt(type, /** @type {string} */ (subType));
854
+ const identity = embeddedIdentity(merged, this.system);
791
855
  const claim = `${actorId}\u0000${itemAddress(/** @type {string} */ (subType), identity)}`;
792
856
  const first = this.#embeddedClaims.get(claim);
793
857
  if (first !== undefined) {