@abraca/schema 2.3.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.
Files changed (92) hide show
  1. package/dist/abracadabra-schema.cjs +853 -0
  2. package/dist/abracadabra-schema.cjs.map +1 -0
  3. package/dist/abracadabra-schema.esm.js +781 -0
  4. package/dist/abracadabra-schema.esm.js.map +1 -0
  5. package/dist/index.d.ts +2538 -0
  6. package/package.json +41 -0
  7. package/src/crdt.ts +63 -0
  8. package/src/generated/calendar.ts +35 -0
  9. package/src/generated/chart.ts +35 -0
  10. package/src/generated/checklist.ts +35 -0
  11. package/src/generated/dashboard.ts +35 -0
  12. package/src/generated/doc.ts +35 -0
  13. package/src/generated/gallery.ts +35 -0
  14. package/src/generated/graph.ts +35 -0
  15. package/src/generated/json-schema/calendar.json +140 -0
  16. package/src/generated/json-schema/chart.json +164 -0
  17. package/src/generated/json-schema/checklist.json +138 -0
  18. package/src/generated/json-schema/dashboard.json +122 -0
  19. package/src/generated/json-schema/doc.json +122 -0
  20. package/src/generated/json-schema/gallery.json +157 -0
  21. package/src/generated/json-schema/graph.json +125 -0
  22. package/src/generated/json-schema/kanban.json +145 -0
  23. package/src/generated/json-schema/map.json +125 -0
  24. package/src/generated/json-schema/outline.json +122 -0
  25. package/src/generated/json-schema/overview.json +122 -0
  26. package/src/generated/json-schema/plugin-manifest.json +221 -0
  27. package/src/generated/json-schema/prose.json +122 -0
  28. package/src/generated/json-schema/sheets.json +135 -0
  29. package/src/generated/json-schema/slides.json +129 -0
  30. package/src/generated/json-schema/table.json +136 -0
  31. package/src/generated/json-schema/timeline.json +122 -0
  32. package/src/generated/kanban.ts +35 -0
  33. package/src/generated/map.ts +35 -0
  34. package/src/generated/markdown/calendar.md +59 -0
  35. package/src/generated/markdown/chart.md +62 -0
  36. package/src/generated/markdown/checklist.md +58 -0
  37. package/src/generated/markdown/dashboard.md +56 -0
  38. package/src/generated/markdown/doc.md +56 -0
  39. package/src/generated/markdown/gallery.md +61 -0
  40. package/src/generated/markdown/graph.md +57 -0
  41. package/src/generated/markdown/kanban.md +62 -0
  42. package/src/generated/markdown/map.md +57 -0
  43. package/src/generated/markdown/outline.md +56 -0
  44. package/src/generated/markdown/overview.md +56 -0
  45. package/src/generated/markdown/prose.md +56 -0
  46. package/src/generated/markdown/sheets.md +59 -0
  47. package/src/generated/markdown/slides.md +57 -0
  48. package/src/generated/markdown/table.md +58 -0
  49. package/src/generated/markdown/timeline.md +56 -0
  50. package/src/generated/outline.ts +35 -0
  51. package/src/generated/overview.ts +35 -0
  52. package/src/generated/prose.ts +35 -0
  53. package/src/generated/rust/calendar.rs +125 -0
  54. package/src/generated/rust/chart.rs +151 -0
  55. package/src/generated/rust/checklist.rs +123 -0
  56. package/src/generated/rust/dashboard.rs +101 -0
  57. package/src/generated/rust/doc.rs +101 -0
  58. package/src/generated/rust/gallery.rs +146 -0
  59. package/src/generated/rust/graph.rs +104 -0
  60. package/src/generated/rust/kanban.rs +127 -0
  61. package/src/generated/rust/map.rs +104 -0
  62. package/src/generated/rust/outline.rs +101 -0
  63. package/src/generated/rust/overview.rs +101 -0
  64. package/src/generated/rust/prose.rs +101 -0
  65. package/src/generated/rust/sheets.rs +110 -0
  66. package/src/generated/rust/slides.rs +111 -0
  67. package/src/generated/rust/table.rs +121 -0
  68. package/src/generated/rust/timeline.rs +101 -0
  69. package/src/generated/sheets.ts +35 -0
  70. package/src/generated/slides.ts +35 -0
  71. package/src/generated/table.ts +35 -0
  72. package/src/generated/timeline.ts +35 -0
  73. package/src/index.ts +389 -0
  74. package/src/manifest/plugin-manifest.ts +212 -0
  75. package/src/query.ts +150 -0
  76. package/src/types/calendar.ts +23 -0
  77. package/src/types/chart.ts +36 -0
  78. package/src/types/checklist.ts +22 -0
  79. package/src/types/dashboard.ts +18 -0
  80. package/src/types/doc.ts +19 -0
  81. package/src/types/gallery.ts +24 -0
  82. package/src/types/graph.ts +21 -0
  83. package/src/types/kanban.ts +27 -0
  84. package/src/types/map.ts +21 -0
  85. package/src/types/outline.ts +17 -0
  86. package/src/types/overview.ts +17 -0
  87. package/src/types/prose.ts +19 -0
  88. package/src/types/sheets.ts +24 -0
  89. package/src/types/slides.ts +22 -0
  90. package/src/types/table.ts +22 -0
  91. package/src/types/timeline.ts +18 -0
  92. package/src/types/universal.ts +59 -0
