@heroiclands/package-build 22.12.2 → 22.13.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.
@@ -79,41 +79,25 @@
79
79
  * to guess which sense was meant. The cost is that `<package>-<type>-<shortcode>`
80
80
  * is **not** a written form, so a link into another package states its system.
81
81
  *
82
- * ## The three rules a complete tuple applies
83
- *
84
- * - **`system` is derived from the type, then defaulted.** An asset type and a
85
- * documentation journal are {@link NO_SYSTEM} because a file and a
86
- * JournalEntry belong to no game system, and the segment is therefore a
87
- * property of the type rather than of where the reference was written.
88
- * Everything else takes the written system, then the position's default.
89
- * - **`type` may not survive unchanged.** A system-bearing type reached at
90
- * `none` is redirected to `doc<type>`, because a note's `none` address *is*
91
- * its documentation journal — the Item is the one with a system.
92
- * - **Acceptability accepts a type's documentation form.** A position that
93
- * accepts `weapongear` accepts `docweapongear`, so a target addressed by its
94
- * documentation journal is acceptable wherever the note itself would be.
82
+ * ## Defaults belong to the citing position
83
+ *
84
+ * Ordinary wikilinks default to `note`, embeds default to `none`, and typed
85
+ * frontmatter fields supply their declared system. The authored type stays
86
+ * the type in the complete tuple. A `doc<type>` qualifier is accepted as an
87
+ * input spelling for a readable note and resolves to `note-<type>`.
95
88
  *
96
89
  * ## The tuple keeps no memory of how it was written
97
90
  *
98
91
  * An abbreviation is a *rendering*, not a different value: `being-kaldor` in a
99
- * `thalorna` note and `thalorna-none-being-kaldor` are the same Address, and
100
- * they parse to the same four fields. So does the pair `weapongear-dgr` in prose
101
- * and `docweapongear-dgr` anywhere, because both name the one documentation
102
- * journal. Nothing here records what the author typed — a tuple that did would
103
- * make identity depend on a spelling, and two notes naming one document would
104
- * compare unequal.
92
+ * `thalorna` note and `thalorna-note-being-kaldor` are the same Address, and
93
+ * they parse to the same four fields. Nothing here records what the author
94
+ * typed — a tuple that did would make identity depend on a spelling.
105
95
  *
106
96
  * The converse matters just as much: **one written string is two Addresses at
107
97
  * two positions**, because the positions supply different defaults.
108
98
  * `skill-wpnc` under `sohl.items` defaults its system from the block and names
109
- * the **Item**; the same string in body prose defaults to `none`, is redirected,
110
- * and names the **page**. Only the resolved tuple can tell them apart.
111
- *
112
- * That is why {@link acceptsType} widens the accepted set rather than reading a
113
- * remembered pre-redirect type off the tuple. The redirect is not reversible
114
- * from the four segments alone: `docmacro-autoattack` addresses a `macro`'s
115
- * journal while `macro-autoattack` addresses the Macro, so `doc`-stripping is a
116
- * question about the type vocabulary, not about the tuple.
99
+ * the Item; the same string in body prose defaults to `note` and names the
100
+ * readable page. Only the resolved tuple can tell them apart.
117
101
  *
118
102
  * ## What this module does not do
119
103
  *
@@ -132,17 +116,15 @@
132
116
  // `engine/systems.mjs` imports nothing but `engine/address-charset.mjs`, so the
133
117
  // direction is toward the leaf and cannot close a cycle.
134
118
  import { isAddressSegment } from "./address-charset.mjs";
135
- import { NO_SYSTEM, assertSystemSegment, isSystemSegment } from "./systems.mjs";
136
- // The asset vocabulary, for the one thing the address grammar asks of it:
137
- // whether a type's `<system>` segment is fixed. `engine/asset-types.mjs` is a
138
- // leaf, so the direction cannot close a cycle.
139
- import { isAssetType } from "./asset-types.mjs";
119
+ import { NO_SYSTEM, NOTE_SYSTEM, assertSystemSegment, isSystemSegment } from "./systems.mjs";
120
+ // Document subtype maps identify the system of a note's own Actor or Item.
140
121
  import { systemOf } from "./document-subtypes.mjs";
