@heroiclands/package-build 9.0.0 → 10.0.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 (78) hide show
  1. package/CHANGELOG.md +721 -0
  2. package/CONTENT.md +273 -13
  3. package/bin/content-build.mjs +437 -7
  4. package/content-config.mjs +259 -28
  5. package/docs/content-format.md +1418 -0
  6. package/engine/address-charset.mjs +62 -0
  7. package/engine/alias-index.mjs +153 -0
  8. package/engine/base-compiler.mjs +194 -4
  9. package/engine/content-address.mjs +4 -4
  10. package/engine/content-format-check.mjs +570 -0
  11. package/engine/content-format.mjs +253 -0
  12. package/engine/content-links.mjs +132 -56
  13. package/engine/content-lint.mjs +8 -1
  14. package/engine/diagnostics.mjs +33 -0
  15. package/engine/document-subtypes.mjs +440 -0
  16. package/engine/field-spec.mjs +49 -43
  17. package/engine/frontmatter-lint.mjs +351 -28
  18. package/engine/generate.mjs +32 -4
  19. package/engine/helpers.mjs +41 -29
  20. package/engine/ids.mjs +19 -1
  21. package/engine/index.mjs +15 -0
  22. package/engine/item-registry.mjs +72 -5
  23. package/engine/kb-manifest.mjs +36 -7
  24. package/engine/map-notes.mjs +34 -18
  25. package/engine/note-claims.mjs +383 -0
  26. package/engine/note-vocabulary.mjs +678 -0
  27. package/engine/pack-config.mjs +39 -22
  28. package/engine/pack-router.mjs +17 -6
  29. package/engine/prose-lint.mjs +55 -3
  30. package/engine/retired-fields.mjs +117 -3
  31. package/engine/scenes.mjs +19 -1
  32. package/engine/schema-check.mjs +347 -3
  33. package/engine/site-build.mjs +1 -1
  34. package/engine/site-index.mjs +38 -21
  35. package/engine/system-block.mjs +513 -0
  36. package/engine/web-wikilinks.mjs +112 -80
  37. package/engine/wikilink-syntax.mjs +30 -0
  38. package/engine/wikilinks.mjs +67 -51
  39. package/package.json +6 -2
  40. package/sohl/actors.mjs +249 -36
  41. package/sohl/document-subtypes.mjs +82 -0
  42. package/sohl/index.mjs +3 -0
  43. package/sohl/items.mjs +110 -14
  44. package/sohl/note-schemas.mjs +11 -7
  45. package/types/content-config.d.mts +48 -4
  46. package/types/engine/address-charset.d.mts +45 -0
  47. package/types/engine/alias-index.d.mts +122 -0
  48. package/types/engine/base-compiler.d.mts +132 -4
  49. package/types/engine/content-address.d.mts +2 -2
  50. package/types/engine/content-format-check.d.mts +163 -0
  51. package/types/engine/content-format.d.mts +101 -0
  52. package/types/engine/content-links.d.mts +16 -1
  53. package/types/engine/content-lint.d.mts +6 -0
  54. package/types/engine/diagnostics.d.mts +29 -0
  55. package/types/engine/document-subtypes.d.mts +233 -0
  56. package/types/engine/field-spec.d.mts +76 -23
  57. package/types/engine/frontmatter-lint.d.mts +47 -2
  58. package/types/engine/generate.d.mts +14 -1
  59. package/types/engine/helpers.d.mts +21 -13
  60. package/types/engine/ids.d.mts +10 -0
  61. package/types/engine/index.d.mts +5 -0
  62. package/types/engine/item-registry.d.mts +21 -2
  63. package/types/engine/kb-manifest.d.mts +35 -8
  64. package/types/engine/map-notes.d.mts +21 -11
  65. package/types/engine/note-claims.d.mts +113 -0
  66. package/types/engine/note-vocabulary.d.mts +251 -0
  67. package/types/engine/pack-config.d.mts +4 -3
  68. package/types/engine/pack-router.d.mts +4 -4
  69. package/types/engine/prose-lint.d.mts +6 -2
  70. package/types/engine/retired-fields.d.mts +73 -2
  71. package/types/engine/schema-check.d.mts +182 -0
  72. package/types/engine/system-block.d.mts +281 -0
  73. package/types/engine/web-wikilinks.d.mts +23 -12
  74. package/types/engine/wikilink-syntax.d.mts +29 -0
  75. package/types/sohl/actors.d.mts +62 -6
  76. package/types/sohl/document-subtypes.d.mts +14 -0
  77. package/types/sohl/index.d.mts +1 -0
  78. package/types/sohl/items.d.mts +21 -0