@@ -0,0 +1,35 @@
1
+ /**
2
+ * AUTO-GENERATED by @abraca/schema (slides).
3
+ *
4
+ * Source: ../types/slides.ts
5
+ * Run `pnpm schema:gen` to regenerate. Do not edit by hand —
6
+ * CI verifies that this file matches the generator output.
7
+ */
8
+
9
+ import type * as Y from "yjs";
10
+ import type { z } from "zod";
11
+ import type { SlidesMeta } from "../types/slides.ts";
12
+
13
+ export type DocTypeName = "slides";
14
+
15
+ export type DocMetaMap = {
16
+ "slides": z.infer<typeof SlidesMeta>;
17
+ };
18
+
19
+ export type DocBodyMap = {
20
+ "slides": never;
21
+ };
22
+
23
+ export type DocChildrenMap = {
24
+ "slides": never;
25
+ };
26
+
27
+ export interface DocOf<N extends DocTypeName> {
28
+ readonly id: string;
29
+ readonly type: N;
30
+ readonly meta: DocMetaMap[N];
31
+ readonly body: DocBodyMap[N];
32
+ readonly children: ReadonlyArray<DocOf<DocChildrenMap[N]>>;
33
+ }
34
+
35
+ export type Doc = { [N in DocTypeName]: DocOf<N> }[DocTypeName];
@@ -0,0 +1,35 @@
1
+ /**
2
+ * AUTO-GENERATED by @abraca/schema (table).
3
+ *
4
+ * Source: ../types/table.ts
5
+ * Run `pnpm schema:gen` to regenerate. Do not edit by hand —
6
+ * CI verifies that this file matches the generator output.
7
+ */
8
+
9
+ import type * as Y from "yjs";
10
+ import type { z } from "zod";
11
+ import type { TableMeta } from "../types/table.ts";
12
+
13
+ export type DocTypeName = "table";
14
+
15
+ export type DocMetaMap = {
16
+ "table": z.infer<typeof TableMeta>;
17
+ };
18
+
19
+ export type DocBodyMap = {
20
+ "table": never;
21
+ };
22
+
23
+ export type DocChildrenMap = {
24
+ "table": never;
25
+ };
26
+
27
+ export interface DocOf<N extends DocTypeName> {
28
+ readonly id: string;
29
+ readonly type: N;
30
+ readonly meta: DocMetaMap[N];
31
+ readonly body: DocBodyMap[N];
32
+ readonly children: ReadonlyArray<DocOf<DocChildrenMap[N]>>;
33
+ }
34
+
35
+ export type Doc = { [N in DocTypeName]: DocOf<N> }[DocTypeName];
@@ -0,0 +1,35 @@
1
+ /**
2
+ * AUTO-GENERATED by @abraca/schema (timeline).
3
+ *
4
+ * Source: ../types/timeline.ts
5
+ * Run `pnpm schema:gen` to regenerate. Do not edit by hand —
6
+ * CI verifies that this file matches the generator output.
7
+ */
8
+
9
+ import type * as Y from "yjs";
10
+ import type { z } from "zod";
11
+ import type { TimelineMeta } from "../types/timeline.ts";
12
+
13
+ export type DocTypeName = "timeline";
14
+
15
+ export type DocMetaMap = {
16
+ "timeline": z.infer<typeof TimelineMeta>;
17
+ };
18
+
19
+ export type DocBodyMap = {
20
+ "timeline": never;
21
+ };
22
+
23
+ export type DocChildrenMap = {
24
+ "timeline": never;
25
+ };
26
+
27
+ export interface DocOf<N extends DocTypeName> {
28
+ readonly id: string;
29
+ readonly type: N;
30
+ readonly meta: DocMetaMap[N];
31
+ readonly body: DocBodyMap[N];
32
+ readonly children: ReadonlyArray<DocOf<DocChildrenMap[N]>>;
33
+ }
34
+
35
+ export type Doc = { [N in DocTypeName]: DocOf<N> }[DocTypeName];
package/src/index.ts ADDED
@@ -0,0 +1,389 @@
1
+ /**
2
+ * @abraca/schema — public surface.
3
+ *
4
+ * The package exposes:
5
+ * 1. A small DSL (`defineDocType`, `defineSchema`, `ref`) for declaring
6
+ * doc-types backed by Zod meta schemas.
7
+ * 2. CRDT primitive markers (`ytext`, `yarray`, `ymap`) — see `./crdt.ts`.
8
+ * 3. A library of independently-importable per-page-type schemas (see
9
+ * `./types/`). Each file exports:
10
+ * - the doc-type value (e.g. `Kanban`)
11
+ * - the meta Zod schema (e.g. `KanbanMeta`)
12
+ * - a single-type `SchemaRegistry` (e.g. `kanbanSchema`)
13
+ * Consumers compose registries by hand:
14
+ * `defineSchema({ types: [Kanban, Calendar, Doc] })`.
15
+ * 4. A shared `UniversalMeta` zod object for cross-cutting meta keys.
16
+ *
17
+ * No aggregate "all page types" schema is shipped — each app picks the
18
+ * subset it needs.
19
+ */
20
+
21
+ import type { z, ZodType } from "zod";
22
+
23
+ export { ytext, yarray, ymap, crdtMetaOf } from "./crdt.ts";
24
+ export type { CrdtKind, CrdtMeta } from "./crdt.ts";
25
+
26
+ export { UniversalMeta } from "./types/universal.ts";
27
+
28
+ export {
29
+ META_COLUMNS,
30
+ WhereClauseSchema,
31
+ parseWhereClause,
32
+ } from "./query.ts";
33
+ export type { MetaColumn, WhereClause } from "./query.ts";
34
+
35
+ export { Doc, DocMeta, docSchema } from "./types/doc.ts";
36
+ export { Prose, ProseMeta, proseSchema } from "./types/prose.ts";
37
+ export { Kanban, KanbanMeta, kanbanSchema } from "./types/kanban.ts";
38
+ export { Gallery, GalleryMeta, gallerySchema } from "./types/gallery.ts";
39
+ export { Table, TableMeta, tableSchema } from "./types/table.ts";
40
+ export { Outline, OutlineMeta, outlineSchema } from "./types/outline.ts";
41
+ export { Checklist, ChecklistMeta, checklistSchema } from "./types/checklist.ts";
42
+ export { Graph, GraphMeta, graphSchema } from "./types/graph.ts";
43
+ export { Timeline, TimelineMeta, timelineSchema } from "./types/timeline.ts";
44
+ export { Calendar, CalendarMeta, calendarSchema } from "./types/calendar.ts";
45
+ export { MapDocType, MapMeta, mapSchema } from "./types/map.ts";
46
+ export { Dashboard, DashboardMeta, dashboardSchema } from "./types/dashboard.ts";
47
+ export { Chart, ChartMeta, chartSchema } from "./types/chart.ts";
48
+ export { Sheets, SheetsMeta, sheetsSchema } from "./types/sheets.ts";
49
+ export { Slides, SlidesMeta, slidesSchema } from "./types/slides.ts";
50
+ export { Overview, OverviewMeta, overviewSchema } from "./types/overview.ts";
51
+
52
+ // ---------------------------------------------------------------------------
53
+ // Plugin manifest validation — opt-in runtime validator for @abraca/plugin's
54
+ // `PluginManifest` type. Used by the registry server, the plugin CLI, and
55
+ // hosts that want defence-in-depth before instantiating external plugins.
56
+ // ---------------------------------------------------------------------------
57
+
58
+ export {
59
+ PluginManifestSchema,
60
+ PluginManifestAuthorSchema,
61
+ PluginManifestContributesSchema,
62
+ PluginCapabilitySchema,
63
+ PluginIdSchema,
64
+ SemverSchema,
65
+ SemverRangeSchema,
66
+ IntegritySchema,
67
+ RelativePathSchema,
68
+ PluginPricingSchema,
69
+ PluginVersionStatusSchema,
70
+ validatePluginManifest,
71
+ } from "./manifest/plugin-manifest.ts";
72
+ export type {
73
+ ManifestValidationIssue,
74
+ ManifestValidationResult,
75
+ } from "./manifest/plugin-manifest.ts";
76
+
77
+ // ---------------------------------------------------------------------------
78
+ // Public types
79
+ // ---------------------------------------------------------------------------
80
+
81
+ /** A reference to another doc-type by name. Resolved at registry build time. */
82
+ export interface DocTypeRef {
83
+ readonly __ref: string;
84
+ }
85
+
86
+ /** Declaration handed to {@link defineDocType}. */
87
+ export interface DocTypeSpec<
88
+ Name extends string = string,
89
+ MetaSchema extends ZodType = ZodType,
90
+ BodySchema extends ZodType | undefined = ZodType | undefined,
91
+ > {
92
+ readonly name: Name;
93
+ readonly version: number;
94
+ readonly meta: MetaSchema;
95
+ /**
96
+ * Body slot — the document's primary content. Typically a CRDT marker
97
+ * (`ytext()`, `yarray(...)`, `ymap(...)`) but can be any Zod schema for
98
+ * documents whose body is read as a POJO snapshot.
99
+ */
100
+ readonly body?: BodySchema;
101
+ /** Allowed child doc-types, by ref. */
102
+ readonly children?: ReadonlyArray<DocTypeRef>;
103
+ /** Reserved for Phase 2 — declarative permissions per field. */
104
+ readonly permissions?: Record<string, unknown>;
105
+ /**
106
+ * Forward migrations between versions. Each entry's key is the
107
+ * **source** version: `migrations[1]` migrates a v1 meta to v2.
108
+ * `runMigrations` chains them up to `version`. Reserved versions
109
+ * with no migration entry stop the chain; the meta is returned at
110
+ * the highest version reached.
111
+ */
112
+ readonly migrations?: {
113
+ readonly [fromVersion: number]: (oldMeta: any) => any;
114
+ };
115
+ }
116
+
117
+ /** Resolved doc-type after registry build. */
118
+ export interface DocType<
119
+ Name extends string = string,
120
+ MetaSchema extends ZodType = ZodType,
121
+ BodySchema extends ZodType | undefined = ZodType | undefined,
122
+ > extends DocTypeSpec<Name, MetaSchema, BodySchema> {
123
+ readonly __resolved: true;
124
+ }
125
+
126
+ export interface SchemaSpec {
127
+ readonly types: ReadonlyArray<DocType>;
128
+ }
129
+
130
+ /**
131
+ * Phantom meta-type witness on the registry. Carrying the meta map at the
132
+ * type level lets consumers (e.g. `provider.docs(schema).get(type, id)`)
133
+ * extract per-doc-type meta types via {@link DocMetaOf}/{@link DocTypeNameOf}
134
+ * without re-importing the source declarations.
135
+ *
136
+ * Default `Record<string, unknown>` keeps existing untyped consumers working
137
+ * unchanged (Rule 4).
138
+ */
139
+ export interface SchemaRegistry<
140
+ TMap extends Record<string, unknown> = Record<string, unknown>,
141
+ > {
142
+ readonly types: ReadonlyMap<string, DocType>;
143
+ get(name: string): DocType | undefined;
144
+ validateMeta(name: string, value: unknown): ValidationResult;
145
+ /**
146
+ * Returns ok if `child` is allowed under `parent`, err otherwise.
147
+ * `parent === null` means the document tree root — only doc-types
148
+ * declared as root-eligible (none today; reserved) are accepted there.
149
+ */
150
+ validateChildType(
151
+ parent: string | null,
152
+ child: string,
153
+ ): ValidationResult;
154
+ /**
155
+ * Phantom-only field. Exists for type-inference (`infer` patterns in
156
+ * {@link DocTypeNameOf}/{@link DocMetaOf}). Always `undefined` at runtime.
157
+ */
158
+ readonly __metaMap?: TMap;
159
+ }
160
+
161
+ /**
162
+ * Extract the doc-type names from a typed registry. Works against any
163
+ * `SchemaRegistry<TMap>` value.
164
+ */
165
+ export type DocTypeNameOf<S> = S extends SchemaRegistry<infer M>
166
+ ? keyof M & string
167
+ : never;
168
+
169
+ /** Extract the meta type for a given doc-type name from a typed registry. */
170
+ export type DocMetaOf<S, N extends string> = S extends SchemaRegistry<infer M>
171
+ ? N extends keyof M
172
+ ? M[N]
173
+ : never
174
+ : never;
175
+
176
+ /**
177
+ * Internal: derive the meta-map type from a `SchemaSpec` so `defineSchema`
178
+ * can return `SchemaRegistry<MetaMapFor<typeof spec>>` and consumers get
179
+ * fully-inferred per-doc-type meta types at the call site.
180
+ */
181
+ type MetaMapFor<S extends SchemaSpec> = {
182
+ [T in S["types"][number] as T["name"]]: T extends DocType<
183
+ string,
184
+ infer M extends ZodType
185
+ >
186
+ ? z.infer<M>
187
+ : never;
188
+ };
189
+
190
+ export type ValidationResult =
191
+ | { ok: true; value: unknown }
192
+ | { ok: false; errors: ReadonlyArray<ValidationIssue> };
193
+
194
+ export interface ValidationIssue {
195
+ readonly path: ReadonlyArray<PropertyKey>;
196
+ readonly message: string;
197
+ readonly code?: string;
198
+ }
199
+
200
+ // ---------------------------------------------------------------------------
201
+ // Public API
202
+ // ---------------------------------------------------------------------------
203
+
204
+ /**
205
+ * Declare a doc-type. The returned value carries enough type information for
206
+ * downstream codegen and is a `DocType` once registered.
207
+ */
208
+ export function defineDocType<
209
+ Name extends string,
210
+ MetaSchema extends ZodType,
211
+ BodySchema extends ZodType | undefined = undefined,
212
+ >(
213
+ spec: DocTypeSpec<Name, MetaSchema, BodySchema>,
214
+ ): DocType<Name, MetaSchema, BodySchema> {
215
+ return { ...spec, __resolved: true };
216
+ }
217
+
218
+ /** Reference another doc-type by name. Resolution happens in {@link defineSchema}. */
219
+ export function ref(name: string): DocTypeRef {
220
+ return { __ref: name };
221
+ }
222
+
223
+ /**
224
+ * Build a registry from a list of doc-types. Validates that all `ref(...)`
225
+ * targets exist; throws on dangling references.
226
+ *
227
+ * The returned registry carries a phantom `__metaMap` witness inferred from
228
+ * `spec`, so call sites like `provider.docs(schema).get("kanban", id)`
229
+ * see fully-typed meta even though the registry's runtime shape is generic.
230
+ */
231
+ export function defineSchema<S extends SchemaSpec>(
232
+ spec: S,
233
+ ): SchemaRegistry<MetaMapFor<S>> {
234
+ const types = new Map<string, DocType>();
235
+ for (const t of spec.types) {
236
+ if (types.has(t.name)) {
237
+ throw new Error(`Duplicate doc-type name: ${t.name}`);
238
+ }
239
+ types.set(t.name, t);
240
+ }
241
+ for (const t of spec.types) {
242
+ for (const child of t.children ?? []) {
243
+ if (!types.has(child.__ref)) {
244
+ throw new Error(
245
+ `Doc-type "${t.name}" references unknown child doc-type "${child.__ref}"`,
246
+ );
247
+ }
248
+ }
249
+ }
250
+ return {
251
+ types,
252
+ get(name) {
253
+ return types.get(name);
254
+ },
255
+ validateChildType(parent, child) {
256
+ if (!types.has(child)) {
257
+ return {
258
+ ok: false,
259
+ errors: [{ path: [], message: `Unknown doc-type: ${child}` }],
260
+ };
261
+ }
262
+ if (parent === null) {
263
+ return {
264
+ ok: false,
265
+ errors: [
266
+ {
267
+ path: [],
268
+ message: `Doc-type "${child}" cannot be a root document — root-eligibility is reserved for Phase 2`,
269
+ },
270
+ ],
271
+ };
272
+ }
273
+ const parentType = types.get(parent);
274
+ if (!parentType) {
275
+ return {
276
+ ok: false,
277
+ errors: [{ path: [], message: `Unknown parent doc-type: ${parent}` }],
278
+ };
279
+ }
280
+ const allowed = (parentType.children ?? []).some((c) => c.__ref === child);
281
+ if (allowed) {
282
+ return { ok: true, value: undefined };
283
+ }
284
+ return {
285
+ ok: false,
286
+ errors: [
287
+ {
288
+ path: [],
289
+ message: `Doc-type "${child}" is not an allowed child of "${parent}"`,
290
+ },
291
+ ],
292
+ };
293
+ },
294
+ validateMeta(name, value) {
295
+ const t = types.get(name);
296
+ // Rule 4 (feedback_schema_free_core): doc-types not in this
297
+ // registry are unconstrained. Existing-app traffic for types the
298
+ // registry doesn't know about is never rejected — callers can
299
+ // check membership explicitly via `registry.get(name)` if they
300
+ // need strict semantics.
301
+ if (!t) {
302
+ return { ok: true, value };
303
+ }
304
+ const parsed = t.meta.safeParse(value);
305
+ if (parsed.success) {
306
+ return { ok: true, value: parsed.data };
307
+ }
308
+ return {
309
+ ok: false,
310
+ errors: parsed.error.issues.map((i) => ({
311
+ path: i.path,
312
+ message: i.message,
313
+ code: i.code,
314
+ })),
315
+ };
316
+ },
317
+ };
318
+ }
319
+
320
+ /**
321
+ * Apply forward migrations to a meta object.
322
+ *
323
+ * Reads `meta.__schemaVersion` (defaults to `1` if absent) and applies
324
+ * each `migrations[v]` from there up to the schema's declared `version`.
325
+ * Each migration function receives the previous version's meta and
326
+ * returns the next version's meta. The final value carries
327
+ * `__schemaVersion = <highest version reached>`.
328
+ *
329
+ * Behaviour:
330
+ * - **Unknown doc-type**: returns `meta` unchanged. Rule 4 — registries
331
+ * only constrain the types they declare.
332
+ * - **No migrations declared**: returns `meta` unchanged. Useful for
333
+ * freshly-introduced doc-types still at v1.
334
+ * - **Missing migration entry mid-chain**: stops at the highest
335
+ * reachable version. Caller can detect by inspecting
336
+ * `result.__schemaVersion` against `schema.get(typeName)?.version`.
337
+ * - **Non-object meta** (null, undefined, primitives): returns
338
+ * unchanged. Migrations only operate on object meta.
339
+ *
340
+ * Migrations are pure: they receive a meta value and return a new
341
+ * one. They MUST NOT mutate the input (callers may pass shared
342
+ * references).
343
+ *
344
+ * @example
345
+ * // v2 of kanban introduces a new required-shape `boardLayout` key.
346
+ * const Kanban = defineDocType({
347
+ * name: "kanban",
348
+ * version: 2,
349
+ * meta: KanbanMetaV2,
350
+ * migrations: {
351
+ * 1: (oldMeta) => ({ ...oldMeta, boardLayout: "default" }),
352
+ * },
353
+ * });
354
+ * const fresh = runMigrations(kanbanSchema, "kanban", { kanbanShowIcon: true });
355
+ * // fresh.boardLayout === "default"
356
+ * // fresh.__schemaVersion === 2
357
+ */
358
+ export function runMigrations(
359
+ registry: SchemaRegistry,
360
+ typeName: string,
361
+ meta: unknown,
362
+ ): unknown {
363
+ if (meta === null || typeof meta !== "object") return meta;
364
+ const t = registry.get(typeName);
365
+ if (!t) return meta; // Rule 4
366
+ const target = t.version;
367
+ const migrations = t.migrations;
368
+
369
+ const m = meta as Record<string, unknown>;
370
+ let currentVersion: number =
371
+ typeof m.__schemaVersion === "number" && m.__schemaVersion >= 1
372
+ ? m.__schemaVersion
373
+ : 1;
374
+ if (currentVersion >= target) return meta;
375
+ if (!migrations) return meta;
376
+
377
+ let working: unknown = meta;
378
+ let migrated = false;
379
+ while (currentVersion < target) {
380
+ const fn = migrations[currentVersion];
381
+ if (typeof fn !== "function") break;
382
+ working = fn(working);
383
+ currentVersion += 1;
384
+ migrated = true;
385
+ }
386
+ if (!migrated) return meta; // no migration ran → return original unchanged
387
+ if (working === null || typeof working !== "object") return working;
388
+ return { ...(working as Record<string, unknown>), __schemaVersion: currentVersion };
389
+ }