@heroiclands/package-build 22.11.0 → 22.12.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @heroiclands/package-build
2
2
 
3
+ ## 22.12.0
4
+
5
+ ### Minor Changes
6
+
7
+ - **References:** An Address in note data that names no local or declared dependency target now reports an error; missing artwork keeps its fallback warning.
8
+
3
9
  ## 22.11.0
4
10
 
5
11
  ### Minor Changes
package/CONTENT.md CHANGED
@@ -1249,6 +1249,12 @@ somewhere other than where they were made, or not at all:
1249
1249
  reference answered by a vendored manifest lands exactly as it would in a
1250
1250
  wikilink. `--no-references` turns this one off for a tree whose cross-package
1251
1251
  references it cannot see.
1252
+ - **Dead Address reference** — an Address-valued `data:` field naming no local
1253
+ note, asset or entry in a declared dependency index. The finding names the
1254
+ field and the complete Address it names. A short form means this package;
1255
+ another package requires the full Address. `--no-references` also turns off
1256
+ this target check. Art slots report unresolved art through their own fallback
1257
+ warning.
1252
1258
 
1253
1259
  **A schema says what a note may _write_, not what the compiler emits.** Those
1254
1260
  are different, and the difference is the whole calibration of the check: a note
@@ -1315,6 +1315,12 @@ Address-keyed relation maps hold typed targets and standing values. Only generat
1315
1315
  output serializes these properties to Address strings. Runtime Shortcodes,
1316
1316
  Foundry UUIDs, file paths and URLs keep their own representations.
1317
1317
 
1318
+ An Address-valued `data:` field must name a target in this package or a declared
1319
+ dependency index. `content-build lint` reports a missing target as an error at
1320
+ the value or map key that names it. A stub counts as a target for data even
1321
+ though it publishes no page. Art slots use their own missing-art warning and
1322
+ document fallback.
1323
+
1318
1324
  #### An omitted segment defaults from where the link is written
1319
1325
 
1320
1326
  It is **not** a wildcard and resolution is not a search. Every short form expands
@@ -3015,9 +3021,7 @@ If `sohl` is present, this becomes an `affliction` item.
3015
3021
  | `subType` | `system.subType` | NA |
3016
3022
  | `data.transmission` | `system.transmission` | NA |
3017
3023
  | `data.outcome` | `system.outcome` | NA |
3018
- | `data.healingRate` | `system.healingRateBase` | NA |
3019
3024
  | `data.contagionIndex` | `system.contagionIndexBase` | NA |
3020
- | `data.outcomeTraumas` | `system.outcomeTraumas` | NA |
3021
3025
  | `data.onsetDurationFormula` | `system.onsetDurationFormula` | NA |
3022
3026
  | `data.onsetDurationBase` | `system.onsetDurationBase` | NA |
3023
3027
  | `data.healingCheckDurationFormula` | `system.healingCheckDurationFormula` | NA |
@@ -3055,6 +3059,10 @@ that writes one fails the build. They are world times, and `0` is a valid one,
3055
3059
  so there is no blank a note could write either; leave them out and the data
3056
3060
  model's `null` stands.
3057
3061
 
3062
+ `data.healingRate` and `data.outcomeTraumas` are page data. SoHL reads an
3063
+ affliction’s base healing rate from `sohl.healingRateBase`. Its item declaration
3064
+ does not read `data.outcomeTraumas`.
3065
+
3058
3066
  ### type: armorgear
3059
3067
 
3060
3068
  Note: `data.quantity` may not be specified. Quantity is always 1.
@@ -3129,7 +3137,9 @@ if a `sohl` property is present, a SoHL item of type "concoctiongear" will be cr
3129
3137
  | `data.value` | `system.valueBase` | NA |
3130
3138
  | `data.quality` | `system.qualityBase` | NA |
3131
3139
  | `data.durability` | `system.durabilityBase` | NA |
3132
- | `data.quantity` | `system.quantity` | NA |
3140
+
3141
+ `data.quantity` is page data for a concoction. SoHL sets `system.quantity` to
3142
+ `1` for each compendium item.
3133
3143
 
3134
3144
  ### type: containergear
3135
3145
 
@@ -3142,19 +3152,21 @@ Note: `data.quantity` may not be specified; quantity is always set to 1.
3142
3152
  | `value` | `number` | Gear value |
3143
3153
  | `quality` | `number` | Gear quality |
3144
3154
  | `durability` | `number` | Gear durability |
