@heroiclands/package-build 3.1.0 → 3.3.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.
@@ -1,11 +1,19 @@
1
1
  /**
2
2
  * The tallies one pass accumulates while walking the tree.
3
3
  *
4
+ * `declined` and `skippedOther` are deliberately separate numbers. A declined
5
+ * note is one this build **refused** — it names a package this repository does
6
+ * not compile — and it is an error; a skipped one legitimately belongs to
7
+ * another pass, and there are thousands of those. Folding the first into the
8
+ * second is what let a whole tree be filtered out in silence (#56).
9
+ *
4
10
  * @typedef {object} PassStats
5
11
  * @property {number} compiled - Notes that became a document.
6
12
  * @property {number} skippedDraft - Notes marked `draft: true`.
7
13
  * @property {number} skippedNoId - Notes with no `id`, where that is tolerated.
8
14
  * @property {number} skippedOther - Notes this pass does not claim.
15
+ * @property {number} declined - Notes refused because they declare another
16
+ * package. Counted as errors, never as skips.
9
17
  */
10
18
  /**
11
19
  * The shared walk → filter → expand → convert → build → write → count loop.
@@ -136,8 +144,9 @@ export class BasePackCompiler {
136
144
  /**
137
145
  * Whether this pass claims a note. **Required.**
138
146
  *
139
- * Called only for a note of the configured content package, so a subclass
140
- * decides on `type` alone.
147
+ * Called only for a note this build compiles every note in the tree
148
+ * belongs to the configured content package (#56) — so a subclass decides
149
+ * on `type` alone.
141
150
  *
142
151
  * @param {object} fm - The note's frontmatter.
143
152
  * @returns {boolean} True to compile it.
@@ -299,6 +308,12 @@ export class BasePackCompiler {
299
308
  }
300
309
  /**
301
310
  * The tallies one pass accumulates while walking the tree.
311
+ *
312
+ * `declined` and `skippedOther` are deliberately separate numbers. A declined
313
+ * note is one this build **refused** — it names a package this repository does
314
+ * not compile — and it is an error; a skipped one legitimately belongs to
315
+ * another pass, and there are thousands of those. Folding the first into the
316
+ * second is what let a whole tree be filtered out in silence (#56).
302
317
  */
303
318
  export type PassStats = {
304
319
  /**
@@ -317,4 +332,9 @@ export type PassStats = {
317
332
  * - Notes this pass does not claim.
318
333
  */
319
334
  skippedOther: number;
335
+ /**
336
+ * - Notes refused because they declare another
337
+ * package. Counted as errors, never as skips.
338
+ */
339
+ declined: number;
320
340
  };