package/CONTENT.md CHANGED
@@ -167,7 +167,7 @@ file can be asked for rather than told:
167
167
  | `rootDir` | the directory the configuration file sits in |
168
168
  | `foundryPackage` | the `name` of the adjacent `package.json`, verbatim |
169
169
  | `stats.systemVersion` | a **system**: that `package.json`'s `version`. A **module**: the `verified` version of the system it declares a relationship with |
170
- | `itemBuilders` | the named registry (`sohl`), required lazily so importing costs nothing |
170
+ | `itemBuilders` | the named registry (`sohl`) — or a list of names — required lazily so importing costs nothing |
171
171
 
172
172
  **Authoring any of the first three is an error**, not an override. Each was
173
173
  previously transcribed from a file that already stated it, and a transcription
@@ -190,6 +190,23 @@ cross-package wikilink writes to reach one of these notes. It is the
190
190
  repository's identity in the address space — not a filter — and a note does not
191
191
  restate it.
192
192
 
193
+ Because it is a segment of an address, the value is **validated** rather than
194
+ taken as written, and a violation fails the build naming the line it is on:
195
+
196
+ - **Alphanumeric** (`^[A-Za-z0-9]+$`). An address is read by counting
197
+ hyphen-separated segments, so the hyphen has to be purely a separator — which
198
+ is why `harn-adventures` is configured as `harnadventures`. This is the same
199
+ rule `shortcode` is already held to, and the two are one constant.
200
+ - **Not a note type.** The package and the type are adjacent segments, and the
201
+ two vocabularies are kept disjoint so a reader never has to decide which slot
202
+ a name is filling. `doc`, `being`, every map type, and every item type this
203
+ repository declares — with its `doc`-prefixed documentation form — are
204
+ refused.
205
+
206
+ ```text
207
+ package-build.config.yaml:1:1: error: package-build config: `contentPackage` is `harn-adventures`, which is not alphanumeric. It is the first segment of every address this package publishes (`harn-adventures-<type>-<shortcode>`), and an address is read by counting hyphen-separated segments — so anything outside `[A-Za-z0-9]` here makes those addresses unreadable rather than merely ugly. `harn-adventures` became `harnadventures`.
208
+ ```
209
+
193
210
  **`package:` in a note's frontmatter is retired, and declaring it fails the
194
211
  build**, naming the file, whatever the value says. An agreeing declaration is
195
212
  refused exactly as a disagreeing one is: there is no value that makes writing
@@ -354,6 +371,33 @@ object. Supplying `itemBuilders` is therefore all a consumer does to define an
354
371
  item type of its own; a table this package ships is one possible value, not the
355
372
  one the compiler holds.
356
373
 
374
+ **A tree feeding two systems declares a set of registries.** One registry is
375
+ also a ceiling: the accepted vocabulary is its keys, so a type only the _other_
376
+ system knows — `spell` and `invocation` are HM3's, `mysticalability` is SoHL's —
377
+ cannot be accepted at all. So `itemBuilders` takes either form:
378
+
379
+ ```yaml
380
+ itemBuilders: sohl # one registry, and what every existing configuration says
381
+ itemBuilders: [sohl, hm3] # a set; the vocabulary is their union
382
+ ```
383
+
384
+ A registry's **name is the system it belongs to**, which is what lets a data
385
+ configuration declare a set without naming each system twice. In an `.mjs`
386
+ configuration the same set is written out:
387
+
388
+ ```js
389
+ itemBuilders: [
390
+ { system: "sohl", builders: SOHL_ITEM_BUILDERS },
391
+ { system: "hm3", builders: HM3_ITEM_BUILDERS },
392
+ ],
393
+ ```
394
+
395
+ A type **both** registries declare — `skill` is one name over two data models —
396
+ keeps a builder on each side. `itemBuilder(type, system)` and `itemArt(type,
397
+ system)` take the system that is asking; asking without one, for a type more
398
+ than one registry declares, **throws** rather than answering with whichever was
399
+ declared first.
400
+
357
401
  **Configuration is the source, and the manifest is generated from it.** That
