@heroiclands/package-build 22.8.0 → 22.9.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 CHANGED
@@ -1,5 +1,31 @@
1
1
  # @heroiclands/package-build
2
2
 
3
+ ## 22.9.1
4
+
5
+ ### Patch Changes
6
+
7
+ **A being's age can be computed rather than written.** Beside a dated `born` and no authored `age`, the age follows from the birth date and the package's declared present — a birthday later in the year counts one less than plain subtraction would.
8
+
9
+ **The present is declared on the world's own `place` note**, in `data.present`. A package that states none computes no age and says nothing about it.
10
+
11
+ **`content-build lint` warns when an authored `age` disagrees** with what a dated `born` computes to, naming both values; it stays silent beside `born: unknown` or an absent `born`.
12
+
13
+ **An estimated age keeps its `~` in the compiled document**, with the plain number carried beside it as `ageYears`.
14
+
15
+ ## 22.9.0
16
+
17
+ ### Minor Changes
18
+
19
+ **Calendars are content, not configuration.** The `calendar:` block retires from `package-build.config.yaml`, and a configuration that still declares it is refused by name.
20
+
21
+ **A calendar is a note.** A `lore` note with `subType: calendar` states the months it keeps, the weekdays it names, the seasons it marks and the year-counts kept in it, and a date names one of those by writing `<calendar shortcode>.<era shortcode>`. Days that belong to no month are a short month like any other, sitting in the list where they fall in the year.
22
+
23
+ **The world states its own year.** How long the year is, how the day divides and how the moon moves are written once on the world's own `place` note and on its moon's, so no calendar restates them — and a calendar whose months do not add up to that year is refused, naming the year rather than the arithmetic.
24
+
25
+ **A body is a place.** `place` gains `celestial`, for something observed from a world rather than located on one.
26
+
27
+ **A written day is bounded by the month it names.** A five-day month accepts five days and a thirty-day month accepts thirty, in whatever order a calendar keeps them; a package that names no calendar bounds nothing.
28
+
3
29
  ## 22.8.0
4
30
 
5
31
  ### Minor Changes
@@ -76,7 +76,6 @@ import { EMPTY_ICON_REGISTRY, checkIconRegistry } from "./engine/content-icons.m
76
76
  import { MAP_TYPES, NO_PACK, PACK_BY_TYPE } from "./engine/ids.mjs";
77
77
  import { ACTOR_TYPES } from "./engine/subtype-registry.mjs";
78
78
  import { NOTE_VOCABULARY } from "./engine/note-vocabulary.mjs";
79
- import { UNKNOWN_DATE, parseNoteDate } from "./engine/note-dates.mjs";
80
79
 