3145
- | `capacity` | `number` | Container capacity (in lbs) |
3155
+ | `capacity` | `number` | HM3 container capacity (in lbs) |
3146
3156
 
3147
3157
  if a `sohl` property is present, a SoHL item of type "containergear" will be created.
3148
3158
 
3149
3159
  if a `hm3` property is present, an HM3 item of type "containergear" will be created.
3150
3160
 
3151
- | shared source | → sohl | → hm3 |
3152
- | ----------------- | ------------------------ | --------------------- |
3153
- | `data.weight` | `system.weightBase` | `system.weight` |
3154
- | `data.value` | `system.valueBase` | `system.value` |
3155
- | `data.quality` | `system.qualityBase` | NA |
3156
- | `data.durability` | `system.durabilityBase` | NA |
3157
- | `data.capacity` | `system.maxCapacityBase` | `system.capacity.max` |
3161
+ | shared source | → sohl | → hm3 |
3162
+ | ----------------- | ----------------------- | --------------------- |
3163
+ | `data.weight` | `system.weightBase` | `system.weight` |
3164
+ | `data.value` | `system.valueBase` | `system.value` |
3165
+ | `data.quality` | `system.qualityBase` | NA |
3166
+ | `data.durability` | `system.durabilityBase` | NA |
3167
+ | `data.capacity` | NA | `system.capacity.max` |
3168
+
3169
+ SoHL reads a container’s capacity in pounds from `sohl.maxCapacity`.
3158
3170
 
3159
3171
  ### type: miscgear
3160
3172
 
@@ -3165,7 +3177,7 @@ if a `hm3` property is present, an HM3 item of type "containergear" will be crea
3165
3177
  | `value` | `number` | Gear value |
3166
3178
  | `quality` | `number` | Gear quality |
3167
3179
  | `durability` | `number` | Gear durability |
3168
- | `quantity` | `number` | Gear quantity (default: 1) |
3180
+ | `quantity` | `number` | HM3 gear quantity (default: 1) |
3169
3181
 
3170
3182
  if a `sohl` property is present, a SoHL item of type "miscgear" will be created.
3171
3183
 
@@ -3177,7 +3189,9 @@ if a `hm3` property is present, an HM3 item of type "miscgear" will be created.
3177
3189
  | `data.value` | `system.valueBase` | `system.value` |
3178
3190
  | `data.quality` | `system.qualityBase` | NA |
3179
3191
  | `data.durability` | `system.durabilityBase` | NA |
3180
- | `data.quantity` | `system.quantity` | `system.quantity` |
3192
+ | `data.quantity` | NA | `system.quantity` |
3193
+
3194
+ SoHL sets `system.quantity` to `1` for each compendium item.
3181
3195
 
3182
3196
  ### type: mystery
3183
3197
 
@@ -3213,10 +3227,12 @@ if a `sohl` property is present, a SoHL item of type "mystery" will be created.
3213
3227
  | --------------------- | ----------------------- | ----- |
3214
3228
  | `subType` | `system.subType` | NA |
3215
3229
  | `data.skillAptitudes` | `system.skillAptitudes` | NA |
3216
- | `data.level` | `system.levelBase` | NA |
3217
3230
  | `data.charges.value` | `system.charges.value` | NA |
3218
3231
  | `data.charges.max` | `system.charges.max` | NA |
3219
3232
 
3233
+ `data.level` is page data for a mystery. SoHL reads the item’s level from
3234
+ `sohl.levelBase`.
3235
+
3220
3236
  ### type: mysticalability
3221
3237
 
3222
3238
  **subType**:
@@ -3250,13 +3266,15 @@ If an `hm3` property is present, an HM3 item is created, and `hm3.type` states w
3250
3266
  `sohl.system` as shortcodes and have no shared source; `data.assocSkill` and
3251
3267
  `data.assocAffiliation` name the same two notes as Addresses, for the page.
3252
3268
 
3253
- | shared source | → sohl | → hm3 |
3254
- | -------------------- | ------------------------- | ------------- |
3255
- | `subType` | `system.subType` | **see above** |
3256
- | `data.masteryLevel` | `system.masteryLevelBase` | NA |
3257
- | `data.level` | `system.levelBase` | NA |
3258
- | `data.charges.value` | `system.charges.value` | NA |
3259
- | `data.charges.max` | `system.charges.max` | NA |
3269
+ | shared source | → sohl | → hm3 |
3270
+ | -------------------- | ---------------------- | ------------- |
3271
+ | `subType` | `system.subType` | **see above** |
3272
+ | `data.charges.value` | `system.charges.value` | NA |
3273
+ | `data.charges.max` | `system.charges.max` | NA |
3274
+
3275
+ `data.masteryLevel` and `data.level` are page data for an ability. SoHL reads
3276
+ the item’s base mastery and level from `sohl.masteryLevelBase` and
3277
+ `sohl.levelBase`.
3260
3278
 
