@heroiclands/package-build 22.14.4 → 22.15.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.
Files changed (60) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/CONTENT.md +25 -1
  3. package/bin/content-build.mjs +3 -7
  4. package/bin/package-build.mjs +25 -0
  5. package/docs/api.md +61 -34
  6. package/docs/commands.md +65 -9
  7. package/docs/configuration.md +6 -3
  8. package/docs/content-format.md +165 -121
  9. package/docs/diagnostics.md +1 -1
  10. package/docs/getting-started.md +23 -0
  11. package/engine/base-compiler.mjs +3 -1
  12. package/engine/being-age.mjs +16 -24
  13. package/engine/calendar-artifacts.mjs +153 -0
  14. package/engine/calendar-notes.mjs +64 -16
  15. package/engine/calendars.mjs +129 -0
  16. package/engine/content-index.mjs +3 -1
  17. package/engine/exported-scene.mjs +230 -0
  18. package/engine/generate.mjs +1 -0
  19. package/engine/ids.mjs +1 -1
  20. package/engine/index.mjs +6 -0
  21. package/engine/infobox.mjs +4 -0
  22. package/engine/itinerary-scenes.mjs +172 -0
  23. package/engine/map-build.mjs +10 -45
  24. package/engine/map-dot.mjs +6 -3
  25. package/engine/map-graphviz.mjs +50 -131
  26. package/engine/map-notes.mjs +26 -3
  27. package/engine/map-places.mjs +12 -6
  28. package/engine/map-raster.mjs +51 -0
  29. package/engine/note-dates.mjs +125 -85
  30. package/engine/note-vocabulary.mjs +22 -6
  31. package/engine/pdf-build.mjs +57 -1
  32. package/engine/pdf-render.mjs +18 -0
  33. package/engine/reckoning-markers.mjs +207 -0
  34. package/engine/scenes.mjs +113 -26
  35. package/engine/site-build.mjs +11 -13
  36. package/engine/site-maps.mjs +5 -35
  37. package/engine/site-preview.mjs +6 -1
  38. package/engine/social-tie-terms.mjs +3 -0
  39. package/engine/social-ties.mjs +30 -19
  40. package/package.json +3 -1
  41. package/types/engine/being-age.d.mts +2 -2
  42. package/types/engine/calendar-artifacts.d.mts +16 -0
  43. package/types/engine/calendar-notes.d.mts +1 -1
  44. package/types/engine/calendars.d.mts +65 -0
  45. package/types/engine/exported-scene.d.mts +12 -0
  46. package/types/engine/index.d.mts +2 -0
  47. package/types/engine/itinerary-scenes.d.mts +29 -0
  48. package/types/engine/map-build.d.mts +4 -13
  49. package/types/engine/map-graphviz.d.mts +17 -59
  50. package/types/engine/map-places.d.mts +5 -1
  51. package/types/engine/map-raster.d.mts +8 -0
  52. package/types/engine/note-dates.d.mts +6 -0
  53. package/types/engine/note-vocabulary.d.mts +3 -3
  54. package/types/engine/pdf-render.d.mts +7 -1
  55. package/types/engine/reckoning-markers.d.mts +27 -0
  56. package/types/engine/scenes.d.mts +9 -2
  57. package/types/engine/site-build.d.mts +1 -5
  58. package/types/engine/site-maps.d.mts +2 -7
  59. package/types/engine/social-tie-terms.d.mts +2 -0
  60. package/types/engine/social-ties.d.mts +2 -1
@@ -326,6 +326,42 @@ flow collection is formatted into one of those forms; folded collections are
326
326
  not used. `content-build format --write` applies this rule and the top-level
327
327
  order without changing the Markdown body.
328
328
 
