@heroiclands/package-build 17.1.0 → 18.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (153) hide show
  1. package/CHANGELOG.md +1773 -0
  2. package/CONTENT.md +288 -72
  3. package/README.md +1 -1
  4. package/bin/content-build.mjs +218 -137
  5. package/bin/package-build.mjs +43 -0
  6. package/content-config.mjs +62 -67
  7. package/docs/content-format.md +768 -58
  8. package/engine/actor-compiler.mjs +586 -0
  9. package/engine/address-charset.mjs +11 -8
  10. package/engine/address-diff.mjs +266 -22
  11. package/engine/anchored-sections.mjs +83 -0
  12. package/engine/anchors.mjs +83 -0
  13. package/engine/base-compiler.mjs +148 -18
  14. package/engine/bundle-notes.mjs +276 -0
  15. package/engine/bundles.mjs +307 -0
  16. package/engine/code-fences.mjs +103 -0
  17. package/engine/compile-corpus.mjs +89 -0
  18. package/engine/content-address.mjs +267 -5
  19. package/engine/content-format-check.mjs +54 -11
  20. package/engine/content-format.mjs +37 -5
  21. package/engine/content-index.mjs +394 -111
  22. package/engine/content-links.mjs +185 -77
  23. package/engine/content-lint.mjs +232 -14
  24. package/engine/content-package.mjs +2 -1
  25. package/engine/content-tables.mjs +99 -3
  26. package/engine/document-subtypes.mjs +49 -6
  27. package/engine/field-reference.mjs +1 -1
  28. package/engine/field-spec.mjs +56 -6
  29. package/engine/folder-notes.mjs +470 -0
  30. package/engine/foreign-catalog.mjs +195 -4
  31. package/engine/{manifest-emit.mjs → foundry-entries.mjs} +78 -81
  32. package/engine/frontmatter-lint.mjs +245 -19
  33. package/engine/frontmatter.mjs +88 -8
  34. package/engine/generate.mjs +304 -13
  35. package/engine/helpers.mjs +293 -64
  36. package/engine/ids.mjs +137 -2
  37. package/engine/index-records.mjs +126 -0
  38. package/engine/index.mjs +23 -5
  39. package/engine/item-compiler.mjs +349 -0
  40. package/engine/item-docs.mjs +7 -1
  41. package/engine/item-registry.mjs +6 -0
  42. package/engine/journals.mjs +93 -27
  43. package/engine/macros.mjs +4 -2
  44. package/engine/metadata-index.mjs +495 -0
  45. package/engine/note-claims.mjs +146 -25
  46. package/engine/note-ids.mjs +112 -0
  47. package/engine/note-renames.mjs +134 -0
  48. package/engine/note-schemas.mjs +25 -0
  49. package/engine/note-vocabulary.mjs +110 -27
  50. package/engine/pack-config.mjs +4 -0
  51. package/engine/pack-router.mjs +23 -0
  52. package/engine/retired-fields.mjs +137 -3
  53. package/engine/scenes.mjs +66 -34
  54. package/engine/schema-check.mjs +58 -17
  55. package/engine/site-build.mjs +71 -12
  56. package/engine/site-index.mjs +20 -7
  57. package/engine/sql-tables.mjs +485 -0
  58. package/engine/subtype-registry.mjs +102 -0
  59. package/engine/system-block.mjs +96 -12
  60. package/engine/systems.mjs +201 -0
  61. package/engine/web-wikilinks.mjs +50 -20
  62. package/engine/wikilink-syntax.mjs +17 -8
  63. package/engine/wikilinks.mjs +124 -51
  64. package/engine/yaml-lint.mjs +282 -0
  65. package/hm3/actors.mjs +352 -0
  66. package/hm3/default-item-art.mjs +75 -0
  67. package/hm3/document-subtypes.mjs +134 -0
  68. package/hm3/index.mjs +56 -0
  69. package/hm3/item-builders.mjs +84 -0
  70. package/hm3/item-fields.mjs +180 -0
  71. package/hm3/items.mjs +96 -0
  72. package/hm3/template-priority.mjs +85 -0
  73. package/labels.mjs +191 -0
  74. package/manifest.mjs +43 -2
  75. package/package.json +19 -4
  76. package/release.mjs +62 -7
  77. package/sohl/actors.mjs +33 -487
  78. package/sohl/being-info.mjs +16 -7
  79. package/sohl/default-item-art.mjs +14 -3
  80. package/sohl/document-subtypes.mjs +16 -10
  81. package/sohl/item-builders.mjs +14 -5
  82. package/sohl/item-fields.mjs +68 -7
  83. package/sohl/items.mjs +44 -258
  84. package/sohl/note-schemas.mjs +2 -2
  85. package/types/content-config.d.mts +21 -42
  86. package/types/engine/actor-compiler.d.mts +204 -0
  87. package/types/engine/address-charset.d.mts +11 -8
  88. package/types/engine/address-diff.d.mts +53 -5
  89. package/types/engine/anchored-sections.d.mts +21 -0
  90. package/types/engine/anchors.d.mts +20 -0
  91. package/types/engine/base-compiler.d.mts +17 -17
  92. package/types/engine/bundle-notes.d.mts +173 -0
  93. package/types/engine/bundles.d.mts +60 -0
  94. package/types/engine/code-fences.d.mts +43 -0
  95. package/types/engine/compile-corpus.d.mts +32 -0
  96. package/types/engine/content-address.d.mts +205 -5
  97. package/types/engine/content-format-check.d.mts +6 -2
  98. package/types/engine/content-format.d.mts +57 -1
  99. package/types/engine/content-index.d.mts +64 -80
  100. package/types/engine/content-links.d.mts +52 -7
  101. package/types/engine/content-lint.d.mts +10 -1
  102. package/types/engine/content-package.d.mts +2 -1
  103. package/types/engine/content-tables.d.mts +21 -39
  104. package/types/engine/document-subtypes.d.mts +37 -3
  105. package/types/engine/field-spec.d.mts +76 -5
  106. package/types/engine/folder-notes.d.mts +159 -0
  107. package/types/engine/foreign-catalog.d.mts +53 -0
  108. package/types/engine/{manifest-emit.d.mts → foundry-entries.d.mts} +16 -38
  109. package/types/engine/frontmatter-lint.d.mts +10 -2
  110. package/types/engine/frontmatter.d.mts +64 -0
  111. package/types/engine/generate.d.mts +38 -0
  112. package/types/engine/helpers.d.mts +94 -30
  113. package/types/engine/ids.d.mts +96 -0
  114. package/types/engine/index-records.d.mts +68 -0
  115. package/types/engine/index.d.mts +9 -3
  116. package/types/engine/item-compiler.d.mts +131 -0
  117. package/types/engine/journals.d.mts +47 -9
  118. package/types/engine/metadata-index.d.mts +226 -0
  119. package/types/engine/note-claims.d.mts +57 -11
  120. package/types/engine/note-ids.d.mts +38 -0
  121. package/types/engine/note-renames.d.mts +102 -0
  122. package/types/engine/note-vocabulary.d.mts +44 -8
  123. package/types/engine/retired-fields.d.mts +75 -0
  124. package/types/engine/scenes.d.mts +3 -2
  125. package/types/engine/schema-check.d.mts +25 -4
  126. package/types/engine/site-build.d.mts +4 -4
  127. package/types/engine/site-index.d.mts +1 -1
  128. package/types/engine/sql-tables.d.mts +185 -0
  129. package/types/engine/subtype-registry.d.mts +49 -0
  130. package/types/engine/system-block.d.mts +40 -1
  131. package/types/engine/systems.d.mts +106 -0
  132. package/types/engine/web-wikilinks.d.mts +4 -2
  133. package/types/engine/wikilink-syntax.d.mts +10 -3
  134. package/types/engine/wikilinks.d.mts +41 -13
  135. package/types/engine/yaml-lint.d.mts +107 -0
  136. package/types/hm3/actors.d.mts +48 -0
  137. package/types/hm3/default-item-art.d.mts +42 -0
  138. package/types/hm3/document-subtypes.d.mts +24 -0
  139. package/types/hm3/index.d.mts +7 -0
  140. package/types/hm3/item-builders.d.mts +11 -0
  141. package/types/hm3/item-fields.d.mts +12 -0
  142. package/types/hm3/items.d.mts +23 -0
  143. package/types/hm3/template-priority.d.mts +21 -0
  144. package/types/manifest.d.mts +22 -1
  145. package/types/release.d.mts +9 -4
  146. package/types/sohl/actors.d.mts +7 -74
  147. package/types/sohl/being-info.d.mts +8 -27
  148. package/types/sohl/default-item-art.d.mts +5 -3
  149. package/types/sohl/items.d.mts +17 -32
  150. package/engine/foreign-manifests.mjs +0 -126
  151. package/engine/kb-manifest.mjs +0 -490
  152. package/types/engine/foreign-manifests.d.mts +0 -43
  153. package/types/engine/kb-manifest.d.mts +0 -241
