@heroiclands/package-build 19.0.0 → 20.2.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 (85) hide show
  1. package/CHANGELOG.md +1100 -0
  2. package/CONTENT.md +264 -33
  3. package/README.md +43 -4
  4. package/bin/content-build.mjs +94 -3
  5. package/config.mjs +9 -1
  6. package/content-config.mjs +99 -19
  7. package/docs/content-format.md +394 -72
  8. package/e2e.mjs +297 -3
  9. package/engine/actor-compiler.mjs +197 -7
  10. package/engine/address-charset.mjs +23 -5
  11. package/engine/base-compiler.mjs +63 -2
  12. package/engine/bundles.mjs +9 -0
  13. package/engine/content-address.mjs +92 -1
  14. package/engine/content-charset.mjs +434 -0
  15. package/engine/content-format.mjs +102 -0
  16. package/engine/content-icons.mjs +388 -0
  17. package/engine/content-index.mjs +11 -8
  18. package/engine/content-links.mjs +37 -21
  19. package/engine/field-reference.mjs +57 -5
  20. package/engine/field-spec.mjs +214 -7
  21. package/engine/folder-notes.mjs +24 -1
  22. package/engine/foreign-catalog.mjs +112 -7
  23. package/engine/foundry-entries.mjs +14 -0
  24. package/engine/frontmatter-lint.mjs +377 -56
  25. package/engine/frontmatter.mjs +11 -11
  26. package/engine/generate.mjs +72 -12
  27. package/engine/helpers.mjs +96 -10
  28. package/engine/index.mjs +9 -0
  29. package/engine/item-compiler.mjs +37 -0
  30. package/engine/journals.mjs +21 -4
  31. package/engine/macros.mjs +8 -0
  32. package/engine/map-notes.mjs +7 -7
  33. package/engine/note-claims.mjs +208 -5
  34. package/engine/note-ids.mjs +25 -1
  35. package/engine/note-vocabulary.mjs +76 -9
  36. package/engine/pack-config.mjs +102 -12
  37. package/engine/pack-router.mjs +0 -0
  38. package/engine/prose-config.mjs +42 -0
  39. package/engine/prose-lint.mjs +126 -0
  40. package/engine/retired-fields.mjs +57 -16
  41. package/engine/runtime-only-fields.mjs +204 -0
  42. package/engine/scenes.mjs +12 -19
  43. package/engine/schema-check.mjs +23 -1
  44. package/engine/schema-extract.mjs +13 -0
  45. package/engine/site-index.mjs +17 -0
  46. package/engine/subtype-registry.mjs +30 -0
  47. package/engine/system-block.mjs +81 -3
  48. package/engine/web-wikilinks.mjs +33 -27
  49. package/engine/wikilink-syntax.mjs +7 -0
  50. package/engine/wikilinks.mjs +74 -16
  51. package/hm3/actors.mjs +63 -13
  52. package/package.json +2 -2
  53. package/sohl/actors.mjs +106 -7
  54. package/sohl/item-fields.mjs +203 -0
  55. package/sohl/note-schemas.mjs +6 -3
  56. package/types/config.d.mts +7 -0
  57. package/types/e2e.d.mts +130 -3
  58. package/types/engine/actor-compiler.d.mts +83 -3
  59. package/types/engine/address-charset.d.mts +22 -4
  60. package/types/engine/base-compiler.d.mts +54 -3
  61. package/types/engine/content-address.d.mts +64 -0
  62. package/types/engine/content-charset.d.mts +127 -0
  63. package/types/engine/content-format.d.mts +9 -0
  64. package/types/engine/content-icons.d.mts +151 -0
  65. package/types/engine/field-spec.d.mts +271 -3
  66. package/types/engine/folder-notes.d.mts +20 -0
  67. package/types/engine/foreign-catalog.d.mts +38 -2
  68. package/types/engine/foundry-entries.d.mts +6 -0
  69. package/types/engine/frontmatter-lint.d.mts +164 -30
  70. package/types/engine/frontmatter.d.mts +11 -11
  71. package/types/engine/generate.d.mts +27 -0
  72. package/types/engine/helpers.d.mts +45 -9
  73. package/types/engine/index.d.mts +3 -0
  74. package/types/engine/map-notes.d.mts +2 -2
  75. package/types/engine/note-claims.d.mts +67 -0
  76. package/types/engine/note-ids.d.mts +14 -0
  77. package/types/engine/pack-config.d.mts +35 -0
  78. package/types/engine/prose-config.d.mts +41 -0
  79. package/types/engine/prose-lint.d.mts +36 -0
  80. package/types/engine/retired-fields.d.mts +29 -13
  81. package/types/engine/runtime-only-fields.d.mts +102 -0
  82. package/types/engine/schema-check.d.mts +10 -1
  83. package/types/engine/subtype-registry.d.mts +21 -0
  84. package/types/engine/system-block.d.mts +28 -2
  85. package/types/sohl/actors.d.mts +3 -3