329
+ #### Dates and reckoning markers
330
+
331
+ A calendar names and orders the days within a year. An **era** states where its
332
+ year count begins. Several eras may use one calendar; each has its own origin.
333
+ An era may declare a short, uppercase `marker` for authored dates. The marker
334
+ selects that era and the calendar whose months interpret the date inside it:
335
+
336
+ ```yaml
337
+ data:
338
+ born: VR(676/12/5)
339
+ ```
340
+
341
+ `VR` is a marker declared on an era row of the Common Calendar. `VR(720/5/14)`
342
+ means its year 720, fifth month, fourteenth day. `VR(720/5)` states month
343
+ precision, and `VR(720)` states year precision. The parser retains that
344
+ precision; it does not invent a first day when the author only knows a year.
345
+ `VR(~-480)` keeps an approximate year. A date whose occurrence is known but
346
+ whose value is not recorded is `unknown`, with no marker.
347
+
348
+ The calendar-neutral spelling is `<year>.<day>[:HHMMSS]`: `720.136` names day
349
+ 136 of canonical year 720, and `720.136:143005` adds 14:30:05. The dot means
350
+ **day of year**, not a month. Its day must fit the world's `data.year.days`,
351
+ and its time must fit a 24-hour day. A bare year is also accepted. Unmarked
352
+ slash dates are accepted as calendar-neutral numeric month/day values; their
353
+ month division has no named calendar, so use a marker when the month is part
354
+ of a people's reckoning.
355
+
356
+ An era's `start` locates its first year on the canonical timeline. The marker's
357
+ own start is written canonically, such as `start: 1`, so its definition has no
358
+ circular reference. Another era may place its start using a marker already
359
+ defined in the corpus. A year-precision start anchors the reckoning at the
360
+ first day of that canonical year for conversion; it does not date the event
361
+ that gave the era its name. A marker must be unique across the corpus and contain
362
+ uppercase letters or digits, beginning with a letter. An optional `end` bounds
363
+ dates in that era; a year-precision end includes the whole named year.
364
+
329
365
  **`data:` is closed.** It holds the type-specific facts about the
330
366
  subject — a weapon's weight, an affliction's transmission, a being's species —
331
367
  and every note type declares which keys it may carry. A misspelled key is a
@@ -2452,6 +2488,19 @@ an acquaintance knows them without a strong disposition; a rival opposes them
2452
2488
  without implacable hostility; a nemesis opposes them personally and
2453
2489
  implacably. The infobox groups targets by term and links to their notes.
2454
2490
 
2491
+ ```yaml
2492
+ data:
2493
+ socialTies:
2494
+ being-kaldor: patron
2495
+ being-alys: dependent
2496
+ affiliation-silverguild: nemesis
2497
+ kethira-note-being-kaldas: rival
2498
+ ```
2499
+
2500
+ The full Address names a target in another package. Each target has one tie:
2501
+ writing its short and full Address in the same map still names one target and is
2502
+ an error.
2503
+
2455
2504
  Each tie is one-sided. The target need not name the subject back, even when
2456
2505
  `patron` and `dependent` would describe opposite ends of the same support.
2457
2506
  Kinship, household membership, organizational membership, rank, and office are
@@ -3425,15 +3474,15 @@ In-world information about people, places, or concepts.
3425
3474
  tournament is not a matter of time-reckoning at all — and from `culture`, which is a grouping
3426
3475
  of people rather than an occasion they attend.
3427
3476
 
3428
- | `data` property | Values | Description |
3429
- | --------------- | ----------------------------------------------------------------------- | --------------------------------------------------------------------------------- |
3430
- | `epoch` | `date` | Which in-world day the world's clock reads zero on |
3431
- | `months` | `{ name, abbreviation?, days }[]` | The months this calendar keeps, in order — the list sums to the world's year |
3432
- | `weekdays` | `{ name, abbreviation? }[]` | The days of the week it names, in order; a calendar with no week writes none |
3433
- | `seasons` | `{ name, abbreviation?, monthStart?, monthEnd?, dayStart?, dayEnd? }[]` | The seasons it marks, bounded by month or by day of year |
3434
- | `eras` | `{ shortcode, name, abbreviation?, proclaimedBy?, start, end? }[]` | The year-counts kept in it, each addressed `<calendar shortcode>.<era shortcode>` |
3435
- | `dateFormats` | `Map<slot, format>` | Calendaria display formats for this calendar |
3436
- | `event` | `Map<field, value>` | A dated occurrence, with its sources, locations, reach, and relationships |
3477
+ | `data` property | Values | Description |
3478
+ | --------------- | --------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
3479
+ | `epoch` | `date` | Which in-world day the world's clock reads zero on |
3480
+ | `months` | `{ name, abbreviation?, days }[]` | The months this calendar keeps, in order — the list sums to the world's year |
3481
+ | `weekdays` | `{ name, abbreviation? }[]` | The days of the week it names, in order; a calendar with no week writes none |
3482
+ | `seasons` | `{ name, abbreviation?, monthStart?, monthEnd?, dayStart?, dayEnd? }[]` | The seasons it marks, bounded by month or by day of year |
3483
+ | `eras` | `{ shortcode, name, marker?, abbreviation?, proclaimedBy?, start, end? }[]` | The year-counts kept in it; a marker names one era and uses this calendar's months |
3484
+ | `dateFormats` | `Map<slot, format>` | Calendaria display formats for this calendar |
3485
+ | `event` | `Map<field, value>` | A dated occurrence, with its sources, locations, reach, and relationships |
3437
3486
 