141
122
  import { KNOWN_DOCUMENT_SUBTYPE_MAPS } from "./subtype-registry.mjs";
142
123
  // `ids.mjs` is a leaf with no local imports, and `pack-config.mjs` reaches only
143
124
  // the configuration loader and the diagnostics writer — neither reads an
144
125
  // address, so the grammar sits below everything that consumes it.
145
- import { currentType, ITEM_PACK, packForType } from "./ids.mjs";
126
+ import { currentType, ITEM_PACK, JOURNAL_TYPES, packForType } from "./ids.mjs";
127
+ import { HOMEPAGE_TYPE } from "./homepage.mjs";
146
128
  import { loadPackConfig } from "./pack-config.mjs";
147
129
 
148
130
  export { NO_SYSTEM };
@@ -166,32 +148,18 @@ export const ADDRESS_SEPARATOR = "-";
166
148
  */
167
149
  export const CANONICAL_KEY_SEGMENTS = 4;
168
150
 
169
- /**
170
- * The qualifier prefix that addresses a note's **documentation** rather than
171
- * the note's own document: `docskill-wpnc` is the JournalEntry that
172
- * `skill-wpnc`'s prose compiled into.
173
- *
174
- * Every doc-carrying type has a `doc<type>` counterpart, formed by prefix and
175
- * never enumerated, so the prefix itself is the whole of that vocabulary.
176
- *
177
- * @type {string}
178
- */
151
+ /** The accepted `doc<type>` input prefix for readable note links. */
179
152
  export const ITEM_DOC_PREFIX = "doc";
180
153
 
181
154
  /** Package, system and type are one spelling each, so they fold to lowercase. */
182
155
  const norm = (s) => String(s).toLowerCase().trim();
183
156
 
184
157
  /**
185
- * A complete Address: four segments, and nothing else.
186
- *
187
- * `type` is the segment the canonical string carries, after the `doc<type>`
188
- * redirect. There is no fifth field recording the written form, because the
189
- * written form is a rendering rather than part of the value.
158
+ * A complete Address: four segments, with no memory of the authored form.
190
159
  *
191
160
  * @typedef {object} AddressTuple
192
161
  * @property {string} package - The owning **content** package.
193
- * @property {string} system - The system whose document this addresses, or
194
- * `none`.
162
+ * @property {string} system - `note`, `none`, or a game system id.
195
163
  * @property {string} type - The type segment of the canonical Address.
196
164
  * @property {string} shortcode - The note's `shortcode`.
197
165
  */
@@ -218,29 +186,8 @@ const norm = (s) => String(s).toLowerCase().trim();
218
186
  */
219
187
 
220
188
  /**
221
- * Every content type whose **prose compiles into a JournalEntry of its own**,
222
- * addressed by the virtual `doc<type>` qualifier.
223
- *
224
- * Every item type, every actor type, every map type, plus `macro` — a macro
225
- * note's body documents the script the note also compiles into a Macro, which
226
- * is the same shape as an item and its description: one note, two documents,
227
- * the prose living in the journals pack.
228
- *
229
- * **One set, read by the compilers and by the grammar alike.** The journals
230
- * pass decides what to compile from it, the link manifest decides what to
231
- * publish a `doc<type>` entry for, and the parser decides whether `doc<type>`
232
- * is a legal type segment. Held apart, the three drift into a manifest that
233
- * asserts documentation nothing compiled, a compiled entry no consumer can
234
- * address, or an authored `doc<type>` link that stops parsing. It is composed
235
- * exactly once, in `defineConfig`, and read from there — never recomposed at a
236
- * call site.
237
- *
238
- * `doc` notes are absent, for the reason that applies to them alone: a `doc`
239
- * note's single document *is* its prose, so there is no second document for a
240
- * `doc<type>` address to name.
241
- *
242
- * An accessor rather than a hoisted constant, so that importing this module
243
- * needs no configuration.
189
+ * The configured types whose prose compiles into a separate JournalEntry.
190
+ * The journal has a `note` Address with the authored type segment.
244
191
  *
245
192
  * @returns {ReadonlySet<string>} The configured doc-carrying types.
246
193
  */
@@ -265,50 +212,20 @@ export function hasDocEntry(type) {
265
212
  return docEntryTypes().has(String(currentType(type)));
266
213
  }
