@heroiclands/package-build 8.1.0 → 10.0.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.
Files changed (122) hide show
  1. package/CHANGELOG.md +748 -0
  2. package/CONTENT.md +273 -13
  3. package/bin/content-build.mjs +479 -123
  4. package/bin/package-build.mjs +27 -69
  5. package/bin/report.mjs +1 -2
  6. package/bundle.mjs +2 -10
  7. package/config.mjs +31 -106
  8. package/container.mjs +13 -57
  9. package/content-config.mjs +300 -188
  10. package/coverage.mjs +14 -55
  11. package/deploy.mjs +4 -13
  12. package/docs/content-format.md +1418 -0
  13. package/e2e.mjs +16 -55
  14. package/engine/address-charset.mjs +62 -0
  15. package/engine/address-diff.mjs +1 -4
  16. package/engine/alias-index.mjs +153 -0
  17. package/engine/base-compiler.mjs +203 -31
  18. package/engine/code-fences.mjs +4 -13
  19. package/engine/compendiums.mjs +13 -37
  20. package/engine/content-address.mjs +6 -10
  21. package/engine/content-format-check.mjs +570 -0
  22. package/engine/content-format.mjs +253 -0
  23. package/engine/content-links.mjs +144 -99
  24. package/engine/content-lint.mjs +12 -16
  25. package/engine/content-slug.mjs +2 -6
  26. package/engine/content-tables.mjs +26 -79
  27. package/engine/diagnostics.mjs +37 -15
  28. package/engine/document-subtypes.mjs +440 -0
  29. package/engine/field-reference.mjs +6 -20
  30. package/engine/field-spec.mjs +49 -45
  31. package/engine/foreign-catalog.mjs +7 -22
  32. package/engine/foreign-manifests.mjs +1 -4
  33. package/engine/frontmatter-lint.mjs +347 -43
  34. package/engine/frontmatter.mjs +3 -8
  35. package/engine/generate.mjs +36 -20
  36. package/engine/helpers.mjs +54 -81
  37. package/engine/homepage.mjs +4 -15
  38. package/engine/ids.mjs +21 -12
  39. package/engine/index.mjs +15 -0
  40. package/engine/item-registry.mjs +72 -9
  41. package/engine/journals.mjs +4 -14
  42. package/engine/kb-manifest.mjs +41 -24
  43. package/engine/macros.mjs +2 -10
  44. package/engine/manifest-emit.mjs +6 -17
  45. package/engine/map-notes.mjs +53 -87
  46. package/engine/note-claims.mjs +383 -0
  47. package/engine/note-package.mjs +1 -4
  48. package/engine/note-vocabulary.mjs +678 -0
  49. package/engine/pack-config.mjs +56 -60
  50. package/engine/pack-router.mjs +18 -8
  51. package/engine/prose-config.mjs +20 -4
  52. package/engine/prose-lint.mjs +61 -17
  53. package/engine/region-events.mjs +1 -3
  54. package/engine/retired-fields.mjs +117 -3
  55. package/engine/scene-levels.mjs +8 -22
  56. package/engine/scenes.mjs +31 -47
  57. package/engine/schema-check.mjs +348 -7
  58. package/engine/schema-extract.mjs +11 -39
  59. package/engine/site-build.mjs +13 -38
  60. package/engine/site-index.mjs +40 -35
  61. package/engine/system-block.mjs +513 -0
  62. package/engine/web-wikilinks.mjs +115 -92
  63. package/engine/wikilink-syntax.mjs +30 -0
  64. package/engine/wikilinks.mjs +147 -183
  65. package/index.mjs +1 -5
  66. package/lang.mjs +1 -3
  67. package/manifest.mjs +10 -37
  68. package/markdownlint-config.mjs +1 -5
  69. package/package.json +6 -2
  70. package/sohl/actors.mjs +251 -68
  71. package/sohl/being-info.mjs +3 -6
  72. package/sohl/document-subtypes.mjs +82 -0
  73. package/sohl/index.mjs +4 -6
  74. package/sohl/item-builders.mjs +1 -3
  75. package/sohl/item-fields.mjs +16 -34
  76. package/sohl/items.mjs +111 -17
  77. package/sohl/kb-passes.mjs +29 -39
  78. package/sohl/note-schemas.mjs +11 -7
  79. package/sohl/skill-base.mjs +7 -23
  80. package/stage.mjs +3 -13
  81. package/templates.mjs +4 -15
  82. package/types/bundle.d.mts +1 -1
  83. package/types/container.d.mts +2 -2
  84. package/types/content-config.d.mts +48 -4
  85. package/types/coverage.d.mts +1 -1
  86. package/types/e2e.d.mts +4 -4
  87. package/types/engine/address-charset.d.mts +45 -0
  88. package/types/engine/alias-index.d.mts +122 -0
  89. package/types/engine/base-compiler.d.mts +132 -4
  90. package/types/engine/content-address.d.mts +2 -2
  91. package/types/engine/content-format-check.d.mts +163 -0
  92. package/types/engine/content-format.d.mts +101 -0
  93. package/types/engine/content-links.d.mts +16 -1
  94. package/types/engine/content-lint.d.mts +6 -0
  95. package/types/engine/diagnostics.d.mts +29 -0
  96. package/types/engine/document-subtypes.d.mts +233 -0
  97. package/types/engine/field-spec.d.mts +76 -23
  98. package/types/engine/frontmatter-lint.d.mts +47 -2
  99. package/types/engine/generate.d.mts +15 -2
  100. package/types/engine/helpers.d.mts +22 -14
  101. package/types/engine/ids.d.mts +10 -0
  102. package/types/engine/index.d.mts +5 -0
  103. package/types/engine/item-registry.d.mts +21 -2
  104. package/types/engine/kb-manifest.d.mts +35 -8
  105. package/types/engine/map-notes.d.mts +21 -11
  106. package/types/engine/note-claims.d.mts +113 -0
  107. package/types/engine/note-vocabulary.d.mts +251 -0
  108. package/types/engine/pack-config.d.mts +4 -3
  109. package/types/engine/pack-router.d.mts +4 -4
  110. package/types/engine/prose-lint.d.mts +6 -2
  111. package/types/engine/retired-fields.d.mts +73 -2
  112. package/types/engine/schema-check.d.mts +182 -0
  113. package/types/engine/schema-extract.d.mts +1 -1
  114. package/types/engine/site-index.d.mts +1 -1
  115. package/types/engine/system-block.d.mts +281 -0
  116. package/types/engine/web-wikilinks.d.mts +23 -12
  117. package/types/engine/wikilink-syntax.d.mts +29 -0
  118. package/types/manifest.d.mts +1 -1
  119. package/types/sohl/actors.d.mts +62 -6
  120. package/types/sohl/document-subtypes.d.mts +14 -0
  121. package/types/sohl/index.d.mts +1 -0
  122. package/types/sohl/items.d.mts +21 -0
