@heroiclands/package-build 3.2.0 → 3.4.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,185 @@
1
1
  # @heroiclands/package-build
2
2
 
3
+ ## 3.4.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 19df269: Stop emitting `assocMysteryCode` on a compiled mystical ability (#35).
8
+
9
+ The `mysticalability` declaration named a field no SoHL DataModel receives.
10
+ `MysticalAbilityDataModel` declares `subType`, `assocSkillCode`,
11
+ `assocAffiliationCode`, `masteryLevelBase`, `improveFlag`, `levelBase` and
12
+ `charges`, and nothing else — Foundry discards the extra key when the document
13
+ is constructed, so every mystical ability in every consuming pack shipped a
14
+ value that was thrown away at load, with nothing at compile or load time saying
15
+ so.
16
+
17
+ This is the exact inverse of #3 and has the same root cause: nothing compares a
18
+ builder's emitted `system` block against the DataModel that receives it. #3 was a
19
+ declared field the builder failed to emit; this is an emitted field the DataModel
20
+ never declares. Both compile clean, both lose data silently, and an author cannot
21
+ tell either from a correct build. The general check is #60.
22
+
23
+ **The field was retired, not renamed.**
24
+ Song-of-Heroic-Lands-FoundryVTT#973 deleted `assocMysteryCode` because nothing in
25
+ production read the mystery it resolved to; #1012 later added
26
+ `assocAffiliationCode` as a separate concept — the faction whose standing confers
27
+ the ability. They look alike and mean different things, so the declaration is
28
+ dropped rather than retargeted.
29
+
30
+ **This changes emitted documents**, so a consumer wants a rebuild rather than a
31
+ silent upgrade — though nothing downstream can have depended on the value:
32
+
33
+ - `sohl` — nine notes authored the key, all of them blank. Corrected in
34
+ Song-of-Heroic-Lands-FoundryVTT#1747.
35
+ - `sohl-kethira-basic` — no note authors it; all 224 mystical abilities carried
36
+ the builder's own `""`. Recompiling `main` with this change removes exactly
37
+ those 224 lines and touches nothing else.
38
+ - dced8b2: Write the derived package into an emitted page's frontmatter (#65).
39
+
40
+ `content-build site` copied a note's frontmatter to the page verbatim. Since
41
+ 3.3.0 a note need not declare `package:` — it is derived from the configured
42
+ `contentPackage` — and `engine/site-build.mjs` resolved that value one line
43
+ before it built the page, carrying it for the index, the table universe and the
44
+ local-package set. It never reached the page's own frontmatter, so a swept tree
45
+ published pages that said nothing about which package they belong to.
46
+
47
+ **The visible symptom is the breadcrumb.**
48
+ `@heroiclands/hugo-theme`'s `layouts/partials/breadcrumbs.html` reads
49
+ `{{ $pkg := .Params.package | default "" }}` and builds the middle crumb from
50
+ it. With no `package` the section is never resolved and the crumb degrades from
51
+ a linked, labelled section to a bare, unlinked type slug:
52
+
53
+ ```text
54
+ before: Home > SoHL Affliction > Aconite (linked)
55
+ after: Home > affliction > Aconite (bare)
56
+ ```
57
+
58
+ Consumer layouts reading the field directly degrade the same way — a `package`
59
+ column renders blank.
60
+
61
+ The fix is where the value was already known: `pageFrontmatter` spreads
62
+ `package` after the note's own frontmatter, so a note that declares the field
63
+ keeps its authored position and value and an unswept tree emits byte-identically,
64
+ while a swept one regains the line it lost. The alternative — teaching every
65
+ theme and consumer layout to default the package from a site parameter — pushes
66
+ a fact the build already knows out to N consumers, and the theme deliberately
67
+ carries no addresses.
68
+
69
+ **This changes emitted output**, so a consumer wants a rebuild rather than a
70
+ silent upgrade, which is why it is a minor rather than a patch — the same
71
+ reasoning as #35. Verified against the swept `sohl` tree: 1,606 emitted content
72
+ pages differ, none added or removed, and the only diff line class across the
73
+ whole tree is the restored `package: sohl`. Rendering that tree, 1,600 pages
74
+ differ and in exactly two ways — the breadcrumb's middle crumb, and a `package`
75
+ column that was blank.
76
+
77
+ `sohl-thalorna` has the same defect independently, in its own
78
+ `utils/build-site-content.mjs`, and is fixed in that repository
79
+ (sohl-thalorna#79) — two emitters, one behaviour, which is a second argument
80
+ for #36.
81
+
82
+ ## 3.3.0
83
+
84
+ ### Minor Changes
85
+
86
+ - 1f6eb85: Compile an unopened skill's opening mastery level into the actor pack.
87
+
88
+ A skill embedded on a being carries `masteryLevelBase: null` when neither the
89
+ catalogue entry nor the note states one — `null` meaning _not yet opened_. The
90
+ client filled that in on import, at Skill Base × `initSkillMult`
91
+ (`SkillLogic.initialize`), so a compiled being said nothing about what its
92
+ skills open at. It materialised on import instead of being visible in the
93
+ document, reviewable in a diff, or testable without standing up Foundry.
94
+
95
+ The actors pass now computes it. `openUnopenedSkills` runs at the end of
96
+ `buildEmbeddedItems`, after the note's frontmatter has been merged onto the
97
+ catalogue skill and after the attribute items exist — the Skill Base formula
98
+ reads the actor's attributes, so it cannot run any earlier. Only nulls are
99
+ filled; a skill that states a `masteryLevelBase` keeps it.
100
+
101
+ Evaluating `skillBaseFormula` needs an expression evaluator, which this package
102
+ had none of. `sohl/skill-base.mjs` reproduces the part of SoHL's
103
+ `SafeExpression` that a Skill Base uses: numeric literals, `attr.<code>` reads
104
+ defaulting to `0`, arithmetic, and a small helper set including `sb()` — whose
105
+ rounding (a pair averages up only when the primary is the greater) and whose
106
+ `Math.max(0, n)` clamp are copied from SoHL deliberately and must not drift. A
107
+ formula outside that subset is reported, never guessed at.
108
+
109
+ Two build-side rules the client does not need, neither of which changes what a
110
+ client computes:
111
+
112
+ - A zero or absent `initSkillMult` leaves `masteryLevelBase` null. The
113
+ multiplier is the switch for whether a skill opens at all, so writing the `0`
114
+ the arithmetic yields would claim the skill opened at zero rather than that it
115
+ never opened.
116
+ - A fractional product is an error, not a rounding. `masteryLevelBase` is an
117
+ integer field, so there is no honest value to write — the same stance
118
+ `resolveSkillAptitudes` takes on a fractional modifier.
119
+
120
+ The scores used are the `scoreBase` values the pass just wrote, where the client
121
+ resolves `attr.<code>` to an attribute's _effective_ score. They agree for a
122
+ being carrying no attribute-altering effects, which is every being in content
123
+ today; one that did carry such an effect would bake a Skill Base its client then
124
+ disagrees with.
125
+
126
+ Closes #46.
127
+ - e37d201: A note's package is the repository's `contentPackage`, and `package:` is
128
+ optional.
129
+
130
+ A note was compiled when its `package:` frontmatter matched the configured
131
+ `contentPackage`, and skipped when it did not — silently, and tallied as
132
+ `skippedOther`, the same bucket as the thousands of notes that legitimately
133
+ belong to another pass. So a tree whose notes named a package no configuration
134
+ answered to compiled **zero notes and exited 0**, which is the state the
135
+ un-migrated `hm-loc-*` / `hm-adv-*` repositories are in today.
136
+
137
+ Every content tree is single-package — each is single-sourced in the repository
138
+ that ships it — so the field restated one constant about 6,200 times across the
139
+ org. This is the first of three steps that retire it, and the only one that
140
+ changes any code here:
141
+
142
+ 1. **Optional**, now. An absent `package:` is normal and the note compiles; a
143
+ present one is accepted while it agrees with `contentPackage`, and is a
144
+ **loud error naming the file** when it does not. Nothing a consumer authors
145
+ has to change, which is what makes the sweep safe to land next.
146
+ 2. **Swept** out of every content tree, on this version.
147
+ 3. **Rejected** outright — a later major, once the sweeps have merged.
148
+
149
+ **What changed**
150
+
151
+ - `engine/note-package.mjs` is the new seam: `notePackage` derives the package
152
+ a note belongs to, `assertNotePackage` refuses one that names another, and
153
+ `searchableFrontmatter` presents a note to a generated table with its package
154
+ present however the note spells it.
155
+ - The compile loop no longer filters on the field. A note declaring another
156
+ package is reported through the ordinary diagnostic channel
157
+ (`file:line:column: error: …`), counted in `errorCount` so the build fails,
158
+ and tallied as its own `PassStats.declined` — never folded into
159
+ `skippedOther`, which is what made the original defect invisible.
160
+ - The link manifest throws rather than skipping such a note: skipping it
161
+ quietly is how a manifest came to claim a package publishes nothing.
162
+ - **No key is derived from frontmatter any more.** The link index
163
+ (`content-links`), the site index (`site-index`) and the site build's
164
+ package grouping all take the derived value; the manifest emitter already
165
+ took it from configuration. A note addresses identically whether or not it
166
+ declares the field.
167
+ - A generated table that scopes itself with `WHERE … and package = "<pkg>"` —
168
+ the shape every collection note uses — keeps matching after the field is
169
+ deleted, so a sweep is a mechanical deletion rather than a silent
170
+ emptying of every table.
171
+
172
+ **`contentPackage` is not becoming dead configuration.** Its selecting job is
173
+ what is going; the value is the **address namespace** — the first segment of
174
+ every canonical key, the name of the link manifest a build emits, and the
175
+ package a cross-package wikilink writes. Its documentation now says so.
176
+
177
+ Verified against the real `Song-of-Heroic-Lands-FoundryVTT` tree (1,606 notes):
178
+ `build/packs-json` is byte-identical to `main`'s output both with the field
179
+ present on every note and with it deleted from every note.
180
+
181
+ Step 1 of #56. Steps 2 (the sweep) and 3 (rejection, a major) follow.
182
+
3
183
  ## 3.2.0
4
184
 
5
185
  ### Minor Changes
package/CONTENT.md CHANGED
@@ -21,7 +21,9 @@ npm install -D @heroiclands/package-build
21
21
  A consuming repository declares one `package-build.config.yaml` at its root:
22
22
 
23
23
  ```yaml
24
- # The value each content note carries in its `package:` frontmatter.
24
+ # The package this repository's content is published as: the first segment of
25
+ # every canonical address, the name of the link manifest it emits, and the
26
+ # package a cross-package wikilink writes to reach one of its notes.
25
27
  contentPackage: thalorna
26
28
  # Where Foundry installs it: "systems" or "modules". Also decides the served
27
29
  # asset root a note's `img:` resolves to — `modules/sohl-thalorna/assets/…`.
@@ -173,6 +175,31 @@ never existed. A module declaring no usable system relationship fails the build
173
175
  rather than guessing — a wrong `_stats.systemVersion` is invisible until
174
176
  something migrates on it.
175
177
 
178
+ ### A note's package is the repository's, not the note's
179
+
180
+ `contentPackage` is the **address namespace** every note in the tree is
181
+ published under. It is not a filter, and a note does not restate it.
182
+
183
+ A note may still carry `package:` — every note written before this did — and it
184
+ is accepted while it **agrees** with `contentPackage`. One that disagrees is an
185
+ error naming the file, in the compile, in the link manifest, and anywhere else
186
+ a note is read. The field is redundant, and it is being retired in three steps:
187
+ optional now, swept out of every content tree on this version, and rejected
188
+ outright in a later major.
189
+
190
+ A generated table that scopes itself with `WHERE … and package = "<pkg>"` keeps
191
+ working either way: a note's package is supplied to the table search whether or
192
+ not the note declares it, so deleting the field never turns a table into an
193
+ empty one.
194
+
195
+ It used to **select**: a note compiled when its `package:` matched and was
196
+ skipped when it did not. Every content tree is single-package — each is
197
+ single-sourced in the repository that ships it — so the field restated one
198
+ constant thousands of times, while a tree whose notes named a package no
199
+ configuration answered to compiled **zero notes and exited 0**. Deleting the
200
+ field from a note is safe on this version and is the fix; deleting the
201
+ _configured_ value is not, since every address derives from it.
202
+
176
203
  ### A registry of your own
177
204
 
178
205
  `itemBuilders` is the one part of the contract that is code — a table of
@@ -240,7 +267,6 @@ packs:
240
267
  name:
241
268
  full: Climbing
242
269
  type: skill
243
- package: kethira
244
270
  id: ...
245
271
  ---
246
272
  # A note that names one lands there instead.
@@ -248,7 +274,6 @@ id: ...
248
274
  name:
249
275
  full: Second Sight
250
276
  type: skill
251
- package: kethira
252
277
  id: ...
253
278
  pack: mysteries
254
279
  ---
@@ -543,7 +568,7 @@ It reads its whole input from configuration and takes nothing else:
543
568
 
544
569
  | Setting | What it decides |
545
570
  | --------------------------- | ------------------------------------------------------------- |
546
- | `contentPackage` | The package emitted, and which notes belong to it. |
571
+ | `contentPackage` | The package emitted, which every note belongs to. |
547
572
  | `foundryPackage` | The package every emitted `uuid` names. |
548
573
  | `paths.content` | The tree walked. |
549
574
  | `paths.manifestOut` | Where the file lands (`build/manifests` by default). |
@@ -414,9 +414,9 @@ export const DEFAULT_ADDRESS_SCHEME = Object.freeze({
414
414
  * repository — every configured path is
415
415
  * resolved against it, so the build never
416
416
  * depends on the working directory.
417
- * @property {string} contentPackage Content package name — the value each
418
- * content note carries in its `package:`
419
- * frontmatter.
417
+ * @property {string} contentPackage Content package name — the address
418
+ * namespace every note in this
419
+ * repository is published under.
420
420
  * @property {string} foundryPackage Foundry package id, as it appears in
421
421
  * `system.json` / `module.json`.
422
422
  * @property {PackageKind} packageKind Whether the package is a system or a module.
@@ -74,16 +74,25 @@ import {
74
74
  } from "./helpers.mjs";
75
75
  import { emitDiagnostic } from "./diagnostics.mjs";
76
76
  import { contentPackage } from "./content-package.mjs";
77
+ import { assertNotePackage } from "./note-package.mjs";
77
78
  import { assertTypeNotRetired, packForType } from "./ids.mjs";
78
79
 
79
80
  /**
80
81
  * The tallies one pass accumulates while walking the tree.
81
82
  *
83
+ * `declined` and `skippedOther` are deliberately separate numbers. A declined
84
+ * note is one this build **refused** — it names a package this repository does
85
+ * not compile — and it is an error; a skipped one legitimately belongs to
86
+ * another pass, and there are thousands of those. Folding the first into the
87
+ * second is what let a whole tree be filtered out in silence (#56).
88
+ *
82
89
  * @typedef {object} PassStats
83
90
  * @property {number} compiled - Notes that became a document.
84
91
  * @property {number} skippedDraft - Notes marked `draft: true`.
85
92
  * @property {number} skippedNoId - Notes with no `id`, where that is tolerated.
86
93
  * @property {number} skippedOther - Notes this pass does not claim.
94
+ * @property {number} declined - Notes refused because they declare another
95
+ * package. Counted as errors, never as skips.
87
96
  */
88
97
 
89
98
  /**
@@ -250,8 +259,9 @@ export class BasePackCompiler {
250
259
  /**
251
260
  * Whether this pass claims a note. **Required.**
252
261
  *
253
- * Called only for a note of the configured content package, so a subclass
254
- * decides on `type` alone.
262
+ * Called only for a note this build compiles every note in the tree
263
+ * belongs to the configured content package (#56) — so a subclass decides
264
+ * on `type` alone.
255
265
  *
256
266
  * @param {object} fm - The note's frontmatter.
257
267
  * @returns {boolean} True to compile it.
@@ -326,7 +336,9 @@ export class BasePackCompiler {
326
336
  const { markdown: tabulated, lineMap } = expandNoteTables(body, {
327
337
  docs: this.contentDocs,
328
338
  name,
329
- pkg: fm.package,
339
+ // The repository's package, not the note's: every note in the tree
340
+ // is this package's note, whether or not it says so (#56).
341
+ pkg: contentPackage(),
330
342
  fm,
331
343
  bodyLine,
332
344
  });
@@ -503,6 +515,14 @@ export class BasePackCompiler {
503
515
  if (stats.skippedDraft) {
504
516
  log.info(`Skipped ${stats.skippedDraft} draft(s)`);
505
517
  }
518
+ if (stats.declined) {
519
+ // Its own line, at error level: these are not skips, and burying
520
+ // them in the skipped tally is the defect (#56). Each one has
521
+ // already been named individually as a diagnostic.
522
+ log.error(
523
+ `Declined ${stats.declined} note(s) declaring another package`,
524
+ );
525
+ }
506
526
  this.reportDetail(stats);
507
527
  }
508
528
 
@@ -518,6 +538,7 @@ export class BasePackCompiler {
518
538
  skippedDraft: 0,
519
539
  skippedNoId: 0,
520
540
  skippedOther: 0,
541
+ declined: 0,
521
542
  };
522
543
  await this.prepare();
523
544
 
@@ -534,10 +555,25 @@ export class BasePackCompiler {
534
555
  // Which note this pass is on, so anything it calls can report a
535
556
  // position without every method having to be handed one (#17).
536
557
  this.currentNote = { absPath, bodyLine, bodyColumn };
537
- if (!fm || fm.package !== contentPackage()) {
558
+ // A file carrying no frontmatter at all is not a note.
559
+ if (!fm) {
538
560
  stats.skippedOther++;
539
561
  continue;
540
562
  }
563
+ // The package a note belongs to is this repository's configured
564
+ // one; `package:` is optional and merely has to agree (#56). A
565
+ // disagreement is reported and counted as an error — never skipped,
566
+ // which is how a tree naming a package nothing answers to used to
567
+ // compile zero notes and exit 0. The file comes from the diagnostic
568
+ // locator, so the message must not repeat it.
569
+ try {
570
+ assertNotePackage(fm);
571
+ } catch (err) {
572
+ stats.declined++;
573
+ this.errorCount++;
574
+ this.noteError(err.message);
575
+ continue;
576
+ }
541
577
  // Checked before `selects`, and therefore for every note this
542
578
  // package owns rather than only the ones some pass claims. A
543
579
  // retired type is claimed by no pass, so the alternative is not a
@@ -52,6 +52,7 @@ import { matchAllOutsideCode } from "./code-fences.mjs";
52
52
  import { expandContentTables } from "./content-tables.mjs";
53
53
  import { walkMarkdownTree } from "./helpers.mjs";
54
54
  import { hasDocEntry } from "./item-docs.mjs";
55
+ import { notePackage, searchableFrontmatter } from "./note-package.mjs";
55
56
  import {
56
57
  canonicalKey,
57
58
  loadForeignManifests,
@@ -131,21 +132,18 @@ export function buildLinkIndex(
131
132
 
132
133
  for (const note of notes) {
133
134
  const { fm, type } = note;
135
+ // Derived, never read out of frontmatter: `package:` is optional, and a
136
+ // note that declares nothing addresses exactly as one that declares the
137
+ // configured package (#56).
138
+ const pkg = notePackage(fm);
134
139
  if (typeof fm.shortcode === "string" && fm.shortcode) {
135
140
  byKey.set(`${type}/${fm.shortcode}`.toLowerCase(), note);
136
141
  // The canonical, fully qualified address alongside the short one,
137
142
  // so a package-qualified link checks the same way a bare one does.
138
- if (fm.package) {
139
- byKey.set(canonicalKey(fm.package, type, fm.shortcode), note);
140
- }
143
+ byKey.set(canonicalKey(pkg, type, fm.shortcode), note);
141
144
  if (hasDocEntry(type)) {
142
145
  byKey.set(`doc${type}/${fm.shortcode}`.toLowerCase(), note);
143
- if (fm.package) {
144
- byKey.set(
145
- canonicalKey(fm.package, `doc${type}`, fm.shortcode),
146
- note,
147
- );
148
- }
146
+ byKey.set(canonicalKey(pkg, `doc${type}`, fm.shortcode), note);
149
147
  }
150
148
  }
151
149
  const aliases = [
@@ -172,9 +170,7 @@ export function buildLinkIndex(
172
170
  // A foreign package may use a type this tree has never seen, so its types
173
171
  // join `types` — otherwise `readQualifier` reads the link as prose and it
174
172
  // is never checked at all.
175
- const localPackages = new Set(
176
- notes.map((n) => n.fm?.package).filter(Boolean),
177
- );
173
+ const localPackages = new Set(notes.map((n) => notePackage(n.fm)));
178
174
  const foreign =
179
175
  manifestDir ?
180
176
  loadForeignManifests(manifestDir, localPackages)
@@ -182,13 +178,15 @@ export function buildLinkIndex(
182
178
  for (const v of foreign.index.values()) if (v.type) types.add(v.type);
183
179
 
184
180
  const packages = new Set([
185
- ...[...byKey.values()].map((n) => n.fm?.package).filter(Boolean),
181
+ ...[...byKey.values()].map((n) => notePackage(n.fm)),
186
182
  ...foreign.packages,
187
183
  ]);
188
184
 
189
185
  /** The searchable universe a `dataview` table draws its rows from. */
190
186
  const tableDocs = notes.map((n) => ({
191
- fm: n.fm,
187
+ // Package present however the note spells it — see
188
+ // {@link searchableFrontmatter} (#56).
189
+ fm: searchableFrontmatter(n.fm),
192
190
  path: n.rel,
193
191
  tld: n.rel.split("/")[0],
194
192
  folder: path.dirname(n.rel).split("/").pop(),
@@ -207,7 +205,9 @@ export function buildLinkIndex(
207
205
  let body = note.body;
208
206
  if (/^[ \t]*(?:`{3,}|~{3,})[ \t]*dataview\b/im.test(body)) {
209
207
  body = expandContentTables(body, {
210
- docs: tableDocs.filter((d) => d.fm.package === note.fm.package),
208
+ docs: tableDocs.filter(
209
+ (d) => notePackage(d.fm) === notePackage(note.fm),
210
+ ),
211
211
  linkable: (d) => Boolean(d.fm.shortcode),
212
212
  source: note.file,
213
213
  }).markdown;
@@ -25,12 +25,25 @@
25
25
  import { loadPackConfig } from "./pack-config.mjs";
26
26
 
27
27
  /**
28
- * The **content** package: the distribution unit a note declares in its
29
- * `package:` frontmatter. The pack compilers select their entries by it.
28
+ * The **content** package: the distribution unit this repository's notes belong
29
+ * to, and the **address namespace** every one of them is published under.
30
+ *
31
+ * It is the first segment of every canonical key (`sohl-skill-clmb`), the name
32
+ * of the link manifest this build emits (`sohl.json`), and the package a
33
+ * cross-package wikilink writes to reach one of these notes. So it is the
34
+ * repository's identity in the address space, not a switch — and never dead
35
+ * configuration, whatever else changes.
36
+ *
37
+ * It was also, until #56, a **selector**: a note declared the same value in its
38
+ * `package:` frontmatter and the compilers kept the ones that matched. Every
39
+ * content tree is single-package — each is single-sourced in the repository that
40
+ * ships it — so the field restated this constant once per note while a value
41
+ * that matched nothing filtered the whole tree out in silence. The field is
42
+ * being retired; the value stays, here, where it is declared once.
30
43
  *
31
44
  * Stable across compilation targets. If this content were ever compiled for a
32
- * second game system, its notes would still declare `package: sohl` — only the
33
- * Foundry package below would differ.
45
+ * second game system, it would still be published as `sohl` — only the Foundry
46
+ * package below would differ.
34
47
  *
35
48
  * An accessor rather than a hoisted constant, so that importing this module
36
49
  * needs no configuration (#2).
@@ -47,7 +60,7 @@ export function contentPackage() {
47
60
  * compendium UUID the compilers emit.
48
61
  *
49
62
  * Distinct from {@link contentPackage}, and equal to it only by coincidence
50
- * here: a note says `package: sohl` and its documents are addressed as
63
+ * here: a note is published under `sohl` and its documents are addressed as
51
64
  * `Compendium.sohl.<pack>.<Type>.<id>`. In `sohl-thalorna` the two differ
52
65
  * (`thalorna` vs `sohl-thalorna`), which is why they are separate values rather
53
66
  * than one — treating them as interchangeable is what #1498 was.
@@ -146,7 +146,9 @@ function workedExample(type, fields) {
146
146
  ` full: An Example ${type}`,
147
147
  `type: ${type}`,
148
148
  "shortcode: xmpl",
149
- "package: <your content package>",
149
+ // No `package:`. A note's package is the repository's configured
150
+ // `contentPackage`, so the field is redundant and is being retired
151
+ // (#56) — and this example is the smallest note that compiles.
150
152
  "id: <16-character id>",
151
153
  "sohl:",
152
154
  " archetype: null",
@@ -21,9 +21,9 @@
21
21
  * into the shipped LevelDB packs) — it is never committed.
22
22
  *
23
23
  * Each `*` compiler walks the whole content tree and selects its own entries by
24
- * frontmatter (a `package:` matching the configured `contentPackage` + `type`),
25
- * so routing is directory-agnostic: a file lands in a pack because of its
26
- * `type`, not its location. Which packs exist, in what order, and which folder
24
+ * the note's `type` every note in the tree belongs to this repository's
25
+ * `contentPackage` (#56) — so routing is directory-agnostic: a file lands in a
26
+ * pack because of its `type`, not its location. Which packs exist, in what order, and which folder
27
27
  * hierarchy each one loads are all declared in `package-build.config.yaml`;
28
28
  * folder files live under the content root and are referenced from entry
29
29
  * frontmatter via `sohl.folder: <id>`.
@@ -196,9 +196,9 @@ async function generatePack(
196
196
  * The passes that compiled nothing when they were expected to compile
197
197
  * something — a build failure, not a quiet no-op.
198
198
  *
199
- * A pack compiler selects its entries by the configured content package, so a
200
- * single wrong package id rejects every note in a perfectly good tree and every
201
- * pack ships blank while the build exits 0 (#1502). The empty-tree guard in
199
+ * A pack ships blank whenever every note in a full tree was rejected — by a
200
+ * `selects` that claims nothing, or a `pack:` that routes everything elsewhere
201
+ * and the build then exits 0 (#1502). The empty-tree guard in
202
202
  * {@link generatePacksJson} cannot see that: the tree is full, it is the
203
203
  * *output* that is empty.
204
204
  *
@@ -212,10 +212,10 @@ export function emptyPassErrors(passes) {
212
212
  .map(
213
213
  (pass) =>
214
214
  `Pack "${pass.name}" compiled 0 entries from a non-empty ` +
215
- `content tree. Every note was rejected — check that the notes ` +
216
- `declare the package this build compiles (\`contentPackage\` in ` +
217
- `package-build.config.yaml), or declare the pack ` +
218
- `\`mayBeEmpty\` if it genuinely ships nothing.`,
215
+ `content tree. Every note was rejected — check that the tree ` +
216
+ `holds notes of the type this pack claims, and that their ` +
217
+ `\`pack:\` routes here, or declare the pack \`mayBeEmpty\` if it ` +
218
+ `genuinely ships nothing.`,
219
219
  );
220
220
  }
221
221
 
@@ -36,6 +36,7 @@ import log from "loglevel";
36
36
  import { loadPackConfig } from "./pack-config.mjs";
37
37
  import { packRouter } from "./pack-router.mjs";
38
38
  import { contentPackage, foundryPackageId } from "./content-package.mjs";
39
+ import { notePackage, searchableFrontmatter } from "./note-package.mjs";
39
40
  import { loadForeignManifests, PACKAGE_BASE } from "./kb-manifest.mjs";
40
41
  import { buildWikilinkIndex, convertWikilinks } from "./wikilinks.mjs";
41
42
  import { expandContentTables } from "./content-tables.mjs";
@@ -601,7 +602,9 @@ export function collectContentDocs(contentBase) {
601
602
  if (!fm) continue;
602
603
  const segments = path.relative(contentBase, absPath).split(path.sep);
603
604
  docs.push({
604
- fm,
605
+ // With its package present whether the note declares one or not, so
606
+ // a `WHERE … package = "…"` query reads the same either way (#56).
607
+ fm: searchableFrontmatter(fm),
605
608
  // POSIX-separated and relative to the content root — what a
606
609
  // `path:` search term globs, on every platform.
607
610
  path: segments.join("/"),
@@ -633,14 +636,17 @@ const packLinkable = (doc) =>
633
636
  * Expand the fenced `dataview` tables in one note's markdown, before wikilinks
634
637
  * are resolved — so a generated cell may itself be a wikilink.
635
638
  *
636
- * A table searches only notes of the source note's own `package`, so a SoHL
637
- * page never tabulates setting-package content (and vice versa).
639
+ * A table searches only notes of the source note's own package, so a SoHL page
640
+ * never tabulates setting-package content (and vice versa). Each candidate's
641
+ * package is **derived** rather than read out of its frontmatter: `package:` is
642
+ * optional, and comparing a declared value with an absent one would drop every
643
+ * unswept — or every swept — note from the table (#56).
638
644
  *
639
645
  * @param {string} body - The note's markdown body.
640
646
  * @param {object} ctx
641
647
  * @param {Array<object>} ctx.docs - From {@link collectContentDocs}.
642
648
  * @param {string} ctx.name - The note, for the error message.
643
- * @param {string} [ctx.pkg] - The source note's `package`.
649
+ * @param {string} [ctx.pkg] - The source note's package.
644
650
  * @param {object} [ctx.fm] - The source note's frontmatter, which is what a
645
651
  * query's `this` reads. Its entry in `docs` supplies the path as well.
646
652
  * @param {number} [ctx.bodyLine] - 1-based file line of the body's first line,
@@ -654,10 +660,12 @@ const packLinkable = (doc) =>
654
660
  * `position`, the directive's own line.
655
661
  */
656
662
  export function expandNoteTables(body, { docs, name, pkg, fm, bodyLine }) {
657
- const scoped = pkg ? docs.filter((d) => d.fm?.package === pkg) : docs;
663
+ const scoped = pkg ? docs.filter((d) => notePackage(d.fm) === pkg) : docs;
658
664
  const self =
659
665
  fm ?
660
- (docs.find((d) => d.fm?.id && d.fm.id === fm.id) ?? { fm })
666
+ (docs.find((d) => d.fm?.id && d.fm.id === fm.id) ?? {
667
+ fm: searchableFrontmatter(fm),
668
+ })
661
669
  : undefined;
662
670
  const { markdown, errors, lineMap } = expandContentTables(body ?? "", {
663
671
  docs: scoped,
package/engine/index.mjs CHANGED
@@ -54,6 +54,9 @@ export * as packRouter from "./pack-router.mjs";
54
54
  /** The content package a build compiles, and the Foundry package it ships in. */
55
55
  export * as contentPackage from "./content-package.mjs";
56
56
 
57
+ /** Which content package a note belongs to, and refusing one that disagrees. */
58
+ export * as notePackage from "./note-package.mjs";
59
+
57
60
  /** The shipped Foundry manifest: locating it, reading it, guarding its id. */
58
61
 
59
62
  /** The URL a content note is published at — the one web-address rule. */
@@ -16,7 +16,7 @@
16
16
  * Foundry compendium from markdown notes in the `assets/content/` tree.
17
17
  *
18
18
  * The content root (`contentBase`) is walked recursively; any `.md` file
19
- * whose frontmatter declares `package: sohl` and either `type: doc` or a
19
+ * whose frontmatter declares either `type: doc` or a
20
20
  * **doc-carrying type** ({@link sohl.utils.packs.docEntryTypes} — every item
21
21
  * type, plus `macro`) is compiled into one JournalEntry document. Each note's
22
22
  * body is split on top-level H1 headings; the optional content before the
@@ -54,7 +54,6 @@ import {
54
54
  md,
55
55
  } from "./helpers.mjs";
56
56
  import { BasePackCompiler } from "./base-compiler.mjs";
57
- import { contentPackage } from "./content-package.mjs";
58
57
  import { anchorPageId } from "./wikilinks.mjs";
59
58
  import { hasDocEntry, itemDocEntryId } from "./item-docs.mjs";
60
59
 
@@ -367,8 +366,7 @@ export class Journals extends BasePackCompiler {
367
366
  /** @inheritdoc */
368
367
  reportDetail(stats) {
369
368
  log.debug(
370
- `Skipped ${stats.skippedOther} non-doc file(s) ` +
371
- `(not type:doc package:${contentPackage()})`,
369
+ `Skipped ${stats.skippedOther} non-doc file(s) (not type:doc)`,
372
370
  );
373
371
  }
374
372
  }
package/engine/macros.mjs CHANGED
@@ -286,8 +286,8 @@ export function buildMacroEntry(
286
286
  /**
287
287
  * Macros pack compiler.
288
288
  *
289
- * Walks the content tree and compiles every `package: sohl`, `type: macro`
290
- * note into one Macro document. The same note's documentation is compiled by
289
+ * Walks the content tree and compiles every `type: macro` note into one Macro
290
+ * document. The same note's documentation is compiled by
291
291
  * the journals pass; neither pass reads the other's output.
292
292
  */
293
293
  export class Macros extends BasePackCompiler {