3438
3487
  `data.event` is available on every `lore` subType. It holds structured
3439
3488
  chronology metadata, including the event's kind, date, sources, and the places
@@ -3450,13 +3499,13 @@ the year and says where the count begins, and one that does neither is prose
3450
3499
  about time rather than a calendar.
3451
3500
 
3452
3501
  ```yaml
3453
- type: lore
3454
- subType: calendar
3455
3502
  shortcode: commoncal
3456
3503
  name:
3457
3504
  full: The Common Calendar
3505
+ type: lore
3506
+ subType: calendar
3458
3507
  data:
3459
- epoch: 720/1/1
3508
+ epoch: VR(720/1/1)
3460
3509
  months:
3461
3510
  - { name: Floralis, abbreviation: Flor, days: 30 }
3462
3511
  - { name: Lusenar, abbreviation: Luse, days: 31 }
@@ -3466,6 +3515,7 @@ data:
3466
3515
  eras:
3467
3516
  - shortcode: founding
3468
3517
  name: After the Founding
3518
+ marker: VR
3469
3519
  abbreviation: AF
3470
3520
  proclaimedBy: vylarinmpr
3471
3521
  start: 1
@@ -3492,8 +3542,10 @@ that: `months` must sum to `year.days`, and a list that does not describes a
3492
3542
  different world rather than a different calendar.
3493
3543
 
3494
3544
  **An era is a way of counting years within a calendar**, so its rows sit beside
3495
- the months. Each states its own `shortcode`, unique within the note, and a date
3496
- names one by writing it after the calendar's address — `3 commoncal.founding`.
3545
+ the months. Each states its own `shortcode`, unique within the note. A date
3546
+ names an era with its marker, such as `VR(720/5/14)`. A calendar may contain
3547
+ several era rows, each with its own marker and year origin. The marker is an
3548
+ authoring identifier; the `abbreviation` is its printed label.
3497
3549
  `proclaimedBy` is optional: a reckoning whose proclaiming body is unknown, or
3498
3550
  whose body has no note, says so by leaving it out.
3499
3551
 
@@ -3503,115 +3555,96 @@ that names none.
3503
3555
 
3504
3556
  ### type: map
3505
3557
 
3506
- A map — the visual rendering of a place or an encounter, plus the pinned
3507
- locations described in its own body. Produces a Foundry **Scene**, and a web page carrying the map image
3508
- and its description.
3558
+ A map note has its own prose and a Foundry Scene. A map can depict a place or an encounter. Several maps can depict the same place; the map names that place with `sohl.place`, while the place note does not list its maps.
3509
3559
 
3510
- A map is a note in its own right rather than a property of a `place`, for three
3511
- reasons. A place commonly has **several** maps — a floor per level of a keep, or
3512
- a feature's detail map alongside its local-area map — and a scene nested in a
3513
- place could only ever be one. A Scene is a substantial document in itself: walls,
3514
- doors, lights, tiles, sounds, region shapes and pins, authored in two deliberate
3515
- unit conventions. And a map has prose and named locations of its own, which makes
3516
- it a document rather than a field.
3560
+ **subType**:
3517
3561
 
3518
- A map may also exist with no place at all — an ambush on a road is an encounter
3519
- map and not a named location.
3562
+ - `battlemap`: A tactical Scene built in Foundry.
3563
+ - `localmap`: A settlement or local-area Scene built in Foundry.
3564
+ - `regionalmap`: A large-scale chart with an authored image and scale.
3565
+ - `totm`: A gridless image Scene for theatre-of-the-mind play.
3566
+
3567
+ | `data` property | Values | Description |
3568
+ | ----------------- | ------------ | ---------------------------------------------------------- |
3569
+ | `scene` | object | Exported Foundry Scene for battle and local maps |
3570
+ | `fixup` | object[] | Asset address replacements in an exported Scene |
3571
+ | `bgImage` | Address | Background image for regional and theatre-of-the-mind maps |
3572
+ | `scale` | object | Regional distance and unit per grid unit |
3573
+ | `dimensions` | `[int, int]` | Optional shared dimensions metadata |
3574
+ | `pxPerGrid` | number | Optional shared grid size metadata |
3575
+ | `navName` | string | Short Scene navigation name |
3576
+ | `levelName` | string | Embedded Level name |
3577
+ | `backgroundColor` | string | Colour outside the art |
3578
+ | `overlay` | string | Foreground art path |
3579
+ | `walls` | object | Regional wall geometry |
3580
+ | `doors` | object | Regional door geometry |
3581
+ | `lights` | object | Regional light geometry |
3582
+ | `tiles` | object | Regional tile geometry |
3583
+ | `sounds` | object | Regional sound geometry |
3584
+ | `regions` | object | Regional region geometry |
3585
+ | `notes` | object | Regional pin geometry |
3586
+ | `place` | Address | Depicted place, when one exists |
3587
+
3588
+ #### Battle and local maps
3589
+
3590
+ Author the Scene in Foundry, export it, convert the export to YAML, and put the resulting object at `data.scene`. Preserve its walls, lights, tiles, regions, levels, environment, flags, and canvas settings. The export can name art from another Foundry package; that path is used as authored in Foundry. The Scene needs positive whole-number `width` and `height`, at least one Level, a valid `initialLevel` when present, and a unique 16-character `_id` on each embedded document. The build derives the top-level Scene `_id` from the map note, adds compendium keys and build metadata, and otherwise preserves the exported fields.
3591
+
3592
+ A pin's `text` can contain `#anchor`, where the map note has a heading such as `# Big Bad Wolf {#myanchor1}`. With `text: '#myanchor1'`, the compiled pin opens the JournalEntry and page generated from this map note and displays **Big Bad Wolf**. Its position, icon, other settings, and embedded `_id` remain as exported. A missing anchor is a build error. Pins whose text does not use `#anchor` retain their exported text and references.
3593
+
3594
+ `data.fixup` replaces exported asset paths without editing the Scene object by
3595
+ hand. Each entry has `path`, `type: address`, and `value`, an asset Address in
3596
+ the local or a fetched content index. Paths start at `data.scene` and use a
3597
+ property path with array indices or stable embedded `_id` selectors. A path
3598
+ must name one existing string or null field, and the address must resolve; any
3599
+ mistake fails the build. An `_id` selector remains stable when Foundry reorders
3600
+ an array on export.
3520
3601
 
