@ontrails/schema 1.0.0-beta.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/dist/hash.js ADDED
@@ -0,0 +1,44 @@
1
+ /**
2
+ * SHA-256 hashing for surface maps.
3
+ *
4
+ * Uses Bun.CryptoHasher for native hashing.
5
+ */
6
+ // ---------------------------------------------------------------------------
7
+ // Helpers
8
+ // ---------------------------------------------------------------------------
9
+ /**
10
+ * Recursively sort all object keys for deterministic JSON serialization.
11
+ * Arrays preserve order; primitives pass through.
12
+ */
13
+ const canonicalize = (value) => {
14
+ if (Array.isArray(value)) {
15
+ return value.map(canonicalize);
16
+ }
17
+ if (value !== null && typeof value === 'object') {
18
+ const sorted = {};
19
+ for (const key of Object.keys(value).toSorted()) {
20
+ sorted[key] = canonicalize(value[key]);
21
+ }
22
+ return sorted;
23
+ }
24
+ return value;
25
+ };
26
+ // ---------------------------------------------------------------------------
27
+ // Public API
28
+ // ---------------------------------------------------------------------------
29
+ /**
30
+ * Compute a SHA-256 hash of a surface map.
31
+ *
32
+ * The `generatedAt` field is excluded so that identical topos always
33
+ * produce the same hash regardless of when they were generated.
34
+ */
35
+ export const hashSurfaceMap = (surfaceMap) => {
36
+ // Strip generatedAt before hashing
37
+ const { generatedAt: _unused, ...rest } = surfaceMap;
38
+ const canonical = canonicalize(rest);
39
+ const json = JSON.stringify(canonical);
40
+ const hasher = new Bun.CryptoHasher('sha256');
41
+ hasher.update(json);
42
+ return hasher.digest('hex');
43
+ };
44
+ //# sourceMappingURL=hash.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hash.js","sourceRoot":"","sources":["../src/hash.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,8EAA8E;AAC9E,UAAU;AACV,8EAA8E;AAE9E;;;GAGG;AACH,MAAM,YAAY,GAAG,CAAC,KAAc,EAAW,EAAE;IAC/C,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO,KAAK,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC;IACjC,CAAC;IACD,IAAI,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAChD,MAAM,MAAM,GAA4B,EAAE,CAAC;QAC3C,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC;YAChD,MAAM,CAAC,GAAG,CAAC,GAAG,YAAY,CAAE,KAAiC,CAAC,GAAG,CAAC,CAAC,CAAC;QACtE,CAAC;QACD,OAAO,MAAM,CAAC;IAChB,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC,CAAC;AAEF,8EAA8E;AAC9E,aAAa;AACb,8EAA8E;AAE9E;;;;;GAKG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,UAAsB,EAAU,EAAE;IAC/D,mCAAmC;IACnC,MAAM,EAAE,WAAW,EAAE,OAAO,EAAE,GAAG,IAAI,EAAE,GAAG,UAAU,CAAC;IAErD,MAAM,SAAS,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC;IACrC,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC;IAEvC,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC;IAC9C,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IACpB,OAAO,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAC9B,CAAC,CAAC"}
@@ -0,0 +1,6 @@
1
+ export { generateSurfaceMap } from './generate.js';
2
+ export { hashSurfaceMap } from './hash.js';
3
+ export { diffSurfaceMaps } from './diff.js';
4
+ export { writeSurfaceMap, readSurfaceMap, writeSurfaceLock, readSurfaceLock, } from './io.js';
5
+ export type { SurfaceMap, SurfaceMapEntry, DiffEntry, DiffResult, JsonSchema, WriteOptions, ReadOptions, } from './types.js';
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AACnD,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAC3C,OAAO,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAG5C,OAAO,EACL,eAAe,EACf,cAAc,EACd,gBAAgB,EAChB,eAAe,GAChB,MAAM,SAAS,CAAC;AAGjB,YAAY,EACV,UAAU,EACV,eAAe,EACf,SAAS,EACT,UAAU,EACV,UAAU,EACV,YAAY,EACZ,WAAW,GACZ,MAAM,YAAY,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,7 @@
1
+ // Generation
2
+ export { generateSurfaceMap } from './generate.js';
3
+ export { hashSurfaceMap } from './hash.js';
4
+ export { diffSurfaceMaps } from './diff.js';
5
+ // File I/O
6
+ export { writeSurfaceMap, readSurfaceMap, writeSurfaceLock, readSurfaceLock, } from './io.js';
7
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,aAAa;AACb,OAAO,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AACnD,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAC3C,OAAO,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAE5C,WAAW;AACX,OAAO,EACL,eAAe,EACf,cAAc,EACd,gBAAgB,EAChB,eAAe,GAChB,MAAM,SAAS,CAAC"}
package/dist/io.d.ts ADDED
@@ -0,0 +1,29 @@
1
+ /**
2
+ * File I/O for surface maps and lock files.
3
+ */
4
+ import type { ReadOptions, SurfaceMap, WriteOptions } from './types.js';
5
+ /**
6
+ * Write a surface map to `<dir>/_surface.json`.
7
+ *
8
+ * Creates the directory if it doesn't exist. Returns the file path.
9
+ */
10
+ export declare const writeSurfaceMap: (surfaceMap: SurfaceMap, options?: WriteOptions) => Promise<string>;
11
+ /**
12
+ * Read a surface map from `<dir>/_surface.json`.
13
+ *
14
+ * Returns `null` if the file doesn't exist.
15
+ */
16
+ export declare const readSurfaceMap: (options?: ReadOptions) => Promise<SurfaceMap | null>;
17
+ /**
18
+ * Write a hash to `<dir>/surface.lock` as a single line.
19
+ *
20
+ * Creates the directory if it doesn't exist. Returns the file path.
21
+ */
22
+ export declare const writeSurfaceLock: (hash: string, options?: WriteOptions) => Promise<string>;
23
+ /**
24
+ * Read the hash from `<dir>/surface.lock`.
25
+ *
26
+ * Returns `null` if the file doesn't exist.
27
+ */
28
+ export declare const readSurfaceLock: (options?: ReadOptions) => Promise<string | null>;
29
+ //# sourceMappingURL=io.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"io.d.ts","sourceRoot":"","sources":["../src/io.ts"],"names":[],"mappings":"AAAA;;GAEG;AAKH,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AA8BxE;;;;GAIG;AACH,eAAO,MAAM,eAAe,GAC1B,YAAY,UAAU,EACtB,UAAU,YAAY,KACrB,OAAO,CAAC,MAAM,CAOhB,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,cAAc,GACzB,UAAU,WAAW,KACpB,OAAO,CAAC,UAAU,GAAG,IAAI,CAY3B,CAAC;AAMF;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,GAC3B,MAAM,MAAM,EACZ,UAAU,YAAY,KACrB,OAAO,CAAC,MAAM,CAMhB,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,eAAe,GAC1B,UAAU,WAAW,KACpB,OAAO,CAAC,MAAM,GAAG,IAAI,CAYvB,CAAC"}
package/dist/io.js ADDED
@@ -0,0 +1,91 @@
1
+ /**
2
+ * File I/O for surface maps and lock files.
3
+ */
4
+ import { mkdir } from 'node:fs/promises';
5
+ import { join } from 'node:path';
6
+ // ---------------------------------------------------------------------------
7
+ // Constants
8
+ // ---------------------------------------------------------------------------
9
+ const DEFAULT_DIR = '.trails';
10
+ const SURFACE_MAP_FILE = '_surface.json';
11
+ const SURFACE_LOCK_FILE = 'surface.lock';
12
+ // ---------------------------------------------------------------------------
13
+ // Helpers
14
+ // ---------------------------------------------------------------------------
15
+ const isNotFound = (err) => typeof err === 'object' &&
16
+ err !== null &&
17
+ err.code === 'ENOENT';
18
+ const resolveDir = (options) => options?.dir ?? DEFAULT_DIR;
19
+ const ensureDir = async (dir) => {
20
+ await mkdir(dir, { recursive: true });
21
+ };
22
+ // ---------------------------------------------------------------------------
23
+ // Surface Map
24
+ // ---------------------------------------------------------------------------
25
+ /**
26
+ * Write a surface map to `<dir>/_surface.json`.
27
+ *
28
+ * Creates the directory if it doesn't exist. Returns the file path.
29
+ */
30
+ export const writeSurfaceMap = async (surfaceMap, options) => {
31
+ const dir = resolveDir(options);
32
+ await ensureDir(dir);
33
+ const filePath = join(dir, SURFACE_MAP_FILE);
34
+ const json = `${JSON.stringify(surfaceMap, null, 2)}\n`;
35
+ await Bun.write(filePath, json);
36
+ return filePath;
37
+ };
38
+ /**
39
+ * Read a surface map from `<dir>/_surface.json`.
40
+ *
41
+ * Returns `null` if the file doesn't exist.
42
+ */
43
+ export const readSurfaceMap = async (options) => {
44
+ const dir = resolveDir(options);
45
+ const filePath = join(dir, SURFACE_MAP_FILE);
46
+ try {
47
+ const content = await Bun.file(filePath).text();
48
+ return JSON.parse(content);
49
+ }
50
+ catch (error) {
51
+ if (isNotFound(error)) {
52
+ return null;
53
+ }
54
+ throw error;
55
+ }
56
+ };
57
+ // ---------------------------------------------------------------------------
58
+ // Surface Lock
59
+ // ---------------------------------------------------------------------------
60
+ /**
61
+ * Write a hash to `<dir>/surface.lock` as a single line.
62
+ *
63
+ * Creates the directory if it doesn't exist. Returns the file path.
64
+ */
65
+ export const writeSurfaceLock = async (hash, options) => {
66
+ const dir = resolveDir(options);
67
+ await ensureDir(dir);
68
+ const filePath = join(dir, SURFACE_LOCK_FILE);
69
+ await Bun.write(filePath, `${hash}\n`);
70
+ return filePath;
71
+ };
72
+ /**
73
+ * Read the hash from `<dir>/surface.lock`.
74
+ *
75
+ * Returns `null` if the file doesn't exist.
76
+ */
77
+ export const readSurfaceLock = async (options) => {
78
+ const dir = resolveDir(options);
79
+ const filePath = join(dir, SURFACE_LOCK_FILE);
80
+ try {
81
+ const content = await Bun.file(filePath).text();
82
+ return content.trim();
83
+ }
84
+ catch (error) {
85
+ if (isNotFound(error)) {
86
+ return null;
87
+ }
88
+ throw error;
89
+ }
90
+ };
91
+ //# sourceMappingURL=io.js.map
package/dist/io.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"io.js","sourceRoot":"","sources":["../src/io.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Types for surface maps, diffing, and lock files.
3
+ */
4
+ /** A JSON Schema object produced by zodToJsonSchema. */
5
+ export type JsonSchema = Readonly<Record<string, unknown>>;
6
+ export interface SurfaceMapEntry {
7
+ readonly id: string;
8
+ readonly kind: 'trail' | 'hike' | 'event';
9
+ readonly surfaces: readonly string[];
10
+ readonly input?: JsonSchema | undefined;
11
+ readonly output?: JsonSchema | undefined;
12
+ readonly readOnly?: boolean | undefined;
13
+ readonly destructive?: boolean | undefined;
14
+ readonly idempotent?: boolean | undefined;
15
+ readonly deprecated?: boolean | undefined;
16
+ readonly replacedBy?: string | undefined;
17
+ readonly follows?: readonly string[] | undefined;
18
+ readonly detours?: Readonly<Record<string, readonly string[]>> | undefined;
19
+ readonly exampleCount: number;
20
+ readonly description?: string | undefined;
21
+ }
22
+ export interface SurfaceMap {
23
+ readonly version: string;
24
+ readonly generatedAt: string;
25
+ readonly entries: readonly SurfaceMapEntry[];
26
+ }
27
+ export interface DiffEntry {
28
+ readonly id: string;
29
+ readonly kind: 'trail' | 'hike' | 'event';
30
+ readonly change: 'added' | 'removed' | 'modified';
31
+ readonly severity: 'info' | 'warning' | 'breaking';
32
+ readonly details: readonly string[];
33
+ }
34
+ export interface DiffResult {
35
+ readonly entries: readonly DiffEntry[];
36
+ readonly breaking: readonly DiffEntry[];
37
+ readonly warnings: readonly DiffEntry[];
38
+ readonly info: readonly DiffEntry[];
39
+ readonly hasBreaking: boolean;
40
+ }
41
+ export interface WriteOptions {
42
+ /** Directory to write to. Defaults to ".trails/" */
43
+ readonly dir?: string | undefined;
44
+ }
45
+ export interface ReadOptions {
46
+ /** Directory to read from. Defaults to ".trails/" */
47
+ readonly dir?: string | undefined;
48
+ }
49
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAMH,wDAAwD;AACxD,MAAM,MAAM,UAAU,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;AAM3D,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,GAAG,OAAO,CAAC;IAC1C,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC,QAAQ,CAAC,KAAK,CAAC,EAAE,UAAU,GAAG,SAAS,CAAC;IACxC,QAAQ,CAAC,MAAM,CAAC,EAAE,UAAU,GAAG,SAAS,CAAC;IACzC,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACxC,QAAQ,CAAC,WAAW,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC3C,QAAQ,CAAC,UAAU,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC1C,QAAQ,CAAC,UAAU,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC1C,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACzC,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,CAAC;IACjD,QAAQ,CAAC,OAAO,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC,CAAC,GAAG,SAAS,CAAC;IAC3E,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC3C;AAED,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,OAAO,EAAE,SAAS,eAAe,EAAE,CAAC;CAC9C;AAMD,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,GAAG,OAAO,CAAC;IAC1C,QAAQ,CAAC,MAAM,EAAE,OAAO,GAAG,SAAS,GAAG,UAAU,CAAC;IAClD,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,GAAG,UAAU,CAAC;IACnD,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;CACrC;AAED,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,OAAO,EAAE,SAAS,SAAS,EAAE,CAAC;IACvC,QAAQ,CAAC,QAAQ,EAAE,SAAS,SAAS,EAAE,CAAC;IACxC,QAAQ,CAAC,QAAQ,EAAE,SAAS,SAAS,EAAE,CAAC;IACxC,QAAQ,CAAC,IAAI,EAAE,SAAS,SAAS,EAAE,CAAC;IACpC,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;CAC/B;AAMD,MAAM,WAAW,YAAY;IAC3B,oDAAoD;IACpD,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACnC;AAED,MAAM,WAAW,WAAW;IAC1B,qDAAqD;IACrD,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACnC"}
package/dist/types.js ADDED
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Types for surface maps, diffing, and lock files.
3
+ */
4
+ export {};
5
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG"}
package/package.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "@ontrails/schema",
3
+ "version": "1.0.0-beta.0",
4
+ "type": "module",
5
+ "exports": {
6
+ ".": "./src/index.ts",
7
+ "./package.json": "./package.json"
8
+ },
9
+ "scripts": {
10
+ "build": "tsc -b",
11
+ "test": "bun test",
12
+ "typecheck": "tsc --noEmit",
13
+ "lint": "oxlint ./src",
14
+ "clean": "rm -rf dist *.tsbuildinfo"
15
+ },
16
+ "peerDependencies": {
17
+ "@ontrails/core": "workspace:*",
18
+ "zod": "catalog:"
19
+ }
20
+ }
@@ -0,0 +1,333 @@
1
+ import { describe, test, expect } from 'bun:test';
2
+
3
+ import { diffSurfaceMaps } from '../diff.js';
4
+ import type { SurfaceMap, SurfaceMapEntry } from '../types.js';
5
+
6
+ // ---------------------------------------------------------------------------
7
+ // Helpers
8
+ // ---------------------------------------------------------------------------
9
+
10
+ const entry = (
11
+ overrides: Partial<SurfaceMapEntry> & { id: string }
12
+ ): SurfaceMapEntry => ({
13
+ exampleCount: 0,
14
+ kind: 'trail',
15
+ surfaces: [],
16
+ ...overrides,
17
+ });
18
+
19
+ const surfaceMap = (entries: SurfaceMapEntry[]): SurfaceMap => ({
20
+ entries,
21
+ generatedAt: new Date().toISOString(),
22
+ version: '1.0',
23
+ });
24
+
25
+ // ---------------------------------------------------------------------------
26
+ // Tests
27
+ // ---------------------------------------------------------------------------
28
+
29
+ describe('diffSurfaceMaps', () => {
30
+ describe('top-level changes', () => {
31
+ test('empty diff for identical maps', () => {
32
+ const e = entry({ id: 'user.create' });
33
+ const result = diffSurfaceMaps(surfaceMap([e]), surfaceMap([e]));
34
+
35
+ expect(result.entries).toHaveLength(0);
36
+ expect(result.hasBreaking).toBe(false);
37
+ });
38
+
39
+ test('added trail detected as info', () => {
40
+ const prev = surfaceMap([]);
41
+ const curr = surfaceMap([entry({ id: 'user.create' })]);
42
+ const result = diffSurfaceMaps(prev, curr);
43
+
44
+ expect(result.entries).toHaveLength(1);
45
+ expect(result.entries[0]?.change).toBe('added');
46
+ expect(result.entries[0]?.severity).toBe('info');
47
+ expect(result.info).toHaveLength(1);
48
+ });
49
+
50
+ test('removed trail detected as breaking', () => {
51
+ const prev = surfaceMap([entry({ id: 'user.delete' })]);
52
+ const curr = surfaceMap([]);
53
+ const result = diffSurfaceMaps(prev, curr);
54
+
55
+ expect(result.entries).toHaveLength(1);
56
+ expect(result.entries[0]?.change).toBe('removed');
57
+ expect(result.entries[0]?.severity).toBe('breaking');
58
+ expect(result.hasBreaking).toBe(true);
59
+ });
60
+
61
+ test('DiffResult.hasBreaking is true when any breaking entries exist', () => {
62
+ const prev = surfaceMap([entry({ id: 'user.delete' })]);
63
+ const curr = surfaceMap([]);
64
+ const result = diffSurfaceMaps(prev, curr);
65
+
66
+ expect(result.hasBreaking).toBe(true);
67
+ expect(result.breaking.length).toBeGreaterThan(0);
68
+ });
69
+ });
70
+
71
+ describe('schema changes', () => {
72
+ test('required input field added classified as breaking', () => {
73
+ const prev = surfaceMap([
74
+ entry({
75
+ id: 'user.create',
76
+ input: {
77
+ properties: { name: { type: 'string' } },
78
+ required: ['name'],
79
+ type: 'object',
80
+ },
81
+ }),
82
+ ]);
83
+ const curr = surfaceMap([
84
+ entry({
85
+ id: 'user.create',
86
+ input: {
87
+ properties: {
88
+ name: { type: 'string' },
89
+ type: { type: 'string' },
90
+ },
91
+ required: ['name', 'type'],
92
+ type: 'object',
93
+ },
94
+ }),
95
+ ]);
96
+ const result = diffSurfaceMaps(prev, curr);
97
+
98
+ expect(result.hasBreaking).toBe(true);
99
+ expect(result.breaking).toHaveLength(1);
100
+ const [breakingEntry] = result.breaking;
101
+ expect(breakingEntry).toBeDefined();
102
+ expect(
103
+ breakingEntry?.details.some((d) =>
104
+ d.includes('Required input field "type" added')
105
+ )
106
+ ).toBe(true);
107
+ });
108
+
109
+ test('optional input field added classified as info', () => {
110
+ const prev = surfaceMap([
111
+ entry({
112
+ id: 'user.create',
113
+ input: {
114
+ properties: { name: { type: 'string' } },
115
+ required: ['name'],
116
+ type: 'object',
117
+ },
118
+ }),
119
+ ]);
120
+ const curr = surfaceMap([
121
+ entry({
122
+ id: 'user.create',
123
+ input: {
124
+ properties: {
125
+ filter: { type: 'string' },
126
+ name: { type: 'string' },
127
+ },
128
+ required: ['name'],
129
+ type: 'object',
130
+ },
131
+ }),
132
+ ]);
133
+ const result = diffSurfaceMaps(prev, curr);
134
+
135
+ expect(result.info).toHaveLength(1);
136
+ const [infoEntry] = result.info;
137
+ expect(infoEntry).toBeDefined();
138
+ expect(
139
+ infoEntry?.details.some((d) =>
140
+ d.includes('Optional input field "filter" added')
141
+ )
142
+ ).toBe(true);
143
+ });
144
+
145
+ test('output field removed classified as breaking', () => {
146
+ const prev = surfaceMap([
147
+ entry({
148
+ id: 'user.get',
149
+ output: {
150
+ properties: {
151
+ id: { type: 'string' },
152
+ name: { type: 'string' },
153
+ },
154
+ type: 'object',
155
+ },
156
+ }),
157
+ ]);
158
+ const curr = surfaceMap([
159
+ entry({
160
+ id: 'user.get',
161
+ output: {
162
+ properties: {
163
+ id: { type: 'string' },
164
+ },
165
+ type: 'object',
166
+ },
167
+ }),
168
+ ]);
169
+ const result = diffSurfaceMaps(prev, curr);
170
+
171
+ expect(result.hasBreaking).toBe(true);
172
+ expect(
173
+ result.breaking[0]?.details.some((d) =>
174
+ d.includes('Output field "name" removed')
175
+ )
176
+ ).toBe(true);
177
+ });
178
+
179
+ test('output field type changed classified as breaking', () => {
180
+ const prev = surfaceMap([
181
+ entry({
182
+ id: 'user.get',
183
+ output: {
184
+ properties: { count: { type: 'number' } },
185
+ type: 'object',
186
+ },
187
+ }),
188
+ ]);
189
+ const curr = surfaceMap([
190
+ entry({
191
+ id: 'user.get',
192
+ output: {
193
+ properties: { count: { type: 'string' } },
194
+ type: 'object',
195
+ },
196
+ }),
197
+ ]);
198
+ const result = diffSurfaceMaps(prev, curr);
199
+
200
+ expect(result.hasBreaking).toBe(true);
201
+ expect(
202
+ result.breaking[0]?.details.some((d) =>
203
+ d.includes('Output field "count" type changed: number -> string')
204
+ )
205
+ ).toBe(true);
206
+ });
207
+ });
208
+
209
+ describe('metadata and surfaces', () => {
210
+ test('surface removed classified as breaking', () => {
211
+ const prev = surfaceMap([
212
+ entry({ id: 'user.list', surfaces: ['cli', 'mcp'] }),
213
+ ]);
214
+ const curr = surfaceMap([entry({ id: 'user.list', surfaces: ['mcp'] })]);
215
+ const result = diffSurfaceMaps(prev, curr);
216
+
217
+ expect(result.hasBreaking).toBe(true);
218
+ expect(
219
+ result.breaking[0]?.details.some((d) =>
220
+ d.includes('Surface "cli" removed')
221
+ )
222
+ ).toBe(true);
223
+ });
224
+
225
+ test('safety marker changed classified as warning', () => {
226
+ const prev = surfaceMap([entry({ id: 'data.wipe', readOnly: true })]);
227
+ const curr = surfaceMap([entry({ id: 'data.wipe', readOnly: false })]);
228
+ const result = diffSurfaceMaps(prev, curr);
229
+
230
+ expect(result.warnings).toHaveLength(1);
231
+ expect(
232
+ result.warnings[0]?.details.some((d) => d.includes('readOnly changed'))
233
+ ).toBe(true);
234
+ });
235
+
236
+ test('description change classified as info', () => {
237
+ const prev = surfaceMap([
238
+ entry({ description: 'Get a user', id: 'user.get' }),
239
+ ]);
240
+ const curr = surfaceMap([
241
+ entry({ description: 'Fetch a user by ID', id: 'user.get' }),
242
+ ]);
243
+ const result = diffSurfaceMaps(prev, curr);
244
+
245
+ expect(result.info).toHaveLength(1);
246
+ expect(
247
+ result.info[0]?.details.some((d) => d.includes('Description updated'))
248
+ ).toBe(true);
249
+ });
250
+
251
+ test('deprecation added classified as warning', () => {
252
+ const prev = surfaceMap([entry({ id: 'entity.list' })]);
253
+ const curr = surfaceMap([
254
+ entry({
255
+ deprecated: true,
256
+ id: 'entity.list',
257
+ replacedBy: 'entity.show',
258
+ }),
259
+ ]);
260
+ const result = diffSurfaceMaps(prev, curr);
261
+
262
+ expect(result.warnings).toHaveLength(1);
263
+ expect(
264
+ result.warnings[0]?.details.some((d) =>
265
+ d.includes('Deprecated (replaced by entity.show)')
266
+ )
267
+ ).toBe(true);
268
+ });
269
+
270
+ test('follows changed produces warning', () => {
271
+ const prev = surfaceMap([
272
+ entry({
273
+ follows: ['user.get', 'user.lookup'],
274
+ id: 'user.update',
275
+ kind: 'hike',
276
+ }),
277
+ ]);
278
+ const curr = surfaceMap([
279
+ entry({
280
+ follows: ['user.get', 'user.search'],
281
+ id: 'user.update',
282
+ kind: 'hike',
283
+ }),
284
+ ]);
285
+ const result = diffSurfaceMaps(prev, curr);
286
+
287
+ expect(result.warnings).toHaveLength(1);
288
+ const followsDetail = result.warnings[0]?.details.find((d) =>
289
+ d.includes('Follows changed')
290
+ );
291
+ expect(followsDetail).toBeDefined();
292
+ expect(followsDetail).toContain('search');
293
+ expect(followsDetail).toContain('lookup');
294
+ });
295
+ });
296
+
297
+ describe('severity partitioning', () => {
298
+ test('DiffResult partitions correctly into breaking, warnings, info', () => {
299
+ const prev = surfaceMap([
300
+ entry({
301
+ description: 'old',
302
+ id: 'a.trail',
303
+ output: {
304
+ properties: { removed: { type: 'string' } },
305
+ type: 'object',
306
+ },
307
+ readOnly: true,
308
+ }),
309
+ ]);
310
+ const curr = surfaceMap([
311
+ entry({
312
+ description: 'new',
313
+ id: 'a.trail',
314
+ output: {
315
+ properties: {},
316
+ type: 'object',
317
+ },
318
+ readOnly: false,
319
+ }),
320
+ entry({ id: 'b.trail' }),
321
+ ]);
322
+ const result = diffSurfaceMaps(prev, curr);
323
+
324
+ expect(result.entries.length).toBeGreaterThanOrEqual(2);
325
+
326
+ const modifiedEntry = result.entries.find((e) => e.id === 'a.trail');
327
+ expect(modifiedEntry?.severity).toBe('breaking');
328
+
329
+ const addedEntry = result.entries.find((e) => e.id === 'b.trail');
330
+ expect(addedEntry?.severity).toBe('info');
331
+ });
332
+ });
333
+ });