@@ -1,10 +1,23 @@
1
1
  /**
2
- * The **content** package: the distribution unit a note declares in its
3
- * `package:` frontmatter. The pack compilers select their entries by it.
2
+ * The **content** package: the distribution unit this repository's notes belong
3
+ * to, and the **address namespace** every one of them is published under.
4
+ *
5
+ * It is the first segment of every canonical key (`sohl-skill-clmb`), the name
6
+ * of the link manifest this build emits (`sohl.json`), and the package a
7
+ * cross-package wikilink writes to reach one of these notes. So it is the
8
+ * repository's identity in the address space, not a switch — and never dead
9
+ * configuration, whatever else changes.
10
+ *
11
+ * It was also, until #56, a **selector**: a note declared the same value in its
12
+ * `package:` frontmatter and the compilers kept the ones that matched. Every
13
+ * content tree is single-package — each is single-sourced in the repository that
14
+ * ships it — so the field restated this constant once per note while a value
15
+ * that matched nothing filtered the whole tree out in silence. The field is
16
+ * being retired; the value stays, here, where it is declared once.
4
17
  *
5
18
  * Stable across compilation targets. If this content were ever compiled for a
6
- * second game system, its notes would still declare `package: sohl` — only the
7
- * Foundry package below would differ.
19
+ * second game system, it would still be published as `sohl` — only the Foundry
20
+ * package below would differ.
8
21
  *
9
22
  * An accessor rather than a hoisted constant, so that importing this module
10
23
  * needs no configuration (#2).
@@ -18,7 +31,7 @@ export function contentPackage(): string;
18
31
  * compendium UUID the compilers emit.
19
32
  *
20
33
  * Distinct from {@link contentPackage}, and equal to it only by coincidence
21
- * here: a note says `package: sohl` and its documents are addressed as
34
+ * here: a note is published under `sohl` and its documents are addressed as
22
35
  * `Compendium.sohl.<pack>.<Type>.<id>`. In `sohl-thalorna` the two differ
23
36
  * (`thalorna` vs `sohl-thalorna`), which is why they are separate values rather
24
37
  * than one — treating them as interchangeable is what #1498 was.
@@ -20,9 +20,9 @@ export function itemPackJsonDirs(config?: object): string[];
20
20
  * The passes that compiled nothing when they were expected to compile
21
21
  * something — a build failure, not a quiet no-op.
22
22
  *
23
- * A pack compiler selects its entries by the configured content package, so a
24
- * single wrong package id rejects every note in a perfectly good tree and every
25
- * pack ships blank while the build exits 0 (#1502). The empty-tree guard in
23
+ * A pack ships blank whenever every note in a full tree was rejected — by a
24
+ * `selects` that claims nothing, or a `pack:` that routes everything elsewhere
25
+ * and the build then exits 0 (#1502). The empty-tree guard in
26
26
  * {@link generatePacksJson} cannot see that: the tree is full, it is the
27
27
  * *output* that is empty.
28
28
  *
@@ -274,14 +274,17 @@ export function collectContentDocs(contentBase: string): Array<{
274
274
  * Expand the fenced `dataview` tables in one note's markdown, before wikilinks
275
275
  * are resolved — so a generated cell may itself be a wikilink.
276
276
  *
277
- * A table searches only notes of the source note's own `package`, so a SoHL
278
- * page never tabulates setting-package content (and vice versa).
277
+ * A table searches only notes of the source note's own package, so a SoHL page
278
+ * never tabulates setting-package content (and vice versa). Each candidate's
279
+ * package is **derived** rather than read out of its frontmatter: `package:` is
280
+ * optional, and comparing a declared value with an absent one would drop every
281
+ * unswept — or every swept — note from the table (#56).
279
282
  *
280
283
  * @param {string} body - The note's markdown body.
281
284
  * @param {object} ctx
282
285
  * @param {Array<object>} ctx.docs - From {@link collectContentDocs}.
283
286
  * @param {string} ctx.name - The note, for the error message.
284
- * @param {string} [ctx.pkg] - The source note's `package`.
287
+ * @param {string} [ctx.pkg] - The source note's package.
285
288
  * @param {object} [ctx.fm] - The source note's frontmatter, which is what a
286
289
  * query's `this` reads. Its entry in `docs` supplies the path as well.
287
290
  * @param {number} [ctx.bodyLine] - 1-based file line of the body's first line,
@@ -5,6 +5,7 @@ export * as contentTree from "./content-tree.mjs";
5
5
  export * as packConfig from "./pack-config.mjs";
6
6
  export * as packRouter from "./pack-router.mjs";
7
7
  export * as contentPackage from "./content-package.mjs";
8
+ export * as notePackage from "./note-package.mjs";
8
9
  export * as contentSlug from "./content-slug.mjs";
9
10
  export * as contentAddress from "./content-address.mjs";
10
11
  export * as foreignManifests from "./foreign-manifests.mjs";
@@ -122,8 +122,8 @@ export const DEFAULT_MACRO_IMG: "icons/svg/dice-target.svg";
122
122
  /**
123
123
  * Macros pack compiler.
124
124
  *
125
- * Walks the content tree and compiles every `package: sohl`, `type: macro`
126
- * note into one Macro document. The same note's documentation is compiled by
125
+ * Walks the content tree and compiles every `type: macro` note into one Macro
126
+ * document. The same note's documentation is compiled by
127
127
  * the journals pass; neither pass reads the other's output.
128
128
  */