81
80
  /**
82
81
  * What kind of package this is.
@@ -726,10 +725,6 @@ export function publishesContentPages(config) {
726
725
  * @property {readonly string[]} packDirectories Derived: every pack directory
727
726
  * the build produces, in compile order —
728
727
  * each pack followed by its companions.
729
- * @property {Readonly<{months: number|null, monthDays: number|null, present: string|null}>} calendar
730
- * How this package's year is divided, and
731
- * the date it calls the present. Every
732
- * value `null` when none is declared.
733
728
  * @property {Readonly<PackageBuildSection>} packageBuild Passed through
734
729
  * frozen, uninterpreted. `{}` when absent.
735
730
  * @property {Readonly<DocsSpec>} docs Frozen; `{}` when absent.
@@ -759,13 +754,11 @@ const CONFIG_KEYS = [
759
754
  "relationships",
760
755
  "systems",
761
756
  "requiresSystem",
762
- "calendar",
763
757
  "packageBuild",
764
758
  "publish",
765
759
  "changelog",
766
760
  ];
767
761
  const SYSTEM_KEYS = ["manifest", "compatibility"];
768
- const CALENDAR_KEYS = ["months", "monthDays", "present"];
769
762
  const COMPATIBILITY_KEYS = ["minimum", "verified"];
770
763
  const DOCS_KEYS = ["itemFields"];
771
764
  const CHANGELOG_KEYS = ["labels"];
@@ -2077,76 +2070,6 @@ function normalizeRequiresSystem(value) {
2077
2070
  return requireNonEmptyString(value, "requiresSystem");
2078
2071
  }
2079
2072
 
2080
- /**
2081
- * How this package's year is divided, and the date it calls the present.
2082
- *
2083
- * **Nothing here is about which people count from what.** A reckoning is an
2084
- * era, declared by the affiliation that proclaimed it, and its epoch is the
2085
- * `start` written on that note — so the only things configuration states are
2086
- * the ones no note could.
2087
- *
2088
- * `months` and `monthDays` bound a written month and day. Both are optional,
2089
- * and an absent one is checked against nothing: a toolchain that assumed a
2090
- * Gregorian year here would refuse `667/2/30`, an ordinary date in a year of
2091
- * twelve thirty-day months. One subdivision to a package — an era moves where
2092
- * year 1 sits, it does not divide the year differently.
2093
- *
2094
- * `present` is the date the package computes an age against, written in the
2095
- * same grammar a note's dates use, so a package whose present is best said in
2096
- * an era says it that way. A package that states none computes no age, which
2097
- * is the right answer for a corpus that has no present.
2098
- *
2099
- * @param {unknown} value - The declared `calendar:` mapping.
2100
- * @returns {Readonly<{months: number|null, monthDays: number|null, present: string|null}>}
2101
- * Frozen; every value `null` when absent.
2102
- */
2103
- function normalizeCalendar(value) {
2104
- if (value === undefined || value === null) {
2105
- return Object.freeze({ months: null, monthDays: null, present: null });
2106
- }
2107
- if (!isPlainObject(value)) fail("calendar", "must be a mapping");
2108
- const input = /** @type {Record<string, unknown>} */ (value);
2109
- rejectUnknownKeys(input, CALENDAR_KEYS, "calendar.");
2110
-
2111
- const bound = (key) => {
2112
- const held = input[key];
2113
- if (held === undefined || held === null) return null;
2114
- if (!Number.isInteger(held) || /** @type {number} */ (held) < 1) {
2115
- fail(`calendar.${key}`, "must be a positive integer");
2116
- }
2117
- return /** @type {number} */ (held);
2118
- };
2119
- const months = bound("months");
2120
- const monthDays = bound("monthDays");
2121
-
2122
- // Read through the parser the notes use rather than a second grammar, and
2123
- // against this package's own bounds — a present outside the year it
2124
- // declares is the one date nothing downstream would report.
2125
- let present = null;
2126
- if (input.present !== undefined && input.present !== null) {
2127
- if (typeof input.present !== "string" && typeof input.present !== "number") {
2128
- fail("calendar.present", "must be a date written as a string");
2129
- }
2130
- if (String(input.present).trim() === UNKNOWN_DATE) {
2131
- fail(
2132
- "calendar.present",
2133
- `must be a date — a package that states no present computes no age, ` +
2134
- `so leave the key out rather than writing \`${UNKNOWN_DATE}\``,
2135
- );
2136
- }
2137
- const { date, findings } = parseNoteDate(input.present, {
2138
- calendar: { months, monthDays },
2139
- allowUnknown: false,
2140
- });
2141
- const refused = findings.find((finding) => finding.severity === "error");
2142
- if (refused)
2143
- fail("calendar.present", `is not a date this package can read — ${refused.message}`);
2144
- present = /** @type {{text: string}} */ (date).text;
2145
- }
2146
-
2147
- return Object.freeze({ months, monthDays, present });
2148
- }
2149
-
2150
2073
  /**
2151
2074
  * Validate the declared relationships.
2152
2075
  *
@@ -2878,7 +2801,6 @@ export function defineConfig(config) {
2878
2801
  relationships: normalizeRelationships(input.relationships),
2879
2802
  systems,
2880
2803
  requiresSystem,
2881
- calendar: normalizeCalendar(input.calendar),
2882
2804
  packageBuild: normalizePackageBuild(input.packageBuild),
2883
2805
  publish: normalizePublish(input.publish),
2884
2806
  changelog: normalizeChangelog(input.changelog),
package/docs/api.md CHANGED
@@ -606,18 +606,23 @@ The closed registry of system ids, and the `none` that stands for no system at a
606
606
 
607
607
  ### `engine.calendars`
608
608
 
609
- The axis every date normalises onto, and the one function that puts a reckoning on it. A reckoning is an **era** declared by the affiliation that proclaimed it, and its epoch is the `start` written on that note — nothing is registered in configuration. The conversion is piecewise on the sign, because the authored numbering has no year zero while the normalised value does: authored `-1` is `0`, and `-1` and `1` are adjacent.
610
-
611
- | Export | Signature | Returns | Use it when |
612
- | ------------------- | ------------------------------- | --------------------------------------------- | ------------------------------------------------------------------------------- |
613
- | `CANONICAL_EPOCH` | `const CANONICAL_EPOCH` | — | naming where the canonical axis's own year 1 sits |
614
- | `canonicalYear` | `canonicalYear(year, epoch?)` | `number` — the signed year on the axis | converting a year written in one reckoning to the number everything compares on |
615
- | `dateSortKey` | `dateSortKey(year, month, day)` | `number` | ordering mixed-precision dates against a single number |
616
- | `calendarStructure` | `calendarStructure(calendar)` | `{months, monthDays}` — `null` where unstated | reading the month bounds a package declares, before phrasing a finding |
609
+ The axis every date normalises onto, the arithmetic over a calendar's month list, and the moon. A reckoning is an **era** declared on the note about the calendar that counts those years, and its epoch is the `start` written on that row — nothing is registered in configuration. The year conversion is piecewise on the sign, because the authored numbering has no year zero while the normalised value does: authored `-1` is `0`, and `-1` and `1` are adjacent. `lunarPhase` takes a floored modulo for the same reason the conversion branches: a truncated one is right on one side of an epoch and wrong on the other.
610
+
611
+ | Export | Signature | Returns | Use it when |
612
+ | ------------------- | ---------------------------------- | -------------------------------------- | ------------------------------------------------------------------------------- |
613
+ | `CANONICAL_EPOCH` | `const CANONICAL_EPOCH` | — | naming where the canonical axis's own year 1 sits |
614
+ | `canonicalYear` | `canonicalYear(year, epoch?)` | `number` — the signed year on the axis | converting a year written in one reckoning to the number everything compares on |
615
+ | `dateSortKey` | `dateSortKey(year, month, day)` | `number` | ordering mixed-precision dates against a single number |
616
+ | `calendarStructure` | `calendarStructure(calendar)` | `{months}` — `null` where unstated | reading the months a calendar keeps, before phrasing a finding |
617
+ | `daysInMonth` | `daysInMonth(calendar, month)` | `number` — `null` where unstated | bounding a written day against the month it names |
618
+ | `daysInYear` | `daysInYear(months)` | `number` | asking what a month list claims the year is |
619
+ | `monthStarts` | `monthStarts(months)` | `number[]` | reading the same sum as a sequence, which is what an ordering error moves |
620
+ | `dayOfYear` | `dayOfYear(months, month, day)` | `number` | placing a written month and day in the year |
621
+ | `lunarPhase` | `lunarPhase(day, epochDay, cycle)` | `number` — `0` to `cycle - 1` | asking how far into its cycle a moon is, on either side of the epoch |
617
622
 
618
623
  ### `engine.noteDates`
619
624
 
620
- A date on a note is one authored string — `[~][-]YYYY[/MM[/DD]] [<affiliation shortcode>.<era shortcode>]` — and every field holding one parses through a single grammar into a single record. Print `text`, order on `sort`, do arithmetic on `canonicalYear`. A bare value sits on the canonical axis and carries `era: null`; one naming an era carries the qualifier and no `canonicalYear` or `sort` keys at all until the era is resolved against the corpus. The literal `unknown` parses to the same record with the year half empty, carrying a null `sort` and a null `canonicalYear`, so nothing can order it to one end of a list.
625
+ A date on a note is one authored string — `[~][-]YYYY[/MM[/DD]] [<calendar shortcode>.<era shortcode>]` — and every field holding one parses through a single grammar into a single record. Print `text`, order on `sort`, do arithmetic on `canonicalYear`. A bare value sits on the canonical axis and carries `era: null`; one naming an era carries the qualifier and no `canonicalYear` or `sort` keys at all until the era is resolved against the corpus. The literal `unknown` parses to the same record with the year half empty, carrying a null `sort` and a null `canonicalYear`, so nothing can order it to one end of a list.
621
626
 
622
627
  | Export | Signature | Returns | Use it when |
623
628
  | ----------------------- | ------------------------------- | -------------------------------------------------- | ------------------------------------------------------------- |
@@ -626,6 +631,37 @@ A date on a note is one authored string — `[~][-]YYYY[/MM[/DD]] [<affiliation
626
631
  | `ERA_QUALIFIER_PATTERN` | `const ERA_QUALIFIER_PATTERN` | — | checking that a string is an era qualifier a date could carry |
627
632
  | `parseNoteDate` | `parseNoteDate(value, options)` | `{date, findings}` — `date` is `null` when refused | reading a note's authored date, with every finding it earned |
628
633
 
634
+ ### `engine.calendarNotes`
635
+
636
+ A calendar is a `lore` note with `subType: calendar`, declaring the months it keeps, the weekdays it names, the seasons it marks and the eras it counts years in. How long the year is, how the day divides and how the moon moves are facts about the **world**, written on its `place` note and its moon's, and no calendar may restate them — which is why the months summing to the world's year is the only arithmetic a calendar note is held to. One definition is emitted per calendar, in array shape: Foundry core prunes the keys outside its schema, and Calendaria converts the arrays to its own keyed collections.
637
+
638
+ | Export | Signature | Returns | Use it when |
639
+ | -------------------- | ----------------------------------------------------- | ------------------------------------------ | --------------------------------------------------------------------------- |
640
+ | `CALENDAR_SUBTYPE` | `const CALENDAR_SUBTYPE` | — | naming the `lore` subType whose notes are calendars |
641
+ | `INVARIANT_SUBTYPES` | `const INVARIANT_SUBTYPES` | — | naming the `place` subTypes a world fact may be written on |
642
+ | `CALENDAR_FIELDS` | `const CALENDAR_FIELDS` | — | reading the closed list of what a calendar note may declare |
643
+ | `INVARIANT_FIELDS` | `const INVARIANT_FIELDS` | — | reading the closed list of what a body may declare about itself |
644
+ | `checkCalendarNote` | `checkCalendarNote(note, opts)` | `object[]` — findings | holding a `lore` note to the family and to the year the world keeps |
645
+ | `checkWorldFacts` | `checkWorldFacts(note, opts)` | `object[]` — findings | holding a `place` note's world facts to one body, one year and one moon |
646
+ | `worldInvariants` | `worldInvariants(index)` | `{year, present, moon, body, moonName, …}` | reading the year, the present and the moon a package's own notes state |
647
+ | `quarterDays` | `quarterDays(daysPerYear)` | `number[]` — four days of the year | asking which days the year turns on |
648
+ | `compileCalendar` | `compileCalendar({note, invariants, contentPackage})` | `object` — the definition | building the one object both a core consumer and Calendaria read |
649
+ | `compileCalendars` | `compileCalendars(index, opts)` | `{calendars, invariants}` | building every calendar a package declares, or none where it states no year |
650
+ | `calendariaEnvelope` | `calendariaEnvelope(definition, opts)` | `object` — the import envelope | wrapping a definition for a hand import through Calendaria's settings |
651
+
652
+ ### `engine.beingAge`
653
+
654
+ The middle state of a being's `age`: absent beside a dated `born`, it is computed against the package's declared present — the two dates compared, not the two years subtracted, so a birthday later in the year than the present counts one less. `applyComputedBeingAge` fills it in on the frontmatter object a pass already holds, never on the note; `checkBeingAge` is `being`'s `NOTE_VOCABULARY` check, raising a warning where an authored `age` disagrees with what a dated `born` computes to. An estimate's `~` is read the way a date's is: the authored string stays verbatim, and `ageYears` carries the magnitude beside it.
655
+
656
+ | Export | Signature | Returns | Use it when |
657
+ | -------------------------- | ---------------------------------------- | ------------------------------ | ------------------------------------------------------------------------- |
658
+ | `computeAge` | `computeAge(bornRaw, presentRaw)` | `number \| null` | reading the age two dates imply, in whole years |
659
+ | `parseAgeMagnitude` | `parseAgeMagnitude(raw)` | `{approximate, value} \| null` | reading an authored `age`'s `~` mark and its magnitude |
660
+ | `applyComputedBeingAge` | `applyComputedBeingAge(fm, presentRaw)` | `void` — mutates `fm.data` | filling in a being's computed `age`, or the `ageYears` beside an estimate |
661
+ | `presentAmongRecords` | `presentAmongRecords(records)` | the present, or `null` | reading the declared present from a batch of flat index records |
662
+ | `presentAmongFrontmatters` | `presentAmongFrontmatters(frontmatters)` | the present, or `null` | reading the declared present from a batch of raw frontmatter objects |
663
+ | `checkBeingAge` | `checkBeingAge(note, opts)` | `object[]` — findings | holding a being's authored `age` to what `born` and the present compute |
664
+
629
665
  ### `engine.contentAddress`
630
666
 
631
667
  Where a content note publishes on the web. One rule, in one place, because two builds need the same answer: the knowledgebase build renders the page, and the link manifest records the address other packages link to. Stating it twice is how a manifest comes to assert a URL that resolves at build time and 404s for the reader.
@@ -74,14 +74,13 @@ authoring.
74
74
  | [`relationships`](#relationships) | object | no; refused in a `documentation` package | `{}` |
75
75
  | [`systems`](#systems) | object | no; refused in a `documentation` package | `{}` |
76
76
  | [`requiresSystem`](#requiressystem) | string | no; refused in a `documentation` package | `null` |
77
- | [`calendar`](#calendar) | object | no | no bounds, no present |
78
77
  | [`packageBuild`](#the-packagebuild-section) | object | no | `{}` |
79
78
  | [`publish`](#publish) | object | no; **required**, with `site: content`, in a `documentation` package | `{site: "homepage", address: {prefix: ""}}` |
80
79
  | [`changelog`](#changelog) | object | no | `{labels: null}` |
81
80
 
82
81
  Any key outside this list is refused:
83
82
 
84
- > `` `<key>` is not a recognized option (expected one of: rootDir, contentPackage, foundryPackage, homepage, author, packageKind, stats, itemBuilders, paths, skipDirectories, icons, packs, docs, site, pdf, compatibility, relationships, systems, requiresSystem, calendar, packageBuild, publish, changelog). ``
83
+ > `` `<key>` is not a recognized option (expected one of: rootDir, contentPackage, foundryPackage, homepage, author, packageKind, stats, itemBuilders, paths, skipDirectories, icons, packs, docs, site, pdf, compatibility, relationships, systems, requiresSystem, packageBuild, publish, changelog). ``
85
84
 
86
85
  (`rootDir` appears in that list because it is a key `defineConfig` itself
87
86
  accepts — an `.mjs` configuration authors it directly. A YAML configuration
@@ -1298,59 +1297,6 @@ With one, naming what it does declare instead:
1298
1297
 
1299
1298
  > ``package-build config: `requiresSystem` must be a non-empty string.``
1300
1299
 
1301
- ### `calendar`
1302
-
1303
- **Type:** object · **Optional** · default: no bounds and no present.
1304
-
1305
- How this package's year is divided, and the date it calls the present:
1306
-
1307
- ```yaml
1308
- calendar:
1309
- months: 12
1310
- monthDays: 30
1311
- present: 720/6/19
1312
- ```
1313
-
1314
- **Nothing here is about which people count from what.** A reckoning is an
1315
- **era**, declared by the affiliation that proclaimed it, and its epoch is the
1316
- `start` written on that note — so the only things this block states are the ones
1317
- no note could.
1318
-
1319
- | Key (under `calendar`) | Type | Required | Default |
1320
- | ---------------------- | ---------------- | -------- | ------- |
1321
- | `calendar.months` | positive integer | no | `null` |
1322
- | `calendar.monthDays` | positive integer | no | `null` |
1323
- | `calendar.present` | date string | no | `null` |
1324
-
1325
- `months` and `monthDays` bound a written month and day. **Both are optional, and
1326
- an absent one is checked against nothing** — a year of twelve thirty-day months
1327
- is as ordinary here as a Gregorian one, so the bound is declared or it is not
1328
- applied. One subdivision to a package: an era moves where year 1 sits, it does
1329
- not divide the year differently.
1330
-
1331
- `present` is the date an age is computed against. It is written in the same
1332
- grammar a note's dates use — `[~][-]YYYY[/MM[/DD]] [<affiliation shortcode>.<era
1333
- shortcode>]` — so a package whose present is best said in an era says it that
1334
- way, and one that states no present computes no age at all.
1335
-
1336
- An age is a function of two **dates**, not two years: subtracting years alone
1337
- reds every note whose birthday falls later in the year than the present date, so
1338
- the day is part of what is declared.
1339
-
1340
- > ``package-build config: `calendar` must be a mapping.``
1341
-
1342
- > ``package-build config: `calendar.months` must be a positive integer.``
1343
-
1344
- > ``package-build config: `calendar.monthDays` must be a positive integer.``
1345
-
1346
- > ``package-build config: `calendar.present` must be a date written as a string.``
1347
-
1348
- > ``package-build config: `calendar.present` must be a date — a package that states no present computes no age, so leave the key out rather than writing `unknown`.``
1349
-
1350
- > ``package-build config: `calendar.present` is not a date this package can read — `720/13/1` writes month 13, and `calendar.months` declares a year of 12.``
1351
-
1352
- > ``package-build config: `calendar.<key>` is not a recognized option (expected one of: months, monthDays, present).``
1353
-
1354
1300
  ### `packageBuild`
1355
1301
 
1356
1302
  Reserved for `@heroiclands/package-build`'s own packaging half — see
@@ -2304,6 +2304,7 @@ Generates a living (or undead, or spirit) being.
2304
2304
  | `born` | `YYYY/MM/DD \| unknown` | When the being was born; absent, it was never born |
2305
2305
  | `died` | `YYYY/MM/DD \| unknown` | When the being died; absent, it is alive |
2306
2306
  | `age` | `34 \| ~34` | Age in years, stated only to override what `born` says; `~` marks an estimate |
2307
+ | `ageYears` | `number` | Written by the compiler beside an `age` estimate — the `~` stripped; a note never authors this |
2307
2308
  | `height` | `number` | Height in meters |
2308
2309
  | `weight` | `number` | Weight in kilograms |
2309
2310
  | `frame` | `scant \| light \| medium \| large \| massive` | Relative frame size |
@@ -2326,13 +2327,35 @@ three fields are built around.
2326
2327
  unrecorded. Both spellings of `died` mean dead.
2327
2328
  - **`age` is optional and is an override.** Written, it wins over anything the
2328
2329
  dates say. Unwritten beside a dated `born`, it is the age that follows from
2329
- the date. Unwritten beside `born: unknown` or an absent `born`, the age is
2330
- unknown.
2330
+ the date and the package's declared present — see below. Unwritten beside
2331
+ `born: unknown` or an absent `born`, the age is unknown.
2331
2332
 
2332
2333
  Most people in a setting do not know their own birth date, so an age beside
2333
2334
  `born: unknown` is an ordinary complete record and not a note working around the
2334
2335
  schema. Where the number is somebody's estimate rather than a fact, write it
2335
- `~34` — the same `~` a date carries.
2336
+ `~34` — the same `~` a date carries. The estimate string is kept exactly as
2337
+ written; the compiler adds `ageYears` beside it, the plain number with the `~`
2338
+ stripped, for anything that needs to sort or filter on the age rather than
2339
+ read it.
2340
+
2341
+ #### The age a dated `born` computes to
2342
+
2343
+ A package states the day its setting stops on `data.present`, on its own
2344
+ `world` or `celestial` `place` note — see _What a body states about itself_
2345
+ under `type: place`. Against that date, a being with a dated `born` and no
2346
+ authored `age` compiles the age it implies: the two dates compared, not the
2347
+ two years subtracted, so a birthday that falls later in the year than the
2348
+ present counts one less than the years alone would say. A package that
2349
+ declares no present computes no age and says nothing about it — the same
2350
+ silence a package with no calendar mechanism meets.
2351
+
2352
+ **An authored `age` beside a dated `born` is expected to agree with what the
2353
+ date computes.** Where it does not, `content-build lint` raises a warning
2354
+ naming both values, so a stale age left behind after a `born` was corrected
2355
+ does not go unnoticed. The warning is silent beside `born: unknown` or an
2356
+ absent `born` — there is nothing there to disagree with — and silent wherever
2357
+ the package declares no present, for the same reason no age is computed at
2358
+ all.
2336
2359
 
2337
2360
  `birthday:` is the retired spelling of `born:`. Both are read and `born` wins,
2338
2361
  so a note compiles identically either way; the old spelling is reported until
@@ -3217,8 +3240,65 @@ In-world information about people, places, or concepts.
3217
3240
  tournament is not a matter of time-reckoning at all — and from `culture`, which is a grouping
3218
3241
  of people rather than an occasion they attend.
3219
3242
 
3220
- | `data` property | Values | Description |
3221
- | --------------- | ------ | ----------- |
3243
+ | `data` property | Values | Description |
3244
+ | --------------- | ----------------------------------------------------------------------- | --------------------------------------------------------------------------------- |
3245
+ | `epoch` | `date` | Which in-world day the world's clock reads zero on |
3246
+ | `months` | `{ name, abbreviation?, days }[]` | The months this calendar keeps, in order — the list sums to the world's year |
3247
+ | `weekdays` | `{ name, abbreviation? }[]` | The days of the week it names, in order; a calendar with no week writes none |
3248
+ | `seasons` | `{ name, abbreviation?, monthStart?, monthEnd?, dayStart?, dayEnd? }[]` | The seasons it marks, bounded by month or by day of year |
3249
+ | `eras` | `{ shortcode, name, abbreviation?, proclaimedBy?, start, end? }[]` | The year-counts kept in it, each addressed `<calendar shortcode>.<era shortcode>` |
3250
+
3251
+ #### What a calendar note declares
3252
+
3253
+ **Those five keys belong to `subType: calendar` and to no other genre of
3254
+ `lore`.** A note carrying one under any other subType is refused, and a calendar
3255
+ note that states no `months` and no `epoch` is refused too: a calendar divides
3256
+ the year and says where the count begins, and one that does neither is prose
3257
+ about time rather than a calendar.
3258
+
3259
+ ```yaml
3260
+ type: lore
3261
+ subType: calendar
3262
+ shortcode: commoncal
3263
+ name:
3264
+ full: The Common Calendar
3265
+ data:
3266
+ epoch: 720/1/1
3267
+ months:
3268
+ - { name: Floralis, abbreviation: Flor, days: 30 }
3269
+ - { name: Lusenar, abbreviation: Luse, days: 31 }
3270
+ # … ten more, summing to the world's year
3271
+ seasons:
3272
+ - { name: Spring, monthStart: 1, monthEnd: 3 }
3273
+ eras:
3274
+ - shortcode: founding
3275
+ name: After the Founding
3276
+ abbreviation: AF
3277
+ proclaimedBy: vylarinmpr
3278
+ start: 1
3279
+ ```
3280
+
3281
+ **Position in the list is position in the year.** Days that belong to no month
3282
+ are a short month like any other: a five-day festival is one entry, and where it
3283
+ sits in the list is where it falls in the year. There is no intercalary
3284
+ mechanism, because there is nothing for one to do.
3285
+
3286
+ **How long the year is, how the day divides and how the moon moves are facts
3287
+ about the world, not about a calendar**, so no calendar note states any of them
3288
+ — they are written once on the world's own `place` note and on its moon's, and
3289
+ every calendar is compiled against them. The one arithmetic check follows from
3290
+ that: `months` must sum to `year.days`, and a list that does not describes a
3291
+ different world rather than a different calendar.
3292
+
3293
+ **An era is a way of counting years within a calendar**, so its rows sit beside
3294
+ the months. Each states its own `shortcode`, unique within the note, and a date
3295
+ names one by writing it after the calendar's address — `3 commoncal.founding`.
3296
+ `proclaimedBy` is optional: a reckoning whose proclaiming body is unknown, or
3297
+ whose body has no note, says so by leaving it out.
3298
+
3299
+ **A calendar with no week writes no `weekdays`.** An empty list and an absent
3300
+ one say the same thing, and nothing downstream shows a weekday for a calendar
3301
+ that names none.
3222
3302
 
3223
3303
  ### type: map
3224
3304
 
@@ -3370,16 +3450,72 @@ three maps, and none of those maps needs to know it is a keep.
3370
3450
  - site: A place significant by what was made or done there — ruins, monoliths, henges, works, battlefields.
3371
3451
  - structure: A single building or habitation — halls, keeps, temples, inns, towers.
3372
3452
  - feature: A place significant by its terrain — forests, rivers, falls, passes, fords.
3453
+ - celestial: A body observed from a world rather than located on one — a sun, a planet, a comet.
3454
+
3455
+ | `data` property | Values | Description |
3456
+ | --------------------------------- | --------------------------------------------------- | ---------------------------------------------------------------------------- |
3457
+ | `demonym` | `string` | What a person from this place is called — a Vylarian |
3458
+ | `lore` | `WikiLink[]` | Lore concerning this place — its peoples, its law, its calendar, its history |
3459
+ | `parents` | `WikiLink[]` | Enclosing places within which this place is located |
3460
+ | `population` | `number` | Approximate population (precision 2 significant digits) |
3461
+ | `market` | `number` | Market class, 1 to 6 — what trade a settlement supports |
3462
+ | `borders` | `{ to, bearing }[]` | Places sharing a frontier with this one, and where each lies from here |
3463
+ | `routes` | `{ to, bearing, mode, days, terrain?, leagues? }[]` | Journeys from this place's centre to another place |
3464
+ | `world.equatorialCircumferenceKm` | `number` | The distance round the body at its equator, in kilometres |
3465
+ | `world.surfaceGravityG` | `number` | Surface gravity, as a multiple of Earth's |
3466
+ | `world.axialTiltDegrees` | `number` | The tilt of the axis, in degrees |
3467
+ | `year.days` | `number` | How many days the body's year holds |
3468
+ | `year.hoursPerDay` | `number` | How many hours the day divides into |
3469
+ | `year.minutesPerHour` | `number` | How many minutes the hour divides into |
3470
+ | `year.secondsPerMinute` | `number` | How many seconds the minute divides into |
3471
+ | `present` | `date` | The day the setting stops and play begins |
3472
+ | `body.diameterKm` | `number` | The body's diameter, in kilometres |
3473
+ | `body.orbitalRadiusKm` | `number` | How far it orbits from the body it circles, in kilometres |
3474
+ | `body.orbit` | `string` | The orbit's shape — `circular` means the cycle never varies |
3475
+ | `body.inclined` | `boolean` | Whether the orbit is inclined to the plane the world orbits in |
3476
+ | `moon.cycle` | `number` | How many days it takes to return to the same phase |
3477
+ | `moon.newOn` | `date` | A day it was new, written in the reference calendar |
3478
+ | `moon.eclipses` | `string` | `never`, `rare`, `occasional` or `frequent` |
3479
+
3480
+ #### What a body states about itself
3481
+
3482
+ **The four groups above are the world's own facts, and they are legal on a
3483
+ `world` or a `celestial` note and nowhere else.** A region, a settlement, a
3484
+ site, a structure or a feature is somewhere _within_ a world and says nothing
3485
+ about the world's year.
3486
+
3487
+ ```yaml
3488
+ # the world — type: place, subType: world
3489
+ data:
3490
+ world: { equatorialCircumferenceKm: 40000, surfaceGravityG: 1.0, axialTiltDegrees: 23.5 }
3491
+ year: { days: 365, hoursPerDay: 24, minutesPerHour: 60, secondsPerMinute: 60 }
3492
+ present: 720
3493
+
3494
+ # its moon — a note of its own, because a moon is a body and not a property
3495
+ data:
3496
+ body: { diameterKm: 3800, orbitalRadiusKm: 388600, orbit: circular, inclined: true }
3497
+ moon: { cycle: 30, newOn: 720/1/1, eclipses: rare }
3498
+ ```
3373
3499
 
3374
- | `data` property | Values | Description |
3375
- | --------------- | --------------------------------------------------- | ---------------------------------------------------------------------------- |
3376
- | `demonym` | `string` | What a person from this place is called — a Vylarian |
3377
- | `lore` | `WikiLink[]` | Lore concerning this place — its peoples, its law, its calendar, its history |
3378
- | `parents` | `WikiLink[]` | Enclosing places within which this place is located |
3379
- | `population` | `number` | Approximate population (precision 2 significant digits) |
3380
- | `market` | `number` | Market class, 1 to 6 — what trade a settlement supports |
3381
- | `borders` | `{ to, bearing }[]` | Places sharing a frontier with this one, and where each lies from here |
3382
- | `routes` | `{ to, bearing, mode, days, terrain?, leagues? }[]` | Journeys from this place's centre to another place |
3500
+ **`year.days` is where the year's length lives, and every calendar in the
3501
+ package is compiled against it.** A package whose notes state no `year` has no
3502
+ calendar mechanism at all: no sum is checked, and nothing is emitted. That is
3503
+ the right answer for a package building a setting whose world facts somebody
3504
+ else wrote down.
3505
+
3506
+ **One year and one moon to a package.** Two years is two settings and nothing
3507
+ can say which one a calendar divides; a `moon` with no `year` anywhere is
3508
+ refused, because a cycle counted in days has no year to be measured against.
3509
+
3510
+ **`present` is the day an age is computed against**, written in the grammar a
3511
+ note's dates are written in, so a setting whose present is best said in an era
3512
+ says it that way.
3513
+
3514
+ **`world.axialTiltDegrees`, the circumference, the gravity, the moon's diameter
3515
+ and its orbital radius are read by nobody.** They are there so a reader knows
3516
+ what kind of world this is. What the build consumes is the year, the day's
3517
+ divisions, the present, the moon's cycle and epoch, and whether its orbit is
3518
+ circular and inclined.
3383
3519
 
3384
3520
  #### What a settlement's market supports
3385
3521
 
@@ -92,6 +92,10 @@ import { resolveNoteId } from "./note-ids.mjs";
92
92
  import { carriesSystemBlock } from "./system-block.mjs";
93
93
  import { checkAuthoredSystemData, checkEmittedSystemData } from "./schema-check.mjs";
94
94
  import { locateFrontmatterKey } from "./retired-fields.mjs";
95
+ // A being's computed `age`, filled in against the package's own declared
96
+ // present before this pass reads `data:` for anything else — see
97
+ // `engine/being-age.mjs`.
98
+ import { applyComputedBeingAge, presentAmongRecords } from "./being-age.mjs";
95
99
 
96
100
  /**
97
101
  * The tallies one pass accumulates while walking the tree.
@@ -594,6 +598,10 @@ export class BasePackCompiler {
594
598
  this.contentDocs = this.corpus.contentDocs;
595
599
  this.sqlTables = this.corpus.sqlTables;
596
600
  }
601
+ // The package's declared present, read once from the same corpus — so
602
+ // a being's computed `age` reaches every compiled document, not only
603
+ // the content index.
604
+ this.worldPresent = presentAmongRecords(this.corpus.records);
597
605
  this.unresolvedLinks = 0;
598
606
  }
599
607
 
@@ -1016,6 +1024,7 @@ export class BasePackCompiler {
1016
1024
  stats.skippedOther++;
1017
1025
  continue;
1018
1026
  }
1027
+ applyComputedBeingAge(fm, this.worldPresent);
1019
1028
  // The retired frontmatter fields, refused before `selects` so a
1020
1029
  // note is answered whichever pass would have claimed it — and
1021
1030
  // whatever the declared value says.