@deepseek-ai/dsh-app-boot 0.2.2-alpha.3 → 0.2.2-rc.2
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/README.i18n.yaml +4 -4
- package/README.md +4 -2
- package/README.zh.md +4 -2
- package/lib/index.js +37 -17
- package/lib/types/bundle-platform.d.ts +4 -2
- package/lib/types/bundle-platform.d.ts.map +1 -0
- package/lib/types/bundle-platform.js.map +1 -0
- package/lib/types/compatibility-preflight.d.ts.map +1 -0
- package/lib/types/compatibility-preflight.js.map +1 -0
- package/lib/types/config-schema/collect.d.ts.map +1 -0
- package/lib/types/config-schema/collect.js.map +1 -0
- package/lib/types/config-schema/document.d.ts.map +1 -0
- package/lib/types/config-schema/document.js.map +1 -0
- package/lib/types/config-schema/index.d.ts.map +1 -0
- package/lib/types/config-schema/index.js.map +1 -0
- package/lib/types/config-schema/native.d.ts.map +1 -0
- package/lib/types/config-schema/native.js.map +1 -0
- package/lib/types/config-schema/pattern.d.ts.map +1 -0
- package/lib/types/config-schema/pattern.js.map +1 -0
- package/lib/types/config-schema/projector.d.ts.map +1 -0
- package/lib/types/config-schema/projector.js.map +1 -0
- package/lib/types/config-schema/types.d.ts.map +1 -0
- package/lib/types/config-schema/types.js.map +1 -0
- package/lib/types/index.d.ts +1 -1
- package/lib/types/index.d.ts.map +1 -0
- package/lib/types/index.js.map +1 -0
- package/lib/types/package-meta.d.ts.map +1 -0
- package/lib/types/package-meta.js.map +1 -0
- package/lib/types/plugin-compatibility.d.ts.map +1 -0
- package/lib/types/plugin-compatibility.js.map +1 -0
- package/lib/types/profile-compatibility.d.ts.map +1 -0
- package/lib/types/profile-compatibility.js.map +1 -0
- package/lib/types/profile-context.d.ts.map +1 -0
- package/lib/types/profile-context.js.map +1 -0
- package/lib/types/profile-plugins.d.ts.map +1 -0
- package/lib/types/profile-plugins.js.map +1 -0
- package/lib/types/profile-resolution/legacy-links.d.ts.map +1 -0
- package/lib/types/profile-resolution/legacy-links.js.map +1 -0
- package/lib/types/profile-resolution/resolver.d.ts.map +1 -0
- package/lib/types/profile-resolution/resolver.js.map +1 -0
- package/lib/types/profile-resolution/service.d.ts.map +1 -0
- package/lib/types/profile-resolution/service.js.map +1 -0
- package/lib/types/profile-resolution/worker-bootstrap.d.ts.map +1 -0
- package/lib/types/profile-resolution/worker-bootstrap.js.map +1 -0
- package/lib/types/profile-sanitize.d.ts.map +1 -0
- package/lib/types/profile-sanitize.js.map +1 -0
- package/lib/types/profile.d.ts.map +1 -0
- package/lib/types/profile.js.map +1 -0
- package/lib/types/startup-diagnostics.d.ts.map +1 -0
- package/lib/types/startup-diagnostics.js.map +1 -0
- package/package.json +25 -21
- package/src/bundle-platform.ts +35 -0
- package/src/compatibility-preflight.ts +187 -0
- package/src/config-schema/collect.ts +249 -0
- package/src/config-schema/document.ts +169 -0
- package/src/config-schema/index.ts +36 -0
- package/src/config-schema/native.ts +16 -0
- package/src/config-schema/pattern.ts +62 -0
- package/src/config-schema/projector.ts +455 -0
- package/src/config-schema/types.ts +95 -0
- package/src/index.ts +1286 -0
- package/src/package-meta.ts +200 -0
- package/src/plugin-compatibility.ts +117 -0
- package/src/profile-compatibility.ts +141 -0
- package/src/profile-context.ts +85 -0
- package/src/profile-plugins.ts +133 -0
- package/src/profile-resolution/legacy-links.ts +33 -0
- package/src/profile-resolution/resolver.ts +1072 -0
- package/src/profile-resolution/service.ts +148 -0
- package/src/profile-resolution/worker-bootstrap.ts +12 -0
- package/src/profile-sanitize.ts +34 -0
- package/src/profile.ts +762 -0
- package/src/startup-diagnostics.ts +104 -0
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
/** Project native Config input constraints without executing their validators or transform callbacks. */
|
|
2
|
+
|
|
3
|
+
import type { ConfigJsonSchema, ConfigJsonSchemaObject, NativeConfigSchema } from './types.ts'
|
|
4
|
+
import { isNativeConfigSchema } from './native.ts'
|
|
5
|
+
import { createPatternCheck } from './pattern.ts'
|
|
6
|
+
|
|
7
|
+
/** Definition that projected value positions reference as `#/$defs/loaderExpression`; an enclosing document must define it. */
|
|
8
|
+
export const LOADER_EXPRESSION_SCHEMA: ConfigJsonSchemaObject = {
|
|
9
|
+
type: 'object', properties: { __jsExpr: { type: 'string' } }, required: ['__jsExpr'],
|
|
10
|
+
description: 'Inert representation of a YAML !!js scalar from the Cordis entry-list parser. Its result is evaluated and validated only at runtime. Extra marker-object fields are ignored by interpolation.',
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** One Config projection; unknown omission behavior is explicit rather than an invented default. */
|
|
14
|
+
export interface ConfigProjection {
|
|
15
|
+
/** Object-form root: value positions are wrapped in `anyOf` with the loader-expression reference. */
|
|
16
|
+
schema: ConfigJsonSchemaObject
|
|
17
|
+
definitions: Record<string, ConfigJsonSchema>
|
|
18
|
+
acceptsMissing: boolean | 'unknown'
|
|
19
|
+
limitations: string[]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface InputSchema extends ConfigJsonSchemaObject {
|
|
23
|
+
'x-cordis'?: Record<string, unknown>
|
|
24
|
+
properties?: Record<string, InputSchema>
|
|
25
|
+
patternProperties?: Record<string, InputSchema>
|
|
26
|
+
items?: InputSchema | false
|
|
27
|
+
additionalProperties?: InputSchema | false
|
|
28
|
+
propertyNames?: InputSchema
|
|
29
|
+
prefixItems?: InputSchema[]
|
|
30
|
+
anyOf?: InputSchema[]
|
|
31
|
+
not?: InputSchema
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface NodeProjection {
|
|
35
|
+
schema: InputSchema
|
|
36
|
+
acceptsMissing: boolean | 'unknown'
|
|
37
|
+
exact: boolean
|
|
38
|
+
recursive: boolean
|
|
39
|
+
/** Native adaptation, key normalization, or opaque code may affect surrounding validation. */
|
|
40
|
+
effectful: boolean
|
|
41
|
+
/** Native resolution may write adapted values or renamed keys into the input before a later union branch reads it. */
|
|
42
|
+
mutating: boolean
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Copy JSON-compatible schema annotations without silently converting non-JSON defaults. */
|
|
46
|
+
function jsonValue(value: unknown, nullifyUndefined = false): unknown {
|
|
47
|
+
const visit = (item: unknown, active: Set<object>): void => {
|
|
48
|
+
if (nullifyUndefined && item === undefined) return
|
|
49
|
+
if (item === null || typeof item === 'string' || typeof item === 'boolean') return
|
|
50
|
+
if (typeof item === 'number' && Number.isFinite(item)) return
|
|
51
|
+
if (typeof item !== 'object') throw new Error('schema annotation is not JSON-compatible')
|
|
52
|
+
if (active.has(item)) throw new Error('schema annotation contains a cycle')
|
|
53
|
+
if (!Array.isArray(item) && Object.getPrototypeOf(item) !== Object.prototype && Object.getPrototypeOf(item) !== null) {
|
|
54
|
+
throw new Error('schema annotation contains a non-JSON object')
|
|
55
|
+
}
|
|
56
|
+
active.add(item)
|
|
57
|
+
for (const child of Object.values(item)) visit(child, active)
|
|
58
|
+
active.delete(item)
|
|
59
|
+
}
|
|
60
|
+
visit(value, new Set())
|
|
61
|
+
const result: unknown = JSON.parse(JSON.stringify(value,
|
|
62
|
+
(_key: string, item: unknown) => nullifyUndefined && item === undefined ? null : item))
|
|
63
|
+
return result
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Native constant equality treats omitted and null-valued object members alike. */
|
|
67
|
+
function constant(value: unknown, objectComparison: () => void): InputSchema {
|
|
68
|
+
if (Array.isArray(value)) {
|
|
69
|
+
return {
|
|
70
|
+
type: 'array', minItems: value.length, maxItems: value.length, items: false,
|
|
71
|
+
...(value.length ? { prefixItems: Array.from(value, item => constant(item, objectComparison)) } : {}),
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (value !== null && typeof value === 'object') {
|
|
75
|
+
objectComparison()
|
|
76
|
+
const properties = Object.fromEntries(Object.entries(value).map(([key, child]) => [key, constant(child, objectComparison)]))
|
|
77
|
+
const inherited = Object.getOwnPropertyNames(Object.prototype)
|
|
78
|
+
const required = Object.keys(properties).filter(key => !inherited.includes(key) && Reflect.get(value, key) != null)
|
|
79
|
+
const pattern = inherited.map(key => key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|')
|
|
80
|
+
return {
|
|
81
|
+
type: 'object', properties, additionalProperties: { type: 'null' },
|
|
82
|
+
patternProperties: { [`^(?:${pattern})$`]: {} },
|
|
83
|
+
...(required.length ? { required } : {}),
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return { const: value }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Add or remove null acceptance; a type list stays deduplicated because a wrapped projection may already accept null. */
|
|
90
|
+
function nullable(core: InputSchema | boolean, accepts: boolean): InputSchema | boolean {
|
|
91
|
+
if (typeof core === 'boolean') return accepts ? core || { type: 'null' } : core && { not: { type: 'null' } }
|
|
92
|
+
const types = typeof core.type === 'string' ? [core.type] : core.type
|
|
93
|
+
if (types === undefined) return accepts ? { anyOf: [core, { type: 'null' }] } : { ...core, not: { type: 'null' } }
|
|
94
|
+
const [single, ...rest] = accepts ? [...new Set([...types, 'null'])] : types.filter(type => type !== 'null')
|
|
95
|
+
if (single === undefined) return false
|
|
96
|
+
return { ...core, type: rest.length ? [single, ...rest] : single }
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Add expression alternatives only at config VALUE positions, not property names or applicator branches. */
|
|
100
|
+
function expressions(schema: InputSchema, valuePosition = true, enabled = true): InputSchema {
|
|
101
|
+
const result: InputSchema = { ...schema }
|
|
102
|
+
if (schema.properties) {
|
|
103
|
+
result.properties = Object.fromEntries(Object.entries(schema.properties)
|
|
104
|
+
.map(([key, child]) => [key, expressions(child, true, enabled)]))
|
|
105
|
+
}
|
|
106
|
+
for (const key of ['items', 'additionalProperties'] as const) {
|
|
107
|
+
if (schema[key] !== undefined) result[key] = schema[key] === false ? false : expressions(schema[key], true, enabled)
|
|
108
|
+
}
|
|
109
|
+
if (schema.propertyNames) result.propertyNames = expressions(schema.propertyNames, false, false)
|
|
110
|
+
if (schema.prefixItems) result.prefixItems = schema.prefixItems.map(child => expressions(child, true, enabled))
|
|
111
|
+
if (schema.anyOf) result.anyOf = schema.anyOf.map(child => expressions(child, false, enabled))
|
|
112
|
+
if (schema.not !== undefined) result.not = expressions(schema.not, false, false)
|
|
113
|
+
if (!valuePosition || !enabled) return result
|
|
114
|
+
const annotations: InputSchema = {}
|
|
115
|
+
for (const key of ['title', 'description', 'default', '$comment', 'x-cordis']) {
|
|
116
|
+
if (Object.hasOwn(result, key)) {
|
|
117
|
+
annotations[key] = result[key]
|
|
118
|
+
Reflect.deleteProperty(result, key)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return { ...annotations, anyOf: [result, { $ref: '#/$defs/loaderExpression' }] }
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Check native volatile containment without evaluating values or invoking unresolved lazy builders. */
|
|
125
|
+
function validateVolatilePlacement(
|
|
126
|
+
node: NativeConfigSchema, path: string, blocked = false,
|
|
127
|
+
seen = new Map<NativeConfigSchema, Set<boolean>>(),
|
|
128
|
+
): void {
|
|
129
|
+
const states = seen.get(node) ?? new Set<boolean>()
|
|
130
|
+
if (states.has(blocked)) return
|
|
131
|
+
states.add(blocked)
|
|
132
|
+
seen.set(node, states)
|
|
133
|
+
if (node.meta.volatile && blocked) throw new Error(`${path}: volatile fields require a fixed object path without an enclosing volatile field`)
|
|
134
|
+
const nested = blocked || !!node.meta.volatile
|
|
135
|
+
for (const [key, child] of Object.entries(node.dict ?? {})) validateVolatilePlacement(child, `${path}/${key}`, nested, seen)
|
|
136
|
+
if (node.sKey) validateVolatilePlacement(node.sKey, `${path}/keys`, true, seen)
|
|
137
|
+
if (node.inner && (node.type !== 'lazy' || isNativeConfigSchema(node.inner))) {
|
|
138
|
+
validateVolatilePlacement(node.inner, `${path}/inner`, true, seen)
|
|
139
|
+
}
|
|
140
|
+
for (const [index, child] of (node.list ?? []).entries()) validateVolatilePlacement(child, `${path}/${index}`, true, seen)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Create an invocation-local projector. Ajv checks only literal defaults against generated schemas;
|
|
145
|
+
* it does not fill defaults, coerce input, or call native plugin validators. Opaque input or metadata effects
|
|
146
|
+
* widen validation with explicit limitations instead of simulating native mutation.
|
|
147
|
+
* @returns a function projecting one trusted native Config graph into the enclosing document's definitions.
|
|
148
|
+
*/
|
|
149
|
+
export async function createConfigProjector(): Promise<(root: NativeConfigSchema, prefix: string) => ConfigProjection> {
|
|
150
|
+
const { Ajv2020 } = await import('ajv/dist/2020.js')
|
|
151
|
+
const validator = new Ajv2020({ strict: false, logger: false, validateFormats: false, addUsedSchema: false })
|
|
152
|
+
const portablePattern = await createPatternCheck()
|
|
153
|
+
// Native lazy resolution invokes each builder once; later visits through other chains reuse the first result.
|
|
154
|
+
const builderResults = new Map<NativeConfigSchema, unknown>()
|
|
155
|
+
return (root, prefix) => {
|
|
156
|
+
validateVolatilePlacement(root, 'config')
|
|
157
|
+
const completed = new Map<NativeConfigSchema, NodeProjection>()
|
|
158
|
+
const strictNodes = new Map<NativeConfigSchema, NativeConfigSchema>()
|
|
159
|
+
const active = new Set<NativeConfigSchema>()
|
|
160
|
+
const recursiveNames = new Map<NativeConfigSchema, string>()
|
|
161
|
+
const definitions: Record<string, InputSchema> = {}
|
|
162
|
+
const limitations: string[] = []
|
|
163
|
+
const metadataEffects: string[] = []
|
|
164
|
+
const limitation = (path: string, message: string): string => {
|
|
165
|
+
const text = `${path}: ${message}`
|
|
166
|
+
limitations.push(text)
|
|
167
|
+
return text
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const visit = (node: NativeConfigSchema, path: string, strict = false): NodeProjection => {
|
|
171
|
+
let key = node
|
|
172
|
+
if (strict) {
|
|
173
|
+
key = strictNodes.get(node) ?? { ...node }
|
|
174
|
+
strictNodes.set(node, key)
|
|
175
|
+
}
|
|
176
|
+
const cached = completed.get(key)
|
|
177
|
+
if (cached) return cached
|
|
178
|
+
if (active.has(key)) {
|
|
179
|
+
let name = recursiveNames.get(key)
|
|
180
|
+
if (name === undefined) {
|
|
181
|
+
name = `${prefix}Recursive${recursiveNames.size}`
|
|
182
|
+
recursiveNames.set(key, name)
|
|
183
|
+
}
|
|
184
|
+
return {
|
|
185
|
+
schema: { $ref: `#/$defs/${name}` }, acceptsMissing: node.type !== 'lazy' && node.meta.required ? false : 'unknown',
|
|
186
|
+
exact: true, recursive: true, effectful: true, mutating: true,
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
active.add(key)
|
|
190
|
+
let projection: NodeProjection
|
|
191
|
+
if (node.type === 'lazy') {
|
|
192
|
+
let inner = node
|
|
193
|
+
let meta = node.meta.volatile ? { ...node.meta, volatile: false } : node.meta
|
|
194
|
+
const chain = new Set<NativeConfigSchema>()
|
|
195
|
+
let looseProjection: NodeProjection | undefined
|
|
196
|
+
while (inner.type === 'lazy') {
|
|
197
|
+
const cachedInner = isNativeConfigSchema(inner.inner)
|
|
198
|
+
if (meta.loose) {
|
|
199
|
+
const message = limitation(path, 'lazy loose fallback can accept inputs rejected by its inner schema')
|
|
200
|
+
if (!cachedInner) metadataEffects.push(limitation(path, 'unresolved loose lazy metadata propagation may affect shared schemas'))
|
|
201
|
+
looseProjection = {
|
|
202
|
+
schema: { 'x-cordis': { loose: true, limitations: [message] } },
|
|
203
|
+
acceptsMissing: true, exact: false, recursive: false, effectful: true, mutating: false,
|
|
204
|
+
}
|
|
205
|
+
break
|
|
206
|
+
}
|
|
207
|
+
if (chain.has(inner)) throw new Error(`${path}: lazy cycle has no concrete schema`)
|
|
208
|
+
chain.add(inner)
|
|
209
|
+
let next: unknown = cachedInner ? inner.inner : builderResults.get(inner)
|
|
210
|
+
if (next === undefined && typeof inner.builder === 'function') {
|
|
211
|
+
next = Reflect.apply(inner.builder, inner, [])
|
|
212
|
+
builderResults.set(inner, next)
|
|
213
|
+
}
|
|
214
|
+
if (!isNativeConfigSchema(next)) throw new Error(`${path}: lazy schema has no native builder result or resolved inner schema`)
|
|
215
|
+
if (!cachedInner && ['required', 'default', 'min', 'max', 'step', 'pattern', 'loose', 'volatile'].some(key => (
|
|
216
|
+
Object.hasOwn(meta, key) && !Object.hasOwn(next.meta, key)
|
|
217
|
+
&& (['required', 'loose', 'volatile'].includes(key) ? !!Reflect.get(meta, key) : Reflect.get(meta, key) != null)
|
|
218
|
+
))) {
|
|
219
|
+
metadataEffects.push(limitation(path, 'lazy metadata propagation may affect shared schemas; validation requires native execution'))
|
|
220
|
+
}
|
|
221
|
+
// Native lazy resolution merges outer metadata only when it first invokes the builder.
|
|
222
|
+
meta = cachedInner ? next.meta : { ...meta, ...next.meta }
|
|
223
|
+
validateVolatilePlacement({ ...next, meta }, `${path}/lazy`, true)
|
|
224
|
+
inner = next
|
|
225
|
+
}
|
|
226
|
+
projection = looseProjection ?? visit({ ...inner, meta }, `${path}/lazy`, strict)
|
|
227
|
+
if (node.meta.volatile) {
|
|
228
|
+
projection = { ...projection, schema: { ...projection.schema, 'x-cordis': { ...projection.schema['x-cordis'], volatile: true } } }
|
|
229
|
+
}
|
|
230
|
+
} else {
|
|
231
|
+
const ownLimitations: string[] = []
|
|
232
|
+
const inputLimitations: string[] = []
|
|
233
|
+
let unsupported = false
|
|
234
|
+
const inputLimitation = (message: string): void => {
|
|
235
|
+
inputLimitations.push(message)
|
|
236
|
+
ownLimitations.push(limitation(path, message))
|
|
237
|
+
}
|
|
238
|
+
const annotation = (value: unknown, name: string): { ok: true; value: unknown } | { ok: false } => {
|
|
239
|
+
try {
|
|
240
|
+
return { ok: true, value: jsonValue(value) }
|
|
241
|
+
} catch (error) {
|
|
242
|
+
ownLimitations.push(limitation(`${path}/${name}`, `annotation omitted: ${error instanceof Error ? error.message : String(error)}`))
|
|
243
|
+
return { ok: false }
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
const children: NodeProjection[] = []
|
|
247
|
+
const child = (value: NativeConfigSchema | undefined, suffix: string, childStrict = false): NodeProjection => {
|
|
248
|
+
if (value === undefined) throw new Error(`${path}: ${node.type} schema is missing ${suffix}`)
|
|
249
|
+
const result = visit(value, `${path}/${suffix}`, childStrict)
|
|
250
|
+
children.push(result)
|
|
251
|
+
return result
|
|
252
|
+
}
|
|
253
|
+
const meta = { ...node.meta }
|
|
254
|
+
for (const key of ['min', 'max', 'step'] as const) {
|
|
255
|
+
if (meta[key] === undefined || Number.isFinite(meta[key])) continue
|
|
256
|
+
// Native range checks default to [-Infinity, Infinity]; a -Infinity origin still breaks step validation.
|
|
257
|
+
const inert = key === 'max' ? meta.max === Infinity : key === 'min' && meta.min === -Infinity && !meta.step
|
|
258
|
+
if (!inert) inputLimitation(`non-finite ${key} constraint is omitted; native validation is required`)
|
|
259
|
+
Reflect.deleteProperty(meta, key)
|
|
260
|
+
}
|
|
261
|
+
let core: InputSchema | boolean
|
|
262
|
+
let dictionaryValidation: { keySchema: InputSchema; valueSchema: InputSchema } | undefined
|
|
263
|
+
switch (node.type) {
|
|
264
|
+
case 'any': core = true; break
|
|
265
|
+
case 'never': core = false; break
|
|
266
|
+
case 'const': {
|
|
267
|
+
try {
|
|
268
|
+
core = node.value == null ? false : constant(jsonValue(node.value, true), () => {
|
|
269
|
+
inputLimitation('object constant inherited-member comparison requires native validation')
|
|
270
|
+
})
|
|
271
|
+
} catch (error) {
|
|
272
|
+
core = true
|
|
273
|
+
inputLimitation(`constant constraint requires native validation: ${error instanceof Error ? error.message : String(error)}`)
|
|
274
|
+
}
|
|
275
|
+
break
|
|
276
|
+
}
|
|
277
|
+
case 'boolean': core = { type: 'boolean' }; break
|
|
278
|
+
case 'string': {
|
|
279
|
+
core = { type: 'string' }
|
|
280
|
+
if (meta.min !== undefined) {
|
|
281
|
+
if (meta.min <= 1) core.minLength = Math.max(0, Math.ceil(meta.min))
|
|
282
|
+
else inputLimitation('UTF-16 minimum length requires native validation')
|
|
283
|
+
}
|
|
284
|
+
if (meta.max !== undefined) {
|
|
285
|
+
if (meta.max < 0) core = false
|
|
286
|
+
else {
|
|
287
|
+
const maximum = Math.floor(meta.max)
|
|
288
|
+
core.maxLength = maximum
|
|
289
|
+
if (maximum > 0) inputLimitation('UTF-16 maximum length requires native validation')
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
if (meta.pattern && core !== false) {
|
|
293
|
+
if (portablePattern(meta.pattern.source, meta.pattern.flags)) core.pattern = meta.pattern.source
|
|
294
|
+
else inputLimitation('regular-expression syntax or Unicode semantics require native validation')
|
|
295
|
+
}
|
|
296
|
+
break
|
|
297
|
+
}
|
|
298
|
+
case 'number': {
|
|
299
|
+
core = { type: 'number' }
|
|
300
|
+
if (meta.min !== undefined) core.minimum = meta.min
|
|
301
|
+
if (meta.max !== undefined) core.maximum = meta.max
|
|
302
|
+
if (meta.step) {
|
|
303
|
+
const step = Math.abs(meta.step)
|
|
304
|
+
const origin = meta.min ?? 0
|
|
305
|
+
if (!Number.isInteger(step) || origin % step !== 0) {
|
|
306
|
+
inputLimitation('fractional or offset numeric step requires native validation')
|
|
307
|
+
} else if (step === 1) core.type = 'integer'
|
|
308
|
+
else core.multipleOf = step
|
|
309
|
+
}
|
|
310
|
+
break
|
|
311
|
+
}
|
|
312
|
+
case 'object': {
|
|
313
|
+
const required: string[] = []
|
|
314
|
+
const properties = Object.fromEntries(Object.entries(node.dict ?? {}).map(([key, field]) => {
|
|
315
|
+
const value = child(field, key)
|
|
316
|
+
if (value.acceptsMissing === false) required.push(key)
|
|
317
|
+
return [key, value.schema]
|
|
318
|
+
}))
|
|
319
|
+
core = { type: 'object', properties }
|
|
320
|
+
if (required.length) core.required = required
|
|
321
|
+
break
|
|
322
|
+
}
|
|
323
|
+
case 'array': {
|
|
324
|
+
const item = child(node.inner, 'items')
|
|
325
|
+
core = { type: 'array', items: item.schema }
|
|
326
|
+
if (meta.min !== undefined && node.inner?.meta.default == null) core.minItems = Math.max(0, Math.ceil(meta.min))
|
|
327
|
+
if (meta.max !== undefined) {
|
|
328
|
+
if (meta.max < 0) core = false
|
|
329
|
+
else core.maxItems = Math.floor(meta.max)
|
|
330
|
+
}
|
|
331
|
+
break
|
|
332
|
+
}
|
|
333
|
+
case 'dict': {
|
|
334
|
+
const valueSchema = child(node.inner, 'values').schema
|
|
335
|
+
core = { type: 'object', additionalProperties: valueSchema }
|
|
336
|
+
if (node.sKey) {
|
|
337
|
+
const key = child(node.sKey, 'keys')
|
|
338
|
+
const totalString = node.sKey.type === 'string'
|
|
339
|
+
&& node.sKey.meta.min === undefined && node.sKey.meta.max === undefined && node.sKey.meta.pattern === undefined
|
|
340
|
+
if (!totalString && (strict || key.effectful)) {
|
|
341
|
+
dictionaryValidation = { keySchema: key.schema, valueSchema }
|
|
342
|
+
core = { type: 'object' }
|
|
343
|
+
inputLimitation(key.effectful
|
|
344
|
+
? 'dictionary key normalization can overwrite unvalidated values; native validation is required'
|
|
345
|
+
: 'strict dictionary filtering and value validation require native validation')
|
|
346
|
+
} else core.propertyNames = key.schema
|
|
347
|
+
}
|
|
348
|
+
break
|
|
349
|
+
}
|
|
350
|
+
case 'tuple': {
|
|
351
|
+
if (node.list === undefined) throw new Error(`${path}: tuple schema is missing its list`)
|
|
352
|
+
const values = node.list.map((item, index) => child(item, String(index)))
|
|
353
|
+
core = { type: 'array', ...(values.length ? { prefixItems: values.map(item => item.schema) } : {}) }
|
|
354
|
+
const minimum = values.reduce((length, value, index) => value.acceptsMissing === false ? index + 1 : length, 0)
|
|
355
|
+
if (minimum) core.minItems = minimum
|
|
356
|
+
break
|
|
357
|
+
}
|
|
358
|
+
case 'union': {
|
|
359
|
+
if (node.list === undefined) throw new Error(`${path}: union schema is missing its list`)
|
|
360
|
+
const variants = node.list.map((item, index) => child(item, String(index), strict))
|
|
361
|
+
if (variants.slice(0, -1).some(variant => variant.mutating)) {
|
|
362
|
+
core = true
|
|
363
|
+
inputLimitation('earlier union branches may alter later validation inputs; native validation is required')
|
|
364
|
+
} else core = variants.length ? { anyOf: variants.map(variant => variant.schema) } : false
|
|
365
|
+
break
|
|
366
|
+
}
|
|
367
|
+
case 'transform':
|
|
368
|
+
core = child(node.inner, 'input', true).schema
|
|
369
|
+
inputLimitation('transform callback validation and normalization are not executed or projected')
|
|
370
|
+
break
|
|
371
|
+
default:
|
|
372
|
+
core = true
|
|
373
|
+
unsupported = true
|
|
374
|
+
inputLimitation(`native schema type ${node.type} is not statically projected`)
|
|
375
|
+
}
|
|
376
|
+
if (meta.loose) {
|
|
377
|
+
core = true
|
|
378
|
+
inputLimitation('loose validation can replace invalid values with defaults')
|
|
379
|
+
}
|
|
380
|
+
const recursive = children.some(value => value.recursive)
|
|
381
|
+
// Only containers write adapted values or renamed keys back; a bare primitive branch cannot alter the input.
|
|
382
|
+
const container = ['object', 'array', 'dict', 'tuple'].includes(node.type)
|
|
383
|
+
const mutating = unsupported || children.some(value => value.mutating || (container && value.effectful))
|
|
384
|
+
const exact = inputLimitations.length === 0 && children.every(value => value.exact)
|
|
385
|
+
const fallback = meta.default === undefined ? undefined : annotation(meta.default, 'default')
|
|
386
|
+
let acceptsMissing: boolean | 'unknown'
|
|
387
|
+
if (meta.required) acceptsMissing = false
|
|
388
|
+
else if (unsupported) acceptsMissing = 'unknown'
|
|
389
|
+
else if (meta.default == null) acceptsMissing = true
|
|
390
|
+
else if (!fallback?.ok) acceptsMissing = 'unknown'
|
|
391
|
+
else if (recursive) {
|
|
392
|
+
acceptsMissing = 'unknown'
|
|
393
|
+
inputLimitation('recursive default validation cannot be decided statically')
|
|
394
|
+
} else if (!exact) acceptsMissing = 'unknown'
|
|
395
|
+
else acceptsMissing = validator.validate(core, fallback.value)
|
|
396
|
+
const annotations: InputSchema = {}
|
|
397
|
+
if (fallback?.ok) annotations.default = fallback.value
|
|
398
|
+
const native: Record<string, unknown> = {}
|
|
399
|
+
if (typeof meta.description === 'string') annotations.description = meta.description
|
|
400
|
+
else if (meta.description) {
|
|
401
|
+
const descriptions = annotation(meta.description, 'description')
|
|
402
|
+
if (descriptions.ok) {
|
|
403
|
+
native.descriptions = descriptions.value
|
|
404
|
+
const description = meta.description.en ?? meta.description[''] ?? Object.values(meta.description)[0]
|
|
405
|
+
if (typeof description === 'string') annotations.description = description
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
if (dictionaryValidation) native.dictionaryValidation = dictionaryValidation
|
|
409
|
+
const copy = (name: string, value: unknown): void => {
|
|
410
|
+
if (value === undefined) return
|
|
411
|
+
const result = annotation(value, name)
|
|
412
|
+
if (result.ok) native[name] = result.value
|
|
413
|
+
}
|
|
414
|
+
for (const key of ['role', 'extra', 'hidden', 'disabled', 'collapse', 'link', 'comment', 'badges', 'loose', 'volatile'] as const) {
|
|
415
|
+
copy(key, meta[key])
|
|
416
|
+
}
|
|
417
|
+
if (meta.pattern?.flags) copy('patternFlags', meta.pattern.flags)
|
|
418
|
+
if (node.type === 'union') native.branchSelection = 'first-success'
|
|
419
|
+
if (acceptsMissing === 'unknown') native.omissionValidation = 'runtime'
|
|
420
|
+
if (ownLimitations.length) {
|
|
421
|
+
native.type = node.type
|
|
422
|
+
for (const key of ['min', 'max', 'step', 'pattern'] as const) copy(key, meta[key])
|
|
423
|
+
native.limitations = ownLimitations
|
|
424
|
+
}
|
|
425
|
+
if (Object.keys(native).length) annotations['x-cordis'] = native
|
|
426
|
+
const input = nullable(core, acceptsMissing !== false)
|
|
427
|
+
projection = {
|
|
428
|
+
schema: { ...(typeof input === 'boolean' ? input ? {} : { not: {} } : input), ...annotations },
|
|
429
|
+
acceptsMissing,
|
|
430
|
+
exact: exact && acceptsMissing !== 'unknown',
|
|
431
|
+
recursive,
|
|
432
|
+
effectful: unsupported || node.type === 'transform' || !!meta.loose || children.some(child => child.effectful),
|
|
433
|
+
mutating,
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
active.delete(key)
|
|
437
|
+
const name = recursiveNames.get(key)
|
|
438
|
+
if (name !== undefined) {
|
|
439
|
+
definitions[name] = projection.schema
|
|
440
|
+
projection = { ...projection, schema: { $ref: `#/$defs/${name}` } }
|
|
441
|
+
}
|
|
442
|
+
completed.set(key, projection)
|
|
443
|
+
return projection
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const result = visit(root, 'config')
|
|
447
|
+
const schema = expressions(result.schema)
|
|
448
|
+
return {
|
|
449
|
+
schema: metadataEffects.length ? { anyOf: [schema, {}] } : schema,
|
|
450
|
+
definitions: Object.fromEntries(Object.entries(definitions).map(([key, value]) => [key, expressions(value, false)])),
|
|
451
|
+
acceptsMissing: metadataEffects.length ? 'unknown' : result.acceptsMissing,
|
|
452
|
+
limitations: [...new Set(limitations)],
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/** JSON Schema output and native graph inputs for boot-free configuration inspection. */
|
|
2
|
+
|
|
3
|
+
/** JSON Schema 2020-12, including annotations that standard validators may ignore. */
|
|
4
|
+
export type ConfigJsonSchema = boolean | ConfigJsonSchemaObject
|
|
5
|
+
|
|
6
|
+
/** Object-form JSON Schema; vocabulary extensions are annotations, not executable validators. */
|
|
7
|
+
export interface ConfigJsonSchemaObject {
|
|
8
|
+
[keyword: string]: unknown
|
|
9
|
+
$schema?: string
|
|
10
|
+
$ref?: string
|
|
11
|
+
$defs?: Record<string, ConfigJsonSchema>
|
|
12
|
+
type?: string | string[]
|
|
13
|
+
properties?: Record<string, ConfigJsonSchema>
|
|
14
|
+
required?: string[]
|
|
15
|
+
items?: ConfigJsonSchema
|
|
16
|
+
prefixItems?: ConfigJsonSchema[]
|
|
17
|
+
additionalProperties?: ConfigJsonSchema
|
|
18
|
+
propertyNames?: ConfigJsonSchema
|
|
19
|
+
anyOf?: ConfigJsonSchema[]
|
|
20
|
+
allOf?: ConfigJsonSchema[]
|
|
21
|
+
not?: ConfigJsonSchema
|
|
22
|
+
if?: ConfigJsonSchema
|
|
23
|
+
then?: ConfigJsonSchema
|
|
24
|
+
default?: unknown
|
|
25
|
+
description?: string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** A collection or projection diagnostic, without independently collected config values. */
|
|
29
|
+
export interface ConfigSchemaDiagnostic {
|
|
30
|
+
level: 'warning' | 'error'
|
|
31
|
+
/** Structural position in the discovered entry tree, when applicable. */
|
|
32
|
+
path?: string
|
|
33
|
+
message: string
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** One declared entry and its generated Config reference; include paths describe discovery, not root JSON pointers. */
|
|
37
|
+
export interface ConfigSchemaEntry {
|
|
38
|
+
path: string
|
|
39
|
+
id?: string
|
|
40
|
+
/** Omitted when a malformed row has no literal plugin name. */
|
|
41
|
+
name?: string
|
|
42
|
+
status: 'schema' | 'partial' | 'absent' | 'unsupported' | 'error'
|
|
43
|
+
configRef?: string
|
|
44
|
+
/** Native Loader carrier, whose config is not interpolated as an ordinary plugin config. */
|
|
45
|
+
tree?: 'group' | 'include'
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** JSON Schema for a composed entry list; `$defs.patchList` describes profile overlays. */
|
|
49
|
+
export interface ConfigSchemaDump extends ConfigJsonSchemaObject {
|
|
50
|
+
$schema: 'https://json-schema.org/draft/2020-12/schema'
|
|
51
|
+
$defs: Record<string, ConfigJsonSchema>
|
|
52
|
+
'x-cordis': {
|
|
53
|
+
profile: string
|
|
54
|
+
/** False for error diagnostics, partial/unsupported/error entries, or ambiguous plugin-name schemas; includes disabled declarations. */
|
|
55
|
+
complete: boolean
|
|
56
|
+
entries: ConfigSchemaEntry[]
|
|
57
|
+
diagnostics: ConfigSchemaDiagnostic[]
|
|
58
|
+
patchSchema: '#/$defs/patchList'
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Native Schemastery graph protocol after its identity marker has been checked. */
|
|
63
|
+
export interface NativeConfigSchema {
|
|
64
|
+
type: string
|
|
65
|
+
meta: {
|
|
66
|
+
role?: string
|
|
67
|
+
extra?: unknown
|
|
68
|
+
hidden?: boolean
|
|
69
|
+
disabled?: boolean
|
|
70
|
+
collapse?: boolean
|
|
71
|
+
link?: string
|
|
72
|
+
comment?: string
|
|
73
|
+
badges?: { text: string; type: string }[]
|
|
74
|
+
required?: boolean
|
|
75
|
+
volatile?: boolean
|
|
76
|
+
default?: unknown
|
|
77
|
+
min?: number
|
|
78
|
+
max?: number
|
|
79
|
+
step?: number
|
|
80
|
+
pattern?: { source: string; flags?: string }
|
|
81
|
+
description?: string | Record<string, string>
|
|
82
|
+
loose?: boolean
|
|
83
|
+
}
|
|
84
|
+
dict?: Record<string, NativeConfigSchema>
|
|
85
|
+
inner?: NativeConfigSchema
|
|
86
|
+
sKey?: NativeConfigSchema
|
|
87
|
+
list?: NativeConfigSchema[]
|
|
88
|
+
value?: unknown
|
|
89
|
+
builder?: unknown
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Collected declaration used only while constructing the JSON Schema document. */
|
|
93
|
+
export interface CollectedConfigEntry extends ConfigSchemaEntry {
|
|
94
|
+
native?: NativeConfigSchema
|
|
95
|
+
}
|