@@ -0,0 +1,383 @@
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
+ * **Which note types a configuration compiles at all** — and the finding for a
16
+ * note whose type nothing claims (#146).
17
+ *
18
+ * Every compile pass answers one question about a note: _is this mine?_ A note
19
+ * every pass answers "no" to is skipped as quietly as the thousands that
20
+ * legitimately belong to another pass — and where **no** pass would have said
21
+ * yes, that quiet is the whole of the report. `harn-ensemble` declares no
22
+ * `itemBuilders`, so its five `affiliation` notes were claimed by nothing: the
23
+ * journals pass rejected them, the Actor passes rejected them, and no Item pack
24
+ * existed to claim them. They vanished from the build with no error, no warning
25
+ * and no census line, while its 2,512 `being` notes each produced a routing
26
+ * error — the same failure with more information, because some pass got far
27
+ * enough to complain.
28
+ *
29
+ * This module supplies the missing complaint, and it is asked **once per
30
+ * build** rather than once per pass. That is not an optimisation: it is the
31
+ * only place the question can be answered correctly. #79's rule is that a
32
+ * markdown type with no mapping in a given system produces no document *for
33
+ * that system*, silently and correctly — so a per-pass check would report
34
+ * `armorlocation` against every system that does not map it, which is precisely
35
+ * the noise the rule forbids. "No system claims it at all" is a different
36
+ * statement, and only the whole configuration can make it.
37
+ *
38
+ * ## The two conditions, and why they are not one
39
+ *
40
+ * | condition | what it means | whose fix |
41
+ * | --- | --- | --- |
42
+ * | in the **vocabulary**, claimed by no pack | this build knows the type; nothing here is configured to compile it | configuration |
43
+ * | not in the vocabulary | nothing anywhere knows the type | authoring |
44
+ *
45
+ * The **vocabulary** is deliberately wider than one repository's
46
+ * configuration: it is what this toolchain and the systems it ships know a note
47
+ * type to be. `affiliation` is a SoHL Item however a given repository is
48
+ * configured, so a tree full of `affiliation` notes with no Item pack behind
49
+ * them is a repository that has not finished configuring itself — not an author
50
+ * who invented a word. Collapsing the two would send `harn-ensemble` to correct
51
+ * five perfectly good notes.
52
+ *
53
+ * ## The claim table mirrors `selects`, and a test holds them together
54
+ *
55
+ * Which note types a pass claims is stated by that pass's `selects`, and the
56
+ * table below is a second statement of the same fact — the only form in which
57
+ * the question can be asked of a pack that is *not* configured, which is exactly
58
+ * the question here. `tests/unclaimed-note-types.test.ts` compares the two for
59
+ * every type in the vocabulary, so the two statements cannot drift apart.
60
+ *
61
+ * @module
62
+ */
63
+
64
+ import { walkMarkdownTree } from "./helpers.mjs";
65
+ import { MAP_TYPES, PACK_BY_TYPE, RETIRED_TYPES } from "./ids.mjs";
66
+ import { itemTypes } from "./item-registry.mjs";
67
+ import { docEntryTypes } from "./item-docs.mjs";
68
+ import { loadPackConfig } from "./pack-config.mjs";
69
+ import { locateFrontmatterKey } from "./retired-fields.mjs";
70
+ import { noteTypesFor, subtypeRow } from "./document-subtypes.mjs";
71
+ import { HOMEPAGE_TYPE } from "./homepage.mjs";
72
+ import { SOHL_DOCUMENT_SUBTYPES } from "../sohl/document-subtypes.mjs";
73
+
74
+ /**
75
+ * Note types that compile into **no compendium document, by design**.
76
+ *
77
+ * A homepage compiles into a *page*: it carries no compendium UUID, appears in
78
+ * no pack and in no link-manifest entry, and every package that publishes one
79
+ * would otherwise be told its front page is unclaimed. It is the one type whose
80
+ * absence from every pack is the intended state rather than a gap.
81
+ *
82
+ * @type {ReadonlySet<string>}
83
+ */
84
+ export const NEVER_PACKED_TYPES = Object.freeze(new Set([HOMEPAGE_TYPE]));
85
+
86
+ /**
87
+ * The note-type → document-subtype maps this toolchain ships.
88
+ *
89
+ * One today. `hm3/` is #139, and when it lands its map joins this list rather
90
+ * than the claim table below growing a second copy of the same fact.
91
+ *
92
+ * `engine/` importing from `sohl/` is the arrangement `generate.mjs` already
93
+ * has — its `COMPILERS` table names the SoHL compilers by class — and for the
94
+ * same reason: the engine owns the *mechanism* that asks each system what it
95
+ * compiles, and the systems own the answers.
96
+ *
97
+ * @type {readonly import("./document-subtypes.mjs").DocumentSubtypeMap[]}
98
+ */
99
+ export const KNOWN_DOCUMENT_SUBTYPE_MAPS = Object.freeze([SOHL_DOCUMENT_SUBTYPES]);
100
+
101
+ /**
102
+ * What a claim question is asked against.
103
+ *
104
+ * Each field defaults to what the compilers themselves read, so the answer here
105
+ * and the answer a pass gives are drawn from one source. They are parameters so
106
+ * that a test can pose a configuration this toolchain does not ship — two
107
+ * systems cutting the vocabulary differently, a registry declaring nothing —
108
+ * without a content tree or a config file on disk.
109
+ *
110
+ * @typedef {object} ClaimSources
111
+ * @property {readonly import("./document-subtypes.mjs").DocumentSubtypeMap[]} [maps] -
112
+ * The systems' note-type → document-subtype maps.
113
+ * @property {ReadonlySet<string>} [itemTypes] - The declared item vocabulary.
114
+ * @property {ReadonlySet<string>} [docEntryTypes] - The doc-carrying types.
115
+ */
116
+
117
+ /**
118
+ * Fill in whatever a caller did not supply, from the configured registries.
119
+ *
120
+ * @param {ClaimSources} [sources] - What the caller supplied.
121
+ * @returns {Required<ClaimSources>} Every source, resolved.
122
+ */
123
+ function resolveSources(sources = {}) {
124
+ return {
125
+ maps: sources.maps ?? KNOWN_DOCUMENT_SUBTYPE_MAPS,
126
+ itemTypes: sources.itemTypes ?? itemTypes(),
127
+ docEntryTypes: sources.docEntryTypes ?? docEntryTypes(),
128
+ };
129
+ }
130
+
131
+ /**
132
+ * The Foundry document classes the systems say a note type compiles into.
133
+ *
134
+ * @param {readonly import("./document-subtypes.mjs").DocumentSubtypeMap[]} maps -
135
+ * The systems' maps.
136
+ * @param {string} type - The note's declared `type`.
137
+ * @returns {string[]} The document classes, deduplicated, in map order.
138
+ */
139
+ function mappedDocuments(maps, type) {
140
+ const documents = [];
141
+ for (const map of maps) {
142
+ const row = subtypeRow(map, type);
143
+ if (row && !documents.includes(row.document)) documents.push(row.document);
144
+ }
145
+ return documents;
146
+ }
147
+
148
+ /**
149
+ * The systems whose map names a note type.
150
+ *
151
+ * @param {readonly import("./document-subtypes.mjs").DocumentSubtypeMap[]} maps -
152
+ * The systems' maps.
153
+ * @param {string} type - The note's declared `type`.
154
+ * @returns {string[]} The system ids, in map order.
155
+ */
156
+ function mappingSystems(maps, type) {
157
+ return maps.filter((map) => subtypeRow(map, type)).map((map) => map.system);
158
+ }
159
+
160
+ /**
161
+ * The note types a pass of one document type claims — the claim table.
162
+ *
163
+ * Each row restates one pass's `selects`, in the only form that can be asked of
164
+ * a pack the configuration does not declare. A document type no compiler is
165
+ * registered for claims nothing, which is what keeps a prebuilt `Adventure`
166
+ * pack from appearing to answer for any note.
167
+ *
168
+ * @param {string} docType - The Foundry document type a pack holds.
169
+ * @param {ClaimSources} [sources] - What to answer from. Defaults to the
170
+ * configured registries and the systems this toolchain ships.
171
+ * @returns {ReadonlySet<string>} The note types such a pass would claim.
172
+ */
173
+ export function noteTypesClaimedBy(docType, sources) {
174
+ const { maps, itemTypes: items, docEntryTypes: docs } = resolveSources(sources);
175
+ switch (docType) {
176
+ // The items pass: the declared registry's keys, filtered by the
177
+ // systems' maps — a type a system sends to some *other* document class
178
+ // is not an item however a registry spells it.
179
+ case "Item":
180
+ return Object.freeze(
181
+ new Set(
182
+ [...items].filter((type) => {
183
+ const documents = mappedDocuments(maps, type);
184
+ return documents.length === 0 || documents.includes("Item");
185
+ }),
186
+ ),
187
+ );
188
+ // The actors pass: every note type a system sends to an `Actor`.
189
+ case "Actor":
190
+ return Object.freeze(new Set(maps.flatMap((map) => noteTypesFor(map, "Actor"))));
191
+ // The journals pass: free prose, plus every doc-carrying type — an
192
+ // item's, a macro's and a map note's description each compile into a
193
+ // JournalEntry of their own.
194
+ case "JournalEntry":
195
+ return Object.freeze(new Set(["doc", ...docs]));
196
+ case "Macro":
197
+ return Object.freeze(new Set(["macro"]));
198
+ case "Scene":
199
+ return Object.freeze(new Set(MAP_TYPES));
200
+ default:
201
+ return Object.freeze(new Set());
202
+ }
203
+ }
204
+
205
+ /**
206
+ * Every note type some pack in a configuration would compile.
207
+ *
208
+ * The union across the configured pack list, so a type claimed by any one pack
209
+ * is claimed — which is what keeps a type deliberately unmapped for one system,
210
+ * and claimed for another, silent (#79).
211
+ *
212
+ * @param {object} [config] - The resolved build configuration. Defaults to this
213
+ * repository's.
214
+ * @param {ClaimSources} [sources] - What to answer from.
215
+ * @returns {ReadonlySet<string>} The claimed note types.
216
+ */
217
+ export function claimedNoteTypes(config = loadPackConfig(), sources) {
218
+ const claimed = new Set();
219
+ for (const pack of config.packs ?? []) {
220
+ for (const type of noteTypesClaimedBy(pack.type, sources)) claimed.add(type);
221
+ }
222
+ return Object.freeze(claimed);
223
+ }
224
+
225
+ /**
226
+ * Every note type this build knows, whatever any one repository configures.
227
+ *
228
+ * Wider than {@link claimedNoteTypes} on purpose: it is what distinguishes a
229
+ * repository that has not configured a pack for a real content type from an
230
+ * author who wrote a word nothing anywhere compiles. The engine's own types,
231
+ * the types every shipped system maps, and whatever the configured registries
232
+ * declare on top.
233
+ *
234
+ * @param {ClaimSources} [sources] - What to answer from.
235
+ * @returns {ReadonlySet<string>} The vocabulary.
236
+ */
237
+ export function noteTypeVocabulary(sources) {
238
+ const { maps, itemTypes: items } = resolveSources(sources);
239
+ return Object.freeze(
240
+ new Set([
241
+ ...Object.keys(PACK_BY_TYPE),
242
+ ...MAP_TYPES,
243
+ ...NEVER_PACKED_TYPES,
244
+ ...items,
245
+ ...maps.flatMap((map) => Object.keys(map.types)),
246
+ ]),
247
+ );
248
+ }
249
+
250
+ /**
251
+ * A readable list — `"a", "b" or "c"`.
252
+ *
253
+ * @param {readonly string[]} values - The values.
254
+ * @returns {string} The list.
255
+ */
256
+ function list(values) {
257
+ const quoted = values.map((value) => `"${value}"`);
258
+ if (quoted.length <= 1) return quoted.join("");
259
+ return `${quoted.slice(0, -1).join(", ")} or ${quoted[quoted.length - 1]}`;
260
+ }
261
+
262
+ /**
263
+ * `"a"` or `"an"`, so a document class reads as English wherever it is named.
264
+ *
265
+ * @param {string} word - The word the article precedes.
266
+ * @returns {string} The article.
267
+ */
268
+ function article(word) {
269
+ return /^[AEIOUaeiou]/.test(word) ? "an" : "a";
270
+ }
271
+
272
+ /**
273
+ * The **configuration** finding: this build knows the type, and nothing here
274
+ * compiles it.
275
+ *
276
+ * The remedy names what is actually missing rather than a generic one, because
277
+ * the two halves fail independently: `harn-ensemble` has neither the registry
278
+ * entry nor the pack, and a repository that declares a registry and forgets the
279
+ * pack has only the second.
280
+ *
281
+ * @param {string} type - The note's declared `type`.
282
+ * @param {object} config - The resolved build configuration.
283
+ * @param {Required<ClaimSources>} sources - What to answer from.
284
+ * @returns {string} The message.
285
+ */
286
+ function configurationMessage(type, config, sources) {
287
+ const documents = mappedDocuments(sources.maps, type);
288
+ if (!documents.length && sources.itemTypes.has(type)) documents.push("Item");
289
+ if (!documents.length && PACK_BY_TYPE[type]) documents.push(PACK_BY_TYPE[type].docType);
290
+
291
+ const systems = mappingSystems(sources.maps, type);
292
+ const configured = new Set((config.packs ?? []).map((pack) => pack.type));
293
+ const packless = documents.filter((document) => !configured.has(document));
294
+ const needsBuilder = documents.includes("Item") && !sources.itemTypes.has(type);
295
+
296
+ const into = documents.map((document) => `${article(document)} ${document}`).join(" or ");
297
+ const becomes =
298
+ documents.length ?
299
+ systems.length ?
300
+ `The ${list(systems)} system${systems.length > 1 ? "s" : ""} ` +
301
+ `compile${systems.length > 1 ? "" : "s"} it into ${into}`
302
+ : `It compiles into ${into}`
303
+ : `Nothing configured here compiles it`;
304
+
305
+ const remedy =
306
+ packless.length && needsBuilder ?
307
+ `\`packs:\` declares no ${packless.join(" or ")} pack and no ` +
308
+ `\`itemBuilders\` registry declares "${type}" — declare both in ` +
309
+ `package-build.config.yaml`
310
+ : packless.length ?
311
+ `\`packs:\` declares no ${packless.join(" or ")} pack — declare one in ` +
312
+ `package-build.config.yaml`
313
+ : needsBuilder ?
314
+ `no \`itemBuilders\` registry declares "${type}" — declare it in ` +
315
+ `package-build.config.yaml`
316
+ : `nothing in \`packs:\` claims it — check the pack list in ` +
317
+ `package-build.config.yaml`;
318
+
319
+ return (
320
+ `no configured pack claims a note of type "${type}", so it compiles ` +
321
+ `into nothing. ${becomes}, but ${remedy}, or stop authoring the type.`
322
+ );
323
+ }
324
+
325
+ /**
326
+ * The **authoring** finding: nothing anywhere knows the type.
327
+ *
328
+ * @param {string} type - The note's declared `type`.
329
+ * @returns {string} The message.
330
+ */
331
+ function authoringMessage(type) {
332
+ return (
333
+ `no configured pack claims a note of type "${type}", so it compiles ` +
334
+ `into nothing — and "${type}" is not a content type this build knows ` +
335
+ `at all: no system maps it and no \`itemBuilders\` registry declares it. ` +
336
+ `Correct the note's \`type:\`, or declare the type alongside the ones ` +
337
+ `this repository already ships.`
338
+ );
339
+ }
340
+
341
+ /**
342
+ * Every note in the content tree that no configured pack would compile.
343
+ *
344
+ * Read-only: it walks the tree and reports, and writes nothing. Three kinds of
345
+ * note are passed over, each for a stated reason rather than by omission — a
346
+ * file with no frontmatter is not a note; a note with no `type:` is the
347
+ * frontmatter linter's finding, which can say what a type is *for*; and a
348
+ * retired type is answered by `assertTypeNotRetired` in `ids.mjs`, which names the
349
+ * replacement.
350
+ *
351
+ * @param {object} [config] - The resolved build configuration. Defaults to this
352
+ * repository's.
353
+ * @param {ClaimSources} [sources] - What to answer from.
354
+ * @returns {Array<{file: string, line?: number, column?: number,
355
+ * severity: "error", message: string, type: string}>} One finding per note.
356
+ */
357
+ export function unclaimedNoteFindings(config = loadPackConfig(), sources) {
358
+ const resolved = resolveSources(sources);
359
+ const claimed = claimedNoteTypes(config, resolved);
360
+ const vocabulary = noteTypeVocabulary(resolved);
361
+ const findings = [];
362
+
363
+ for (const { frontmatter: fm, absPath } of walkMarkdownTree(config.paths.content)) {
364
+ if (!fm) continue;
365
+ const type = typeof fm.type === "string" ? fm.type.trim() : "";
366
+ if (!type) continue;
367
+ if (NEVER_PACKED_TYPES.has(type)) continue;
368
+ if (RETIRED_TYPES[type]) continue;
369
+ if (claimed.has(type)) continue;
370
+
371
+ findings.push({
372
+ file: absPath,
373
+ ...locateFrontmatterKey(absPath, "type", type),
374
+ severity: /** @type {"error"} */ ("error"),
375
+ type,
376
+ message:
377
+ vocabulary.has(type) ?
378
+ configurationMessage(type, config, resolved)
379
+ : authoringMessage(type),
380
+ });
381
+ }
382
+ return findings;
383
+ }
@@ -104,10 +104,7 @@ export function searchableFrontmatter(fm, configured) {
104
104
  * @returns {void}
105
105
  * @throws {Error} When the note declares the field.
106
106
  */
107
- export function assertNoDeclaredPackage(
108
- fm,
109
- { file, absPath, configured } = {},
110
- ) {
107
+ export function assertNoDeclaredPackage(fm, { file, absPath, configured } = {}) {
111
108
  if (!fm || typeof fm !== "object" || !Object.hasOwn(fm, "package")) return;
112
109
 
113
110
  const target = configured ?? contentPackage();