@heroiclands/package-build 22.24.0 → 22.26.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.
@@ -35,16 +35,15 @@
35
35
  * package publishes is listed within a local region, and a house another
36
36
  * package publishes is named on the local manor it holds.
37
37
  *
38
- * **A place's tenure is checked.** A settlement, a site or a structure that no
39
- * affiliation's `domains` names is land nobody holds — a gap in tenure the
40
- * lint reports as a warning at the note's `type:` line. A region is held
41
- * through its polity's `domains` and a feature by nobody, so both are exempt.
38
+ * **Government is stated separately from tenure.** A place with positive
39
+ * `data.population` and no authored `data.government` receives an advisory.
40
+ * Explicit null means complete anarchy. Holdings never supply a government.
42
41
  *
43
42
  * @module
44
43
  */
45
44
 
46
45
  import { isAddressTuple } from "./address.mjs";
47
- import { positionInFrontmatter } from "./diagnostics.mjs";
46
+ import { positionOfFrontmatterPath } from "./diagnostics.mjs";
48
47
  import { parseAddress } from "./address.mjs";
49
48
  import { readCanonicalKey } from "./content-address.mjs";
50
49
 
@@ -66,9 +65,9 @@ const PLACE_TYPES = Object.freeze(new Set(["place"]));
66
65
  export const HOLDINGS_KEYS = Object.freeze(["contains", "held_by", "holdings"]);
67
66
 
68
67
  /**
69
- * The place subTypes tenure is checked on. A region is held through its
70
- * polity's `domains`, a world by nobody, a feature by nobody — a river has no
71
- * lord — so the rest are the kinds of place a body holds directly.
68
+ * Legacy subtypes of the retired tenure advisory. Government advisories
69
+ * apply to every place subtype when its authored population is positive.
70
+ * @deprecated This list no longer gates any advisory.
72
71
  *
73
72
  * @type {readonly string[]}
74
73
  */