3521
- **The place is named on the map, not the other way round.** A place commonly has
3522
- several maps, so listing them from the place is the end that goes stale; naming
3523
- the place from the map is the end that cannot, because a map is written once and
3524
- depicts what it depicts. A place's maps are therefore derived — every map whose
3525
- `place` is this one — and the relation exists in exactly one place.
3602
+ ```yaml
3603
+ data:
3604
+ fixup:
3605
+ - { path: ".notes[NNNNNNNNNNNNNNNN].texture.src", type: address, value: sohl-none-icon-book }
3606
+ - { path: ".sounds[0].path", type: address, value: sohl-none-audio-forest }
3607
+ ```
3526
3608
 
3527
- **subType**:
3609
+ Fixups change the compiled copy only. The exported Scene in the note stays
3610
+ intact, and pin anchor binding runs after fixups. A package name such as
3611
+ `fvtt` is an ordinary address package name: it needs published asset records
3612
+ before its addresses can resolve.
3528
3613
 
3529
- - battlemap: Tactical scale, for a scene played out square by square.
3530
- - localmap: Roughly a kilometre across — a settlement, a holding, a small valley.
3531
- - regionalmap: Large scale, covering a region or a journey between places.
3532
-
3533
- The three differ only in the canvas defaults derived for them, which is why they
3534
- are subTypes of one type rather than three types.
3535
-
3536
- **NoteLocation** is `[GridLocation, anchor]` where the `anchor` is an anchor identified in the body of the note, and `GridLocation` represents a particular grid location on the document.
3537
-
3538
- The `data:` fields, of which three are required:
3539
-
3540
- | `data` property | Values | Description |
3541
- | ----------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
3542
- | `bgImage` | `Address` | **Required.** The map art — an `image` address. Becomes the level's `background.src`, what tokens stand on |
3543
- | `dimensions` | `[int, int]` | **Required.** `[width, height]` in whole pixels, the art's own size |
3544
- | `pxPerGrid` | `integer` | **Required.** Whole pixels per grid square; must match the art |
3545
- | `navName` | `string` | Short name for Foundry's scene navigation bar |
3546
- | `levelName` | `string` | The name of the embedded level. Defaults to `Ground` |
3547
- | `backgroundColor` | `ColorHexValue` | Shown where the art does not reach. Defaults to `#999999` |
3548
- | `overlay` | `string` | Path to **foreground** art |
3549
- | `walls` | `WallSegment[]` | List of wall segments |
3550
- | `doors` | `Door[]` | List of doors |
3551
- | `lights` | `Light[]` | List of lights |
3552
- | `tiles` | `Tile[]` | List of tiles, each naming its art in `image` |
3553
- | `sounds` | `Sound[]` | List of sounds, each naming its clip in `audio` |
3554
- | `regions` | `SceneRegion[]` | List of scene regions |
3555
- | `place` | `Address` | The place this map depicts. Optional, because an encounter map depicts no named place — but that is the exception, and a map without one is a map of nowhere |
3556
- | `notes` | `NoteLocation[]` | grid coordinates of note markers mapped to anchors in this document |
3557
-
3558
- Everything else a Scene holds is **derived**, not authored: padding, grid type,
3559
- grid distance and units, token vision and fog mode all come from the subType, and
3560
- ids, ordering and elevation are synthesised. A map note carries the map's
3561
- essence, not a Scene's schema — the same division a weapon note makes against an
3562
- Item's.
3563
-
3564
- **`levels:` is never authored, and must never be.** A Scene has to ship at least
3565
- one Level or it has no map at all, and nothing supplies one after the fact: the
3566
- client-side `_preCreate` that would create it does not run during offline pack
3567
- compilation, and the server-side migration shim is version-gated on
3568
- `_stats.coreVersion`, so a pack stamped 14.x or later skips it entirely. The
3569
- single Level is synthesised from `bgImage`, `overlay`, `levelName` and
3570
- `backgroundColor`.
3571
-
3572
- > **`bgImage` is an `Address` under `data:`, as every other type's artwork is.**
3573
- > Art is not system-specific — a Scene is a core Foundry document, and a second
3574
- > system would want the identical art — so the field sits under `data:` rather
3575
- > than inside a system block.
3576
- >
3577
- > **A map has no `img`.** `bgImage` is the only key naming a map's background,
3578
- > and a note writing `img:` or `data.img:` is refused. The two are not
3579
- > alternative spellings: `img` is a Foundry destination and no Scene has one.
3580
-
3581
- **Every art key inside a map is an address, and each is named for the type it
3582
- reaches.** A map's geometry is authored in Foundry's own units, and its art is
3583
- not authored in Foundry's own paths.
3584
-
3585
- | authored | default type | emitted as |
3586
- | -------------------- | ------------ | ---------------------------- |
3587
- | `data.bgImage` | `image` | the level's `background.src` |
3588
- | `tiles.<key>.image` | `image` | the tile's `texture.src` |
3589
- | `sounds.<key>.audio` | `audio` | the `AmbientSound`'s `path` |
3590
-
3591
- Naming each key for the type it references is what makes the default worth
3592
- having: `audio:` reaches an `audio` address without a note ever writing the
3593
- type segment. Where a tile places a glyph rather than artwork it qualifies —
3594
- `image: sohl-none-icon-chest` — which is the ordinary short-form ladder rather
3595
- than an exception to it.
3596
-
3597
- **The authored key and the emitted field differ where Foundry's name differs.**
3598
- An `AmbientSound` stores its clip in `path`, and that is what the compiled
3599
- document carries; the note writes `audio` because the note is naming an
3600
- address, not filling in a Foundry field.
3601
-
3602
- **Two unit conventions, deliberately.** Geometry — walls, doors, lights, tiles,
3603
- sounds, region shapes — is authored in **pixels**, Foundry's native storage,
3604
- because traced walls often fall between grid intersections. Map pins are
3605
- authored in **grid squares**, commonly
3606
- half-integers, because that is how a person reads a position off a map. The two
3607
- are told apart by their key: `position:` and segment or shape coordinates are
3608
- pixels, `at:` is grid squares. Mixing them fails silently and visually in
3609
- Foundry, so the build refuses rather than resolving.
3610
-
3611
- **Map pins stay within the map.** `notes:` names anchors in this map note's own
3612
- body, so a pin opens the journal page describing that spot. `data.place` names
3613
- the place depicted by the map when one exists. A place's map listing is derived
3614
- from those map notes, so each map states the relationship once.
3614
+ ```yaml
3615
+ shortcode: wolfden
3616
+ name: { full: The Wolf's Den }
3617
+ type: map
3618
+ subType: battlemap
3619
+ data:
3620
+ scene:
3621
+ name: Wolf Den Scene
3622
+ width: 1900
3623
+ height: 2600
3624
+ grid: { type: 1, size: 100, distance: 5, units: ft }
3625
+ initialLevel: defaultLevel0000
3626
+ levels:
3627
+ - { _id: defaultLevel0000, background: { src: modules/maps/den.webp } }
3628
+ notes:
3629
+ - { _id: NNNNNNNNNNNNNNNN, text: "#myanchor1", x: 950, y: 2350 }
3630
+ ---
3631
+ # Big Bad Wolf {#myanchor1}
3632
+ ```
3633
+
3634
+ When the Scene has pins, its Adventure bundle carries the Scene and the JournalEntry generated from its Markdown. Importing that bundle preserves the IDs the pins address.
3635
+
3636
+ #### Regional maps
3637
+
3638
+ A `regionalmap` uses `data.bgImage`, an image Address, and `sohl.dimensions: [width, height]` plus `sohl.pxPerGrid` to build a gridless Scene. `data.scale: {distance: positive number, unit: nonempty string}` sets the distance per grid unit. Its shared `data` fields can also name `overlay`, `backgroundColor`, `levelName`, `navName`, `walls`, `doors`, `lights`, `tiles`, `sounds`, `regions`, and `notes`; the latter geometry uses the map-note conventions described in the SoHL map authoring guide. `data.scene` is not used for a regional map.
3639
+
3640
+ An SVG `data.bgImage` remains vector art on the site and in the book. The Foundry build stages a PNG at the note's stated dimensions with the installed SVG renderer; the source SVG is unchanged. Its aspect ratio must match the stated dimensions. A selected regional SVG map takes a full page in the PDF.
3641
+
3642
+ #### Theatre-of-the-mind maps
3643
+
3644
+ A `totm` map names its background with `data.bgImage` and its canvas size with
3645
+ `sohl.dimensions: [width, height]`. It builds a gridless Scene with token vision
3646
+ off. It needs no `sohl.pxPerGrid` or `data.scale`. The Markdown body remains the
3647
+ map's prose.
3615
3648
 
