@deepseek-ai/dsh-app-boot 0.2.2-rc.1 → 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.
Files changed (73) hide show
  1. package/README.i18n.yaml +4 -4
  2. package/README.md +4 -2
  3. package/README.zh.md +4 -2
  4. package/lib/index.js +37 -17
  5. package/lib/types/bundle-platform.d.ts +4 -2
  6. package/lib/types/bundle-platform.d.ts.map +1 -0
  7. package/lib/types/bundle-platform.js.map +1 -0
  8. package/lib/types/compatibility-preflight.d.ts.map +1 -0
  9. package/lib/types/compatibility-preflight.js.map +1 -0
  10. package/lib/types/config-schema/collect.d.ts.map +1 -0
  11. package/lib/types/config-schema/collect.js.map +1 -0
  12. package/lib/types/config-schema/document.d.ts.map +1 -0
  13. package/lib/types/config-schema/document.js.map +1 -0
  14. package/lib/types/config-schema/index.d.ts.map +1 -0
  15. package/lib/types/config-schema/index.js.map +1 -0
  16. package/lib/types/config-schema/native.d.ts.map +1 -0
  17. package/lib/types/config-schema/native.js.map +1 -0
  18. package/lib/types/config-schema/pattern.d.ts.map +1 -0
  19. package/lib/types/config-schema/pattern.js.map +1 -0
  20. package/lib/types/config-schema/projector.d.ts.map +1 -0
  21. package/lib/types/config-schema/projector.js.map +1 -0
  22. package/lib/types/config-schema/types.d.ts.map +1 -0
  23. package/lib/types/config-schema/types.js.map +1 -0
  24. package/lib/types/index.d.ts +1 -1
  25. package/lib/types/index.d.ts.map +1 -0
  26. package/lib/types/index.js.map +1 -0
  27. package/lib/types/package-meta.d.ts.map +1 -0
  28. package/lib/types/package-meta.js.map +1 -0
  29. package/lib/types/plugin-compatibility.d.ts.map +1 -0
  30. package/lib/types/plugin-compatibility.js.map +1 -0
  31. package/lib/types/profile-compatibility.d.ts.map +1 -0
  32. package/lib/types/profile-compatibility.js.map +1 -0
  33. package/lib/types/profile-context.d.ts.map +1 -0
  34. package/lib/types/profile-context.js.map +1 -0
  35. package/lib/types/profile-plugins.d.ts.map +1 -0
  36. package/lib/types/profile-plugins.js.map +1 -0
  37. package/lib/types/profile-resolution/legacy-links.d.ts.map +1 -0
  38. package/lib/types/profile-resolution/legacy-links.js.map +1 -0
  39. package/lib/types/profile-resolution/resolver.d.ts.map +1 -0
  40. package/lib/types/profile-resolution/resolver.js.map +1 -0
  41. package/lib/types/profile-resolution/service.d.ts.map +1 -0
  42. package/lib/types/profile-resolution/service.js.map +1 -0
  43. package/lib/types/profile-resolution/worker-bootstrap.d.ts.map +1 -0
  44. package/lib/types/profile-resolution/worker-bootstrap.js.map +1 -0
  45. package/lib/types/profile-sanitize.d.ts.map +1 -0
  46. package/lib/types/profile-sanitize.js.map +1 -0
  47. package/lib/types/profile.d.ts.map +1 -0
  48. package/lib/types/profile.js.map +1 -0
  49. package/lib/types/startup-diagnostics.d.ts.map +1 -0
  50. package/lib/types/startup-diagnostics.js.map +1 -0
  51. package/package.json +15 -11
  52. package/src/bundle-platform.ts +35 -0
  53. package/src/compatibility-preflight.ts +187 -0
  54. package/src/config-schema/collect.ts +249 -0
  55. package/src/config-schema/document.ts +169 -0
  56. package/src/config-schema/index.ts +36 -0
  57. package/src/config-schema/native.ts +16 -0
  58. package/src/config-schema/pattern.ts +62 -0
  59. package/src/config-schema/projector.ts +455 -0
  60. package/src/config-schema/types.ts +95 -0
  61. package/src/index.ts +1286 -0
  62. package/src/package-meta.ts +200 -0
  63. package/src/plugin-compatibility.ts +117 -0
  64. package/src/profile-compatibility.ts +141 -0
  65. package/src/profile-context.ts +85 -0
  66. package/src/profile-plugins.ts +133 -0
  67. package/src/profile-resolution/legacy-links.ts +33 -0
  68. package/src/profile-resolution/resolver.ts +1072 -0
  69. package/src/profile-resolution/service.ts +148 -0
  70. package/src/profile-resolution/worker-bootstrap.ts +12 -0
  71. package/src/profile-sanitize.ts +34 -0
  72. package/src/profile.ts +762 -0
  73. 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
+ }