267
214
 
268
- /**
269
- * Whether a note type's **own** document carries a game system.
270
- *
271
- * True for the types some shipped map compiles into an Item or an Actor; false
272
- * for the core-document types — `doc`, `lore`, `place`, `scenario`, `macro` and
273
- * the map types — whose documents Foundry itself defines and which therefore
274
- * already live at `none`.
275
- *
276
- * It is what the `doc<type>` redirect tests rather than {@link hasDocEntry}: a
277
- * `macro` has a documentation journal *and* a `none` address of its own, so
278
- * redirecting on "has a doc entry" would collapse two live addresses into one
279
- * and a `[[macro-autoattack|]]` would stop naming the Macro.
280
- *
281
- * @param {string} type - The note type.
282
- * @returns {boolean} True when the type compiles into a system document.
283
- */
284
- function isSystemBearing(type) {
285
- return systemOf(type, KNOWN_DOCUMENT_SUBTYPE_MAPS) !== NO_SYSTEM;
215
+ /** The segment of a note's own document. */
216
+ export function ownDocumentSystem(type) {
217
+ return JOURNAL_TYPES.has(String(type)) || type === HOMEPAGE_TYPE ?
218
+ NOTE_SYSTEM
219
+ : systemOf(type, KNOWN_DOCUMENT_SUBTYPE_MAPS);
286
220
  }
287
221
 
288
222
  /**
289
- * Reads a qualifier as the **virtual `doc<type>`** form, or reports that it is
290
- * not one.
291
- *
292
- * A document and its documentation are two documents in two packs, so they need
293
- * two addresses. `skill-wpnc` is the item; `docskill-wpnc` is the JournalEntry
294
- * its prose compiled into, and `docmacro-autoattack` is the same arrangement
295
- * for a macro.
296
- *
297
- * The virtual form exists for a type that carries separate documentation
298
- * ({@link docEntryTypes} — the set the journals compiler and the link manifest
299
- * read too), **or** for one that routes to the items pack. The second clause
300
- * covers the open, unenumerated set: types that compile into items are not
301
- * enumerated anywhere, and a foreign package may publish an item type this
302
- * build has never heard of. Dropping it would silently unlink every `doc<type>`
303
- * address into such a package.
304
- *
305
- * A **real** type of the same name always wins: the virtual reading is only
306
- * consulted for a qualifier no authored note claims.
307
- *
308
- * @param {string} qualifier - The already-normalised type segment.
309
- * @param {Set<string>} types - Every type the content tree contains.
310
- * @returns {string|null} The underlying document type, or `null` when the
311
- * qualifier is not a virtual one.
223
+ * Read a `doc<type>` input qualifier as the underlying authored type.
224
+ * A real type with that spelling takes precedence.
225
+ *
226
+ * @param {string} qualifier - The type segment from the input.
227
+ * @param {Set<string>} types - Types in the content tree.
228
+ * @returns {string|null} The underlying type, or `null`.
312
229
  */
313
230
  export function resolveItemDocType(qualifier, types) {
314
231
  if (types.has(qualifier)) return null; // a real type owns its own name
@@ -445,21 +362,11 @@ export function renderAddress(tuple) {
445
362
  }
446
363
 
447
364
  /**
448
- * Whether the type an Address names is one this position accepts.
449
- *
450
- * The second of the two questions, and separate from the first because the
451
- * accepted **set** is a property of the position while the grammar is not.
452
- *
453
- * **An accepted type carries its documentation form with it.** A position
454
- * accepting `weapongear` accepts `docweapongear`, because the journal that
455
- * documents a weapon is the same thing to a position that wanted a weapon — and
456
- * a prose address of one *is* the journal, so refusing it would refuse the form
457
- * an author most often writes. It is the widening `defineConfig` applies to the
458
- * types a package may claim, applied here to the types a position may take.
365
+ * Whether the canonical type an Address names is accepted at this position.
459
366
  *
460
- * @param {AddressTuple} tuple - A parsed Address.
461
- * @param {Iterable<string>} allowed - The types this position accepts.
462
- * @returns {boolean} True when the Address names one of them.
367
+ * @param {AddressTuple} tuple - A complete Address.
368
+ * @param {Iterable<string>} allowed - Accepted types.
369
+ * @returns {boolean} Whether the type is accepted.
463
370
  */
464
371
  export function acceptsType(tuple, allowed) {
465
372
  const type = norm(tuple?.type ?? "");
@@ -467,7 +374,7 @@ export function acceptsType(tuple, allowed) {
467
374
  for (const one of allowed ?? []) {
468
375
  const want = norm(one);
469
376
  if (!want) continue;
470
- if (type === want || type === `${ITEM_DOC_PREFIX}${want}`) return true;
377
+ if (type === want) return true;
471
378
  }
472
379
  return false;
473
380
  }
@@ -475,9 +382,7 @@ export function acceptsType(tuple, allowed) {
475
382
  /**
476
383
  * Apply the defaults to a partial reading, giving a complete Address.
477
384
  *
478
- * The three rules of the module note, applied in the order they depend on each
479
- * other: the package, then the system the type may fix, then the redirect the
480
- * system may force.
385
+ * Complete a parsed Address using the citing position's package and system.
481
386
  *
482
387
  * @param {{type: string, shortcode: string, package?: string, system?: string,
483
388
  * itemDoc?: boolean}} read - A partial reading, as {@link readQualifier}
@@ -490,27 +395,8 @@ export function completeAddress(read, where = {}) {
490
395
  if (isAddressTuple(read)) return read;
491
396
  const written = read.type;
492
397
  const pkg = read.package ?? where.package;
493
- // A documentation journal is a core document, so it is `none` however it was
494
- // reached, and an **asset** is `none` for a stronger reason: a file belongs
495
- // to no game system, so the segment is a property of the type rather than of
496
- // where the reference was written. Without that, a `sohl:` block naming
497
- // `icon-anvil` would expand to `sohl-sohl-icon-anvil` and resolve to
498
- // nothing, which is exactly where an embedded item's art is written.
499
- // Otherwise the block's system, which body prose reports as `none`.
500
- const system =
501
- read.itemDoc || isAssetType(written) ?
502
- NO_SYSTEM
503
- : (read.system ?? where.system ?? NO_SYSTEM);
504
- // **Under `none`, a system-bearing type addresses its documentation
505
- // journal.** A note's `none` address *is* its `doc<type>` entry — the Item
506
- // is the one with a system — so a prose `[[affiliation-sirvadar|…]]` names
507
- // the page, which is almost always what prose means. A value that means the
508
- // Item states the system and gets it. A `doc<type>` written explicitly is
509
- // `none` wherever it appears, even inside a system block: no game system
510
- // defines a JournalEntry.
511
- const redirected = system === NO_SYSTEM && isSystemBearing(written);
512
- const type = read.itemDoc || redirected ? `${ITEM_DOC_PREFIX}${written}` : written;
513
- return addressTuple({ package: pkg, system, type, shortcode: read.shortcode });
398
+ const system = read.itemDoc ? NOTE_SYSTEM : (read.system ?? where.system ?? NOTE_SYSTEM);
399
+ return addressTuple({ package: pkg, system, type: written, shortcode: read.shortcode });
514
400
  }
515
401
 
516
402
  /**
@@ -88,7 +88,7 @@ export function artSlot(key) {
88
88
  * not parse; or `not-accepted` when it parses to a type outside `accepts`.
89
89
  */
90
90
  export function artTarget(value, defaultType, accepts, vocabulary = {}) {
91
- const tuple = parseAddress(value, { ...vocabulary, type: defaultType });
91
+ const tuple = parseAddress(value, { ...vocabulary, system: ASSET_SYSTEM, type: defaultType });
92
92
  if (tuple.reason) return tuple;
93
93
  if (accepts && !acceptsType(tuple, accepts)) {
94
94
  return { reason: "not-accepted", type: tuple.type };
@@ -71,7 +71,7 @@ import {
71
71
  import { contentPackage } from "./content-package.mjs";
72
72
  import { FOLDER_TYPE } from "./folder-notes.mjs";
73
73
  import { HOMEPAGE_TYPE } from "./homepage.mjs";
74
- import { itemDocEntryId } from "./item-docs.mjs";
74
+ import { hasDocEntry, itemDocEntryId } from "./item-docs.mjs";
75
75
  import { folderField, renderFoundryMarkdown, resolveName } from "./helpers.mjs";
76
76
  import { packForType } from "./ids.mjs";
77
77
  import { readQualifier } from "./wikilinks.mjs";
@@ -251,8 +251,9 @@ export class Bundles extends BasePackCompiler {
251
251
 
252
252
  const baseType =
253
253
  isAddressTuple(read) ? resolveItemDocType(read.type, index.types) : undefined;
254
- const itemDoc = read.itemDoc || Boolean(baseType);
255
254
  const type = baseType ?? read.type;
255
+ const itemDoc =
256
+ read.itemDoc || Boolean(baseType) || (read.system === "note" && hasDocEntry(type));
256
257
  const note = index.byShortcode.get(`${type}/${read.shortcode}`);
257
258
  if (!note) {
258
259
  throw new Error(
@@ -45,7 +45,7 @@ import { DEFAULT_ADDRESS_SCHEME } from "../content-config.mjs";
45
45
  // The system vocabulary is the `<system>` segment's own registry, and
46
46
  // `engine/systems.mjs` imports nothing but `engine/address-charset.mjs`, so
47
47
  // the direction is toward the leaf and cannot close a cycle.
48
- import { NO_SYSTEM, isSystemSegment } from "./systems.mjs";
48
+ import { NO_SYSTEM, NOTE_SYSTEM, isSystemId } from "./systems.mjs";
49
49
 
50
50
  // The address grammar, which sits below this module: a page URL is derived from
51
51
  // an address, so the rule for reading and writing one is stated there and
@@ -171,12 +171,9 @@ export function packageAddress(fm) {
171
171
  *
172
172
  * The **enclosing system block** decides, at any depth within it, and nothing
173
173
  * else does: `sohl.items[3].model` and `sohl.system.body.structure` are both
174
- * `sohl` because both sit under `sohl:`. Everywhere else is {@link NO_SYSTEM} —
175
- * top-level frontmatter, the shared `data:` container, and body prose, which has
176
- * no key path at all and passes `undefined`.
177
- *
178
- * It is the block rather than the field, so an `Address` field needs no opinion
179
- * about systems and no per-field table has to be kept in step with the schema.
174
+ * `sohl` because both sit under `sohl:`. Everywhere else takes
175
+ * {@link NOTE_SYSTEM}; fields declared for assets or folders supply their own
176
+ * `none` default at the field parser.
180
177
  *
181
178
  * The first segment must **be** a declared system, not merely look like one:
182
179
  * `sohlish.items` is a key called `sohlish`, and `notes.sohl.thing` names no
@@ -187,9 +184,9 @@ export function packageAddress(fm) {
187
184
  * @returns {string} The system id, or `none`.
188
185
  */
189
186
  export function blockSystem(keyPath) {
190
- if (typeof keyPath !== "string" || !keyPath) return NO_SYSTEM;
187
+ if (typeof keyPath !== "string" || !keyPath) return NOTE_SYSTEM;
191
188
  const first = keyPath.split(".")[0].trim().toLowerCase();
192
- return isSystemSegment(first) && first !== NO_SYSTEM ? first : NO_SYSTEM;
189
+ return isSystemId(first) ? first : NOTE_SYSTEM;
193
190
  }
194
191
 
195
192
  /**
@@ -102,12 +102,13 @@ import unidecode from "unidecode";
102
102
  import { metadataFileName } from "./metadata-index.mjs";
103
103
  import { collectAssetRecords } from "./asset-index.mjs";
104
104
  import { addressSlug, canonicalKey } from "./content-address.mjs";
105
+ import { ownDocumentSystem } from "./address.mjs";
106
+ import { NOTE_SYSTEM } from "./systems.mjs";
105
107
  // One reader for a note's anchors, shared with the link checker and with the
106
108
  // builds that emit a link. Re-exported because this is where callers
107
109
  // have always addressed it.
108
110
  import { collectAnchors } from "./anchors.mjs";
109
- import { subtypeRow, NO_SYSTEM, systemOf } from "./document-subtypes.mjs";
110
- import { KNOWN_DOCUMENT_SUBTYPE_MAPS } from "./note-claims.mjs";
111
+ import { NO_SYSTEM } from "./document-subtypes.mjs";
111
112
 
112
113
  /**
113
114
  * The `<system>` a note belongs to when it belongs to none.
@@ -204,7 +205,7 @@ export function noteAddress(frontmatter, contentPackage) {
204
205
  slug,
205
206
  canonical: canonicalKey(
206
207
  contentPackage,
207
- systemOf(frontmatter.type, KNOWN_DOCUMENT_SUBTYPE_MAPS),
208
+ ownDocumentSystem(frontmatter.type),
208
209
  frontmatter.type,
209
210
  frontmatter.shortcode,
210
211
  ),
@@ -504,10 +505,7 @@ export function buildIndexRecord({
504
505
  link: address ? `${address.slug}#${a.slug}` : null,
505
506
  }))
506
507
  ),
507
- foundry: foundryBlock(
508
- entries?.own,
509
- systemOf(frontmatter?.type, KNOWN_DOCUMENT_SUBTYPE_MAPS),
510
- ),
508
+ foundry: foundryBlock(entries?.own, ownDocumentSystem(frontmatter?.type)),
511
509
  // Forward link to the note's documentation journal, which is its
512
510
  // own record. Named rather than nested, because the journal is a
513
511
  // separate document with its own address — see `buildDocRecord`.
@@ -536,7 +534,7 @@ export function buildIndexRecord({
536
534
  *
537
535
  * An item note compiles into two documents — the item, and a JournalEntry
538
536
  * holding its prose — and the second is a document in its own right: its own
539
- * canonical address (`doc<type>/<shortcode>`), its own UUID, its own pages.
537
+ * canonical `note` address with the authored type, its own UUID and pages.
540
538
  * So it gets its own record, and resolving `docaffliction/blkdth` is the same
541
539
  * lookup as resolving anything else. Nested inside the item's record it would
542
540
  * be the one address in the index reachable only by knowing to look somewhere
@@ -572,7 +570,7 @@ function buildDocRecord({ frontmatter, address, entry, file, contentPackage, anc
572
570
  return /** @type {Record<string, any>} */ (
573
571
  sortKeysDeep({
574
572
  package: contentPackage,
575
- type: `doc${frontmatter.type}`,
573
+ type: frontmatter.type,
576
574
  shortcode: frontmatter.shortcode,
577
575
  name: frontmatter.name,
578
576
  // The journal's own `_id`, taken from the entry rather than
@@ -586,7 +584,7 @@ function buildDocRecord({ frontmatter, address, entry, file, contentPackage, anc
586
584
  // the forward link on that record, so either end reaches the other.
587
585
  documents: address.canonical,
588
586
  anchors: made ? anchors : null,
589
- foundry: foundryBlock(entry), // a journal: no system key
587
+ foundry: foundryBlock(entry, NOTE_SYSTEM),
590
588
  file,
591
589
  })
592
590
  );
@@ -76,8 +76,8 @@ import { ASSET_TYPE_NAMES } from "./asset-types.mjs";
76
76
  import { resolveEmbeds } from "./content-embeds.mjs";
77
77
  import { foundryAddressProblem, servesFoundry } from "./pathnames.mjs";
78
78
  import { hasDocEntry } from "./item-docs.mjs";
79
- import { NO_SYSTEM, systemOf } from "./document-subtypes.mjs";
80
- import { KNOWN_DOCUMENT_SUBTYPE_MAPS } from "./note-claims.mjs";
79
+ import { ownDocumentSystem } from "./address.mjs";
80
+ import { NOTE_SYSTEM } from "./systems.mjs";
81
81
  import { loadPackConfig } from "./pack-config.mjs";
82
82
  import { searchableFrontmatter } from "./note-package.mjs";
83
83
  import {
@@ -138,12 +138,8 @@ export function anchorsOf(body) {
138
138
  * before, since the walk read the file and it then read it again for the raw
139
139
  * text.
140
140
  *
141
- * The index mirrors what both builds construct, including the two addresses a
142
- * doc-carrying note answers to: `type/shortcode` for the document, and
143
- * `doc<type>/shortcode` for the JournalEntry its prose compiles into. Once a
144
- * manifest publishes `doc<type>` entries that prefix is a *known type*, and the
145
- * virtual reading that used to answer for it no longer fires — a real type owns
146
- * its own name — so the note is indexed under both.
141
+ * The index holds each compiled document under its complete Address. Readable
142
+ * content uses `note`, and system documents use their declared system.
147
143
  *
148
144
  * @param {string} contentBase - Root of the content tree.
149
145
  * @param {object} [opts]
@@ -250,24 +246,20 @@ export function buildLinkIndex(
250
246
  // once for the whole build.
251
247
  const canonical =
252
248
  encodeAddresses(record.address?.canonical) ??
253
- canonicalKey(pkg, systemOf(type, KNOWN_DOCUMENT_SUBTYPE_MAPS), type, fm.shortcode);
249
+ canonicalKey(pkg, ownDocumentSystem(type), type, fm.shortcode);
254
250
  // A note with no address publishes no page, so its identity is
255
251
  // filed where a link cannot reach it and a reference still can.
256
252
  const into = record.address ? byKey : byStub;
257
253
  into.set(canonical, note);
258
254
  if (hasDocEntry(type)) {
259
- // A documentation journal is `none`: no game system defines a
260
- // JournalEntry, and one note has one of them however many
261
- // system blocks it carries.
262
- into.set(canonicalKey(pkg, NO_SYSTEM, `doc${type}`, fm.shortcode), note);
255
+ // The prose has one `note` Address across all system blocks.
256
+ into.set(canonicalKey(pkg, NOTE_SYSTEM, type, fm.shortcode), note);
263
257
  }
264
258
  }
265
259
  }
266
260
 
267
261
  // The types a link may name, which are the ones notes declare. A
268
- // documentation journal's `doc<type>` is deliberately not among them: it is
269
- // virtual, and `readQualifier` resolves it from the base type rather than
270
- // from a type any tree declares.
262
+ // The `doc<type>` input spelling is an alias, not a published type.
271
263
  //
272
264
  // The **asset** types join unconditionally, whether or not this tree holds
273
265
  // a file of each. They are a closed vocabulary rather than a census of what
@@ -423,7 +415,7 @@ export function buildLinkIndex(
423
415
  for (const [all, rawInner] of matchAllOutsideCode(body, new RegExp(WIKILINK.source, "g"))) {
424
416
  const parsed = readWikilink(rawInner, {
425
417
  package: pkg,
426
- system: "none",
418
+ system: "note",
427
419
  types,
428
420
  packages,
429
421
  noIndexPackages,
@@ -1041,7 +1041,7 @@ export function renderContentTable(spec, rows, linkable, self) {
1041
1041
  const label = text.replace(/\\?\|/g, "/");
1042
1042
  const target = completeAddress(
1043
1043
  { type: doc.fm.type, shortcode: doc.fm.shortcode },
1044
- { package: doc.fm.package ?? contentPackage(), system: "none" },
1044
+ { package: doc.fm.package ?? contentPackage(), system: "note" },
1045
1045
  );
1046
1046
  return `[[${renderAddress(target)}\\|${label}]]`;
1047
1047
  }),
@@ -60,7 +60,8 @@ import path from "node:path";
60
60
 
61
61
  import { noteAddressContext } from "./note-addresses.mjs";
62
62
  import { canonicalKey, packageAddress } from "./content-address.mjs";
63
- import { NO_SYSTEM, systemOf } from "./document-subtypes.mjs";
63
+ import { ownDocumentSystem } from "./address.mjs";
64
+ import { NOTE_SYSTEM } from "./systems.mjs";
64
65
  import {
65
66
  KNOWN_DOCUMENT_SUBTYPE_MAPS,
66
67
  NEVER_PACKED_TYPES,
@@ -150,12 +151,7 @@ export function anchorsOf(entryUuid, entryId, body, name) {
150
151
  */
151
152
  export function entriesForNote(fm, name, address, body, ctx) {
152
153
  const { contentPackage, foundryPackageId, packRouter } = ctx;
153
- const key = canonicalKey(
154
- contentPackage,
155
- systemOf(fm.type, KNOWN_DOCUMENT_SUBTYPE_MAPS),
156
- fm.type,
157
- fm.shortcode,
158
- );
154
+ const key = canonicalKey(contentPackage, ownDocumentSystem(fm.type), fm.type, fm.shortcode);
159
155
  // `buildManifest` records `packageRelative(url, base)`, so the pair it is
160
156
  // given has to round-trip. The address is already package-relative, so the
161
157
  // honest pair is the address under a base of `"/"` — which strips straight
@@ -244,7 +240,7 @@ export function entriesForNote(fm, name, address, body, ctx) {
244
240
  // `NO_SYSTEM`, whatever the item is: a documentation journal is a
245
241
  // JournalEntry, which no game system defines, and there is one of them
246
242
  // however many system blocks the note carries.
247
- const docKey = canonicalKey(contentPackage, NO_SYSTEM, `doc${fm.type}`, fm.shortcode);
243
+ const docKey = canonicalKey(contentPackage, NOTE_SYSTEM, fm.type, fm.shortcode);
248
244
  const docEntryId = fm.id ? itemDocEntryId(fm.id) : undefined;
249
245
  const docUuid = uuidFor("doc", docEntryId);
250
246
  return [
@@ -1109,7 +1109,7 @@ export function lintNote(
1109
1109
  const fm = note.fm ?? {};
1110
1110
  const referenceContext = {
1111
1111
  package: index?.contentPackage,
1112
- system: "none",
1112
+ system: "note",
1113
1113
  ...addressContext,
1114
1114
  types: new Set([
1115
1115
  ...Object.keys(vocabulary ?? {}),
@@ -1773,7 +1773,7 @@ export function lintFrontmatter(
1773
1773
  index: references ? index : undefined,
1774
1774
  addressContext: {
1775
1775
  package: index.contentPackage,
1776
- system: "none",
1776
+ system: "note",
1777
1777
  types: index.types,
1778
1778
  },
1779
1779
  ...(systems ? { systems } : {}),
@@ -54,7 +54,12 @@ import { metadataFileName, PACKAGEBUILD_PACKAGE } from "./packages.mjs";
54
54
  import { packageBuildRecords } from "./packagebuild-index.mjs";
55
55
 
56
56
  export { metadataFileName };
57
- import { PACKAGE_BASE, readCanonicalKey, resolvePackageUrl } from "./content-address.mjs";
57
+ import {
58
+ PACKAGE_BASE,
59
+ canonicalKey,
60
+ readCanonicalKey,
61
+ resolvePackageUrl,
62
+ } from "./content-address.mjs";
58
63
  import { HOMEPAGE_TYPE } from "./homepage.mjs";
59
64
 
60
65
  /**
@@ -369,11 +374,23 @@ export function loadForeignIndexes(config, localPackages, bases = PACKAGE_BASE)
369
374
  error.position = line >= 0 ? { line: line + 1 } : {};
370
375
  throw error;
371
376
  }
372
- referenceRecords.push(record);
373
- const key = encodeAddresses(record?.address?.canonical);
377
+ let key = encodeAddresses(record?.address?.canonical);
374
378
  if (!key) continue;
375
- const parts = readCanonicalKey(key);
379
+ let parts = readCanonicalKey(key);
376
380
  if (!parts) continue;
381
+ if (
382
+ parts.system === "none" &&
383
+ record.documents &&
384
+ parts.type.startsWith("doc") &&
385
+ record.type === parts.type
386
+ ) {
387
+ record.type = parts.type.slice(3);
388
+ key = canonicalKey(parts.package, "note", record.type, parts.shortcode);
389
+ parts = readCanonicalKey(key);
390
+ record.foundry = { note: record.foundry?.none };
391
+ record.address.canonical = key;
392
+ }
393
+ referenceRecords.push(record);
377
394
  // First writer wins, so two packages claiming one address cannot
378
395
  // make the build depend on the order the cache was read in.
379
396
  if (index.has(key)) continue;
@@ -436,11 +453,11 @@ export function loadForeignIndexes(config, localPackages, bases = PACKAGE_BASE)
436
453
  if (entry.url) {
437
454
  const identity = completeAddress({
438
455
  package: record.package,
439
- system: "none",
456
+ system: "note",
440
457
  type: record.type,
441
458
  shortcode: record.shortcode,
442
459
  });
443
- if (identity.type !== record.type)
460
+ if (key !== renderAddress(identity))
444
461
  pageAliases.push([
445
462
  renderAddress(identity),
446
463
  {