3261
3279
  ### type: projectilegear
3262
3280
 
@@ -3276,7 +3294,7 @@ If an `hm3` property is present, an HM3 item is created, and `hm3.type` states w
3276
3294
  | `value` | `number` | Gear value |
3277
3295
  | `quality` | `number` | Gear quality |
3278
3296
  | `durability` | `number` | Gear durability |
3279
- | `quantity` | `number` | Gear quantity (default: 1) |
3297
+ | `quantity` | `number` | HM3 gear quantity (default: 1) |
3280
3298
 
3281
3299
  if a `sohl` property is present, a SoHL item of type "projectilegear" will be created.
3282
3300
 
@@ -3293,7 +3311,9 @@ HM3 side while remaining distinct on the SoHL side.
3293
3311
  | `data.value` | `system.valueBase` | `system.value` |
3294
3312
  | `data.quality` | `system.qualityBase` | NA |
3295
3313
  | `data.durability` | `system.durabilityBase` | NA |
3296
- | `data.quantity` | `system.quantity` | `system.quantity` |
3314
+ | `data.quantity` | NA | `system.quantity` |
3315
+
3316
+ SoHL sets `system.quantity` to `1` for each compendium item.
3297
3317
 
3298
3318
  ### type: skill
3299
3319
 
@@ -3313,7 +3333,7 @@ HM3 side while remaining distinct on the SoHL side.
3313
3333
  | `data` property | Values | Description |
3314
3334
  | ------------------ | --------- | ------------------------------------------ |
3315
3335
  | `templatePriority` | `number` | Template priority, _null_ = not a template |
3316
- | `masteryLevel` | `number` | Mastery Level |
3336
+ | `masteryLevel` | `number` | HM3 mastery level |
3317
3337
  | `parentSkill` | `Address` | Parent skill this skill specializes |
3318
3338
 
3319
3339
  if a `sohl` property is present, a SoHL item of type "skill" will be created.
@@ -3326,10 +3346,12 @@ Note: `hm3.system.type` (skill types) use the values "Craft", "Physical", "Commu
3326
3346
  skill this one specializes, and has no shared source; `data.parentSkill` names that
3327
3347
  skill as an Address, for the page.
3328
3348
 
3329
- | shared source | → sohl | → hm3 |
3330
- | ------------------- | ------------------------- | --------------------- |
3331
- | `subType` | `system.subType` | See notes above |
3332
- | `data.masteryLevel` | `system.masteryLevelBase` | `system.masteryLevel` |
3349
+ | shared source | → sohl | → hm3 |
3350
+ | ------------------- | ---------------- | --------------------- |
3351
+ | `subType` | `system.subType` | See notes above |
3352
+ | `data.masteryLevel` | NA | `system.masteryLevel` |
3353
+
3354
+ SoHL reads a skill’s base mastery from `sohl.masteryLevelBase`.
3333
3355
 
3334
3356
  ### type: trauma
3335
3357
 
@@ -68,7 +68,7 @@ import { positionInFrontmatter, positionOfFrontmatterPath } from "./diagnostics.
68
68
  import { pathnameProblem } from "./pathnames.mjs";
69
69
  import { checkHomepageAddressFields } from "./homepage.mjs";
70
70
  import { RETIRED_TYPES, RENAMED_TYPES, currentType, renamedTypeMessage } from "./ids.mjs";
71
- import { parseAddress, acceptsType } from "./address.mjs";
71
+ import { parseAddress, acceptsType, renderAddress } from "./address.mjs";
72
72
  import { ASSET_TYPE_NAMES } from "./asset-types.mjs";
73
73
  import { isAddressSegment } from "./address-charset.mjs";
74
74
  // The one place the "every pack not named" key is spelled. Imported rather