358
402
  arrow used to point the other way: `paths.packageManifest` said where a
359
403
  hand-authored `system.template.json` lived, and the package-id guard and the
@@ -464,12 +508,115 @@ that same map, so there is still exactly one map — and the drift a test used t
464
508
  watch for is now unrepresentable, because building the registry throws if a type
465
509
  has no art.
466
510
 
511
+ ## The per-system block
512
+
513
+ A note is **system-agnostic**. The only system-specific things it carries are
514
+ the properties named after a system, and one note may carry more than one — a
515
+ `being` in `harn-ensemble` compiles into a SoHL `being` _and_ an HM3
516
+ `character`. Within a system's block:
517
+
518
+ | property | maps to |
519
+ | ------------------ | ------------------------------------------------------------- |
520
+ | `<system>.system` | `document.system` — the DataModel schema, verbatim paths |
521
+ | `<system>.type` | `document.type` — the subtype the note compiles into |
522
+ | `<system>.img` | `document.img` |
523
+ | `<system>.items` | `document.items` — actors only |
524
+ | `<system>.effects` | `document.effects` |
525
+ | `<system>.flags` | `document.flags` |
526
+ | `<system>.pack` | _nothing on the document_ — a build directive naming the pack |
527
+
528
+ Everything else a system declares sits directly under the block. `kbcat` is
529
+ toolchain vocabulary, and the _generators_ `items` and `attributes` expand into
530
+ embedded documents rather than mapping anywhere, so neither has a `system` path
531
+ to be written at.
532
+
533
+ `archetype` is authored there too, and is the one such key that _is_ a field:
534
+ the builder writes it to `system.archetype` — a number is an archetype at that
535
+ priority, `null` is not an archetype — exactly as `portrait` reaches
536
+ `sohl.system.portrait` from a shared top-level property. Its authored position
537
+ is unchanged (#126).
538
+
539
+ ```yaml
540
+ type: being # the content type — system-agnostic
541
+ pack: actors # shared: unless a block says otherwise
542
+ portrait: kaldor.webp # shared: reaches both systems' fields, differently named
543
+ hm3:
544
+ type: character # this system's document subtype
545
+ pack: actors-hm3 # overrides the shared one, for HM3 only
546
+ system: # → document.system, verbatim
547
+ species: human
548
+ sunsign: ulandus
549
+ attributes: { str: 10, sta: 14 } # a generator, not a system field
550
+ sohl:
551
+ type: being
552
+ system:
553
+ currentMoveMedium: walk
554
+ archetype: 1
555
+ ```
556
+
557
+ **The shared fallback is declared, not name-matched.** `sohl.system.portrait`
558
+ and `hm3.system.bioImage` both default from one shared property, and they are
559
+ two real fields with different names — SoHL's `Actor.being` and HM3's
560
+ `Actor.character` share **no** field name at all, so a rule matching on spelling
561
+ would never fire. Each field declares its source instead, and resolution for a
562
+ system `S` is:
563
+
564
+ 1. `S.system.<to>` — authored directly, wins outright;
565
+ 2. `S.<name>` — the legacy in-block position, until the corpus moves off it;
566
+ 3. the shared top-level property the field declares as its source, which may be
567
+ a **dotted path** (`data.portrait`) rather than a sibling key;
568
+ 4. the field's own default.
569
+
570
+ `FieldSpec.name` is that declared source. It used to mean "frontmatter key under
571
+ `sohl:`", which is the degenerate case where source and destination happen to
572
+ share a name.
573
+
574
+ **`<system>.system` is written through verbatim**, at the DataModel's own paths,
575
+ with no renaming layer. A key the system's published `schema.json` does not
576
+ declare for the subtype the note compiles into is an **error naming the note**,
577
+ not a silent drop: Foundry discards an unknown `system` key at construction
578
+ without a word, so the alternative is a field the author wrote and nobody will
579
+ ever see. A path a declared field already writes is left to that field, so the
580
+ value goes through one coercion rather than two.
581
+
582
+ **What the compiler writes on its own is checked too.** A compiled document
583
+ carries keys no field declaration and no note ever names — `shortcode`,
584
+ `archetype`, `actionDefs`, `notes`, `docHtml` — because the pass writes them
585
+ itself, and they were compared against nothing: the declaration-derived check
586
+ reads `itemBuilders`, the note-side check reads `<system>.system`, and these are
587
+ in neither. A compile now reads the `system` block each pass **assembled** and
588
+ checks its keys against the same published `schema.json`, so the emitted set is
589
+ observed rather than listed and a compiler that grows a key is covered without
590
+ anyone remembering to add it. A key the schema does not declare is an error,
591
+ reported once per subtype rather than once per document, and the message says
592
+ which of two things wrote it: a `fields:` entry, which the repository can change,
593
+ or the compiler, which it cannot — that one means the build is running ahead of
594
+ the system it compiles for, and the fixes are to declare the field there or to
595
+ hold this package at a build that does not write it. A subtree the schema
596
+ declares but describes no further — a discriminated `TypedSchemaField`, stored
597
+ flat — is left alone rather than reported wholesale.
598
+
599
+ **A pack that declares a `system:` takes only notes carrying that block.** A
600
+ note that says nothing about a system has no system data, and compiling it there
601
+ would emit a hollow document — a subtype, and none of the fields the subtype
602
+ exists for. The build fails naming the note and the pack. A pack declaring no
603
+ system constrains nothing, and a pass whose document is not system data at all —
604
+ journals, macros, scenes — is not subject to the rule.
605
+
606
+ **`(type, shortcode)` resolves inside one system's catalogue.** A being names its
607
+ embedded items by address and never by pack, so the Item packs are read as one
608
+ address space; with two systems in the tree that space stops being one, because
609
+ `skill:sword` exists under both names. An Actor pass reads the Item packs of its
610
+ **own** system plus the system-neutral ones.
611
+
467
612
  ## Command line
468
613
 
469
614
  ```
470
615
  npx content-build package <compile|unpack|clean> [pack] [entry]
471
616
  npx content-build docs item-fields [--out <path>] [--title <title>]
472
617
  npx content-build lint [root] [--no-references]
618
+ npx content-build content-format schema --schema <system>=<path>
619
+ npx content-build content-format notes [root] [--strict]
473
620
  npx content-build links [root] [--manifests <dir>]
474
621
  npx content-build format [paths..] [--write]
475
622
  npx content-build markdown [paths..] [--fix]
@@ -479,18 +626,19 @@ npx content-build reachability <dir> [file] [--index <shortcode>]
479
626
  npx content-build addresses diff --from <zip|dir> [--strict]
480
627
  ```
481
628
 
482
- | Command | What it does |
483
- | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
484
- | `package` | Compile the content tree into LevelDB packs, unpack a shipped pack back to JSON, or clean one. See [Install](#install). |
485
- | `docs` | Render a generated reference from the configured registries. `item-fields` is the item-frontmatter page. |
486
- | `lint` | Check a content tree's addresses and its frontmatter. See [Linting a content tree](#linting-a-content-tree). |
487
- | `links` | Check that every link in the tree lands: dead anchors, dead qualified addresses, wikilinks in frontmatter, drifted manifests, and the package homepage's own addresses. |
488
- | `format` | Prettier, with the shared configuration. See [Prose: formatting and markdown](#prose-formatting-and-markdown). |
489
- | `markdown` | markdownlint, with the shared rule set the structure Prettier is indifferent to. |
490
- | `manifest` | Emit this package's cross-package link manifest. See [Publishing a link manifest](#publishing-a-link-manifest). |
491
- | `site` | Publish the content tree as a website. See [Publishing a website](#publishing-a-website). |
492
- | `reachability` | Walk outward from an index note and report what no path reaches, for a tree meant to be navigable from one entry point. |
493
- | `addresses` | Report every published item address this build has stopped publishing. See [Diffing published addresses](#diffing-published-addresses). |
629
+ | Command | What it does |
630
+ | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
631
+ | `package` | Compile the content tree into LevelDB packs, unpack a shipped pack back to JSON, or clean one. See [Install](#install). |
632
+ | `docs` | Render a generated reference from the configured registries. `item-fields` is the item-frontmatter page. |
633
+ | `lint` | Check a content tree's addresses and its frontmatter. See [Linting a content tree](#linting-a-content-tree). |
634
+ | `content-format` | Check the content format specification itself. See [The content format specification](#the-content-format-specification). |
635
+ | `links` | Check that every link in the tree lands: dead anchors, dead qualified addresses, wikilinks in frontmatter, drifted manifests, and the package homepage's own addresses. |
636
+ | `format` | Prettier, with the shared configuration. See [Prose: formatting and markdown](#prose-formatting-and-markdown). |
637
+ | `markdown` | markdownlint, with the shared rule set the structure Prettier is indifferent to. |
638
+ | `manifest` | Emit this package's cross-package link manifest. See [Publishing a link manifest](#publishing-a-link-manifest). |
639
+ | `site` | Publish the content tree as a website. See [Publishing a website](#publishing-a-website). |
640
+ | `reachability` | Walk outward from an index note and report what no path reaches, for a tree meant to be navigable from one entry point. |
641
+ | `addresses` | Report every published item address this build has stopped publishing. See [Diffing published addresses](#diffing-published-addresses). |
494
642
 
495
643
  Every path, pack name and root it needs comes from the consuming repository's
496
644
  `package-build.config.yaml`, so the usual invocation takes no arguments beyond
@@ -609,6 +757,45 @@ builder, so schema and compiler cannot disagree. The hand-written compilers —
609
757
 
610
758
  Nothing here writes. A check reports and an author fixes.
611
759
 
760
+ ### The `data:` container is closed; the top level is not
761
+
762
+ A note's frontmatter has three regions, and only one of them is open (#128):
763
+
764
+ | region | describes | an unknown key is |
765
+ | ---------------- | -------------------------------------------- | ------------------ |
766
+ | top level | the note as a published artefact | passed to the page |
767
+ | `data:` | the subject itself, whatever system reads it | an **error** |
768
+ | `sohl:` / `hm3:` | the subject as one system's documents | an **error** |
769
+
770
+ **Top level is deliberately open**, for the reason the homepage rule above gives
771
+ at length: every key of it is copied into the generated page, so an
772
+ unrecognised one is a Hugo or theme parameter this build has no standing to
773
+ refuse. `description` is the everyday case — not a document field at all, but
774
+ the page's description.
775
+
776
+ **`data:` is deliberately closed**, and that is the point of having it. The
777
+ type-specific facts about a subject — a weapon's weight, an affliction's
778
+ transmission, a being's species — used to sit at the top level, where the
779
+ pass-through rule applied to them too. So a misspelled `wieght` became a theme
780
+ parameter rather than a finding, indistinguishable from a weapon that weighs
781
+ nothing. Under `data:` the same key is reported where it was written, with the
782
+ key it was probably meant to be, drawn from that type's own vocabulary:
783
+
784
+ ```text
785
+ assets/content/Gear/Axe.md:14:5: error: "wieght" is not a `data:` property of a weapongear; the container is closed, so unlike a top-level key it is not passed through to the page. Did you mean "weight"?
786
+ ```
787
+
788
+ **`subType` stays at the top level**, and is closed in its own way: a type
789
+ either declares a `subType` or does not, and a type that does declares its
790
+ values. A `weapon` declares none — SoHL distinguishes a weapon's uses by strike
791
+ mode rather than by kind — so `subType` on one is a finding; a `skill` declares
792
+ ten, so `subType: crafte` is a finding naming `craft`.
793
+
794
+ The vocabulary lives in `engine/note-vocabulary.mjs`, one entry per note type,
795
+ taken from the content-format specification. It is note-format knowledge rather
796
+ than any system's: `data:` holds what is true of the thing, and what a system
797
+ makes of that value is declared in that system's own half.
798
+
612
799
  **A third rule was retired (#79).** Every note used to be required to repeat its
613
800
  own `type-shortcode` address in `aliases:`. That served one reader — Obsidian,
614
801
  so `[[type-shortcode]]` resolved in the editor — and no build ever read it: both
@@ -722,6 +909,79 @@ attempted:
722
909
  cannot be improved: a package homepage is in no link manifest, so there is no
723
910
  better form to write.
724
911
 
912
+ ## The content format specification
913
+
914
+ `docs/content-format.md` is the contract this package honours: how a note
915
+ becomes a Foundry document and a web page. Three frontmatter regions, a note
916
+ vocabulary with its own `type` and `subType`, a declared map onto each system's
917
+ document fields, the precedence between a shared source and a system's override,
918
+ and the wikilink address grammar.
919
+
920
+ **It does not define the `sohl:` or `hm3:` schemas.** Each system defines its
921
+ own, and its published `schema.json` is the authoritative statement of it. The
922
+ format says which shared source feeds which system field; what fields exist, and
923
+ what they mean, belongs to the system.
924
+
925
+ That makes a mapping row **checkable rather than declarative**, and two commands
926
+ check it. Both read the document's own tables, so editing the specification
927
+ changes what they assert — a transcribed copy would be free to drift from the
928
+ prose the moment either was edited, which is the failure they exist to prevent.
929
+
930
+ ### `content-format schema` — the specification against a published schema
931
+
932
+ ```bash
933
+ npx content-build content-format schema --schema sohl=./schema.json
934
+ ```
935
+
936
+ Every `system.*` target the document names must appear in the naming system's
937
+ published `schema.json`, in the `version: 1` shape `package-build schema` emits.
938
+ A target no schema declares is an error naming the note type, the field, the
939
+ system and the version it was checked at — because a field may be perfectly well
940
+ defined on the system's `main` and simply unreleased, and that is the difference
941
+ between "the specification is wrong" and "the schema has not caught up".
942
+
943
+ A target is resolved against the **union** of the system's document subtypes.
944
+ The mapping tables say which field a shared source reaches; _which subtype
945
+ receives it_ is the note-type → subtype map, which does not exist yet — so the
946
+ question asked is the one that can be answered honestly today, and it narrows to
947
+ the subtype when that map lands.
948
+
949
+ `--schema` is repeatable and takes `<system>=<path>`, because a consumer holds
950
+ its system's artifact and this repository holds a committed fixture, and neither
951
+ arrangement should have to pretend to be the other. A system the document maps
952
+ onto but no schema was supplied for is reported as **unchecked**, with a count:
953
+ HM3 publishes no artifact today, so that is the ordinary case for a fifth of the
954
+ claims, and a check that skipped them in silence would read as one that passed.
955
+
956
+ ### `content-format notes` — a content tree against the declared vocabulary
957
+
958
+ ```bash
959
+ npx content-build content-format notes # the configured `paths.content`
960
+ npx content-build content-format notes --strict # and fail on what it finds
961
+ ```
962
+
963
+ Measures every authored note against the per-type `data` tables, and counts the
964
+ findings by class:
965
+
966
+ | class | what it means |
967
+ | ----------------------- | --------------------------------------------------------------------- |
968
+ | `unknown-type` | the format declares no `### type:` section for this note's `type` |
969
+ | `unknown-data-key` | a key in `data:`, which is closed, that the type does not declare |
970
+ | `top-level-data-key` | a declared `data` property written at top level instead |
971
+ | `system-block-data-key` | a declared shared source written straight into a `sohl:`/`hm3:` block |
972
+
973
+ **It reports; it does not fail.** Every authored note predates the format, so a
974
+ failing check would be red in every repository on the day it lands and would
975
+ stay red for the length of the migration — which is a check nobody can act on
976
+ and everybody learns to skip. The counts are the migration's progress bar
977
+ instead, and each class is promoted to fatal, by turning `--strict` on, as it
978
+ reaches zero.
979
+
980
+ What it deliberately leaves alone is a key inside a system block that the format
981
+ says nothing about. Those regions are closed against _the system's_ schema, not
982
+ against this document, and `content-build lint` already checks them against the
983
+ declared fields.
984
+
725
985
  ## Prose: formatting and markdown
726
986
 
727
987
  ```bash