@heroiclands/package-build 5.0.0 → 6.1.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.
@@ -150,3 +150,31 @@ export function positionOfLiteral(text: string, needle: string, occurrence?: num
150
150
  line?: number;
151
151
  column?: number;
152
152
  };
153
+ /**
154
+ * Where a **key or value in a YAML document** sits, addressed by its path.
155
+ *
156
+ * {@link positionOfLiteral} is the plain search, and it is the wrong tool for a
157
+ * configuration file: a pack name like `items` or `actors` appears in the
158
+ * `packs:` block, in a folder's list, in a path, and often in prose, so the
159
+ * first occurrence is routinely not the one the finding is about — which is the
160
+ * one thing the located form exists to prevent.
161
+ *
162
+ * A path resolves the exact node instead. The document is re-parsed here rather
163
+ * than threaded from the loader because the loader returns plain data: `yaml`
164
+ * discards ranges once a document is materialised, and carrying a parallel
165
+ * position tree through configuration resolution would be a second
166
+ * representation of the same file to keep in step.
167
+ *
168
+ * Every failure — unparseable text, an `.mjs` configuration, a path that
169
+ * resolves to nothing — yields `{}`, so a caller spreads the result and the
170
+ * position is dropped rather than guessed.
171
+ *
172
+ * @param {string} text - The document's contents.
173
+ * @param {ReadonlyArray<string|number>} keyPath - Path to the node: map keys as
174
+ * strings, sequence entries as numbers.
175
+ * @returns {{line?: number, column?: number}} Spreadable position fields.
176
+ */
177
+ export function positionOfYamlPath(text: string, keyPath: ReadonlyArray<string | number>): {
178
+ line?: number;
179
+ column?: number;
180
+ };
@@ -6,8 +6,9 @@
6
6
  * and an actor's embedded items may be sourced from any of them. Finding one
7
7
  * pack and stopping is how embedded-item resolution would silently miss every
8
8
  * item that landed in another. Returned in configured order, which is also the
9
- * order they compile in, so a pack later in the list cannot be read before it
10
- * is written.
9
+ * order they compile in {@link orderPassesByDependency} keeps the declared
10
+ * order among packs of one type — and every one of them is written before the
11
+ * actors pass that reads them.
11
12
  *
12
13
  * @param {object} [config] - The resolved build configuration. Defaults to this
13
14
  * repository's.
@@ -16,6 +17,53 @@
16
17
  * caller that needs one, refuses that itself.
17
18
  */
18
19
  export function itemPackJsonDirs(config?: object): string[];
20
+ /**
21
+ * The passes to run, ordered so that each one follows the output it reads.
22
+ *
23
+ * **Declaration order is presentation, not compile order (#73).** The same
24
+ * `packs:` list is the manifest's `packs` array, which a consumer orders for a
25
+ * reader browsing compendiums; the actors pass, meanwhile, resolves each
26
+ * being's embedded items against the item passes' *output*. Making one list
27
+ * satisfy both meant an Actor pack declared first compiled only where a
28
+ * previous run had already left `build/packs-json` populated — green on a warm
29
+ * tree, exit 1 on every fresh checkout and every CI runner, and `build/` is
30
+ * gitignored so that is the state CI always starts from.
31
+ *
32
+ * **The reordering is the smallest one that works.** Each step takes the
33
+ * *earliest declared* pass whose dependencies are all already emitted, so a
34
+ * list that was already in a workable order comes back untouched, and one that
35
+ * was not moves exactly the passes that had to move. A dependency is satisfied
36
+ * only when **every** pack of that type has run: a being addresses an item by
37
+ * `(type, shortcode)` without knowing which Item pack ships it, so waiting for
38
+ * one of several would resolve some beings and silently fail others.
39
+ *
40
+ * A dependency on a type this configuration declares no pack of is not waited
41
+ * for. A package may ship an Actor pack and no Item pack; the pass that needs
42
+ * one refuses on its own, with a message about items rather than about order.
43
+ *
44
+ * @param {readonly object[]} packs - The passes to be run, as declared.
45
+ * @returns {object[]} A new list, in compile order. The input is untouched.
46
+ * @throws {Error} If the passes read each other's output in a cycle, which no
47
+ * order can satisfy. Only reachable from a mis-declared compiler, so it names
48
+ * the passes rather than blaming the pack list.
49
+ */
50
+ export function orderPassesByDependency(packs: readonly object[]): object[];
51
+ /**
52
+ * The dependencies this run cannot satisfy by ordering, because the pass that
53
+ * would produce them is not in it.
54
+ *
55
+ * Ordering answers the whole-package build; a run restricted to one pack
56
+ * (`content-build package compile <name>`) cannot conjure the passes it left
57
+ * out. Where their output is already on disk from an earlier run that is fine
58
+ * — it is how compiling one pack at a time is meant to work — so this reports
59
+ * only what is genuinely absent, and names the pack that would write it rather
60
+ * than the directory that is missing.
61
+ *
62
+ * @param {readonly object[]} running - The passes this run will execute.
63
+ * @param {object} config - The resolved build configuration.
64
+ * @returns {string[]} One message per unsatisfiable dependency.
65
+ */
66
+ export function unsatisfiedPassDependencies(running: readonly object[], config: object): string[];
19
67
  /**
20
68
  * The passes that compiled nothing when they were expected to compile
21
69
  * something — a build failure, not a quiet no-op.
@@ -5,6 +5,87 @@
5
5
  * @returns {boolean} Whether it is a homepage note.
6
6
  */