@@ -67,8 +67,16 @@ import { loadPackConfig } from "../engine/pack-config.mjs";
67
67
  import {
68
68
  fetchAllCatalogs,
69
69
  fetchCatalogFromPath,
70
+ fetchAllMetadata,
71
+ fetchMetadataFromPath,
70
72
  itemCatalogRelationships,
71
73
  } from "../engine/foreign-catalog.mjs";
74
+ import {
75
+ metadataRelationships,
76
+ cachedIndexPath,
77
+ unaddressableForeignPackages,
78
+ formatUnaddressableFinding,
79
+ } from "../engine/metadata-index.mjs";
72
80
  import { renderItemFieldReference } from "../engine/field-reference.mjs";
73
81
  import { lintContentTree } from "../engine/content-lint.mjs";
74
82
  import { lintFrontmatter } from "../engine/frontmatter-lint.mjs";
@@ -91,19 +99,27 @@ import { NOTE_SCHEMAS } from "../sohl/note-schemas.mjs";
91
99
  // The shipped declarations, so this repository can check its own specification
92
100
  // against them without standing up a consumer's configuration (#136).
93
101
  import { ITEM_FIELDS } from "../sohl/item-fields.mjs";
102
+ import { HM3_ITEM_FIELDS } from "../hm3/item-fields.mjs";
94
103
  // The engine's own types, merged under the registry's so the vocabulary stands