@@ -17,6 +17,26 @@
17
17
  * @returns {boolean} True when the value came from the retiring position.
18
18
  */
19
19
  export function readsLegacyKey(field: FieldSpec, from: import("./system-block.mjs").FieldSource): boolean;
20
+ /**
21
+ * Whether a resolution read a field from the top-level key `data:` gathered it
22
+ * off — the shared level's retiring position (#332).
23
+ *
24
+ * {@link readsLegacyKey}'s sibling, and the same signal: a finding here counts
25
+ * one note still on the pre-`data:` spelling, so the sweep has something to
26
+ * count down instead of a corpus nobody has surveyed.
27
+ *
28
+ * The `from` tag already carries the whole answer — step 3b is the only thing
29
+ * that produces it, and it produces it only for a `data.` source — so this is a
30
+ * name for the question rather than a second test of it. Named all the same,
31
+ * because a compile-time report and the frontmatter lint both ask it and must
32
+ * not drift apart about what counts.
33
+ *
34
+ * @param {FieldSpec} field - The declaration.
35
+ * @param {import("./system-block.mjs").FieldSource} from - Where
36
+ * {@link resolveFieldValue} said the value came from.
37
+ * @returns {boolean} True when the value came from the retiring top-level key.
38
+ */
39
+ export function readsRetiredTopLevel(field: FieldSpec, from: import("./system-block.mjs").FieldSource): boolean;
20
40
  /**
21
41
  * Read one declared field out of a note's frontmatter.
22
42
  *
@@ -38,12 +58,77 @@ export function readsLegacyKey(field: FieldSpec, from: import("./system-block.mj
38
58
  * rather than a returned list because the caller is a compiler, which already
39
59
  * knows the note and how to locate a key in it; this module knows neither and
40
60
  * would have to invent a finding shape to say so.
61
+ * @param {(field: FieldSpec) => void} [options.onRetiredTopLevel] - Called with
62
+ * each field read from the top-level key `data:` gathered it off (#332). The
63
+ * shared level's counterpart to `onLegacyKey`, and a separate callback
64
+ * because it is a separate position: a note may have moved one of the two and
65
+ * not the other, and a caller that conflated them would tell its author to
66
+ * fix the wrong line.
41
67
  * @returns {any} The value to emit.
42
68
  */