7
7
  export function isHomepage(fm: object | null | undefined): boolean;
8
+ /**
9
+ * The address-bearing fields one note authors, in the order it authored them.
10
+ *
11
+ * Authoring order rather than declaration order, so a caller emitting one
12
+ * diagnostic per finding emits them top to bottom down the file — the order a
13
+ * reader and a compiler-output parser both expect.
14
+ *
15
+ * Presence is the whole test: `shortcode:` authored empty still says "this page
16
+ * has an address of its own", and a value cannot make the claim true.
17
+ *
18
+ * Returned without a locator, because the two things that would supply one —
19
+ * the raw note text and the position helper — belong to the caller. This
20
+ * mirrors {@link module:engine/retired-fields}, whose retired-field messages
21
+ * are likewise positioned by whoever reports them.
22
+ *
23
+ * @param {object|null|undefined} fm - Parsed frontmatter.
24
+ * @returns {Array<{key: string, message: string}>} One entry per field the note
25
+ * authored, empty for any note that is not a homepage.
26
+ */
27
+ export function checkHomepageAddressFields(fm: object | null | undefined): Array<{
28
+ key: string;
29
+ message: string;
30
+ }>;
31
+ /**
32
+ * Require exactly one homepage note in a content tree (#52).
33
+ *
34
+ * "Exactly one" is two rules, and they are **one severity** because they are
35
+ * one defect: a package whose front page is not the page a person chose.
36
+ *
37
+ * - _None_ and the package serves nothing at `/<package>/`. That is the failure
38
+ * #50 exists to prevent, and it is silent — the site build reports `wrote 0
39
+ * homepage(s)` and exits 0.
40
+ * - _Two_ and it serves a page nobody chose. Every homepage is written to the
41
+ * same {@link HOMEPAGE_DESTINATION}, so the second overwrites the first and
42
+ * the package's front page is decided by the order the walk happened to reach
43
+ * the files in — by *filename*, on a type whose whole point is that it is
44
+ * routed by frontmatter. There is no "first wins" convention to fall back on,
45
+ * so nothing here can pick the right one.
46
+ *
47
+ * Neither has a safe default, so neither is a warning. A warning is the right
48
+ * severity for something a build can proceed past correctly, and a build that
49
+ * proceeds past either of these publishes the wrong front page while reporting
50
+ * success — which is the exact outcome a warning would be tolerating.
51
+ *
52
+ * **Two is reported once per note, not once for the tree.** Each note is a
53
+ * place an author has to open and edit, and a single finding saying "there are
54
+ * two" sends them hunting for the second.
55
+ *
56
+ * **None is located at the tree, honestly.** There is no file to name, so the
57
+ * locator is the content root — the directory the note is missing from, which
58
+ * is a real path and the one the author adds it to. No line and no column are
59
+ * invented for it, per the diagnostic rules in
60
+ * {@link module:engine/diagnostics}. {@link lintContentTree} already reports an
61
+ * empty walk against the same locator.
62
+ *
63
+ * The rule reads no `site:` configuration and does not vary by
64
+ * `publish.site`: that setting chooses whether the *content* surfaces are
65
+ * published, and the homepage is the floor underneath both modes.
66
+ *
67
+ * @param {ReadonlyArray<{file: string}>} found - The homepage notes, in walk
68
+ * order. Paths may be absolute or relative to the working directory.
69
+ * @param {object} options - Options.
70
+ * @param {string} options.contentBase - Root of the content tree, for the
71
+ * locator when there is no file to name.
72
+ * @param {string} [options.contentPackage] - The package this tree builds.
73
+ * Dropped from the message when unknown rather than guessed.
74
+ * @returns {Array<{file: string, line?: number, column?: number,
75
+ * severity: "error", message: string}>} The findings, one per offending note.
76
+ */
77
+ export function checkHomepageCount(found: ReadonlyArray<{
78
+ file: string;
79
+ }>, { contentBase, contentPackage }: {
80
+ contentBase: string;
81
+ contentPackage?: string | undefined;
82
+ }): Array<{
83
+ file: string;
84
+ line?: number;
85
+ column?: number;
86
+ severity: "error";
87
+ message: string;
88
+ }>;
8
89
  /**
9
90
  * The title a homepage publishes under.
10
91
  *
@@ -46,49 +127,40 @@ export function homepageFrontmatter(fm: object, { contentPackage, title }: {
46
127
  title: string;
47
128
  }): object;
48
129
  /**
49
- * The package homepage — a note that compiles to a **page** rather than to a
50
- * compendium document (#51).
51
- *
52
- * Every HeroicLands package is reachable at `https://www.heroiclands.org/<contentPackage>/`,
53
- * and what a reader finds there is one markdown file in the content tree,
54
- * written by a person: what the module is, which system it needs, how to install
55
- * it, where its source lives. Nothing about it is derived.
56
- *
57
- * **Authored, not assembled.** An earlier sketch generated the page in tiers —
58
- * identity and licence from the manifest, install URL from the release address,
59
- * "requires" links from `relationships`, a card per configured section. It would
60
- * have worked and needed almost no authoring, and it produces a page nobody
61
- * chose the contents of. The things that matter most on these pages cannot be
62
- * derived: that Kethira requires buying the book from Keléstia, what Thalorna's
63
- * setting *is*, which of twenty sections a reader should start with. So the only
64
- * thing defaulted here is the title, from `packageBuild.manifest.title`, so that
65
- * the package's name is not written twice.
66
- *
67
- * **Dispatched by `type`, not by filename.** A fixed `homepage.md` the walker
68
- * special-cased would be the anomaly: notes are routed by frontmatter, not by
69
- * location, and `NOTE_SCHEMAS` already routes `doc`, `macro`, `being` and the
70
- * map types. `homepage` is one more entry whose compile step emits a page. It is
71
- * deliberately not `README.md`: `landing: readme` already means "a `README.md`
72
- * is its section's landing page", and `sohl-thalorna/assets/content/README.md`
73
- * is a developer explainer about the source tree — adopting that name would make
74
- * Thalorna's public front page its build documentation.
75
- *
76
- * **Engine, not `sohl/`.** The `engine/` ÷ `sohl/` line separates *note-format*
77
- * knowledge from *game-system* knowledge, and a homepage is note format: it
78
- * carries no `system` block, mirrors no item builder, and would mean the same
79
- * thing for a game system that is not SoHL. Reachability is the symptom that
80
- * makes it obvious — `HarnMaster-3-FoundryVTT` declares no `itemBuilders`, so a
81
- * type living in the SoHL registry would be unavailable to HM3 and to every HM3
82
- * module, which is most of the packages that need a homepage and nothing else.
83
- *
84
- * **Its address is the package's, not the note's.** A homepage publishes at
85
- * `/<contentPackage>/` because that is where the package is, so `name.full`,
86
- * `shortcode` and `id` decide nothing on it (#53 refuses them outright; this
87
- * module simply never reads them). It compiles into no document, so it carries
88
- * no compendium UUID and appears in no pack and in no link-manifest entry.
89
- *
90
- * @module
130
+ * Every address a homepage carries, wherever it is written.
131
+ *
132
+ * **Both halves of the page are in scope, and that is the finding rather than
133
+ * the assumption.** Of the six homepages authored today, four carry every link
134
+ * in the body as ordinary markdown and two carry them in `landing:` and the
135
+ * one whose dead links prompted the check has an *empty body*, so a body-only
136
+ * reading would have found nothing at all on it. A dead link in a card is
137
+ * exactly as broken as one in a paragraph.
138
+ *
139
+ * Three shapes are gathered, and the caller needs to tell them apart because
140
+ * the rules differ:
141
+ *
142
+ * - **`url`** package-relative, resolved against the site by the theme.
143
+ * - **`href`** already resolved, used verbatim.
144
+ * - **prose and body markdown links** emitted as written and resolved by the
145
+ * browser against the landing's own address, which *is* the package root, so
146
+ * a relative one means the same thing a `url` does.
147
+ *
148
+ * `banner:` is not an address: it is an image path resolved through the CDN
149
+ * base, and `banner: none` is a sentinel rather than a target. Top-level
150
+ * `title` and `description` are not walked either they are set as text, never
151
+ * rendered as markdown.
152
+ *
153
+ * @param {object|null|undefined} fm - The note's frontmatter.
154
+ * @param {string} [body] - The note's markdown body.
155
+ * @returns {Array<{field: string, url: string, kind: string}>} Every address,
156
+ * frontmatter first and then the body, each with the dotted path it was
157
+ * written at.
91
158
  */
