@heroiclands/package-build 22.4.7 → 22.5.1
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 +34 -0
- package/CONTENT.md +89 -1
- package/bin/content-build.mjs +16 -2
- package/docs/api.md +50 -12
- package/docs/content-format.md +192 -0
- package/docs/diagnostics.md +44 -0
- package/engine/content-index.mjs +75 -24
- package/engine/content-links.mjs +60 -6
- package/engine/content-tables.mjs +29 -2
- package/engine/foundry-entries.mjs +32 -16
- package/engine/holdings.mjs +42 -17
- package/engine/index-records.mjs +25 -0
- package/engine/index.mjs +7 -0
- package/engine/journals.mjs +13 -5
- package/engine/note-state.mjs +242 -0
- package/engine/note-vocabulary.mjs +40 -2
- package/engine/pdf-build.mjs +4 -2
- package/engine/populations.mjs +557 -0
- package/engine/site-build.mjs +8 -0
- package/engine/sql-tables.mjs +157 -4
- package/engine/stub-lint.mjs +237 -0
- package/engine/wikilink-syntax.mjs +27 -1
- package/package.json +1 -1
- package/types/engine/content-index.d.mts +20 -5
- package/types/engine/content-links.d.mts +3 -3
- package/types/engine/content-tables.d.mts +2 -1
- package/types/engine/holdings.d.mts +17 -6
- package/types/engine/index-records.d.mts +22 -0
- package/types/engine/index.d.mts +3 -0
- package/types/engine/note-state.d.mts +136 -0
- package/types/engine/note-vocabulary.d.mts +15 -0
- package/types/engine/populations.d.mts +121 -0
- package/types/engine/stub-lint.d.mts +51 -0
- package/types/engine/wikilink-syntax.d.mts +19 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,39 @@
|
|
|
1
1
|
# @heroiclands/package-build
|
|
2
2
|
|
|
3
|
+
## 22.5.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
**Unwritten notes**
|
|
8
|
+
|
|
9
|
+
- A note left unwritten builds: an empty body no longer stops a package
|
|
10
|
+
compiling its packs.
|
|
11
|
+
- An unwritten note offers no compendium link, so nothing sends a reader to a
|
|
12
|
+
journal that was never made.
|
|
13
|
+
|
|
14
|
+
## 22.5.0
|
|
15
|
+
|
|
16
|
+
### Minor Changes
|
|
17
|
+
|
|
18
|
+
**Three states of a note** — a note with no body is now a **stub**: it carries
|
|
19
|
+
|
|
20
|
+
- A page's settlements, deities or characters can be listed with the unwritten
|
|
21
|
+
ones beside the written, and counted by how finished they are — select
|
|
22
|
+
`FROM entries` rather than `FROM notes`, which leaves stubs out as it always
|
|
23
|
+
has.
|
|
24
|
+
- A folder note and the homepage are complete with no body, and keep their pages.
|
|
25
|
+
- An unwritten note still compiles the compendium document its facts describe.
|
|
26
|
+
- `content-build lint` asks a stub for a description, refuses one tagged
|
|
27
|
+
`draft`, and reports a body that says only "TBD".
|
|
28
|
+
- Every build now prints how many of a package's notes are full, draft and stub.
|
|
29
|
+
|
|
30
|
+
### Patch Changes
|
|
31
|
+
|
|
32
|
+
**Population figures are checked against each other.** A region whose polities
|
|
33
|
+
or inner regions count more people than it states, a settlement larger than the
|
|
34
|
+
land around it, and a reference page carrying a figure the place's own note
|
|
35
|
+
contradicts are each reported while the note is open.
|
|
36
|
+
|
|
3
37
|
## 22.4.7
|
|
4
38
|
|
|
5
39
|
### Patch Changes
|
package/CONTENT.md
CHANGED
|
@@ -1093,6 +1093,66 @@ it was addressed by the package rather than by a slug. It carries an address
|
|
|
1093
1093
|
like every other note now; the guard is unchanged, because what it reads
|
|
1094
1094
|
was never the key count.)
|
|
1095
1095
|
|
|
1096
|
+
### An empty body is a stub, on purpose
|
|
1097
|
+
|
|
1098
|
+
A note whose body is empty is a **stub**: it carries its frontmatter into the
|
|
1099
|
+
content index and is queryable there, it keeps its `id`, and it publishes no
|
|
1100
|
+
page, no address and no link target. A note with a body and the `draft` tag is a
|
|
1101
|
+
**draft**, and one with a body and no tag is **full**.
|
|
1102
|
+
`docs/content-format.md` states the rule; this is what the lint does with it.
|
|
1103
|
+
|
|
1104
|
+
**A document that would be empty is not created**, and the index then names no
|
|
1105
|
+
artefact — `uuid`, `path` and `address` are `null`. An unwritten note still
|
|
1106
|
+
compiles every document derived from `data:`, so an arcane talent is still its
|
|
1107
|
+
Item; what it compiles no journal for is its prose, because there is none. An
|
|
1108
|
+
infobox does not count as content: the panel renders `data:` the index already
|
|
1109
|
+
carries.
|
|
1110
|
+
|
|
1111
|
+
Four findings and two reports:
|
|
1112
|
+
|
|
1113
|
+
- **A stub carries a `description`.** With nothing in the body it is the only
|
|
1114
|
+
sentence a reader gets, and it is what every table listing the note prints. An
|
|
1115
|
+
**error**, at the note's `type:` line.
|
|
1116
|
+
- **A stub is not tagged `draft`.** A thing not started is not a thing in
|
|
1117
|
+
progress. An **error**: remove the tag, or write a body.
|
|
1118
|
+
- **A body that reduces to nothing is an abandoned draft, not a stub.** A body
|
|
1119
|
+
that is only headings, rules, comments and placeholder phrases — `TBD`, `To be
|
|
1120
|
+
written`, `Coming soon`, `Placeholder`, `N/A` — publishes a page that tells a
|
|
1121
|
+
reader nothing. An **error** naming the phrase and where it sits: empty the
|
|
1122
|
+
body to make it a stub, or write it.
|
|
1123
|
+
- **A short unmarked body may be a draft that forgot its marker.** Under
|
|
1124
|
+
twenty-five words with no `draft` tag is a **warning, never an error**. A note
|
|
1125
|
+
reading `See [[affiliation-meivor|Mëivōr]]` says everything it has to say, and
|
|
1126
|
+
a corpus holds many of them.
|
|
1127
|
+
|
|
1128
|
+
And two things the rules cannot decide, so they are reported as prose rather
|
|
1129
|
+
than flagged:
|
|
1130
|
+
|
|
1131
|
+
- **The oldest drafts are listed** by last modification. A long-standing draft
|
|
1132
|
+
is unfinished, which the tag already says; a warning nobody can clear is how a
|
|
1133
|
+
report teaches its reader to stop looking at it.
|
|
1134
|
+
- **The counts are reported**, per package and per type:
|
|
1135
|
+
|
|
1136
|
+
```text
|
|
1137
|
+
thalorna: 2572 notes — 2089 full, 464 draft, 19 stub
|
|
1138
|
+
```
|
|
1139
|
+
|
|
1140
|
+
`content-build content-index` prints the same line, so the ratio of written to
|
|
1141
|
+
unwritten is visible on every build rather than discovered in a year.
|
|
1142
|
+
|
|
1143
|
+
#### Adopting it
|
|
1144
|
+
|
|
1145
|
+
A package with no empty-bodied note is unaffected. A package that has them:
|
|
1146
|
+
|
|
1147
|
+
1. Take the release and run `content-build lint`. Act on the stub findings — a
|
|
1148
|
+
`description` on every stub, and no stub tagged `draft`.
|
|
1149
|
+
2. Change the `sql` fences that **should** see stubs from `FROM notes` to
|
|
1150
|
+
`FROM entries`. The ones that would gain rows are warned about at the fence,
|
|
1151
|
+
so the list comes to you.
|
|
1152
|
+
3. Read the site's page count. A stub publishes no page, so a tree with stubs
|
|
1153
|
+
publishes fewer pages than it did — which is the point, and worth confirming
|
|
1154
|
+
against any `min-pages` gate the deploy declares.
|
|
1155
|
+
|
|
1096
1156
|
### Exactly one homepage
|
|
1097
1157
|
|
|
1098
1158
|
A content tree declares **exactly one** `type: homepage` note. Zero is an
|
|
@@ -1190,6 +1250,26 @@ is a warning, `unheld land`, at the note's `type:` line, so a gap in tenure
|
|
|
1190
1250
|
shows. A region is held through its polity's `domains` and a feature by
|
|
1191
1251
|
nobody, so neither is checked.
|
|
1192
1252
|
|
|
1253
|
+
**Population figures agree from the region down.** A place states
|
|
1254
|
+
`data.population` and an affiliation states how many people it counts, and four
|
|
1255
|
+
warnings read those figures against each other. The polities whose `domains`
|
|
1256
|
+
name a place must not count more people than the place states — `over-held
|
|
1257
|
+
land` — with a polity subordinate to another holding the same place counted
|
|
1258
|
+
once and a settlement exempt, its holder's figure counting a hinterland the
|
|
1259
|
+
settlement does not. The regions whose `parents` name a place must not count
|
|
1260
|
+
more than it states either — `over-full region` — a region naming both its
|
|
1261
|
+
parent and its grandparent counting under the nearer. A settlement must not
|
|
1262
|
+
hold more than any place containing it states, which is `oversized settlement`.
|
|
1263
|
+
And a `doc` note carrying a figure beside a wikilink to the place or
|
|
1264
|
+
affiliation it belongs to must state that note's figure, which is `disputed
|
|
1265
|
+
figure`, located at the number it wrote. Figures are authored to two
|
|
1266
|
+
significant digits, so a parent rounded up to five per cent below what it
|
|
1267
|
+
contains is not a finding, and a place or affiliation stating no figure is
|
|
1268
|
+
neither compared nor counted. A fetched index carries no figure, so a
|
|
1269
|
+
dependency's people are counted nowhere. There is deliberately **no urban-share
|
|
1270
|
+
rule**: a share computed from the named settlements against a region's total
|
|
1271
|
+
measures how much of the region has been written, not how much of it is urban.
|
|
1272
|
+
|
|
1193
1273
|
**A settlement's market class is one of six.** `data.market` states what trade
|
|
1194
1274
|
a settlement supports — `1` a hamlet, with no market beyond what neighbours
|
|
1195
1275
|
trade among themselves; `2` a village and its weekly market; `3` a town with
|
|
@@ -2054,7 +2134,15 @@ the build fails naming both.
|
|
|
2054
2134
|
### Every note's address, and every anchor it defines
|
|
2055
2135
|
|
|
2056
2136
|
A record states the address a wikilink writes to reach the note, and every
|
|
2057
|
-
`{#slug}` anchor its body declares
|
|
2137
|
+
`{#slug}` anchor its body declares — unless the note is a **stub**, which
|
|
2138
|
+
carries both as `null`: it publishes no page, so it holds no address and offers
|
|
2139
|
+
no anchor to reach. Everything else it declares is kept, its `id` and `foundry`
|
|
2140
|
+
block included, because a document is derived from `data:` rather than from
|
|
2141
|
+
prose.
|
|
2142
|
+
|
|
2143
|
+
The SQL views read that absence as the state: `entries` is every row plus a
|
|
2144
|
+
derived `state` column, and `notes` is `entries` without the stubs. Nothing
|
|
2145
|
+
about the state is stored, and no note may author it.
|
|
2058
2146
|
|
|
2059
2147
|
```json
|
|
2060
2148
|
{
|
package/bin/content-build.mjs
CHANGED
|
@@ -83,6 +83,7 @@ import {
|
|
|
83
83
|
import { fetchNavigation, generateHugoConfig, writeHugoConfig } from "../engine/site-config.mjs";
|
|
84
84
|
import { renderItemFieldReference, renderItemFieldsPage } from "../engine/field-reference.mjs";
|
|
85
85
|
import { lintContentTree } from "../engine/content-lint.mjs";
|
|
86
|
+
import { lintNoteStates } from "../engine/stub-lint.mjs";
|
|
86
87
|
import { lintContentCharset } from "../engine/content-charset.mjs";
|
|
87
88
|
import { lintContentHtml } from "../engine/content-html.mjs";
|
|
88
89
|
import { lintContentIcons } from "../engine/content-icons.mjs";
|
|
@@ -1004,6 +1005,17 @@ function lintCommand() {
|
|
|
1004
1005
|
config,
|
|
1005
1006
|
});
|
|
1006
1007
|
|
|
1008
|
+
// Whether every empty body is a stub on purpose, and how
|
|
1009
|
+
// finished the tree is. The counts and the oldest drafts are
|
|
1010
|
+
// prose rather than findings: a note nobody has finished is not
|
|
1011
|
+
// wrong, and 464 warnings a reader cannot clear is how a report
|
|
1012
|
+
// teaches its reader to stop looking at it.
|
|
1013
|
+
const states = lintNoteStates(root, {
|
|
1014
|
+
records,
|
|
1015
|
+
contentPackage: config.contentPackage,
|
|
1016
|
+
});
|
|
1017
|
+
for (const line of states.summary) log.info(line);
|
|
1018
|
+
|
|
1007
1019
|
const findings = [
|
|
1008
1020
|
...addresses.findings,
|
|
1009
1021
|
...frontmatter.findings,
|
|
@@ -1012,6 +1024,7 @@ function lintCommand() {
|
|
|
1012
1024
|
...icons.findings,
|
|
1013
1025
|
...html.findings,
|
|
1014
1026
|
...images.findings,
|
|
1027
|
+
...states.findings,
|
|
1015
1028
|
];
|
|
1016
1029
|
// Only an **error** fails the run. Every finding was an error
|
|
1017
1030
|
// by then, so this changes nothing on its own —
|
|
@@ -1404,14 +1417,15 @@ function contentIndexCommand() {
|
|
|
1404
1417
|
handler: (argv) => {
|
|
1405
1418
|
try {
|
|
1406
1419
|
const config = loadPackConfig();
|
|
1407
|
-
const { file, notes, bytes } = emitContentIndex({
|
|
1420
|
+
const { file, notes, bytes, full, draft, stub } = emitContentIndex({
|
|
1408
1421
|
config,
|
|
1409
1422
|
...(argv.root ? { contentBase: argv.root } : {}),
|
|
1410
1423
|
...(argv.out ? { outDir: argv.out } : {}),
|
|
1411
1424
|
});
|
|
1412
1425
|
log.info(
|
|
1413
1426
|
`${config.contentPackage} → ${path.relative(process.cwd(), file)} ` +
|
|
1414
|
-
`(${notes} notes
|
|
1427
|
+
`(${notes} notes — ${full} full, ${draft} draft, ` +
|
|
1428
|
+
`${stub} stub — ${Math.round(bytes / 1024)} KiB)`,
|
|
1415
1429
|
);
|
|
1416
1430
|
} catch (err) {
|
|
1417
1431
|
reportFailure(err);
|
package/docs/api.md
CHANGED
|
@@ -412,6 +412,30 @@ Drawing a declared infobox: HTML for a Foundry Journal Page, Typst for the book.
|
|
|
412
412
|
| `linkToUuid` | `linkToUuid(value)` | `string` | drawing a link value as a Foundry document reference |
|
|
413
413
|
| `sectionHasContent` | `sectionHasContent(section)` | `boolean` | deciding whether a section holds anything worth drawing |
|
|
414
414
|
|
|
415
|
+
### `engine.noteState`
|
|
416
|
+
|
|
417
|
+
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.
|
|
418
|
+
|
|
419
|
+
| Export | Signature | Returns | Use it when |
|
|
420
|
+
| --------------------- | ------------------------------------------- | --------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
|
|
421
|
+
| `NOTE_STATES` | `const NOTE_STATES` | `ReadonlyArray<string>` | naming the three states in ladder order |
|
|
422
|
+
| `isEmptyBody` | `isEmptyBody(body)` | `boolean` | asking the exact question: is everything after the closing frontmatter fence whitespace? |
|
|
423
|
+
| `isStubbableType` | `isStubbableType(type, vocabulary)` | `boolean` | asking whether an empty body suppresses this type's page; `folder` and `homepage` are structural and exempt |
|
|
424
|
+
| `isStubNote` | `isStubNote(frontmatter, body, vocabulary)` | `boolean` | deciding whether one note is a stub, which is what the index and the site build both ask |
|
|
425
|
+
| `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 |
|
|
426
|
+
| `PLACEHOLDER_PHRASES` | `const PLACEHOLDER_PHRASES` | `ReadonlyArray<string>` | reading the phrases that stand in for prose nobody has written |
|
|
427
|
+
| `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 |
|
|
428
|
+
| `bodyWordCount` | `bodyWordCount(body)` | `number` | counting the prose a reader meets, with a wikilink counting as the one word it renders |
|
|
429
|
+
|
|
430
|
+
### `engine.stubLint`
|
|
431
|
+
|
|
432
|
+
An empty body must be deliberate. Four rules hold the state honest — a stub states what it is, a stub is not tagged `draft`, a body that renders to nothing is an abandoned draft, and a short unmarked body is asked whether it forgot its marker — and two reports say what the rules cannot decide.
|
|
433
|
+
|
|
434
|
+
| Export | Signature | Returns | Use it when |
|
|
435
|
+
| ------------------ | ---------------------------------------------------------- | --------------------------------------------------- | --------------------------------------------------------------------------------------------- |
|
|
436
|
+
| `SHORT_BODY_WORDS` | `const SHORT_BODY_WORDS` | `number` | reading the word count below which an unmarked body is asked whether it is a draft |
|
|
437
|
+
| `lintNoteStates` | `lintNoteStates(contentBase, { records, contentPackage })` | `{findings, counts, byType, oldestDrafts, summary}` | checking a tree's states and counting the ladder, from the corpus the command already derived |
|
|
438
|
+
|
|
415
439
|
### `engine.placeRelations`
|
|
416
440
|
|
|
417
441
|
What a place is next to (`data.borders`) and reachable from (`data.routes`): the closed sets both draw from, stated once for the lint, the specification and a consumer drawing a map, and the checks that hold a note to them and to the note at the other end.
|
|
@@ -440,8 +464,20 @@ What lies within a place, who holds it, and what an affiliation holds — read o
|
|
|
440
464
|
| `holdingsNode` | `holdingsNode(fm, { title, url })` | `HoldingsNode\|null` | reading a local note as a node of the derivation, or `null` for one that is neither a place nor an affiliation |
|
|
441
465
|
| `foreignHoldingsNodes` | `foreignHoldingsNodes(foreignIndex)` | `HoldingsNode[]` | reading a fetched index's places and affiliations as nodes |
|
|
442
466
|
| `holdingsPages` | `holdingsPages(nodes)` | `Map<string, Holdings>` | inverting `parents` and `domains` into each page's sorted lists, keyed by URL, for every page with at least one entry |
|
|
467
|
+
| `shortcodesOf` | `shortcodesOf(value)` | `string[]` | the shortcodes a `parents` or `domains` value names — bare, addressed or wikilinked — empties dropped and repeats collapsed |
|
|
443
468
|
| `checkHeld` | `checkHeld(note, { index })` | `object[]` | linting a place's tenure — a settlement, site or structure no affiliation's `domains` names is a warning at its `type:` line |
|
|
444
469
|
|
|
470
|
+
### `engine.populations`
|
|
471
|
+
|
|
472
|
+
Whether a world's population figures agree from the region down — the polities and regions inside a place against what it states, a settlement against every place containing it, and a `doc` note's cited figures against the notes that own them. `content-build lint` runs all four as warnings.
|
|
473
|
+
|
|
474
|
+
| Export | Signature | Returns | Use it when |
|
|
475
|
+
| ----------------------- | ---------------------------------------- | ------------------- | ------------------------------------------------------------------------------------------------------------ |
|
|
476
|
+
| `POPULATION_TOLERANCE` | `const POPULATION_TOLERANCE` | `number` | how far a figure written to two significant digits may round below what it contains before that is a finding |
|
|
477
|
+
| `POPULATION_RULES` | `const POPULATION_RULES` | `readonly object[]` | the rules in order, each its `name` — the prefix its findings carry — and what it `describe`s |
|
|
478
|
+
| `checkPopulation` | `checkPopulation(note, { index })` | `object[]` | linting a place's `population` — over-held land, an over-full region, and an oversized settlement |
|
|
479
|
+
| `checkCitedPopulations` | `checkCitedPopulations(note, { index })` | `object[]` | linting a `doc` note's cited figures, each located at the `~figure` that states it |
|
|
480
|
+
|
|
445
481
|
### `engine.marketClass`
|
|
446
482
|
|
|
447
483
|
What trade a settlement supports (`data.market`): the six-step scale, stated once for the lint, the author-facing reference and the specification, and the check that holds a note's value to it.
|
|
@@ -651,19 +687,19 @@ Emitting this package's cross-package link manifest. `engine/content-address.mjs
|
|
|
651
687
|
|
|
652
688
|
Emitting this package's content index. Every content build already walks the whole note tree and parses every note's frontmatter — the pack compilers, the site build, and the content-table expander each do it — and every one of them throws the result away when it finishes. So nothing outside a build can ask a question about the content: "which beings carry no `kbcat`?", "what does this table actually select?", "did that type rename leave anything behind?" have no answer short of writing a throwaway script that re-walks the tree. Eight dead Bestiary tables shipped for weeks behind exactly that gap.
|
|
653
689
|
|
|
654
|
-
| Export | Signature | Returns
|
|
655
|
-
| ----------------------- | ------------------------------------------------------------------------------------------------------- |
|
|
656
|
-
| `noteAddress` | `function noteAddress(frontmatter, contentPackage)` | {{slug: string, canonical: string}\|null} `slug` is what goes inside `[[…]]` within this package; `canonical` is the fully qualified key the manifest files the note under, carrying the package and the system as well.
|
|
657
|
-
| `sortKeysDeep` | `function sortKeysDeep(value)` | {unknown} The value with every plain object's keys in sorted order.
|
|
658
|
-
| `asciiName` | `function asciiName(name)` | {string\|null} The ASCII form, or `null` when there is no name, or nothing printable survives.
|
|
659
|
-
| `asciiAliases` | `function asciiAliases(aliases)` | {Array<string>} Possibly empty, never null: a note with no aliases has an empty set of them, which is a fact rather than a missing value, and a consumer iterating it should not have to check first.
|
|
660
|
-
| `serializeContentIndex` | `function serializeContentIndex(records)` | {string} One compact JSON object per line, newline-terminated.
|
|
661
|
-
| `indexRecordsFor` | `indexRecordsFor({ contentBase, config, skipDirectories, problems })` | {object[]} One record per note, plus one per documentation entry.
|
|
662
|
-
| `emitContentIndex` | `emitContentIndex({ contentBase, outDir, config })` | {{file: string, notes: number, assets: number, records: number, bytes: number}} Where it was written, how many notes and how many assets it holds, how many records that is in all, and
|
|
663
|
-
| `buildIndexRecord` | `buildIndexRecord({ frontmatter, relPath, absPath, contentPackage, body, bodyLine, manifest })` | `Record<string, any>` — the record, with derived fields sorted deep
|
|
664
|
-
| `collectContentIndex` | `collectContentIndex(contentBase, { contentPackage, skipDirectories, assetsBase, manifest, problems })` | `Array<Record<string, any>>`
|
|
690
|
+
| Export | Signature | Returns | Use it when |
|
|
691
|
+
| ----------------------- | ------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
692
|
+
| `noteAddress` | `function noteAddress(frontmatter, contentPackage)` | {{slug: string, canonical: string}\|null} `slug` is what goes inside `[[…]]` within this package; `canonical` is the fully qualified key the manifest files the note under, carrying the package and the system as well. | The address a wikilink writes to reach a note, or `null` when it has none. |
|
|
693
|
+
| `sortKeysDeep` | `function sortKeysDeep(value)` | {unknown} The value with every plain object's keys in sorted order. | Recursively sort an object's keys, so serialization is order-independent. |
|
|
694
|
+
| `asciiName` | `function asciiName(name)` | {string\|null} The ASCII form, or `null` when there is no name, or nothing printable survives. | A note's display name reduced to printable 7-bit ASCII. |
|
|
695
|
+
| `asciiAliases` | `function asciiAliases(aliases)` | {Array<string>} Possibly empty, never null: a note with no aliases has an empty set of them, which is a fact rather than a missing value, and a consumer iterating it should not have to check first. | A note's `name.aliases` reduced to printable 7-bit ASCII, in order. |
|
|
696
|
+
| `serializeContentIndex` | `function serializeContentIndex(records)` | {string} One compact JSON object per line, newline-terminated. | Serialize records as JSON Lines. |
|
|
697
|
+
| `indexRecordsFor` | `indexRecordsFor({ contentBase, config, skipDirectories, problems })` | {object[]} One record per note, plus one per documentation entry. | The index records for a content tree, without writing anything. |
|
|
698
|
+
| `emitContentIndex` | `emitContentIndex({ contentBase, outDir, config })` | {{file: string, notes: number, assets: number, records: number, bytes: number, full: number, draft: number, stub: number}} Where it was written, how many notes and how many assets it holds, how many records that is in all, its size, and the ladder those notes sit on. | Emit this package's content index. |
|
|
699
|
+
| `buildIndexRecord` | `buildIndexRecord({ frontmatter, relPath, absPath, contentPackage, body, bodyLine, manifest })` | `Record<string, any>` — the record, with derived fields sorted deep | building one note's content-index record; refuses a note that authors a key the index derives itself (`package` among them) before building anything |
|
|
700
|
+
| `collectContentIndex` | `collectContentIndex(contentBase, { contentPackage, skipDirectories, assetsBase, manifest, problems })` | `Array<Record<string, any>>` | reading a whole content tree into index records, in a total order independent of directory-read order; an item note yields two records (its own and its documentation journal's), and each asset root yields one per file |
|
|
665
701
|
|
|
666
|
-
`engine.contentIndex` also re-exports
|
|
702
|
+
`engine.contentIndex` also re-exports eight names from neighbouring leaf modules, at this same import path:
|
|
667
703
|
|
|
668
704
|
| Export | Signature | Returns | Use it when |
|
|
669
705
|
| --------------------- | -------------------------------- | -------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
@@ -672,6 +708,7 @@ Emitting this package's content index. Every content build already walks the who
|
|
|
672
708
|
| `noteFile` | `noteFile(contentBase, record)` | `string` — the note's absolute path | composing an index record's `file.path` (recorded relative, for a byte-stable artifact) back into an openable absolute path |
|
|
673
709
|
| `authoredFrontmatter` | `authoredFrontmatter(record)` | `Record<string, any>` — the frontmatter without `DERIVED_KEYS` | reading back exactly what a note authored from its index record, so a pass can lint or compile from the index without reasoning about derived fields as if the author wrote them |
|
|
674
710
|
| `isNoteRecord` | `isNoteRecord(record)` | `boolean` | telling a note's own record apart from its documentation journal's and from an asset's, when enumerating the corpus |
|
|
711
|
+
| `isStub` | `isStub(record)` | `boolean` | telling a stub — a note with an empty body — apart from a note that publishes a page; asked of the absent address, which is the whole signal |
|
|
675
712
|
| `isAssetRecord` | `isAssetRecord(record)` | `boolean` | telling a record that addresses a file apart from one that addresses a note; the `asset` block is what marks it |
|
|
676
713
|
| `recordPath` | `recordPath(record)` | `string` — the path the record names inside its package | ordering or reporting across both record shapes: a note names the `.md` it was parsed from, an asset the file it is |
|
|
677
714
|
|
|
@@ -977,6 +1014,7 @@ What a `[[…]]` **is**, before anything decides where it points. One authored l
|
|
|
977
1014
|
| `unlabelledLinkMessage` | `function unlabelledLinkMessage(target)` | {string} The message, unpunctuated at the end as a finding is. | What an author writing an unlabelled link is told, in one place. |
|
|
978
1015
|
| `LINK_FINDING_REASONS` | `const LINK_FINDING_REASONS` | — | Every way a link can fail, named once for all three resolvers. |
|
|
979
1016
|
| `unresolvedAddressMessage` | `function unresolvedAddressMessage(target)` | {string} The message, unpunctuated at the end as a finding is. | What an author writing an address that resolves to nothing is told. |
|
|
1017
|
+
| `stubAddressMessage` | `function stubAddressMessage(target, file)` | {string} The message, unpunctuated at the end as a finding is. | What an author linking to a stub is told, naming the file to open. |
|
|
980
1018
|
| `ambiguousAddressMessage` | `function ambiguousAddressMessage(target, packages` | {string} The message. | What an author writing a short address more than one package publishes is told. |
|
|
981
1019
|
| `linkFindingMessage` | `linkFindingMessage({ reason, target, packages, anchor })` | {string} The message. | The message for one link finding, whichever resolver found it. |
|
|
982
1020
|
| `isSamePage` | `isSamePage({ target, anchor })` | {boolean} True when the link is same-page. | Whether a parsed link addresses a section of the page it is written on. |
|
package/docs/content-format.md
CHANGED
|
@@ -1657,6 +1657,108 @@ not honoured in part either.
|
|
|
1657
1657
|
convention rather than declared in a field — see
|
|
1658
1658
|
[the lead image](#the-lead-image).
|
|
1659
1659
|
|
|
1660
|
+
### Three states of a note
|
|
1661
|
+
|
|
1662
|
+
A note has three states, and two of them are nothing but conventions the format
|
|
1663
|
+
already carried.
|
|
1664
|
+
|
|
1665
|
+
| State | Body | Marker | Page | Document | Link target |
|
|
1666
|
+
| --------- | ------- | --------------- | ----------- | --------------------------------- | ----------- |
|
|
1667
|
+
| **stub** | empty | none | no | yes, where one would not be empty | no |
|
|
1668
|
+
| **draft** | written | `tags: [draft]` | yes, marked | yes | yes |
|
|
1669
|
+
| **full** | written | none | yes | yes | yes |
|
|
1670
|
+
|
|
1671
|
+
**Full is the absence of everything.** It is the default and carries no marker,
|
|
1672
|
+
which is right: the overwhelming majority of a finished corpus should say
|
|
1673
|
+
nothing about its own finishedness.
|
|
1674
|
+
|
|
1675
|
+
There is no new frontmatter key, no new filetype and no second parser. A stub is
|
|
1676
|
+
a `.md` file the walk already finds, and the moment somebody writes a body it
|
|
1677
|
+
becomes an ordinary note — nothing moves, nothing is deleted, and every inbound
|
|
1678
|
+
link starts resolving.
|
|
1679
|
+
|
|
1680
|
+
#### A stub is a note with an empty body
|
|
1681
|
+
|
|
1682
|
+
> **The body is empty when everything after the closing frontmatter fence is
|
|
1683
|
+
> whitespace.** Nothing else is empty.
|
|
1684
|
+
|
|
1685
|
+
The rule is that severe because the test has to be one you can apply by looking
|
|
1686
|
+
at the file. Every softer rule makes two files that look different behave the
|
|
1687
|
+
same.
|
|
1688
|
+
|
|
1689
|
+
| After the fence | Empty? | What happens |
|
|
1690
|
+
| ----------------------- | ------ | --------------------------------------------------------------- |
|
|
1691
|
+
| Nothing, or blank lines | yes | A stub |
|
|
1692
|
+
| An HTML comment | no | A note with a body that renders to nothing. The lint reports it |
|
|
1693
|
+
| A horizontal rule | no | The same |
|
|
1694
|
+
| A lone heading | no | The same |
|
|
1695
|
+
| `_To be written._` | no | The same |
|
|
1696
|
+
|
|
1697
|
+
An HTML comment is deliberately not empty. An invisible marker deciding whether
|
|
1698
|
+
a page exists is exactly the failure the retired `draft:` field was removed for.
|
|
1699
|
+
|
|
1700
|
+
#### What a stub emits
|
|
1701
|
+
|
|
1702
|
+
A stub's index record is its frontmatter plus the derived keys, **minus
|
|
1703
|
+
`address` and `anchors`**. The absent address is the whole signal, and it is the
|
|
1704
|
+
column a table renderer already reads.
|
|
1705
|
+
|
|
1706
|
+
It keeps everything else:
|
|
1707
|
+
|
|
1708
|
+
- **`file`**, which is what you edit and what every diagnostic names.
|
|
1709
|
+
- **`id`**, which is the identity every diagnostic and every table reads.
|
|
1710
|
+
- **`foundry`**, naming each document that was made.
|
|
1711
|
+
|
|
1712
|
+
#### A document that would be empty is not created
|
|
1713
|
+
|
|
1714
|
+
> **If the resulting document would be empty, it is not created** — and the
|
|
1715
|
+
> index then names no artefact: `uuid`, `path` and `address` are `null`, which
|
|
1716
|
+
> is how a record says one was not made.
|
|
1717
|
+
|
|
1718
|
+
The test is about the **output**, which is what lets one rule cover every case:
|
|
1719
|
+
|
|
1720
|
+
| The note | The document | Made? |
|
|
1721
|
+
| ---------------------------------- | -------------------------------------- | ----- |
|
|
1722
|
+
| `place`, `lore`, `doc`, `scenario` | its JournalEntry, which _is_ the prose | no |
|
|
1723
|
+
| any item type, `macro` | the Item or Macro, from `data:` | yes |
|
|
1724
|
+
| the same note's documentation | a JournalEntry of the same prose | no |
|
|
1725
|
+
| `map` | the Scene, from `data:` | yes |
|
|
1726
|
+
|
|
1727
|
+
An unwritten arcane talent still compiles into its Item, because an Item is its
|
|
1728
|
+
`data:` and that is not empty. What it has no documentation journal for is the
|
|
1729
|
+
same thing an unwritten `place` has no journal for: there is nothing to put in
|
|
1730
|
+
one.
|
|
1731
|
+
|
|
1732
|
+
**An infobox does not count as content.** The panel is a rendering of `data:`
|
|
1733
|
+
that the index already publishes, so an entry holding nothing else would tell a
|
|
1734
|
+
reader exactly what the record told them.
|
|
1735
|
+
|
|
1736
|
+
Nothing anywhere names a document that was not made: a compendium UUID for one
|
|
1737
|
+
would send every consumer that resolves it to a document the pack does not hold.
|
|
1738
|
+
|
|
1739
|
+
What is suppressed with the page is the web address and the link target: a
|
|
1740
|
+
wikilink into a stub is refused, and a table cell listing one prints its name as
|
|
1741
|
+
plain text rather than as a link.
|
|
1742
|
+
|
|
1743
|
+
#### Two types are structural and exempt
|
|
1744
|
+
|
|
1745
|
+
`folder` and `homepage` have no body by design — a folder note's page is a
|
|
1746
|
+
generated section index, and the homepage's is the site's front door. An empty
|
|
1747
|
+
body on either keeps its address and resolves as **full**, because such a note is
|
|
1748
|
+
complete.
|
|
1749
|
+
|
|
1750
|
+
Every type in the vocabulary declares whether an empty body suppresses its page,
|
|
1751
|
+
so adding a type means answering the question rather than inheriting a default.
|
|
1752
|
+
|
|
1753
|
+
#### A stub is still referenceable as data
|
|
1754
|
+
|
|
1755
|
+
`borders.to` and `routes.to` name a place by shortcode, and those are **index
|
|
1756
|
+
relations rather than page links**: a border with a stub on the far side is a
|
|
1757
|
+
valid statement about the world and resolves. So does `parents` for the
|
|
1758
|
+
containment tree and `domains` for tenure — a stub appears in its region's
|
|
1759
|
+
`contains` and on its holder's `holdings`, rendered as plain text. What a stub
|
|
1760
|
+
cannot be is the target of a wikilink, because a wikilink points at a page.
|
|
1761
|
+
|
|
1660
1762
|
### What a note produces
|
|
1661
1763
|
|
|
1662
1764
|
Note types fall into two groups, and only the first has a mapping table.
|
|
@@ -1903,6 +2005,49 @@ journals. A nested field is addressed exactly as a note authors it —
|
|
|
1903
2005
|
every nested object is inferred as a struct. A field a note type does not carry
|
|
1904
2006
|
reads `NULL` rather than failing.
|
|
1905
2007
|
|
|
2008
|
+
**`notes` leaves out stubs; `entries` does not.** Both carry a derived `state`
|
|
2009
|
+
column — `stub`, `draft` or `full` — and `notes` is exactly
|
|
2010
|
+
`SELECT * FROM entries WHERE state <> 'stub'`. So every table already written
|
|
2011
|
+
selects what it always selected, and gains `state` for free; a table that should
|
|
2012
|
+
list the unwritten beside the written changes one word:
|
|
2013
|
+
|
|
2014
|
+
````markdown
|
|
2015
|
+
```sql
|
|
2016
|
+
SELECT address.slug AS _ref,
|
|
2017
|
+
name.full AS "Name",
|
|
2018
|
+
state AS "State",
|
|
2019
|
+
m.name AS "Market",
|
|
2020
|
+
data.population AS "People"
|
|
2021
|
+
FROM entries
|
|
2022
|
+
LEFT JOIN market m ON m.value = data.market
|
|
2023
|
+
WHERE type = 'place' AND subType = 'settlement'
|
|
2024
|
+
AND list_contains(data.parents, 'place-aelwyth')
|
|
2025
|
+
ORDER BY data.population DESC NULLS LAST, name.full COLLATE NOCASE
|
|
2026
|
+
```
|
|
2027
|
+
````
|
|
2028
|
+
|
|
2029
|
+
A written settlement renders as a link and a stub renders as plain text, from
|
|
2030
|
+
one query with no branch in it: `_ref` is the stub's absent address, and the
|
|
2031
|
+
renderer already prints a bare cell where it is null.
|
|
2032
|
+
|
|
2033
|
+
A fence selecting `FROM notes` that **would** gain rows from `FROM entries` is
|
|
2034
|
+
warned about at the fence, with the count — so adopting stubs in a given table
|
|
2035
|
+
stays a decision somebody makes rather than one they forget.
|
|
2036
|
+
|
|
2037
|
+
The ladder is what makes "how finished is this?" a query:
|
|
2038
|
+
|
|
2039
|
+
```sql
|
|
2040
|
+
SELECT state, count(*) AS n FROM entries GROUP BY state ORDER BY state
|
|
2041
|
+
```
|
|
2042
|
+
|
|
2043
|
+
**`market`** is the six-step market scale as a relation — `value`, `name`,
|
|
2044
|
+
`trade` — so a table prints `village` beside the number a note wrote without a
|
|
2045
|
+
second copy of the scale living in authored content.
|
|
2046
|
+
|
|
2047
|
+
**The state is derived, never stored.** No index record carries it, no note can
|
|
2048
|
+
author it, and nothing in `data:` may claim it: the only authored signal is the
|
|
2049
|
+
`draft` tag, which marks the one thing reading the body cannot reveal.
|
|
2050
|
+
|
|
1906
2051
|
**Two aliases are read by the renderer rather than printed**, because which
|
|
1907
2052
|
column links and where a section breaks are decisions about output, not
|
|
1908
2053
|
relational operations:
|
|
@@ -3258,6 +3403,53 @@ a warning from `content-build lint`, `unheld land`, located at the note's
|
|
|
3258
3403
|
`region` and a `feature` are exempt: a region is held through its polity's
|
|
3259
3404
|
`domains`, and a river by nobody.
|
|
3260
3405
|
|
|
3406
|
+
#### Population figures
|
|
3407
|
+
|
|
3408
|
+
`population` is an approximate count, written to two significant digits. A
|
|
3409
|
+
place states one, and so does an affiliation — how many people it counts,
|
|
3410
|
+
wherever they are. `content-build lint` reads those figures against each other
|
|
3411
|
+
and reports four warnings, each named by the phrase its message begins with.
|
|
3412
|
+
|
|
3413
|
+
- **`over-held land`** — the polities whose `domains` name a place count more
|
|
3414
|
+
people than the place states. A polity subordinate to another polity holding
|
|
3415
|
+
the same place is already inside that polity's figure and is not counted
|
|
3416
|
+
twice. A **settlement is exempt**: the polity of a city-state holds the city
|
|
3417
|
+
and counts the hinterland around it, so its figure is properly the larger.
|
|
3418
|
+
- **`over-full region`** — the regions whose `parents` name a place count more
|
|
3419
|
+
people than the place states. A region naming both its parent and its
|
|
3420
|
+
grandparent counts under the nearer of the two, so a place written into two
|
|
3421
|
+
levels of the hierarchy is not counted at both.
|
|
3422
|
+
- **`oversized settlement`** — a settlement states more people than a place
|
|
3423
|
+
containing it does, whether its own region or any place above that.
|
|
3424
|
+
- **`disputed figure`** — a `doc` note carries a figure beside a wikilink to
|
|
3425
|
+
the place or affiliation it belongs to, and the two disagree. The finding is
|
|
3426
|
+
located at the number the page wrote, and a figure written twice is located
|
|
3427
|
+
twice.
|
|
3428
|
+
|
|
3429
|
+
The first three are reported on the note's own `data.population`; the fourth
|
|
3430
|
+
on the citation.
|
|
3431
|
+
|
|
3432
|
+
**A figure is read as a population only where it carries `~`.** That marker is
|
|
3433
|
+
what separates a population from the other numbers a table puts beside a place
|
|
3434
|
+
— a distance, a stage count, a tally of days — so `| [[place-rgn\|X]] | ~2,000 |`
|
|
3435
|
+
is a claim about people and `| [[place-rgn\|X]] | 3 |` is not.
|
|
3436
|
+
|
|
3437
|
+
**Rounding is admitted.** Two significant digits carry a half-unit of error,
|
|
3438
|
+
widest at the bottom of a decade, so a place whose figure falls up to five per
|
|
3439
|
+
cent below what it contains is not a finding.
|
|
3440
|
+
|
|
3441
|
+
**An unstated figure is silent.** A place or affiliation with no `population`
|
|
3442
|
+
contributes nothing to a sum and is never the subject of a finding, so a
|
|
3443
|
+
half-written region is quiet rather than noisy. A fetched index carries
|
|
3444
|
+
`parents` and `domains` but no figure, so a dependency's places sit in the
|
|
3445
|
+
geography and its people are counted nowhere.
|
|
3446
|
+
|
|
3447
|
+
**There is deliberately no urban-share rule.** The share of a region's people
|
|
3448
|
+
living in its named settlements cannot be measured from the notes: the
|
|
3449
|
+
settlement layer names the notable places and is complete nowhere, so the ratio
|
|
3450
|
+
reports how much of a region has been written rather than how much of it is
|
|
3451
|
+
urban.
|
|
3452
|
+
|
|
3261
3453
|
**A place declares only what is true of ground.** Four properties were removed
|
|
3262
3454
|
because they were true of something else, and each removal has a home to go to.
|
|
3263
3455
|
|
package/docs/diagnostics.md
CHANGED
|
@@ -163,6 +163,50 @@ A configuration finding reads the same as any other:
|
|
|
163
163
|
package-build.config.yaml:382:64: error: package-build config: `site.sections.being.descrption` is not a recognized option (expected one of: title, banner, description, listType, listSubType).
|
|
164
164
|
```
|
|
165
165
|
|
|
166
|
+
## A worked case: the three states of a note
|
|
167
|
+
|
|
168
|
+
A note with an empty body is a **stub**, and the diagnostics about one show each
|
|
169
|
+
rule above in turn.
|
|
170
|
+
|
|
171
|
+
An empty body must be deliberate, so a stub that says nothing about itself is an
|
|
172
|
+
error, located at its `type:` line — the key whose value made the note what it
|
|
173
|
+
is:
|
|
174
|
+
|
|
175
|
+
```text
|
|
176
|
+
Regions/Aelwyth/Weyshott.md:4:1: error: this note has no body, so it is a stub — and a stub carries a `description`, which is the only sentence a reader gets and what every table listing it prints
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
A body that reduces to a placeholder is located **by searching the file for the
|
|
180
|
+
phrase the rule matched**, which is the implicit-position rule: the finding is
|
|
181
|
+
about a literal, so it lands on that literal rather than on the frontmatter:
|
|
182
|
+
|
|
183
|
+
```text
|
|
184
|
+
Regions/Aelwyth/Harad.md:14:1: error: this note's body says only "_To be written._", so it publishes a page that tells a reader nothing — empty the body to make this a stub, or write it
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
A link into a stub is refused, and because the stub is in the index the message
|
|
188
|
+
can name the file to open rather than reporting that the address resolves
|
|
189
|
+
nowhere:
|
|
190
|
+
|
|
191
|
+
```text
|
|
192
|
+
Regions/Aelwyth/Aelwyth_Region.md:41:14: error: address [[place-weyshott]] names a stub at Regions/Aelwyth/Weyshott.md, which has no body and therefore no page — name it in prose, or write the note
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
A body that may be a draft that forgot its marker is a **warning**, because the
|
|
196
|
+
rule is a guess and a corpus holds many short notes that are complete:
|
|
197
|
+
|
|
198
|
+
```text
|
|
199
|
+
Rituals/Meivor.md:3:1: warning: this note's body is 4 word(s) and carries no `draft` tag — tag it if it is a beginning, and leave it alone if it says everything it has to say
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
And the counts, and the drafts nothing has touched in longest, are **prose**:
|
|
203
|
+
a note nobody has finished is not wrong, so it is listed rather than flagged,
|
|
204
|
+
and the summary carries no position because it has none.
|
|
205
|
+
|
|
206
|
+
```text
|
|
207
|
+
thalorna: 2572 notes — 2089 full, 464 draft, 19 stub.
|
|
208
|
+
```
|
|
209
|
+
|
|
166
210
|
## Summary counts and prose are not findings
|
|
167
211
|
|
|
168
212
|
A run's progress and summary lines — `42 address(es) across 10 note(s).`,
|