@rpgm-tools/neo-angband-mod-sdk 0.13.0 → 0.15.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.
package/README.md CHANGED
@@ -21,9 +21,12 @@ compose records without pulling in a game.
21
21
  | Area | Exports |
22
22
  | --- | --- |
23
23
  | Manifests | `validateManifest`, `PackManifest`, `PackShape`, `packFacets`, `hasFacet`, `packRef`, `slugify` |
24
- | Load order | `resolveLoadOrder`, `satisfies` (the semver subset the manifests use) |
24
+ | Load order | `resolveLoadOrder` (enforces), `satisfies` (the semver subset the manifests use) |
25
+ | Auto-sort | `sortModOrder` (proposes, and cannot fail), `collectSortEdges`, `SortPin`, `SortTier`, `PACK_GROUPS` |
26
+ | Sections | `resolveSectionState`, `expandSections`, `sectionFlag`, `PackSection`, `SECTION_BANDS` — the named parts of a mod |
27
+ | Compatibility | `PackCompat`, `COMPAT_CLAIMS` — what an author may claim about another mod (never binding) |
25
28
  | Composition | `composePacks`, `composeContentPacks`, `mergePatch`, `applyFieldPatch`, `composeFieldPatches` |
26
- | Conflicts | `computeConflictReport`, `RecordConflict`, `FieldTouch`, `RecordOverride` |
29
+ | Conflicts | `computeConflictReport` (records), `contestedSlots` / `describeContested` (every other layer), `Fold`, `foldDiscards` |
27
30
  | Record identity | `recordKey`, `keySpecFor`, `KEYED_RECORD_FILES`, `RECORD_KEY_SPECS` |
28
31
  | Capabilities | `CapabilitySet`, `parseCapability` — what a scripted plugin is allowed to reach |
29
32
 
package/dist/compose.d.ts CHANGED
@@ -44,6 +44,23 @@ export interface FileContribution {
44
44
  * conflicts `patches` produces.
45
45
  */
46
46
  fieldPatches?: Record<string, FieldPatch>;
47
+ /**
48
+ * Contributions attributed to a NAMED PART of this pack, keyed by a section id
49
+ * the manifest declares (see PackSection). Each value is an ordinary
50
+ * FileContribution, so a section contributes exactly what the pack itself can.
51
+ *
52
+ * Nested rather than a `section` key on each entry because `patches`,
53
+ * `replaces` and `fieldPatches` are all keyed BY REF - there is no room for a
54
+ * per-entry tag without changing three shapes, and every existing pack would
55
+ * have had to be rewritten. This way an unsectioned contribution stays exactly
56
+ * where it is and belongs to the pack's implicit default part.
57
+ *
58
+ * composePacks never sees this: expandSections (sections.ts) drops the
59
+ * disabled sections and flattens the rest into the pack list, in band order,
60
+ * BEFORE composition. So a switched-off section is absent rather than
61
+ * overridden - the same rule a disabled mod's hooks follow.
62
+ */
63
+ sections?: Record<string, FileContribution>;
47
64
  }