3616
3649
  ### type: place
3617
3650
 
@@ -3764,9 +3797,10 @@ data:
3764
3797
  on a land route — or a road on a ship route — is an error.
3765
3798
  - `leagues` — optional on a route; a distance, only where the note states one.
3766
3799
 
3767
- **Regions border; settlements are days apart.** A frontier is a fact about two
3768
- regions, and a journey is a fact about two places a traveller sets out from and
3769
- arrives at. A pair appears in `routes` once per `mode` — a port and its
3800
+ **A border is a shared frontier between areas.** Regions commonly state one;
3801
+ a settlement does so only when it actually shares a frontier with another
3802
+ place. Most settlements use `parents` for where they sit and `routes` for
3803
+ journeys between them. A pair appears in `routes` once per `mode` — a port and its
3770
3804
  neighbour may be three days by land and one by ship — and in `borders` once.
3771
3805
  **A place never borders its own parent or child**: containment is `parents`,
3772
3806
  and restating it as adjacency makes the two contradict each other.
@@ -3781,6 +3815,16 @@ neighbour that states a different bearing, a different `days` or a different
3781
3815
  tell which. Every `to` must resolve to a place, every value must come from its
3782
3816
  closed set, and each finding is located at the entry that states it.
3783
3817
 
3818
+ The terrain names form one shared registry in package-build. A route crossing
3819
+ a package boundary uses the same terrain and mode rules at both ends.
3820
+
3821
+ For every place that participates in a border or route, the map builder draws
3822
+ an itinerary from that place. A site page carries it as a linked SVG. A PDF
3823
+ book that includes the place gives its map a separate full page after the
3824
+ entry. A configured Foundry Scene pack carries a gridless map backed by a
3825
+ generated PNG, with Notes linked to importable place journals. Places with no
3826
+ relations create no itinerary output.
3827
+
3784
3828
  **A relation that crosses a package boundary is written in full at both ends.**
