@@ -18,19 +18,19 @@ import { isObject, resolveJsonPointer } from './schema-refs';
1818
1919const packageDir = dirname ( fileURLToPath ( import . meta. url ) ) ;
2020export const bundledSchemasDir = join ( packageDir , '..' , '..' , 'schemas' ) ;
21- /** Parsed JSON schema object — always a plain object (never a boolean schema). */
22- type JsonSchemaObject = Record < string , unknown > ;
2321
24- export function readRawSchema ( schemaPath : string ) : JsonSchemaObject {
25- return JSON5 . parse ( readFileSync ( resolve ( schemaPath ) , 'utf-8' ) ) as JsonSchemaObject ;
22+ const validateMetadataContract = new Ajv ( ) . compile ( OST_TOOLS_METADATA_SCHEMA ) ;
23+
24+ export function readRawSchema ( schemaPath : string ) : AnySchemaObject {
25+ return JSON5 . parse ( readFileSync ( resolve ( schemaPath ) , 'utf-8' ) ) as AnySchemaObject ;
2626}
2727
2828/**
2929 * Build a registry of all schemas in the given directory, keyed by $id.
3030 * Only loads "partial" schemas (starting with _) and an optional target file.
3131 */
32- function buildSchemaRegistry ( dir : string , targetFile ?: string ) : Map < string , JsonSchemaObject > {
33- const registry = new Map < string , JsonSchemaObject > ( ) ;
32+ function buildSchemaRegistry ( dir : string , targetFile ?: string ) : Map < string , AnySchemaObject > {
33+ const registry = new Map < string , AnySchemaObject > ( ) ;
3434 if ( ! existsSync ( dir ) ) return registry ;
3535 for ( const file of readdirSync ( dir ) ) {
3636 if ( ! file . endsWith ( '.json' ) ) continue ;
@@ -50,12 +50,12 @@ function buildSchemaRegistry(dir: string, targetFile?: string): Map<string, Json
5050 * - Layer 2: schema's own dir (partials + target file) — overrides layer 1
5151 * Does not throw on $id collision; layer 2 silently wins.
5252 */
53- export function buildFullRegistry ( schemaPath : string ) : Map < string , JsonSchemaObject > {
53+ export function buildFullRegistry ( schemaPath : string ) : Map < string , AnySchemaObject > {
5454 const absPath = resolve ( schemaPath ) ;
5555 const targetFile = basename ( absPath ) ;
5656 const targetDir = dirname ( absPath ) ;
5757
58- const registry = new Map < string , JsonSchemaObject > ( ) ;
58+ const registry = new Map < string , AnySchemaObject > ( ) ;
5959
6060 // Layer 1: bundled schemas/ dir (partials only)
6161 for ( const [ id , schema ] of buildSchemaRegistry ( bundledSchemasDir ) ) {
@@ -79,16 +79,16 @@ export function buildFullRegistry(schemaPath: string): Map<string, JsonSchemaObj
7979 return registry ;
8080}
8181
82- function compileValidator ( targetSchema : JsonSchemaObject , registry : Map < string , JsonSchemaObject > ) : ValidateFunction {
82+ function compileValidator ( targetSchema : AnySchemaObject , registry : Map < string , AnySchemaObject > ) : ValidateFunction {
8383 const ajv = new Ajv ( ) ;
8484 ajv . addKeyword ( {
8585 keyword : '$metadata' ,
8686 schemaType : 'object' ,
87- metaSchema : OST_TOOLS_METADATA_SCHEMA as unknown as JsonSchemaObject ,
87+ metaSchema : OST_TOOLS_METADATA_SCHEMA as unknown as AnySchemaObject ,
8888 valid : true ,
8989 errors : false ,
9090 } ) ;
91- const metaSchema = OST_TOOLS_DIALECT_META_SCHEMA as unknown as JsonSchemaObject ;
91+ const metaSchema = OST_TOOLS_DIALECT_META_SCHEMA as unknown as AnySchemaObject ;
9292 ajv . addSchema ( metaSchema , OST_TOOLS_SCHEMA_META_ID ) ;
9393
9494 // Register all except target schema (AJV compiles targetSchema explicitly)
@@ -110,23 +110,31 @@ export function resolveNodeType(type: string, typeAliases: Record<string, string
110110
111111interface MetadataProvider {
112112 schemaId : string ;
113- schema : JsonSchemaObject ;
113+ schema : AnySchemaObject ;
114114 metadata : MetadataContract ;
115115}
116116
117117const RULE_CATEGORIES = new Set < RuleCategory > ( [ 'validation' , 'coherence' , 'workflow' , 'best-practice' ] ) ;
118118const RULE_ALLOWED_KEYS = new Set ( [ 'id' , 'category' , 'description' , 'check' , 'type' , 'scope' , 'override' ] ) ;
119119
120- function readTopLevelMetadata ( schema : JsonSchemaObject ) : MetadataContract | undefined {
120+ function readTopLevelMetadata ( schema : AnySchemaObject ) : MetadataContract | undefined {
121121 const metadata = schema . $metadata ;
122- return isObject ( metadata ) ? ( metadata as MetadataContract ) : undefined ;
122+ if ( ! isObject ( metadata ) ) return undefined ;
123+ if ( ! validateMetadataContract ( metadata ) ) {
124+ const schemaId = typeof schema . $id === 'string' ? schema . $id : '(unknown schema)' ;
125+ const errors =
126+ validateMetadataContract . errors ?. map ( ( e ) => `${ e . instancePath || '(root)' } ${ e . message } ` ) . join ( '; ' ) ??
127+ 'unknown error' ;
128+ throw new Error ( `Invalid $metadata in schema "${ schemaId } ": ${ errors } ` ) ;
129+ }
130+ return metadata as MetadataContract ;
123131}
124132
125133function resolveRefTargetForRule (
126134 ref : string ,
127- currentRootSchema : JsonSchemaObject ,
128- registry : Map < string , JsonSchemaObject > ,
129- ) : { value : unknown ; rootSchema : JsonSchemaObject ; refKey : string } {
135+ currentRootSchema : AnySchemaObject ,
136+ registry : Map < string , AnySchemaObject > ,
137+ ) : { value : unknown ; rootSchema : AnySchemaObject ; refKey : string } {
130138 if ( ref . startsWith ( '#' ) ) {
131139 const pointer = ref . slice ( 1 ) ;
132140 const rootId = typeof currentRootSchema . $id === 'string' ? currentRootSchema . $id : '(root schema)' ;
@@ -176,13 +184,13 @@ function collectExternalRefIdsInOrder(schema: unknown): string[] {
176184}
177185
178186function collectMetadataProviders (
179- rootSchema : JsonSchemaObject ,
180- registry : Map < string , JsonSchemaObject > ,
187+ rootSchema : AnySchemaObject ,
188+ registry : Map < string , AnySchemaObject > ,
181189) : MetadataProvider [ ] {
182190 const providers : MetadataProvider [ ] = [ ] ;
183191 const visitedSchemaIds = new Set < string > ( ) ;
184192
185- const walk = ( schema : JsonSchemaObject ) : void => {
193+ const walk = ( schema : AnySchemaObject ) : void => {
186194 const refs = collectExternalRefIdsInOrder ( schema ) ;
187195 for ( const schemaId of refs ) {
188196 if ( visitedSchemaIds . has ( schemaId ) ) continue ;
@@ -239,7 +247,7 @@ function isMetadataRule(value: unknown): value is Rule {
239247function resolveRuleEntries (
240248 ruleEntry : RuleEntry ,
241249 provider : MetadataProvider ,
242- registry : Map < string , JsonSchemaObject > ,
250+ registry : Map < string , AnySchemaObject > ,
243251 stack : Set < string > ,
244252) : Rule [ ] {
245253 if ( isMetadataRule ( ruleEntry ) ) {
@@ -313,7 +321,7 @@ function areRulesEquivalent(left: Rule, right: Rule): boolean {
313321 return isDeepStrictEqual ( normalizeRule ( left ) , normalizeRule ( right ) ) ;
314322}
315323
316- function extractMetadata ( schema : JsonSchemaObject , registry : Map < string , JsonSchemaObject > ) : SchemaMetadata {
324+ function extractMetadata ( schema : AnySchemaObject , registry : Map < string , AnySchemaObject > ) : SchemaMetadata {
317325 const metadataProviders = collectMetadataProviders ( schema , registry ) ;
318326
319327 let hierarchyProvider : string | undefined ;
@@ -420,10 +428,10 @@ export interface LoadedSchema {
420428export function loadSchema ( schemaPath : string ) : LoadedSchema {
421429 const rawSchema = readRawSchema ( schemaPath ) ;
422430 const registry = buildFullRegistry ( schemaPath ) ;
423- const schema : SchemaWithMetadata = { ...rawSchema , metadata : extractMetadata ( rawSchema , registry ) } ;
431+ const schema = { ...rawSchema , metadata : extractMetadata ( rawSchema , registry ) } as unknown as SchemaWithMetadata ;
424432 return {
425433 schema,
426- registry : registry as Map < string , AnySchemaObject > ,
434+ registry,
427435 validator : compileValidator ( rawSchema , registry ) ,
428436 } ;
429437}
0 commit comments