@@ -373,71 +372,44 @@ export function holdingsPages(nodes) {
373
372
  /* --------------------------------------------------------------------- */
374
373
 
375
374
  /**
376
- * Every shortcode some affiliation's `domains` names, computed once per link
377
- * index. The lint asks the question once per place note, and the answer is a
378
- * fact about the whole corpus — local notes and every fetched entry — so it
379
- * is read off the index the first time and kept beside it.
380
- *
381
- * @type {WeakMap<object, Set<string>>}
382
- */
383
- const HELD = new WeakMap();
384
-
385
- /**
386
- * The shortcodes every affiliation's `domains` names, across the link index.
387
- *
388
- * @param {object} index - The link index.
389
- * @returns {Set<string>} The held shortcodes, lower case.
375
+ * Advise when a place with an authored positive population omits government.
376
+ * Missing, null or zero population establishes no inhabited population.
377
+ * Property presence distinguishes omitted government from explicit null,
378
+ * which means complete anarchy. Non-null values are validated by the declared
379
+ * affiliation reference field. Neither geographic containment nor tenure
380
+ * supplies this fact, and the rule is independent of place subtype.
381
+ *
382
+ * @param {object} note - The note with parsed frontmatter and raw source.
383
+ * @returns {object[]} Located warning findings.
390
384
  */
391
- function heldShortcodes(index) {
392
- let held = HELD.get(index);
393
- if (held) return held;
394
- held = new Set();
395
- for (const note of index.notes ?? []) {
396
- if (String(note.type ?? note.fm?.type ?? "") !== "affiliation") continue;
397
- const data = note.fm?.data;
398
- const domains = data && typeof data === "object" ? data.domains : undefined;
399
- for (const shortcode of shortcodesOf(domains)) held.add(shortcode);
400
- }
401
- for (const entry of index.foreign?.index?.values?.() ?? []) {
402
- if (String(entry?.type ?? "") !== "affiliation") continue;
403
- for (const shortcode of shortcodesOf(entry.domains)) held.add(shortcode);
404
- }
405
- HELD.set(index, held);
406
- return held;
407
- }
408
-
409
- /**
410
- * Check a place note's tenure: a settlement, a site or a structure that no
411
- * affiliation's `domains` names is unheld land, reported as a warning at the
412
- * note's `type:` line.
413
- *
414
- * Declared on the `place` vocabulary as its type-level check, so the lint
415
- * runs it beside the field checks with the same index. Without an index the
416
- * question cannot be asked and nothing is reported.
417
- *
418
- * @param {object} note - The note, as the link index hands it over.
419
- * @param {object} [opts]
420
- * @param {object} [opts.index] - The link index, holding every affiliation.
421
- * @returns {object[]} Findings.
422
- */
423
- export function checkHeld(note, { index } = {}) {
424
- if (!index) return [];
385
+ export function checkGovernment(note) {
425
386
  const fm = note.fm ?? {};
426
- if (String(fm.type ?? "") !== "place") return [];
427
- const subType = String(fm.subType ?? "");
428
- if (!HELD_SUBTYPES.includes(subType)) return [];
429
- const shortcode = String(fm.shortcode ?? "").toLowerCase();
430
- if (!shortcode) return [];
431
- if (heldShortcodes(index).has(shortcode)) return [];
387
+ if (fm.type !== "place") return [];
388
+ const data = fm.data;
389
+ if (!data || typeof data !== "object" || Array.isArray(data)) return [];
390
+ const population = data.population;
391
+ if (typeof population !== "number" && typeof population !== "string") return [];
392
+ const count = Number(population);
393
+ if (!Number.isFinite(count) || count <= 0 || Object.hasOwn(data, "government")) return [];
432
394
  return [
433
395
  {
434
396
  file: note.file,
435
- ...positionInFrontmatter(note.raw ?? "", "type", undefined, { topLevel: true }),
397
+ ...positionOfFrontmatterPath(note.raw ?? "", ["data", "population"], { key: true }),
436
398
  severity: "warning",
437
399
  message:
438
- `unheld land: no affiliation's \`domains\` names "${shortcode}", so nothing ` +
439
- `says who holds this ${subType}; name it in the \`domains\` of the house, ` +
440
- `order or polity that does`,
400
+ `missing government: place "${fm.shortcode ?? ""}" has positive population ` +
401
+ "but no `data.government`; name an affiliation or write null for complete anarchy",
441
402
  },
442
403
  ];
443
404
  }
405
+
406
+ /**
407
+ * The historical API name for the government advisory. Tenure is no longer checked.
408
+ * @deprecated Use checkGovernment.
409
+ * @param {object} note - The place note.
410
+ * @param {object} [_opts] - Historical options, no longer needed.
411
+ * @returns {object[]} Government advisory findings.
412
+ */
413
+ export function checkHeld(note, _opts = {}) {
414
+ return checkGovernment(note);
415
+ }
package/engine/index.mjs CHANGED
@@ -99,7 +99,7 @@ export * as stubLint from "./stub-lint.mjs";
99
99
  /** What a place is next to and reachable from: the closed sets, and the checks. */
100
100
  export * as placeRelations from "./place-relations.mjs";
101
101
 
102
- /** What lies within a place, who holds it, and what an affiliation holds — the page lists, and the tenure check. */
102
+ /** What lies within a place, who holds it, and what an affiliation holds — the page lists, and the government advisory. */
103
103
  export * as holdings from "./holdings.mjs";
104
104
  export * as populations from "./populations.mjs";
105
105
 
@@ -427,6 +427,7 @@ export function isUnsetSentinel(value) {
427
427
  /**
428
428
  * Whether a value is worth a row.
429
429
  *
430
+ * A field declaring `nullText` handles explicit null before this general test.
430
431
  * Rule 4: an absent field is absent. `null`, `""` and `[]` are how the corpus
431
432
  * writes "nobody filled this in" — a note that declares every key of its type
432
433
  * and leaves most of them empty is the ordinary shape, not the exception — and
@@ -943,6 +944,15 @@ function noteBox(
943
944
  if (structured.length) shown.add(field.name);
944
945
  continue;
945
946
  }
947
+ if (raw === null && field.nullText) {
948
+ rows.push({
949
+ label: overlay.label ?? humanizeFieldName(field.name),
950
+ kind: "text",
951
+ value: field.nullText,
952
+ });
953
+ shown.add(field.name);
954
+ continue;
955
+ }
946
956
  if (!hasValue(raw)) continue;
947
957
 
948
958
  if (overlay.group) {
@@ -435,6 +435,9 @@ export function loadForeignIndexes(config, localPackages, bases = PACKAGE_BASE)
435
435
  parents: record.data?.parents ?? undefined,
436
436
  domains: record.data?.domains ?? undefined,
437
437
  population: record.data?.population ?? undefined,
438
+ ...(Object.hasOwn(record.data ?? {}, "government") ?
439
+ { government: record.data.government }
440
+ : {}),
438
441
  package: pkg,
439
442
  });
440
443
  const entry = index.get(key);
@@ -105,14 +105,14 @@ import { checkSocialTies } from "./social-ties.mjs";
105
105
  import { checkStandings } from "./standings.mjs";
106
106
  import { STANDING_BODY_TYPES } from "./standing-terms.mjs";
107
107
  import { checkDatedOffices } from "./office-holders.mjs";
108
- import { checkRankLadder } from "./rank-ladder.mjs";
108
+ import { checkAffiliationRankFloor, checkRankLadder } from "./rank-ladder.mjs";
109
109
  import { checkCalendarChoice } from "./calendar-choice.mjs";
110
110
  import { checkLoreEvents } from "./lore-events.mjs";
111
111
  import { checkCultureChoice } from "./culture-choice.mjs";
112
112
  import { SOCIAL_TIES, SOCIAL_TIE_TARGET_TYPES } from "./social-tie-terms.mjs";
113
113
  import { parseNoteDate } from "./note-dates.mjs";
114
114
  import { reckoningContext } from "./reckoning-markers.mjs";
115
- import { checkHeld } from "./holdings.mjs";
115
+ import { checkGovernment } from "./holdings.mjs";
116
116
  import {
117
117
  checkBeingMeasurement,
118
118
  parseBeingHeight,
@@ -153,6 +153,7 @@ import { positionOfFrontmatterPath } from "./diagnostics.mjs";
153
153
  * so each takes a row labelled and anchored by the post's own name.
154
154
  * @property {"subType"} [keySelector] - An alternative `subType:<skill-subtype>` key.
155
155
  * @property {readonly string[]} [accepts] - Allowed Address types, separate from `ref`.
156
+ * @property {string} [nullText] - Infobox text for an explicitly authored null with a declared meaning.
156
157
  * @property {string} [shape] - Human-readable shape, for a finding and for
157
158
  * documentation.
158
159
  * @property {string} [entryShape] - For a `scalar-or-map` field, what one
@@ -512,8 +513,8 @@ const CHARGES = Object.freeze([
512
513
  /**
513
514
  * Everything `place` asks of a whole note.
514
515
  *
515
- * Two questions about one subject, and they are independent: who holds this
516
- * land, and — where the note is a body rather than somewhere within one — what
516
+ * Two questions about one subject, and they are independent: who governs this
517
+ * inhabited place, and — where the note is a body rather than somewhere within one — what
517
518
  * the body states about itself. A type declares one whole-note check, so the
518
519
  * two are composed here rather than either being folded into the other.
519
520
  *
@@ -522,7 +523,7 @@ const CHARGES = Object.freeze([
522
523
  * @returns {object[]} Findings from both.
523
524
  */
524
525
  function checkPlace(note, opts) {
525
- return [...checkHeld(note, opts), ...checkWorldFacts(note, opts)];
526
+ return [...checkGovernment(note), ...checkWorldFacts(note, opts)];
526
527
  }
527
528
 
528
529
  /** Validate a being's authored date using the shared note-date grammar. */
@@ -1014,6 +1015,7 @@ export const NOTE_VOCABULARY = Object.freeze({
1014
1015
 
1015
1016
  affiliation: Object.freeze({
1016
1017
  stubbable: true,
1018
+ check: checkAffiliationRankFloor,
1017
1019
  subTypes: Object.freeze([
1018
1020
  "guild",
1019
1021
  "order",
@@ -1562,6 +1564,17 @@ export const NOTE_VOCABULARY = Object.freeze({
1562
1564
  check: checkPopulation,
1563
1565
  describe: "Approximate population, to two significant digits.",
1564
1566
  },
1567
+ {
1568
+ name: "government",
1569
+ ...LINK,
1570
+ shape: "an affiliation Address or null",
1571
+ ref: "affiliation",
1572
+ accepts: ["affiliation"],
1573
+ nullText: "Complete anarchy",
1574
+ describe:
1575
+ "The governing affiliation; explicit null means complete anarchy. " +
1576
+ "A positive population with no government key produces an advisory.",
1577
+ },
1565
1578
  {
1566
1579
  name: "market",
1567
1580
  ...NUM,
@@ -744,7 +744,7 @@ export async function buildPdf({
744
744
  * @param {number} columns - The entry's page columns.
745
745
  * @returns {string} Typst markup.
746
746
  */
747
- const figureCounts = { code: 0, table: 0, figure: 0, map: 0, prose: 0 };
747
+ const figureCounts = { code: 0, table: 0, figure: 0, map: 0, poem: 0, prose: 0 };
748
748
  // The role lookup a `:::figure` fence's `map` counter reaches through —
749
749
  // see `engine/content-figures.mjs`'s `resolveRole`. Addressed form, for a
750
750
  // page's own scan below, which runs before its embeds are rewritten into
@@ -88,6 +88,7 @@ export const BOOK_IMAGE_WIDTHS = Object.freeze({
88
88
  import { slugify } from "./content-slug.mjs";
89
89
  import { scanFigures } from "./content-figures.mjs";
90
90
  import { BLOCK_CONTAINERS, scanBlocks, BLOCK_NAMES } from "./content-blocks.mjs";
91
+ import { inlineVerse, poetryStanzas } from "./content-poetry.mjs";
91
92
  import { matchAllOutsideCode, replaceOutsideCode } from "./code-fences.mjs";
92
93
  import { HTML_TAG, htmlMessage } from "./content-html.mjs";
93
94
  import { EXPRESSION } from "./markdown-expressions.mjs";
@@ -526,7 +527,16 @@ export function markdownToTypst(markdown, opts = {}) {
526
527
  }
527
528
  }
528
529
  if (!opts.insideAdmonition) {
529
- const { blocks } = scanBlocks(source);
530
+ const figuresHoldingPoetry = scanFigures(source).figures;
531
+ const blocks = scanBlocks(source).blocks.filter(
532
+ (block) =>
533
+ !figuresHoldingPoetry.some(
534
+ (figure) =>
535
+ block.name === "poetry" &&
536
+ block.start >= figure.bodyStart &&
537
+ block.end < figure.bodyEnd,
538
+ ),
539
+ );
530
540
  if (blocks.length) {
531
541
  const lines = source.split("\n");
532
542
  const output = [];
@@ -538,6 +548,24 @@ export function markdownToTypst(markdown, opts = {}) {
538
548
  insideAdmonition: true,
539
549
  }),
540
550
  );
551
+ if (block.name === "poetry") {
552
+ const content = poetryStanzas(block.body)
553
+ .map((stanza) =>
554
+ stanza
555
+ .map(({ text, level }) => {
556
+ const verse = markdownToTypst(inlineVerse(text), {
557
+ ...sharedOptions,
558
+ insideAdmonition: true,
559
+ });
560
+ return `${level ? `#h(${level * 1.25}em)` : ""}${verse}`;
561
+ })
562
+ .join(" \\\n"),
563
+ )
564
+ .join("\n\n");
565
+ output.push(`\n#block(width: 100%)[${content}]\n`);
566
+ cursor = block.end + 1;
567
+ continue;
568
+ }
541
569
  const { color, background, symbol } = BLOCK_PRINT[block.name];
542
570
  // The title is inline markdown, rendered the way every other
543
571
  // phrase in the book is. Set as it was written it would be read
@@ -601,7 +629,9 @@ export function markdownToTypst(markdown, opts = {}) {
601
629
  { ...caption, imagesRemaining: { n: countImages(block) } }
602
630
  : caption;
603
631
  const segments = [
604
- renderMarkdownSegment(block, md, { ...ctx, caption: captioned }, definitions),
632
+ caption.kind === "poem" ?
633
+ markdownToTypst(block, { ...sharedOptions, captions: undefined })
634
+ : renderMarkdownSegment(block, md, { ...ctx, caption: captioned }, definitions),
605
635
  ];
606
636
  if (caption.kind !== "table" && caption.kind !== "figure" && caption.kind !== "map") {
607
637
  segments.push(
@@ -50,6 +50,38 @@ const RUNG_KEYS = Object.freeze(["level", "title", "description", "lore"]);
50
50
  /** The keys a rung must carry a value for. */
51
51
  const REQUIRED_TEXT = Object.freeze(["title", "description"]);
52
52
 
53
+ /** Every affiliation has an ordinary standing that a being can name as rank 1. */
54
+ export function checkAffiliationRankFloor(note) {
55
+ if (note.fm?.type !== "affiliation") return [];
56
+ const governance = note.fm?.data?.governance;
57
+ const ranks = governance?.ranks;
58
+ // A non-list already has a shape finding. Do not repeat it here.
59
+ if (ranks !== undefined && ranks !== null && !Array.isArray(ranks)) return [];
60
+ if (
61
+ Array.isArray(ranks) &&
62
+ ranks.some((rung) => integer(rung?.level) && Number(rung.level) === 1)
63
+ )
64
+ return [];
65
+
66
+ const path = Array.isArray(ranks) ? ["data", "governance", "ranks"] : ["data", "governance"];
67
+ const atRankOrGovernance = positionOfFrontmatterPath(note.raw ?? "", path, { key: true });
68
+ const position =
69
+ atRankOrGovernance.line !== undefined ?
70
+ atRankOrGovernance
71
+ : positionOfFrontmatterPath(note.raw ?? "", ["data"], { key: true });
72
+ return [
73
+ {
74
+ file: note.file,
75
+ ...position,
76
+ severity: "error",
77
+ message:
78
+ !Array.isArray(ranks) || ranks.length === 0 ?
79
+ "affiliation needs data.governance.ranks with a level 1 standing"
80
+ : "affiliation data.governance.ranks needs a level 1 standing",
81
+ },
82
+ ];
83
+ }
84
+
53
85
  /**
54
86
  * Whether a value is a YAML mapping rather than a list or a scalar.
55
87
  *
@@ -19,7 +19,7 @@
19
19
  # used as given and never looked for here.
20
20
  #
21
21
  # Keeping it in step is not left to hope: `npm run lint:banners`
22
- # (`utils/check-banners.mjs`, part of `npm run lint`) fetches every name below
22
+ # (`ci/check-banners.mjs`, part of `npm run lint`) fetches every name below
23
23
  # from a real asset host and fails on any non-200. Add a name here only after
24
24
  # the file is published, or the check will say so.
25
25
  #
@@ -1373,6 +1373,35 @@ a.tag:hover {
1373
1373
  background: var(--bg-card);
1374
1374
  }
1375
1375
 
1376
+ /* Verse lines are hard breaks in the markup; paragraphs mark stanzas. */
1377
+ .single-content .poetry {
1378
+ margin: 1.5rem 0;
1379
+ line-height: 1.7;
1380
+ }
1381
+
1382
+ .single-content .poetry p {
1383
+ margin: 0;
1384
+ }
1385
+
1386
+ .single-content .poetry p + p {
1387
+ margin-top: 1em;
1388
+ }
1389
+
1390
+ .single-content .poetry .poetry-line {
1391
+ display: inline-block;
1392
+ margin-inline-start: var(--poetry-indent, 0em);
1393
+ white-space: pre-wrap;
1394
+ }
1395
+
1396
+ .single-content .poetry .poetry-line.i1 { --poetry-indent: 1.25em; }
1397
+ .single-content .poetry .poetry-line.i2 { --poetry-indent: 2.5em; }
1398
+ .single-content .poetry .poetry-line.i3 { --poetry-indent: 3.75em; }
1399
+ .single-content .poetry .poetry-line.i4 { --poetry-indent: 5em; }
1400
+ .single-content .poetry .poetry-line.i5 { --poetry-indent: 6.25em; }
1401
+ .single-content .poetry .poetry-line.i6 { --poetry-indent: 7.5em; }
1402
+ .single-content .poetry .poetry-line.i7 { --poetry-indent: 8.75em; }
1403
+ .single-content .poetry .poetry-line.i8 { --poetry-indent: 10em; }
1404
+
1376
1405
  /* Ornamental horizontal rule: thin line flanking a centered gold
1377
1406
  diamond. Uses gradients on ::before/::after to keep the markup
1378
1407
  clean (authors just write `---` in markdown). */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heroiclands/package-build",
3
- "version": "22.24.0",
3
+ "version": "22.26.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",
@@ -187,10 +187,12 @@
187
187
  "build:asset-index": "node ci/emit-asset-index.mjs",
188
188
  "format": "prettier --write .",
189
189
  "format:check": "prettier --check .",
190
- "lint": "run-p --aggregate-output -c --max-parallel 4 format:check lint:markdown lint:yaml lint:labels lint:content-format",
190
+ "lint": "run-p --aggregate-output -c --max-parallel 4 format:check lint:markdown lint:yaml lint:labels lint:image-roles lint:banners lint:content-format",
191
191
  "lint:markdown": "node bin/package-build.mjs markdown",
192
192
  "lint:yaml": "node bin/package-build.mjs yaml",
193
193
  "lint:labels": "node bin/package-build.mjs labels check",
194
+ "lint:image-roles": "vitest run tests/theme-image-roles.test.ts",
195
+ "lint:banners": "node ci/check-banners.mjs",
194
196
  "lint:markdown:fix": "node bin/package-build.mjs markdown --fix",
195
197
  "lint:content-format": "npm run lint:content-format:schema && npm run lint:content-format:fields && node ci/content-format-reference.mjs --check",
196
198
  "lint:content-format:schema": "node bin/package-build.mjs content-format schema --schema sohl=tests/fixtures/content-format/schema-sohl.json --schema hm3=tests/fixtures/content-format/schema-hm3.json",
@@ -29,7 +29,7 @@ export function checkCalendarDateFormats(note: any): {
29
29
  /**
30
30
  * Check a `place` note's world facts.
31
31
  *
32
- * Declared on `place` beside its tenure check, so the lint runs both with the
32
+ * Declared on `place` beside its government advisory, so the lint runs both with the
33
33
  * same index. Four rules, and each is a fact about the setting rather than
34
34
  * about the file:
35
35
  *
@@ -87,6 +87,7 @@ export const FIGURE_NAMES: Readonly<{
87
87
  table: "Table";
88
88
  figure: "Figure";
89
89
  map: "Map";
90
+ poem: "Poem";
90
91
  prose: "Prose";
91
92
  }>;
92
93
  /**
@@ -0,0 +1,19 @@
1
+ /** The authored verse lines, excluding stanza separators. */
2
+ export function verseLines(body: any): string[];
3
+ /**
4
+ * The least-indented verse is level zero. Every two additional leading spaces
5
+ * add one level; a leftover space is discarded and the level stops at eight.
6
+ * Blank lines divide stanzas rather than contributing to the baseline.
7
+ */
8
+ export function poetryStanzas(body: any): {
9
+ text: string;
10
+ level: number;
11
+ }[][];
12
+ /** A verse is inline Markdown even when it starts with block syntax. */
13
+ export function inlineVerse(text: any): string;
14
+ /**
15
+ * Keep stanzas as Markdown paragraphs and make every verse line a hard break.
16
+ * The surrounding renderer still sees the entire note, so links and footnote
17
+ * definitions remain in scope. The span carries the indent for each line.
18
+ */
19
+ export function poetryMarkdown(body: any): string;
@@ -53,22 +53,25 @@ export function foreignHoldingsNodes(foreignIndex: Map<string, object> | undefin
53
53
  */
54
54
  export function holdingsPages(nodes: Iterable<HoldingsNode | null>): Map<string, Holdings>;
55
55
  /**
56
- * Check a place note's tenure: a settlement, a site or a structure that no
57
- * affiliation's `domains` names is unheld land, reported as a warning at the
58
- * note's `type:` line.
56
+ * Advise when a place with an authored positive population omits government.
57
+ * Missing, null or zero population establishes no inhabited population.
58
+ * Property presence distinguishes omitted government from explicit null,
59
+ * which means complete anarchy. Non-null values are validated by the declared
60
+ * affiliation reference field. Neither geographic containment nor tenure
61
+ * supplies this fact, and the rule is independent of place subtype.
59
62
  *
60
- * Declared on the `place` vocabulary as its type-level check, so the lint
61
- * runs it beside the field checks with the same index. Without an index the
62
- * question cannot be asked and nothing is reported.
63
- *
64
- * @param {object} note - The note, as the link index hands it over.
65
- * @param {object} [opts]
66
- * @param {object} [opts.index] - The link index, holding every affiliation.
67
- * @returns {object[]} Findings.
63
+ * @param {object} note - The note with parsed frontmatter and raw source.
64
+ * @returns {object[]} Located warning findings.
65
+ */
66
+ export function checkGovernment(note: object): object[];
67
+ /**
68
+ * The historical API name for the government advisory. Tenure is no longer checked.
69
+ * @deprecated Use checkGovernment.
70
+ * @param {object} note - The place note.
71
+ * @param {object} [_opts] - Historical options, no longer needed.
72
+ * @returns {object[]} Government advisory findings.
68
73
  */
69
- export function checkHeld(note: object, { index }?: {
70
- index?: object | undefined;
71
- }): object[];
74
+ export function checkHeld(note: object, _opts?: object): object[];
72
75
  /**
73
76
  * The keys this module writes, and which a note cannot author: `contains`
74
77
  * and `held_by` on a place, `holdings` on an affiliation.
@@ -77,9 +80,9 @@ export function checkHeld(note: object, { index }?: {
77
80
  */
78
81
  export const HOLDINGS_KEYS: readonly string[];
79
82
  /**
80
- * The place subTypes tenure is checked on. A region is held through its
81
- * polity's `domains`, a world by nobody, a feature by nobody — a river has no
82
- * lord — so the rest are the kinds of place a body holds directly.
83
+ * Legacy subtypes of the retired tenure advisory. Government advisories
84
+ * apply to every place subtype when its authored population is positive.
85
+ * @deprecated This list no longer gates any advisory.
83
86
  *
84
87
  * @type {readonly string[]}
85
88
  */
@@ -27,6 +27,7 @@ export function isUnsetSentinel(value: unknown): boolean;
27
27
  /**
28
28
  * Whether a value is worth a row.
29
29
  *
30
+ * A field declaring `nullText` handles explicit null before this general test.
30
31
  * Rule 4: an absent field is absent. `null`, `""` and `[]` are how the corpus
31
32
  * writes "nobody filled this in" — a note that declares every key of its type
32
33
  * and leaves most of them empty is the ordinary shape, not the exception — and
@@ -380,6 +380,10 @@ export type DataFieldSpec = {
380
380
  * - Allowed Address types, separate from `ref`.
381
381
  */
382
382
  accepts?: readonly string[] | undefined;
383
+ /**
384
+ * - Infobox text for an explicitly authored null with a declared meaning.
385
+ */
386
+ nullText?: string | undefined;
383
387
  /**
384
388
  * - Human-readable shape, for a finding and for
385
389
  * documentation.
@@ -1,3 +1,11 @@
1
+ /** Every affiliation has an ordinary standing that a being can name as rank 1. */
2
+ export function checkAffiliationRankFloor(note: any): {
3
+ severity: string;
4
+ message: string;
5
+ line?: number;
6
+ column?: number;
7
+ file: any;
8
+ }[];
1
9
  /**
2
10
  * Check one affiliation's `data.governance.ranks` against the rung contract.
3
11
  *