@vertekum/schema-builder 0.3.8 → 0.3.11

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/CHANGELOG.md CHANGED
@@ -1,5 +1,28 @@
1
1
  # @vertekum/schema-builder
2
2
 
3
+ ## 0.3.11
4
+
5
+ ### Patch Changes
6
+
7
+ - [`6265f42`](https://github.com/moros-oxus/vertekum/commit/6265f4285ed7b2e9a5e1dd2f6801911ddf9247d0) Thanks [@tschemmer](https://github.com/tschemmer)! - `vertekum schema mock` renders the matrix a vocabulary grants: a grouped name listing (`--style names`), a sample DTCG token file with per-type default values (`--style tokens`; types via the `mock.types` glob map, then `--type`), at `least` (every name adjacency once) or `full` (the whole matrix) coverage — and with `--break <p>`, a separate deliberately-broken sibling file that `check` must refuse. Deterministic at a fixed `--seed`; outputs land in the configured `mock.out`.
8
+ - Updated dependencies []:
9
+ - @vertekum/core@0.3.11
10
+
11
+ ## 0.3.10
12
+
13
+ ### Patch Changes
14
+
15
+ - [`351366b`](https://github.com/moros-oxus/vertekum/commit/351366b8a92c60e9186cb0c74d774000c0d5366c) Thanks [@tschemmer](https://github.com/tschemmer)! - Emitted schemas share repeated subtrees: identical tails (optional-slot syntagms expand them once per branch) hoist into `$defs` as content-named `shared-*` entries. A consumer module that emitted 12 MB with 22k ref-sites — enough to overflow ajv's call stack at validation — now emits ~30 KB with identical validation behaviour.
16
+ - Updated dependencies []:
17
+ - @vertekum/core@0.3.10
18
+
19
+ ## 0.3.9
20
+
21
+ ### Patch Changes
22
+
23
+ - Updated dependencies [[`a35485a`](https://github.com/moros-oxus/vertekum/commit/a35485a227a9e94271b041bc020f8edf28e0137b)]:
24
+ - @vertekum/core@0.3.9
25
+
3
26
  ## 0.3.8
4
27
 
5
28
  ### Patch Changes
package/README.md CHANGED
@@ -62,6 +62,7 @@ root = color.<property>.<role>?.<emphasis>?.<state>?
62
62
  - [`schema build`](./docs/build.md) — the command, the sweep, the ownership stamp, the
63
63
  `--check` CI gate, programmatic use.
64
64
  - [`schema lint`](./docs/lint.md) — validate the `.dfn` sources themselves: fragments
65
+ - [`schema mock`](./docs/mock.md) — render the granted matrix: name listings, sample token files, deliberate breakage.
65
66
  included, every production, findings collected with positions; `--fix` repairs the
66
67
  mechanical ones.
67
68
  - [`schema fmt`](./docs/format.md) — canonical formatting: JS-literal blocks, indent
package/docs/mock.md ADDED
@@ -0,0 +1,63 @@
1
+ # `schema mock` — the matrix, made tangible
2
+
3
+ A vocabulary grants a space of names. `schema mock` renders that space as files you
4
+ can read, validate, and break on purpose:
5
+
6
+ ```bash
7
+ # vertekum schema mock [module] [--style names|tokens] [--coverage least|full] [--break <p>] [--type <t>] [--seed <n>]
8
+ vertekum schema mock # every module in the configured source
9
+ vertekum schema mock schemas/color.dfn # one module
10
+ vertekum schema mock --style tokens --break 0.3
11
+ ```
12
+
13
+ | Flag | Values | Default | What it does |
14
+ | --- | --- | --- | --- |
15
+ | `--style` | `names` \| `tokens` | both | `names` writes the markdown listing; `tokens` writes a sample DTCG file. |
16
+ | `--coverage` | `least` \| `full` | `least` | How much of the matrix each output holds (below). |
17
+ | `--break` | probability `0..1` | `0` | Each token independently breaks with this probability — emitted as a SEPARATE `*.broken.tokens.json`; the clean mock stays valid. |
18
+ | `--type` | a DTCG `$type` | `color` | Fallback type for mock tokens (the settings `mock.types` map wins). |
19
+ | `--seed` | integer | `1` | The breakage RNG seed — identical invocations produce identical files; vary it to vary the breaks. |
20
+
21
+ Outputs land in the configured `mock.out` (default `./mocks`), one trio per module:
22
+ `<module>.names.md` (grouped by top-level segment, with counts),
23
+ `<module>.mock.tokens.json`, and — with `--break` — `<module>.broken.tokens.json`.
24
+
25
+ ## Coverage
26
+
27
+ - **`least`** — every parent→child name adjacency appears at least once: each member
28
+ of each position is exercised somewhere, without the cross-product. The smoke-test
29
+ size.
30
+ - **`full`** — every complete token name the vocabulary grants. On a generative
31
+ module this is the whole matrix; expect thousands.
32
+
33
+ Token names are the tree's **leaves** — an interior name whose optional tail was
34
+ skipped is a group, never a token (that is what `$root` exists for).
35
+
36
+ ## Types and values
37
+
38
+ A mock token's `$type` resolves through: the settings map → `--type` → `color`.
39
+
40
+ ```ts
41
+ schemaBuilderExtension({
42
+ mock: {
43
+ out: './mocks',
44
+ types: { 'color.*': 'color', 'space.*': 'dimension' }, // name-glob → $type
45
+ },
46
+ })
47
+ ```
48
+
49
+ Values come from a fixed per-type table: color → white, dimension → `0px`,
50
+ duration → `0ms`, number → `0`, fontWeight → `400`, fontFamily → `["sans-serif"]`,
51
+ cubicBezier → `[0,0,1,1]`, strokeStyle → `"solid"`, and minimal spec objects for the
52
+ composites (border, shadow, transition, gradient, typography). An unknown type gets
53
+ the string `"mock"`.
54
+
55
+ ## Breakage
56
+
57
+ With `--break 0.5`, each token has a 50% chance of one deliberate violation — a coin
58
+ picks **name** (an ungranted `-broken` sibling, caught by the vocabulary seal) or
59
+ **value** (a type-mismatched literal, caught by the format binding; types without a
60
+ published value schema break by name instead, so every break stays catchable). Each
61
+ broken token carries `$description: "deliberately broken (name|value)"`. Copy the
62
+ broken file into a collection and watch `vertekum check` refuse it — a self-test of
63
+ the whole validation stack.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vertekum/schema-builder",
3
- "version": "0.3.8",
3
+ "version": "0.3.11",
4
4
  "description": "Declare token vocabularies as grammar; build them into JSON Schema files",
5
5
  "license": "Apache-2.0",
6
6
  "keywords": [
@@ -37,13 +37,13 @@
37
37
  "./cli": "./src/cli.ts"
38
38
  },
39
39
  "peerDependencies": {
40
- "@vertekum/core": "0.3.8"
40
+ "@vertekum/core": "0.3.11"
41
41
  },
42
42
  "dependencies": {
43
43
  "zod": "^3.25.76"
44
44
  },
45
45
  "devDependencies": {
46
- "@vertekum/core": "0.3.8",
46
+ "@vertekum/core": "0.3.11",
47
47
  "@vertekum/schema-vocabulary": "0.3.0"
48
48
  }
49
49
  }
package/src/api.ts CHANGED
@@ -1,5 +1,10 @@
1
1
  import type { ActivateContext } from '@vertekum/core';
2
- import { schemaBuildCommand, schemaFmtCommand, schemaLintCommand } from './cli';
2
+ import {
3
+ schemaBuildCommand,
4
+ schemaFmtCommand,
5
+ schemaLintCommand,
6
+ schemaMockCommand,
7
+ } from './cli';
3
8
  import type { schemaBuilderManifest } from './index';
4
9
 
5
10
  export {
@@ -33,4 +38,5 @@ export function activate(
33
38
  ctx.commands.register(schemaBuildCommand);
34
39
  ctx.commands.register(schemaLintCommand);
35
40
  ctx.commands.register(schemaFmtCommand);
41
+ ctx.commands.register(schemaMockCommand);
36
42
  }
package/src/cli.ts CHANGED
@@ -12,6 +12,16 @@ import { emit, isStamped } from './emit';
12
12
  import { DfnError } from './error';
13
13
  import { fixSource, formatSource, resolveIndent } from './format';
14
14
  import { lintModule } from './lint';
15
+ import {
16
+ breakTokens,
17
+ fullPaths,
18
+ leastPaths,
19
+ mockTokens,
20
+ renderNames,
21
+ renderTokens,
22
+ rng,
23
+ typeResolver,
24
+ } from './mock';
15
25
  import { type ResolvedModule, resolveModule } from './resolve';
16
26
 
17
27
  /**
@@ -37,6 +47,7 @@ function settingsOf(ctx: { project: unknown }): {
37
47
  out?: string;
38
48
  link: boolean;
39
49
  schemaId?: string;
50
+ mock: { out: string; types?: Record<string, string> };
40
51
  } {
41
52
  const kernel = (ctx.project as ProjectDir).kernel;
42
53
  const slice = kernel?.config.get<{
@@ -44,12 +55,17 @@ function settingsOf(ctx: { project: unknown }): {
44
55
  out?: string;
45
56
  link?: boolean;
46
57
  schemaId?: string;
58
+ mock?: { out?: string; types?: Record<string, string> };
47
59
  }>('vtk.schema-builder');
48
60
  return {
49
61
  source: slice?.source ?? './schemas',
50
62
  out: slice?.out,
51
63
  link: slice?.link === true,
52
64
  schemaId: slice?.schemaId,
65
+ mock: {
66
+ out: slice?.mock?.out ?? './mocks',
67
+ ...(slice?.mock?.types ? { types: slice.mock.types } : {}),
68
+ },
53
69
  };
54
70
  }
55
71
 
@@ -495,3 +511,135 @@ export const schemaBuildCommand: CommandDescriptor = {
495
511
  return { summary: notes.join('\n'), files, data: { skipped, fragments } };
496
512
  },
497
513
  };
514
+
515
+ /**
516
+ * `vertekum schema mock` — make the granted matrix tangible: a names listing, a usable sample
517
+ * token file, and (with `--break`) a deliberately broken sibling that `check` must catch.
518
+ * Coverage `least` exercises every parent→child name adjacency once; `full` is the whole
519
+ * matrix. Deterministic at a fixed `--seed`, so mocks are diff-stable.
520
+ */
521
+ export const schemaMockCommand: CommandDescriptor = {
522
+ name: 'schema mock',
523
+ description:
524
+ 'generate the name matrix and sample token files a vocabulary grants',
525
+ args: [
526
+ {
527
+ name: 'module',
528
+ required: false,
529
+ description:
530
+ "a .dfn file or directory; default: the configured source ('./schemas')",
531
+ },
532
+ ],
533
+ options: [
534
+ {
535
+ flag: '--style <style>',
536
+ description: "'names' (markdown) or 'tokens' (DTCG sample); default both",
537
+ },
538
+ {
539
+ flag: '--coverage <coverage>',
540
+ description:
541
+ "'least' (every name adjacency once; default) or 'full' (the whole matrix)",
542
+ },
543
+ {
544
+ flag: '--break <p>',
545
+ description:
546
+ 'probability 0..1 that each token breaks (name or value) — emitted as a separate *.broken.tokens.json',
547
+ },
548
+ {
549
+ flag: '--type <type>',
550
+ description: "fallback DTCG $type for mock tokens (default 'color')",
551
+ },
552
+ {
553
+ flag: '--seed <n>',
554
+ description: 'RNG seed for the breakage pass (default 1)',
555
+ },
556
+ ],
557
+ run(ctx): CommandResult {
558
+ const { projectDir } = ctx.project as ProjectDir;
559
+ const settings = settingsOf(ctx);
560
+ const style = (ctx.options.style as string | undefined) ?? 'both';
561
+ if (!['names', 'tokens', 'both'].includes(style)) {
562
+ throw new Error(`--style must be 'names' or 'tokens', got '${style}'`);
563
+ }
564
+ const coverage = (ctx.options.coverage as string | undefined) ?? 'least';
565
+ if (!['least', 'full'].includes(coverage)) {
566
+ throw new Error(
567
+ `--coverage must be 'least' or 'full', got '${coverage}'`,
568
+ );
569
+ }
570
+ const breakP = Number((ctx.options.break as string | undefined) ?? '0');
571
+ if (!Number.isFinite(breakP) || breakP < 0 || breakP > 1) {
572
+ throw new Error(`--break must be a probability 0..1`);
573
+ }
574
+ const seed = Number((ctx.options.seed as string | undefined) ?? '1');
575
+ const typeOf = typeResolver(
576
+ settings.mock.types,
577
+ ctx.options.type as string | undefined,
578
+ );
579
+
580
+ const { modules, explicitFile, root } = resolveModules(
581
+ projectDir,
582
+ ctx.args.module,
583
+ settings.source,
584
+ );
585
+ if (modules.length === 0) {
586
+ return { summary: 'no .dfn modules found', files: [] };
587
+ }
588
+
589
+ const files: Array<{ path: string; content: string }> = [];
590
+ const skipped: string[] = [];
591
+ let names = 0;
592
+ let tokensOut = 0;
593
+
594
+ for (const modulePath of modules) {
595
+ const resolved = resolveModule(modulePath);
596
+ if (natureOf(resolved) === 'inline' || !resolved.module.root) {
597
+ // Inline modules never emit; a rootless fragment has no matrix of its own. In a sweep
598
+ // both are notices; an explicit fragment is worth an error.
599
+ if (explicitFile) {
600
+ throw new Error(
601
+ `${relative(projectDir, modulePath)} declares no root — nothing to mock`,
602
+ );
603
+ }
604
+ skipped.push(relative(projectDir, modulePath));
605
+ continue;
606
+ }
607
+ const tree = build(resolved);
608
+ const paths = coverage === 'full' ? fullPaths(tree) : leastPaths(tree);
609
+ const rel = relative(root, modulePath).replace(/\.dfn$/, '');
610
+ const base = join(settings.mock.out, rel);
611
+ const moduleName = rel.split('/').pop() as string;
612
+
613
+ if (style !== 'tokens') {
614
+ files.push({
615
+ path: `${base}.names.md`,
616
+ content: renderNames(moduleName, coverage, paths),
617
+ });
618
+ names += paths.length;
619
+ }
620
+ if (style !== 'names') {
621
+ const tokens = mockTokens(paths, typeOf);
622
+ files.push({
623
+ path: `${base}.mock.tokens.json`,
624
+ content: renderTokens(tokens),
625
+ });
626
+ tokensOut += tokens.length;
627
+ if (breakP > 0) {
628
+ const broken = breakTokens(tokens, breakP, rng(seed));
629
+ files.push({
630
+ path: `${base}.broken.tokens.json`,
631
+ content: renderTokens(broken),
632
+ });
633
+ }
634
+ }
635
+ }
636
+
637
+ const parts = [
638
+ `mocked ${files.length} file(s) (${coverage})`,
639
+ style !== 'tokens' ? `${names} name(s)` : null,
640
+ style !== 'names' ? `${tokensOut} token(s)` : null,
641
+ skipped.length > 0 ? `skipped ${skipped.join(', ')}` : null,
642
+ ].filter(Boolean);
643
+ return { summary: parts.join(' — '), files };
644
+ },
645
+ };
package/src/dedupe.ts ADDED
@@ -0,0 +1,181 @@
1
+ /**
2
+ * Structural sharing for emitted schemas.
3
+ *
4
+ * A generative syntagm with optional slots (`color.<pattern>?.<property>.<role>?.<state>?`)
5
+ * repeats the TAIL of the expression under every enumerated sibling name — byte-identical
6
+ * subtrees, hundreds of kilobytes each, multiplied into megabytes (a consumer module reached
7
+ * 12 MB / 22k ref-sites, and ajv's compiled validator overflowed the call stack). This pass
8
+ * hoists every repeated schema subtree into `$defs` once and references it — validation
9
+ * behaviour is unchanged because 2020-12 `unevaluatedProperties` sees through `$ref`.
10
+ *
11
+ * Only nodes in SCHEMA POSITION are candidates: hoisting the object under `properties` itself
12
+ * would turn a keyword map into `{ "$ref": … }`, which is a property named `$ref`, not a
13
+ * reference. Names are content-derived (`shared-<hash>`), so rebuilds stay diff-stable.
14
+ */
15
+
16
+ type Json = Record<string, unknown>;
17
+
18
+ /** Keywords whose value is a map of name → schema. */
19
+ const SCHEMA_MAPS = new Set(['properties', 'patternProperties', '$defs']);
20
+ /** Keywords whose value is an array of schemas. */
21
+ const SCHEMA_LISTS = new Set(['allOf', 'anyOf', 'oneOf']);
22
+ /** Keywords whose value is a single schema (when it is an object). */
23
+ const SCHEMA_VALUES = new Set([
24
+ 'additionalProperties',
25
+ 'unevaluatedProperties',
26
+ 'items',
27
+ 'not',
28
+ 'if',
29
+ 'then',
30
+ 'else',
31
+ ]);
32
+
33
+ /**
34
+ * Only subtrees at least this large hoist. Small repetition is deliberate emission style (a
35
+ * shared `allOf` pair reads better inline, and tests pin those shapes); the pathology this pass
36
+ * exists for repeats subtrees of hundreds of kilobytes.
37
+ */
38
+ const MIN_BYTES = 512;
39
+
40
+ /** FNV-1a over a string, as fixed-width hex — composition-friendly and dependency-free. */
41
+ function fnv(input: string): string {
42
+ let hash = 0xcbf29ce484222325n;
43
+ const prime = 0x100000001b3n;
44
+ for (let i = 0; i < input.length; i++) {
45
+ hash ^= BigInt(input.charCodeAt(i));
46
+ hash = (hash * prime) & 0xffffffffffffffffn;
47
+ }
48
+ return hash.toString(16).padStart(16, '0');
49
+ }
50
+
51
+ /** Visit every schema-position object under `schema` (excluding `schema` itself). */
52
+ function visitChildren(
53
+ schema: Json,
54
+ visit: (child: Json, replace: (next: Json) => void) => void,
55
+ ): void {
56
+ for (const [key, value] of Object.entries(schema)) {
57
+ if (SCHEMA_MAPS.has(key) && value && typeof value === 'object') {
58
+ const map = value as Json;
59
+ for (const [name, child] of Object.entries(map)) {
60
+ if (child && typeof child === 'object' && !Array.isArray(child)) {
61
+ visit(child as Json, (next) => {
62
+ map[name] = next;
63
+ });
64
+ }
65
+ }
66
+ } else if (SCHEMA_LISTS.has(key) && Array.isArray(value)) {
67
+ value.forEach((child, index) => {
68
+ if (child && typeof child === 'object' && !Array.isArray(child)) {
69
+ visit(child as Json, (next) => {
70
+ (value as unknown[])[index] = next;
71
+ });
72
+ }
73
+ });
74
+ } else if (
75
+ SCHEMA_VALUES.has(key) &&
76
+ value &&
77
+ typeof value === 'object' &&
78
+ !Array.isArray(value)
79
+ ) {
80
+ visit(value as Json, (next) => {
81
+ schema[key] = next;
82
+ });
83
+ }
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Hoist repeated subtrees of `document` into its `$defs`, in place. Iterates to a fixpoint:
89
+ * hoisting inner duplicates makes outer subtrees identical, which the next round catches.
90
+ */
91
+ export function dedupeSubtrees(document: Json): void {
92
+ const defs = (): Json => {
93
+ if (!document.$defs || typeof document.$defs !== 'object') {
94
+ document.$defs = {};
95
+ }
96
+ return document.$defs as Json;
97
+ };
98
+ const reserved = new Set(Object.keys((document.$defs as Json) ?? {}));
99
+
100
+ for (let round = 0; round < 32; round++) {
101
+ // Pass 1: post-order signatures (composed from child hashes — compact and linear).
102
+ const signature = new Map<Json, string>();
103
+ const size = new Map<Json, number>();
104
+ const buckets = new Map<string, Json[]>();
105
+ const sign = (node: Json): string => {
106
+ const held = signature.get(node);
107
+ if (held) return held;
108
+ visitChildren(node, (child) => {
109
+ sign(child);
110
+ });
111
+ const parts: string[] = [];
112
+ let bytes = 2;
113
+ for (const key of Object.keys(node).sort()) {
114
+ const value = node[key];
115
+ parts.push(`${key}:${serialize(value)}`);
116
+ bytes += key.length + byteLength(value);
117
+ }
118
+ const hash = fnv(parts.join('|'));
119
+ signature.set(node, hash);
120
+ size.set(node, bytes);
121
+ const bucket = buckets.get(hash) ?? [];
122
+ bucket.push(node);
123
+ buckets.set(hash, bucket);
124
+ return hash;
125
+ };
126
+ const serialize = (value: unknown): string => {
127
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
128
+ return `#${sign(value as Json)}`;
129
+ }
130
+ return JSON.stringify(value) ?? 'null';
131
+ };
132
+ const byteLength = (value: unknown): number => {
133
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
134
+ return size.get(value as Json) ?? 16;
135
+ }
136
+ return (JSON.stringify(value) ?? 'null').length;
137
+ };
138
+
139
+ // The whole forest: the document body plus every existing def.
140
+ const roots: Json[] = [document];
141
+ signature.set(document, 'root'); // the document itself is never a candidate
142
+ visitChildren(document, (child) => {
143
+ sign(child);
144
+ });
145
+
146
+ // Pass 2: pick duplicated, big-enough subtrees; verify true equality (hash paranoia).
147
+ const hoisted = new Map<string, { name: string; canonical: string }>();
148
+ for (const [hash, bucket] of buckets) {
149
+ if (bucket.length < 2) continue;
150
+ const first = bucket[0] as Json;
151
+ if ((size.get(first) ?? 0) < MIN_BYTES) continue;
152
+ if (Object.keys(first).length === 1 && '$ref' in first) continue;
153
+ const canonical = JSON.stringify(first);
154
+ if (bucket.some((node) => JSON.stringify(node) !== canonical)) continue;
155
+ let name = `shared-${hash.slice(0, 8)}`;
156
+ while (reserved.has(name)) name = `shared-${fnv(name).slice(0, 8)}`;
157
+ hoisted.set(hash, { name, canonical });
158
+ }
159
+ if (hoisted.size === 0) return;
160
+
161
+ // Pass 3: replace every occurrence top-down (an outer hoist swallows its inner ones this
162
+ // round; the next round dedupes inside the new defs).
163
+ const defsMap = defs();
164
+ const replaceIn = (node: Json): void => {
165
+ visitChildren(node, (child, replace) => {
166
+ const hash = signature.get(child);
167
+ const hit = hash === undefined ? undefined : hoisted.get(hash);
168
+ if (hit) {
169
+ if (!(hit.name in defsMap)) {
170
+ defsMap[hit.name] = JSON.parse(hit.canonical);
171
+ reserved.add(hit.name);
172
+ }
173
+ replace({ $ref: `#/$defs/${hit.name}` });
174
+ return;
175
+ }
176
+ replaceIn(child);
177
+ });
178
+ };
179
+ for (const root of roots) replaceIn(root);
180
+ }
181
+ }
package/src/emit.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { PatternRef, TreeNode } from './build';
2
+ import { dedupeSubtrees } from './dedupe';
2
3
  import type { ResolvedModule } from './resolve';
3
4
 
4
5
  /**
@@ -230,6 +231,9 @@ export function emit(tree: TreeNode | undefined, options: EmitOptions): string {
230
231
  }
231
232
  }
232
233
 
234
+ // Structural sharing: repeated tails (optional-slot syntagms) hoist into $defs once.
235
+ dedupeSubtrees(document as Record<string, unknown>);
236
+
233
237
  return `${JSON.stringify(document, null, 2)}\n`;
234
238
  }
235
239
 
package/src/index.ts CHANGED
@@ -24,6 +24,13 @@ export const SchemaBuilderSettings = z.object({
24
24
  * A file's `id` pragma wins. Unset: no `$id` unless the pragma supplies one.
25
25
  */
26
26
  schemaId: z.string().optional(),
27
+ /** `schema mock` output: where mocks land, and a name-glob → DTCG `$type` map. */
28
+ mock: z
29
+ .object({
30
+ out: z.string().default('./mocks'),
31
+ types: z.record(z.string()).optional(),
32
+ })
33
+ .default({}),
27
34
  });
28
35
 
29
36
  export const schemaBuilderManifest = {
package/src/mock.ts ADDED
Binary file