@wildwinter/toolkit 0.1.0 → 0.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.
@@ -0,0 +1,324 @@
1
+ /** The manifest schema tag this build reads and writes. */
2
+ declare const MANIFEST_SCHEMA = "wildwinter/extension@0";
3
+ /** What a manifest file is called: `<id>.extension.json`. */
4
+ declare const MANIFEST_FILE_SUFFIX = ".extension.json";
5
+ /** The hosts an extension can target. */
6
+ type HostName = "patter" | "storylets";
7
+ interface HostSpec {
8
+ /** The node kinds a target may name. */
9
+ readonly kinds: readonly string[];
10
+ /** The node kinds a `reference` may point at. */
11
+ readonly references: readonly string[];
12
+ /** The node kinds with host fields of their own, which a claim may present. */
13
+ readonly claimable: readonly string[];
14
+ }
15
+ /** Each host's node kinds, references and claimable kinds. */
16
+ declare const HOSTS: Readonly<Record<HostName, HostSpec>>;
17
+ /** The family's seven scalar types, spelled as the family spells them. */
18
+ declare const SCALAR_TYPES: readonly ["text", "multiline", "number", "boolean", "enum", "flags", "quality"];
19
+ type ScalarType = typeof SCALAR_TYPES[number];
20
+ /** Every type a value can take. */
21
+ declare const VALUE_TYPES: readonly ["text", "multiline", "number", "boolean", "enum", "flags", "quality", "reference", "record", "map", "list"];
22
+ type ValueType = typeof VALUE_TYPES[number];
23
+ /** A JSON value, as extension data holds it. */
24
+ type JsonValue = null | boolean | number | string | JsonValue[] | {
25
+ [key: string]: JsonValue;
26
+ };
27
+ /**
28
+ * A value's type, with the constraints that belong to it. A target's fields
29
+ * and a record's fields add a name and the editor's words (`FieldDecl`); the
30
+ * `of` of a map or a list is a bare type.
31
+ */
32
+ interface TypeDecl {
33
+ readonly type: string;
34
+ /** An enum's values, or the flags a flags value may hold. */
35
+ readonly values?: readonly string[];
36
+ /** A quality's ladder. */
37
+ readonly stages?: readonly string[];
38
+ /** A quality's renames: every name a stage has had, to the stage it is now. */
39
+ readonly renamedStages?: Readonly<Record<string, string>>;
40
+ /** A reference's node kind. */
41
+ readonly to?: string;
42
+ /** A record's fields. */
43
+ readonly fields?: readonly FieldDecl[];
44
+ /** A map's key. */
45
+ readonly key?: {
46
+ readonly type: string;
47
+ readonly to?: string;
48
+ };
49
+ /** A map's or a list's value type. */
50
+ readonly of?: TypeDecl;
51
+ /** A list's id field: the text field of its records that keys each item. */
52
+ readonly id?: string;
53
+ /** A number's bounds. */
54
+ readonly min?: number;
55
+ readonly max?: number;
56
+ /** One sentence per enum or flags value, or quality stage. Editor only. */
57
+ readonly valueHelp?: Readonly<Record<string, string>>;
58
+ /** Optional in source: the type's own default when absent (`defaultOf`). */
59
+ readonly default?: unknown;
60
+ }
61
+ /** A named field: a target's, or a record's. */
62
+ interface FieldDecl extends TypeDecl {
63
+ readonly name: string;
64
+ readonly label?: string;
65
+ readonly help?: string;
66
+ /** A node of this kind must set it. */
67
+ readonly required?: boolean;
68
+ /** A heading within the extension's group in the editor. */
69
+ readonly group?: string;
70
+ /** Stripped at compile: editor data, never in a bundle. A target's own fields only. */
71
+ readonly editorOnly?: boolean;
72
+ }
73
+ /** A claim on a host field, named through one of the manifest's settings. */
74
+ interface ClaimDecl extends TypeDecl {
75
+ readonly setting: string;
76
+ readonly label?: string;
77
+ readonly help?: string;
78
+ readonly required?: boolean;
79
+ }
80
+ /** What an extension declares for one node kind of one host. */
81
+ interface TargetDecl {
82
+ readonly fields?: readonly FieldDecl[];
83
+ readonly claims?: readonly ClaimDecl[];
84
+ }
85
+ /** A project setting, naming a host field a claim presents. */
86
+ interface SettingDecl {
87
+ readonly name: string;
88
+ readonly label?: string;
89
+ readonly help?: string;
90
+ /** The field name used when the project names none. */
91
+ readonly default: string;
92
+ }
93
+ interface Manifest {
94
+ readonly schema: string;
95
+ readonly id: string;
96
+ readonly name: string;
97
+ readonly version: string;
98
+ readonly docs?: string;
99
+ readonly settings?: readonly SettingDecl[];
100
+ readonly targets: Readonly<Partial<Record<HostName, Readonly<Record<string, TargetDecl>>>>>;
101
+ }
102
+ /** One thing wrong, with where it is. A host words it in its own report shape. */
103
+ interface Problem {
104
+ readonly severity: "error" | "warning";
105
+ /** A stable name for the kind of problem, for tests and quick fixes. */
106
+ readonly code: string;
107
+ /** Where, relative to what was checked: `targets.storylets.card.fields[2].stages`. */
108
+ readonly path: string;
109
+ readonly message: string;
110
+ /** The values a quick fix may offer (an enum's values, the stages either side). */
111
+ readonly options?: readonly string[];
112
+ }
113
+ /** The format-neutral part of an extension data shard. */
114
+ interface ExtensionData {
115
+ readonly extension: string;
116
+ /** By node kind, then node id, then field name. */
117
+ readonly entries: Readonly<Record<string, Readonly<Record<string, Readonly<Record<string, unknown>>>>>>;
118
+ readonly unresolved?: readonly UnresolvedEntry[];
119
+ }
120
+ /** How a three-way merge failed to agree on one value. */
121
+ type ConflictKind = "both-changed" | "delete-vs-edit" | "added-both" | "structural";
122
+ /** One clash, as the reserved `unresolved` key of a merged shard holds it. */
123
+ interface UnresolvedEntry {
124
+ readonly path: string;
125
+ readonly kind: ConflictKind;
126
+ readonly base?: unknown;
127
+ readonly ours?: unknown;
128
+ readonly theirs?: unknown;
129
+ }
130
+
131
+ /** Read a manifest file's text (JSON, or JSON5). Throws on text that will not parse. */
132
+ declare function parseManifest(text: string): Manifest;
133
+ /** Every problem with a manifest, by path. An empty list means it is sound. */
134
+ declare function manifestProblems(manifest: unknown): Problem[];
135
+ /** The host field each of a manifest's settings names, the project's choice
136
+ * first, else the setting's default. */
137
+ declare function settingValues(manifest: Manifest, chosen?: Readonly<Record<string, unknown>>): Record<string, string>;
138
+ /** A claim as a project resolves it: the host field it presents, by name. */
139
+ interface ResolvedClaim {
140
+ readonly extension: string;
141
+ readonly host: HostName;
142
+ readonly kind: string;
143
+ /** The host field's name, through the setting. */
144
+ readonly field: string;
145
+ readonly claim: ClaimDecl;
146
+ }
147
+ /** Every claim of a manifest for one host, resolved through the project's settings. */
148
+ declare function resolvedClaims(manifest: Manifest, host: HostName, chosen?: Readonly<Record<string, unknown>>): ResolvedClaim[];
149
+ /** Two extensions claiming one host field: an error naming both, once per pair. */
150
+ declare function claimClashes(claims: readonly ResolvedClaim[]): Problem[];
151
+ /** One manifest's declarations for one host and node kind, or undefined. */
152
+ declare function targetOf(manifest: Manifest | undefined, host: HostName, kind: string): TargetDecl | undefined;
153
+ /**
154
+ * A claim as a host draws and checks its field: the claim's label, help, value help and constraints, over the
155
+ * host's own declaration for what the claim leaves out (an enum's or flags' values, a quality's stages, the
156
+ * default). The host's vocabulary stands when the claim names none, as the host's ladder always does.
157
+ */
158
+ declare function effectiveClaim<C extends ClaimDecl>(claim: C, hostDecl: {
159
+ readonly type?: string;
160
+ readonly values?: readonly string[];
161
+ readonly stages?: readonly string[];
162
+ readonly default?: unknown;
163
+ }): C;
164
+
165
+ /** What the host knows about its nodes, for references. */
166
+ interface ValueContext {
167
+ /** Does a node of `kind` with `id` exist? Absent, references are not checked
168
+ * (a host checking a manifest's default, say, has no nodes to hand). */
169
+ readonly exists?: (kind: string, id: string) => boolean;
170
+ }
171
+ /** Every problem with `value` as a value of `decl`, paths relative to the value. */
172
+ declare function valueProblems(decl: TypeDecl, value: unknown, ctx?: ValueContext, path?: string): Problem[];
173
+ /** Every problem with one node's entry, against its target. */
174
+ declare function entryProblems(target: TargetDecl, entry: unknown, ctx?: ValueContext, path?: string): Problem[];
175
+ /**
176
+ * A declaration's default: the one it gives, else its type's. A scalar's is
177
+ * the kernel's (`defaultFor`: false, 0, "", the first enum value, no flags,
178
+ * the first stage), a reference's is none (""), a record's is each of its
179
+ * fields' defaults, a map's and a list's are empty.
180
+ */
181
+ declare function defaultOf(decl: TypeDecl): unknown;
182
+ /** A target's fields with every default resolved and the editor's words gone, as a bundle carries them;
183
+ * editor-only fields are left out. */
184
+ declare function compiledTarget(target: TargetDecl): FieldDecl[];
185
+ /**
186
+ * A node's values as a bundle carries them: raw and sparse, with editor-only
187
+ * and undeclared fields left out and renamed stages renamed. Undefined when
188
+ * nothing is left, so a host writes no `extensions` entry for the node.
189
+ */
190
+ declare function compileEntry(target: TargetDecl, entry: Readonly<Record<string, unknown>>): Record<string, unknown> | undefined;
191
+ interface StageRename {
192
+ readonly path: string;
193
+ readonly from: string;
194
+ readonly to: string;
195
+ }
196
+ /** Rename every stage the manifest says was renamed, at any depth. Returns the
197
+ * entry (a new object when anything changed) and the renames made. */
198
+ declare function renameStages(target: TargetDecl, entry: Readonly<Record<string, unknown>>): {
199
+ entry: Record<string, unknown>;
200
+ renames: StageRename[];
201
+ };
202
+
203
+ /**
204
+ * A merge strategy, as data:
205
+ * atomic one value, classic three-way (objects compare by content)
206
+ * map(of) an open string-keyed map, each value merged by `of`
207
+ * set a list of strings as a set: additions union, deletions respected
208
+ * keyed(key, of) a list of objects keyed by the string field `key`; output
209
+ * sorted by key, or in the authored order with `order: "preserve"`
210
+ * object(fields) named keys by their own strategy, every other key by `other`
211
+ * (atomic when absent)
212
+ */
213
+ type Strategy = {
214
+ readonly kind: "atomic";
215
+ } | {
216
+ readonly kind: "map";
217
+ readonly of: Strategy;
218
+ } | {
219
+ readonly kind: "set";
220
+ } | {
221
+ readonly kind: "keyed";
222
+ readonly key: string;
223
+ readonly of: Strategy;
224
+ readonly order?: "sorted" | "preserve";
225
+ readonly renameWarning?: string;
226
+ } | {
227
+ readonly kind: "object";
228
+ readonly fields: Readonly<Record<string, Strategy>>;
229
+ readonly other?: Strategy;
230
+ };
231
+ declare const ATOMIC: Strategy;
232
+ declare const SET: Strategy;
233
+ declare const mapOf: (of: Strategy) => Strategy;
234
+ declare const keyedBy: (key: string, of: Strategy, opts?: {
235
+ order?: "sorted" | "preserve";
236
+ renameWarning?: string;
237
+ }) => Strategy;
238
+ declare const objectOf: (fields: Readonly<Record<string, Strategy>>, other?: Strategy) => Strategy;
239
+ /** One clash; the merged value holds OURS for it. */
240
+ interface Conflict {
241
+ /** The entity key involved (a keyed item's key, a map's key), or "" at the top. */
242
+ readonly id: string;
243
+ /** Where, from the root: `entries.card.c_1.perf.v_ann.look`, `cards[c_1].priority`. */
244
+ readonly path: string;
245
+ readonly base: unknown;
246
+ readonly ours: unknown;
247
+ readonly theirs: unknown;
248
+ readonly kind: ConflictKind;
249
+ }
250
+ /** A note on a merge that went through but that a person should check. */
251
+ interface MergeWarning {
252
+ readonly id: string;
253
+ readonly path: string;
254
+ readonly message: string;
255
+ }
256
+ interface TreeMergeResult {
257
+ readonly merged: unknown;
258
+ readonly conflicts: Conflict[];
259
+ readonly warnings: MergeWarning[];
260
+ }
261
+ interface TreeMergeOptions {
262
+ /** Whether two values say the same thing. Content equality with keys in any
263
+ * order by default; a format whose canonical form is looser passes its own. */
264
+ readonly equal?: (a: unknown, b: unknown) => boolean;
265
+ }
266
+ /** JSON with every object's keys sorted: two values with this text say the same thing. */
267
+ declare function stableStringify(value: unknown): string;
268
+ /** Three-way merge `base`, `ours` and `theirs` by `strategy`. A clash keeps ours. */
269
+ declare function mergeTree(strategy: Strategy, base: unknown, ours: unknown, theirs: unknown, opts?: TreeMergeOptions): TreeMergeResult;
270
+ /** How a value of this type merges (the README's table). */
271
+ declare function strategyForType(decl: TypeDecl): Strategy;
272
+ /** How one node's entry merges: field by field, each by its type. */
273
+ declare function strategyForTarget(target: TargetDecl): Strategy;
274
+ /**
275
+ * How an extension data shard merges for one host. With the manifest, each
276
+ * kind's entries merge node by node and each entry field by field; a kind the
277
+ * manifest does not target, and every kind without the manifest, merges node
278
+ * by node with each entry as one value.
279
+ */
280
+ declare function extensionDataStrategy(manifest: Manifest | undefined, host: HostName): Strategy;
281
+ /** The reserved key a merged shard carries its clashes under. */
282
+ declare const UNRESOLVED_KEY = "unresolved";
283
+ /** A shard's mark, when it carries one. */
284
+ declare function unresolvedOf(shard: unknown): readonly UnresolvedEntry[] | undefined;
285
+ /** A shard without its mark, for merging and for comparing content. */
286
+ declare function withoutMark<T>(shard: T): T;
287
+ /**
288
+ * Write a merge's clashes into the merged shard under `unresolved`, carrying
289
+ * any mark either side already had (a mark is a set by path: one side
290
+ * resolving it, by removing it, resolves it). A clash found now replaces a
291
+ * carried one at the same path. With nothing to mark, the key is absent.
292
+ */
293
+ declare function markUnresolved<T extends Record<string, unknown>>(merged: T, conflicts: readonly Conflict[], sides?: {
294
+ base?: unknown;
295
+ ours?: unknown;
296
+ theirs?: unknown;
297
+ }): T;
298
+ /** Validation's refusal of a marked shard: one error per clash, or one for an empty mark. */
299
+ declare function unresolvedProblems(shard: unknown): Problem[];
300
+ interface DataMergeResult {
301
+ /** The merged shard: ours where it clashed, with the mark written in. */
302
+ readonly merged: Record<string, unknown>;
303
+ readonly conflicts: Conflict[];
304
+ readonly warnings: MergeWarning[];
305
+ }
306
+ /**
307
+ * Three-way merge of one extension data shard. `manifest` is the project's copy,
308
+ * read from the working tree, or undefined when it is missing, unreadable or
309
+ * itself in conflict, which merges each node's entry as one value: always safe.
310
+ * The shard's other keys (a format's schema tag, the extension id) merge as values.
311
+ */
312
+ declare function mergeExtensionData(base: Record<string, unknown>, ours: Record<string, unknown>, theirs: Record<string, unknown>, opts: {
313
+ manifest?: Manifest;
314
+ host: HostName;
315
+ } & TreeMergeOptions): DataMergeResult;
316
+ /** Entries naming a node the host no longer has: kept, and reported as warnings. */
317
+ declare function orphanedEntries(data: unknown, exists: (kind: string, id: string) => boolean): Problem[];
318
+
319
+ /** `base` and then the key `key`. */
320
+ declare const atKey: (base: string, key: string) => string;
321
+ /** `base` and then list position `i`. */
322
+ declare const atIndex: (base: string, i: number) => string;
323
+
324
+ export { ATOMIC, type ClaimDecl, type Conflict, type ConflictKind, type DataMergeResult, type ExtensionData, type FieldDecl, HOSTS, type HostName, type HostSpec, type JsonValue, MANIFEST_FILE_SUFFIX, MANIFEST_SCHEMA, type Manifest, type MergeWarning, type Problem, type ResolvedClaim, SCALAR_TYPES, SET, type ScalarType, type SettingDecl, type StageRename, type Strategy, type TargetDecl, type TreeMergeOptions, type TreeMergeResult, type TypeDecl, UNRESOLVED_KEY, type UnresolvedEntry, VALUE_TYPES, type ValueContext, type ValueType, atIndex, atKey, claimClashes, compileEntry, compiledTarget, defaultOf, effectiveClaim, entryProblems, extensionDataStrategy, keyedBy, manifestProblems, mapOf, markUnresolved, mergeExtensionData, mergeTree, objectOf, orphanedEntries, parseManifest, renameStages, resolvedClaims, settingValues, stableStringify, strategyForTarget, strategyForType, targetOf, unresolvedOf, unresolvedProblems, valueProblems, withoutMark };