@@ -421,7 +421,7 @@ function dataBlock(fm) {
421
421
  * was loaded to recognise it would be worse than not checking.
422
422
  * @returns {object[]} Findings.
423
423
  */
424
- function checkDataContainer(note, { type, fields, packs, addressContext }) {
424
+ function checkDataContainer(note, { type, fields, packs, addressContext, index }) {
425
425
  const findings = [];
426
426
  const { present, entries, malformed } = dataBlock(note.fm ?? {});
427
427
  if (!present) return findings;
@@ -504,7 +504,7 @@ function checkDataContainer(note, { type, fields, packs, addressContext }) {
504
504
  });
505
505
  continue;
506
506
  }
507
- findings.push(...checkDataReferences(note, field, value, segments, addressContext));
507
+ findings.push(...checkDataReferences(note, field, value, segments, addressContext, index));
508
508
  // A `scalar-or-map` written in its map form is checked entry by entry,
509
509
  // because that is the correction an author has to make: one key's
510
510
  // value, not the whole map. Quoting the map back would name every
@@ -521,15 +521,16 @@ function checkDataContainer(note, { type, fields, packs, addressContext }) {
521
521
  }
522
522
 
523
523
  /**
524
- * Validate typed entries without resolving their targets.
524
+ * Validate typed entries and resolve declared non-art Address targets.
525
525
  * @param {object} note - The authored note.
526
526
  * @param {object} field - Its data field declaration.
527
527
  * @param {unknown} value - The field value.
528
528
  * @param {string[]} segments - The field's path under `data`.
529
529
  * @param {object} context - The builder's Address context.
530
+ * @param {object} [index] - The local and declared dependency Address index.
530
531
  * @returns {object[]} Located findings.
531
532
  */
532
- function checkDataReferences(note, field, value, segments, context) {
533
+ function checkDataReferences(note, field, value, segments, context, index) {
533
534
  const checks = [];
534
535
  if (field.kind === "address") checks.push({ value, path: [], kind: "address" });
535
536
  if (field.entryKind) {
@@ -573,10 +574,16 @@ function checkDataReferences(note, field, value, segments, context) {
573
574
  let reason;
574
575
  if (!matchesKind(check.value, check.kind, defaults))
575
576
  reason = `should be ${check.kind === "address" ? "an Address" : "a Shortcode"}`;
576
- else if (check.kind === "address" && accepted) {
577
+ else if (check.kind === "address") {
577
578
  const tuple = parseAddress(check.value, defaults, { declared: true });
578
- if (!acceptsType(tuple, accepted))
579
+ if (accepted && !acceptsType(tuple, accepted))
579
580
  reason = `accepts ${accepted.join(" or ")}, not ${tuple.type}`;
581
+ else if (
582
+ index?.addressHit &&
583
+ !ART_SLOTS.some((slot) => slot.key === field.name) &&
584
+ !index.addressHit(renderAddress(tuple))
585
+ )
586
+ reason = `names ${renderAddress(tuple)}, which does not resolve in this package or its declared dependencies`;
580
587
  }
581
588
  if (!reason) continue;
582
589
  const path = ["data", ...segments, ...check.path];
@@ -1519,6 +1526,7 @@ export function lintNote(
1519
1526
  fields,
1520
1527
  packs,
1521
1528
  addressContext: referenceContext,
1529
+ index,
1522
1530
  }),
1523
1531
  );
1524
1532
  findings.push(...checkSubType(note, { type, entry }));
@@ -124,15 +124,12 @@ export const HELD_SUBTYPES = Object.freeze(["settlement", "site", "structure"]);
124
124
  * system, only the shortcode {@link module:engine/address} would resolve it
125
125
  * to anyway.
126
126
  *
127
- * **The match this shortcode feeds is deliberately package-blind.** `places`
128
- * and `affiliations` merge this package's own notes with every fetched
129
- * dependency's into one shortcode space, first declaration wins — the way a
130
- * dependency's generic geography attaches to whatever a consumer calls the
131
- * same shortcode, which is the point of _A dependency's places and
132
- * affiliations take part_ above. Comparing the full Address instead would
133
- * ask an entry's package to agree with the citing note's, and a fetched
134
- * entry's own `parents` or `domains` is written in *its* package while the
135
- * blend it takes part in is *this* one's — see
127
+ * **The holdings graph has package-blind Shortcode identity.** `places` and
128
+ * `affiliations` merge local and fetched notes into one shortcode space; the
129
+ * first declaration wins. A fetched place attaches to a local parent with
130
+ * the same shortcode, and a fetched affiliation can name both local and
131
+ * fetched domains. The authored entries remain Addresses; this graph uses
132
+ * their shortcode projection for containment and tenure. See
136
133
  * `tests/site-holdings.test.ts`'s `empire.domains: ["mill", "abroad"]`, where
137
134
  * `mill` is local and `abroad` is the dependency's own, both bare, both
138
135
  * correct only because neither package is asked.
@@ -321,6 +318,7 @@ export function holdingsPages(nodes) {
321
318
  // Read through the one reader, so a caller handing in what a note
322
319
  // wrote — an address, a wikilink, a repeat — is read as `holdingsNode`
323
320
  // reads it.
321
+ // Shortcode identity: the first node of each type owns this graph key.
324
322
  by.set(shortcode, {
325
323
  ...node,
326
324
  shortcode,
@@ -108,15 +108,14 @@ const PLACE_TYPES = Object.freeze(new Set(["place"]));
108
108
  * since this position asks neither its package nor its system, only the
109
109
  * shortcode {@link module:engine/address} would resolve it to anyway.
110
110
  *
111
- * **The match this shortcode feeds is deliberately package-blind.**
112
- * `world.places` merges this package's own places with every fetched
113
- * dependency's into one shortcode space, the way a dependency's place
114
- * attaches to the local containment tree it is drawn beside — see
111
+ * **The map graph has package-blind Shortcode identity.** `world.places`
112
+ * merges local and fetched places by shortcode, with local declarations
113
+ * taking precedence. A fetched place attaches to the local containment tree
114
+ * by its parent's shortcode. The authored parent remains an Address; the map
115
+ * uses its shortcode projection for containment, layout, and DOT nodes. See
115
116
  * `tests/map.test.ts`'s _loads a dependency's places beside the package's
116
117
  * own_, where a fetched place's bare `parents: ["world"]` reaches this
117
- * package's own root. Comparing the full Address instead would ask the
118
- * dependency's own package to agree with this one's, which the feature is
119
- * built to cross.
118
+ * package's own root.
120
119
  *
121
120
  * @param {unknown} parent - One `parents` entry.
122
121
  * @returns {string} Its shortcode, or `""` for an entry that names nothing.
@@ -193,6 +192,7 @@ export function placesFromRecords(records, { contentBase, base }) {
193
192
  // lint's finding.
194
193
  if (places.has(shortcode)) continue;
195
194
  const data = record.data && typeof record.data === "object" ? record.data : {};
195
+ // Shortcode identity: local places claim the map node first.
196
196
  places.set(shortcode, {
197
197
  shortcode,
198
198
  name,
@@ -243,6 +243,7 @@ function addForeignPlaces(world, foreignIndex) {
243
243
  if (entry?.type !== "place") continue;
244
244
  const shortcode = readCanonicalKey(canonical)?.shortcode?.toLowerCase() ?? "";
245
245
  if (!shortcode || world.places.has(shortcode)) continue;
246
+ // Shortcode identity: fetched places fill only unclaimed map nodes.
246
247
  world.places.set(shortcode, {
247
248
  shortcode,
248
249
  name: String(entry.name ?? shortcode),
@@ -242,8 +242,10 @@ export function foreignNode(canonical, entry) {
242
242
  /**
243
243
  * The corpus a link index describes.
244
244
  *
245
- * A shortcode is declared once per type and the first declaration wins, so a
246
- * local note shadows a dependency's the way the link resolver answers.
245
+ * Population containment and tenure use the same package-blind Shortcode
246
+ * identity as holdings and maps. Each type has one node per shortcode, with
247
+ * local notes taking precedence over fetched entries. A fetched place can
248
+ * attach to local geography, while its authored `parents` remains an Address.
247
249
  *
248
250
  * @param {object} index - The link index.
249
251
  * @returns {Corpus} The corpus.
@@ -263,6 +265,7 @@ function corpusOf(index) {
263
265
  if (!node) return;
264
266
  const by = node.type === "place" ? places : affiliations;
265
267
  if (by.has(node.shortcode)) return;
268
+ // Shortcode identity: the first node of each type owns this graph key.
266
269
  by.set(node.shortcode, node);
267
270
  // Not a canonical Address: package-blind by design, so a local place
268
271
  // shadows a foreign one of the same type and shortcode. `/` keeps that
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heroiclands/package-build",
3
- "version": "22.11.0",
3
+ "version": "22.12.0",
4
4
  "description": "Shared toolchain for building and shipping a HeroicLands Foundry VTT package — content compilation, manifest, localization, staging, bundle, release and deployment.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "type": "module",