43
- export function readField(field: FieldSpec, fm: object, { block, onLegacyKey }?: {
69
+ export function readField(field: FieldSpec, fm: object, options?: {
44
70
  block?: string | undefined;
45
71
  onLegacyKey?: ((field: FieldSpec) => void) | undefined;
72
+ onRetiredTopLevel?: ((field: FieldSpec) => void) | undefined;
46
73
  }): any;
74
+ /**
75
+ * The same read, reporting **where the value came from** as well.
76
+ *
77
+ * {@link readField} answers "what does this field hold", which is what almost
78
+ * every caller wants. A builder has one further question — *should the key be
79
+ * written at all* — and it cannot be answered from the value: `null` from a
80
+ * note and `null` from a declared default are the same value and opposite
81
+ * facts (#329).
82
+ *
83
+ * So the position rides back beside the value, resolved **once**. The
84
+ * alternative is a builder that calls {@link resolveFieldValue} for the source
85
+ * and {@link readField} for the value, which resolves the position twice and
86
+ * states in two places the rule that a field is authored in exactly one.
87
+ *
88
+ * @param {FieldSpec} field - The declaration.
89
+ * @param {object} fm - The note's frontmatter.
90
+ * @param {object} [options] - Options, as {@link readField} takes them.
91
+ * @param {string} [options.block="sohl"] - Which system's block to resolve
92
+ * against.
93
+ * @param {(field: FieldSpec) => void} [options.onLegacyKey] - See
94
+ * {@link readField}.
95
+ * @param {(field: FieldSpec) => void} [options.onRetiredTopLevel] - See
96
+ * {@link readField}.
97
+ * @returns {{value: any, from: import("./system-block.mjs").FieldSource}} The
98
+ * value to emit, and the position it was read from.
99
+ */
100
+ export function readFieldEntry(field: FieldSpec, fm: object, { block, onLegacyKey, onRetiredTopLevel }?: {
101
+ block?: string | undefined;
102
+ onLegacyKey?: ((field: FieldSpec) => void) | undefined;
103
+ onRetiredTopLevel?: ((field: FieldSpec) => void) | undefined;
104
+ }): {
105
+ value: any;
106
+ from: import("./system-block.mjs").FieldSource;
107
+ };
108
+ /**
109
+ * Whether a note supplied a value for a field, as opposed to a default doing it.
110
+ *
111
+ * The question {@link FieldSpec.omitWhenAbsent} turns on, asked of the
112
+ * *position* rather than of the value — which cannot answer it, since a
113
+ * declared `default: null` and an authored `null` are indistinguishable once
114
+ * the value is in hand (#329).
115
+ *
116
+ * `undefined` counts as absent whatever position reported it, because writing
117
+ * the key then emits a value `JSON.stringify` drops — the key present in the
118
+ * object and absent from the pack, which is the sort of disagreement this
119
+ * package exists to remove. It arrives from one place: the in-block step
120
+ * answers `value ?? field.default` for a key authored as `null`, so a field
121
+ * declaring no default resolves through "authored" to nothing at all. A field
122
+ * that also declares a `read` never reaches this, since its coercion has by
123
+ * then turned the `undefined` into whatever it makes of an absent value —
124
+ * ordinarily `null`, which is a value the note asked for and is emitted.
125
+ *
126
+ * @param {import("./system-block.mjs").FieldSource} from - Where
127
+ * {@link resolveFieldValue} said the value came from.
128
+ * @param {any} value - The value it gave back.
129
+ * @returns {boolean} True when the note wrote one.
130
+ */
131
+ export function isAuthored(from: import("./system-block.mjs").FieldSource, value: any): boolean;
47
132
  /**
48
133
  * Turn a field declaration into the builder it declares.
49
134
  *
@@ -55,11 +140,15 @@ export function readField(field: FieldSpec, fm: object, { block, onLegacyKey }?:
55
140
  * @param {(field: FieldSpec) => void} [options.onLegacyKey] - Passed through to
56
141
  * {@link readField}: called with each field the note authored at the position
57
142
  * it is being swept off (#305).
143
+ * @param {(field: FieldSpec) => void} [options.onRetiredTopLevel] - Passed
144
+ * through to {@link readField}: called with each field the note authored at
145
+ * the top-level key `data:` gathered it off (#332).
58
146
  * @returns {(fm: object) => object} A `system`-block builder.
59
147
  */
60
- export function buildFromFields(fields: readonly FieldSpec[], { block, onLegacyKey }?: {
148
+ export function buildFromFields(fields: readonly FieldSpec[], { block, onLegacyKey, onRetiredTopLevel }?: {
61
149
  block?: string | undefined;
62
150
  onLegacyKey?: ((field: FieldSpec) => void) | undefined;
151
+ onRetiredTopLevel?: ((field: FieldSpec) => void) | undefined;
63
152
  }): (fm: object) => object;
64
153
  /**
65
154
  * The fields of a declaration an author actually writes.
@@ -73,6 +162,20 @@ export function buildFromFields(fields: readonly FieldSpec[], { block, onLegacyK
73
162
  * @returns {FieldSpec[]} Only the fields with a frontmatter `name`.
74
163
  */
75
164
  export function authoredFields(fields: readonly FieldSpec[]): FieldSpec[];
165
+ /**
166
+ * The fields of a declaration a note may **never** write.
167
+ *
168
+ * The complement of {@link authoredFields} in the direction that matters: those
169
+ * are the fields an author may write, these are the ones authoring is an error
170
+ * (#330). Everything else in a declaration — a constant, a derived value — is
171
+ * simply not authored, which is a statement about the *builder* rather than
172
+ * about the author, and says nothing about what happens if a note writes the
173
+ * path anyway.
174
+ *
175
+ * @param {readonly FieldSpec[]} fields - The declaration.
176
+ * @returns {FieldSpec[]} Only the fields declaring `runtimeOnly`.
177
+ */
178
+ export function runtimeOnlyFields(fields: readonly FieldSpec[]): FieldSpec[];
76
179
  /**
77
180
  * @typedef {object} FieldSpec
78
181
  * @property {string} to - Dotted path in the emitted `system` block — and,
@@ -135,6 +238,13 @@ export function authoredFields(fields: readonly FieldSpec[]): FieldSpec[];
135
238
  * describe the *document* rather than the note: `<system>.system.<to>` and
136
239
  * the legacy in-block `<system>.<name>`. Absent means the ordinary case —
137
240
  * the top level is read, as the third step.
241
+ *
242
+ * **It is read from the other side too**, because the statement is symmetric:
243
+ * if the two positions hold unrelated quantities then the *in-block* position
244
+ * is not the note-level field either, so a check about the note-level field
245
+ * reads past it. `engine/frontmatter-lint.mjs` resolves that through
246
+ * `collidingBlockKeys`. Read for the emitted field alone until #312, which is
247
+ * how an affiliation's office style came to answer for its page heading.
138
248
  * @property {string} [shape] - Human-readable shape, for documentation. Comes
139
249
  * paired with `read` from one of the coercion constants below.
140
250
  * @property {(raw: any, ctx: {fm: object, field: FieldSpec}) => any} [read] -
@@ -164,6 +274,78 @@ export function authoredFields(fields: readonly FieldSpec[]): FieldSpec[];
164
274
  * note, so it declares none.
165
275
  * @property {any|((fm: object) => any)} [value] - For a field with no `name`:
166
276
  * the constant, or a function deriving it from the frontmatter.
277
+ * @property {boolean} [omitWhenAbsent] - **The key is left out entirely when
278
+ * the note does not carry the field** (#329), rather than written from a
279
+ * declared default.
280
+ *
281
+ * Every other field answers absence with a value: an unauthored `weight` is
282
+ * `0`, an unauthored `seat` is `null`. That is right wherever the type has an
283
+ * opinion about the empty case. It is wrong wherever the **DataModel** is the
284
+ * one holding the answer — an affliction's `onsetDurationFormula` has no
285
+ * compile-time value, and writing `null` over it does not merely fail to
286
+ * help: it makes "the author said none" and "the author said nothing"
287
+ * indistinguishable to every reader downstream, and it overwrites an
288
+ * `initial` the system chose on purpose.
289
+ *
290
+ * It is the other conditional row of the same table
291
+ * {@link FieldSpec.runtimeOnly} completes, and the two differ only in what
292
+ * they do about an *authored* value:
293
+ *
294
+ * | declaration | authored | absent |
295
+ * | --- | --- | --- |
296
+ * | ordinary | emitted | default written |
297
+ * | `omitWhenAbsent` | emitted | key omitted |
298
+ * | `runtimeOnly` | refused | key omitted |
299
+ *
300
+ * **A field declaring it must declare no `default`**, and the two are
301
+ * contradictory rather than merely redundant — a default is a value for the
302
+ * absent case, which is the case this says has none. Nor may it be combined
303
+ * with `required` (which fails the build on absence, so nothing is ever
304
+ * omitted) or with `runtimeOnly` (which is never emitted at all). The shipped
305
+ * declarations are checked for all three in `tests/item-fields.test.ts`.
306
+ *
307
+ * Unlike `runtimeOnly` this is a flag rather than a reason, because there is
308
+ * only ever one reason and no message prints it: the DataModel's `initial`
309
+ * stands. What an author needs to know is *that* the field has no default,
310
+ * which the generated reference states in the field's own row.
311
+ * @property {string} [runtimeOnly] - **What the field holds once play has
312
+ * started** — declared on a field the *document* writes for itself, which no
313
+ * note may author (#330).
314
+ *
315
+ * A schema declares plenty of fields a compiled document has no business
316
+ * carrying: an affliction's `onsetDate` is the world time its onset fired
317
+ * at, crystallized when the phase runs. World time does not exist while
318
+ * content is compiled, so there is no authoring-time value — and `0` is
319
+ * itself a valid world time, which is why such a field is nullable rather
320
+ * than sentinelled and why a default cannot stand in for one.
321
+ *
322
+ * Nothing used to stop a note writing one. The three checks that might have
323
+ * each declined for its own correct reason —
324
+ * {@link module:engine/system-block.unknownBlockKeys} reads the block's top
325
+ * level and never descends into `system:`;
326
+ * {@link module:engine/system-block.mergeSystemData} passes through every
327
+ * authored path no declared field claims; and the schema check's fatal
328
+ * direction is *undeclared*, which a field the schema really does declare
329
+ * satisfies. What was missing was a rule saying "declared by the system,
330
+ * but never authorable", and this is it.
331
+ *
332
+ * Declaring it does two things, which are the two directions of one fact:
333
+ *
334
+ * | declaration | authored | absent |
335
+ * | --- | --- | --- |
336
+ * | ordinary | emitted | default written |
337
+ * | runtime-only | **refused** | key omitted |
338
+ *
339
+ * The refusal is {@link module:engine/runtime-only-fields.assertNoRuntimeOnlyFields}'s;
340
+ * the omission is {@link buildFromFields}'s. A runtime-only entry declares a
341
+ * `to` and **no `name`**, so it stays out of {@link authoredFields} and every
342
+ * author-facing surface built on it, while still giving `mergeSystemData` a
343
+ * claimed path and the refusal something to name.
344
+ *
345
+ * **The value is the reason**, as {@link FieldSpec.topLevelMeans}'s is: a
346
+ * boolean would record the decision and lose the case for it, and the reason
347
+ * is what the refusal's message and the generated reference both print. It
348
+ * completes the sentence "it holds …".
167
349
  * @property {string} describe - One line, for the author-facing reference.
168
350
  */
169
351
  /** Whatever the author wrote, unconverted. */
@@ -290,6 +472,13 @@ export type FieldSpec = {
290
472
  * describe the *document* rather than the note: `<system>.system.<to>` and
291
473
  * the legacy in-block `<system>.<name>`. Absent means the ordinary case —
292
474
  * the top level is read, as the third step.
475
+ *
476
+ * **It is read from the other side too**, because the statement is symmetric:
477
+ * if the two positions hold unrelated quantities then the *in-block* position
478
+ * is not the note-level field either, so a check about the note-level field
479
+ * reads past it. `engine/frontmatter-lint.mjs` resolves that through
480
+ * `collidingBlockKeys`. Read for the emitted field alone until #312, which is
481
+ * how an affiliation's office style came to answer for its page heading.
293
482
  */
294
483
  topLevelMeans?: string | undefined;
295
484
  /**
@@ -348,11 +537,90 @@ export type FieldSpec = {
348
537
  * the constant, or a function deriving it from the frontmatter.
349
538
  */
350
539
  value?: any | ((fm: object) => any);
540
+ /**
541
+ * - **The key is left out entirely when
542
+ * the note does not carry the field** (#329), rather than written from a
543
+ * declared default.
544
+ *
545
+ * Every other field answers absence with a value: an unauthored `weight` is
546
+ * `0`, an unauthored `seat` is `null`. That is right wherever the type has an
547
+ * opinion about the empty case. It is wrong wherever the **DataModel** is the
548
+ * one holding the answer — an affliction's `onsetDurationFormula` has no
549
+ * compile-time value, and writing `null` over it does not merely fail to
550
+ * help: it makes "the author said none" and "the author said nothing"
551
+ * indistinguishable to every reader downstream, and it overwrites an
552
+ * `initial` the system chose on purpose.
553
+ *
554
+ * It is the other conditional row of the same table
555
+ * {@link FieldSpec.runtimeOnly} completes, and the two differ only in what
556
+ * they do about an *authored* value:
557
+ *
558
+ * | declaration | authored | absent |
559
+ * | --- | --- | --- |
560
+ * | ordinary | emitted | default written |
561
+ * | `omitWhenAbsent` | emitted | key omitted |
562
+ * | `runtimeOnly` | refused | key omitted |
563
+ *
564
+ * **A field declaring it must declare no `default`**, and the two are
565
+ * contradictory rather than merely redundant — a default is a value for the
566
+ * absent case, which is the case this says has none. Nor may it be combined
567
+ * with `required` (which fails the build on absence, so nothing is ever
568
+ * omitted) or with `runtimeOnly` (which is never emitted at all). The shipped
569
+ * declarations are checked for all three in `tests/item-fields.test.ts`.
570
+ *
571
+ * Unlike `runtimeOnly` this is a flag rather than a reason, because there is
572
+ * only ever one reason and no message prints it: the DataModel's `initial`
573
+ * stands. What an author needs to know is *that* the field has no default,
574
+ * which the generated reference states in the field's own row.
575
+ */
576
+ omitWhenAbsent?: boolean | undefined;
577
+ /**
578
+ * - **What the field holds once play has
579
+ * started** — declared on a field the *document* writes for itself, which no
580
+ * note may author (#330).
581
+ *
582
+ * A schema declares plenty of fields a compiled document has no business
583
+ * carrying: an affliction's `onsetDate` is the world time its onset fired
584
+ * at, crystallized when the phase runs. World time does not exist while
585
+ * content is compiled, so there is no authoring-time value — and `0` is
586
+ * itself a valid world time, which is why such a field is nullable rather
587
+ * than sentinelled and why a default cannot stand in for one.
588
+ *
589
+ * Nothing used to stop a note writing one. The three checks that might have
590
+ * each declined for its own correct reason —
591
+ * {@link module :engine/system-block.unknownBlockKeys} reads the block's top
592
+ * level and never descends into `system:`;
593
+ * {@link module :engine/system-block.mergeSystemData} passes through every
594
+ * authored path no declared field claims; and the schema check's fatal
595
+ * direction is *undeclared*, which a field the schema really does declare
596
+ * satisfies. What was missing was a rule saying "declared by the system,
597
+ * but never authorable", and this is it.
598
+ *
599
+ * Declaring it does two things, which are the two directions of one fact:
600
+ *
601
+ * | declaration | authored | absent |
602
+ * | --- | --- | --- |
603
+ * | ordinary | emitted | default written |
604
+ * | runtime-only | **refused** | key omitted |
605
+ *
606
+ * The refusal is {@link module :engine/runtime-only-fields.assertNoRuntimeOnlyFields}'s;
607
+ * the omission is {@link buildFromFields}'s. A runtime-only entry declares a
608
+ * `to` and **no `name`**, so it stays out of {@link authoredFields} and every
609
+ * author-facing surface built on it, while still giving `mergeSystemData` a
610
+ * claimed path and the refusal something to name.
611
+ *
612
+ * **The value is the reason**, as {@link FieldSpec.topLevelMeans}'s is: a
613
+ * boolean would record the decision and lose the case for it, and the reason
614
+ * is what the refusal's message and the generated reference both print. It
615
+ * completes the sentence "it holds …".
616
+ */
617
+ runtimeOnly?: string | undefined;
351
618
  /**
352
619
  * - One line, for the author-facing reference.
353
620
  */
354
621
  describe: string;
355
622
  };
356
623
  import { legacyKeyOf } from "./system-block.mjs";
624
+ import { retiredTopLevelKey } from "./system-block.mjs";
357
625
  import { setPath } from "./system-block.mjs";
358
- export { legacyKeyOf, setPath };
626
+ export { legacyKeyOf, retiredTopLevelKey, setPath };
@@ -18,6 +18,26 @@ export function bareAddress(value: string | null): string | null;
18
18
  * @returns {string} `<pkg>-none-folder-<shortcode>`.
19
19
  */
20
20
  export function folderAddress(pkg: string, shortcode: string): string;
21
+ /**
22
+ * The Foundry `_id` a folder note's documents are filed under.
23
+ *
24
+ * **The one derivation, because two passes need the same answer.** The folder
25
+ * pass hashes it here on its way to emitting the `Folder` documents; the
26
+ * content index publishes it, through `noteDocId`, for a consumer who will
27
+ * never run this build. Deriving it twice is how the index came to publish a
28
+ * plausible-looking id that resolved to nothing — the general id rule hashes a
29
+ * note's address under the `document` namespace, and a folder's is hashed under
30
+ * {@link FOLDER_ID_NAMESPACE} (#310).
31
+ *
32
+ * An **authored `id` still wins**, and is applied by the caller: this is the
33
+ * derivation, not the resolution, so the pin rule stays stated once, where
34
+ * every other note type states it ({@link module:engine/note-ids.noteDocId}).
35
+ *
36
+ * @param {string} pkg - The content package.
37
+ * @param {string} shortcode - The folder's shortcode.
38
+ * @returns {string} The folder's 16-character Foundry id.
39
+ */
40
+ export function folderDocId(pkg: string, shortcode: string): string;
21
41
  /**
22
42
  * Collect every folder note in a content tree.
23
43
  *
@@ -32,6 +32,25 @@ export function catalogDir(config: object, id: string, version: string): string;
32
32
  * @returns {string} The path, whether or not it exists.
33
33
  */
34
34
  export function cachedSchemaPath(config: object, id: string, version: string): string;
35
+ /**
36
+ * What to record about the packs being extracted.
37
+ *
38
+ * Exported so the pair is one fact: {@link foreignItemCatalogDirs} reads what
39
+ * this writes, and a test that hand-wrote the file would prove the reader
40
+ * against a transcription of the format rather than against the format.
41
+ *
42
+ * A pack declaring no `system` records `null` — Foundry requires the field on
43
+ * an Item pack, so this is the shape of a manifest that is wrong rather than a
44
+ * case with a meaning, and `null` reads as "neutral", which is the safe way to
45
+ * be wrong: a neutral pack is read by every system rather than by none.
46
+ *
47
+ * @param {readonly object[]} itemPacks - The manifest's Item pack entries.
48
+ * @returns {Array<{name: string, system: string|null}>} What each one is.
49
+ */
50
+ export function itemPackManifest(itemPacks: readonly object[]): Array<{
51
+ name: string;
52
+ system: string | null;
53
+ }>;
35
54
  /**
36
55
  * Write an unzipped archive's entries under `dest`.
37
56
  *
@@ -159,10 +178,27 @@ export function fetchAllCatalogs(config: object): Promise<number>;
159
178
  * Reads the cache only. A cold cache is an error naming the command that fills
160
179
  * it, rather than a download nobody asked for.
161
180
  *
181
+ * **Scoped to one system when the caller compiles for one (#58)**, exactly as
182
+ * {@link module:engine/generate.itemPackJsonDirs} scopes the local half. The
183
+ * two halves answer the same lookup — `loadItemsMap` merges them into one
184
+ * address space keyed by `subType:shortcode` — so scoping only the local one
185
+ * leaves the collision it was meant to remove: `skill:awar` is a real address
186
+ * in both vocabularies, and a `harn-ensemble` actor compiled for `hm3` would
187
+ * resolve three quarters of its references against whichever document the
188
+ * dependency's `sohl` pack happened to supply. A pack that declares no system
189
+ * is neutral and always read; asking for no system reads every pack, which is
190
+ * every single-system build.
191
+ *
162
192
  * @param {object} config - The resolved build configuration.
163
- * @returns {string[]} Every cached dependency's item directories.
193
+ * @param {string|null} [system] - The system the caller is compiling for.
194
+ * Omitted or `null`, every cached pack is read.
195
+ * @returns {Array<{dir: string, package: string}>} Every cached dependency's
196
+ * item directories, each with the package that published it.
164
197
  */
165
- export function foreignItemCatalogDirs(config: object): string[];
198
+ export function foreignItemCatalogDirs(config: object, system?: string | null): Array<{
199
+ dir: string;
200
+ package: string;
201
+ }>;
166
202
  /**
167
203
  * The file a system publishes its `system` field sets as (#60).
168
204
  *
@@ -29,6 +29,12 @@ export function anchorsOf(entryUuid: string, entryId: string, body: string, name
29
29
  * @param {string} name - The note's display name.
30
30
  * @param {string} address - The note's package-relative address.
31
31
  * @param {string} body - The note's markdown body.
32
+ * **Each entry carries the `id` of the document it addresses**, not only its
33
+ * UUID. The two are one fact — a UUID ends in the id — but only the entry knows
34
+ * which derivation produced it: an item's is its note's `fm.id`, and its
35
+ * documentation journal's is {@link itemDocEntryId} of that. Stating it here is
36
+ * what lets the content index publish an identity it did not re-derive (#310).
37
+ *
32
38
  * @param {object} ctx - Resolved identities: `{ contentPackage,
33
39
  * foundryPackageId, packRouter }`.
34
40
  * @returns {Array<object>} One or two entries, in {@link buildManifest}'s shape.