95
104
  // in a package that configures no `itemBuilders` at all (#51).
96
105
  import { ENGINE_NOTE_SCHEMAS } from "../engine/note-schemas.mjs";
106
+ import { schemaSubtypeOf } from "../engine/subtype-registry.mjs";
97
107
  import { NOTE_VOCABULARY } from "../engine/note-vocabulary.mjs";
98
108
  import { checkFormatting, lintMarkdown } from "../engine/prose-lint.mjs";
99
- import { emitLinkManifest } from "../engine/manifest-emit.mjs";
100
- import { emitContentIndex } from "../engine/content-index.mjs";
109
+ import {
110
+ authoredFrontmatter,
111
+ emitContentIndex,
112
+ indexRecordsFor,
113
+ isNoteRecord,
114
+ noteFile,
115
+ } from "../engine/content-index.mjs";
101
116
  import {
102
117
  buildSite,
103
118
  gatesFailed,
104
119
  formatUnaddressableFinding as formatUnaddressable,
105
120
  } from "../engine/site-build.mjs";
106
121
  import { auditLinks, buildLinkIndex, walkReachability } from "../engine/content-links.mjs";
122
+ import { prepareTreeSqlTables } from "../engine/sql-tables.mjs";
107
123
  // The one place a link finding is worded, shared with both builds (#184).
108
124
  import { linkFindingMessage } from "../engine/wikilink-syntax.mjs";
