@heroiclands/package-build 18.1.0 → 18.1.1

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,31 @@
1
1
  # @heroiclands/package-build
2
2
 
3
+ ## 18.1.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 029cf03: **A homepage note is no longer refused for an `id` it did not author** (#319).
8
+
9
+ `resolveNoteId` fills `fm.id` **in place**, so every downstream reader sees one
10
+ derived value — deliberate, and documented as such. The homepage refusal
11
+ iterated that same object, so a note that authors no `id` was reported with a
12
+ message telling the author to delete a field that is not in the file.
13
+
14
+ It was an **error**, so it failed `lint:addresses`, and `lint` heads the build
15
+ chain — which meant it failed every pull request opened against a repository
16
+ carrying a homepage note, whatever that pull request changed.
17
+
18
+ A refused field must now be one the note actually wrote. The caller already owns
19
+ the raw note text and already positions these findings with it, so it answers
20
+ which keys are declared at the note's own top level; `positionInFrontmatter`'s
21
+ `topLevel` option is the existing helper for exactly that question, so a nested
22
+ `id:` under some other key is not mistaken for the note's own. With no answer
23
+ supplied, every key in `fm` still counts — the previous behaviour, and the right
24
+ one for a caller holding authored frontmatter only.
25
+
26
+ Measured on `sohl`: `lint:addresses` goes from one error to **clean across all
27
+ 1,685 notes**.
28
+
3
29
  ## 18.1.0
4
30
 
5
31
  ### Minor Changes
@@ -890,7 +890,18 @@ export function lintNote(
890
890
  // so the finding must survive the early returns below.
891
891
  findings.push(...checkTags(note, { type }));
892
892
 
893
- for (const { locator, message } of checkHomepageAddressFields(fm)) {
893
+ // A refused field must be one the note *wrote*: `resolveNoteId` fills
894
+ // `fm.id` in place, so the parsed frontmatter carries a derived id the
895
+ // author never typed (#319). The raw text is the only place that
896
+ // distinguishes them, and `positionInFrontmatter` already answers it —
897
+ // `topLevel` so a nested `id:` under some other key is not mistaken for the
898
+ // note's own.
899
+ const authoredAtTopLevel = (key) =>
900
+ positionInFrontmatter(note.raw ?? "", key, undefined, { topLevel: true }).line !==
901
+ undefined;
902
+ for (const { locator, message } of checkHomepageAddressFields(fm, {
903
+ isAuthored: authoredAtTopLevel,
904
+ })) {
894
905
  findings.push({
895
906
  file: note.file,
896
907
  ...at(locator.key, locator.literal),
@@ -192,12 +192,24 @@ export const HOMEPAGE_REFUSED_FIELDS = Object.freeze(
192
192
  * belong to the caller. This mirrors {@link module:engine/retired-fields},
193
193
  * whose retired-field messages are likewise positioned by whoever reports them.
194
194
  *
195
+ * **A refused field must be one the note *wrote*.** `resolveNoteId` fills
196
+ * `fm.id` **in place** so every downstream reader sees one derived value —
197
+ * deliberately, and documented as such — and this ran over the same object, so
198
+ * a homepage that authors no `id` was told to delete one that is not there
199
+ * (#319). Since the caller already owns the raw note text, it also answers
200
+ * which keys the note actually declared; without an answer every key in `fm`
201
+ * counts, which is the old behaviour and right for a caller holding authored
202
+ * frontmatter only.
203
+ *
195
204
  * @param {object|null|undefined} fm - Parsed frontmatter.
205
+ * @param {object} [options] - Options.
206
+ * @param {(key: string) => boolean} [options.isAuthored] - Whether the note
207
+ * declares this key at its own top level. Defaults to "every key in `fm`".
196
208
  * @returns {Array<{field: string, locator: {key: string, literal?: string},
197
209
  * message: string}>} One entry per finding, empty for any note that is not a
198
210
  * homepage and declares nothing wrong.
199
211
  */
200
- export function checkHomepageAddressFields(fm) {
212
+ export function checkHomepageAddressFields(fm, { isAuthored } = {}) {
201
213
  if (!isHomepage(fm)) return [];
202
214
  const out = [];
203
215
 
@@ -218,7 +230,9 @@ export function checkHomepageAddressFields(fm) {
218
230
 
219
231
  for (const key of Object.keys(fm)) {
220
232
  const message = HOMEPAGE_REFUSED_FIELDS.get(key);
221
- if (message) out.push({ field: key, locator: { key }, message });
233
+ if (!message) continue;
234
+ if (isAuthored && !isAuthored(key)) continue;
235
+ out.push({ field: key, locator: { key }, message });
222
236
  }
223
237
  return out;
224
238
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heroiclands/package-build",
3
- "version": "18.1.0",
3
+ "version": "18.1.1",
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",
@@ -42,12 +42,26 @@ export function isHomepage(fm: object | null | undefined): boolean;
42
42
  * belong to the caller. This mirrors {@link module:engine/retired-fields},
43
43
  * whose retired-field messages are likewise positioned by whoever reports them.
44
44
  *
45
+ * **A refused field must be one the note *wrote*.** `resolveNoteId` fills
46
+ * `fm.id` **in place** so every downstream reader sees one derived value —
47
+ * deliberately, and documented as such — and this ran over the same object, so
48
+ * a homepage that authors no `id` was told to delete one that is not there
49
+ * (#319). Since the caller already owns the raw note text, it also answers
50
+ * which keys the note actually declared; without an answer every key in `fm`
51
+ * counts, which is the old behaviour and right for a caller holding authored
52
+ * frontmatter only.
53
+ *
45
54
  * @param {object|null|undefined} fm - Parsed frontmatter.
55
+ * @param {object} [options] - Options.
56
+ * @param {(key: string) => boolean} [options.isAuthored] - Whether the note
57
+ * declares this key at its own top level. Defaults to "every key in `fm`".
46
58
  * @returns {Array<{field: string, locator: {key: string, literal?: string},
47
59
  * message: string}>} One entry per finding, empty for any note that is not a
48
60
  * homepage and declares nothing wrong.
49
61
  */
50
- export function checkHomepageAddressFields(fm: object | null | undefined): Array<{
62
+ export function checkHomepageAddressFields(fm: object | null | undefined, { isAuthored }?: {
63
+ isAuthored?: ((key: string) => boolean) | undefined;
64
+ }): Array<{
51
65
  field: string;
52
66
  locator: {
53
67
  key: string;