159
+ export function homepageAddresses(fm: object | null | undefined, body?: string): Array<{
160
+ field: string;
161
+ url: string;
162
+ kind: string;
163
+ }>;
92
164
  /**
93
165
  * The note type that compiles to the package homepage.
94
166
  *
@@ -116,3 +188,55 @@ export const HOMEPAGE_FIELDS: readonly import("./field-spec.mjs").FieldSpec[];
116
188
  * @type {string}
117
189
  */
118
190
  export const HOMEPAGE_DESTINATION: string;
191
+ /**
192
+ * The top-level fields a homepage refuses, and what each one would decide (#53).
193
+ *
194
+ * A note's URL derives from `name.full` and its identity from
195
+ * `(type, shortcode)`. The homepage is the one page for which neither holds: it
196
+ * publishes at `/<package>/`, fixed by the package id. An author fluent in the
197
+ * conventions writes them here expecting exactly what they do everywhere else,
198
+ * and gets none of it.
199
+ *
200
+ * **They were never inert, which is why ignoring them was the wrong answer.** A
201
+ * `shortcode` puts the note in the address index and in the `dataview` link
202
+ * universe, so `[[homepage-<shortcode>]]` resolves *green* — to
203
+ * `homepage/<slug>/`, an address derived from `name.full` and published by
204
+ * nothing, because a homepage is written to {@link HOMEPAGE_DESTINATION} at the
205
+ * package root. A build that reports a live link to a 404 is worse than one
206
+ * that says nothing. It also inflates `content-build lint`'s address tally, so
207
+ * the lint and the link manifest disagree about what the package publishes.
208
+ *
209
+ * **A named class, not an allow-list, and that boundary is the decision.** The
210
+ * documented envelope is `type` plus an optional `title`, and `landing`,
211
+ * `description` and `banner` are legitimate beside them — but a homepage's
212
+ * frontmatter is *emitted into the published page*
213
+ * ({@link homepageFrontmatter}), so an unrecognised key is a Hugo or theme
214
+ * parameter this build has never heard of and has no standing to refuse.
215
+ * Rejecting unknown keys would make every new theme parameter wait on a
216
+ * package-build release. What is refused is the specific class that makes a
217
+ * false claim about *where this page is*.
218
+ *
219
+ * `aliases` is deliberately not in the class: {@link homepageFrontmatter}
220
+ * already drops it from every emitted page, with a reason of its own, so
221
+ * authoring one is the same no-op it is on any other page rather than a wrong
222
+ * belief about this one's address.
223
+ *
224
+ * @type {ReadonlyMap<string, string>}
225
+ */
226
+ export const HOMEPAGE_REFUSED_FIELDS: ReadonlyMap<string, string>;
227
+ /**
228
+ * The two frontmatter keys that hold an address, and what each one means.
229
+ *
230
+ * They are **not** interchangeable, and a check that treated them as one would
231
+ * be wrong about both. The theme resolves a `url` against the site with
232
+ * `relURL`, so a package writes `kb/rules/` and is served `/sohl/kb/rules/`
233
+ * without ever naming its own prefix. An `href` is an address that is *already*
234
+ * resolved and is used verbatim — which is what `cards.source: sections` fills
235
+ * in, since a section's permalink already carries the prefix.
236
+ *
237
+ * So a leading `/` is a defect in a `url` (it is prefixed a second time) and
238
+ * correct in an `href`.
239
+ *
240
+ * @type {ReadonlySet<string>}
241
+ */
242
+ export const HOMEPAGE_ADDRESS_KEYS: ReadonlySet<string>;
@@ -56,6 +56,19 @@ export function configFromData(data: unknown, configPath: string): import("../co
56
56
  * compilers know neither what to compile nor where to put it.
57
57
  */
58
58
  export function loadPackConfig(): import("../config.mjs").ContentBuildConfig;
59
+ /**
60
+ * The file {@link loadPackConfig} resolved the configuration from.
61
+ *
62
+ * A diagnostic about a *configured* value has to name the file it was declared
63
+ * in, and re-deriving that path at the point of the finding would be a second
64
+ * resolution free to disagree with the first — the `PACKAGE_BUILD_CONFIG`
65
+ * override, the upward walk and the one-file-per-directory rule all have to
66
+ * come out the same way. This reports the path actually read.
67
+ *
68
+ * @returns {string} Its absolute path.
69
+ * @throws {Error} As {@link loadPackConfig}, when there is no configuration.
70
+ */
71
+ export function packConfigPath(): string;
59
72
  /** The stem every consuming repository declares its build under. */
60
73
  export const CONFIG_BASENAME: "package-build.config";
61
74
  /**
@@ -50,9 +50,9 @@ export function collectTreePages(tree: object, ctx: object): {
50
50
  * packages ship under is a property of the code path rather than of a
51
51
  * configuration that happens to be empty (#55).
52
52
  *
53
- * Returned as a list rather than as the one note there should be. Requiring
54
- * exactly one is #52's, and it is a separate decision — this reports what it
55
- * found so a count is visible either way.
53
+ * Returned as a list rather than as the one note there should be, because the
54
+ * count is what {@link checkHomepageCount} judges (#52) — this walk reports
55
+ * what it found, and {@link buildSite} decides whether that is one.
56
56
  *
57
57
  * @param {string} contentBase - Absolute path to the content tree.
58
58
  * @param {object} ctx - `{ skipDirectories }`.
@@ -65,10 +65,16 @@ export function collectHomepages(contentBase: string, ctx: object): {
65
65
  * Writes each homepage at the package's own root.
66
66
  *
67
67
  * Its own writer, deliberately small. A homepage is authored markdown published
68
- * verbatim — no table expansion, no section landing, and (until #54) no link
69
- * resolution — so routing it through {@link renderPages} would buy it a pipeline
70
- * it has no input for, and would make homepage-only mode depend on the index,
71
- * the foreign manifests and the table universe that mode exists to not build.
68
+ * verbatim — no table expansion, no section landing and no link resolution — so
69
+ * routing it through {@link renderPages} would buy it a pipeline it has no input
70
+ * for, and would make homepage-only mode depend on the index, the foreign
71
+ * manifests and the table universe that mode exists to not build.
72
+ *
73
+ * **Verbatim is the answer to #54, not a gap left by it.** A landing's links
74
+ * could not be *resolved* here without giving `homepage` mode the index its
75
+ * licensing fence exists to not build, so they are **checked** instead:
76
+ * {@link auditHomepageLinks} reads the `landing:` addresses and the body's
77
+ * markdown links, and reports a wikilink on the page rather than resolving one.
72
78
  *
73
79
  * @param {string} outRoot - The package's site root — the configured `site.out`,
74
80
  * one level above the content mount.
@@ -123,6 +129,31 @@ export function gatesFailed(gates: any): boolean;
123
129
  * @returns {Map<string, object[]>} Package → the notes it may tabulate.
124
130
  */
125
131
  export function tableUniverse(pages: object[]): Map<string, object[]>;
132
+ /**
133
+ * The front matter a section's landing states about itself.
134
+ *
135
+ * The section metadata a configuration resolved, ready to be written or merged
136
+ * onto a page. Two things happen here and nothing else does:
137
+ *
138
+ * - **`title` leads.** It is the one key every landing has carried since the
139
+ * first one, and a landing whose block opened with `banner:` would be a
140
+ * gratuitous diff on every consumer's tree.
141
+ * - **An absent value is left off**, not written as `undefined` — which is not
142
+ * a value YAML can carry, and would abort the serializer.
143
+ *
144
+ * Everything else the section declared is passed through. That is the point of
145
+ * the function: before #91 both writers transcribed `title` and `banner` by
146
+ * name, so the vocabulary lived in three places — the schema that admits a key
147
+ * and the two writers that copy it — and a key added to the schema alone
148
+ * validated cleanly and then reached no page. The *schema* is the bound worth
149
+ * keeping (see `normalizeSectionMeta`, which refuses a key it does not know and
150
+ * names it); a second, silent bound in the writers is not.
151
+ *
152
+ * @param {object} meta - A resolved `site.sections` / `site.readmeSections`
153
+ * entry.
154
+ * @returns {object} Its front matter, `title` first.
155
+ */
156
+ export function sectionFrontmatter(meta: object): object;
126
157
  /**
127
158
  * The frontmatter a page publishes with.
128
159
  *
@@ -59,6 +59,60 @@ export function releaseUrls({ repoUrl, version, artifact }: {
59
59
  * @returns {object[]} The manifest's `packs` array.
60
60
  */
61
61
  export function manifestPacks(config: object): object[];
62
+ /**
63
+ * What `packFolders` and the derived `packs[]` disagree about.
64
+ *
65
+ * `packFolders` is the one **declared** manifest key that names something the
66
+ * build **derives**: every other declared key states a fact about the package
67
+ * (`title`, `socket`, `grid`) or addresses a staged file (`esmodules`,
68
+ * `styles`, `languages`), and a staged file is a different relation, checked
69
+ * against the stage rather than against configuration. So this is the one place
70
+ * a declaration can go stale against a value the build already computed — and
71
+ * until now nothing compared them (#81).
72
+ *
73
+ * `HarnMaster-3-FoundryVTT` shipped the consequence: its folder named four
74
+ * packs, three of which had not existed since the compendium was consolidated,
75
+ * and omitted `items` — 1,577 of 1,597 documents, loose in Foundry's compendium
76
+ * browser, with the build reporting nothing (HM3#420).
77
+ *
78
+ * **The two findings are not the same finding**, and giving them one severity
79
+ * gets one of them wrong:
80
+ *
81
+ * - _A folder names a pack that does not exist_ is an **error**. Foundry
82
+ * resolves the name against the package's own packs and silently skips what
83
+ * it cannot find, so the declaration does nothing at all; there is no
84
+ * arrangement in which it is intended, and the fix is unambiguous.
85
+ * - _A pack no folder names_ is a **warning**. It is legal and can be
86
+ * deliberate — a package may want one pack at the root — so failing on it
87
+ * would break working packages for a matter of taste. But a package that
88
+ * bothered to declare a folder rarely meant to leave one out, which is
89
+ * exactly how HM3's `items` went unnoticed.
90
+ * - _A package declaring no folders_ says **nothing**. Everything at the root
91
+ * is the majority arrangement, not an omission.
92
+ *
93
+ * Errors come first, in declaration order, then warnings in pack order: the
94
+ * unresolvable names are what a reader fixes, and a folder gaining a name often
95
+ * settles a warning too.
96
+ *
97
+ * @param {object} options
98
+ * @param {unknown} [options.packFolders] - The declared `packFolders`.
99
+ * @param {ReadonlyArray<{name: string}>} [options.packs] - The derived packs,
100
+ * as {@link manifestPacks} returns them.
101
+ * @returns {Array<{severity: "error"|"warning", message: string, pack: string,
102
+ * folder?: string, keyPath: Array<string|number>}>} The findings, ordered.
103
+ */
104
+ export function packFolderFindings({ packFolders, packs }: {
105
+ packFolders?: unknown;
106
+ packs?: readonly {
107
+ name: string;
108
+ }[] | undefined;
109
+ }): Array<{
110
+ severity: "error" | "warning";
111
+ message: string;
112
+ pack: string;
113
+ folder?: string;
114
+ keyPath: Array<string | number>;
115
+ }>;
62
116
  /**
63
117
  * The `relationships` block as published — every declared dependency, with the
64
118
  * build's own keys dropped.
@@ -109,20 +163,32 @@ export function buildManifest({ config, packageJson, artifact, flags }: {
109
163
  /**
110
164
  * Write the generated manifest into the staged package.
111
165
  *
166
+ * The declared `packFolders` is checked against the derived `packs[]` first,
167
+ * and an unresolvable name **stops the write**: a manifest already known to
168
+ * describe packs the package does not ship should not reach the stage, where
169
+ * the next command would deploy it (#81). See {@link packFolderFindings} for
170
+ * the rule and why its two findings carry different severities.
171
+ *
112
172
  * @param {object} options - As {@link buildManifest}, plus where to write.
113
173
  * @param {object} options.config - The resolved content configuration.
114
174
  * @param {object} options.packageJson - The repository's `package.json`.
115
175
  * @param {string} options.artifact - `system` or `module`.
116
176
  * @param {string} options.outDir - Directory to write into.
117
177
  * @param {Record<string, object>} [options.flags] - Namespaced flags to merge.
178
+ * @param {string} [options.configFile] - Absolute path of the configuration
179
+ * file the manifest was resolved from, so a finding about it can be located.
180
+ * Omitting it costs the position, not the finding.
118
181
  * @returns {Promise<{path: string, manifest: object}>} Where it went, and what.
182
+ * @throws {Error} When a `packFolders` entry names a pack the package does not
183
+ * ship. Nothing is written in that case.
119
184
  */
120
- export function writeManifest({ config, packageJson, artifact, outDir, flags, }: {
185
+ export function writeManifest({ config, packageJson, artifact, outDir, flags, configFile, }: {
121
186
  config: object;
122
187
  packageJson: object;
123
188
  artifact: string;
124
189
  outDir: string;
125
190
  flags?: Record<string, object> | undefined;
191
+ configFile?: string | undefined;
126
192
  }): Promise<{
127
193
  path: string;
128
194
  manifest: object;
@@ -54,11 +54,15 @@ export function rewriteRepoLinks(body: string, docRel: string, options: object):
54
54
  * Both run inside code-fence protection, so neither can rewrite a fenced example.
55
55
  *
56
56
  * @param {object} options - Resolved from `site.passOptions`.
57
- * @param {string} [options.symbolMap] - Path to the TypeDoc symbol map.
57
+ * @param {string} [options.symbolMap] - Path to the TypeDoc symbol map,
58
+ * relative to `repoRoot`. Absent means no API links; present and unusable is
59
+ * a build failure.
58
60
  * @param {string} [options.apiBase] - Where the API documentation is served.
59
61
  * @param {string} [options.blob] - GitHub blob base for repository files.
60
62
  * @param {string} options.repoRoot - The repository root, for relative paths.
61
63
  * @returns {{beforeLinks: Function, afterLinks: Function}} The bundle.
64
+ * @throws {Error} When a configured `symbolMap` cannot be resolved, read,
65
+ * parsed, or is not a name → page object.
62
66
  */
63
67
  export function sohlKbPass(options: {
64
68
  symbolMap?: string | undefined;