3785
3829
  An omitted `<package>` segment is the package being built, in whatever note the
3786
3830
  value is read from — a note of this package's own, or a record a consumer fetched
@@ -286,7 +286,7 @@ over one would teach people to stop reading the report.
286
286
  | `content-build markdown [paths..]` | it reports any markdownlint finding |
287
287
  | `content-build site` | any gate finding (no homepage, a wikilink in frontmatter, an address that cannot be derived, an unusable or unaddressable foreign manifest, a conflicting address) or any table or wikilink error while writing pages — the run stops at the first gate that fires |
288
288
  | `content-build pdf` | it could not build the book for a reason other than a deliberate no-op — never on a reported finding by itself |
289
- | `content-build map` | `--tree` reports an `error`-severity finding (a place with no parent, a parent that does not resolve, a cycle), a named place is not one, the GraphViz engine a drawing needs is not installed, or the corpus cannot be indexed — never on a warning |
289
+ | `content-build map` | `--tree` reports an `error`-severity finding (a place with no parent, a parent that does not resolve, a cycle), a named place is not one, the renderer fails, or the corpus cannot be indexed — never on a warning |
290
290
  | `content-build docs item-fields --check` | the generated page differs from what is committed |
291
291
  | `content-build package compile` | pack JSON generation reported any `error`-severity finding (checked, and the compile refused, before any pack is written), or a compiled pack fails its Scene/Level integrity check |
