@heroiclands/package-build 22.27.0 → 22.28.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 +6 -0
- package/bin/content-commands.mjs +53 -5
- package/ci/publish-npm.mjs +49 -0
- package/docs/api.md +11 -11
- package/docs/authoring/first-note.md +1 -1
- package/docs/content-format.md +2 -0
- package/docs/reference/format-details.md +21 -11
- package/engine/folder-notes.mjs +2 -3
- package/engine/index.mjs +1 -1
- package/engine/note-vocabulary.mjs +3 -4
- package/engine/stub-lint.mjs +20 -64
- package/githooks/pre-push +1 -1
- package/package.json +3 -1
- package/types/engine/note-vocabulary.d.mts +2 -2
package/CHANGELOG.md
CHANGED
package/bin/content-commands.mjs
CHANGED
|
@@ -226,7 +226,12 @@ const SHIPPED_ITEM_FIELDS = { sohl: ITEM_FIELDS, hm3: HM3_ITEM_FIELDS };
|
|
|
226
226
|
/** Register the content operations on the package command line. */
|
|
227
227
|
export function registerContentCommands(cli) {
|
|
228
228
|
return cli
|
|
229
|
-
.command(
|
|
229
|
+
.command(
|
|
230
|
+
withIndexPreflight(
|
|
231
|
+
withBodyPreflight(packageCommand(), (_config, argv) => argv.action === "compile"),
|
|
232
|
+
(_config, argv) => argv.action === "compile",
|
|
233
|
+
),
|
|
234
|
+
)
|
|
230
235
|
.command(depsCommand())
|
|
231
236
|
.command(docsCommand())
|
|
232
237
|
.command(withIndexPreflight(lintCommand()))
|
|
@@ -235,11 +240,17 @@ export function registerContentCommands(cli) {
|
|
|
235
240
|
.command(withIndexPreflight(linksCommand()))
|
|
236
241
|
.command(formatCommand())
|
|
237
242
|
.command(markdownCommand())
|
|
238
|
-
.command(
|
|
239
|
-
|
|
243
|
+
.command(
|
|
244
|
+
withBodyPreflight(
|
|
245
|
+
contentIndexCommand(),
|
|
246
|
+
() => true,
|
|
247
|
+
(config, argv) => argv.root || config.paths.content,
|
|
248
|
+
),
|
|
249
|
+
)
|
|
250
|
+
.command(withIndexPreflight(withBodyPreflight(siteCommand()), publishesContentPages))
|
|
240
251
|
.command(
|
|
241
252
|
withIndexPreflight(
|
|
242
|
-
pdfCommand(),
|
|
253
|
+
withBodyPreflight(pdfCommand(), (config) => Boolean(config.pdf)),
|
|
243
254
|
(config) => config.pdf && publishesContentPages(config),
|
|
244
255
|
),
|
|
245
256
|
)
|
|
@@ -380,6 +391,43 @@ function proseCommand() {
|
|
|
380
391
|
};
|
|
381
392
|
}
|
|
382
393
|
|
|
394
|
+
/** Refuse invalid note bodies before emitting metadata, packs, pages or a book. */
|
|
395
|
+
function withBodyPreflight(
|
|
396
|
+
command,
|
|
397
|
+
shouldCheck = () => true,
|
|
398
|
+
contentRoot = (config) => config.paths.content,
|
|
399
|
+
) {
|
|
400
|
+
const handler = command.handler;
|
|
401
|
+
return {
|
|
402
|
+
...command,
|
|
403
|
+
handler: async (argv) => {
|
|
404
|
+
try {
|
|
405
|
+
const config = loadPackConfig();
|
|
406
|
+
if (shouldCheck(config, argv)) {
|
|
407
|
+
const contentBase = contentRoot(config, argv);
|
|
408
|
+
if (!fs.existsSync(contentBase)) return handler(argv);
|
|
409
|
+
const records = indexRecordsFor({ config, contentBase });
|
|
410
|
+
const { findings } = lintNoteStates(contentBase, {
|
|
411
|
+
records,
|
|
412
|
+
contentPackage: config.contentPackage,
|
|
413
|
+
});
|
|
414
|
+
const errors = findings.filter((finding) => finding.severity === "error");
|
|
415
|
+
if (errors.length) {
|
|
416
|
+
for (const finding of errors) emitDiagnostic(finding);
|
|
417
|
+
process.exitCode = 1;
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
} catch (err) {
|
|
422
|
+
reportFailure(err);
|
|
423
|
+
process.exitCode = 1;
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
return handler(argv);
|
|
427
|
+
},
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
|
|
383
431
|
/** Check declared dependency indexes before an index-consuming command does any work. */
|
|
384
432
|
function withIndexPreflight(command, shouldCheck = () => true) {
|
|
385
433
|
const handler = command.handler;
|
|
@@ -1167,7 +1215,7 @@ function lintCommand() {
|
|
|
1167
1215
|
skipDirectories: config.skipDirectories,
|
|
1168
1216
|
});
|
|
1169
1217
|
|
|
1170
|
-
// Whether every
|
|
1218
|
+
// Whether every note has a body, and how
|
|
1171
1219
|
// finished the tree is. The counts and the oldest drafts are
|
|
1172
1220
|
// prose rather than findings: a note nobody has finished is not
|
|
1173
1221
|
// wrong, and 464 warnings a reader cannot clear is how a report
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This file is part of the Song of Heroic Lands (SoHL) system for Foundry VTT.
|
|
3
|
+
* Copyright (c) 2024-2026 Tom Rodriguez ("Toasty") — <toasty@heroiclands.org>
|
|
4
|
+
*
|
|
5
|
+
* This work is licensed under the GNU General Public License v3.0 (GPLv3).
|
|
6
|
+
* You may copy, modify, and distribute it under the terms of that license.
|
|
7
|
+
*
|
|
8
|
+
* For full terms, see the LICENSE.md file in the project root or visit:
|
|
9
|
+
* https://www.gnu.org/licenses/gpl-3.0.html
|
|
10
|
+
*
|
|
11
|
+
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Publish this package to npm, with provenance wherever it can be generated.
|
|
16
|
+
*
|
|
17
|
+
* `publishConfig.provenance` is `true`, and npm refuses a publish with
|
|
18
|
+
* `Automatic provenance generation not supported for provider: null` anywhere
|
|
19
|
+
* that cannot mint a Sigstore identity. That is a CI runner holding an OIDC
|
|
20
|
+
* token: GitHub Actions with `id-token: write`, which exposes
|
|
21
|
+
* `ACTIONS_ID_TOKEN_REQUEST_URL`, or GitLab CI with `SIGSTORE_ID_TOKEN`.
|
|
22
|
+
* Everywhere else — a desk — this passes `--provenance=false`, so the publish
|
|
23
|
+
* goes ahead unattested rather than failing.
|
|
24
|
+
*
|
|
25
|
+
* Arguments after `--` reach `npm publish` unchanged, so
|
|
26
|
+
* `npm run deploy:npm -- --dry-run` shows the tarball without publishing it.
|
|
27
|
+
* `prepack` builds the declarations and the asset index on the way.
|
|
28
|
+
*
|
|
29
|
+
* Not a `package-build` subcommand: it publishes this package and no other.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import { spawnSync } from "node:child_process";
|
|
33
|
+
|
|
34
|
+
const env = process.env;
|
|
35
|
+
const provenance =
|
|
36
|
+
(env.GITHUB_ACTIONS === "true" && Boolean(env.ACTIONS_ID_TOKEN_REQUEST_URL)) ||
|
|
37
|
+
(env.GITLAB_CI === "true" && Boolean(env.SIGSTORE_ID_TOKEN));
|
|
38
|
+
|
|
39
|
+
if (!provenance) {
|
|
40
|
+
console.log("deploy:npm: no OIDC identity here; publishing without provenance.");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const args = ["publish", ...(provenance ? ["--provenance"] : ["--provenance=false"])];
|
|
44
|
+
const result = spawnSync("npm", [...args, ...process.argv.slice(2)], {
|
|
45
|
+
stdio: "inherit",
|
|
46
|
+
shell: process.platform === "win32",
|
|
47
|
+
});
|
|
48
|
+
if (result.error) throw result.error;
|
|
49
|
+
process.exit(result.status ?? 1);
|
package/docs/api.md
CHANGED
|
@@ -441,16 +441,16 @@ Drawing a declared infobox: HTML for separate Foundry pages appended after autho
|
|
|
441
441
|
|
|
442
442
|
The three states of a note — **stub**, **draft**, **full** — and the one observable fact they rest on. A note whose body is empty is a stub: it carries its facts into the index and is queryable there, and it has no page, no address and no link target. Nothing is stored: the ladder is derived once in the SQL `entries` view and no note can assert its own state.
|
|
443
443
|
|
|
444
|
-
| Export | Signature | Returns | Use it when
|
|
445
|
-
| --------------------- | ------------------------------------------- | --------------------------------------- |
|
|
446
|
-
| `NOTE_STATES` | `const NOTE_STATES` | `ReadonlyArray<string>` | naming the three states in ladder order
|
|
447
|
-
| `isEmptyBody` | `isEmptyBody(body)` | `boolean` | asking the exact question: is everything after the closing frontmatter fence whitespace?
|
|
448
|
-
| `isStubbableType` | `isStubbableType(type, vocabulary)` | `boolean` | asking whether an empty body suppresses this type's page; `folder` and `homepage`
|
|
449
|
-
| `isStubNote` | `isStubNote(frontmatter, body, vocabulary)` | `boolean` | deciding whether one note is a stub, which is what the index and the site build both ask
|
|
450
|
-
| `journalHasContent` | `journalHasContent(body)` | `boolean` | asking whether the JournalEntry a body compiles into would hold anything, which is what decides whether one is made at all
|
|
451
|
-
| `PLACEHOLDER_PHRASES` | `const PLACEHOLDER_PHRASES` | `ReadonlyArray<string>` | reading the phrases that stand in for prose nobody has written
|
|
452
|
-
| `placeholderBody` | `placeholderBody(body)` | `Array<{phrase: string, line: number}>` | finding a body that reduces to nothing but placeholders, which is an abandoned draft rather than a stub
|
|
453
|
-
| `bodyWordCount` | `bodyWordCount(body)` | `number` | counting rendered prose, with wikilinks as one word and closed dashes, slashes, and ellipses separating adjacent words
|
|
444
|
+
| Export | Signature | Returns | Use it when |
|
|
445
|
+
| --------------------- | ------------------------------------------- | --------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
446
|
+
| `NOTE_STATES` | `const NOTE_STATES` | `ReadonlyArray<string>` | naming the three states in ladder order |
|
|
447
|
+
| `isEmptyBody` | `isEmptyBody(body)` | `boolean` | asking the exact question: is everything after the closing frontmatter fence whitespace? |
|
|
448
|
+
| `isStubbableType` | `isStubbableType(type, vocabulary)` | `boolean` | asking whether an empty body suppresses this type's page; `folder` and `homepage` retain their addresses in this classification, but still require bodies |
|
|
449
|
+
| `isStubNote` | `isStubNote(frontmatter, body, vocabulary)` | `boolean` | deciding whether one note is a stub, which is what the index and the site build both ask |
|
|
450
|
+
| `journalHasContent` | `journalHasContent(body)` | `boolean` | asking whether the JournalEntry a body compiles into would hold anything, which is what decides whether one is made at all |
|
|
451
|
+
| `PLACEHOLDER_PHRASES` | `const PLACEHOLDER_PHRASES` | `ReadonlyArray<string>` | reading the phrases that stand in for prose nobody has written |
|
|
452
|
+
| `placeholderBody` | `placeholderBody(body)` | `Array<{phrase: string, line: number}>` | finding a body that reduces to nothing but placeholders, which is an abandoned draft rather than a stub |
|
|
453
|
+
| `bodyWordCount` | `bodyWordCount(body)` | `number` | counting rendered prose, with wikilinks as one word and closed dashes, slashes, and ellipses separating adjacent words |
|
|
454
454
|
|
|
455
455
|
### `engine.draftNotice`
|
|
456
456
|
|
|
@@ -471,7 +471,7 @@ What a draft note says about itself, in one sentence, on every surface it reache
|
|
|
471
471
|
|
|
472
472
|
### `engine.stubLint`
|
|
473
473
|
|
|
474
|
-
|
|
474
|
+
Every typed note must have a nonempty body. Empty and whitespace-only bodies are errors regardless of description or `draft` tag, including structural types such as `folder` and `homepage`. Placeholder-only bodies also produce errors. Nonempty bodies with fewer than 25 prose words produce warnings unless tagged `draft`. Untyped vault scaffolding is excluded. The lint, content-index, package compile, site and PDF commands enforce body errors before publishing. The state classification helpers still describe incomplete records for inspection; their classifications do not exempt notes from validation.
|
|
475
475
|
|
|
476
476
|
| Export | Signature | Returns | Use it when |
|
|
477
477
|
| ------------------ | ---------------------------------------------------------- | --------------------------------------------------- | --------------------------------------------------------------------------------------------- |
|
|
@@ -45,4 +45,4 @@ The full Address is `<package>-note-lore-harbor`; `<package>` comes from `conten
|
|
|
45
45
|
|
|
46
46
|
Add `description` for a short page summary, `tags: [draft]` to mark unfinished prose, or `data` for fields the note's type permits. A system block such as `sohl:` or `hm3:` adds mechanics and can produce a Foundry Actor or Item. It does not replace the shared prose or make another JournalEntry. [Frontmatter](frontmatter.md) explains the allowed keys, and the [note-type reference](../reference/note-types.md) lists every type-specific field.
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
Every typed note must have a nonempty body after its frontmatter. An empty or whitespace-only body is an error, even with `tags: [draft]` or a description. This includes folder and homepage notes. A nonempty body with fewer than 25 prose words produces a warning unless the note has a `draft` tag. See [note states](../reference/format-details.md#three-states-of-a-note).
|
package/docs/content-format.md
CHANGED
|
@@ -9,6 +9,8 @@ subType: userguide
|
|
|
9
9
|
|
|
10
10
|
A content package begins with Markdown notes under `assets/content/`. Each note has YAML frontmatter describing its identity and data, followed by prose. Folder names beneath `assets/content/` are for authors; `type`, `subType`, and Addresses determine what the build creates.
|
|
11
11
|
|
|
12
|
+
Every typed note requires a nonempty body, including drafts, folders and homepages. An empty or whitespace-only body is an error. A nonempty body with fewer than 25 prose words warns unless tagged `draft`; the tag never exempts an empty body.
|
|
13
|
+
|
|
12
14
|
Start with [your first note](authoring/first-note.md). It explains the smallest valid note and shows the JournalEntry, web page, book entry, and content-index record made from it. Then use the chapters below in the order your note needs them.
|
|
13
15
|
|
|
14
16
|
| Need | Read |
|
|
@@ -2006,8 +2006,18 @@ convention rather than declared in a field — see
|
|
|
2006
2006
|
|
|
2007
2007
|
### Three states of a note
|
|
2008
2008
|
|
|
2009
|
-
|
|
2010
|
-
|
|
2009
|
+
Every typed note requires a nonempty body after its closing frontmatter fence.
|
|
2010
|
+
An empty or whitespace-only body is an error, regardless of description or
|
|
2011
|
+
`tags: [draft]`. This includes `folder` and `homepage` notes. Untyped vault
|
|
2012
|
+
scaffolding is excluded. A nonempty body with fewer than 25 prose words warns
|
|
2013
|
+
unless tagged `draft`; the tag never exempts an empty body.
|
|
2014
|
+
|
|
2015
|
+
The `lint`, `content-index`, `package compile`, `site` and `pdf` commands enforce
|
|
2016
|
+
body errors. Write a short beginning and tag it `draft` when work is unfinished.
|
|
2017
|
+
|
|
2018
|
+
The internal classification below describes records, including incomplete notes
|
|
2019
|
+
that inspection APIs can still read. A **stub** is invalid authored content and
|
|
2020
|
+
must receive a body before these commands can publish it.
|
|
2011
2021
|
|
|
2012
2022
|
| State | Body | Marker | Page | Document | Link target |
|
|
2013
2023
|
| --------- | ------- | --------------- | ------------ | --------------------------------- | ----------- |
|
|
@@ -2039,13 +2049,13 @@ same.
|
|
|
2039
2049
|
|
|
2040
2050
|
| After the fence | Empty? | What happens |
|
|
2041
2051
|
| ----------------------- | ------ | --------------------------------------------------------------- |
|
|
2042
|
-
| Nothing, or blank lines | yes |
|
|
2052
|
+
| Nothing, or blank lines | yes | An error, with or without `draft` |
|
|
2043
2053
|
| An HTML comment | no | A note with a body that renders to nothing. The lint reports it |
|
|
2044
2054
|
| A horizontal rule | no | The same |
|
|
2045
2055
|
| A lone heading | no | The same |
|
|
2046
2056
|
| `_To be written._` | no | The same |
|
|
2047
2057
|
|
|
2048
|
-
An HTML comment is deliberately not empty
|
|
2058
|
+
An HTML comment is deliberately not empty, but an invisible marker does not supply prose. Replace placeholder-only content with written text.
|
|
2049
2059
|
|
|
2050
2060
|
#### What a stub emits
|
|
2051
2061
|
|
|
@@ -2090,12 +2100,12 @@ What is suppressed with the page is the web address and the link target: a
|
|
|
2090
2100
|
wikilink into a stub is refused, and a table cell listing one prints its name as
|
|
2091
2101
|
plain text rather than as a link.
|
|
2092
2102
|
|
|
2093
|
-
####
|
|
2103
|
+
#### Structural classification does not exempt validation
|
|
2094
2104
|
|
|
2095
|
-
`folder`
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2105
|
+
A `folder` note's page is a generated section index, and the homepage is the
|
|
2106
|
+
site's front door. The classification helpers retain their addresses and report
|
|
2107
|
+
**full** even when their bodies are empty. Validation still rejects an empty
|
|
2108
|
+
body for both types.
|
|
2099
2109
|
|
|
2100
2110
|
Every type in the vocabulary declares whether an empty body suppresses its page,
|
|
2101
2111
|
so adding a type means answering the question rather than inheriting a default.
|
|
@@ -4735,8 +4745,8 @@ the map for the pack it is writing and falls back to `default` when there is no
|
|
|
4735
4745
|
such key, so a mistyped `journal:` files the folder wherever the default puts it
|
|
4736
4746
|
— exactly the hierarchy the key was written to override, and silently.
|
|
4737
4747
|
|
|
4738
|
-
**A folder note
|
|
4739
|
-
|
|
4748
|
+
**A folder note requires a body**, like every typed note. Its body does not
|
|
4749
|
+
produce a documentation journal or take part in `docEntryTypes`.
|
|
4740
4750
|
In a compiled Foundry journal, a wikilink to a folder prints its label as text:
|
|
4741
4751
|
the folder can materialise in several packs and has no single compendium UUID.
|
|
4742
4752
|
|
package/engine/folder-notes.mjs
CHANGED
|
@@ -42,9 +42,8 @@
|
|
|
42
42
|
* because a `Folder` is a core Foundry document like a `JournalEntry` or a
|
|
43
43
|
* `Scene`, not a system's.
|
|
44
44
|
*
|
|
45
|
-
* **A folder note
|
|
46
|
-
* no documentation journal and takes no part in `docEntryTypes
|
|
47
|
-
* open decision, settled the way it recommended.
|
|
45
|
+
* **A folder note requires a body** under author validation. That body creates
|
|
46
|
+
* no documentation journal and takes no part in `docEntryTypes`.
|
|
48
47
|
*
|
|
49
48
|
* @module
|
|
50
49
|
*/
|
package/engine/index.mjs
CHANGED
|
@@ -93,7 +93,7 @@ export * as noteState from "./note-state.mjs";
|
|
|
93
93
|
/** What a draft note says about itself, in one sentence, on every surface. */
|
|
94
94
|
export * as draftNotice from "./draft-notice.mjs";
|
|
95
95
|
|
|
96
|
-
/**
|
|
96
|
+
/** Require written bodies on typed notes and report how finished a tree is. */
|
|
97
97
|
export * as stubLint from "./stub-lint.mjs";
|
|
98
98
|
|
|
99
99
|
/** What a place is next to and reachable from: the closed sets, and the checks. */
|
|
@@ -197,8 +197,8 @@ import { positionOfFrontmatterPath } from "./diagnostics.mjs";
|
|
|
197
197
|
*
|
|
198
198
|
* Two types answer **false**, and both are structural: a `folder` note's page
|
|
199
199
|
* is a generated section index and a `homepage`'s is the site's front door, so
|
|
200
|
-
*
|
|
201
|
-
*
|
|
200
|
+
* an empty body on one of them keeps its address and resolves as `full`.
|
|
201
|
+
* This is classification only: author validation requires a body for every type.
|
|
202
202
|
*
|
|
203
203
|
* @typedef {object} TypeVocabulary
|
|
204
204
|
* @property {readonly DataFieldSpec[]} data - The `data:` keys, closed.
|
|
@@ -1413,8 +1413,7 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
1413
1413
|
// document, so its address carries the `none` system segment and
|
|
1414
1414
|
// everything it says is a `data` property.
|
|
1415
1415
|
//
|
|
1416
|
-
//
|
|
1417
|
-
// is structure, not content, so it wants no documentation journal and takes
|
|
1416
|
+
// Its required body produces no documentation journal and takes
|
|
1418
1417
|
// no part in `docEntryTypes`.
|
|
1419
1418
|
folder: Object.freeze({
|
|
1420
1419
|
stubbable: false,
|
package/engine/stub-lint.mjs
CHANGED
|
@@ -12,34 +12,16 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
|
-
*
|
|
15
|
+
* Every typed note needs a nonempty body.
|
|
16
16
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
17
|
+
* A missing or whitespace-only body is an error, whether or not the note has
|
|
18
|
+
* a `draft` tag or a frontmatter description. Placeholder-only bodies remain
|
|
19
|
+
* errors. A nonempty body below twenty-five words receives a warning when it
|
|
20
|
+
* is not tagged `draft`; the author decides whether that short note is done.
|
|
20
21
|
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* 2. **A stub is not tagged `draft`.** A thing not started is not a thing in
|
|
25
|
-
* progress. An **error**.
|
|
26
|
-
* 3. **A body that renders to nothing is an abandoned draft, not a stub.** This
|
|
27
|
-
* is what keeps the empty-body rule's severity honest: the softer cases are
|
|
28
|
-
* *reported* rather than silently treated as one state or the other. An
|
|
29
|
-
* **error**.
|
|
30
|
-
* 4. **A short unmarked body may be a draft that forgot its marker.** A
|
|
31
|
-
* **warning**, never an error — a note reading `See [[affiliation-meivor|Mëivōr]]`
|
|
32
|
-
* says everything it has to say, and a corpus holds many of them.
|
|
33
|
-
* 5. **A long-standing draft is a report, not a finding.** It is unfinished,
|
|
34
|
-
* which the tag already says, so the oldest are listed and nobody is asked
|
|
35
|
-
* to dismiss a warning they cannot clear.
|
|
36
|
-
* 6. **The counts are reported**, per package and per type, so the ratio of
|
|
37
|
-
* written to unwritten is visible on every run rather than discovered in a
|
|
38
|
-
* year.
|
|
39
|
-
*
|
|
40
|
-
* **What this does not decide is what earns a body.** Whether a given village
|
|
41
|
-
* deserves prose is a question about a setting, and it belongs in the
|
|
42
|
-
* repository that ships it. The toolchain's job is to make promotion free.
|
|
22
|
+
* Counts and the oldest drafts remain reports. Classification and indexing
|
|
23
|
+
* still derive full, draft and stub states from the existing note records;
|
|
24
|
+
* validation reports missing content without changing those records.
|
|
43
25
|
*
|
|
44
26
|
* @module
|
|
45
27
|
*/
|
|
@@ -50,7 +32,7 @@ import path from "node:path";
|
|
|
50
32
|
import { authoredFrontmatter, isNoteRecord, isStub, noteFile } from "./index-records.mjs";
|
|
51
33
|
import { positionInFrontmatter, positionOfLiteral } from "./diagnostics.mjs";
|
|
52
34
|
import { parseMarkdownFile } from "./helpers.mjs";
|
|
53
|
-
import { bodyWordCount,
|
|
35
|
+
import { bodyWordCount, placeholderBody } from "./note-state.mjs";
|
|
54
36
|
import { isDraftNote } from "./note-vocabulary.mjs";
|
|
55
37
|
|
|
56
38
|
/**
|
|
@@ -67,16 +49,6 @@ export const SHORT_BODY_WORDS = 25;
|
|
|
67
49
|
/** How many of the oldest drafts the report names. */
|
|
68
50
|
const OLDEST_DRAFTS = 10;
|
|
69
51
|
|
|
70
|
-
/**
|
|
71
|
-
* Whether a note states a description.
|
|
72
|
-
*
|
|
73
|
-
* @param {Record<string, any>} fm - The note's authored frontmatter.
|
|
74
|
-
* @returns {boolean} Whether `description` carries a sentence.
|
|
75
|
-
*/
|
|
76
|
-
function hasDescription(fm) {
|
|
77
|
-
return String(fm?.description ?? "").trim() !== "";
|
|
78
|
-
}
|
|
79
|
-
|
|
80
52
|
/**
|
|
81
53
|
* Check one note's state, and say where the fault is.
|
|
82
54
|
*
|
|
@@ -87,29 +59,14 @@ function checkNote({ fm, body, file, raw }) {
|
|
|
87
59
|
const findings = [];
|
|
88
60
|
const at = (key, value) => positionInFrontmatter(raw, key, value, { topLevel: true });
|
|
89
61
|
|
|
90
|
-
if (
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
"a `description`, which is the only sentence a reader gets " +
|
|
99
|
-
"and what every table listing it prints",
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
if (isDraftNote(fm)) {
|
|
103
|
-
findings.push({
|
|
104
|
-
file,
|
|
105
|
-
...at("type", fm.type),
|
|
106
|
-
severity: "error",
|
|
107
|
-
message:
|
|
108
|
-
"this note has no body and is tagged `draft`, and a thing " +
|
|
109
|
-
"not started is not a thing in progress — remove the tag, " +
|
|
110
|
-
"or write a body",
|
|
111
|
-
});
|
|
112
|
-
}
|
|
62
|
+
if (String(body ?? "").trim() === "") {
|
|
63
|
+
findings.push({
|
|
64
|
+
file,
|
|
65
|
+
...at("type", fm.type),
|
|
66
|
+
severity: "error",
|
|
67
|
+
message:
|
|
68
|
+
"this note has no body — write a nonempty body, even when it is tagged `draft`",
|
|
69
|
+
});
|
|
113
70
|
return findings;
|
|
114
71
|
}
|
|
115
72
|
|
|
@@ -122,8 +79,7 @@ function checkNote({ fm, body, file, raw }) {
|
|
|
122
79
|
severity: "error",
|
|
123
80
|
message:
|
|
124
81
|
`this note's body says only "${phrase}", so it publishes a page ` +
|
|
125
|
-
`that tells a reader nothing —
|
|
126
|
-
`stub, or write it`,
|
|
82
|
+
`that tells a reader nothing — replace the placeholder with content`,
|
|
127
83
|
});
|
|
128
84
|
return findings;
|
|
129
85
|
}
|
|
@@ -204,8 +160,8 @@ export function lintNoteStates(contentBase, { records, contentPackage } = {}) {
|
|
|
204
160
|
if (state === "draft") drafts.push({ file, modified: fs.statSync(absPath).mtime });
|
|
205
161
|
|
|
206
162
|
// Held to the rules only where there is a note to hold: a file that
|
|
207
|
-
// declares no type declares nothing for them to read
|
|
208
|
-
//
|
|
163
|
+
// declares no type declares nothing for them to read; requiring its
|
|
164
|
+
// body would report vault scaffolding as content.
|
|
209
165
|
if (record.type) findings.push(...checkNote({ fm, body, file, raw }));
|
|
210
166
|
}
|
|
211
167
|
|
package/githooks/pre-push
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
# container, over a clean export of HEAD — so a red GitHub check is something
|
|
5
5
|
# you chose rather than something you discover.
|
|
6
6
|
#
|
|
7
|
-
# The steps are not listed here. `
|
|
7
|
+
# The steps are not listed here. `ci/ci-steps.mjs` reads them out of
|
|
8
8
|
# `.github/workflows/build.yml`, because a hook holding its own copy of the
|
|
9
9
|
# command list is a second statement of one thing, and the copy is the one that
|
|
10
10
|
# goes stale — silently, since running four of five steps still exits 0.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@heroiclands/package-build",
|
|
3
|
-
"version": "22.
|
|
3
|
+
"version": "22.28.0",
|
|
4
4
|
"description": "Shared toolchain for building and shipping a HeroicLands Foundry VTT package — content compilation, manifest, localization, staging, bundle, release and deployment.",
|
|
5
5
|
"license": "GPL-3.0-or-later",
|
|
6
6
|
"type": "module",
|
|
@@ -182,6 +182,8 @@
|
|
|
182
182
|
"scripts": {
|
|
183
183
|
"test": "vitest run",
|
|
184
184
|
"test:watch": "vitest",
|
|
185
|
+
"build": "node bin/package-build.mjs ci",
|
|
186
|
+
"deploy:npm": "node ci/publish-npm.mjs",
|
|
185
187
|
"prepack": "run-s build:types build:asset-index",
|
|
186
188
|
"build:types": "tsc -p tsconfig.dts.json",
|
|
187
189
|
"build:asset-index": "node ci/emit-asset-index.mjs",
|
|
@@ -455,8 +455,8 @@ export type DataFieldSpec = {
|
|
|
455
455
|
*
|
|
456
456
|
* Two types answer **false**, and both are structural: a `folder` note's page
|
|
457
457
|
* is a generated section index and a `homepage`'s is the site's front door, so
|
|
458
|
-
*
|
|
459
|
-
*
|
|
458
|
+
* an empty body on one of them keeps its address and resolves as `full`.
|
|
459
|
+
* This is classification only: author validation requires a body for every type.
|
|
460
460
|
*/
|
|
461
461
|
export type TypeVocabulary = {
|
|
462
462
|
/**
|