entangled-worlds-orbiters-shared 2.17.0 → 3.0.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.
@@ -1,164 +0,0 @@
1
- /**
2
- * Tiny JSON-Schema-subset validator (shared machinery, TypeScript).
3
- *
4
- * The ONE validator both registries use (supersedes the duplicated
5
- * `studio/capabilities/schema.js` + `core/edit/capabilities/schema.ts`). Deliberately NOT a
6
- * dependency (no zod/ajv) — the app is bundle-weight sensitive and the house style is
7
- * lightweight guard clauses. Each capability carries an `inputSchema` literal in this subset;
8
- * the SAME literal is what the future agent layer hands the model as a tool definition, so
9
- * there is no second schema language to keep in sync.
10
- *
11
- * Supported keywords (a pragmatic subset of JSON Schema draft-07):
12
- * type 'object' | 'array' | 'string' | 'number' | 'integer' | 'boolean' | 'null'
13
- * (or an array of those — value matches if it satisfies any)
14
- * properties per-key subschemas (objects)
15
- * required array of keys that must be present and not undefined (objects)
16
- * items subschema applied to every array element
17
- * enum value must be one of the listed values
18
- * const value must deep-equal the given value
19
- * nullable when true, null always passes regardless of type
20
- * minimum / maximum numbers
21
- * minLength / maxLength strings
22
- * minItems / maxItems arrays
23
- * pattern string regex source (tested with RegExp)
24
- *
25
- * A schema with no `type` accepts any value (still honouring enum/const/properties if set).
26
- * Unknown keywords are ignored — forward-compatible, never throws on shape it doesn't know.
27
- */
28
-
29
- import { ValidationError, type SchemaIssue } from './errors.js'
30
-
31
- export type JsonType = 'object' | 'array' | 'string' | 'number' | 'integer' | 'boolean' | 'null'
32
-
33
- export interface JsonSchema {
34
- type?: JsonType | JsonType[]
35
- properties?: Record<string, JsonSchema>
36
- required?: string[]
37
- items?: JsonSchema
38
- enum?: unknown[]
39
- const?: unknown
40
- nullable?: boolean
41
- minimum?: number
42
- maximum?: number
43
- minLength?: number
44
- maxLength?: number
45
- minItems?: number
46
- maxItems?: number
47
- pattern?: string
48
- }
49
-
50
- const TYPE_CHECKS: Record<JsonType, (v: unknown) => boolean> = {
51
- object: (v) => v !== null && typeof v === 'object' && !Array.isArray(v),
52
- array: (v) => Array.isArray(v),
53
- string: (v) => typeof v === 'string',
54
- number: (v) => typeof v === 'number' && Number.isFinite(v),
55
- integer: (v) => typeof v === 'number' && Number.isInteger(v),
56
- boolean: (v) => typeof v === 'boolean',
57
- null: (v) => v === null,
58
- }
59
-
60
- function deepEqual(a: unknown, b: unknown): boolean {
61
- if (a === b) return true
62
- if (typeof a !== typeof b || a === null || b === null || typeof a !== 'object') return false
63
- const ka = Object.keys(a as object)
64
- const kb = Object.keys(b as object)
65
- if (ka.length !== kb.length) return false
66
- return ka.every((k) => deepEqual((a as Record<string, unknown>)[k], (b as Record<string, unknown>)[k]))
67
- }
68
-
69
- function checkType(schema: JsonSchema, value: unknown): boolean {
70
- if (schema.type == null) return true
71
- const types = Array.isArray(schema.type) ? schema.type : [schema.type]
72
- return types.some((t) => (TYPE_CHECKS[t] ? TYPE_CHECKS[t](value) : true))
73
- }
74
-
75
- function describeType(value: unknown): string {
76
- if (value === null) return 'null'
77
- if (Array.isArray(value)) return 'array'
78
- return typeof value
79
- }
80
-
81
- function validateNode(schema: JsonSchema | undefined, value: unknown, path: string, issues: SchemaIssue[]): void {
82
- if (!schema || typeof schema !== 'object') return
83
- const at = path || '(root)'
84
-
85
- if (schema.nullable && value === null) return
86
-
87
- if (!checkType(schema, value)) {
88
- const want = Array.isArray(schema.type) ? schema.type.join('|') : schema.type
89
- issues.push({ path: at, message: `expected type ${want}, got ${describeType(value)}` })
90
- return // type is the gate; downstream checks would be noise
91
- }
92
-
93
- if ('const' in schema && !deepEqual(value, schema.const)) {
94
- issues.push({ path: at, message: `must equal ${JSON.stringify(schema.const)}` })
95
- }
96
-
97
- if (Array.isArray(schema.enum) && !schema.enum.some((e) => deepEqual(e, value))) {
98
- issues.push({ path: at, message: `must be one of ${JSON.stringify(schema.enum)}` })
99
- }
100
-
101
- if (typeof value === 'number') {
102
- if (typeof schema.minimum === 'number' && value < schema.minimum) {
103
- issues.push({ path: at, message: `must be >= ${schema.minimum}` })
104
- }
105
- if (typeof schema.maximum === 'number' && value > schema.maximum) {
106
- issues.push({ path: at, message: `must be <= ${schema.maximum}` })
107
- }
108
- }
109
-
110
- if (typeof value === 'string') {
111
- if (typeof schema.minLength === 'number' && value.length < schema.minLength) {
112
- issues.push({ path: at, message: `must be at least ${schema.minLength} chars` })
113
- }
114
- if (typeof schema.maxLength === 'number' && value.length > schema.maxLength) {
115
- issues.push({ path: at, message: `must be at most ${schema.maxLength} chars` })
116
- }
117
- if (typeof schema.pattern === 'string' && !new RegExp(schema.pattern).test(value)) {
118
- issues.push({ path: at, message: `must match /${schema.pattern}/` })
119
- }
120
- }
121
-
122
- if (Array.isArray(value)) {
123
- if (typeof schema.minItems === 'number' && value.length < schema.minItems) {
124
- issues.push({ path: at, message: `must have at least ${schema.minItems} items` })
125
- }
126
- if (typeof schema.maxItems === 'number' && value.length > schema.maxItems) {
127
- issues.push({ path: at, message: `must have at most ${schema.maxItems} items` })
128
- }
129
- if (schema.items) {
130
- value.forEach((el, i) => validateNode(schema.items, el, `${path}[${i}]`, issues))
131
- }
132
- }
133
-
134
- if (TYPE_CHECKS.object(value)) {
135
- const obj = value as Record<string, unknown>
136
- if (Array.isArray(schema.required)) {
137
- for (const key of schema.required) {
138
- if (obj[key] === undefined) issues.push({ path: path ? `${path}.${key}` : key, message: 'is required' })
139
- }
140
- }
141
- if (schema.properties) {
142
- for (const [key, sub] of Object.entries(schema.properties)) {
143
- if (obj[key] !== undefined) validateNode(sub, obj[key], path ? `${path}.${key}` : key, issues)
144
- }
145
- }
146
- }
147
- }
148
-
149
- /** Returns { ok, issues }. Never throws. */
150
- export function validate(schema: JsonSchema, value: unknown): { ok: boolean; issues: SchemaIssue[] } {
151
- const issues: SchemaIssue[] = []
152
- validateNode(schema ?? {}, value, '', issues)
153
- return { ok: issues.length === 0, issues }
154
- }
155
-
156
- /** Throws ValidationError when invalid; returns the value otherwise. Used by the registry. */
157
- export function assertValid<T>(schema: JsonSchema, value: T, capability: string | null = null): T {
158
- const { ok, issues } = validate(schema, value)
159
- if (!ok) {
160
- const summary = issues.map((i) => `${i.path}: ${i.message}`).join('; ')
161
- throw new ValidationError(`Invalid input${capability ? ` for "${capability}"` : ''}: ${summary}`, { capability, issues })
162
- }
163
- return value
164
- }