@heroiclands/package-build 3.3.0 → 4.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,184 @@
1
1
  # @heroiclands/package-build
2
2
 
3
+ ## 4.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - 8cfa834: Reject `package:` in a note's frontmatter (#56).
8
+
9
+ A note's package is the repository's configured `contentPackage`, full stop. A
10
+ note that declares the field fails the build, naming the file — **whatever the
11
+ value says**. An agreeing declaration is refused exactly as a disagreeing one
12
+ is: there is no value that makes writing the field correct, and a field accepted
13
+ while it agrees is a field that grows back one note at a time.
14
+
15
+ ```text
16
+ assets/content/Gear/Axe.md:12:1: error: `package: sohl` is a retired frontmatter field — delete it. A note's package is this repository's configured `contentPackage` ("sohl", in package-build.config.yaml), and every note in the tree belongs to it.
17
+ ```
18
+
19
+ `content-build lint` reports every such note in one pass, so a tree can be
20
+ checked before it is compiled; `package compile` and `manifest` refuse it.
21
+
22
+ **This is the third and last step**, and the two before it are already released
23
+ and adopted. 3.3.0 made the field optional so an absent one was normal and a
24
+ disagreeing one was an error; every content tree on the org was then swept on
25
+ that version — `sohl` (1,639 files), `thalorna` (1,716), `kethira` (363),
26
+ `harnensemble` (2,517). Nothing this release refuses is authored anywhere today.
27
+
28
+ **Why a major.** Consumers resolve `^3`, so a minor would reach every repository
29
+ on the next Dependabot run. A major is adopted deliberately, one repository at a
30
+ time, in a pull request that can also delete the field if any grew back — which
31
+ is the whole mechanism that made the deprecate → migrate → remove sequence safe.
32
+
33
+ **Migrating** is one line, and nothing else reads the field:
34
+
35
+ ```bash
36
+ find assets/content -name '*.md' -print0 | xargs -0 sed -i '' '/^package: /d'
37
+ ```
38
+
39
+ `package compile` then produces byte-identical output, because the value the
40
+ build derives is the value the notes restated. See `MIGRATING.md`.
41
+
42
+ **`contentPackage` is unaffected, and is not vestigial.** It is the address
43
+ namespace — the first segment of every canonical key, the name of the emitted
44
+ link manifest, and the package a cross-package wikilink writes. Retiring the
45
+ frontmatter field is what leaves it as the single source of that value: every
46
+ key is now derived from the configuration, where it used to come from two
47
+ sources that happened to agree.
48
+
49
+ **A generated table's `WHERE … and package = "<pkg>"` clause keeps matching** —
50
+ 45 such clauses across `sohl` and `thalorna` depend on it. The package is
51
+ _synthesised_ into what the table search sees, from `contentPackage`; it is a
52
+ search value, never an authored one.
53
+
54
+ **API.** `engine/note-package.mjs` no longer exports `notePackage` — every call
55
+ site takes `contentPackage` directly, so no key is derived from frontmatter
56
+ anywhere — and `assertNotePackage` is now `assertNoDeclaredPackage`, which
57
+ asserts the field's absence rather than answering which package a note belongs
58
+ to. `expandNoteTables` no longer takes `pkg`: a table searches the whole tree,
59
+ which is one package's notes and nothing else.
60
+ - ad7691f: Remove `draft:` from a note's frontmatter (#69).
61
+
62
+ The field excluded a note from the compiled packs, from the link manifest and
63
+ from a consuming site build — and **nothing reported the consequence**.
64
+ `content-links.mjs`, `site-index.mjs` and `content-lint.mjs` never read it, so a
65
+ wikilink into a drafted note was indistinguishable from a link to a note that
66
+ does not exist, and no checker could say which. Its entire effect was to move a
67
+ note from _published_ to _unresolvable_, in silence. It also suppressed real
68
+ build failures: a note the compilers never reached could not fail on the defects
69
+ it carried.
70
+
71
+ Nothing used it. Across every HeroicLands content repository — `sohl`,
72
+ `sohl-thalorna`, `sohl-kethira-basic`, `harn-ensemble`, `harn-adventures` — not
73
+ one note declared it.
74
+
75
+ **What changed**
76
+
77
+ - The three readers are gone: the compile loop, the link-manifest walk, and the
78
+ scenes pass's map collection. So are the `skippedDraft` tally, its `PassStats`
79
+ field, and the `Skipped N draft(s)` log line.
80
+ - A note declaring `draft:` now **fails the build**, naming the file and the
81
+ line, whatever the value says — `draft: false` included, since it reads as
82
+ "publish this note", which is what happens either way. A field left merely
83
+ ignored reads to its author as though it still works, which is the same
84
+ silence in a different place.
85
+ - `content-build lint` reports it too, so a whole tree is answered at once
86
+ rather than one note per build.
87
+
88
+ **The `draft` _tag_ is untouched.** It is an authoring marker, read only by the
89
+ generated-table pass for `FROM #draft` queries, and 268 `sohl-thalorna` notes
90
+ carry it. An unfinished page is honest about being unfinished; a dropped link is
91
+ silent.
92
+
93
+ **Adopting**
94
+
95
+ Nothing to sweep — no note in the org declares the field. A consumer that
96
+ carries one deletes the line.
97
+
98
+ This lands in the same major as the `package:` rejection (#56 step 3), so the
99
+ two retired fields are one adoption rather than two. They are refused the same
100
+ way, through the same diagnostic format and the same positioning, and the
101
+ locator both need is now shared rather than written twice.
102
+
103
+ ## 3.4.0
104
+
105
+ ### Minor Changes
106
+
107
+ - 19df269: Stop emitting `assocMysteryCode` on a compiled mystical ability (#35).
108
+
109
+ The `mysticalability` declaration named a field no SoHL DataModel receives.
110
+ `MysticalAbilityDataModel` declares `subType`, `assocSkillCode`,
111
+ `assocAffiliationCode`, `masteryLevelBase`, `improveFlag`, `levelBase` and
112
+ `charges`, and nothing else — Foundry discards the extra key when the document
113
+ is constructed, so every mystical ability in every consuming pack shipped a
114
+ value that was thrown away at load, with nothing at compile or load time saying
115
+ so.
116
+
117
+ This is the exact inverse of #3 and has the same root cause: nothing compares a
118
+ builder's emitted `system` block against the DataModel that receives it. #3 was a
119
+ declared field the builder failed to emit; this is an emitted field the DataModel
120
+ never declares. Both compile clean, both lose data silently, and an author cannot
121
+ tell either from a correct build. The general check is #60.
122
+
123
+ **The field was retired, not renamed.**
124
+ Song-of-Heroic-Lands-FoundryVTT#973 deleted `assocMysteryCode` because nothing in
125
+ production read the mystery it resolved to; #1012 later added
126
+ `assocAffiliationCode` as a separate concept — the faction whose standing confers
127
+ the ability. They look alike and mean different things, so the declaration is
128
+ dropped rather than retargeted.
129
+
130
+ **This changes emitted documents**, so a consumer wants a rebuild rather than a
131
+ silent upgrade — though nothing downstream can have depended on the value:
132
+
133
+ - `sohl` — nine notes authored the key, all of them blank. Corrected in
134
+ Song-of-Heroic-Lands-FoundryVTT#1747.
135
+ - `sohl-kethira-basic` — no note authors it; all 224 mystical abilities carried
136
+ the builder's own `""`. Recompiling `main` with this change removes exactly
137
+ those 224 lines and touches nothing else.
138
+ - dced8b2: Write the derived package into an emitted page's frontmatter (#65).
139
+
140
+ `content-build site` copied a note's frontmatter to the page verbatim. Since
141
+ 3.3.0 a note need not declare `package:` — it is derived from the configured
142
+ `contentPackage` — and `engine/site-build.mjs` resolved that value one line
143
+ before it built the page, carrying it for the index, the table universe and the
144
+ local-package set. It never reached the page's own frontmatter, so a swept tree
145
+ published pages that said nothing about which package they belong to.
146
+
147
+ **The visible symptom is the breadcrumb.**
148
+ `@heroiclands/hugo-theme`'s `layouts/partials/breadcrumbs.html` reads
149
+ `{{ $pkg := .Params.package | default "" }}` and builds the middle crumb from
150
+ it. With no `package` the section is never resolved and the crumb degrades from
151
+ a linked, labelled section to a bare, unlinked type slug:
152
+
153
+ ```text
154
+ before: Home > SoHL Affliction > Aconite (linked)
155
+ after: Home > affliction > Aconite (bare)
156
+ ```
157
+
158
+ Consumer layouts reading the field directly degrade the same way — a `package`
159
+ column renders blank.
160
+
161
+ The fix is where the value was already known: `pageFrontmatter` spreads
162
+ `package` after the note's own frontmatter, so a note that declares the field
163
+ keeps its authored position and value and an unswept tree emits byte-identically,
164
+ while a swept one regains the line it lost. The alternative — teaching every
165
+ theme and consumer layout to default the package from a site parameter — pushes
166
+ a fact the build already knows out to N consumers, and the theme deliberately
167
+ carries no addresses.
168
+
169
+ **This changes emitted output**, so a consumer wants a rebuild rather than a
170
+ silent upgrade, which is why it is a minor rather than a patch — the same
171
+ reasoning as #35. Verified against the swept `sohl` tree: 1,606 emitted content
172
+ pages differ, none added or removed, and the only diff line class across the
173
+ whole tree is the restored `package: sohl`. Rendering that tree, 1,600 pages
174
+ differ and in exactly two ways — the breadcrumb's middle crumb, and a `package`
175
+ column that was blank.
176
+
177
+ `sohl-thalorna` has the same defect independently, in its own
178
+ `utils/build-site-content.mjs`, and is fixed in that repository
179
+ (sohl-thalorna#79) — two emitters, one behaviour, which is a second argument
180
+ for #36.
181
+
3
182
  ## 3.3.0
4
183
 
5
184
  ### Minor Changes
package/CONTENT.md CHANGED
@@ -178,27 +178,44 @@ something migrates on it.
178
178
  ### A note's package is the repository's, not the note's
179
179
 
180
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.
181
+ published under: the first segment of every canonical key (`sohl-skill-clmb`),
182
+ the name of the link manifest this build emits (`sohl.json`), and the package a
183
+ cross-package wikilink writes to reach one of these notes. It is the
184
+ repository's identity in the address space — not a filter — and a note does not
185
+ restate it.
186
+
187
+ **`package:` in a note's frontmatter is retired, and declaring it fails the
188
+ build**, naming the file, whatever the value says. An agreeing declaration is
189
+ refused exactly as a disagreeing one is: there is no value that makes writing
190
+ the field correct. The diagnostic says so, and says where the value comes from
191
+ instead:
182
192
 
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.
193
+ ```text
194
+ assets/content/Gear/Axe.md:12:1: error: `package: sohl` is a retired frontmatter field — delete it. A note's package is this repository's configured `contentPackage` ("sohl", in package-build.config.yaml), and every note in the tree belongs to it.
195
+ ```
196
+
197
+ `content-build lint` reports every such note in one pass; `content-build
198
+ package compile` and `content-build manifest` refuse the tree.
189
199
 
190
200
  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.
201
+ working: the package is **synthesised** into what the table search sees,
202
+ supplied from `contentPackage` rather than read off the note. It is a search
203
+ value, never an authored one.
194
204
 
195
205
  It used to **select**: a note compiled when its `package:` matched and was
196
206
  skipped when it did not. Every content tree is single-package — each is
197
207
  single-sourced in the repository that ships it — so the field restated one
198
208
  constant thousands of times, while a tree whose notes named a package no
199
209
  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.
210
+ field from a note is the fix; deleting the _configured_ value is not, since
211
+ every address derives from it.
212
+
213
+ Sweeping a tree is mechanical — the field is a whole line, and nothing else
214
+ reads it:
215
+
216
+ ```bash
217
+ find assets/content -name '*.md' -print0 | xargs -0 sed -i '' '/^package: /d'
218
+ ```
202
219
 
203
220
  ### A registry of your own
204
221
 
package/MIGRATING.md CHANGED
@@ -1,3 +1,50 @@
1
+ # Migrating to `@heroiclands/package-build` 4.0.0
2
+
3
+ **One authoring change: delete `package:` from every content note.** A note's
4
+ package is the repository's configured `contentPackage`, and declaring the field
5
+ is now a build error rather than a redundancy the build tolerated.
6
+
7
+ ## 1. Sweep the content tree
8
+
9
+ The field is a whole line, and nothing else reads it:
10
+
11
+ ```bash
12
+ find assets/content -name '*.md' -print0 | xargs -0 sed -i '' '/^package: /d'
13
+ ```
14
+
15
+ (GNU `sed`: `sed -i '/^package: /d'`.) Then compile — `content-build package
16
+ compile` must produce byte-identical output to the run before the sweep, because
17
+ the value the build derives is the value the notes restated.
18
+
19
+ A note that still carries the field fails the build where it is:
20
+
21
+ ```text
22
+ assets/content/Gear/Axe.md:12:1: error: `package: sohl` is a retired frontmatter field — delete it. A note's package is this repository's configured `contentPackage` ("sohl", in package-build.config.yaml), and every note in the tree belongs to it.
23
+ ```
24
+
25
+ `content-build lint` reports every one of them in a single pass, so the sweep can
26
+ be checked before it is compiled.
27
+
28
+ ## 2. Nothing else
29
+
30
+ - **`contentPackage` stays**, and is unchanged. It is the address namespace —
31
+ the first segment of every canonical key, the name of the emitted link
32
+ manifest, and the package a cross-package wikilink writes. Every address in
33
+ every manifest is identical across this upgrade.
34
+ - **A generated table's `WHERE … and package = "<pkg>"` clause keeps matching.**
35
+ The package is synthesised into what the table search sees, from
36
+ `contentPackage`; it was never the authored field that answered the clause
37
+ after 3.3.0.
38
+ - **No configuration key changed**, and no CLI command, flag or exit code.
39
+
40
+ ## What this replaced
41
+
42
+ A note used to be _selected_ by the field: it compiled when `package:` matched
43
+ `contentPackage` and was skipped, silently and as "belongs to another pass",
44
+ when it did not. A tree whose notes named a package no configuration answered to
45
+ compiled **zero notes and exited 0** (#56). 3.3.0 made the field optional so
46
+ every repository could be swept on a non-breaking version; this major removes it.
47
+
1
48
  # Migrating to `@heroiclands/package-build` 3.0.0
2
49
 
3
50
  `@heroiclands/content-build` and `@heroiclands/package-build` are one package.
@@ -14,9 +14,9 @@
14
14
  /**
15
15
  * `BasePackCompiler` — the one compile loop every pack pass runs.
16
16
  *
17
- * Walking the content tree, rejecting what this build does not own, skipping
18
- * drafts, expanding generated tables, converting wikilinks, writing the JSON
19
- * and counting what failed are the same in every pass. They were written out
17
+ * Walking the content tree, rejecting what this build does not own, expanding
18
+ * generated tables, converting wikilinks, writing the JSON and counting what
19
+ * failed are the same in every pass. They were written out
20
20
  * once per pass — three times when this was filed, five by the time it landed —
21
21
  * so a fix to any of them had to be made everywhere, and the passes drifted
22
22
  * apart in exactly the places nobody was comparing (#1509).
@@ -73,26 +73,25 @@ import {
73
73
  expandNoteTables,
74
74
  } from "./helpers.mjs";
75
75
  import { emitDiagnostic } from "./diagnostics.mjs";
76
- import { contentPackage } from "./content-package.mjs";
77
- import { assertNotePackage } from "./note-package.mjs";
76
+ import { assertNoDeclaredPackage } from "./note-package.mjs";
77
+ import { assertNoDraftField } from "./retired-fields.mjs";
78
78
  import { assertTypeNotRetired, packForType } from "./ids.mjs";
79
79
 
80
80
  /**
81
81
  * The tallies one pass accumulates while walking the tree.
82
82
  *
83
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).
84
+ * note is one this build **refused** — it declares a retired frontmatter field
85
+ * — and it is an error; a skipped one legitimately belongs to another pass, and
86
+ * there are thousands of those. Folding the first into the second is what let a
87
+ * whole tree be filtered out in silence (#56).
88
88
  *
89
89
  * @typedef {object} PassStats
90
90
  * @property {number} compiled - Notes that became a document.
91
- * @property {number} skippedDraft - Notes marked `draft: true`.
92
91
  * @property {number} skippedNoId - Notes with no `id`, where that is tolerated.
93
92
  * @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.
93
+ * @property {number} declined - Notes refused because they declare a retired
94
+ * frontmatter field. Counted as errors, never as skips.
96
95
  */
97
96
 
98
97
  /**
@@ -336,9 +335,6 @@ export class BasePackCompiler {
336
335
  const { markdown: tabulated, lineMap } = expandNoteTables(body, {
337
336
  docs: this.contentDocs,
338
337
  name,
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(),
342
338
  fm,
343
339
  bodyLine,
344
340
  });
@@ -512,15 +508,13 @@ export class BasePackCompiler {
512
508
  if (stats.skippedNoId) {
513
509
  log.info(`Skipped ${stats.skippedNoId} note(s) missing id`);
514
510
  }
515
- if (stats.skippedDraft) {
516
- log.info(`Skipped ${stats.skippedDraft} draft(s)`);
517
- }
518
511
  if (stats.declined) {
519
512
  // Its own line, at error level: these are not skips, and burying
520
513
  // them in the skipped tally is the defect (#56). Each one has
521
514
  // already been named individually as a diagnostic.
522
515
  log.error(
523
- `Declined ${stats.declined} note(s) declaring another package`,
516
+ `Declined ${stats.declined} note(s) declaring a retired ` +
517
+ `frontmatter field`,
524
518
  );
525
519
  }
526
520
  this.reportDetail(stats);
@@ -535,7 +529,6 @@ export class BasePackCompiler {
535
529
  /** @type {PassStats} */
536
530
  const stats = {
537
531
  compiled: 0,
538
- skippedDraft: 0,
539
532
  skippedNoId: 0,
540
533
  skippedOther: 0,
541
534
  declined: 0,
@@ -560,18 +553,27 @@ export class BasePackCompiler {
560
553
  stats.skippedOther++;
561
554
  continue;
562
555
  }
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.
556
+ // The retired frontmatter fields, refused before `selects` so a
557
+ // note is answered whichever pass would have claimed it — and
558
+ // whatever the declared value says.
559
+ //
560
+ // - `package:` (#56): a note's package is the repository's
561
+ // configured one, so declaring it restates a constant.
562
+ // - `draft:` (#69): it excluded the note from the packs, the
563
+ // manifest and the site, and no checker reported the links that
564
+ // left dangling.
565
+ //
566
+ // Both are reported and counted — never skipped, which is how a
567
+ // tree naming a package nothing answers to used to compile zero
568
+ // notes and exit 0. The file comes from the diagnostic locator, so
569
+ // neither message may repeat it.
569
570
  try {
570
- assertNotePackage(fm);
571
+ assertNoDeclaredPackage(fm, { absPath });
572
+ assertNoDraftField(fm, { absPath });
571
573
  } catch (err) {
572
574
  stats.declined++;
573
575
  this.errorCount++;
574
- this.noteError(err.message);
576
+ this.noteError(err.message, err.position);
575
577
  continue;
576
578
  }
577
579
  // Checked before `selects`, and therefore for every note this
@@ -584,11 +586,6 @@ export class BasePackCompiler {
584
586
  stats.skippedOther++;
585
587
  continue;
586
588
  }
587
- if (fm.draft === true) {
588
- stats.skippedDraft++;
589
- log.debug(`Skipping draft: ${absPath}`);
590
- continue;
591
- }
592
589
  if (!fm.id) {
593
590
  if (this.constructor.requiresId) {
594
591
  throw new Error(`${Label} missing id: ${absPath}`);
@@ -597,9 +594,9 @@ export class BasePackCompiler {
597
594
  this.noteWarn(`${label} note has no id, skipping`);
598
595
  continue;
599
596
  }
600
- // Which pack of this type takes it. Applied after the draft and
601
- // id checks a draft is not compiled anywhere, so its declaration
602
- // is nobody's business — and before `skipNote`, so a note this pack
597
+ // Which pack of this type takes it. Applied after the id check —
598
+ // a note with no id is nobody's document, so its routing is
599
+ // nobody's business — and before `skipNote`, so a note this pack
603
600
  // does not own never reaches this pass's own rejection rules.
604
601
  try {
605
602
  if (!this.routesHere(fm)) {
@@ -52,7 +52,8 @@ 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
+ import { contentPackage } from "./content-package.mjs";
56
+ import { searchableFrontmatter } from "./note-package.mjs";
56
57
  import {
57
58
  canonicalKey,
58
59
  loadForeignManifests,
@@ -130,12 +131,13 @@ export function buildLinkIndex(
130
131
  const byAlias = new Map();
131
132
  const aliasCollide = new Set();
132
133
 
134
+ // The one package every note in this tree belongs to. Taken from the
135
+ // configuration, never from a note: `package:` is retired, so there is no
136
+ // second source an address could disagree with (#56).
137
+ const pkg = contentPackage();
138
+
133
139
  for (const note of notes) {
134
140
  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);
139
141
  if (typeof fm.shortcode === "string" && fm.shortcode) {
140
142
  byKey.set(`${type}/${fm.shortcode}`.toLowerCase(), note);
141
143
  // The canonical, fully qualified address alongside the short one,
@@ -170,7 +172,7 @@ export function buildLinkIndex(
170
172
  // A foreign package may use a type this tree has never seen, so its types
171
173
  // join `types` — otherwise `readQualifier` reads the link as prose and it
172
174
  // is never checked at all.
173
- const localPackages = new Set(notes.map((n) => notePackage(n.fm)));
175
+ const localPackages = new Set([pkg]);
174
176
  const foreign =
175
177
  manifestDir ?
176
178
  loadForeignManifests(manifestDir, localPackages)
@@ -178,15 +180,15 @@ export function buildLinkIndex(
178
180
  for (const v of foreign.index.values()) if (v.type) types.add(v.type);
179
181
 
180
182
  const packages = new Set([
181
- ...[...byKey.values()].map((n) => notePackage(n.fm)),
183
+ ...(byKey.size ? [pkg] : []),
182
184
  ...foreign.packages,
183
185
  ]);
184
186
 
185
187
  /** The searchable universe a `dataview` table draws its rows from. */
186
188
  const tableDocs = notes.map((n) => ({
187
- // Package present however the note spells it see
188
- // {@link searchableFrontmatter} (#56).
189
- fm: searchableFrontmatter(n.fm),
189
+ // Package present for a `WHERE package = "…"` clause, synthesised
190
+ // rather than authored — see {@link searchableFrontmatter} (#56).
191
+ fm: searchableFrontmatter(n.fm, pkg),
190
192
  path: n.rel,
191
193
  tld: n.rel.split("/")[0],
192
194
  folder: path.dirname(n.rel).split("/").pop(),
@@ -205,9 +207,9 @@ export function buildLinkIndex(
205
207
  let body = note.body;
206
208
  if (/^[ \t]*(?:`{3,}|~{3,})[ \t]*dataview\b/im.test(body)) {
207
209
  body = expandContentTables(body, {
208
- docs: tableDocs.filter(
209
- (d) => notePackage(d.fm) === notePackage(note.fm),
210
- ),
210
+ // Unfiltered: every note in the tree is this package's, so
211
+ // there is no other package's note to exclude (#56).
212
+ docs: tableDocs,
211
213
  linkable: (d) => Boolean(d.fm.shortcode),
212
214
  source: note.file,
213
215
  }).markdown;
@@ -38,8 +38,9 @@ import { loadPackConfig } from "./pack-config.mjs";
38
38
  * `package:` frontmatter and the compilers kept the ones that matched. Every
39
39
  * content tree is single-package — each is single-sourced in the repository that
40
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.
41
+ * that matched nothing filtered the whole tree out in silence. That field is
42
+ * retired and declaring it now fails the build; this value stays, here, where
43
+ * it is declared once.
43
44
  *
44
45
  * Stable across compilation targets. If this content were ever compiled for a
45
46
  * second game system, it would still be published as `sohl` — only the Foundry
@@ -147,8 +147,8 @@ function workedExample(type, fields) {
147
147
  `type: ${type}`,
148
148
  "shortcode: xmpl",
149
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
+ // `contentPackage`, and declaring the field is a build error (#56)
151
+ // this example is the smallest note that compiles.
152
152
  "id: <16-character id>",
153
153
  "sohl:",
154
154
  " archetype: null",
@@ -51,6 +51,7 @@
51
51
  import { authoredFields } from "./field-spec.mjs";
52
52
  import { positionInFrontmatter } from "./diagnostics.mjs";
53
53
  import { RETIRED_TYPES } from "./ids.mjs";
54
+ import { draftRetiredMessage } from "./retired-fields.mjs";
54
55
 
55
56
  /**
56
57
  * `sohl:` keys every type accepts, whatever its schema says.
@@ -211,6 +212,31 @@ export function lintNote(note, { schemas, index }) {
211
212
  const at = (key, literal) =>
212
213
  positionInFrontmatter(raw(), key, literal ?? undefined);
213
214
 
215
+ // The retired top-level fields, checked before the type: a note may carry
216
+ // one whatever its type is, and each finding stands on its own. Reported
217
+ // here as well as refused at compile because this is where an author meets
218
+ // every finding in the tree at once, rather than one note at a time (#56).
219
+ if (Object.hasOwn(fm, "package")) {
220
+ findings.push({
221
+ file: note.file,
222
+ ...at("package"),
223
+ severity: "error",
224
+ message:
225
+ "`package:` is a retired frontmatter field — delete it. A " +
226
+ "note's package is this repository's configured " +
227
+ "`contentPackage`, in package-build.config.yaml, and every " +
228
+ "note in the tree belongs to it",
229
+ });
230
+ }
231
+ if (Object.hasOwn(fm, "draft")) {
232
+ findings.push({
233
+ file: note.file,
234
+ ...at("draft"),
235
+ severity: "error",
236
+ message: draftRetiredMessage(),
237
+ });
238
+ }
239
+
214
240
  const replacement = RETIRED_TYPES[type];
215
241
  if (replacement) {
216
242
  findings.push({
@@ -36,7 +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
+ import { searchableFrontmatter } from "./note-package.mjs";
40
40
  import { loadForeignManifests, PACKAGE_BASE } from "./kb-manifest.mjs";
41
41
  import { buildWikilinkIndex, convertWikilinks } from "./wikilinks.mjs";
42
42
  import { expandContentTables } from "./content-tables.mjs";
@@ -602,8 +602,9 @@ export function collectContentDocs(contentBase) {
602
602
  if (!fm) continue;
603
603
  const segments = path.relative(contentBase, absPath).split(path.sep);
604
604
  docs.push({
605
- // With its package present whether the note declares one or not, so
606
- // a `WHERE package = "…"` query reads the same either way (#56).
605
+ // With its package supplied for a `WHERE package = "…"` query —
606
+ // synthesised from the configuration, since no note declares it
607
+ // (#56).
607
608
  fm: searchableFrontmatter(fm),
608
609
  // POSIX-separated and relative to the content root — what a
609
610
  // `path:` search term globs, on every platform.
@@ -636,17 +637,15 @@ const packLinkable = (doc) =>
636
637
  * Expand the fenced `dataview` tables in one note's markdown, before wikilinks
637
638
  * are resolved — so a generated cell may itself be a wikilink.
638
639
  *
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).
640
+ * A table searches the whole tree, which is one package's notes and nothing
641
+ * else so there is no longer a package to scope on. It used to filter, back
642
+ * when a tree could hold several packages' notes and `package:` said which was
643
+ * which; that field is retired and the filter with it (#56).
644
644
  *
645
645
  * @param {string} body - The note's markdown body.
646
646
  * @param {object} ctx
647
647
  * @param {Array<object>} ctx.docs - From {@link collectContentDocs}.
648
648
  * @param {string} ctx.name - The note, for the error message.
649
- * @param {string} [ctx.pkg] - The source note's package.
650
649
  * @param {object} [ctx.fm] - The source note's frontmatter, which is what a
651
650
  * query's `this` reads. Its entry in `docs` supplies the path as well.
652
651
  * @param {number} [ctx.bodyLine] - 1-based file line of the body's first line,
@@ -659,8 +658,7 @@ const packLinkable = (doc) =>
659
658
  * compile rather than shipping a table-shaped hole. The error carries
660
659
  * `position`, the directive's own line.
661
660
  */
662
- export function expandNoteTables(body, { docs, name, pkg, fm, bodyLine }) {
663
- const scoped = pkg ? docs.filter((d) => notePackage(d.fm) === pkg) : docs;
661
+ export function expandNoteTables(body, { docs, name, fm, bodyLine }) {
664
662
  const self =
665
663
  fm ?
666
664
  (docs.find((d) => d.fm?.id && d.fm.id === fm.id) ?? {
@@ -668,7 +666,7 @@ export function expandNoteTables(body, { docs, name, pkg, fm, bodyLine }) {
668
666
  })
669
667
  : undefined;
670
668
  const { markdown, errors, lineMap } = expandContentTables(body ?? "", {
671
- docs: scoped,
669
+ docs,
672
670
  linkable: packLinkable,
673
671
  source: name,
674
672
  self,
package/engine/index.mjs CHANGED
@@ -54,9 +54,12 @@ 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. */
57
+ /** Which content package a note belongs to, and refusing one that says so. */
58
58
  export * as notePackage from "./note-package.mjs";
59
59
 
60
+ /** Frontmatter fields a note may no longer declare, and the refusal of them. */
61
+ export * as retiredFields from "./retired-fields.mjs";
62
+
60
63
  /** The shipped Foundry manifest: locating it, reading it, guarding its id. */
61
64
 
62
65
  /** The URL a content note is published at — the one web-address rule. */