129
129
  export class Macros extends BasePackCompiler {
@@ -39,9 +39,12 @@ export function entriesForNote(fm: object, name: string, address: string, body:
39
39
  *
40
40
  * Drafts are excluded because the site does not publish them, and an entry for
41
41
  * an unpublished page is exactly the dead link the manifest exists to prevent.
42
- * A note belonging to another content package is skipped for the same reason in
43
- * reverse: this build is not authoritative for it, and its own build says where
44
- * it lives.
42
+ *
43
+ * Every note in the tree is this package's note, whether or not it says so:
44
+ * `package:` is optional and merely has to agree (#56). A note naming a
45
+ * different package **throws** rather than being skipped — this build is not
46
+ * authoritative for it, and skipping it silently is how a whole tree came to be
47
+ * filtered out of a manifest that then claimed the package published nothing.
45
48
  *
46
49
  * A note that has no address is **reported, not guessed** — the finding carries
47
50
  * the file and the reason, so a caller can print it or fail on it. Inventing an
@@ -0,0 +1,54 @@
1
+ /**
2
+ * The package a note belongs to.
3
+ *
4
+ * Non-validating: the answer for a note that declares nothing, and for one that
5
+ * declares the configured package, is the same value. A note declaring some
6
+ * *other* package is answered literally here rather than corrected — the
7
+ * compile pass reports that, once, through {@link assertNotePackage}.
8
+ *
9
+ * @param {object|null|undefined} fm - Parsed frontmatter, or nothing when it
10
+ * could not be parsed.
11
+ * @param {string} [configured] - The package this build compiles. Defaults to
12
+ * the configured `contentPackage`; passed explicitly by callers that already
13
+ * carry it in a context object, so a caller's configuration drives every read.
14
+ * @returns {string} The package.
15
+ */
16
+ export function notePackage(fm: object | null | undefined, configured?: string): string;
17
+ /**
18
+ * A note's frontmatter as a generated table searches it — its package present
19
+ * whether or not the note declares one.
20
+ *
21
+ * A `dataview` query resolves `package` out of frontmatter like any other
22
+ * field, so a collection note that scopes itself with `WHERE … and package =
23
+ * "sohl"` matches nothing once the field is deleted, and renders an **empty
24
+ * table** in silence. Deriving the value here keeps the two spellings
25
+ * equivalent, so a sweep that deletes the field is mechanical rather than a
26
+ * trap (#56) — and a query that never mentions `package` is unaffected either
27
+ * way.
28
+ *
29
+ * The declared value is left alone when there is one, so nothing about an
30
+ * unswept tree changes.
31
+ *
32
+ * @param {object|null|undefined} fm - Parsed frontmatter.
33
+ * @param {string} [configured] - The package this build compiles.
34
+ * @returns {object|null|undefined} The frontmatter itself when it declares a
35
+ * package, else a shallow copy carrying the derived one.
36
+ */
37
+ export function searchableFrontmatter(fm: object | null | undefined, configured?: string): object | null | undefined;
38
+ /**
39
+ * The package a note belongs to, refusing one that names another package.
40
+ *
41
+ * @param {object|null|undefined} fm - Parsed frontmatter.
42
+ * @param {object} [options] - Options.
43
+ * @param {string} [options.file] - The note's path, named in the message. Omit
44
+ * it where the caller emits through a diagnostic, which puts the locator at
45
+ * the start of the line already — repeating it prints the path twice.
46
+ * @param {string} [options.configured] - The package this build compiles.
47
+ * Defaults to the configured `contentPackage`.
48
+ * @returns {string} The package, which is always `configured`.
49
+ * @throws {Error} When the note declares a different package.
50
+ */
51
+ export function assertNotePackage(fm: object | null | undefined, { file, configured }?: {
52
+ file?: string | undefined;
53
+ configured?: string | undefined;
54
+ }): string;
@@ -46,8 +46,9 @@ export class PackRoutingError extends Error {
46
46
  /**
47
47
  * The frontmatter field a note declares its pack in.
48
48
  *
49
- * Deliberately close to `package:` and deliberately not the same word: a note's
50
- * `package:` says which *distribution* owns it, `pack:` which *compendium*
49
+ * Deliberately close to the retiring `package:` and deliberately not the same
50
+ * word: `package:` said which *distribution* owned a note now the
51
+ * repository's `contentPackage` (#56) — while `pack:` says which *compendium*
51
52
  * receives its document.
52
53
  */
53
54
  export const PACK_FIELD: "pack";
@@ -16,7 +16,9 @@ export function walkSiteTree(dir: string, skip?: readonly string[]): string[];
16
16
  * The content tree's pages, and what could not be addressed.
17
17
  *
18
18
  * @param {string} contentBase - Absolute path to the content tree.
19
- * @param {object} ctx - `{ packages, skipDirectories, mount, scheme }`.
19
+ * @param {object} ctx - `{ packages, contentPackage, skipDirectories, mount,
20
+ * scheme }`. `contentPackage` is the package a note that declares none
21
+ * belongs to.
20
22
  * @returns {{pages: object[], slugFindings: object[], fmLinkFindings: object[]}}
21
23
  */
22
24
  export function collectContentPages(contentBase: string, ctx: object): {
@@ -25,6 +25,33 @@ export class Actors extends BasePackCompiler {
25
25
  * entry plus one per `sohl.items` entry. `sohl.skills` is ignored.
26
26
  */
27
27
  buildEmbeddedItems(itemsMap: any, actorId: any, fm: any, ctx: any): any[];
28
+ /**
29
+ * Bake each unopened skill's opening mastery level into the document (#46).
30
+ *
31
+ * A skill whose `masteryLevelBase` is still null once the note's frontmatter
32
+ * has been merged onto the catalogue entry is *not yet opened*, and the
33
+ * client fills it in on import — `Skill Base × initSkillMult`, in
34
+ * `SkillLogic.initialize`. Computing it here instead leaves the compiled
35
+ * pack self-describing: what a being's skills open at is visible in the
36
+ * document, reviewable in a diff, and testable without standing up Foundry.
37
+ *
38
+ * This runs last because the Skill Base formula reads the actor's
39
+ * attributes, so every attribute item has to exist first. It only ever
40
+ * fills nulls — a skill that states a `masteryLevelBase`, whether from the
41
+ * catalogue or the note, keeps it untouched.
42
+ *
43
+ * **The scores used are the ones just written.** `SkillLogic` resolves
44
+ * `attr.<code>` to an attribute's *effective* score, after active effects;
45
+ * all this pass has is the `scoreBase` it set from `sohl.attributes`. For a
46
+ * compiled being carrying no attribute-altering effects the two agree,
47
+ * which is every being in content today. One that did carry such an effect
48
+ * would bake a Skill Base its client then disagrees with — that is the
49
+ * limit of doing this at build time, and the point to revisit if it bites.
50
+ *
51
+ * @param {object[]} items - The actor's embedded items, attributes included.
52
+ * @param {string} ctx - Diagnostic context (the actor's label).
53
+ */
54
+ openUnopenedSkills(items: object[], ctx: string): void;
28
55
  buildBeing(itemsMap: any, fm: any, body: any): {
29
56
  name: any;
30
57
  type: string;
@@ -0,0 +1,53 @@
1
+ /**
2
+ * The HârnMaster Skill Base reduction, mirroring SoHL's `sb()` helper exactly.
3
+ *
4
+ * @param {...number} values - One or more attribute values.
5
+ * @returns {number} The reduced Skill Base.
6
+ * @throws {Error} If called with no arguments.
7
+ */
8
+ export function sb(...values: number[]): number;
9
+ /**
10
+ * Evaluate a `skillBaseFormula` against an actor's attribute scores.
11
+ *
12
+ * Mirrors `SkillLogic.computeSkillBase`: an absent or blank formula is Skill
13
+ * Base `0` (not an error — a skill may legitimately have none), and the result
14
+ * is clamped to `>= 0`.
15
+ *
16
+ * @param {string|null|undefined} formula - The expression source.
17
+ * @param {Record<string, number>} attrs - Attribute scores by shortcode.
18
+ * @returns {{ value: number, error?: string }} The Skill Base, or the reason it
19
+ * could not be computed. On error `value` is `0`, matching the client.
20
+ */
21
+ export function evaluateSkillBase(formula: string | null | undefined, attrs?: Record<string, number>): {
22
+ value: number;
23
+ error?: string;
24
+ };
25
+ /**
26
+ * The mastery level an unopened skill opens at, or `null` when it does not
27
+ * open at all.
28
+ *
29
+ * The client's rule (`SkillLogic.initialize`) is `Skill Base × initSkillMult`,
30
+ * applied only when `masteryLevelBase` is unset and the skill is on an actor.
31
+ * Two build-side refinements, neither of which changes what a client computes:
32
+ *
33
+ * - **A zero or absent `initSkillMult` stays `null`.** The multiplier is the
34
+ * switch for whether a skill opens at all, so writing the `0` the arithmetic
35
+ * yields would claim the skill opened at zero rather than that it never
36
+ * opened. `null` is what the field means by *not yet opened*, and the client
37
+ * arrives at the same place either way.
38
+ * - **A fractional product is an error, not a rounding.** `masteryLevelBase` is
39
+ * an integer field (`min: 0`), so a fractional value cannot be persisted
40
+ * honestly — where the client multiplies raw into a modifier and is free to
41
+ * carry the fraction, this is not. Reporting it follows
42
+ * `resolveSkillAptitudes`, which rejects a fractional modifier rather than
43
+ * rounding one.
44
+ *
45
+ * @param {object} system - The merged skill `system` block.
46
+ * @param {Record<string, number>} attrs - Attribute scores by shortcode.
47
+ * @returns {{ value: number|null, error?: string }} The opening mastery level,
48
+ * `null` to leave the field unset, or the reason it could not be computed.
49
+ */
50
+ export function openingMasteryLevel(system?: object, attrs?: Record<string, number>): {
51
+ value: number | null;
52
+ error?: string;
53
+ };