diff --git a/docs/standard-schema.md b/docs/standard-schema.md index 896eab1..615c55b 100644 --- a/docs/standard-schema.md +++ b/docs/standard-schema.md @@ -5,8 +5,8 @@ ```ts import { Product, - ProductSchema, type ProductPropertiesType, + ProductSchema, type ProductType, } from '@okikio/vocab/schema' ``` @@ -24,11 +24,11 @@ The permanent `standard_test.ts` also imports `@standard-schema/spec`; under the The names are related but they solve different problems. -| Contract | Purpose | Generated vocabulary use | -| --- | --- | --- | -| Standard Typed | common metadata and input/output inference | base shape of the generated `~standard` object | -| Standard Schema | runtime validation | `ProductSchema['~standard'].validate(value)` | -| Standard JSON Schema | JSON Schema conversion | `ProductSchema['~standard'].jsonSchema.input(...)` and `.output(...)` | +| Contract | Purpose | Generated vocabulary use | +| -------------------- | ------------------------------------------ | --------------------------------------------------------------------- | +| Standard Typed | common metadata and input/output inference | base shape of the generated `~standard` object | +| Standard Schema | runtime validation | `ProductSchema['~standard'].validate(value)` | +| Standard JSON Schema | JSON Schema conversion | `ProductSchema['~standard'].jsonSchema.input(...)` and `.output(...)` | One generated schema object implements both Standard Schema and Standard JSON Schema: @@ -127,7 +127,7 @@ A schema whose generated `types` list contains several class names requires all The current generated runtime intentionally uses a small structural range model: ```ts -export type RangeKind = +export type RangeKindType = | 'string' | 'number' | 'boolean' diff --git a/docs/vocabulary-generation.md b/docs/vocabulary-generation.md index b7f392d..93c2fc6 100644 --- a/docs/vocabulary-generation.md +++ b/docs/vocabulary-generation.md @@ -31,7 +31,7 @@ serialized RDF / dataset / store @okikio/rdf/ontology | v - @okikio/vocab.read() + @okikio/vocab.inspect() | v naming + collision planning @@ -124,12 +124,12 @@ Generated vocabularies use direct imports: ```ts import { + name, + offers, Product, - ProductSchema, type ProductPropertiesType, + ProductSchema, type ProductType, - name, - offers, } from '@okikio/vocab/schema' ``` @@ -213,7 +213,7 @@ Package-local runtime benchmark: packages/vocab/compile_bench.ts ``` -It measures a deterministic ontology `read -> name plan -> emit` workload. +It measures a deterministic ontology `inspect -> name plan -> emit` workload. Cross-process TypeScript benchmark: diff --git a/fixtures/vocab/schema-bootstrap.json b/fixtures/vocab/schema-bootstrap.json new file mode 100644 index 0000000..3411383 --- /dev/null +++ b/fixtures/vocab/schema-bootstrap.json @@ -0,0 +1,147 @@ +{ + "sources": [ + { + "id": "schema.org-bootstrap", + "iri": "https://schema.org/", + "version": "bootstrap" + } + ], + "classes": [ + { + "iri": "https://schema.org/Offer", + "names": ["Offer"], + "labels": [{ "value": "Offer" }], + "comments": [ + { + "value": "A compact bootstrap class used to validate the generator and direct-import API." + } + ], + "superClasses": ["https://schema.org/Intangible"], + "equivalentClasses": [], + "deprecated": false + }, + { + "iri": "https://schema.org/Product", + "names": ["Product"], + "labels": [{ "value": "Product" }], + "comments": [ + { + "value": "A compact bootstrap class used to validate the generator and direct-import API." + } + ], + "superClasses": ["https://schema.org/Thing"], + "equivalentClasses": [], + "deprecated": false + }, + { + "iri": "https://schema.org/Thing", + "names": ["Thing"], + "labels": [{ "value": "Thing" }], + "comments": [ + { "value": "The most generic type of item in this bootstrap vocabulary slice." } + ], + "superClasses": [], + "equivalentClasses": [], + "deprecated": false + }, + { + "iri": "https://schema.org/Intangible", + "names": ["Intangible"], + "labels": [{ "value": "Intangible" }], + "comments": [], + "superClasses": ["https://schema.org/Thing"], + "equivalentClasses": [], + "deprecated": false + } + ], + "properties": [ + { + "iri": "https://schema.org/description", + "names": ["description"], + "labels": [{ "value": "description" }], + "comments": [], + "domains": ["https://schema.org/Thing"], + "ranges": ["https://schema.org/Text"], + "superProperties": [], + "equivalentProperties": [], + "inverseOf": [], + "functional": false, + "deprecated": false + }, + { + "iri": "https://schema.org/name", + "names": ["name"], + "labels": [{ "value": "name" }], + "comments": [], + "domains": ["https://schema.org/Thing"], + "ranges": ["https://schema.org/Text"], + "superProperties": [], + "equivalentProperties": [], + "inverseOf": [], + "functional": false, + "deprecated": false + }, + { + "iri": "https://schema.org/offers", + "names": ["offers"], + "labels": [{ "value": "offers" }], + "comments": [], + "domains": ["https://schema.org/Product"], + "ranges": ["https://schema.org/Offer"], + "superProperties": [], + "equivalentProperties": [], + "inverseOf": [], + "functional": false, + "deprecated": false + }, + { + "iri": "https://schema.org/price", + "names": ["price"], + "labels": [{ "value": "price" }], + "comments": [], + "domains": ["https://schema.org/Offer"], + "ranges": ["https://schema.org/Number", "https://schema.org/Text"], + "superProperties": [], + "equivalentProperties": [], + "inverseOf": [], + "functional": false, + "deprecated": false + }, + { + "iri": "https://schema.org/priceCurrency", + "names": ["priceCurrency"], + "labels": [{ "value": "priceCurrency" }], + "comments": [], + "domains": ["https://schema.org/Offer"], + "ranges": ["https://schema.org/Text"], + "superProperties": [], + "equivalentProperties": [], + "inverseOf": [], + "functional": false, + "deprecated": false + }, + { + "iri": "https://schema.org/sku", + "names": ["sku"], + "labels": [{ "value": "sku" }], + "comments": [], + "domains": ["https://schema.org/Product"], + "ranges": ["https://schema.org/Text"], + "superProperties": [], + "equivalentProperties": [], + "inverseOf": [], + "functional": false, + "deprecated": false + } + ], + "datatypes": [ + "https://schema.org/Boolean", + "https://schema.org/Number", + "https://schema.org/Text", + "https://schema.org/URL" + ], + "assertions": [], + "diagnostics": [ + "This checked-in module is a bootstrap slice for generator/runtime validation, not the complete Schema.org release." + ] +} diff --git a/packages/vocab/README.md b/packages/vocab/README.md index 52185f2..c7500db 100644 --- a/packages/vocab/README.md +++ b/packages/vocab/README.md @@ -7,12 +7,7 @@ RDF ontology compiler and generated vocabulary runtime. Generated vocabularies expose direct RDF terms, TypeScript types, and Standard Schema validators: ```ts -import { - Product, - ProductSchema, - type ProductType, - name, -} from '@okikio/vocab/schema' +import { name, Product, ProductSchema, type ProductType } from '@okikio/vocab/schema' ``` Generated terms are `@okikio/rdf` named nodes, so they work directly with datasets and SPARQL builders. @@ -20,7 +15,7 @@ Generated terms are `@okikio/rdf` named nodes, so they work directly with datase ```ts import * as rdf from '@okikio/rdf' import * as sparql from '@okikio/sparql' -import { Product, name } from '@okikio/vocab/schema' +import { name, Product } from '@okikio/vocab/schema' const query = sparql.select(['?product', '?name']).where( sparql.triple('?product', rdf.namedNode(rdf.RDF.type), Product), diff --git a/packages/vocab/compile.ts b/packages/vocab/compile.ts index 8a02602..cdcc779 100644 --- a/packages/vocab/compile.ts +++ b/packages/vocab/compile.ts @@ -2,12 +2,12 @@ import type { OntologySourceType } from '@okikio/rdf/ontology' import { emit, type EmitOptionsType, type EmitResultType } from './emit.ts' -import { read, type ReadOptions } from './read.ts' +import { inspect, type InspectOptionsType } from './inspect.ts' /** Options for one ontology compilation. */ export interface CompileOptionsType extends EmitOptionsType { - /** Ontology-reading limits and vocabulary-specific relationship aliases. */ - readonly read?: ReadOptions + /** Ontology-inspection limits and vocabulary-specific relationship aliases. */ + readonly inspect?: InspectOptionsType } /** @@ -17,11 +17,23 @@ export interface CompileOptionsType extends EmitOptionsType { * JSON-LD, RDF/XML, a triplestore cursor, or any future source can participate * as long as it exposes RDF quads. This is the reusable replacement for the old * format-specific `ttl-to-ts` script. + * + * @example + * ```ts + * import * as vocab from '@okikio/vocab' + * + * const result = await vocab.compile([{ id: 'example', quads }], { + * vocabulary: 'Example', + * namespace: 'https://example.test/', + * prefix: 'ex', + * }) + * console.log(result.source) + * ``` */ export async function compile( sources: readonly OntologySourceType[], options: CompileOptionsType, ): Promise { - const model = await read(sources, options.read) + const model = await inspect(sources, options.inspect) return emit(model, options) } diff --git a/packages/vocab/compile_test.ts b/packages/vocab/compile_test.ts index 8f35c56..4902a97 100644 --- a/packages/vocab/compile_test.ts +++ b/packages/vocab/compile_test.ts @@ -13,7 +13,9 @@ describe('@okikio/vocab compile', () => { const direct = { id: 'direct', quads: [quad(product, RDF_TYPE, RDFS_CLASS)] } const turtle = { id: 'turtle', - quads: parseTurtle('@prefix rdfs: .\n a rdfs:Class .'), + quads: parseTurtle( + '@prefix rdfs: .\n a rdfs:Class .', + ), } const result = await compile([direct, turtle], { vocabulary: 'Example', diff --git a/packages/vocab/deno.json b/packages/vocab/deno.json index 9a97b5d..1fc177a 100644 --- a/packages/vocab/deno.json +++ b/packages/vocab/deno.json @@ -8,5 +8,12 @@ "./schema": "./schema/mod.ts", "./compile": "./compile.ts", "./standard": "./standard.ts" + }, + "publish": { + "exclude": [ + "**/*_test.ts", + "**/*_bench.ts", + "**/*_property_test.ts" + ] } } diff --git a/packages/vocab/emit.ts b/packages/vocab/emit.ts index 8c076c3..ed205ea 100644 --- a/packages/vocab/emit.ts +++ b/packages/vocab/emit.ts @@ -2,21 +2,28 @@ import type { ManifestType, PropertyType, VocabularyModelType } from './model.ts' import { createManifest } from './manifest.ts' -import { plan, type NamePlanType } from './name.ts' -import type { RangeKind } from './runtime.ts' +import { type NamePlanType, plan } from './name.ts' +import type { RangeKindType } from './runtime.ts' /** TypeScript vocabulary emission options. */ export interface EmitOptionsType { + /** Human-readable vocabulary name used in generated module documentation and manifest metadata. */ readonly vocabulary: string + /** Base vocabulary namespace IRI used by every generated term. */ readonly namespace: string + /** Preferred generated identifier prefix when a vocabulary needs one. */ readonly prefix: string + /** Module specifier used by generated source for RDF runtime imports. */ readonly rdfImport?: string + /** Module specifier used by generated source for vocabulary runtime imports. */ readonly runtimeImport?: string } /** Complete deterministic vocabulary generation result. */ export interface EmitResultType { + /** Complete generated TypeScript module source. */ readonly source: string + /** Deterministic manifest that maps source IRIs to emitted TypeScript symbols. */ readonly manifest: ManifestType } @@ -41,7 +48,11 @@ export function emit(model: VocabularyModelType, options: EmitOptionsType): Emit writer.line(' */') writer.line('') writer.line(`import { namedNode } from ${quote(rdfImport)}`) - writer.line(`import { createSchema, type IdReferenceType, type NodeType, type ValueType } from ${quote(runtimeImport)}`) + writer.line( + `import { createSchema, type IdReferenceType, type NodeType, type ValueType } from ${ + quote(runtimeImport) + }`, + ) writer.line('') writer.line('/** Base IRI used by every generated vocabulary term in this module. */') writer.line(`export const namespace = ${quote(options.namespace)}`) @@ -95,12 +106,16 @@ function emitProperties(writer: Writer, model: VocabularyModelType, names: NameP .filter((name): name is string => name !== undefined) .map((name) => `${name}PropertiesType`) const heritage = supers.length > 0 ? ` extends ${supers.join(', ')}` : '' - writer.line(`/** JSON-LD properties directly available to ${className}, including inherited interfaces. */`) + writer.line( + `/** JSON-LD properties directly available to ${className}, including inherited interfaces. */`, + ) writer.line(`export interface ${className}PropertiesType${heritage} {`) writer.indent(() => { for (const property of byDomain.get(value.iri) ?? []) { const propertyName = names.properties.get(property.iri)! - writer.line(`readonly ${propertyKey(propertyName)}?: ValueType<${propertyType(property, names)}>`) + writer.line( + `readonly ${propertyKey(propertyName)}?: ValueType<${propertyType(property, names)}>`, + ) } }) writer.line('}') @@ -122,7 +137,9 @@ function emitClasses(writer: Writer, model: VocabularyModelType, names: NamePlan writer.line(`export const ${name}Schema = createSchema<${name}Type>({`) writer.indent(() => { writer.line(`types: [${quote(name)}],`) - if (parents.length > 0) writer.line(`parents: () => [${parents.map((parent) => `${parent}Schema`).join(', ')}],`) + if (parents.length > 0) { + writer.line(`parents: () => [${parents.map((parent) => `${parent}Schema`).join(', ')}],`) + } const properties = directProperties.get(value.iri) ?? [] if (properties.length > 0) { writer.line('properties: {') @@ -149,7 +166,9 @@ function emitTypeMap(writer: Writer, model: VocabularyModelType, names: NamePlan writer.line(`export type ${name}Type = ${scalarType(iri) ?? 'unknown'}`) } writer.line('') - writer.line('/** Generated class-name to property-interface map used by multi-typed JSON-LD nodes. */') + writer.line( + '/** Generated class-name to property-interface map used by multi-typed JSON-LD nodes. */', + ) writer.line('export interface TypeMapType {') writer.indent(() => { for (const value of model.classes) { @@ -162,14 +181,28 @@ function emitTypeMap(writer: Writer, model: VocabularyModelType, names: NamePlan writer.line('/** Every generated vocabulary class name accepted by multi-type nodes. */') writer.line('export type ClassNameType = keyof TypeMapType') writer.line('/** Resolves one generated class name to its property interface. */') - writer.line('type PropertiesForType = Type extends keyof TypeMapType ? TypeMapType[Type] : never') - writer.line('/** Converts the selected class-property union into one intersection for multi-typed nodes. */') - writer.line('type UnionToIntersection = (Value extends unknown ? (value: Value) => void : never) extends (value: infer Intersection) => void ? Intersection : never') + writer.line( + 'type PropertiesForType = Type extends keyof TypeMapType ? TypeMapType[Type] : never', + ) + writer.line( + '/** Converts the selected class-property union into one intersection for multi-typed nodes. */', + ) + writer.line( + 'type UnionToIntersection = (Value extends unknown ? (value: Value) => void : never) extends (value: infer Intersection) => void ? Intersection : never', + ) writer.line('') - writer.line('/** Intersects the properties contributed by every class on a multi-typed JSON-LD node. */') - writer.line('type MergedPropertiesType = UnionToIntersection> & object') - writer.line('/** JSON-LD node carrying all properties contributed by the selected generated class names. */') - writer.line('export type MultiTypeType = NodeType>') + writer.line( + '/** Intersects the properties contributed by every class on a multi-typed JSON-LD node. */', + ) + writer.line( + 'type MergedPropertiesType = UnionToIntersection> & object', + ) + writer.line( + '/** JSON-LD node carrying all properties contributed by the selected generated class names. */', + ) + writer.line( + 'export type MultiTypeType = NodeType>', + ) } /** Indexes properties by directly declared domain without treating RDFS domain as requiredness. */ @@ -186,7 +219,6 @@ function propertiesByDomain(model: VocabularyModelType): Map() + const kinds = new Set() if (property.ranges.length === 0) kinds.add('unknown') for (const range of property.ranges) { const scalar = scalarType(range) @@ -234,7 +269,10 @@ function rangeLiteral(property: PropertyType): string { /** Writes normalized ontology documentation and deprecation metadata into generated TSDoc. */ function emitDoc( writer: Writer, - comments: readonly { readonly value: string }[], + comments: readonly { + /** One human-readable documentation line emitted before the generated vocabulary symbol. */ + readonly value: string + }[], deprecated: boolean, fallback: string, ): void { @@ -268,7 +306,9 @@ function quote(value: string): string { /** Internal Writer implementation and its owned state. */ class Writer { + /** Generated source lines accumulated in deterministic emission order. */ #lines: string[] = [] + /** Current indentation depth applied when the writer appends a source line. */ #depth = 0 /** Appends one source line at the current indentation depth. */ diff --git a/packages/vocab/emit_test.ts b/packages/vocab/emit_test.ts index 3a8458c..2e46323 100644 --- a/packages/vocab/emit_test.ts +++ b/packages/vocab/emit_test.ts @@ -1,7 +1,7 @@ import { describe, it } from 'node:test' import { expect } from '@std/expect' import { namedNode, quad } from '@okikio/rdf' -import { emit, read } from './mod.ts' +import { emit, inspect } from './mod.ts' const RDF_TYPE = namedNode('http://www.w3.org/1999/02/22-rdf-syntax-ns#type') const RDFS_CLASS = namedNode('http://www.w3.org/2000/01/rdf-schema#Class') @@ -9,8 +9,10 @@ const RDFS_CLASS = namedNode('http://www.w3.org/2000/01/rdf-schema#Class') describe('@okikio/vocab', () => { it('emits deterministic direct class term/type/schema exports', async () => { const product = namedNode('https://schema.org/Product') - const model = await read([{ id: 'schema', quads: [quad(product, RDF_TYPE, RDFS_CLASS)] }]) - const source = emit(model, { vocabulary: 'Schema.org', namespace: 'https://schema.org/', prefix: 'schema' }).source + const model = await inspect([{ id: 'schema', quads: [quad(product, RDF_TYPE, RDFS_CLASS)] }]) + const source = + emit(model, { vocabulary: 'Schema.org', namespace: 'https://schema.org/', prefix: 'schema' }) + .source expect(source.includes('export const Product =')).toBe(true) expect(source.includes('export type ProductType =')).toBe(true) expect(source.includes('export const ProductSchema =')).toBe(true) diff --git a/packages/vocab/read.ts b/packages/vocab/inspect.ts similarity index 73% rename from packages/vocab/read.ts rename to packages/vocab/inspect.ts index 91bfb62..116a18f 100644 --- a/packages/vocab/read.ts +++ b/packages/vocab/inspect.ts @@ -9,9 +9,9 @@ */ import { - read as readOntology, + inspect as inspectOntology, + type InspectOptionsType as OntologyInspectOptionsType, type OntologySourceType, - type ReadOptions as OntologyReadOptions, } from '@okikio/rdf/ontology' import type { ClassType, PropertyType, VocabularyModelType } from './model.ts' @@ -26,24 +26,35 @@ const SCHEMA_HTTP_RANGE = 'http://schema.org/rangeIncludes' export type { OntologySourceType } -/** Additional vocabulary conventions accepted by the generator reader. */ -export interface ReadOptions extends Omit { +/** Additional vocabulary conventions accepted by the vocabulary inspector. */ +export interface InspectOptionsType + extends Omit { + /** Additional predicate IRIs interpreted as ontology property-domain declarations. */ readonly domainPredicates?: readonly string[] + /** Additional predicate IRIs interpreted as ontology property-range declarations. */ readonly rangePredicates?: readonly string[] } /** - * Reads ontology sources into the deterministic vocabulary compiler model. + * Inspects ontology sources into the deterministic vocabulary compiler model. * * Schema.org domain/range aliases are enabled because generated Schema.org is a * first-class consumer. Callers can add equivalent vocabulary-specific aliases * without teaching the generic RDF ontology package about those vocabularies. + * + * @example + * ```ts + * import * as vocab from '@okikio/vocab' + * + * const model = await vocab.inspect([{ id: 'example', quads }]) + * console.log(model.classes.length) + * ``` */ -export async function read( +export async function inspect( sources: readonly OntologySourceType[], - options: ReadOptions = {}, + options: InspectOptionsType = {}, ): Promise { - const model = await readOntology(sources, { + const model = await inspectOntology(sources, { domainPredicates: [SCHEMA_DOMAIN, SCHEMA_HTTP_DOMAIN, ...(options.domainPredicates ?? [])], rangePredicates: [SCHEMA_RANGE, SCHEMA_HTTP_RANGE, ...(options.rangePredicates ?? [])], ...(options.maxQuads === undefined ? {} : { maxQuads: options.maxQuads }), @@ -66,12 +77,16 @@ function toClass(value: Parameters[0]): ClassType { } /** Adds the deterministic source symbol candidate used by vocabulary naming. */ -function classValue(value: Awaited>['classes'][number]): ClassType { +function classValue( + value: Awaited>['classes'][number], +): ClassType { return { ...value, names: [localName(value.iri)] } } /** Projects a generic ontology property into the vocabulary compiler model and retains functional semantics. */ -function toProperty(value: Awaited>['properties'][number]): PropertyType { +function toProperty( + value: Awaited>['properties'][number], +): PropertyType { return { ...value, names: [localName(value.iri)], diff --git a/packages/vocab/read_test.ts b/packages/vocab/inspect_test.ts similarity index 85% rename from packages/vocab/read_test.ts rename to packages/vocab/inspect_test.ts index 33f4f75..af20ef6 100644 --- a/packages/vocab/read_test.ts +++ b/packages/vocab/inspect_test.ts @@ -1,10 +1,10 @@ import { describe, it } from 'node:test' import { expect } from '@std/expect' import { parse } from '@okikio/rdf/turtle' -import { read } from './read.ts' +import { inspect } from './inspect.ts' describe('@okikio/vocab ontology adapter', () => { - it('adds Schema.org domain/range aliases without changing the generic ontology reader', async () => { + it('adds Schema.org domain/range aliases without changing the generic ontology inspector', async () => { const source = ` @prefix rdf: . @prefix rdfs: . @@ -13,7 +13,7 @@ describe('@okikio/vocab ontology adapter', () => { ex:Product a rdfs:Class . ex:name a rdf:Property ; schema:domainIncludes ex:Product ; schema:rangeIncludes schema:Text . ` - const model = await read([{ id: 'schema-style', quads: parse(source) }]) + const model = await inspect([{ id: 'schema-style', quads: parse(source) }]) expect(model.properties[0]?.domains).toEqual(['https://example.com/Product']) expect(model.properties[0]?.ranges).toEqual(['https://schema.org/Text']) expect(model.properties[0]?.names).toEqual(['name']) @@ -27,7 +27,7 @@ describe('@okikio/vocab ontology adapter', () => { ex:Thing a rdfs:Class . ex:value a rdf:Property ; ex:appliesTo ex:Thing . ` - const model = await read([{ id: 'custom', quads: parse(source) }], { + const model = await inspect([{ id: 'custom', quads: parse(source) }], { domainPredicates: ['https://example.com/appliesTo'], }) expect(model.properties[0]?.domains).toEqual(['https://example.com/Thing']) diff --git a/packages/vocab/mod.ts b/packages/vocab/mod.ts index 1755943..5b8281f 100644 --- a/packages/vocab/mod.ts +++ b/packages/vocab/mod.ts @@ -5,6 +5,6 @@ export * from './emit.ts' export * from './manifest.ts' export * from './model.ts' export * from './name.ts' -export * from './read.ts' +export * from './inspect.ts' export * from './runtime.ts' export * from './standard.ts' diff --git a/packages/vocab/model.ts b/packages/vocab/model.ts index 3863cdd..8657d8b 100644 --- a/packages/vocab/model.ts +++ b/packages/vocab/model.ts @@ -13,14 +13,17 @@ export type LabelType = OntologyTextType /** One ontology class with generator-facing symbol candidates. */ export interface ClassType extends OntologyClassType { + /** Localized names retained for display, documentation, or generated symbols. */ readonly names: readonly string[] } /** One ontology property with generator-facing symbol candidates. */ export interface PropertyType extends Omit { + /** Localized names retained for display, documentation, or generated symbols. */ readonly names: readonly string[] /** Convenience projection used by current emitters. */ readonly functional: boolean + /** OWL property characteristics, such as functional or transitive, retained as normalized identifiers. */ readonly characteristics: OntologyPropertyType['characteristics'] } @@ -32,27 +35,42 @@ export type SourceType = OntologySourceType /** Stable language-neutral ontology model consumed by emitters. */ export interface VocabularyModelType { + /** Source provenance records retained by the normalized ontology or vocabulary model. */ readonly sources: readonly SourceType[] + /** Normalized RDF/OWL class records discovered across the inspected sources. */ readonly classes: readonly ClassType[] + /** Property records or property definitions owned by this model. */ readonly properties: readonly PropertyType[] + /** Datatype IRIs discovered or referenced by the inspected ontology sources. */ readonly datatypes: readonly string[] + /** RDF assertions retained because the current semantic layer does not interpret them further. */ readonly assertions: readonly AssertionType[] + /** Structured diagnostics retained so recoverable source information is not silently discarded. */ readonly diagnostics: readonly string[] } /** Generated symbol mapping retained for collision review and provenance. */ export interface SymbolType { + /** Vocabulary IRI represented by this generated symbol mapping. */ readonly iri: string + /** Vocabulary declaration category used to select the generated TypeScript representation. */ readonly kind: 'class' | 'property' | 'datatype' + /** Generated TypeScript export name selected for this vocabulary IRI. */ readonly name: string } /** Machine-readable output manifest. */ export interface ManifestType { + /** Manifest format revision used to validate generated vocabulary metadata. */ readonly version: 1 + /** Generator identity and version recorded for reproducible vocabulary output. */ readonly generator: string + /** Human-readable vocabulary name recorded in the generated manifest. */ readonly vocabulary: string + /** Source provenance records retained by the normalized ontology or vocabulary model. */ readonly sources: readonly SourceType[] + /** Generated source symbols indexed by their vocabulary IRIs. */ readonly symbols: readonly SymbolType[] + /** Structured diagnostics retained so recoverable source information is not silently discarded. */ readonly diagnostics: readonly string[] } diff --git a/packages/vocab/name.ts b/packages/vocab/name.ts index c234ec4..541bd31 100644 --- a/packages/vocab/name.ts +++ b/packages/vocab/name.ts @@ -10,19 +10,65 @@ export interface NameOptionsType { /** Planned generated symbols for classes, properties, and datatypes. */ export interface NamePlanType { + /** Normalized RDF/OWL class records discovered across the inspected sources. */ readonly classes: ReadonlyMap + /** Property records or property definitions owned by this model. */ readonly properties: ReadonlyMap + /** Datatype IRIs discovered or referenced by the inspected ontology sources. */ readonly datatypes: ReadonlyMap + /** Generated source symbols indexed by their vocabulary IRIs. */ readonly symbols: readonly SymbolType[] } /** ECMAScript/TypeScript words that cannot be emitted unchanged as binding identifiers. */ const RESERVED = new Set([ - 'await', 'break', 'case', 'catch', 'class', 'const', 'continue', 'debugger', 'default', - 'delete', 'do', 'else', 'enum', 'export', 'extends', 'false', 'finally', 'for', 'function', - 'if', 'implements', 'import', 'in', 'instanceof', 'interface', 'let', 'new', 'null', - 'package', 'private', 'protected', 'public', 'return', 'static', 'super', 'switch', 'this', - 'throw', 'true', 'try', 'typeof', 'undefined', 'var', 'void', 'while', 'with', 'yield', + 'await', + 'break', + 'case', + 'catch', + 'class', + 'const', + 'continue', + 'debugger', + 'default', + 'delete', + 'do', + 'else', + 'enum', + 'export', + 'extends', + 'false', + 'finally', + 'for', + 'function', + 'if', + 'implements', + 'import', + 'in', + 'instanceof', + 'interface', + 'let', + 'new', + 'null', + 'package', + 'private', + 'protected', + 'public', + 'return', + 'static', + 'super', + 'switch', + 'this', + 'throw', + 'true', + 'try', + 'typeof', + 'undefined', + 'var', + 'void', + 'while', + 'with', + 'yield', ]) /** @@ -45,8 +91,16 @@ export function plan(model: VocabularyModelType, options: NameOptionsType): Name classes.set(value.iri, name) symbols.push({ iri: value.iri, kind: 'class', name }) } - for (const value of [...model.properties].sort((left, right) => left.iri.localeCompare(right.iri))) { - const name = claim(preferred(value.names, value.iri), value.iri, options.prefix, used, 'Property') + for ( + const value of [...model.properties].sort((left, right) => left.iri.localeCompare(right.iri)) + ) { + const name = claim( + preferred(value.names, value.iri), + value.iri, + options.prefix, + used, + 'Property', + ) properties.set(value.iri, name) symbols.push({ iri: value.iri, kind: 'property', name }) } @@ -106,7 +160,9 @@ function safePrefix(value: string): string { /** Converts punctuation-separated source text into a valid PascalCase identifier candidate. */ function pascal(value: string): string { const parts = value.split(/[^A-Za-z0-9_$]+/g).filter(Boolean) - const joined = parts.map((part) => part.length === 0 ? '' : `${part[0]!.toUpperCase()}${part.slice(1)}`).join('') + const joined = parts.map((part) => + part.length === 0 ? '' : `${part[0]!.toUpperCase()}${part.slice(1)}` + ).join('') if (!joined) return 'Term' return /^[A-Za-z_$]/.test(joined) ? joined : `Term${joined}` } diff --git a/packages/vocab/name_test.ts b/packages/vocab/name_test.ts index f7a8870..3e389d7 100644 --- a/packages/vocab/name_test.ts +++ b/packages/vocab/name_test.ts @@ -33,10 +33,13 @@ describe('@okikio/vocab symbol planning', () => { }) it('qualifies reserved and invalid TypeScript identifiers', () => { - const result = plan(model([ - cls('urn:class', 'class'), - cls('urn:bad', 'not-valid!'), - ]), { prefix: 'demo' }) + const result = plan( + model([ + cls('urn:class', 'class'), + cls('urn:bad', 'not-valid!'), + ]), + { prefix: 'demo' }, + ) expect(result.classes.get('urn:class') === 'class').toBe(false) expect(result.classes.get('urn:bad')?.startsWith('Demo')).toBe(true) }) diff --git a/packages/vocab/runtime.ts b/packages/vocab/runtime.ts index f3c3724..57c294d 100644 --- a/packages/vocab/runtime.ts +++ b/packages/vocab/runtime.ts @@ -11,8 +11,8 @@ import type { JsonSchemaOptions, JsonSchemaTarget, - StandardJSONSchemaV1Props, StandardIssue, + StandardJSONSchemaV1Props, StandardResult, StandardSchemaV1Props, } from './standard.ts' @@ -37,12 +37,17 @@ export type { /** Combined validator and JSON Schema converter exposed by generated classes. */ export interface VocabularySchema { - readonly '~standard': StandardSchemaV1Props & StandardJSONSchemaV1Props + /** Standard Schema V1 metadata property consumed by compatible schema tooling. */ + readonly '~standard': + & StandardSchemaV1Props + & StandardJSONSchemaV1Props } /** JSON-LD reference to another node by IRI. */ export interface IdReferenceType { + /** JSON-LD identifier preserved on generated vocabulary node values. */ readonly '@id': string + /** Additional keyed values accepted by this standards-compatible structural record. */ readonly [key: string]: unknown } @@ -52,27 +57,35 @@ export type ValueType = T | readonly T[] /** Open-world generated JSON-LD node. */ export type NodeType = Readonly< Properties & { + /** JSON-LD type discriminator used by the generated vocabulary node. */ readonly '@type': Type + /** JSON-LD node identifier used by the generated vocabulary node. */ readonly '@id'?: string + /** Optional JSON-LD context retained with the generated vocabulary node. */ readonly '@context'?: unknown + /** Retains vocabulary-specific JSON-LD properties not modeled by the standard fields. */ readonly [key: string]: unknown } > /** Runtime range classes that can be represented safely by the structural validator. */ -export type RangeKind = 'string' | 'number' | 'boolean' | 'node' | 'unknown' +export type RangeKindType = 'string' | 'number' | 'boolean' | 'node' | 'unknown' /** Generated runtime schema configuration. */ export interface SchemaConfigType { + /** Named RDF types or generated type names attached to this record. */ readonly types: readonly string[] - readonly properties?: Readonly> + /** Property records or property definitions owned by this model. */ + readonly properties?: Readonly> /** Parent class schemas whose property ranges also apply to this class. */ readonly parents?: () => readonly VocabularySchema[] } /** Internal generated schema metadata retained without copying inherited properties. */ interface SchemaStateType { - readonly properties: Readonly> + /** Property records or property definitions owned by this model. */ + readonly properties: Readonly> + /** Parent schemas composed into this generated schema before local properties are checked. */ readonly parents: () => readonly VocabularySchema[] } @@ -91,7 +104,10 @@ export function createSchema(config: SchemaConfigType): VocabularySchema if (!isRecord(value)) return { issues: [{ message: 'Expected a JSON-LD object.' }] } if (!hasType(value['@type'], config.types)) { - issues.push({ message: `Expected @type to include ${config.types.join(', ')}.`, path: ['@type'] }) + issues.push({ + message: `Expected @type to include ${config.types.join(', ')}.`, + path: ['@type'], + }) } visitProperties(schema, (name, range) => { @@ -101,7 +117,10 @@ export function createSchema(config: SchemaConfigType): VocabularySchema const values = Array.isArray(property) ? property : [property] for (let index = 0; index < values.length; index++) { if (!matches(values[index], kinds)) { - issues.push({ message: `Property '${name}' does not match its generated vocabulary range.`, path: [name, index] }) + issues.push({ + message: `Property '${name}' does not match its generated vocabulary range.`, + path: [name, index], + }) } } }) @@ -162,7 +181,7 @@ export function createSchema(config: SchemaConfigType): VocabularySchema */ function visitProperties( schema: VocabularySchema, - visit: (name: string, range: RangeKind | readonly RangeKind[]) => void, + visit: (name: string, range: RangeKindType | readonly RangeKindType[]) => void, ): void { const schemas = [schema] const seenSchemas = new Set() @@ -196,28 +215,38 @@ function hasType(value: unknown, required: readonly string[]): boolean { } /** Checks one JSON-LD property value against the generated open-world range kinds. */ -function matches(value: unknown, kinds: readonly RangeKind[]): boolean { +function matches(value: unknown, kinds: readonly RangeKindType[]): boolean { if (kinds.includes('unknown')) return true return kinds.some((kind) => { switch (kind) { - case 'string': return typeof value === 'string' - case 'number': return typeof value === 'number' && Number.isFinite(value) - case 'boolean': return typeof value === 'boolean' - case 'node': return typeof value === 'string' || isRecord(value) - case 'unknown': return true + case 'string': + return typeof value === 'string' + case 'number': + return typeof value === 'number' && Number.isFinite(value) + case 'boolean': + return typeof value === 'boolean' + case 'node': + return typeof value === 'string' || isRecord(value) + case 'unknown': + return true } }) } /** Converts generated vocabulary range kinds into their JSON Schema representation. */ -function jsonRange(kinds: readonly RangeKind[]): Record { +function jsonRange(kinds: readonly RangeKindType[]): Record { const schemas = kinds.map((kind): Record => { switch (kind) { - case 'string': return { type: 'string' } - case 'number': return { type: 'number' } - case 'boolean': return { type: 'boolean' } - case 'node': return { anyOf: [{ type: 'string' }, { type: 'object' }] } - case 'unknown': return {} + case 'string': + return { type: 'string' } + case 'number': + return { type: 'number' } + case 'boolean': + return { type: 'boolean' } + case 'node': + return { anyOf: [{ type: 'string' }, { type: 'object' }] } + case 'unknown': + return {} } }) return schemas.length === 1 ? schemas[0]! : { anyOf: schemas } @@ -227,6 +256,10 @@ function jsonRange(kinds: readonly RangeKind[]): Record { function getSchemaUri(target: JsonSchemaTarget): string | undefined { if (target === 'draft-2020-12') return 'https://json-schema.org/draft/2020-12/schema' if (target === 'draft-07') return 'http://json-schema.org/draft-07/schema#' - if (target === 'openapi-3.0') throw new TypeError('OpenAPI 3.0 conversion is not implemented because it is not JSON Schema-equivalent.') + if (target === 'openapi-3.0') { + throw new TypeError( + 'OpenAPI 3.0 conversion is not implemented because it is not JSON Schema-equivalent.', + ) + } throw new TypeError(`Unsupported JSON Schema target '${target}'.`) } diff --git a/packages/vocab/runtime_test.ts b/packages/vocab/runtime_test.ts index 4ed1e20..015497c 100644 --- a/packages/vocab/runtime_test.ts +++ b/packages/vocab/runtime_test.ts @@ -5,7 +5,12 @@ import { createSchema } from './runtime.ts' describe('@okikio/vocab runtime', () => { it('validates every required multi-type name and accepts extension fields', async () => { const schema = createSchema({ types: ['Product', 'SoftwareApplication'] }) - expect(await schema['~standard'].validate({ '@type': ['Product', 'SoftwareApplication'], extension: true })).toEqual({ + expect( + await schema['~standard'].validate({ + '@type': ['Product', 'SoftwareApplication'], + extension: true, + }), + ).toEqual({ value: { '@type': ['Product', 'SoftwareApplication'], extension: true }, }) const invalid = await schema['~standard'].validate({ '@type': ['Product'] }) @@ -21,12 +26,14 @@ describe('@okikio/vocab runtime', () => { code: ['string', 'number'], }, }) - expect(await schema['~standard'].validate({ - '@type': 'Product', - price: [10, 20], - brand: { '@id': 'urn:brand:1' }, - code: ['A', 2], - })).toEqual({ + expect( + await schema['~standard'].validate({ + '@type': 'Product', + price: [10, 20], + brand: { '@id': 'urn:brand:1' }, + code: ['A', 2], + }), + ).toEqual({ value: { '@type': 'Product', price: [10, 20], @@ -40,7 +47,11 @@ describe('@okikio/vocab runtime', () => { let left = createSchema({ types: ['Left'] }) let right = createSchema({ types: ['Right'] }) left = createSchema({ types: ['Left'], properties: { left: 'string' }, parents: () => [right] }) - right = createSchema({ types: ['Right'], properties: { right: 'number' }, parents: () => [left] }) + right = createSchema({ + types: ['Right'], + properties: { right: 'number' }, + parents: () => [left], + }) const invalid = await left['~standard'].validate({ '@type': 'Left', left: 'ok', right: 'bad' }) expect('issues' in invalid).toBe(true) diff --git a/packages/vocab/schema/mod.ts b/packages/vocab/schema/mod.ts index 9f8a293..905845c 100644 --- a/packages/vocab/schema/mod.ts +++ b/packages/vocab/schema/mod.ts @@ -67,19 +67,25 @@ export const sku = namedNode('https://schema.org/sku') /** JSON-LD properties directly available to Offer, including inherited interfaces. */ export interface OfferPropertiesType extends IntangiblePropertiesType { + /** Offer price represented using the generated schema.org vocabulary contract. */ readonly price?: ValueType + /** ISO-style currency code associated with the offer price. */ readonly priceCurrency?: ValueType } /** JSON-LD properties directly available to Product, including inherited interfaces. */ export interface ProductPropertiesType extends ThingPropertiesType { + /** Offer nodes associated with this product. */ readonly offers?: ValueType + /** Merchant or catalog SKU associated with this product. */ readonly sku?: ValueType } /** JSON-LD properties directly available to Thing, including inherited interfaces. */ export interface ThingPropertiesType { + /** Human-readable description of this schema.org Thing. */ readonly description?: ValueType + /** Schema.org `name` value for this Thing. */ readonly name?: ValueType } @@ -142,20 +148,32 @@ export type URLType = string /** Generated class-name to property-interface map used by multi-typed JSON-LD nodes. */ export interface TypeMapType { + /** Property contract contributed by the generated Offer class. */ readonly Offer: OfferPropertiesType + /** Property contract contributed by the generated Product class. */ readonly Product: ProductPropertiesType + /** Property contract contributed by the generated Thing class. */ readonly Thing: ThingPropertiesType + /** Property contract contributed by the generated Intangible class. */ readonly Intangible: IntangiblePropertiesType } /** Every generated vocabulary class name accepted by multi-type nodes. */ export type ClassNameType = keyof TypeMapType /** Resolves one generated class name to its property interface. */ -type PropertiesForType = Type extends keyof TypeMapType ? TypeMapType[Type] : never +type PropertiesForType = Type extends keyof TypeMapType + ? TypeMapType[Type] + : never /** Converts the selected class-property union into one intersection for multi-typed nodes. */ -type UnionToIntersection = (Value extends unknown ? (value: Value) => void : never) extends (value: infer Intersection) => void ? Intersection : never +type UnionToIntersection = (Value extends unknown ? (value: Value) => void : never) extends + (value: infer Intersection) => void ? Intersection : never /** Intersects the properties contributed by every class on a multi-typed JSON-LD node. */ -type MergedPropertiesType = UnionToIntersection> & object +type MergedPropertiesType = + & UnionToIntersection> + & object /** JSON-LD node carrying all properties contributed by the selected generated class names. */ -export type MultiTypeType = NodeType> +export type MultiTypeType = NodeType< + Types, + MergedPropertiesType +> diff --git a/packages/vocab/standard.ts b/packages/vocab/standard.ts index d1fa56a..98a499e 100644 --- a/packages/vocab/standard.ts +++ b/packages/vocab/standard.ts @@ -14,52 +14,69 @@ /** Base Standard Typed v1 contract shared by the other Standard Schema traits. */ export interface StandardTypedV1 { + /** Standard Schema V1 metadata property consumed by compatible schema tooling. */ readonly '~standard': StandardTypedV1Props } /** Inferred input/output pair carried by Standard Typed metadata. */ export interface StandardTypedTypes { + /** Standard Schema input type metadata or runtime value supplied to conversion. */ readonly input: Input + /** Standard Schema output type metadata or conversion result. */ readonly output: Output } /** Base metadata and optional inference types carried by a Standard v1 object. */ export interface StandardTypedV1Props { + /** Standard Typed contract revision implemented by this metadata object. */ readonly version: 1 + /** Stable vendor identifier required by Standard Typed v1 metadata. */ readonly vendor: string + /** Named RDF types or generated type names attached to this record. */ readonly types?: StandardTypedTypes | undefined } /** Infers the declared input type from any Standard Typed-compatible object. */ -export type StandardInferInput = NonNullable['input'] +export type StandardInferInput = NonNullable< + Schema['~standard']['types'] +>['input'] /** Infers the declared output type from any Standard Typed-compatible object. */ -export type StandardInferOutput = NonNullable['output'] +export type StandardInferOutput = NonNullable< + Schema['~standard']['types'] +>['output'] /** Standard Schema v1 validation contract. */ export interface StandardSchemaV1 { + /** Standard Schema V1 metadata property consumed by compatible schema tooling. */ readonly '~standard': StandardSchemaV1Props } /** One structured segment in a Standard Schema issue path. */ export interface StandardPathSegment { + /** Property key that identifies one segment in a validation issue path. */ readonly key: PropertyKey } /** One Standard Schema validation issue. */ export interface StandardIssue { + /** Human-readable explanation of the diagnostic or failure. */ readonly message: string + /** Nested input location associated with this issue, from outermost to innermost segment. */ readonly path?: ReadonlyArray | undefined } /** Successful Standard Schema validation result. */ export interface StandardSuccessResult { + /** Validated output returned by the Standard Schema implementation. */ readonly value: Output + /** Validation issues returned by a Standard Schema-compatible validator. */ readonly issues?: undefined } /** Failed Standard Schema validation result. */ export interface StandardFailureResult { + /** Validation issues returned by a Standard Schema-compatible validator. */ readonly issues: ReadonlyArray } @@ -68,12 +85,14 @@ export type StandardResult = StandardSuccessResult | StandardFai /** Optional vendor-specific Standard Schema validation options. */ export interface StandardSchemaOptions { + /** Standard JSON Schema library options forwarded to a compatible converter. */ readonly libraryOptions?: Record | undefined } /** Standard Schema v1 validation properties. */ export interface StandardSchemaV1Props extends StandardTypedV1Props { + /** Validates one unknown input and returns the Standard Schema success or failure shape. */ readonly validate: ( value: unknown, options?: StandardSchemaOptions | undefined, @@ -82,6 +101,7 @@ export interface StandardSchemaV1Props /** Standard JSON Schema v1 conversion contract. */ export interface StandardJSONSchemaV1 { + /** Standard Schema V1 metadata property consumed by compatible schema tooling. */ readonly '~standard': StandardJSONSchemaV1Props } @@ -94,18 +114,23 @@ export type JsonSchemaTarget = /** Options passed by a Standard JSON Schema consumer. */ export interface JsonSchemaOptions { + /** JSON Schema dialect or integration target requested by the consumer. */ readonly target: JsonSchemaTarget + /** Standard JSON Schema library options forwarded to a compatible converter. */ readonly libraryOptions?: Record | undefined } /** Input/output converter carried by Standard JSON Schema metadata. */ export interface StandardJSONSchemaConverter { + /** Standard Schema input type metadata or runtime value supplied to conversion. */ readonly input: (options: JsonSchemaOptions) => Record + /** Standard Schema output type metadata or conversion result. */ readonly output: (options: JsonSchemaOptions) => Record } /** Standard JSON Schema v1 conversion properties. */ export interface StandardJSONSchemaV1Props extends StandardTypedV1Props { + /** Input/output JSON Schema converter required by Standard JSON Schema v1. */ readonly jsonSchema: StandardJSONSchemaConverter } diff --git a/packages/vocab/standard_test.ts b/packages/vocab/standard_test.ts index 5858cd6..7848a2e 100644 --- a/packages/vocab/standard_test.ts +++ b/packages/vocab/standard_test.ts @@ -1,21 +1,33 @@ import { describe, it } from 'node:test' import { expect } from '@std/expect' -import type { StandardJSONSchemaV1 as OfficialJSONSchemaV1, StandardSchemaV1 as OfficialSchemaV1 } from '@standard-schema/spec' +import type { + StandardJSONSchemaV1 as OfficialJSONSchemaV1, + StandardSchemaV1 as OfficialSchemaV1, +} from '@standard-schema/spec' import { ProductSchema, type ProductType } from './schema/mod.ts' -import type { StandardInferInput, StandardInferOutput, StandardJSONSchemaV1, StandardSchemaV1 } from './standard.ts' +import type { + StandardInferInput, + StandardInferOutput, + StandardJSONSchemaV1, + StandardSchemaV1, +} from './standard.ts' /** Compile-time assertion that generated schemas satisfy both local and official contracts. */ /** Compile-time proof that local Standard Typed inference preserves the generated schema output type. */ -function acceptInference(_input: StandardInferInput, output: StandardInferOutput): ProductType { +function acceptInference( + _input: StandardInferInput, + output: StandardInferOutput, +): ProductType { return output } function acceptSchema( - schema: StandardSchemaV1 & - StandardJSONSchemaV1 & - OfficialSchemaV1 & - OfficialJSONSchemaV1, + schema: + & StandardSchemaV1 + & StandardJSONSchemaV1 + & OfficialSchemaV1 + & OfficialJSONSchemaV1, ): void { void schema }