@heroiclands/package-build 20.0.0 → 20.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CONTENT.md CHANGED
@@ -349,6 +349,21 @@ pack: mysteries
349
349
  that type's default implicitly; a type with several designates one with
350
350
  `default: true`. Where several exist and none is marked, a declaration is
351
351
  **mandatory** and an undeclared note fails the build.
352
+ - **A default is per system, not merely per type** (#58). The rule above counts
353
+ every pack of a type together, so a tree shipping one Actor pack per system has
354
+ two and would need a flag — except that asked _per system_ the layout is
355
+ unambiguous, one pack each, for the same reason a single-pack type needs no
356
+ flag. And a system is never answered with another system's pack: a type-wide
357
+ default declaring `system: sohl` does not route the HM3 document, which
358
+ otherwise had that pass see a pack name that was not its own and skip every
359
+ note in the tree without a word.
360
+ - **A `pack:` naming another system's pack** is refused where the _block_
361
+ declares it — `hm3.pack` says where the HM3 document goes, so naming a SoHL
362
+ pack is a contradiction, reported naming the note and the pack. At the top
363
+ level it is no contradiction: the shared position is the value a note states
364
+ once for every system, and a system-specific pack cannot be that value, so it
365
+ simply does not answer for the other system, which falls through to its own
366
+ default.
352
367
  - **A `pack:` naming no configured pack is a build error**, not a fall-through to
353
368
  the default. A typo'd name that quietly landed content in the wrong compendium
354
369
  would be silent partial compilation — the failure mode this toolchain's guards
@@ -357,12 +372,43 @@ pack: mysteries
357
372
  - **A note's `pack:` names where its _own_ document goes.** Anything derived from
358
373
  it — an item's or a macro's prose, which compiles into a JournalEntry of its
359
374
  own — lands in the default pack of _that_ type.
360
-
361
- **The configuration is found by walking up, not from the working directory.**
362
- `engine/pack-config.mjs` climbs from itself so it works from `packages/` and
363
- from `node_modules/` alike, and does not depend on the directory the build was
364
- launched from. Set `PACKAGE_BUILD_CONFIG` to point at the file explicitly if a
365
- consumer keeps it somewhere else.
375
+ - **Every document a note produces needs a pack, and one that has none is a
376
+ finding** (#152). A note produces more than one document as a matter of
377
+ course: an item note an Item and the JournalEntry its prose becomes, a map
378
+ note a Scene and a JournalEntry, an actor note an Actor and a JournalEntry
379
+ since #337. Where the configuration declares no pack for one of them, that
380
+ document used to be dropped while the rest of the note compiled into a pack
381
+ that does exist — a build that succeeds and ships half of what was written.
382
+ The finding names the note, the class with no pack, and the class that did
383
+ compile, which is what distinguishes it from a note **nothing** claims: that
384
+ one is a `type:` to correct, this one a pack to declare.
385
+
386
+ It is asked **per note, not per type**, because documentation is: `Journals`
387
+ declines a doc-carrying note whose body is empty — an item with no prose gets
388
+ no doc — so a tree of deliberately description-less items loses nothing by
389
+ having no JournalEntry pack, and is told nothing. And it names no system, so a
390
+ type one system maps and another does not stays silent for the system that
391
+ declines it, per #79.
392
+
393
+ **The configuration is found by walking up from the working directory, and from
394
+ the installed package only if that finds nothing.** `engine/pack-config.mjs`
395
+ climbs from `process.cwd()` first, so a build reads the tree it was run in —
396
+ from the repository root, from `packages/`, from anywhere below, since the walk
397
+ climbs. Climbing from the module itself is the fallback, for an invocation from
398
+ outside any repository.
399
+
400
+ The order matters in one shape: a git worktree nested under its parent checkout
401
+ with **no `node_modules` of its own** resolves `@heroiclands/package-build` out
402
+ of the parent's, because Node's resolution walks parent directories. Climbing
403
+ from the module then landed on the _parent's_ configuration, and the build
404
+ compiled the parent's content tree into the parent's `build/` and exited 0
405
+ (#364). When both walks find a configuration and they disagree, the working
406
+ directory's is read and the ignored one is named in a warning — that
407
+ disagreement is also the cheapest signal that this tree is building on another
408
+ checkout's `node_modules`. Run `npm ci` in the worktree to give it its own.
409
+
410
+ Set `PACKAGE_BUILD_CONFIG` to point at the file explicitly if a consumer keeps
411
+ it somewhere else; it skips both walks.
366
412
 
367
413
  **The configuration is resolved on first read, never at import.** Every module
368
414
  here can be imported — and `content-build --version` and `--help` answered — in a
@@ -456,12 +502,27 @@ systems:
456
502
  sohl: { compatibility: { verified: "0.9.0" } }
457
503
  hm3: { compatibility: { verified: "1.6.3" } }
458
504
  packs:
459
- - { name: items-sohl, type: Item, system: sohl, default: true }
505
+ - { name: items-sohl, type: Item, system: sohl }
460
506
  - { name: items-hm3, type: Item, system: hm3 }
461
- - { name: actors-sohl, type: Actor, system: sohl, default: true }
507
+ - { name: actors-sohl, type: Actor, system: sohl }
462
508
  - { name: actors-hm3, type: Actor, system: hm3 }
463
509
  ```
464
510
 
511
+ **The `systems:` block is required here, not decorative.** A pack's `system:` is
512
+ what its documents are stamped `_stats.systemId` and `systemVersion` from, and
513
+ the version can only come from that block — or, for a package whose packs are
514
+ all for its own system, from the package-wide stats. A pack naming a system that
515
+ resolves to neither is refused at configuration time, naming the pack and the
516
+ entry to add. It used to fall through and stamp `null` for both, which is the
517
+ plausible lie #43 was about arriving by the one path the check did not cover:
518
+ `harn-ensemble` shipped 2,513 compiled actors that way, out of a pack whose
519
+ configuration says `system: sohl` on the line above.
520
+
521
+ No `default: true` anywhere, because each system has exactly one pack of each
522
+ type and a default is resolved per system. Marking one is still allowed and
523
+ still means what it says — it designates that _system's_ default where a system
524
+ has several packs of a type.
525
+
465
526
  A note carrying both a `sohl:` and an `hm3:` block then compiles **one document
466
527
  in each system**, each shaped by its own builders and stamped with its own
467
528
  system version. A note carrying only one block compiles only that system's
@@ -1020,6 +1081,44 @@ key it was probably meant to be, drawn from that type's own vocabulary:
1020
1081
  assets/content/Gear/Axe.md:14:5: error: "wieght" is not a `data:` property of a weapongear; the container is closed, so unlike a top-level key it is not passed through to the page. Did you mean "weight"?
1021
1082
  ```
1022
1083
 
1084
+ **A system block is closed too, and which blocks exist is the configuration's
1085
+ answer** (#58). A package is held to the blocks named after the systems it
1086
+ declares it ships for, read from the three places that already declare them:
1087
+ `systems:`, a pack's own `system:`, and `stats.systemId` where neither is
1088
+ written. So a package shipping for HM3 has its `hm3:` block checked and a
1089
+ package shipping for SoHL its `sohl:`. It used to be a constant, and the
1090
+ constant was `sohl`: an `hm3:` block was never read at all, so every key in it
1091
+ was discarded at compile without a word, while the block that _was_ checked was
1092
+ named after a system the package does not ship for.
1093
+
1094
+ A pack's `system:` counts because it is already authoritative at compile — a
1095
+ note routed to a pack declaring one and carrying no such block fails the build —
1096
+ so a lint blind to it would refuse a note for want of a block it never checked.
1097
+ `harn-ensemble` declares its two systems that way and no other.
1098
+
1099
+ **Each block is checked against its own system's vocabulary**, and that has two
1100
+ sources. A system's **`itemBuilders` registry** covers its item types: `skill` is
1101
+ one name over two data models, so a key SoHL's `skill` declares is not thereby a
1102
+ key HM3's declares, and a block that borrowed its neighbour's field names would
1103
+ accept the one mistake this check exists to report. The **note schemas** cover
1104
+ the rest — `being` above all, which is an actor type and sits in no item
1105
+ registry — and they belong to one system, SoHL, because that is the vocabulary
1106
+ `content-build` is built with.
1107
+
1108
+ A type neither source names — `mysticalability` is SoHL's, `invocation` is
1109
+ HM3's — is a type that system says nothing about, and its block is left alone on
1110
+ such a note rather than reported wholesale. A package that names no system at all
1111
+ is system-agnostic on purpose: its packs are core document types carrying no
1112
+ system data, so it has no system block and none is invented for it.
1113
+
1114
+ **A block whose vocabulary nothing states is said out loud.** A package
1115
+ declaring a system other than SoHL and no `itemBuilders` registry for it has
1116
+ nothing that can say what that block may carry, so the block goes unchecked — and
1117
+ `content-build lint` reports that once, naming the system and the registry to
1118
+ declare, because a check that quietly does nothing is indistinguishable from one
1119
+ that passed. `harn-ensemble` is the tree that gets it today: its `sohl:` block is
1120
+ checked, and its `hm3:` waits on `itemBuilders: [hm3, sohl]`.
1121
+
1023
1122
  **`subType` stays at the top level**, and is closed in its own way: a type
1024
1123
  either declares a `subType` or does not, and a type that does declares its
1025
1124
  values. A `weapon` declares none — SoHL distinguishes a weapon's uses by strike
@@ -1413,6 +1512,28 @@ markdownlint honour them natively. The one exception is `CHANGELOG.md`, which
1413
1512
  `changeset version` regenerates in every repository here: linting it reports on
1414
1513
  the generator, so it is skipped by default.
1415
1514
 
1515
+ **A default that says when it is not in force.** Because a local config wins
1516
+ wholesale rather than merging, the conventions above otherwise hold by convention
1517
+ alone: a `prettier.config.mjs` that spreads `PRETTIER_BASE` without the `**/*.md`
1518
+ override reindents every note at 4, and a partial `.prettierrc` such as
1519
+ `{"tabWidth": 2}` silently discards `printWidth: 100`, `trailingComma` and the
1520
+ rest. So every `format` run first names, as warnings, each shared convention this
1521
+ repository resolves differently (#133):
1522
+
1523
+ ```text
1524
+ prettier.config.mjs: warning: markdown `tabWidth` is 4 here; the shared configuration says 2
1525
+ .prettierrc: warning: `printWidth` is not set here, so Prettier's own default applies; the shared configuration says 100
1526
+ ```
1527
+
1528
+ A repository with **no** Prettier config is warned too, and it is the sharper
1529
+ case: the shared conventions then reach this command and nothing else, so an
1530
+ editor's format-on-save and a bare `npx prettier --check .` apply Prettier's own
1531
+ defaults to the same tree and the two take turns rewriting the same lines. The
1532
+ fix is the one-line re-export below.
1533
+
1534
+ None of this fails a run. A deliberate local choice still wins — it just stops
1535
+ being silent.
1536
+
1416
1537
  Neither tool's file discovery is reimplemented, so `content-build format --check`
1417
1538
  and a bare `prettier --check .` report the same thing. A file Prettier cannot
1418
1539
  parse is a **finding**, with its position — not a crash that costs the report on
@@ -1556,6 +1677,17 @@ asked for — a build that downloads silently is not reproducible and fails
1556
1677
  strangely offline. The cache is keyed by version, so changing the pinned version
1557
1678
  is a miss rather than a silent overwrite.
1558
1679
 
1680
+ **A fetched catalogue is read one system at a time.** A dependency may ship a
1681
+ pack per system, and the two hold the same `(type, shortcode)` addresses with
1682
+ different data models — `skill:awar` is a real address in both vocabularies and
1683
+ means two different documents. So a pack declaring `system: hm3` resolves its
1684
+ embedded items against the dependency's `hm3` packs and its system-neutral ones,
1685
+ never against another system's, exactly as it already does for this repository's
1686
+ own packs. What each cached pack is comes from the dependency's manifest at
1687
+ fetch time, so a cache filled before this rule existed is treated as incomplete
1688
+ and `deps fetch` refills it — the alternative is a lookup that answers with the
1689
+ wrong system's document and reports nothing.
1690
+
1559
1691
  **`--from` is for two packages changing together.** It fills the cache from a
1560
1692
  locally built artifact — a package zip or the directory it was built from — so a
1561
1693
  consumer can be built against a dependency that has not shipped. Without it,
@@ -2318,32 +2450,60 @@ JSDoc.
2318
2450
 
2319
2451
  ## Tests
2320
2452
 
2321
- The package carries its own suite and its own vitest project, so it is
2322
- verifiable without the repository that happens to host it:
2453
+ The package carries its own suite, so it is verifiable without any repository
2454
+ that consumes it:
2323
2455
 
2324
2456
  ```
2325
- npm test -w @heroiclands/package-build # from the SoHL repository root
2326
- npm test # from packages/content-build/
2457
+ npm test # the whole suite
2458
+ npx vitest run tests/wikilinks.test.ts # one file
2327
2459
  ```
2328
2460
 
2329
- The SoHL repository's root `npm run test` names the very same project config, so
2330
- one command still gates everything CI runs and neither entry point can drift
2331
- into a different suite.
2461
+ **The suite is configured from a fixture, not from this repository's root.**
2462
+ `vitest.config.ts` points `PACKAGE_BUILD_CONFIG` at
2463
+ `tests/fixtures/repo/package-build.config.yaml`, whose adjacent `package.json`
2464
+ is shaped like a consumer's. The Foundry package id and the system version are
2465
+ derived from the manifest beside the configuration, and at the root that
2466
+ manifest is this toolchain's own — `@heroiclands/package-build`, which is
2467
+ neither a Foundry package id nor a game system version, so the derivations
2468
+ would assert nothing.
2332
2469
 
2333
2470
  The harness is deliberately austere: no global setup, no Foundry stubs, and no
2334
2471
  alias onto a consuming repository's source. `tests/suite-is-self-contained.test.ts`
2335
- enforces that — a test in this suite that reached for `globalThis.game` or `@src`
2336
- would pass in situ and fail the moment the package was installed from npm.
2472
+ enforces that — a test that reached for `globalThis.game`, imported `@src/…`, or
2473
+ resolved a path climbing out of the package would pass in situ and fail the
2474
+ moment the package was installed from npm. Before the extraction five files
2475
+ resolved `"../../.."` and asserted about whatever happened to be there, which
2476
+ was the system repository, because the package was vendored inside it (#1).
2337
2477
 
2338
2478
  `tests/dependencies-are-declared.test.ts` guards the same failure from the
2339
- shipping side. Because this package is a workspace, npm hoists the root
2340
- repository's `devDependencies` into the workspace root, so an import this
2341
- package never declared still resolves here and fails nowhere but a consumer's
2342
- install (#1557). The test walks every module named by the `files` field and
2343
- holds each bare specifier to one of three cases a Node builtin, this package
2344
- addressing itself, or a declared `dependency` and checks the converse: nothing
2345
- shipped may import a `devDependency`, and no declared dependency may go
2346
- unimported.
2479
+ shipping side. This package spent its first six changes as a workspace inside
2480
+ the Song of Heroic Lands repository, where npm hoisted the root's
2481
+ `devDependencies` into the workspace root: an import this package never declared
2482
+ still resolved, and failed nowhere but a consumer's install (#1557). The test
2483
+ walks every module named by the `files` field and holds each bare specifier to
2484
+ one of three cases — a Node builtin, this package addressing itself, or a
2485
+ declared `dependency` and checks the converse: nothing shipped may import a
2486
+ `devDependency`, and no declared dependency may go unimported.
2487
+
2488
+ `tests/import-needs-no-config.test.ts` is the third of the same family, and the
2489
+ one that keeps "resolved on first read, never at import" honest. It copies the
2490
+ files the package ships into a temporary directory outside this repository and
2491
+ imports each shipped module on its own, in a process whose environment has
2492
+ `PACKAGE_BUILD_CONFIG` deleted. Outside is load-bearing: the config walk climbs
2493
+ from the working directory and from the module's own directory, and the copy is
2494
+ both — so one left inside the tree could reach a configuration above it and
2495
+ prove nothing, which is why the first case asserts that none is reachable from
2496
+ the copy before the rest run. A module that hoisted a configured value to import
2497
+ time fails there, and only there.
2498
+
2499
+ `tests/config-from-working-directory.test.ts` describes the resolution order
2500
+ itself (#364). It builds the shape no unit test can fake — a repository with the
2501
+ toolchain installed under it, and a second checkout nested inside that
2502
+ repository with its own configuration and no `node_modules` — and asserts which
2503
+ configuration a build run in each place reads. The nested case is the one that
2504
+ was wrong, and it is untestable any other way: the tell of a wrong-tree build is
2505
+ normally an unexpected zero diff, and the sweeps that provoke this shape expect
2506
+ zero differences, so only "which file was read" separates the outcomes.
2347
2507
 
2348
2508
  ## Releasing
2349
2509
 
package/README.md CHANGED
@@ -208,6 +208,11 @@ packageBuild:
208
208
  run: [npx, cypress, run]
209
209
  open: [npx, cypress, open]
210
210
 
211
+ # Where that suite writes its results. Optional, and what it buys is the
212
+ # difference between "the suite ran and passed" and "the suite did not
213
+ # run" — a headless run that writes nothing here is reported as a failure.
214
+ results: [cypress/results]
215
+
211
216
  # What the fast loop can rebuild, in the order it must be built: the
212
217
  # bundler empties the stage, so it goes first. `recreate` marks a target
213
218
  # whose output Foundry reads once, at world launch.
@@ -483,6 +488,35 @@ repository can hold without rotting.
483
488
  A green sweep is what licenses moving `compatibility.verified` to that build. A
484
489
  red one is the early warning the sweep exists to produce.
485
490
 
491
+ ### A run that executed nothing is not a pass
492
+
493
+ Because a sweep is evidence, the exit status of the suite is not enough on its
494
+ own to call a run green. Every way of stopping a runner before it starts — a
495
+ corrupt install, a missing browser, a killed process, an `npm ci` racing the
496
+ run and taking `node_modules` with it — produces a run that executed nothing,
497
+ and a harness that reports that as 0 makes the evidence unfalsifiable in the
498
+ one direction that matters (#153).
499
+
500
+ So the suite is bracketed rather than trusted:
501
+
502
+ | Check | What it catches |
503
+ | ----------------------------- | ---------------------------------------------------------------------------------------------------------- |
504
+ | Before, the command resolves | The runner is not installed. The error names it, before a container is stood up and a world seeded. |
505
+ | A runner is read through | `npx cypress run` is checked for **`cypress`**, not `npx` — `npx` is never missing, so it answers nothing. |
506
+ | After, the command still does | The runner went away mid-run. That is a run that cannot have finished, whatever it exited with. |
507
+ | After, results are newer | The suite started and produced nothing. Requires `packageBuild.e2e.results`. |
508
+
509
+ `results` names one path or a list of them, relative to the repository root.
510
+ Existence is not the test — a directory the _previous_ run left behind exists,
511
+ and reading that as evidence would make the check agree with exactly what it
512
+ was built to catch. What counts is a file modified since the spawn.
513
+
514
+ Two things it deliberately does not do. It only ever makes a verdict **worse**:
515
+ a suite that failed keeps its own status, so there is no second path by which
516
+ the harness can report a result that did not happen. And it does not apply to
517
+ `open`, where a person decides what to execute and when to close the runner —
518
+ "it wrote no results" describes that session rather than faulting it.
519
+
486
520
  ### Waiting for a world, not for a port
487
521
 
488
522
  Foundry answers on its port long before a world is serving. A suite started at
@@ -575,13 +609,18 @@ npm test
575
609
  ```
576
610
 
577
611
  Plain `vitest`, no setup file and no aliases: everything here is ESM over Node
578
- built-ins and three dependencies, and a harness that offered a Foundry global
579
- would let something reach for one.
612
+ built-ins and what `dependencies` declares, and a harness that offered a Foundry
613
+ global would let something reach for one. (This said "three dependencies" while
614
+ it described the packaging half alone; absorbing the content half at 3.0.0
615
+ brought the compendium, Markdown and SQL toolchains with it. The number is left
616
+ out rather than restated, because the next dependency would make a restated one
617
+ wrong again.)
580
618
 
581
619
  `tests/dependencies-are-declared.test.ts` is the guard an extraction most needs
582
620
  — every bare specifier in a shipped file must be a builtin, this package, or a
583
- declared `dependency`. Inside a workspace a missing declaration is invisible;
584
- installed from npm it fails on the first import.
621
+ declared `dependency`. Inside a workspace, which this package was for its first
622
+ six changes, a missing declaration is invisible; installed from npm it fails on
623
+ the first import.
585
624
 
586
625
  ## Releasing
587
626
 
@@ -79,7 +79,9 @@ import {
79
79
  } from "../engine/metadata-index.mjs";
80
80
  import { renderItemFieldReference } from "../engine/field-reference.mjs";
81
81
  import { lintContentTree } from "../engine/content-lint.mjs";
82
- import { lintFrontmatter } from "../engine/frontmatter-lint.mjs";
82
+ import { lintContentCharset } from "../engine/content-charset.mjs";
83
+ import { lintContentIcons } from "../engine/content-icons.mjs";
84
+ import { declaredSystems, lintFrontmatter, systemBlocksFor } from "../engine/frontmatter-lint.mjs";
83
85
  import { loadContentFormat } from "../engine/content-format.mjs";
84
86
  import {
85
87
  checkDeclaredFields,
@@ -105,7 +107,7 @@ import { HM3_ITEM_FIELDS } from "../hm3/item-fields.mjs";
105
107
  import { ENGINE_NOTE_SCHEMAS } from "../engine/note-schemas.mjs";
106
108
  import { schemaSubtypeOf } from "../engine/subtype-registry.mjs";
107
109
  import { NOTE_VOCABULARY } from "../engine/note-vocabulary.mjs";
108
- import { checkFormatting, lintMarkdown } from "../engine/prose-lint.mjs";
110
+ import { checkFormatting, checkPrettierConventions, lintMarkdown } from "../engine/prose-lint.mjs";
109
111
  import {
110
112
  authoredFrontmatter,
111
113
  emitContentIndex,
@@ -787,6 +789,38 @@ function lintCommand() {
787
789
  skipDirectories: config.skipDirectories,
788
790
  }),
789
791
  });
792
+ // Which system blocks this tree carries, derived from what it
793
+ // declares it ships for (#58). Read before the lint so the
794
+ // systems it will *not* check can be said out loud below.
795
+ // `schemaSystem` names whose vocabulary the `schemas` below
796
+ // are. They are `sohl/note-schemas.mjs`, imported here
797
+ // unconditionally, so this states a fact about this file rather
798
+ // than introducing one — and it is what lets a type no item
799
+ // registry declares, `being` above all, be checked at all.
800
+ const systemBlocks = systemBlocksFor(config, { schemaSystem: "sohl" });
801
+ const uncheckedSystems = declaredSystems(config).filter(
802
+ (system) => !(system in systemBlocks),
803
+ );
804
+ if (uncheckedSystems.length) {
805
+ // Said out loud for the reason the missing schema artifact
806
+ // is: a check that quietly does nothing is indistinguishable
807
+ // from one that passed, and the thing going unchecked here
808
+ // is a closed region whose unknown keys the compiler drops
809
+ // without a word. `harn-ensemble` is the tree this names —
810
+ // two systems declared through its packs and an
811
+ // `itemBuilders` registry for neither.
812
+ log.info(
813
+ `No \`itemBuilders\` registry declares the vocabulary of ` +
814
+ `${uncheckedSystems.map((s) => `\`${s}\``).join(" or ")}, so ` +
815
+ `${
816
+ uncheckedSystems.length === 1 ?
817
+ "that system's block is"
818
+ : "those systems' blocks are"
819
+ } unchecked — a key inside one is discarded at ` +
820
+ `compile with no warning. Declare ` +
821
+ `\`itemBuilders: [${declaredSystems(config).join(", ")}]\`.`,
822
+ );
823
+ }
790
824
  const frontmatter = lintFrontmatter(index, {
791
825
  schemas: { ...ENGINE_NOTE_SCHEMAS, ...NOTE_SCHEMAS },
792
826
  // The closed frontmatter regions (#128). Passed in rather
@@ -805,6 +839,13 @@ function lintCommand() {
805
839
  // it is where the derivation is handed over — the linter
806
840
  // states no list of iconless types of its own.
807
841
  emittedArt: emittedArtFor,
842
+ // Handed over for the same reason the vocabulary is: the
843
+ // linter checks the blocks it is given and names no system
844
+ // itself. Until this it was given none, so every tree was
845
+ // held to a `sohl:` whatever system it ships for — and an
846
+ // `hm3:` block went unread, which is a key discarded at
847
+ // compile in silence.
848
+ systems: systemBlocks,
808
849
  references: argv.references,
809
850
  });
810
851
 
@@ -883,10 +924,33 @@ function lintCommand() {
883
924
  }
884
925
  }
885
926
 
927
+ // The charset the tree is held to, so a book can pick its face
928
+ // (#377). Run over the raw files rather than the parsed notes:
929
+ // the subject is every character authored, including the ones
930
+ // that stopped a note parsing at all.
931
+ const charset = lintContentCharset(root, {
932
+ skipDirectories: config.skipDirectories,
933
+ });
934
+
935
+ // The names the charset check leaves room for (#378). An icon
936
+ // the registry does not declare renders as its own literal
937
+ // text, which is visible but easy to publish, so it is reported
938
+ // here rather than left for a reader to notice.
939
+ //
940
+ // The shipped registry, with no per-package override yet: an
941
+ // `icons:` configuration key is a change to the configuration
942
+ // contract and belongs with its own validation, rather than
943
+ // being read here before anything declares it.
944
+ const icons = lintContentIcons(root, {
945
+ skipDirectories: config.skipDirectories,
946
+ });
947
+
886
948
  const findings = [
887
949
  ...addresses.findings,
888
950
  ...frontmatter.findings,
889
951
  ...schemaFindings,
952
+ ...charset.findings,
953
+ ...icons.findings,
890
954
  ];
891
955
  // Only an **error** fails the run. Every finding was an error
892
956
  // until #142, so this changed nothing on the day it landed —
@@ -934,6 +998,13 @@ function lintCommand() {
934
998
  * which paths a repository skips is knowledge about that repository's layout,
935
999
  * and it stays there.
936
1000
  *
1001
+ * It is a default that **says when it is not in force**, though. Every run first
1002
+ * reports, as warnings, each shared convention the repository's own
1003
+ * configuration resolves differently — or that it declares no configuration at
1004
+ * all, which guarantees an editor and a bare `npx prettier` disagree with this
1005
+ * command about the same tree (#133). Nothing there fails the run: the point is
1006
+ * that a local choice is deliberate rather than silent.
1007
+ *
937
1008
  * @returns {object} The yargs command module.
938
1009
  */
939
1010
  // eslint-disable-next-line
@@ -967,6 +1038,20 @@ function formatCommand() {
967
1038
  return;
968
1039
  }
969
1040
  const root = process.cwd();
1041
+ // Before the per-file report, because it is the context for it:
1042
+ // which rules this run applied, and whether anything else in
1043
+ // the repository applies the same ones (#133). Warnings only —
1044
+ // a consumer's config wins by design, so none of this touches
1045
+ // the exit code.
1046
+ const conventions = await checkPrettierConventions(root);
1047
+ for (const finding of conventions.findings) emitDiagnostic(finding);
1048
+ if (conventions.findings.length && conventions.configFile) {
1049
+ log.warn(
1050
+ `${conventions.findings.length} shared Prettier convention(s) are not ` +
1051
+ "what this repository resolves; a deliberate local choice wins on " +
1052
+ "purpose, so this is a warning and not a failure.",
1053
+ );
1054
+ }
970
1055
  const { findings, checked, written } = await checkFormatting(root, {
971
1056
  paths: argv.paths,
972
1057
  write: argv.write,
package/config.mjs CHANGED
@@ -126,7 +126,7 @@ const CONTAINER_KEYS = ["image", "name", "stages"];
126
126
  */
127
127
  const CONTAINER_NAME = /^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/;
128
128
  const CONTAINER_STAGE_KEYS = ["port", "world", "version"];
129
- const E2E_KEYS = ["stage", "suite", "build", "world", "gm", "documents"];
129
+ const E2E_KEYS = ["stage", "suite", "results", "build", "world", "gm", "documents"];
130
130
  const E2E_SUITE_KEYS = ["run", "open"];
131
131
  const E2E_WORLD_KEYS = ["id", "title", "description"];
132
132
  const E2E_GM_KEYS = ["name", "password"];
@@ -557,6 +557,8 @@ function normalizeExceptions(value, field, where) {
557
557
  * @property {string} e2eStage Which stage the suite runs against.
558
558
  * @property {Readonly<{run: readonly string[], open: readonly string[]|null}>|null} e2eSuite
559
559
  * What to run against the served world; `null` when the repository has none.
560
+ * @property {readonly string[]} e2eResults Where the suite writes its results,
561
+ * so a run that wrote none is not mistaken for one that passed.
560
562
  * @property {Readonly<Record<string, Readonly<{script: string, recreate: boolean}>>>} e2eBuild
561
563
  * Build targets the fast loop can produce, in declaration order.
562
564
  * @property {Readonly<Record<string, string>>} e2eWorld Declared world identity.
@@ -798,6 +800,12 @@ export function resolvePackageBuildConfig(shared) {
798
800
  "test"
799
801
  : requireNonEmptyString(e2eInput.stage, "packageBuild.e2e.stage"),
800
802
  e2eSuite: normalizeE2ESuite(e2eInput.suite),
803
+ // Declaring nothing keeps the old contract — the suite's exit status is
804
+ // taken at its word. Declaring where results land is what lets the
805
+ // harness tell "the suite ran and passed" from "the suite did not run"
806
+ // (#153); a repository that wants that distinction has to say where to
807
+ // look for it, because the harness does not know what the suite is.
808
+ e2eResults: normalizeGlobs(e2eInput.results, [], "packageBuild.e2e.results"),
801
809
  e2eBuild,
802
810
  e2eWorld: normalizeStringMap(e2eInput.world, "packageBuild.e2e.world", E2E_WORLD_KEYS),
803
811
  e2eGm: normalizeStringMap(e2eInput.gm, "packageBuild.e2e.gm", E2E_GM_KEYS),