@heroiclands/package-build 22.4.6 → 22.4.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # @heroiclands/package-build
2
2
 
3
+ ## 22.4.7
4
+
5
+ ### Patch Changes
6
+
7
+ Every place page that states a border or a route, or is named in one, carries the map from that place: `from-<shortcode>.svg` beside the page and `map:` naming it in the front matter, with every place name on it a link to that place's page. GraphViz draws it; when GraphViz is not installed the site build says so once and writes every page without a map, and `site.maps: false` draws none.
8
+
9
+ A place's page carries what lies within it and who holds it, and an affiliation's page carries what it holds, read from the `parents` and `domains` the notes already state — a dependency's places and affiliations included. `content-build lint` warns of unheld land: a settlement, site or structure that no affiliation's `domains` names.
10
+
11
+ A `place` note can state a settlement's market class: `data.market`, a whole number from 1 to 6 beside `population`. `1` is a hamlet, with no market beyond what neighbours trade among themselves; `2` a village and its weekly market; `3` a town with several trades working full time; `4` a market town, its chartered fair and its moneylender; `5` a city, where anything ordinary can be had in quantity; `6` a great city that banks, imports as a matter of course, and is the market from which the other markets buy. The key is optional, and a value off the scale is reported at the line that states it rather than rounded onto a class nobody wrote.
12
+
3
13
  ## 22.4.6
4
14
 
5
15
  ### Patch Changes
package/CONTENT.md CHANGED
@@ -1183,6 +1183,25 @@ parent or child, or a pair listed twice by one mode. Each finding lands on the
1183
1183
  entry that states it. The keys, the sets and the days scale are specified under
1184
1184
  `type: place` in `docs/content-format.md`.
1185
1185
 
1186
+ **A settlement is held by someone.** An affiliation's `domains` names the
1187
+ places it holds, and a `place` of subType `settlement`, `site` or `structure`
1188
+ that no affiliation's `domains` names — in this tree or in a fetched index —
1189
+ is a warning, `unheld land`, at the note's `type:` line, so a gap in tenure
1190
+ shows. A region is held through its polity's `domains` and a feature by
1191
+ nobody, so neither is checked.
1192
+
1193
+ **A settlement's market class is one of six.** `data.market` states what trade
1194
+ a settlement supports — `1` a hamlet, with no market beyond what neighbours
1195
+ trade among themselves; `2` a village and its weekly market; `3` a town with
1196
+ several trades working full time; `4` a market town, its chartered fair and its
1197
+ moneylender; `5` a city, where anything ordinary can be had in quantity; `6` a
1198
+ great city that banks, imports as a matter of course, and is the market from
1199
+ which the other markets buy. The numbers order the steps and do nothing else, so a value off the
1200
+ scale is an error at the value that states it rather than a number rounded onto
1201
+ a class the author did not write. The key is optional and the lint checks no
1202
+ `subType` condition, as it checks none for `population`. The scale is stated
1203
+ under `type: place` in `docs/content-format.md`.
1204
+
1186
1205
  Nothing here writes. A check reports and an author fixes.
1187
1206
 
1188
1207
  **`content-build map` draws what those keys state.** `--tree` is the author's
@@ -2409,6 +2428,96 @@ The theme's `related.html` partial reads both lists and renders them as one
2409
2428
  `related` is derived, never authored: what links to a page is a fact about
2410
2429
  every other page in the tree. A note that writes one has it replaced.
2411
2430
 