292
292
  | `content-build deps fetch` | the fetch itself throws — it emits no diagnostics |
@@ -739,6 +739,29 @@ compiles to a page, so there is no document to address.
739
739
  The index is derived and disposable; it is written under `build/` and rebuilt
740
740
  from the tree every time.
741
741
 
742
+ ### Calendar files for a setting module
743
+
744
+ A content package with `lore` notes of `subType: calendar` can emit Foundry
745
+ calendar definitions after the content index is built:
746
+
747
+ ```console
748
+ $ npx package-build calendars --calendaria-version 1.4.2
749
+ ✅ 5 calendars emitted (10 files).
750
+ ```
751
+
752
+ The command writes `build/calendars/<shortcode>.json` and
753
+ `build/calendars/<shortcode>.calendaria.json` for each calendar. The first is a
754
+ calendar definition; the second wraps the same definition for import through
755
+ Calendaria's settings. The version argument names the Calendaria import format
756
+ the package targets. The envelope uses a fixed `exportedAt` sentinel because
757
+ these files are reproducible build artifacts. A package with no calendars gets
758
+ an empty `build/calendars/` directory.
759
+
760
+ To ship the files in a Foundry package, add
761
+ `{ from: build/calendars, to: calendars }` to `packageBuild.assets` and run the
762
+ calendar command before `package-build assets`. Running the command requires
763
+ the current content index; regenerate it after changing calendar notes.
764
+
742
765
  ## Step 11 — the release archive
743
766
 