48
65
  export interface PackContent {
49
66
  manifest: PackManifest;
@@ -62,10 +79,52 @@ export declare class ComposeError extends Error {
62
79
  }
63
80
  /** Deep merge per the pack patch rules. Returns a new object. */
64
81
  export declare function mergePatch(base: JsonRecord, patch: JsonRecord): JsonRecord;
82
+ /**
83
+ * How composePacks should react to a contribution it cannot honour.
84
+ *
85
+ * WHY THIS IS AN OPTION AND NOT A DECISION. The same event has two right answers
86
+ * depending on who is watching. A mod's BUILD should stop dead on a patch aimed
87
+ * at nothing, because the author is right there and a silent no-op is the worst
88
+ * thing you can hand them. A player's GAME should not: their mod was fine when it
89
+ * was published and the record it patches has since moved, and taking the whole
90
+ * mod away - or the whole game - is a punishment for the engine's change.
91
+ *
92
+ * So the throwing behaviour stays the default (every existing caller and the
93
+ * author-facing tooling keep it) and the host passes a reporter.
94
+ */
95
+ export interface ComposePacksOptions {
96
+ /**
97
+ * Called instead of throwing, with the offending pack's id kept separate from
98
+ * the sentence so a host can put the line on that mod's own row. The
99
+ * contribution is then SKIPPED and composition continues.
100
+ */
101
+ readonly onRefuse?: (packId: string, why: string) => void;
102
+ }
103
+ /**
104
+ * The tail every "no such record" refusal carries, in both merge phases.
105
+ *
106
+ * Said because the likeliest cause is not a typo. A pack that composed when it
107
+ * was published and does not now is usually pointing at a record the engine or
108
+ * another pack has since renamed, and an author reading "does not exist" about a
109
+ * ref they know they got right will go looking in the wrong place.
110
+ */
111
+ export declare const RENAMED_HINT = " - it may have been renamed or removed by a newer version of the pack that owns it";
65
112
  /**
66
113
  * Compose packs (already in resolved load order) into per-file record
67
114
  * maps. Iteration order of each map is deterministic: records appear
68
115
  * in the order their owning packs added them.
116
+ *
117
+ * ONE BROKEN OP COSTS THAT OP, when `onRefuse` is supplied. Until 2026-08-02 the
118
+ * only behaviour was to throw, and the caller that mattered - composeContentPacks
119
+ * on the web host - sat under composeDroppingBroken, which answers a throw by
120
+ * removing the whole PACK. The result was an asymmetry nobody chose: the 20
121
+ * passthrough record files reported a missing ref and carried on, and the 24
122
+ * composable ones took the entire mod down for the same author mistake. A mod
123
+ * patching forty monsters lost all forty, plus its code and its rules, because
124
+ * one of the forty had been renamed in the engine.
125
+ *
126
+ * That is also the difference between an engine patch that costs mod authors a
127
+ * release and one that costs them nothing, which is the property this exists for.
69
128
  */
70
- export declare function composePacks(packs: readonly PackContent[]): Map<string, Map<PackRef, ComposedRecord>>;
129
+ export declare function composePacks(packs: readonly PackContent[], options?: ComposePacksOptions): Map<string, Map<PackRef, ComposedRecord>>;
71
130
  //# sourceMappingURL=compose.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"compose.d.ts","sourceRoot":"","sources":["../src/compose.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAG3D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE7C,MAAM,MAAM,SAAS,GACjB,IAAI,GACJ,OAAO,GACP,MAAM,GACN,MAAM,GACN,SAAS,EAAE,GACX;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,CAAC;AACjC,MAAM,MAAM,UAAU,GAAG;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,CAAC;AAEtD,mEAAmE;AACnE,MAAM,WAAW,gBAAgB;IAC/B,qEAAqE;IACrE,OAAO,CAAC,EAAE,UAAU,EAAE,CAAC;IACvB,sEAAsE;IACtE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IACrC,4DAA4D;IAC5D,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IACtC,6CAA6C;IAC7C,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB;;;;;;;;OAQG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;CAC3C;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,YAAY,CAAC;IACvB,mEAAmE;IACnE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;CACzC;AAED,MAAM,WAAW,cAAc;IAC7B,GAAG,EAAE,OAAO,CAAC;IACb,sCAAsC;IACtC,KAAK,EAAE,MAAM,CAAC;IACd,6DAA6D;IAC7D,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,KAAK,EAAE,UAAU,CAAC;CACnB;AAED,qBAAa,YAAa,SAAQ,KAAK;CAAG;AAE1C,iEAAiE;AACjE,wBAAgB,UAAU,CAAC,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,UAAU,GAAG,UAAU,CAkB1E;AAWD;;;;GAIG;AACH,wBAAgB,YAAY,CAC1B,KAAK,EAAE,SAAS,WAAW,EAAE,GAC5B,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,OAAO,EAAE,cAAc,CAAC,CAAC,CA4E3C"}
1
+ {"version":3,"file":"compose.d.ts","sourceRoot":"","sources":["../src/compose.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAG3D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE7C,MAAM,MAAM,SAAS,GACjB,IAAI,GACJ,OAAO,GACP,MAAM,GACN,MAAM,GACN,SAAS,EAAE,GACX;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,CAAC;AACjC,MAAM,MAAM,UAAU,GAAG;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,CAAC;AAEtD,mEAAmE;AACnE,MAAM,WAAW,gBAAgB;IAC/B,qEAAqE;IACrE,OAAO,CAAC,EAAE,UAAU,EAAE,CAAC;IACvB,sEAAsE;IACtE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IACrC,4DAA4D;IAC5D,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IACtC,6CAA6C;IAC7C,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB;;;;;;;;OAQG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IAC1C;;;;;;;;;;;;;;;OAeG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;CAC7C;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,YAAY,CAAC;IACvB,mEAAmE;IACnE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;CACzC;AAED,MAAM,WAAW,cAAc;IAC7B,GAAG,EAAE,OAAO,CAAC;IACb,sCAAsC;IACtC,KAAK,EAAE,MAAM,CAAC;IACd,6DAA6D;IAC7D,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,KAAK,EAAE,UAAU,CAAC;CACnB;AAED,qBAAa,YAAa,SAAQ,KAAK;CAAG;AAE1C,iEAAiE;AACjE,wBAAgB,UAAU,CAAC,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,UAAU,GAAG,UAAU,CAkB1E;AAWD;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,mBAAmB;IAClC;;;;OAIG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CAC3D;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,YAAY,uFAC6D,CAAC;AAUvF;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,YAAY,CAC1B,KAAK,EAAE,SAAS,WAAW,EAAE,EAC7B,OAAO,GAAE,mBAAwB,GAChC,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,OAAO,EAAE,cAAc,CAAC,CAAC,CA8F3C"}
package/dist/compose.js CHANGED
@@ -47,43 +47,94 @@ function ownerOf(ref) {
47
47
  const at = ref.indexOf(":");
48
48
  return at === -1 ? "" : ref.slice(0, at);
49
49
  }
50
+ /**
51
+ * The tail every "no such record" refusal carries, in both merge phases.
52
+ *
53
+ * Said because the likeliest cause is not a typo. A pack that composed when it
54
+ * was published and does not now is usually pointing at a record the engine or
55
+ * another pack has since renamed, and an author reading "does not exist" about a
56
+ * ref they know they got right will go looking in the wrong place.
57
+ */
58
+ export const RENAMED_HINT = " - it may have been renamed or removed by a newer version of the pack that owns it";
59
+ /** How a refused op reads, matched to the passthrough path's wording. */
60
+ const REF_VERB = {
61
+ patches: "patches",
62
+ replaces: "replaces",
63
+ fieldPatches: "fieldPatches",
64
+ removes: "removes",
65
+ };
50
66
  /**
51
67
  * Compose packs (already in resolved load order) into per-file record
52
68
  * maps. Iteration order of each map is deterministic: records appear
53
69
  * in the order their owning packs added them.
70
+ *
71
+ * ONE BROKEN OP COSTS THAT OP, when `onRefuse` is supplied. Until 2026-08-02 the
72
+ * only behaviour was to throw, and the caller that mattered - composeContentPacks
73
+ * on the web host - sat under composeDroppingBroken, which answers a throw by
74
+ * removing the whole PACK. The result was an asymmetry nobody chose: the 20
75
+ * passthrough record files reported a missing ref and carried on, and the 24
76
+ * composable ones took the entire mod down for the same author mistake. A mod
77
+ * patching forty monsters lost all forty, plus its code and its rules, because
78
+ * one of the forty had been renamed in the engine.
79
+ *
80
+ * That is also the difference between an engine patch that costs mod authors a
81
+ * release and one that costs them nothing, which is the property this exists for.
54
82
  */
55
- export function composePacks(packs) {
83
+ export function composePacks(packs, options = {}) {
56
84
  const game = new Map();
85
+ const onRefuse = options.onRefuse;
57
86
  for (const pack of packs) {
58
87
  const pid = pack.manifest.id;
88
+ /** Report and skip, or throw when nobody is listening. Returns false either way. */
89
+ const refuse = (why, thrown) => {
90
+ if (!onRefuse)
91
+ throw new ComposeError(`${pid}/${thrown}`);
92
+ onRefuse(pid, why);
93
+ return false;
94
+ };
59
95
  for (const [file, contrib] of Object.entries(pack.files)) {
60
96
  let table = game.get(file);
61
97
  if (!table) {
62
98
  table = new Map();
63
99
  game.set(file, table);
64
100
  }
101
+ /**
102
+ * Can `pid` touch `ref`, and does it exist? Reports the same two reasons
103
+ * the passthrough path reports, in the same words, so one mod's row does
104
+ * not read differently depending on which of the two merge phases its file
105
+ * happened to land in.
106
+ */
107
+ const addressable = (kind, ref) => {
108
+ const verb = REF_VERB[kind];
109
+ if (!table.has(ref)) {
110
+ const noun = kind === "removes" ? "remove" : kind === "replaces" ? "replace" : kind === "patches" ? "patch" : "fieldPatch";
111
+ return refuse(`${file} ${verb} "${ref}", but no such record exists in ${file} (identity is the record's name)${RENAMED_HINT}`, `${file}: ${noun} target ${ref} does not exist`);
112
+ }
113
+ if (!mayModify(pack.manifest, ownerOf(ref))) {
114
+ const act = kind === "removes" ? "remove" : "modify";
115
+ return refuse(`${file} ${verb} "${ref}", but ${pid} does not declare ${ownerOf(ref)} as a dependency`, `${file}: cannot ${act} ${ref} without declaring ${ownerOf(ref)} as a dependency`);
116
+ }
117
+ return true;
118
+ };
65
119
  for (const rec of contrib.records ?? []) {
66
120
  const name = rec["name"];
67
121
  if (typeof name !== "string" || name.length === 0) {
68
- throw new ComposeError(`${pid}/${file}: record without a name`);
122
+ refuse(`${file} contributes a record with no "name", so nothing can address it and it was left out`, `${file}: record without a name`);
123
+ continue;
69
124
  }
70
125
  const ref = packRef(pid, name);
71
126
  if (table.has(ref)) {
72
- throw new ComposeError(`${pid}/${file}: duplicate record ${ref}`);
127
+ refuse(`${file} adds two records that both resolve to "${ref}", so the second was left out`, `${file}: duplicate record ${ref}`);
128
+ continue;
73
129
  }
74
130
  table.set(ref, { ref, owner: pid, modifiedBy: [], value: rec });
75
131
  }
76
132
  for (const kind of ["patches", "replaces"]) {
77
133
  for (const [refStr, body] of Object.entries(contrib[kind] ?? {})) {
78
134
  const ref = refStr;
135
+ if (!addressable(kind, ref))
136
+ continue;
79
137
  const existing = table.get(ref);
80
- if (!existing) {
81
- const verb = kind === "patches" ? "patch" : "replace";
82
- throw new ComposeError(`${pid}/${file}: ${verb} target ${ref} does not exist`);
83
- }
84
- if (!mayModify(pack.manifest, ownerOf(ref))) {
85
- throw new ComposeError(`${pid}/${file}: cannot modify ${ref} without declaring ${ownerOf(ref)} as a dependency`);
86
- }
87
138
  existing.value =
88
139
  kind === "patches" ? mergePatch(existing.value, body) : body;
89
140
  existing.modifiedBy.push(pid);
@@ -91,24 +142,16 @@ export function composePacks(packs) {
91
142
  }
92
143
  for (const [refStr, ops] of Object.entries(contrib.fieldPatches ?? {})) {
93
144
  const ref = refStr;
145
+ if (!addressable("fieldPatches", ref))
146
+ continue;
94
147
  const existing = table.get(ref);
95
- if (!existing) {
96
- throw new ComposeError(`${pid}/${file}: fieldPatch target ${ref} does not exist`);
97
- }
98
- if (!mayModify(pack.manifest, ownerOf(ref))) {
99
- throw new ComposeError(`${pid}/${file}: cannot modify ${ref} without declaring ${ownerOf(ref)} as a dependency`);
100
- }
101
148
  existing.value = applyFieldPatch(existing.value, ops);
102
149
  existing.modifiedBy.push(pid);
103
150
  }
104
151
  for (const refStr of contrib.removes ?? []) {
105
152
  const ref = refStr;
106
- if (!table.has(ref)) {
107
- throw new ComposeError(`${pid}/${file}: remove target ${ref} does not exist`);
108
- }
109
- if (!mayModify(pack.manifest, ownerOf(ref))) {
110
- throw new ComposeError(`${pid}/${file}: cannot remove ${ref} without declaring ${ownerOf(ref)} as a dependency`);
111
- }
153
+ if (!addressable("removes", ref))
154
+ continue;
112
155
  table.delete(ref);
113
156
  }
114
157
  }
@@ -1 +1 @@
1
- {"version":3,"file":"compose.js","sourceRoot":"","sources":["../src/compose.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"compose.js","sourceRoot":"","sources":["../src/compose.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,126 @@
1
+ /**
2
+ * One conflict report over every composition layer.
3
+ *
4
+ * WHAT WAS WRONG. `computeConflictReport` (conflicts.ts) walks `pack.files`, so
5
+ * it sees CONTENT RECORDS and nothing else. Four other layers collided in
6
+ * complete silence:
7
+ *
8
+ * - GRAPHICS: two mods claiming one grafID. Nothing said so, and until
9
+ * 2026-08-01 the loser was the LATER mod, backwards from every other layer.
10
+ * - BEHAVIOUR: two mods contributing the same ModHooks member. For a
11
+ * last-answer hook the earlier mod's rule simply never runs, and until
12
+ * 2026-08-02 it was the LATER mod's that did not, backwards from the row the
13
+ * player reads.
14
+ * - RULES: two mods declaring the same flag string. `resolveModRules` is a flat
15
+ * last-wins namespace, so one mod silently reads the other's toggle.
16
+ * - CONTROLLER: two mods shipping an autoplayer. There is one slot; the second
17
+ * install wins and the first is not told.
18
+ *
19
+ * A report that sees one layer of five cannot be the substrate for author-
20
+ * declared conflict resolution, which is what this whole model needs it to be.
21
+ *
22
+ * WHY THE FOLD IS PART OF THE ANSWER. The layers do not resolve the same way and
23
+ * pretending they do would be the RimWorld trap - XML, then xpath, then C#, each
24
+ * with its own effective precedence, so "load order" quietly means three
25
+ * different things. Here the report NAMES the fold, because "these two combine"
26
+ * and "one of these is being ignored" are different news for the player and only
27
+ * one of them needs acting on.
28
+ *
29
+ * DERIVED, NOT DECLARED. Every claim is observed from what a mod actually
30
+ * contributes - the refs in its files, the hooks its factory returned, the
31
+ * grafIDs its manifest claims, the controller it handed over. A `touches` field
32
+ * in the manifest would have been easier and would drift the first time an
33
+ * author forgot to update it, which is the failure this report exists to catch.
34
+ */
35
+ /** Which composition layer a contested slot belongs to. */
36
+ export type ContestedLayer = "record" | "graphics" | "behaviour" | "rule" | "controller";
37
+ /**
38
+ * How a layer resolves several claims on one slot.
39
+ *
40
+ * EVERY ONE OF THESE IS "THE LATER MOD WINS", which is the point: the mod
41
+ * manager promises the player one lever ("Move later (loads last, wins
42
+ * conflicts)"), and a lever that means five different things is not a lever. The
43
+ * folds differ only in whether there is anything for a winner to win.
44
+ *
45
+ * The three that DISCARD a claim - `last-wins`, `last-answer`, `single-slot` -
46
+ * are the ones worth a player's attention; the rest combine, and are reported so
47
+ * the picture is complete rather than because anything is wrong.
48
+ */
49
+ export type Fold = "last-wins" | "last-answer" | "single-slot" | "all-must-agree" | "chained" | "any-yes";
50
+ /** Whether a fold silently drops somebody's contribution. */
51
+ export declare function foldDiscards(fold: Fold): boolean;
52
+ /** One mod's claim on one slot. */
53
+ export interface Claim {
54
+ /** The pack that contributed. */
55
+ packId: string;
56
+ /** The section it came from, when the pack attributed it to one. */
57
+ sectionId?: string;
58
+ /**
59
+ * Set when a section's band moved this claim away from its pack's own
60
+ * position, so the report can explain an order the load list does not show.
61
+ */
62
+ band?: string;
63
+ }
64
+ /** One thing more than one mod contributed to. */
65
+ export interface ContestedSlot {
66
+ layer: ContestedLayer;
67
+ /** Stable identity, for deduping and tests: "core:kobold.speed", "graphics:2". */
68
+ key: string;
69
+ /** What a player would call it: "kobold's speed", "the Graphics row for mode 2". */
70
+ what: string;
71
+ /** How this layer resolves the claims, and therefore whether one is discarded. */
72
+ fold: Fold;
73
+ /** Every claim, in load order. */
74
+ claims: Claim[];
75
+ /** The pack whose claim takes effect, when the fold picks one. */
76
+ winner?: string;
77
+ }
78
+ /** A pack's display name, for lines a player reads; falls back to the id. */
79
+ export type NameOf = (packId: string) => string;
80
+ /**
81
+ * One plain-language line per contested slot.
82
+ *
83
+ * Written so the sentence tells the player whether to DO anything: a discarding
84
+ * fold names the loser and the lever that changes it, a combining fold says the
85
+ * contributions stack. A line that just says "conflict" is what makes players
86
+ * stop reading conflict lists.
87
+ */
88
+ export declare function describeContested(slot: ContestedSlot, nameOf?: NameOf): string;
89
+ /**
90
+ * Build the contested slots for one layer from a flat list of claims.
91
+ *
92
+ * `claims` is every claim on every key of that layer, IN LOAD ORDER. Keys with a
93
+ * single claimant are dropped - one mod changing something is not a conflict,
94
+ * and listing it would bury the ones that are.
95
+ *
96
+ * Two claims from the SAME pack on one key do not contest each other: a pack
97
+ * whose base contributions and one of its own sections both touch a field has
98
+ * made one decision in two places, and the pack's own order settles it.
99
+ */
100
+ export declare function contestedSlots(layer: ContestedLayer, fold: Fold, claims: readonly {
101
+ key: string;
102
+ what: string;
103
+ claim: Claim;
104
+ }[]): ContestedSlot[];
105
+ /**
106
+ * An author's `conflicts` claim that applies to the current mod set.
107
+ *
108
+ * Carried separately from the contested slots because it is a DECLARATION rather
109
+ * than an observation: nobody measured a collision, an author stated one. It is
110
+ * shown with their reason attached and it never blocks - ratified decision 18,
111
+ * the engine labels and does not forbid, and a third-party author does not get a
112
+ * veto over the player's setup.
113
+ */
114
+ export interface DeclaredConflict {
115
+ /** The pack making the claim. */
116
+ packId: string;
117
+ /** The pack it names. */
118
+ with: string;
119
+ /** The claimant's own sections the claim is about, if it scoped itself. */
120
+ scope?: string[];
121
+ /** The author's stated reason. */
122
+ because: string;
123
+ }
124
+ /** The warning line for a declared conflict. */
125
+ export declare function describeDeclaredConflict(conflict: DeclaredConflict, nameOf?: NameOf): string;
126
+ //# sourceMappingURL=contested.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"contested.d.ts","sourceRoot":"","sources":["../src/contested.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAEH,2DAA2D;AAC3D,MAAM,MAAM,cAAc,GACtB,QAAQ,GACR,UAAU,GACV,WAAW,GACX,MAAM,GACN,YAAY,CAAC;AAEjB;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,IAAI,GACZ,WAAW,GACX,aAAa,GACb,aAAa,GACb,gBAAgB,GAChB,SAAS,GACT,SAAS,CAAC;AAEd,6DAA6D;AAC7D,wBAAgB,YAAY,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAEhD;AAED,mCAAmC;AACnC,MAAM,WAAW,KAAK;IACpB,iCAAiC;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,oEAAoE;IACpE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,kDAAkD;AAClD,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,cAAc,CAAC;IACtB,kFAAkF;IAClF,GAAG,EAAE,MAAM,CAAC;IACZ,oFAAoF;IACpF,IAAI,EAAE,MAAM,CAAC;IACb,kFAAkF;IAClF,IAAI,EAAE,IAAI,CAAC;IACX,kCAAkC;IAClC,MAAM,EAAE,KAAK,EAAE,CAAC;IAChB,kEAAkE;IAClE,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,6EAA6E;AAC7E,MAAM,MAAM,MAAM,GAAG,CAAC,MAAM,EAAE,MAAM,KAAK,MAAM,CAAC;AAehD;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,aAAa,EAAE,MAAM,GAAE,MAAmB,GAAG,MAAM,CA0B1F;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,cAAc,CAC5B,KAAK,EAAE,cAAc,EACrB,IAAI,EAAE,IAAI,EACV,MAAM,EAAE,SAAS;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,KAAK,CAAA;CAAE,EAAE,GAC7D,aAAa,EAAE,CA6BjB;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,gBAAgB;IAC/B,iCAAiC;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,yBAAyB;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,2EAA2E;IAC3E,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,kCAAkC;IAClC,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,gDAAgD;AAChD,wBAAgB,wBAAwB,CACtC,QAAQ,EAAE,gBAAgB,EAC1B,MAAM,GAAE,MAAmB,GAC1B,MAAM,CAGR"}
@@ -0,0 +1,131 @@
1
+ /**
2
+ * One conflict report over every composition layer.
3
+ *
4
+ * WHAT WAS WRONG. `computeConflictReport` (conflicts.ts) walks `pack.files`, so
5
+ * it sees CONTENT RECORDS and nothing else. Four other layers collided in
6
+ * complete silence:
7
+ *
8
+ * - GRAPHICS: two mods claiming one grafID. Nothing said so, and until
9
+ * 2026-08-01 the loser was the LATER mod, backwards from every other layer.
10
+ * - BEHAVIOUR: two mods contributing the same ModHooks member. For a
11
+ * last-answer hook the earlier mod's rule simply never runs, and until
12
+ * 2026-08-02 it was the LATER mod's that did not, backwards from the row the
13
+ * player reads.
14
+ * - RULES: two mods declaring the same flag string. `resolveModRules` is a flat
15
+ * last-wins namespace, so one mod silently reads the other's toggle.
16
+ * - CONTROLLER: two mods shipping an autoplayer. There is one slot; the second
17
+ * install wins and the first is not told.
18
+ *
19
+ * A report that sees one layer of five cannot be the substrate for author-
20
+ * declared conflict resolution, which is what this whole model needs it to be.
21
+ *
22
+ * WHY THE FOLD IS PART OF THE ANSWER. The layers do not resolve the same way and
23
+ * pretending they do would be the RimWorld trap - XML, then xpath, then C#, each
24
+ * with its own effective precedence, so "load order" quietly means three
25
+ * different things. Here the report NAMES the fold, because "these two combine"
26
+ * and "one of these is being ignored" are different news for the player and only
27
+ * one of them needs acting on.
28
+ *
29
+ * DERIVED, NOT DECLARED. Every claim is observed from what a mod actually
30
+ * contributes - the refs in its files, the hooks its factory returned, the
31
+ * grafIDs its manifest claims, the controller it handed over. A `touches` field
32
+ * in the manifest would have been easier and would drift the first time an
33
+ * author forgot to update it, which is the failure this report exists to catch.
34
+ */
35
+ /** Whether a fold silently drops somebody's contribution. */
36
+ export function foldDiscards(fold) {
37
+ return fold === "last-wins" || fold === "last-answer" || fold === "single-slot";
38
+ }
39
+ /** "frost", "frost and runes", "frost, runes and mist". */
40
+ function joinNames(names) {
41
+ if (names.length <= 1)
42
+ return names.join("");
43
+ if (names.length === 2)
44
+ return `${names[0]} and ${names[1]}`;
45
+ return `${names.slice(0, -1).join(", ")} and ${names[names.length - 1]}`;
46
+ }
47
+ /** "frost" or "frost (kobold rebalance)" when the claim named a section. */
48
+ function claimName(claim, nameOf) {
49
+ const base = nameOf(claim.packId);
50
+ return claim.sectionId ? `${base} (${claim.sectionId})` : base;
51
+ }
52
+ /**
53
+ * One plain-language line per contested slot.
54
+ *
55
+ * Written so the sentence tells the player whether to DO anything: a discarding
56
+ * fold names the loser and the lever that changes it, a combining fold says the
57
+ * contributions stack. A line that just says "conflict" is what makes players
58
+ * stop reading conflict lists.
59
+ */
60
+ export function describeContested(slot, nameOf = (id) => id) {
61
+ const names = slot.claims.map((c) => claimName(c, nameOf));
62
+ const who = joinNames(names);
63
+ const winner = slot.winner ? nameOf(slot.winner) : undefined;
64
+ /* A band that repositioned the winning claim is the one thing the load-order
65
+ * list cannot show, so it is worth a clause of its own. */
66
+ const winning = slot.claims.find((c) => c.packId === slot.winner);
67
+ const banded = winning?.band && winning.band !== "normal"
68
+ ? ` (its "${winning.sectionId}" part is set to load ${winning.band})`
69
+ : "";
70
+ switch (slot.fold) {
71
+ case "last-wins":
72
+ return `${who} all change ${slot.what}; ${winner} wins${banded} because it loads last.`;
73
+ case "last-answer":
74
+ return `${who} all handle ${slot.what}; only ${winner} runs${banded} because it loads last - the rest never get asked.`;
75
+ case "single-slot":
76
+ return `${who} each provide ${slot.what}, and there is room for one; ${winner} takes it${banded} and the others do nothing.`;
77
+ case "all-must-agree":
78
+ return `${who} all have a say in ${slot.what}; every one of them has to agree, so any single refusal decides.`;
79
+ case "chained":
80
+ return `${who} all change ${slot.what}, in load order, each one seeing the last one's result.`;
81
+ case "any-yes":
82
+ return `${who} all ask for ${slot.what}; one asking is enough, so they do not conflict.`;
83
+ }
84
+ }
85
+ /**
86
+ * Build the contested slots for one layer from a flat list of claims.
87
+ *
88
+ * `claims` is every claim on every key of that layer, IN LOAD ORDER. Keys with a
89
+ * single claimant are dropped - one mod changing something is not a conflict,
90
+ * and listing it would bury the ones that are.
91
+ *
92
+ * Two claims from the SAME pack on one key do not contest each other: a pack
93
+ * whose base contributions and one of its own sections both touch a field has
94
+ * made one decision in two places, and the pack's own order settles it.
95
+ */
96
+ export function contestedSlots(layer, fold, claims) {
97
+ const byKey = new Map();
98
+ for (const entry of claims) {
99
+ const slot = byKey.get(entry.key) ?? { what: entry.what, claims: [] };
100
+ slot.claims.push(entry.claim);
101
+ byKey.set(entry.key, slot);
102
+ }
103
+ const out = [];
104
+ for (const [key, slot] of byKey) {
105
+ const packs = new Set(slot.claims.map((c) => c.packId));
106
+ if (packs.size < 2)
107
+ continue;
108
+ /* Every discarding fold now picks the SAME claim - the last one - which is
109
+ * the property this whole model is for. Kept as a `foldDiscards` test rather
110
+ * than collapsed into "last unless undefined", so adding a fold that picks
111
+ * differently has to come here and say so. */
112
+ const winner = foldDiscards(fold)
113
+ ? slot.claims[slot.claims.length - 1]?.packId
114
+ : undefined;
115
+ out.push({
116
+ layer,
117
+ key,
118
+ what: slot.what,
119
+ fold,
120
+ claims: slot.claims,
121
+ ...(winner === undefined ? {} : { winner }),
122
+ });
123
+ }
124
+ return out;
125
+ }
126
+ /** The warning line for a declared conflict. */
127
+ export function describeDeclaredConflict(conflict, nameOf = (id) => id) {
128
+ const where = conflict.scope?.length ? ` over ${conflict.scope.join(", ")}` : "";
129
+ return `${nameOf(conflict.packId)} says it conflicts with ${nameOf(conflict.with)}${where}: ${conflict.because}`;
130
+ }
131
+ //# sourceMappingURL=contested.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"contested.js","sourceRoot":"","sources":["../src/contested.ts"],"names":[],"mappings":"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"}
package/dist/engine.d.ts CHANGED
@@ -27,6 +27,21 @@
27
27
  * A pack can fail either, both, or neither, and the reasons a player must read are
28
28
  * different in each case - so this returns a discriminated verdict rather than a
29
29
  * boolean, and the caller keeps them apart.
30
+ *
31
+ * AND A FAILED RANGE IS NOT ALWAYS A REFUSAL (2026-08-02). Ratified decision 18
32
+ * is that the engine LABELS and does not FORBID, and this gate was applying it
33
+ * backwards: an out-of-range `engine` refused to load a pack of JSON that would
34
+ * have composed perfectly well. Nothing in a data pack's manifest can make its
35
+ * data unloadable - the worst case is a patch aimed at a record that has since
36
+ * moved, and composePacks now reports that one op and keeps the rest.
37
+ *
38
+ * So the range is a GATE on a pack that ships code and a LABEL on one that does
39
+ * not, and `modApi` is the signal, because the manifest already requires it of
40
+ * exactly the packs that ship a plugin.js. Code is the thing that genuinely
41
+ * breaks across an engine release: it calls functions, and a function that has
42
+ * been renamed is a crash rather than a warning. This is the difference between
43
+ * an engine patch that costs every content author a release and one that costs
44
+ * them nothing, which is the whole reason the distinction is here.
30
45
  */
31
46
  import type { PackManifest } from "./manifest.js";
32
47
  /**
@@ -43,6 +58,18 @@ export type EngineVerdict = {
43
58
  } | {
44
59
  readonly ok: false;
45
60
  readonly kind: "out-of-range" | "bad-manifest";
61
+ /**
62
+ * Whether this is a REFUSAL or a warning to show beside a pack that loads
63
+ * anyway. True exactly when the pack ships code (`modApi` is declared).
64
+ *
65
+ * Separate from `ok` rather than folded into it, because a caller that
66
+ * only wants to know "did the versions line up" and a caller deciding "do I
67
+ * load this" are asking different questions, and collapsing them is how the
68
+ * warning would end up either suppressed or treated as fatal. `ok: false,
69
+ * blocks: false` is a real and common state: the pack loads, and the player
70
+ * is told why it might misbehave.
71
+ */
72
+ readonly blocks: boolean;
46
73
  readonly why: string;
47
74
  };
48
75
  /**
@@ -58,5 +85,14 @@ export type EngineVerdict = {
58
85
  * Pure, and takes the version as an argument rather than importing it, so a test
59
86
  * can drive any build and so mod-sdk stays independent of core.
60
87
  */
61
- export declare function engineVerdict(manifest: Pick<PackManifest, "engine">, engineVersion: string): EngineVerdict;
88
+ export declare function engineVerdict(manifest: Pick<PackManifest, "engine" | "modApi">, engineVersion: string,
89
+ /**
90
+ * Whether this pack ships CODE, and therefore whether a mismatch is a gate or
91
+ * a label. Defaults to what the manifest says, and is overridable for the one
92
+ * caller that knows better: the plugin loader is holding a plugin.js it can
93
+ * see, and it runs this gate BEFORE it checks whether `modApi` was declared at
94
+ * all - so a code pack that forgot the field must not buy the lenient path
95
+ * with the omission.
96
+ */
97
+ hasCode?: boolean): EngineVerdict;
62
98
  //# sourceMappingURL=engine.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAGlD;;;;;;;;GAQG;AACH,MAAM,MAAM,aAAa,GACrB;IAAE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAA;CAAE,GACrB;IACE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IACnB,QAAQ,CAAC,IAAI,EAAE,cAAc,GAAG,cAAc,CAAC;IAC/C,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;CACtB,CAAC;AAIN;;;;;;;;;;;;GAYG;AACH,wBAAgB,aAAa,CAC3B,QAAQ,EAAE,IAAI,CAAC,YAAY,EAAE,QAAQ,CAAC,EACtC,aAAa,EAAE,MAAM,GACpB,aAAa,CAuCf"}
1
+ {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAGlD;;;;;;;;GAQG;AACH,MAAM,MAAM,aAAa,GACrB;IAAE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAA;CAAE,GACrB;IACE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IACnB,QAAQ,CAAC,IAAI,EAAE,cAAc,GAAG,cAAc,CAAC;IAC/C;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;CACtB,CAAC;AAIN;;;;;;;;;;;;GAYG;AACH,wBAAgB,aAAa,CAC3B,QAAQ,EAAE,IAAI,CAAC,YAAY,EAAE,QAAQ,GAAG,QAAQ,CAAC,EACjD,aAAa,EAAE,MAAM;AACrB;;;;;;;GAOG;AACH,OAAO,GAAE,OAAuC,GAC/C,aAAa,CA0Df"}