2431
+ ### A place page says what lies within it and who holds it; an affiliation page says what it holds
2432
+
2433
+ Two keys the notes already carry answer three questions a reader asks of a
2434
+ page. A place's `data.parents` is geography — what it sits within — and an
2435
+ affiliation's `data.domains` is tenure — what it holds. The build reads both
2436
+ across the tree and every fetched index, inverts them, and writes each page
2437
+ the lists that concern it, shaped like `related`:
2438
+
2439
+ ```yaml
2440
+ # on a place
2441
+ contains: # every place whose `parents` names this one
2442
+ - title: Khaset-Mehtet
2443
+ url: /thalorna/place-khasetmehtet/
2444
+ type: place
2445
+ subType: settlement
2446
+ held_by: # every affiliation whose `domains` names this one
2447
+ - title: The Nome of Ankhsetun
2448
+ url: /thalorna/affiliation-nomenkhstn/
2449
+ type: affiliation
2450
+ subType: polity
2451
+
2452
+ # on an affiliation
2453
+ holdings: # every place its `domains` names
2454
+ - title: Ankhsetun
2455
+ url: /thalorna/place-ankhsetunnome/
2456
+ type: place
2457
+ subType: region
2458
+ ```
2459
+
2460
+ - **`contains` and `holdings` are sorted by `subType`, then `title`**;
2461
+ `held_by` by `title`. An entry carries `subType` where the note declares
2462
+ one.
2463
+ - **A key is present only when it has an entry.** A place nobody holds and
2464
+ nothing sits within carries neither key; an affiliation whose `domains` is
2465
+ empty carries no `holdings`. The theme's silent-disappear convention, kept
2466
+ at the source.
2467
+ - **`domains` names what an affiliation holds directly, and is never
2468
+ expanded.** A polity whose `domains` names a region holds the region; the
2469
+ settlements within it are reached through the region's `contains`, and are
2470
+ not repeated in the polity's `holdings`. Tenure runs through
2471
+ `affiliation.parents` — a house of its earl, an earl of the crown — and
2472
+ geography through `place.parents`. A subinfeudated manor sits in one region
2473
+ by the second and under a lord of another polity by the first, and both
2474
+ pages say so.
2475
+ - **A dependency's places and affiliations take part.** A fetched index entry
2476
+ carries the `parents` and `domains` its record stated, so a place another
2477
+ package publishes is listed within a local region, and a house another
2478
+ package publishes is named on the local manor it holds. A local note
2479
+ declaring a shortcode shadows a fetched entry declaring the same one.
2480
+ - **`url` composes `<base><slug>/`**, as every `related` entry does.
2481
+
2482
+ All three are derived, never authored: a note that writes one has it
2483
+ replaced. The theme renders each list as a table on the page.
2484
+
2485
+ ### A place page carries the map from that place
2486
+
2487
+ A place that states a border or a route, or is named in one, has a map from
2488
+ it — the drawing `content-build map --from` makes, with the place at the
2489
+ centre and each neighbour at its bearing. The site build draws that map for
2490
+ every such place and writes it with the page: the page is a leaf bundle,
2491
+ `place-<shortcode>/index.md`, the drawing sits beside it as
2492
+ `from-<shortcode>.svg`, and the front matter names it:
2493
+
2494
+ ```yaml
2495
+ map: from-kthrmr.svg
2496
+ ```
2497
+
2498
+ The theme's "From here" panel reads the key, inlines the file as a page
2499
+ resource, and so every place name on the map is a link to that place's page.
2500
+ What the build guarantees:
2501
+
2502
+ - **The address does not move.** A bundled page states the same `url` a flat
2503
+ one does, `/<slug>/`, so `place-kthrmr/index.md` publishes exactly where
2504
+ `place-kthrmr.md` would.
2505
+ - **Every name links through the site base.** The map is drawn with the base
2506
+ the pages are served under, so each `href` composes `<base><slug>/` the way
2507
+ every other href the build renders does.
2508
+ - **It is written for inlining.** No XML prologue, no DOCTYPE, no comment, no
2509
+ reference to anything outside the file; the root carries a `viewBox` and no
2510
+ fixed width or height, so the theme sizes it to the column.
2511
+ - **A place with no relation carries nothing**: no file beside it, no `map`
2512
+ key, and the page stays a flat file. So does every page that is not a place.
2513
+ - **A site never fails for want of a map.** GraphViz draws; when it is not
2514
+ installed the build says so once, as a warning naming what to install, and
2515
+ every page is written as it would be without maps. `site.maps: false` in
2516
+ `package-build.config.yaml` draws nothing and asks nothing of GraphViz.
2517
+
2518
+ `map` is derived, never authored: a note that writes one has it replaced when
2519
+ the build draws a map, and dropped when there is none to draw.
2520
+
2412
2521
  ### Why the output location is fixed