744
767
  ```console
@@ -100,6 +100,7 @@ import { locateFrontmatterKey } from "./retired-fields.mjs";
100
100
  // present before this pass reads `data:` for anything else — see
101
101
  // `engine/being-age.mjs`.
102
102
  import { applyComputedBeingAge, presentAmongRecords } from "./being-age.mjs";
103
+ import { reckoningContext } from "./reckoning-markers.mjs";
103
104
 
104
105
  /**
105
106
  * The tallies one pass accumulates while walking the tree.
@@ -1078,6 +1079,7 @@ export class BasePackCompiler {
1078
1079
  // to have the body anyway. So the read is one this pass was already
1079
1080
  // making; what it no longer does is decide for itself which files to
1080
1081
  // make it over.
1082
+ const dates = reckoningContext({ notes: this.corpus.records });
1081
1083
  for (const record of this.corpus.records) {
1082
1084
  if (!isNoteRecord(record)) continue;
1083
1085
  const absPath = noteFile(this.contentBase, record);
@@ -1091,7 +1093,7 @@ export class BasePackCompiler {
1091
1093
  stats.skippedOther++;
1092
1094
  continue;
1093
1095
  }
1094
- applyComputedBeingAge(fm, this.worldPresent);
1096
+ applyComputedBeingAge(fm, this.worldPresent, dates);
1095
1097
  // The retired frontmatter fields, refused before `selects` so a
1096
1098
  // note is answered whichever pass would have claimed it — and
1097
1099
  // whatever the declared value says.
@@ -36,17 +36,10 @@
36
36
  *
37
37
  * A year subtracted from a year is wrong whenever the birthday has not yet
38
38
  * happened in the present's year — the common shape, not the exception. So
39
- * {@link computeAge} compares `born`'s month and day against `present`'s,
40
- * **ordinally**, and drops a year where born's has not yet arrived. That
41
- * ordinal comparison needs no month-length table: two dates written in one
42
- * calendar order the same way whether the comparison counts days-of-year
43
- * through the calendar's month lengths or simply reads `(month, day)` as a
44
- * pair, because a calendar's months keep the same lengths every year — this
45
- * package's calendars carry no leap rule (`calendar-notes.mjs`'s
46
- * `years.leapYear` is always `null`). So the comparison is calendar-agnostic
47
- * by construction, and correct for a twelve-month calendar of thirty-day
48
- * months, a ten-month calendar, or none at all — a bare `born` with no era
49
- * names no calendar to consult in the first place.
39
+ * {@link computeAge} compares the two dates' canonical day ordinals where an
40
+ * era marker supplies them. The birthday that still lies ahead in the current
41
+ * year does not count as a completed year. An unmarked slash date has no
42
+ * calendar to convert, so it uses its written month and day as its ordinal.
50
43
  *
51
44
  * ## `~` follows the convention {@link module:engine/note-dates} sets
52
45
  *
@@ -65,6 +58,7 @@
65
58
  import { worldInvariants } from "./calendar-notes.mjs";
66
59
  import { positionOfFrontmatterPath } from "./diagnostics.mjs";
67
60
  import { parseNoteDate } from "./note-dates.mjs";
61
+ import { reckoningContext } from "./reckoning-markers.mjs";
68
62
  // `born`'s retired spelling is still read during its retirement window — see
69
63
  // `docs/content-format.md`'s "`birthday:` is the retired spelling of `born:`"
70
64
  // — so a being on the old spelling still computes an age rather than reading
@@ -124,23 +118,21 @@ export function parseAgeMagnitude(raw) {
124
118
  * @returns {number|null} The age, or `null` where `born` is `unknown` or
125
119
  * absent, where no present is declared, or where either date fails to parse.
126
120
  */
127
- export function computeAge(bornRaw, presentRaw) {
121
+ export function computeAge(bornRaw, presentRaw, dateContext = {}) {
128
122
  if (bornRaw === undefined || bornRaw === null) return null;
129
123
  if (presentRaw === undefined || presentRaw === null) return null;
130
124
 
131
- const born = parseNoteDate(bornRaw, { allowUnknown: true }).date;
125
+ const born = parseNoteDate(bornRaw, { ...dateContext, allowUnknown: true }).date;
132
126
  if (!born || !born.known || born.canonicalYear == null) return null;
133
127
 
134
- const present = parseNoteDate(presentRaw, { allowUnknown: false }).date;
128
+ const present = parseNoteDate(presentRaw, { ...dateContext, allowUnknown: false }).date;
135
129
  if (!present || !present.known || present.canonicalYear == null) return null;
136
130
 
137
- // Ordinal position within the year, day of year or not: see the module
138
- // doc for why `(month, day)` alone orders correctly here. Missing month or
139
- // day reads as the year's first day — the earliest a birthday could fall —
140
- // so a `born` written to less precision than `present` never counts as
141
- // "not yet happened".
142
- const bornOrdinal = (born.month ?? 1) * 100 + (born.day ?? 1);
143
- const presentOrdinal = (present.month ?? 1) * 100 + (present.day ?? 1);
131
+ // A resolved marker supplies a canonical day. An unmarked slash date uses
132
+ // its written month and day. Missing parts read as the first day that the
133
+ // value can name.
134
+ const bornOrdinal = born.canonicalDay ?? (born.month ?? 1) * 100 + (born.day ?? 1);
135
+ const presentOrdinal = present.canonicalDay ?? (present.month ?? 1) * 100 + (present.day ?? 1);
144
136
  const notYetHappened = bornOrdinal > presentOrdinal;
145
137
 
146
138
  return present.canonicalYear - born.canonicalYear - (notYetHappened ? 1 : 0);
@@ -163,7 +155,7 @@ export function computeAge(bornRaw, presentRaw) {
163
155
  * `undefined`/`null` where it states none.
164
156
  * @returns {void}
165
157
  */
166
- export function applyComputedBeingAge(fm, presentRaw) {
158
+ export function applyComputedBeingAge(fm, presentRaw, dateContext = {}) {
167
159
  if (!isBeingType(fm?.type)) return;
168
160
  const data = dataOf(fm);
169
161
  if (!data) return;
@@ -179,7 +171,7 @@ export function applyComputedBeingAge(fm, presentRaw) {
179
171
  }
180
172
 
181
173
  const born = readAliasedField(fm, "born", { inData: true });
182
- const computed = computeAge(born, presentRaw);
174
+ const computed = computeAge(born, presentRaw, dateContext);
183
175
  if (computed !== null) data.age = computed;
184
176
  }
185
177
 
@@ -251,7 +243,7 @@ export function checkBeingAge(note, { index } = {}) {
251
243
  if (!index) return [];
252
244
 
253
245
  const present = worldInvariants(index).present;
254
- const computed = computeAge(bornRaw, present);
246
+ const computed = computeAge(bornRaw, present, reckoningContext(index));
255
247
  if (computed === null) return [];
256
248
 
257
249
  const authored = parseAgeMagnitude(ageRaw);