@heroiclands/package-build 22.17.0 → 22.17.2

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 (37) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/bin/content-commands.mjs +114 -37
  3. package/config.mjs +54 -3
  4. package/docs/api.md +4 -14
  5. package/docs/commands.md +89 -52
  6. package/docs/configuration.md +49 -22
  7. package/docs/reference/format-details.md +27 -45
  8. package/docs/reference/note-types.md +1 -1
  9. package/engine/actor-compiler.mjs +5 -5
  10. package/engine/address-diff.mjs +32 -279
  11. package/engine/art-fields.mjs +8 -5
  12. package/engine/content-format.yaml +4 -4
  13. package/engine/content-index.mjs +28 -3
  14. package/engine/content-lint.mjs +3 -182
  15. package/engine/frontmatter-lint.mjs +112 -59
  16. package/engine/helpers.mjs +15 -3
  17. package/engine/index.mjs +0 -3
  18. package/engine/map-graphviz.mjs +4 -1
  19. package/engine/note-frontmatter.mjs +1 -0
  20. package/engine/note-vocabulary.mjs +1 -13
  21. package/engine/readability-lint.mjs +196 -18
  22. package/engine/subtype-registry.mjs +2 -6
  23. package/engine/systems.mjs +11 -13
  24. package/package.json +5 -1
  25. package/types/config.d.mts +14 -2
  26. package/types/engine/actor-compiler.d.mts +5 -5
  27. package/types/engine/address-diff.d.mts +18 -107
  28. package/types/engine/art-fields.d.mts +4 -5
  29. package/types/engine/frontmatter-lint.d.mts +14 -0
  30. package/types/engine/helpers.d.mts +12 -1
  31. package/types/engine/index.d.mts +0 -1
  32. package/types/engine/note-vocabulary.d.mts +0 -9
  33. package/types/engine/readability-lint.d.mts +11 -5
  34. package/types/engine/subtype-registry.d.mts +2 -6
  35. package/types/engine/systems.d.mts +6 -6
  36. package/engine/note-renames.mjs +0 -134
  37. package/types/engine/note-renames.d.mts +0 -102
package/CHANGELOG.md CHANGED
@@ -1,5 +1,28 @@
1
1
  # @heroiclands/package-build
2
2
 
3
+ ## 22.17.2
4
+
5
+ ### Patch Changes
6
+
7
+ Being notes can identify encounterable NPCs, playable characters, and creatures with a checked subtype.
8
+
9
+ **Content notes**
10
+
11
+ - Notes can carry an additional system-specific block in their frontmatter.
12
+
13
+ **Content checks**
14
+
15
+ - Lint and link checks report unresolved references in SoHL system fields.
16
+
17
+ ## 22.17.1
18
+
19
+ ### Patch Changes
20
+
21
+ **Prose review**
22
+
23
+ - Prose lint focuses on difficult sentences by default and skips tables and code; simpler-word suggestions remain available on request.
24
+ - Prose score reports each note's readability and lets a content package define an advisory range for its writing.
25
+
3
26
  ## 22.17.0
4
27
 
5
28
  ### Minor Changes
@@ -70,7 +70,12 @@ import { lintContentCharset } from "../engine/content-charset.mjs";
70
70
  import { lintContentHtml } from "../engine/content-html.mjs";
71
71
  import { lintContentIcons } from "../engine/content-icons.mjs";
72
72
  import { lintContentImages } from "../engine/content-images.mjs";
73
- import { declaredSystems, lintFrontmatter, systemBlocksFor } from "../engine/frontmatter-lint.mjs";
73
+ import {
74
+ declaredSystems,
75
+ lintFrontmatter,
76
+ systemAddressFindings,
77
+ systemBlocksFor,
78
+ } from "../engine/frontmatter-lint.mjs";
74
79
  import { loadContentFormat } from "../engine/content-format.mjs";