2413
2522
 
2414
2523
  The content mount is a build artifact and is **deleted on every run**, so that a
@@ -1515,7 +1515,8 @@ function pdfCommand() {
1515
1515
  * wikilink resolution, code-fence protection and the foreign-manifest merge —
1516
1516
  * happens here, from configuration. So does the Hugo configuration: the whole
1517
1517
  * source tree Hugo reads lands under `build/hugo/`, and the consumer's script
1518
- * runs Hugo over it.
1518
+ * runs Hugo over it. With `site.maps` on and GraphViz installed, every place
1519
+ * page with a border or a route carries the map from that place.
1519
1520
  *
1520
1521
  * **The Hugo configuration is generated before anything is written.** Its
1521
1522
  * sources — `package.json`'s `homepage`, the cached navigation, the installed
@@ -1653,10 +1654,17 @@ function siteCommand() {
1653
1654
  return;
1654
1655
  }
1655
1656
 
1657
+ // What drawing the maps found: warnings, every one. A site
1658
+ // never fails for want of a map, so none of these touches the
1659
+ // exit code — GraphViz absent is one line naming what to
1660
+ // install, and every page is written as it would be without.
1661
+ for (const f of result.mapFindings) emitDiagnostic(f);
1662
+
1656
1663
  const s = result.stats;
1657
1664
  log.info(
1658
1665
  `wrote ${s.homepages ?? 0} homepage(s) + ` +
1659
- `${s.content ?? 0} content page(s) to ` +
1666
+ `${s.content ?? 0} content page(s), ` +
1667
+ `${s.maps ?? 0} with a map, to ` +
1660
1668
  `${path.relative(process.cwd(), s.out)}`,
1661
1669
  );
1662
1670
  const { file } = writeHugoConfig(config, hugo);
@@ -772,6 +772,7 @@ const SITE_KEYS = [
772
772
  "notfound",
773
773
  "hugo",
774
774
  "search",
775
+ "maps",
775
776
  ];
776
777
  const SITE_NOTFOUND_KEYS = ["tagline", "sitenoun", "heroimage", "links"];
777
778
  const SITE_NOTFOUND_LINK_KEYS = ["title", "url", "text"];
@@ -1711,9 +1712,11 @@ const SITE_INDEX_MESSAGE =
1711
1712
  * shared with the link manifest so the two cannot disagree about where a
1712
1713
  * page is.
1713
1714
  *
1714
- * `search` is the one switch here: whether `package-build site-root` writes a
1715
- * search index beside the rendered pages. On by default — every site carries
1716
- * one unless it says otherwise.
1715
+ * Two switches sit here, both on by default. `search` is whether
1716
+ * `package-build site-root` writes a search index beside the rendered pages.
1717
+ * `maps` is whether `content-build site` draws the map from each place that
1718
+ * states a border or a route and writes it into that place's page — a site
1719
+ * carries both unless it says otherwise.
1717
1720
  *
1718
1721
  * **What the site publishes is the homepage and the content tree, and nothing
1719
1722
  * beside them.** A page of documentation is a note — `type: doc`, addressed
@@ -1739,6 +1742,7 @@ function normalizeSite(value) {
1739
1742
  notfound: null,
1740
1743
  hugo: Object.freeze({}),
1741
1744
  search: true,
1745
+ maps: true,
1742
1746
  });
1743
1747
  if (value === undefined) return empty;
1744
1748
  if (!isPlainObject(value)) fail("site", "must be a mapping");
@@ -1795,6 +1799,7 @@ function normalizeSite(value) {
1795
1799
  notfound: normalizeSiteNotfound(input.notfound),
1796
1800
  hugo: normalizeSiteHugo(input.hugo),
1797
1801
  search: optionalBoolean(input.search, "site.search", true),
1802
+ maps: optionalBoolean(input.maps, "site.maps", true),
1798
1803
  });
1799
1804
  }
1800
1805
 
package/docs/api.md CHANGED
@@ -429,6 +429,30 @@ What a place is next to (`data.borders`) and reachable from (`data.routes`): the
429
429
  | `checkBorders` | `checkBorders(note, { index })` | `object[]` | linting a place's `borders` — targets, values, once per pair, no parent or child, and reciprocity with the other end |
430
430
  | `checkRoutes` | `checkRoutes(note, { index })` | `object[]` | linting a place's `routes` — as `checkBorders`, plus `mode`, `days` and `terrain`, once per pair per mode |
431
431
 
432
+ ### `engine.holdings`
433
+
434
+ What lies within a place, who holds it, and what an affiliation holds — read off `data.parents` and `data.domains` across this package and every fetched index, inverted into the `contains`, `held_by` and `holdings` lists the site build writes — and the tenure check `content-build lint` runs on a place.
435
+
436
+ | Export | Signature | Returns | Use it when |
437
+ | ---------------------- | ------------------------------------ | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
438
+ | `HOLDINGS_KEYS` | `const HOLDINGS_KEYS` | `readonly string[]` | the three derived keys — `contains`, `held_by`, `holdings` — which a note cannot author |
439
+ | `HELD_SUBTYPES` | `const HELD_SUBTYPES` | `readonly string[]` | the place subTypes tenure is checked on — `settlement`, `site`, `structure` |
440
+ | `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
+ | `foreignHoldingsNodes` | `foreignHoldingsNodes(foreignIndex)` | `HoldingsNode[]` | reading a fetched index's places and affiliations as nodes |
442
+ | `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 |
443
+ | `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
+
445
+ ### `engine.marketClass`
446
+
447
+ 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.
448
+
449
+ | Export | Signature | Returns | Use it when |
450
+ | ---------------- | ---------------------- | ------------------- | ------------------------------------------------------------------------------------------------- |
451
+ | `MARKET_CLASSES` | `const MARKET_CLASSES` | `readonly object[]` | the scale in order, each step its `value`, the `name` a settlement of it goes by, and its `trade` |
452
+ | `isMarketClass` | `isMarketClass(value)` | `boolean` | asking whether an authored value names a class, a quoted number included |
453
+ | `marketClass` | `marketClass(value)` | `object\|undefined` | the class a value names, for rendering what a number means |
454
+ | `checkMarket` | `checkMarket(note)` | `object[]` | linting a place's `market` — a value off the scale, located at the value that states it |
455
+
432
456
  ### `engine.mapPlaces`
433
457
 
434
458
  The places a map is drawn from — this package's index records and every fetched dependency's entries, read through one shape — and what their `parents` say: the world, the continents, each place's continent, and everything the containment tree can get wrong, reported as findings located at the entry that states it.
@@ -437,6 +461,7 @@ The places a map is drawn from — this package's index records and every fetche
437
461
  | --------------------- | ---------------------------------------------------------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------ |
438
462
  | `placesFromRecords` | `placesFromRecords(records, { contentBase, base })` | `MapWorld` | reading this package's places and polities out of its index records, with a page URL each where a site base is given |
439
463
  | `loadMapWorld` | `loadMapWorld({ config, contentBase, base, problems })` | `MapWorld` | loading the world a map is drawn from — this package's index and every declared dependency's |
464
+ | `mapWorld` | `mapWorld({ records, foreignIndex, contentBase, base, config })` | `MapWorld` | the same world out of records and indexes a caller already holds, as the site build does |
440
465
  | `analyzeContainment` | `analyzeContainment(places, polities)` | `Containment` | finding the world, the continents, each place's continent and the anomalies of `parents` |
441
466
  | `subtreeOf` | `subtreeOf(analysis, root)` | `Set<string>` | every place beneath one, by following `parents` downward |
442
467
  | `containmentFindings` | `containmentFindings(analysis, places, { contentBase, config })` | `object[]` | the anomalies as `file:line:column: severity: message` findings — no parent, an unresolved parent and a cycle are errors |
@@ -485,6 +510,7 @@ GraphViz, as the map command reaches it: a build-time tool of that command alone
485
510
  | Export | Signature | Returns | Use it when |
486
511
  | ------------------------ | ------------------------------------------------------- | ------------------- | ------------------------------------------------------------------------------------------ |
487
512
  | `GRAPHVIZ_ENGINES` | `const GRAPHVIZ_ENGINES` | `readonly string[]` | the engines the map command may ask for — `dot`, `twopi`, `neato` |
513
+ | `GRAPHVIZ_INSTALL` | `const GRAPHVIZ_INSTALL` | `string` | what installs GraphViz, as the clause every "not installed" message ends with |
488
514
  | `findGraphviz` | `findGraphviz(engine, { env, fallbacks })` | `string\|undefined` | finding an engine's binary on `PATH` or in the directories a package manager installs into |
489
515
  | `graphvizMissingMessage` | `graphvizMissingMessage(engine)` | `string` | saying an engine is not installed, and what installs it |
490
516
  | `renderDot` | `renderDot(dotPath, outPath, { binary, format, args })` | `{warnings}` | rendering a `.dot` file, the engine's warnings returned and a non-zero exit thrown |
@@ -500,6 +526,16 @@ GraphViz, as the map command reaches it: a build-time tool of that command alone
500
526
  | `relatedPlaces` | `relatedPlaces(places)` | `string[]` | every place that states, or is named in, a border or a route |
501
527
  | `buildMaps` | `buildMaps({ world, outDir, tree, root, from, chart, horizon, travel, engine, rankdir, nodesep, ranksep, scale, polities, locate, requireGraphviz })` | `{written, findings, skipped}` | drawing any of the four from a loaded world |
502
528
 
529
+ ### `engine.siteMaps`
530
+
531
+ The map from each place, drawn for the site: `content-build site` draws the map from every place of this package that states, or is named in, a border or a route — with the site's base, so every name links through it — and the page writer lays each drawing beside its page as `from-<shortcode>.svg` and names it in the front matter as `map`. GraphViz absent is one warning and no map; `site.maps: false` draws none.
532
+
533
+ | Export | Signature | Returns | Use it when |
534
+ | -------------- | --------------------------------------------------------------- | ------------------ | --------------------------------------------------------------------------------------------------------------- |
535
+ | `SITE_MAP_DIR` | `const SITE_MAP_DIR` | `string` | where the site build draws below a package's root, `build/map/site` |
536
+ | `inlineSvg` | `inlineSvg(svg)` | `string` | rewriting a GraphViz SVG for inlining — no prologue, no comment, no `xlink:`, the root sized by `viewBox` alone |
537
+ | `drawSiteMaps` | `drawSiteMaps({ records, foreignIndex, config, base, locate })` | `{maps, findings}` | drawing the map from every related place of this package, keyed by the URL of the page that carries it |
538
+
503
539
  ### `engine.systems`
504
540
 
505
541
  The closed registry of system ids, and the `none` that stands for no system at all. An unknown system value is an error; adding a system is a data change to this registry rather than a hardcoded set scattered through the pipeline.
@@ -694,20 +730,20 @@ The toolchain's own content index — the files it ships, addressed. Every other
694
730
 
695
731
  Publishing a content tree as a website. Compiling a content tree into compendium packs is `content-build package compile`. Publishing the _same tree_ as a website was a script each consumer wrote for itself — 473 code lines in `sohl` and 462 in `sohl-thalorna`, 87 of them identical — and the copies drifted in ways neither repository could see. `sohl-thalorna` reimplemented four things this package already exported, not because it needed different behaviour but because its script predates the extraction. That is the failure a command removes: a consumer cannot accidentally reimplement one.
696
732
 
697
- | Export | Signature | Returns | Use it when |
698
- | --------------------- | ------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
699
- | `collectContentPages` | `function collectContentPages(contentBase, ctx)` | {{pages: object[], addressFindings: object[], fmLinkFindings: object[]}} | The content tree's pages, and what could not be addressed. |
700
- | `collectHomepages` | `function collectHomepages(contentBase, ctx)` | {{pages: object[], addressFindings: object[]}} The homepage notes, in walk order, and the ones among them that could not be addressed. | The package's homepage notes — the authored page at `/<contentPackage>/`. |
701
- | `writeHomepages` | `function writeHomepages(outRoot, pages, config)` | {number} How many pages were written. | Writes each homepage as the package root's `_index.md`. |
702
- | `siteGates` | `function siteGates(pages, findings,` | {object} The gate results and, when they pass, the built index. | The integrity gates a site build runs before it writes anything. |
703
- | `emptyGates` | `function emptyGates()` | {object} An all-clear gate result. | The gate result of a build that ran none of them. |
704
- | `gatesFailed` | `function gatesFailed(gates)` | — | Whether any gate produced a finding. |
705
- | `tableUniverse` | `function tableUniverse(pages)` | {Map<string, object[]>} Package → the notes it may tabulate. | The universe a generated table searches, grouped by package. |
706
- | `pageFrontmatter` | `function pageFrontmatter(page,` | {object} The frontmatter to write. | The frontmatter a page publishes with. |
707
- | `pageDestination` | `function pageDestination(page)` | — | Where a page is written, relative to the output root. |
708
- | `renderPages` | `function renderPages(pages, options)` | {{written: number, byKind: Record<string, number>, tableErrors: object[], wikiErrors: object[]}} | Renders and writes every page. |
709
- | `resolveSitePass` | `function resolveSitePass(name, options)` | {{beforeLinks?: Function}} The bundle. | Resolves `site.pass` to its bundle. |
710
- | `buildSite` | `buildSite({ config, sqlTables })` | {{gates: object, stats: object\|null, tableErrors: object[], wikiErrors: object[], manifests: object\|null}} | Builds a Hugo content tree from a content tree, and reports what it found. |
733
+ | Export | Signature | Returns | Use it when |
734
+ | --------------------- | ------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
735
+ | `collectContentPages` | `function collectContentPages(contentBase, ctx)` | {{pages: object[], addressFindings: object[], fmLinkFindings: object[]}} | The content tree's pages, and what could not be addressed. |
736
+ | `collectHomepages` | `function collectHomepages(contentBase, ctx)` | {{pages: object[], addressFindings: object[]}} The homepage notes, in walk order, and the ones among them that could not be addressed. | The package's homepage notes — the authored page at `/<contentPackage>/`. |
737
+ | `writeHomepages` | `function writeHomepages(outRoot, pages, config)` | {number} How many pages were written. | Writes each homepage as the package root's `_index.md`. |
738
+ | `siteGates` | `function siteGates(pages, findings,` | {object} The gate results and, when they pass, the built index. | The integrity gates a site build runs before it writes anything. |
739
+ | `emptyGates` | `function emptyGates()` | {object} An all-clear gate result. | The gate result of a build that ran none of them. |
740
+ | `gatesFailed` | `function gatesFailed(gates)` | — | Whether any gate produced a finding. |
741
+ | `tableUniverse` | `function tableUniverse(pages)` | {Map<string, object[]>} Package → the notes it may tabulate. | The universe a generated table searches, grouped by package. |
742
+ | `pageFrontmatter` | `function pageFrontmatter(page,` | {object} The frontmatter to write. | The frontmatter a page publishes with. |
743
+ | `pageDestination` | `function pageDestination(page, { bundle })` | — | Where a page is written, relative to the output root: flat, or as a leaf bundle when it carries a file beside itself. |
744
+ | `renderPages` | `function renderPages(pages, options)` | {{written: number, byKind: Record<string, number>, tableErrors: object[], wikiErrors: object[]}} | Renders and writes every page. |
745
+ | `resolveSitePass` | `function resolveSitePass(name, options)` | {{beforeLinks?: Function}} The bundle. | Resolves `site.pass` to its bundle. |
746
+ | `buildSite` | `buildSite({ config, sqlTables, locate })` | {{gates: object, stats: object\|null, tableErrors: object[], wikiErrors: object[], mapFindings: object[], manifests: object\|null}} | Builds a Hugo content tree from a content tree, and reports what it found. |
711
747
 
712
748
  ### `engine.contentLint`
713
749
 
@@ -770,6 +770,7 @@ Any other key under `docs.itemFields` is refused:
770
770
  | `site.notfound` | object | `null` |
771
771
  | `site.hugo` | object | `{}` |
772
772
  | `site.search` | boolean | `true` |
773
+ | `site.maps` | boolean | `true` |
773
774
 
774
775
  How much of a package reaches the web at all is **not** here — it is
775
776
  [`publish.site`](#publish). `site` is framing: which named pass bundle
@@ -819,7 +820,7 @@ published. A `site.out` is refused by name:
819
820
 
820
821
  > ``package-build config: `site` must be a mapping.``
821
822
 
822
- > ``package-build config: `site.<key>` is not a recognized option (expected one of: base, assets, description, packages, pass, passOptions, notfound, hugo, search).``
823
+ > ``package-build config: `site.<key>` is not a recognized option (expected one of: base, assets, description, packages, pass, passOptions, notfound, hugo, search, maps).``
823
824
 
824
825
  `site.assets` is the host every package's imagery is served from, and it is
825
826
  the one address in this file that is not this repository's own. A note names
@@ -950,6 +951,24 @@ site:
950
951
 
951
952
  > ``package-build config: `site.search` must be a boolean.``
952
953
 
954
+ `site.maps` is whether every place page carries the map from that place. On
955
+ by default: `content-build site` draws the map `content-build map --from`
956
+ draws — the place's borders and routes, each neighbour at its bearing — for
957
+ every place that states a border or a route or is named in one, lays it beside
958
+ the page as `from-<shortcode>.svg`, and names the file in the page's front
959
+ matter as `map`, which the theme inlines so every place name on it is a link
960
+ to that place's page. A place with no relation carries no map and no key.
961
+ GraphViz draws; when it is not installed the build says so once, as a
962
+ warning, and writes every page as it would without maps — a site never fails
963
+ for want of one. `false` draws nothing and asks nothing of GraphViz.
964
+
965
+ ```yaml
966
+ site:
967
+ maps: false
968
+ ```
969
+
970
+ > ``package-build config: `site.maps` must be a boolean.``
971
+
953
972
  ### The generated Hugo configuration
954
973
 
955
974
  `content-build site` writes `build/hugo/hugo.toml` on every run. Every value
@@ -2440,6 +2440,23 @@ subType `deity` covers a god's nature, domains, epithets and aspects in prose.
2440
2440
  That frees the name for the territorial sense, which earns it: every affiliation
2441
2441
  subType holds sway somewhere, while the divine sense applies to one.
2442
2442
 
2443
+ **`domains` names what the affiliation holds directly**, and nothing beneath
2444
+ it. A polity whose `domains` names a region holds the region; the settlements
2445
+ within it are the region's, reached through its `contains`, and are not
2446
+ repeated here. A house holds its manors and names each; the crown holds the
2447
+ realm and names the realm. What lies below a holder is answered by the two
2448
+ hierarchies together — `place.parents` for where a place sits, and
2449
+ `affiliation.parents` for whom a holder answers to — so a subinfeudated manor
2450
+ sits in one region by the first and under a lord of another polity by the
2451
+ second, and both pages say so.
2452
+
2453
+ The website reads `domains` across the package and every fetched index and
2454
+ writes the affiliation's page a `holdings` list — every place its `domains`
2455
+ names, each entry `{ title, url, type, subType }`, sorted by `subType` then
2456
+ `title`, and absent when `domains` names nothing. Every place it names carries
2457
+ the affiliation back in `held_by`. Both are derived; a note that writes one has
2458
+ it replaced.
2459
+
2443
2460
  If `sohl` is present, this becomes an `affiliation` item.
2444
2461
 
2445
2462
  | shared source | → sohl | → hm3 |
@@ -3087,9 +3104,38 @@ three maps, and none of those maps needs to know it is a keep.
3087
3104
  | `lore` | `WikiLink[]` | Lore concerning this place — its peoples, its law, its calendar, its history |
3088
3105
  | `parents` | `WikiLink[]` | Enclosing places within which this place is located |
3089
3106
  | `population` | `number` | Approximate population (precision 2 significant digits) |
3107
+ | `market` | `number` | Market class, 1 to 6 — what trade a settlement supports |
3090
3108
  | `borders` | `{ to, bearing }[]` | Places sharing a frontier with this one, and where each lies from here |
3091
3109
  | `routes` | `{ to, bearing, mode, days, terrain?, leagues? }[]` | Journeys from this place's centre to another place |
3092
3110
 
3111
+ #### What a settlement's market supports
3112
+
3113
+ `population` states how many people a settlement holds; `market` states what a
3114
+ traveller can buy there. Those are different questions — two settlements of
3115
+ eight hundred are a different proposition when one holds a weekly market and
3116
+ the other a chartered fair with a moneylender — so a note states both, either,
3117
+ or neither.
3118
+
3119
+ The value is a **class**, not a measurement. The numbers order six steps and do
3120
+ nothing else: a `5` is not a market two fifths larger than a `3`, it is a city
3121
+ rather than a town. `content-build lint` refuses a value off the scale instead
3122
+ of rounding it onto one, so a reader can trust that a number means the same
3123
+ thing on every page.
3124
+
3125
+ | `market` value | the settlement is | what can be had there |
3126
+ | -------------- | ----------------- | ------------------------------------------------------------------------------------------------- |
3127
+ | `1` | a hamlet | no market — what neighbours trade among themselves |
3128
+ | `2` | a village | a weekly market: staples, a smith, what a household cannot make |
3129
+ | `3` | a town | a regular market, most common goods, several trades working full time |
3130
+ | `4` | a market town | a chartered fair, goods carried from a week away, a moneylender |
3131
+ | `5` | a city | anything ordinary, in quantity; foreign goods; a guild structure |
3132
+ | `6` | a great city | the rare and the imported as a matter of course; banking; the market from which other markets buy |
3133
+
3134
+ The key is meaningful on a settlement, and the lint checks no `subType`
3135
+ condition — as it checks none for `population`. Which places a fact says
3136
+ something about is the author's judgement, and the `data:` container is closed
3137
+ per type rather than per subType.
3138
+
3093
3139
  #### Where a place is, in relation to other places
3094
3140
 
3095
3141
  `parents` states what a place is **within**. Two more properties state what
@@ -3187,6 +3233,31 @@ closed set, and each finding is located at the entry that states it.
3187
3233
  | `river` | boat |
3188
3234
  | `lake` | boat |
3189
3235
 
3236
+ #### What lies within a place, and who holds it
3237
+
3238
+ `parents` states what a place is within; an affiliation's `domains` states
3239
+ what it holds. The website reads both across the package and every fetched
3240
+ index and writes each place's page two lists, each entry
3241
+ `{ title, url, type, subType }` and each absent when empty:
3242
+
3243
+ - `contains` — every place whose `parents` names this one, sorted by
3244
+ `subType` then `title`. A region lists its settlements, sites, structures
3245
+ and features; a settlement lists the structures within it.
3246
+ - `held_by` — every affiliation whose `domains` names this one, sorted by
3247
+ `title`.
3248
+
3249
+ Both are derived; a note that writes one has it replaced. **`domains` is
3250
+ never expanded**: a region held by a polity lists the polity in `held_by`, and
3251
+ a settlement within that region lists only the house or order whose `domains`
3252
+ names it — its lord's lord is reached through `affiliation.parents`.
3253
+
3254
+ **A settlement, a site or a structure is held by someone.** One of those that
3255
+ no affiliation's `domains` names — in this package or in a fetched index — is
3256
+ a warning from `content-build lint`, `unheld land`, located at the note's
3257
+ `type:` line, so a gap in tenure shows while a note is written. A `world`, a
3258
+ `region` and a `feature` are exempt: a region is held through its polity's
3259
+ `domains`, and a river by nobody.
3260
+
3190
3261
  **A place declares only what is true of ground.** Four properties were removed
3191
3262
  because they were true of something else, and each removal has a home to go to.
3192
3263
 
@@ -1424,6 +1424,8 @@ export function lintNote(
1424
1424
  if (typeof field.check !== "function") continue;
1425
1425
  findings.push(...field.check(note, { index }));
1426
1426
  }
1427
+ // A check the type declares of the whole note, with the same index.
1428
+ if (typeof entry.check === "function") findings.push(...entry.check(note, { index }));
1427
1429
  }
1428
1430
 
1429
1431
  const fields = authoredFields(schema);