109
125
  import {
@@ -115,16 +131,12 @@ import { reportFindings } from "./report.mjs";
115
131
  import {
116
132
  readItemAddresses,
117
133
  diffItemAddresses,
134
+ declaredPredecessors,
118
135
  noteFilesById,
119
136
  locateAddressFinding,
120
137
  addressFindingMessage,
121
138
  } from "../engine/address-diff.mjs";
122
139
  import { itemPackJsonDirs } from "../engine/generate.mjs";
123
- import { walkMarkdownTree } from "../engine/helpers.mjs";
124
- import {
125
- formatUnaddressableFinding,
126
- unaddressableForeignPackages,
127
- } from "../engine/foreign-manifests.mjs";
128
140
 
129
141
  /**
130
142
  * The packs `unpack` extracts.
@@ -217,7 +229,7 @@ function readRawNote(file) {
217
229
  *
218
230
  * @type {Record<string, Record<string, readonly object[]>>}
219
231
  */
220
- const SHIPPED_ITEM_FIELDS = { sohl: ITEM_FIELDS };
232
+ const SHIPPED_ITEM_FIELDS = { sohl: ITEM_FIELDS, hm3: HM3_ITEM_FIELDS };
221
233
 
222
234
  const argv = yargs(hideBin(process.argv))
223
235
  .command(packageCommand())
@@ -228,7 +240,6 @@ const argv = yargs(hideBin(process.argv))
228
240
  .command(linksCommand())
229
241
  .command(formatCommand())
230
242
  .command(markdownCommand())
231
- .command(manifestCommand())
232
243
  .command(contentIndexCommand())
233
244
  .command(siteCommand())
234
245
  .command(reachabilityCommand())
@@ -647,14 +658,33 @@ function contentFormatNotesCommand() {
647
658
  const root = argv.root ?? config.paths.content;
648
659
  const format = specFrom(argv);
649
660
 
650
- const notes = [];
651
- for (const { frontmatter, absPath } of walkMarkdownTree(root, {
661
+ // The corpus from the index, like every other check (#243).
662
+ // A report measuring the tree against the declared vocabulary
663
+ // has to be looking at the same tree the compile will, or its
664
+ // counts describe a corpus nobody builds.
665
+ const corpusProblems = [];
666
+ const records = indexRecordsFor({
667
+ contentBase: root,
668
+ config,
652
669
  skipDirectories: config.skipDirectories,
653
- })) {
654
- if (!frontmatter || typeof frontmatter.type !== "string") continue;
670
+ problems: corpusProblems,
671
+ });
672
+ for (const problem of corpusProblems) emitDiagnostic(problem);
673
+ if (corpusProblems.length) process.exitCode = 1;
674
+
675
+ const notes = [];
676
+ for (const record of records) {
677
+ // A documentation journal is a document this tree emits,
678
+ // not a note in it, and has no authored frontmatter to
679
+ // measure.
680
+ if (!isNoteRecord(record) || typeof record.type !== "string") continue;
681
+ const absPath = noteFile(root, record);
655
682
  notes.push({
656
683
  file: absPath,
657
- fm: frontmatter,
684
+ // What the author wrote, never the keys the index
685
+ // derived — this measures a note against a vocabulary,
686
+ // and `address:` is in no vocabulary.
687
+ fm: authoredFrontmatter(record),
658
688
  raw: fs.readFileSync(absPath, "utf8"),
659
689
  });
660
690
  }
@@ -706,29 +736,56 @@ function lintCommand() {
706
736
  type: "boolean",
707
737
  default: true,
708
738
  });
709
- yargs.option("manifests", {
710
- describe:
711
- "Directory of vendored foreign link manifests, for the reference check. Defaults to the configured `paths.manifests`.",
712
- type: "string",
713
- });
714
739
  },
715
- handler: (argv) => {
740
+ handler: async (argv) => {
716
741
  try {
717
742
  const config = loadPackConfig();
718
743
  const root = argv.root ?? config.paths.content;
719
- const manifestDir = argv.manifests ?? config.paths.manifests;
744
+
745
+ // The corpus, enumerated once for this command and handed to
746
+ // every pass below, rather than derived again by each (#243).
747
+ // The address lint reads it, the `sql` tables select over it
748
+ // and the link index is built from it, so no two findings this
749
+ // command reports can be drawn from different ideas of which
750
+ // files the content is.
751
+ //
752
+ // A note the index cannot record is reported like any other
753
+ // finding rather than thrown (#243): one malformed note must
754
+ // not take every other finding in the tree with it, and the
755
+ // reader needs a line to open, not a stack.
756
+ const corpusProblems = [];
757
+ const records = indexRecordsFor({
758
+ contentBase: root,
759
+ config,
760
+ skipDirectories: config.skipDirectories,
761
+ problems: corpusProblems,
762
+ });
763
+ for (const problem of corpusProblems) emitDiagnostic(problem);
764
+ // An error whatever the command's own strictness: the note is
765
+ // absent from every answer below, so reporting it and exiting 0
766
+ // would call the tree clean while silently omitting a note.
767
+ if (corpusProblems.length) process.exitCode = 1;
720
768
 
721
769
  // The package is passed for the homepage rule (#52), which
722
770
  // names the address a tree with no front page fails to serve.
723
771
  const addresses = lintContentTree(root, {
724
772
  contentPackage: config.contentPackage,
773
+ skipDirectories: config.skipDirectories,
774
+ config,
775
+ records,
725
776
  });
726
777
  // One index, built once, for the reference check. It is the
727
778
  // same resolver the wikilink audit uses, so a frontmatter
728
779
  // reference and a body link answer the same way.
729
780
  const index = buildLinkIndex(root, {
730
- manifestDir,
781
+ config,
782
+ records,
731
783
  skipDirectories: config.skipDirectories,
784
+ sqlTables: await prepareTreeSqlTables(root, {
785
+ config,
786
+ records,
787
+ skipDirectories: config.skipDirectories,
788
+ }),
732
789
  });
733
790
  const frontmatter = lintFrontmatter(index, {
734
791
  schemas: { ...ENGINE_NOTE_SCHEMAS, ...NOTE_SCHEMAS },
@@ -737,6 +794,11 @@ function lintCommand() {
737
794
  // whatever it is handed and this stays the one place that
738
795
  // decides which vocabulary a tree is held to.
739
796
  vocabulary: NOTE_VOCABULARY,
797
+ // The pack names, for a `data:` field keyed by pack — a
798
+ // folder's `parent` is one (#288). Companions included:
799
+ // the compile asks the map for whichever pack it is
800
+ // writing, and a companion is a pack it writes.
801
+ packs: config.packDirectories,
740
802
  references: argv.references,
741
803
  });
742
804
 
@@ -789,6 +851,12 @@ function lintCommand() {
789
851
  const { undeclared, unemitted } = compareFields({
790
852
  builders: fieldSpecs,
791
853
  artifact: schema.artifact,
854
+ // A schema is keyed by document subtype and a field
855
+ // declaration by note type. Those were one string until
856
+ // #78 renamed three of them, and joining them by name
857
+ // after that would drop `armorgear`'s findings without
858
+ // saying so — the seam exists for exactly this.
859
+ subtypeOf: (type) => schemaSubtypeOf(config.stats?.systemId, type),
792
860
  });
793
861
  for (const f of undeclared) {
794
862
  schemaFindings.push({
@@ -1000,20 +1068,37 @@ function linksCommand() {
1000
1068
  describe: "Content tree to check. Defaults to the configured contentBase.",
1001
1069
  type: "string",
1002
1070
  });
1003
- yargs.option("manifests", {
1004
- describe:
1005
- "Directory of vendored foreign link manifests. Defaults " +
1006
- "to the configured `paths.manifests`.",
1007
- type: "string",
1008
- });
1009
1071
  },
1010
- handler: (argv) => {
1072
+ handler: async (argv) => {
1011
1073
  try {
1012
1074
  const config = loadPackConfig();
1013
1075
  const contentBase = argv.root ?? config.paths.content;
1014
- const manifestDir = argv.manifests ?? config.paths.manifests;
1015
1076
 
1016
- const index = buildLinkIndex(contentBase, { manifestDir });
1077
+ const scope = { skipDirectories: config.skipDirectories };
1078
+ // Enumerated once and shared, as in `lint` (#243), and a note
1079
+ // it cannot record is reported rather than thrown.
1080
+ const corpusProblems = [];
1081
+ const records = indexRecordsFor({
1082
+ contentBase,
1083
+ config,
1084
+ ...scope,
1085
+ problems: corpusProblems,
1086
+ });
1087
+ for (const problem of corpusProblems) emitDiagnostic(problem);
1088
+ // An error whatever the command's own strictness: the note is
1089
+ // absent from every answer below, so reporting it and exiting 0
1090
+ // would call the tree clean while silently omitting a note.
1091
+ if (corpusProblems.length) process.exitCode = 1;
1092
+ const index = buildLinkIndex(contentBase, {
1093
+ config,
1094
+ records,
1095
+ ...scope,
1096
+ sqlTables: await prepareTreeSqlTables(contentBase, {
1097
+ config,
1098
+ records,
1099
+ ...scope,
1100
+ }),
1101
+ });
1017
1102
 
1018
1103
  // An unusable manifest would otherwise surface as a pile of
1019
1104
  // dead addresses pointing at the notes that cite it, rather
@@ -1021,12 +1106,12 @@ function linksCommand() {
1021
1106
  if (index.foreign.stale.length) {
1022
1107
  for (const s of index.foreign.stale) {
1023
1108
  emitDiagnostic({
1024
- file: path.join(manifestDir, `${s.package}.json`),
1109
+ file: cachedIndexPath(config, s.package),
1025
1110
  severity: "error",
1026
- message: `unusable link manifest: ${s.reason}`,
1111
+ message: `unusable content index: ${s.reason}`,
1027
1112
  });
1028
1113
  }
1029
- log.error("Refresh the vendored copy from that package's own build.");
1114
+ log.error("Re-run `content-build deps fetch`.");
1030
1115
  process.exitCode = 1;
1031
1116
  return;
1032
1117
  }
@@ -1037,7 +1122,7 @@ function linksCommand() {
1037
1122
  const drifted = unaddressableForeignPackages(index.foreign.index);
1038
1123
  if (drifted.length) {
1039
1124
  for (const f of drifted) {
1040
- console.error(formatUnaddressableFinding(f, manifestDir));
1125
+ console.error(formatUnaddressableFinding(f, config));
1041
1126
  }
1042
1127
  process.exitCode = 1;
1043
1128
  return;
@@ -1126,77 +1211,6 @@ function linksCommand() {
1126
1211
  };
1127
1212
  }
1128
1213
 
1129
- /**
1130
- * `content-build manifest` — emit this package's cross-package link manifest.
1131
- *
1132
- * The last capability the library exposed without a command (#58). Every
1133
- * consumer that publishes a manifest had to write the walk, the address
1134
- * derivation, the anchor pass and the entry assembly for itself, and the two
1135
- * that did drifted apart: one routed its UUIDs through the pack router and one
1136
- * did not, so a repository shipping several packs of a type published UUIDs
1137
- * naming the wrong one.
1138
- *
1139
- * Takes no paths. The content tree, the output directory, the content and
1140
- * Foundry package identities and the address scheme all come from
1141
- * `package-build.config.yaml`; `[root]` and `--out` exist to point the same
1142
- * derivation at a scratch tree, not because a build needs to name them.
1143
- *
1144
- * @returns {object} The yargs command module.
1145
- */
1146
- // eslint-disable-next-line
1147
- function manifestCommand() {
1148
- return {
1149
- command: "manifest [root]",
1150
- describe: "Emit this package's cross-package link manifest",
1151
- builder: (yargs) => {
1152
- yargs.positional("root", {
1153
- describe: "Content tree to read. Defaults to the configured contentBase.",
1154
- type: "string",
1155
- });
1156
- yargs.option("out", {
1157
- describe:
1158
- "Directory to write into. Defaults to the configured " + "`paths.manifestOut`.",
1159
- type: "string",
1160
- });
1161
- },
1162
- handler: (argv) => {
1163
- try {
1164
- const config = loadPackConfig();
1165
- const { written, notes, skipped } = emitLinkManifest({
1166
- config,
1167
- ...(argv.root ? { contentBase: argv.root } : {}),
1168
- ...(argv.out ? { outDir: argv.out } : {}),
1169
- });
1170
-
1171
- for (const { package: pkg, file, count } of written) {
1172
- log.info(
1173
- `${pkg} → ${path.relative(process.cwd(), file)} ` +
1174
- `(${count} entries, from ${notes} addressable ` +
1175
- `note(s))`,
1176
- );
1177
- }
1178
-
1179
- // Reported rather than fatal: a note with no address is
1180
- // ordinary — a template, a stub, a `doc` with no category —
1181
- // and failing the build on one would make the manifest
1182
- // unemittable for a reason that is not about the manifest.
1183
- // Silence is the thing to avoid, since a note that quietly
1184
- // lost its address becomes a dead link in every consumer.
1185
- for (const s of skipped) {
1186
- emitDiagnostic({
1187
- file: path.join(argv.root ?? config.paths.content, s.file),
1188
- severity: "warning",
1189
- message: `no address, so it is absent from the manifest: ${s.reason}`,
1190
- });
1191
- }
1192
- } catch (err) {
1193
- reportFailure(err);
1194
- process.exitCode = 1;
1195
- }
1196
- },
1197
- };
1198
- }
1199
-
1200
1214
  /**
1201
1215
  * `content-build content-index` — emit this package's note index.
1202
1216
  *
@@ -1276,9 +1290,12 @@ function siteCommand() {
1276
1290
  type: "string",
1277
1291
  });
1278
1292
  },
1279
- handler: (argv) => {
1293
+ handler: async (argv) => {
1280
1294
  try {
1281
1295
  const result = buildSite({
1296
+ sqlTables: await prepareTreeSqlTables(loadPackConfig().paths.content, {
1297
+ skipDirectories: loadPackConfig().skipDirectories,
1298
+ }),
1282
1299
  ...(argv.out ? { outRoot: argv.out } : {}),
1283
1300
  });
1284
1301
  const { gates } = result;
@@ -1307,13 +1324,13 @@ function siteCommand() {
1307
1324
  }
1308
1325
  for (const s of gates.staleManifests) {
1309
1326
  emitDiagnostic({
1310
- file: path.join(loadPackConfig().paths.manifests, `${s.package}.json`),
1327
+ file: cachedIndexPath(loadPackConfig(), s.package),
1311
1328
  severity: "error",
1312
- message: `unusable link manifest: ${s.reason}`,
1329
+ message: `unusable content index: ${s.reason}`,
1313
1330
  });
1314
1331
  }
1315
1332
  for (const f of gates.unaddressable) {
1316
- console.error(formatUnaddressable(f, loadPackConfig().paths.manifests));
1333
+ console.error(formatUnaddressable(f, loadPackConfig()));
1317
1334
  }
1318
1335
  for (const c of gates.conflicts) {
1319
1336
  log.error(`address ${c.key} is also published by ${c.package}`);
@@ -1327,8 +1344,20 @@ function siteCommand() {
1327
1344
  // failures of individual notes, and stopping the whole build
1328
1345
  // before anything is emitted would make a single bad table
1329
1346
  // hide every other problem in the tree.
1347
+ // `file:line:column: error: message`, path first — the same
1348
+ // shape the pack build and the wikilink findings below use. It
1349
+ // was prose with a timestamp where a parser reads the path,
1350
+ // which made one authored table produce a machine-readable
1351
+ // diagnostic from one build and something ungreppable from the
1352
+ // other (#223).
1330
1353
  for (const e of result.tableErrors) {
1331
- log.error(`bad content table: ${e.reason} (${e.source})`);
1354
+ emitDiagnostic({
1355
+ file: e.source,
1356
+ line: typeof e.line === "number" ? e.line + 1 : undefined,
1357
+ column: e.column,
1358
+ severity: "error",
1359
+ message: e.reason,
1360
+ });
1332
1361
  }
1333
1362
  // Reported the way the pack build reports the very same
1334
1363
  // finding: `file:line:column: error: message`, path first, and
@@ -1351,20 +1380,6 @@ function siteCommand() {
1351
1380
  return;
1352
1381
  }
1353
1382
 
1354
- if (result.manifests && !result.manifests.complete) {
1355
- // Not a softening any more (#184): an address into one of
1356
- // these packages fails like any other that resolves
1357
- // nowhere. The warning names them so an author meeting that
1358
- // failure knows the fix may be to vendor a manifest rather
1359
- // than to correct a shortcode.
1360
- log.warn(
1361
- `no link manifest vendored for ` +
1362
- `${result.manifests.missing.join(", ")} — an ` +
1363
- `address into one of those packages resolves ` +
1364
- `nowhere and fails the build.`,
1365
- );
1366
- }
1367
-
1368
1383
  const s = result.stats;
1369
1384
  log.info(
1370
1385
  `wrote ${s.homepages ?? 0} homepage(s) + ` +
@@ -1421,11 +1436,37 @@ function reachabilityCommand() {
1421
1436
  type: "string",
1422
1437
  });
1423
1438
  },
1424
- handler: (argv) => {
1439
+ handler: async (argv) => {
1425
1440
  try {
1426
- const contentBase = argv.root ?? loadPackConfig().paths.content;
1441
+ // Resolved once and passed on, so every pass below runs
1442
+ // against the configuration this command resolved rather than
1443
+ // whichever one the working directory answers with (#243).
1444
+ const config = loadPackConfig();
1445
+ const contentBase = argv.root ?? config.paths.content;
1427
1446
  const dir = String(argv.dir).replace(/\/+$/, "");
1428
- const index = buildLinkIndex(contentBase);
1447
+ const scope = { skipDirectories: config.skipDirectories };
1448
+ const corpusProblems = [];
1449
+ const records = indexRecordsFor({
1450
+ contentBase,
1451
+ config,
1452
+ ...scope,
1453
+ problems: corpusProblems,
1454
+ });
1455
+ for (const problem of corpusProblems) emitDiagnostic(problem);
1456
+ // An error whatever the command's own strictness: the note is
1457
+ // absent from every answer below, so reporting it and exiting 0
1458
+ // would call the tree clean while silently omitting a note.
1459
+ if (corpusProblems.length) process.exitCode = 1;
1460
+ const index = buildLinkIndex(contentBase, {
1461
+ config,
1462
+ records,
1463
+ ...scope,
1464
+ sqlTables: await prepareTreeSqlTables(contentBase, {
1465
+ config,
1466
+ records,
1467
+ ...scope,
1468
+ }),
1469
+ });
1429
1470
  const indexes = new Set(argv.index.map(String));
1430
1471
 
1431
1472
  const { orphans } = walkReachability(index, {
@@ -1471,8 +1512,14 @@ function reachabilityCommand() {
1471
1512
 
1472
1513
  // eslint-disable-next-line
1473
1514
  /**
1474
- * `deps fetch` — fill the item-catalogue cache for every dependency that
1475
- * declares `itemCatalog: true`.
1515
+ * `deps fetch` — fill the caches this build resolves other packages through:
1516
+ * the **content index** of every declared dependency (#239), and the **item
1517
+ * catalogue** of those additionally declaring `itemCatalog: true`.
1518
+ *
1519
+ * The two sets differ deliberately. Citing another package's *addresses* and
1520
+ * embedding its *items* are separate edges, and a package may have either
1521
+ * without the other — `harn-ensemble` cites no foreign address and embeds
1522
+ * 324,016 item references.
1476
1523
  *
1477
1524
  * Its own command rather than a step of `package compile`, so that a compile
1478
1525
  * never reaches the network. A build that downloads silently is not
@@ -1496,7 +1543,11 @@ function reachabilityCommand() {
1496
1543
  * @returns {Promise<void>}
1497
1544
  */
1498
1545
  async function fetchFromLocalArtifact(config, argv) {
1499
- const rels = itemCatalogRelationships(config);
1546
+ // Two caches, two dependency sets: an index is fetched for every declared
1547
+ // dependency, a catalogue only for those declaring `itemCatalog: true`
1548
+ // (#239). `--from` fills whichever of them this dependency belongs to, so
1549
+ // that testing against an unreleased build behaves like a release would.
1550
+ const rels = metadataRelationships(config);
1500
1551
  const named = rels.map((r) => r.id).join(", ") || "none";
1501
1552
  const rel =
1502
1553
  argv.id ? rels.find((r) => r.id === argv.id)
@@ -1507,11 +1558,14 @@ async function fetchFromLocalArtifact(config, argv) {
1507
1558
  // config is the only place that says which those are.
1508
1559
  throw new Error(
1509
1560
  argv.id ?
1510
- `no dependency "${argv.id}" declares \`itemCatalog: true\` (declared: ${named})`
1511
- : `--from needs --id when several dependencies declare \`itemCatalog: true\` (declared: ${named})`,
1561
+ `no declared dependency "${argv.id}" (declared: ${named})`
1562
+ : `--from needs --id when a package declares several dependencies (declared: ${named})`,
1512
1563
  );
1513
1564
  }
1514
- await fetchCatalogFromPath(config, rel, argv.from);
1565
+ await fetchMetadataFromPath(config, rel, argv.from);
1566
+ if (itemCatalogRelationships(config).some((r) => r.id === rel.id)) {
1567
+ await fetchCatalogFromPath(config, rel, argv.from);
1568
+ }
1515
1569
  }
1516
1570
 
1517
1571
  function depsCommand() {
@@ -1537,7 +1591,7 @@ function depsCommand() {
1537
1591
  yargs.option("id", {
1538
1592
  describe:
1539
1593
  "Which declared dependency `--from` supplies. Only needed " +
1540
- "when more than one declares `itemCatalog: true`.",
1594
+ "when a package declares more than one.",
1541
1595
  type: "string",
1542
1596
  });
1543
1597
  },
@@ -1548,8 +1602,11 @@ function depsCommand() {
1548
1602
  await fetchFromLocalArtifact(config, argv);
1549
1603
  return;
1550
1604
  }
1605
+ const indexes = await fetchAllMetadata(config);
1606
+ if (indexes) log.info(`Fetched ${indexes} dependency content index(es).`);
1551
1607
  const count = await fetchAllCatalogs(config);
1552
1608
  if (count) log.info(`Fetched ${count} dependency catalogue(s).`);
1609
+ if (!indexes && !count) log.info("This package declares no dependencies.");
1553
1610
  } catch (err) {
1554
1611
  reportFailure(err);
1555
1612
  process.exitCode = 1;
@@ -1613,10 +1670,34 @@ async function diffAddresses(config, argv) {
1613
1670
  );
1614
1671
  }
1615
1672
 
1673
+ // Stated by the caller, like every other corpus read in this file (#243):
1674
+ // the two tree reads below must agree with each other and with the compile
1675
+ // about which files are the corpus. They now do so by construction — the
1676
+ // corpus is derived once, here, and handed to both.
1677
+ const scope = { skipDirectories: config.skipDirectories };
1678
+ const corpusProblems = [];
1679
+ const records = indexRecordsFor({
1680
+ contentBase: config.paths.content,
1681
+ config,
1682
+ ...scope,
1683
+ problems: corpusProblems,
1684
+ });
1685
+ for (const problem of corpusProblems) emitDiagnostic(problem);
1686
+ // An error whatever `--strict` says: the note is in none of the answers
1687
+ // below, so exiting 0 would call the tree clean while omitting a note.
1688
+ if (corpusProblems.length) process.exitCode = 1;
1689
+ const corpus = { config, records, ...scope };
1690
+
1616
1691
  const findings = diffItemAddresses(
1617
1692
  readItemAddresses(baselineDirs),
1618
1693
  readItemAddresses(currentDirs),
1619
- { baseline: label },
1694
+ {
1695
+ baseline: label,
1696
+ // Read whether or not anything departed: an id match needs no tree,
1697
+ // but the diff decides rename-versus-withdrawal as it walks the
1698
+ // baseline, so the declarations have to be in hand before it does.
1699
+ predecessors: declaredPredecessors(config.paths.content, corpus),
1700
+ },
1620
1701
  );
1621
1702
  if (!findings.length) {
1622
1703
  log.info(`Every address ${label} published is still published.`);
@@ -1626,7 +1707,7 @@ async function diffAddresses(config, argv) {
1626
1707
  // A rename is fixed in the note that made it, so findings are placed
1627
1708
  // against the tree rather than against the compiled output they were read
1628
1709
  // from.
1629
- const noteFiles = noteFilesById(config.paths.content);
1710
+ const noteFiles = noteFilesById(config.paths.content, corpus);
1630
1711
  const severity = argv.strict ? "error" : "warning";
1631
1712
  for (const finding of findings) {
1632
1713
  emitDiagnostic({
@@ -84,6 +84,7 @@ import { buildSchemaArtifact } from "../engine/schema-extract.mjs";
84
84
  import { SCHEMA_ARTIFACT_FILE } from "../engine/foreign-catalog.mjs";
85
85
  import { validateLangSource } from "../lang.mjs";
86
86
  import { checkLabelRegistry } from "../labels.mjs";
87
+ import { lintYaml } from "../engine/yaml-lint.mjs";
87
88
  import {
88
89
  analyzeCoverage,
89
90
  collectScriptReferences,
@@ -713,6 +714,47 @@ function langHardcoded(config) {
713
714
  *
714
715
  * @returns {object} The yargs command module.
715
716
  */
717
+ /**
718
+ * `package-build yaml` — lint note frontmatter and every YAML file.
719
+ *
720
+ * Frontmatter carries a note's type, shortcode, address and system blocks, and
721
+ * until this command existed nothing checked it *as YAML*: a duplicate key was
722
+ * caught during the parse, logged at `warn`, and turned into a note with no
723
+ * frontmatter, which every later pass then skipped while the build reported
724
+ * success.
725
+ *
726
+ * It ships as a command rather than as a configuration to adopt, so a consumer
727
+ * needs no ESLint and no `eslint.config.js` of its own — and a repository that
728
+ * has one, as `Song-of-Heroic-Lands-FoundryVTT` does for `src/`, keeps it
729
+ * untouched and unconsulted.
730
+ *
731
+ * @returns {object} The yargs command module.
732
+ */
733
+ function yamlCommand() {
734
+ return {
735
+ command: "yaml [paths..]",
736
+ describe: "Lint YAML — note frontmatter, and every YAML file in the repository",
737
+ builder: (y) =>
738
+ y.positional("paths", {
739
+ describe:
740
+ "Files or globs to lint. Defaults to every YAML file and " +
741
+ "markdown frontmatter git would consider — tracked, plus " +
742
+ "untracked and not ignored.",
743
+ type: "string",
744
+ }),
745
+ handler: handler(async (args) => {
746
+ const { findings, checked } = await lintYaml(process.cwd(), { paths: args.paths });
747
+ const errors = reportFindings(findings, {});
748
+ const warnings = findings.length - errors;
749
+ console.log(
750
+ `package-build: ${checked} file(s) checked · ` +
751
+ `${errors} error(s) · ${warnings} warning(s)`,
752
+ );
753
+ if (errors) process.exitCode = 1;
754
+ }),
755
+ };
756
+ }
757
+
716
758
  /**
717
759
  * `labels check` — do the machine registry and the documented table agree?
718
760
  *
@@ -1065,6 +1107,7 @@ yargs(hideBin(process.argv))
1065
1107
  .command(schemaCommand())
1066
1108
  .command(langCommand())
1067
1109
  .command(labelsCommand())
1110
+ .command(yamlCommand())
1068
1111
  .command(bundleCommand())
1069
1112
  .command(releaseCommand())
1070
1113
  .command(deployCommand())