75
80
  import {
76
81
  checkDeclaredFields,
@@ -97,7 +102,7 @@ import { ENGINE_NOTE_SCHEMAS } from "../engine/note-schemas.mjs";
97
102
  import { schemaSubtypeOf } from "../engine/subtype-registry.mjs";
98
103
  import { NOTE_VOCABULARY } from "../engine/note-vocabulary.mjs";
99
104
  import { checkFormatting, checkPrettierConventions, lintMarkdown } from "../engine/prose-lint.mjs";
100
- import { lintProse } from "../engine/readability-lint.mjs";
105
+ import { lintProse, scoreProseTree } from "../engine/readability-lint.mjs";
101
106
  import { loadPackageBuildConfig } from "../config.mjs";
102
107
  import {
103
108
  authoredFrontmatter,
@@ -124,7 +129,6 @@ import { reportFindings } from "./report.mjs";
124
129
  import {
125
130
  readItemAddresses,
126
131
  diffItemAddresses,
127
- declaredPredecessors,
128
132
  noteFilesById,
129
133
  locateAddressFinding,
130
134
  addressFindingMessage,
@@ -240,14 +244,14 @@ export function registerContentCommands(cli) {
240
244
  .command(addressesCommand());
241
245
  }
242
246
 
243
- /** Opt-in readability and plain-language suggestions for Markdown notes. */
247
+ /** Optional sentence findings and note-level readability reports. */
244
248
  function proseCommand() {
245
249
  return {
246
250
  command: "prose <action> [path]",
247
251
  describe: "Analyze note prose on demand",
248
252
  builder: (yargs) =>
249
253
  yargs
250
- .positional("action", { choices: ["lint"], describe: "Analyze prose" })
254
+ .positional("action", { choices: ["lint", "score"], describe: "Analyze prose" })
251
255
  .positional("path", {
252
256
  type: "string",
253
257
  describe: "One Markdown file or a content tree",
@@ -257,17 +261,97 @@ function proseCommand() {
257
261
  type: "number",
258
262
  describe: "Readability algorithms required (1–7)",
259
263
  })
260
- .option("min-words", { type: "number", describe: "Minimum words per sentence" }),
264
+ .option("min-words", {
265
+ type: "number",
266
+ describe: "Minimum words per sentence or note",
267
+ })
268
+ .option("rules", {
269
+ choices: ["readability", "simplify", "all"],
270
+ describe: "Lint rule set",
271
+ })
272
+ .option("fail-on-warning", {
273
+ type: "boolean",
274
+ default: false,
275
+ describe: "Exit unsuccessfully when lint reports suggestions",
276
+ })
277
+ .option("fail-outside", {
278
+ type: "boolean",
279
+ default: false,
280
+ describe: "Exit unsuccessfully when a scored note is outside a configured band",
281
+ }),
261
282
  handler: async (argv) => {
262
283
  try {
263
284
  const config = loadPackConfig();
264
- const defaults = loadPackageBuildConfig().proseLint;
285
+ const defaults = loadPackageBuildConfig();
286
+ const root = argv.path ?? config.paths.content;
287
+ const displayFile = (file) => path.relative(process.cwd(), path.resolve(file));
288
+ if (argv.action === "score") {
289
+ if (
290
+ argv.age !== undefined ||
291
+ argv.threshold !== undefined ||
292
+ argv.rules !== undefined ||
293
+ argv.failOnWarning
294
+ ) {
295
+ throw new Error(
296
+ "--age, --threshold, --rules, and --fail-on-warning apply only to prose lint",
297
+ );
298
+ }
299
+ const minWords = argv.minWords ?? defaults.proseScore.minWords;
300
+ if (!Number.isInteger(minWords) || minWords < 1) {
301
+ throw new Error("minWords must be a positive integer");
302
+ }
303
+ const result = await scoreProseTree(
304
+ root,
305
+ { ...defaults.proseScore, minWords },
306
+ config.skipDirectories,
307
+ );
308
+ const fixed = (number, places = 1) => number.toFixed(places);
309
+ for (const note of result.notes) {
310
+ if (note.insufficient) {
311
+ console.log(
312
+ `${displayFile(note.file)}: insufficient prose (${note.words}/${minWords} words; coverage=${fixed(note.coveragePercent)}%)`,
313
+ );
314
+ continue;
315
+ }
316
+ const metrics = note.metrics;
317
+ const detail =
318
+ `flesch=${fixed(metrics.flesch)} syl/word=${fixed(metrics.syllablesPerWord, 2)} ` +
319
+ `mean=${fixed(metrics.meanSentenceWords)}w longest=${note.longestSentenceWords}w ` +
320
+ `unfamiliar=${fixed(metrics.unfamiliarWordPercent)}% ` +
321
+ `nominalizations/1k=${fixed(metrics.nominalizationsPer1000Words)} ` +
322
+ `words=${note.words} coverage=${fixed(note.coveragePercent)}%`;
323
+ if (note.violations.length) {
324
+ emitDiagnostic({
325
+ file: displayFile(note.file),
326
+ severity: "warning",
327
+ message: `prose-score/band: ${detail}; ${note.violations.join(", ")}`,
328
+ });
329
+ } else {
330
+ console.log(`${displayFile(note.file)}: ${detail}`);
331
+ }
332
+ }
333
+ const aggregate = result.summary;
334
+ console.log(
335
+ `Total: ${aggregate.scored} scored, ${aggregate.insufficient} insufficient, ` +
336
+ `${aggregate.outside} outside band; ${aggregate.words} words; ` +
337
+ `pooled flesch=${aggregate.flesch === null ? "n/a" : fixed(aggregate.flesch)} ` +
338
+ `syl/word=${aggregate.words ? fixed(aggregate.syllables / aggregate.words, 2) : "n/a"} ` +
339
+ `mean=${aggregate.sentences ? fixed(aggregate.words / aggregate.sentences) : "n/a"}w ` +
340
+ `unfamiliar=${aggregate.words ? fixed((100 * aggregate.unfamiliar) / aggregate.words) : "n/a"}% ` +
341
+ `nominalizations/1k=${aggregate.words ? fixed((1000 * aggregate.nominalizations) / aggregate.words) : "n/a"}`,
342
+ );
343
+ if (argv.failOutside && aggregate.outside) process.exitCode = 1;
344
+ return;
345
+ }
346
+ if (argv.failOutside) throw new Error("--fail-outside applies only to prose score");
265
347
  const options = {
266
- age: argv.age ?? defaults.age,
267
- threshold: argv.threshold ?? defaults.threshold,
268
- minWords: argv.minWords ?? defaults.minWords,
348
+ age: argv.age ?? defaults.proseLint.age,
349
+ threshold: argv.threshold ?? defaults.proseLint.threshold,
350
+ minWords: argv.minWords ?? defaults.proseLint.minWords,
351
+ rules: argv.rules ?? defaults.proseLint.rules,
269
352
  };
270
353
  for (const [key, value] of Object.entries(options)) {
354
+ if (key === "rules") continue;
271
355
  if (
272
356
  !Number.isInteger(value) ||
273
357
  value < 1 ||
@@ -278,13 +362,12 @@ function proseCommand() {
278
362
  );
279
363
  }
280
364
  }
281
- const findings = await lintProse(
282
- argv.path ?? config.paths.content,
283
- options,
284
- config.skipDirectories,
285
- );
286
- for (const finding of findings) emitDiagnostic(finding);
365
+ const findings = await lintProse(root, options, config.skipDirectories);
366
+ for (const finding of findings) {
367
+ emitDiagnostic({ ...finding, file: displayFile(finding.file) });
368
+ }
287
369
  log.info(`${findings.length} prose suggestion(s).`);
370
+ if (argv.failOnWarning && findings.length) process.exitCode = 1;
288
371
  } catch (err) {
289
372
  reportFailure(err);
290
373
  process.exitCode = 1;
@@ -1370,6 +1453,13 @@ function linksCommand() {
1370
1453
  homepageLinks,
1371
1454
  usedManifest,
1372
1455
  } = auditLinks(index);
1456
+ const systemReferences = index.notes.flatMap((note) =>
1457
+ systemAddressFindings(note, {
1458
+ index,
1459
+ schemas: { ...ENGINE_NOTE_SCHEMAS, ...NOTE_SCHEMAS },
1460
+ systems: systemBlocksFor(config, { schemaSystem: "sohl" }),
1461
+ }),
1462
+ );
1373
1463
 
1374
1464
  for (const d of deadAnchors) {
1375
1465
  emitDiagnostic({
@@ -1407,6 +1497,7 @@ function linksCommand() {
1407
1497
  `${f.path} — frontmatter is data and is never resolved`,
1408
1498
  });
1409
1499
  }
1500
+ for (const finding of systemReferences) emitDiagnostic(finding);
1410
1501
 
1411
1502
  // The package homepage. Its addresses are markdown links and
1412
1503
  // `landing:` url/href fields rather than wikilinks — it is
@@ -1427,6 +1518,7 @@ function linksCommand() {
1427
1518
  deadEmbeds.length +
1428
1519
  unlabelledLinks.length +
1429
1520
  frontmatterLinks.length +
1521
+ systemReferences.length +
1430
1522
  homepageLinks.length;
1431
1523
  if (failures) {
1432
1524
  log.error(`${failures} link problem(s) across ${index.notes.length} note(s).`);
@@ -2179,21 +2271,11 @@ function depsCommand() {
2179
2271
  * `addresses diff` — report every published `(type, shortcode)` this build no
2180
2272
  * longer publishes, against a released artifact.
2181
2273
  *
2182
- * The address space is a published interface (see `engine/address-diff.mjs`),
2183
- * and renaming a shortcode used to cost nothing and produce no signal. This is
2184
- * the signal, emitted in the repository doing the renaming while the change is
2185
- * still in front of the author.
2186
- *
2187
- * **Its own command rather than a step of `package compile`.** It reads a
2188
- * *second* artifact that the compile knows nothing about and that has to be
2189
- * obtained separately, and it is a question about a release rather than about a
2190
- * build — a repository between releases has nothing to compare against.
2274
+ * Compare this package's compiled Item addresses with the release artifact
2275
+ * named by `--from`. The command reports departed addresses and locates
2276
+ * findings against the current content tree or baseline artifact.
2191
2277
  *
2192
- * **The baseline is named, never derived, and never downloaded.** `--from`
2193
- * takes the artifact — the `.zip` a release publishes, or a directory built
2194
- * from one — for the same reason `deps fetch --from` does: a command that
2195
- * reaches the network on its own is not reproducible and fails strangely
2196
- * offline. In a release workflow the artifact is one line ahead of it:
2278
+ * `--from` accepts a release `.zip` or a directory built from one:
2197
2279
  *
2198
2280
  * ```sh
2199
2281
  * gh release download v0.8.2 -p system.zip -D build/baseline
@@ -2253,10 +2335,6 @@ async function diffAddresses(config, argv) {
2253
2335
  readItemAddresses(currentDirs),
2254
2336
  {
2255
2337
  baseline: label,
2256
- // Read whether or not anything departed: an id match needs no tree,
2257
- // but the diff decides rename-versus-withdrawal as it walks the
2258
- // baseline, so the declarations have to be in hand before it does.
2259
- predecessors: declaredPredecessors(config.paths.content, corpus),
2260
2338
  },
2261
2339
  );
2262
2340
  if (!findings.length) {
@@ -2264,9 +2342,8 @@ async function diffAddresses(config, argv) {
2264
2342
  return;
2265
2343
  }
2266
2344
 
2267
- // A rename is fixed in the note that made it, so findings are placed
2268
- // against the tree rather than against the compiled output they were read
2269
- // from.
2345
+ // The tree locates a matched document; otherwise the baseline locates the
2346
+ // departed address.
2270
2347
  const noteFiles = noteFilesById(config.paths.content, corpus);
2271
2348
  const severity = argv.strict ? "error" : "warning";
2272
2349
  for (const finding of findings) {
package/config.mjs CHANGED
@@ -76,6 +76,7 @@ const SECTION_KEYS = [
76
76
  "container",
77
77
  "e2e",
78
78
  "proseLint",
79
+ "proseScore",
79
80
  ];
80
81
 
81
82
  /**
@@ -528,7 +529,8 @@ function normalizeExceptions(value, field, where) {
528
529
  *
529
530
  * @typedef {object} PackageBuildConfig
530
531
  * @property {string} rootDir The repository root, from package-build.
531
- * @property {{age: number, threshold: number, minWords: number}} proseLint Optional prose analysis settings.
532
+ * @property {{age: number, threshold: number, minWords: number, rules: string}} proseLint Optional prose lint settings.
533
+ * @property {{minWords: number, bands: Record<string, {min?: number, max?: number}>}} proseScore Optional note score settings.
532
534
  * @property {string} packageKind `systems` or `modules`.
533
535
  * @property {string} packageId The Foundry package id.
534
536
  * @property {string} artifact Derived: `system` or `module`.
@@ -693,9 +695,20 @@ export function resolvePackageBuildConfig(shared) {
693
695
 
694
696
  const proseLint = section.proseLint ?? {};
695
697
  if (!isMapping(proseLint)) fail("packageBuild.proseLint", "must be a mapping");
696
- rejectUnknownKeys(proseLint, ["age", "threshold", "minWords"], "packageBuild.proseLint.");
697
- const proseOptions = { age: 21, threshold: 5, minWords: 8 };
698
+ rejectUnknownKeys(
699
+ proseLint,
700
+ ["age", "threshold", "minWords", "rules"],
701
+ "packageBuild.proseLint.",
702
+ );
703
+ const proseOptions = { age: 21, threshold: 5, minWords: 8, rules: "readability" };
698
704
  for (const [key, value] of Object.entries(proseLint)) {
705
+ if (key === "rules") {
706
+ if (!["readability", "simplify", "all"].includes(value)) {
707
+ fail("packageBuild.proseLint.rules", "must be readability, simplify, or all");
708
+ }
709
+ proseOptions.rules = value;
710
+ continue;
711
+ }
699
712
  const upper = key === "threshold" ? 7 : Infinity;
700
713
  if (!Number.isInteger(value) || value < 1 || value > upper) {
701
714
  fail(
@@ -708,6 +721,43 @@ export function resolvePackageBuildConfig(shared) {
708
721
  proseOptions[key] = value;
709
722
  }
710
723
 
724
+ const proseScore = section.proseScore ?? {};
725
+ if (!isMapping(proseScore)) fail("packageBuild.proseScore", "must be a mapping");
726
+ rejectUnknownKeys(proseScore, ["minWords", "bands"], "packageBuild.proseScore.");
727
+ const scoreMinWords = proseScore.minWords ?? 80;
728
+ if (!Number.isInteger(scoreMinWords) || scoreMinWords < 1) {
729
+ fail("packageBuild.proseScore.minWords", "must be a positive integer");
730
+ }
731
+ const bands = proseScore.bands ?? {};
732
+ if (!isMapping(bands)) fail("packageBuild.proseScore.bands", "must be a mapping");
733
+ const scoreMetrics = [
734
+ "flesch",
735
+ "syllablesPerWord",
736
+ "meanSentenceWords",
737
+ "longestSentenceWords",
738
+ "unfamiliarWordPercent",
739
+ "nominalizationsPer1000Words",
740
+ ];
741
+ rejectUnknownKeys(bands, scoreMetrics, "packageBuild.proseScore.bands.");
742
+ const scoreBands = {};
743
+ for (const [metric, band] of Object.entries(bands)) {
744
+ const where = `packageBuild.proseScore.bands.${metric}`;
745
+ if (!isMapping(band)) fail(where, "must be a mapping");
746
+ rejectUnknownKeys(band, ["min", "max"], `${where}.`);
747
+ if (band.min === undefined && band.max === undefined) {
748
+ fail(where, "must specify min, max, or both");
749
+ }
750
+ for (const [limit, value] of Object.entries(band)) {
751
+ if (typeof value !== "number" || !Number.isFinite(value)) {
752
+ fail(`${where}.${limit}`, "must be a finite number");
753
+ }
754
+ }
755
+ if (band.min !== undefined && band.max !== undefined && band.min > band.max) {
756
+ fail(where, "min must not exceed max");
757
+ }
758
+ scoreBands[metric] = Object.freeze({ ...band });
759
+ }
760
+
711
761
  if (section.assets !== undefined && !Array.isArray(section.assets)) {
712
762
  fail("packageBuild.assets", "must be a list");
713
763
  }
@@ -809,6 +859,7 @@ export function resolvePackageBuildConfig(shared) {
809
859
  return Object.freeze({
810
860
  rootDir: shared.rootDir,
811
861
  proseLint: Object.freeze(proseOptions),
862
+ proseScore: Object.freeze({ minWords: scoreMinWords, bands: Object.freeze(scoreBands) }),
812
863
  // Where the package is assembled before it is zipped or deployed. Every
813
864
  // asset destination is relative to it, so a repository's table says
814
865
  // `lang`, not `build/stage/lang` — the latter is what each consumer's
package/docs/api.md CHANGED
@@ -347,7 +347,7 @@ The note types the engine itself declares — vocabulary that is a fact about th
347
347
 
348
348
  ### `engine.noteVocabulary`
349
349
 
350
- The closed half of a note's frontmatter: the `data:` container and each type's `subType` — the only two regions where an unrecognized key is refused rather than passed through to the page.
350
+ An addressed content note accepts exactly these top-level keys, in this order: `shortcode`, `name`, `type`, `subType`, `description`, `tags`, `data`, `hm3`, `sohl`, `dnd5e`. A key outside this list is an error. The address vocabulary recognizes three systems: `sohl`, `hm3`, and `dnd5e`. No dnd5e-specific document types are declared. Within that shape, the closed vocabularies are the `data:` container and each type's `subType`.
351
351
 
352
352
  | Export | Signature | Returns | Use it when |
353
353
  | ------------------------- | -------------------------------------------- | ---------------------------------------- | -------------------------------------------------------------------------------------------- |
@@ -358,7 +358,7 @@ The closed half of a note's frontmatter: the `data:` container and each type's `
358
358
  | `DECLARED_TAGS` | `const DECLARED_TAGS` | — | looking up the tags a note type may declare, grouped |
359
359
  | `declaredTags` | `declaredTags(type, groups)` | `readonly string[]` | reading the declared tags a note of a type may carry, flattened |
360
360
  | `applicableTagGroups` | `applicableTagGroups(type, groups)` | `object[]` | reading the declared tag groups that apply to a note type |
361
- | `exclusiveTagGroups` | `exclusiveTagGroups(type, groups)` | `Array<{slot, tags}>` | reading the single-valued tag slots a note type has, such as a being's kind |
361
+ | `exclusiveTagGroups` | `exclusiveTagGroups(type, groups)` | `Array<{slot, tags}>` | reading single-valued tag slots declared for a note type |
362
362
  | `hasTag` | `hasTag(fm, tag)` | `boolean` | checking whether a note carries a given tag, whatever scalar-or-list form it was authored in |
363
363
  | `isDraftNote` | `isDraftNote(fm)` | `boolean` | checking whether a note is tagged as an unfinished draft |
364
364
  | `isGmNote` | `isGmNote(fm)` | `boolean` | checking whether a note is restricted to GM-facing output |
@@ -607,13 +607,13 @@ The map from each place, drawn for the site: `package-build site` draws the map
607
607
 
608
608
  ### `engine.systems`
609
609
 
610
- The closed registry of system ids, and the `none` that stands for no system at all. An unknown system value is an error; adding a system is a data change to this registry rather than a hardcoded set scattered through the pipeline.
610
+ The closed registry of system ids, and the `none` that stands for no system at all. An unknown system value is an error; adding a system is a data change to this registry rather than a hardcoded set scattered through the pipeline. The registry recognizes `sohl`, `hm3`, and `dnd5e`; no dnd5e-specific document types are declared.
611
611
 
612
612
  | Export | Signature | Returns | Use it when |
613
613
  | ---------------------- | -------------------------------------- | ------------------------------- | -------------------------------------------------------------------------------------- |
614
614
  | `NO_SYSTEM` | `const NO_SYSTEM` | — | naming a systemless asset or Foundry document |
615
615
  | `NOTE_SYSTEM` | `const NOTE_SYSTEM` | — | naming readable note content across web, book and Foundry |
616
- | `SYSTEM_IDS` | `const SYSTEM_IDS` | — | enumerating every game system this toolchain compiles for |
616
+ | `SYSTEM_IDS` | `const SYSTEM_IDS` | — | enumerating game system ids accepted in configuration and addresses |
617
617
  | `SYSTEM_SEGMENTS` | `const SYSTEM_SEGMENTS` | — | enumerating the game systems, `note` and `none` |
618
618
  | `isSystemId` | `isSystemId(value)` | `boolean` | checking whether a value names a game system this toolchain knows (`none` is rejected) |
619
619
  | `isSystemSegment` | `isSystemSegment(value)` | `boolean` | checking whether a value is something the `<system>` address segment may hold |
@@ -809,16 +809,6 @@ The id a note's document is filed under: its pinned `id:`, when a note authors o
809
809
  | `noteDocId` | `function noteDocId(fm,` | {string\|undefined} The document's `_id`, or `undefined` when the note has no address to derive one from. | The document id a note compiles under: its pin, or its address. |
810
810
  | `resolveNoteId` | `function resolveNoteId(fm, opts)` | {object\|null\|undefined} `fm`, for chaining. | Fill a note's `id` in place, so everything downstream reads one value. |
811
811
 
812
- ### `engine.noteRenames`
813
-
814
- The shortcodes a note declares it used to be published under. A package's `(type, shortcode)` addresses are a published interface, and `addresses diff` reports what a build stopped publishing — telling a **rename** from a **withdrawal** by matching document ids across two releases. The property that rested on is gone: an id is derived from the canonical address, which carries the shortcode, so renaming a shortcode moves the id too: both sides of the join move together, the match finds nothing, and a rename is reported as a withdrawal with no successor named.
815
-
816
- | Export | Signature | Returns | Use it when |
817
- | --------------------- | ---------------------------------- | ------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------- |
818
- | `renamedFromEntries` | `function renamedFromEntries(fm)` | {readonly unknown[]} The authored entries, in authored order; empty when the note declares none. | The `renamedFrom:` entries a note authors, exactly as authored. |
819
- | `renamedFrom` | `function renamedFrom(fm)` | {string[]} The declared predecessor shortcodes. | The well-formed shortcodes among a note's `renamedFrom:` entries. |
820
- | `declaresRenamedFrom` | `function declaresRenamedFrom(fm)` | {boolean} `true` when the key is present and not null. | Whether a note declares the key at all, however malformed its value. |
821
-
822
812
  ### `engine.metadataIndex`
823
813
 
824
814
  The published content index — the artifact packages exchange addresses through. **A package publishes its own index; a consumer fetches the ones it depends on.** That is the whole mechanism, and it replaces a vendored link manifest that each repository committed a copy of every other repository's file into. Vendoring failed three ways, and only the last is about staleness: