From fc88bb4be72e0b1dffdaa40d561db68ccfd5a458 Mon Sep 17 00:00:00 2001 From: Nokome Bentley Date: Thu, 22 Jan 2026 18:21:18 +1300 Subject: [PATCH 01/14] feat(conformance): add conformance test suite package --- CONTRIBUTING.md | 60 ++++++++- packages/oxa-conformance/README.md | 186 ++++++++++++++++++++++++++ packages/oxa-conformance/index.cjs | 7 + packages/oxa-conformance/index.js | 12 ++ packages/oxa-conformance/package.json | 37 +++++ scripts/codegen.ts | 2 + scripts/lib/generate-conformance.ts | 149 +++++++++++++++++++++ 7 files changed, 447 insertions(+), 6 deletions(-) create mode 100644 packages/oxa-conformance/README.md create mode 100644 packages/oxa-conformance/index.cjs create mode 100644 packages/oxa-conformance/index.js create mode 100644 packages/oxa-conformance/package.json create mode 100644 scripts/lib/generate-conformance.ts diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ec1a01d..fb5382a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -55,15 +55,16 @@ pnpm typecheck ### Code Generation ```bash -# Run all code generation (validate, ts, py, rs, docs) +# Run all code generation (validate, ts, py, rs, docs, conformance) pnpm codegen all # Or run individual steps: -pnpm codegen validate # Validate schema files -pnpm codegen ts # Generate TypeScript types -pnpm codegen py # Generate Python Pydantic models -pnpm codegen rs # Generate Rust types -pnpm codegen docs # Generate MyST documentation +pnpm codegen validate # Validate schema files +pnpm codegen ts # Generate TypeScript types +pnpm codegen py # Generate Python Pydantic models +pnpm codegen rs # Generate Rust types +pnpm codegen conformance # Generate conformance suite manifest +pnpm codegen docs # Generate MyST documentation ``` ### Building @@ -111,6 +112,53 @@ Commit the generated changeset file with your PR. - `oxa-types` (TypeScript), `oxa-types` (Python), and `oxa-types` (Rust) stay in sync - `@oxa/core` and `oxa` can version independently +## Conformance Suite + +The `oxa-conformance` package provides test cases for validating OXA format conversion implementations. If you're building a tool that converts between OXA and other formats, you can use these test cases to validate your implementation. + +To add new test cases to the conformance suite: + +1. Create a new JSON file in the appropriate directory: + - `packages/oxa-conformance/cases/inline/` for inline node tests + - `packages/oxa-conformance/cases/block/` for block node tests + +2. Follow the test case format: + +```json +{ + "$schema": "../../schemas/test-case.schema.json", + "title": "Short descriptive title", + "description": "What this test validates", + "category": "inline", + "formats": { + "oxa": { ... }, + "myst": { ... }, + "pandoc": { ... }, + "stencila": { ... }, + "markdown": "...", + "html": "...", + "jats": "..." + }, + "notes": { + "markdown": "Optional format-specific notes" + } +} +``` + +3. Formats should be in canonical order: `oxa`, `myst`, `pandoc`, `stencila`, `markdown`, `html`, `jats`. The `oxa` format is required; others are optional. + +4. Regenerate the manifest: + +```bash +pnpm codegen conformance +``` + +5. Run tests to validate your test case: + +```bash +pnpm test +``` + ## Documentation The documentation uses [MyST](https://mystmd.org) and requires the automated codegen tool to run. To start the documentation use `npm install -g mystmd` and run `myst start` in the `docs/` folder. The online documentation is hosted using Curvenote under https://oxa.dev diff --git a/packages/oxa-conformance/README.md b/packages/oxa-conformance/README.md new file mode 100644 index 0000000..71be502 --- /dev/null +++ b/packages/oxa-conformance/README.md @@ -0,0 +1,186 @@ +# OXA Conformance Suite + +A collection of test cases for validating OXA format conversion implementations. + +## Overview + +The OXA Conformance Suite provides test cases containing OXA JSON alongside equivalent representations in other formats. Tool developers building converters between OXA and other formats can use these test cases to validate their implementations. + +## Installation + +```bash +npm install oxa-conformance +``` + +Or include it as a dev dependency: + +```bash +npm install -D oxa-conformance +``` + +## Usage + +### Loading Test Cases + +The package exports a manifest listing all available test cases: + +```javascript +// ESM (recommended) +import manifest from "oxa-conformance"; + +// CommonJS +const manifest = require("oxa-conformance"); + +console.log(manifest.cases); // Array of test case metadata +``` + +Load individual test cases by reading the JSON files directly: + +```javascript +import { readFileSync } from "fs"; +import { dirname, resolve } from "path"; +import { fileURLToPath } from "url"; + +// Get the path to the conformance package +const conformancePath = dirname(fileURLToPath(import.meta.resolve("oxa-conformance"))); + +// Load a specific test case +const textBasic = JSON.parse( + readFileSync(resolve(conformancePath, "cases/inline/text-basic.json"), "utf-8") +); + +console.log(textBasic.formats.oxa); // OXA representation +console.log(textBasic.formats.markdown); // Markdown representation +``` + +**Note:** If using TypeScript with `resolveJsonModule` enabled, you can also import JSON files directly. + +### Running Tests + +Here's an example of using the conformance suite with a testing framework: + +```javascript +import { describe, it, expect } from "vitest"; +import manifest from "oxa-conformance"; +import { readFileSync } from "fs"; +import { dirname, resolve } from "path"; +import { fileURLToPath } from "url"; + +// Your converter functions +import { oxaToMarkdown, markdownToOxa } from "./your-converter"; + +// Get path to conformance package +const conformancePath = dirname(fileURLToPath(import.meta.resolve("oxa-conformance"))); + +describe("OXA Conformance", () => { + for (const testCase of manifest.cases) { + const casePath = resolve(conformancePath, testCase.path); + const caseData = JSON.parse(readFileSync(casePath, "utf-8")); + + if (caseData.formats.markdown) { + it(`${testCase.id}: OXA → Markdown`, () => { + const result = oxaToMarkdown(caseData.formats.oxa); + expect(result).toBe(caseData.formats.markdown); + }); + + it(`${testCase.id}: Markdown → OXA`, () => { + const result = markdownToOxa(caseData.formats.markdown); + expect(result).toEqual(caseData.formats.oxa); + }); + } + } +}); +``` + +## Test Case Format + +Each test case is a JSON file with the following structure: + +```json +{ + "$schema": "../../schemas/test-case.schema.json", + "title": "Basic emphasis", + "description": "Emphasized text (typically rendered as italic)", + "category": "inline", + "formats": { + "oxa": { ... }, + "myst": { ... }, + "pandoc": { ... }, + "stencila": { ... }, + "markdown": "...", + "html": "...", + "jats": "..." + }, + "notes": { + "markdown": "Additional format-specific notes" + } +} +``` + +### Fields + +- **title**: Short descriptive title +- **description**: Detailed description of what the test validates +- **category**: One of `inline`, `block`, `document`, or `edge-case` +- **formats**: Object containing equivalent representations in each format +- **notes**: Optional format-specific notes and considerations + +### Supported Formats + +1. **oxa** - OXA JSON (always present, authoritative) +2. **myst** - MyST Markdown AST +3. **pandoc** - Pandoc AST JSON format +4. **stencila** - Stencila Schema JSON +5. **markdown** - CommonMark/GFM Markdown +6. **html** - Semantic HTML5 +7. **jats** - JATS Publishing XML + +AST-based formats (oxa, myst, pandoc, stencila) are grouped first, followed by string-based interchange formats (markdown, html, jats). This ordering makes it easier to visually scan test cases for correctness across related formats. + +Not every test case includes all formats. The `oxa` format is always present and serves as the authoritative representation. + +## Manifest Structure + +The `manifest.json` file provides an index of all test cases: + +```json +{ + "$schema": "./schemas/manifest.schema.json", + "version": "0.1.0", + "formats": ["oxa", "myst", "pandoc", "stencila", "markdown", "html", "jats"], + "cases": [ + { + "id": "text-basic", + "path": "cases/inline/text-basic.json", + "category": "inline", + "nodeTypes": ["Text"] + } + ] +} +``` + +## Directory Structure + +``` +oxa-conformance/ +├── manifest.json # Index of all test cases +├── cases/ +│ ├── inline/ # Inline node test cases +│ │ ├── text-basic.json +│ │ ├── emphasis-basic.json +│ │ └── strong-basic.json +│ └── block/ # Block node test cases +│ ├── paragraph-basic.json +│ └── heading-basic.json +└── schemas/ + ├── test-case.schema.json + └── manifest.schema.json +``` + +## Contributing New Test Cases + +See the [CONTRIBUTING.md](../../CONTRIBUTING.md) guide for instructions on adding new test cases to the conformance suite. + +## License + +MIT diff --git a/packages/oxa-conformance/index.cjs b/packages/oxa-conformance/index.cjs new file mode 100644 index 0000000..8fe109e --- /dev/null +++ b/packages/oxa-conformance/index.cjs @@ -0,0 +1,7 @@ +/** + * OXA Conformance Suite CommonJS entrypoint + */ + +const manifest = require("./manifest.json"); +module.exports = manifest; +module.exports.default = manifest; diff --git a/packages/oxa-conformance/index.js b/packages/oxa-conformance/index.js new file mode 100644 index 0000000..552e36a --- /dev/null +++ b/packages/oxa-conformance/index.js @@ -0,0 +1,12 @@ +/** + * OXA Conformance Suite entrypoint + * + * Re-exports manifest.json for Node ESM compatibility. + * This avoids the need for import assertions when consuming the package. + */ + +import { createRequire } from "module"; +const require = createRequire(import.meta.url); + +const manifest = require("./manifest.json"); +export default manifest; diff --git a/packages/oxa-conformance/package.json b/packages/oxa-conformance/package.json new file mode 100644 index 0000000..7211bb2 --- /dev/null +++ b/packages/oxa-conformance/package.json @@ -0,0 +1,37 @@ +{ + "name": "oxa-conformance", + "version": "0.1.0", + "description": "Conformance test suite for OXA schema implementations", + "type": "module", + "main": "./index.js", + "exports": { + ".": { + "import": "./index.js", + "require": "./index.cjs" + }, + "./manifest.json": "./manifest.json", + "./cases/*": "./cases/*", + "./schemas/*": "./schemas/*" + }, + "files": [ + "index.js", + "index.cjs", + "manifest.json", + "cases", + "schemas", + "README.md" + ], + "keywords": [ + "oxa", + "conformance", + "test-suite", + "schema", + "validation" + ], + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/oxa-dev/oxa.git", + "directory": "packages/oxa-conformance" + } +} diff --git a/scripts/codegen.ts b/scripts/codegen.ts index 4228d70..c79389d 100644 --- a/scripts/codegen.ts +++ b/scripts/codegen.ts @@ -6,6 +6,7 @@ */ import { program } from "commander"; +import { generateConformance } from "./lib/generate-conformance.js"; import { generateDocs } from "./lib/generate-docs.js"; import { generateJson } from "./lib/generate-json.js"; import { generatePy } from "./lib/generate-py.js"; @@ -24,6 +25,7 @@ const generators: Generator[] = [ { name: "py", label: "Python Pydantic models", fn: generatePy }, { name: "rs", label: "Rust types", fn: generateRs }, { name: "ts", label: "TypeScript types", fn: generateTs }, + { name: "conformance", label: "Conformance suite manifest", fn: generateConformance }, { name: "docs", label: "Schema documentation", fn: generateDocs }, ]; diff --git a/scripts/lib/generate-conformance.ts b/scripts/lib/generate-conformance.ts new file mode 100644 index 0000000..54c8de3 --- /dev/null +++ b/scripts/lib/generate-conformance.ts @@ -0,0 +1,149 @@ +/** + * Generate conformance suite manifest from test case files. + * + * Scans the cases directory and generates manifest.json with metadata + * about all test cases. + */ + +import { readdirSync, readFileSync, writeFileSync } from "fs"; +import { basename, join } from "path"; +import prettier from "prettier"; + +const CONFORMANCE_PATH = join( + import.meta.dirname, + "../../packages/oxa-conformance", +); +const CASES_PATH = join(CONFORMANCE_PATH, "cases"); +const OUTPUT_PATH = join(CONFORMANCE_PATH, "manifest.json"); + +interface TestCase { + title: string; + category: string; + formats: { + oxa: Record; + }; +} + +interface ManifestCase { + id: string; + path: string; + category: string; + nodeTypes: string[]; +} + +interface Manifest { + $schema: string; + version: string; + formats: string[]; + cases: ManifestCase[]; +} + +/** + * Extract OXA node types from a test case's oxa format. + */ +function extractNodeTypes(oxa: Record): string[] { + const types = new Set(); + + function walk(node: unknown): void { + if (node && typeof node === "object" && !Array.isArray(node)) { + const obj = node as Record; + if (typeof obj.type === "string") { + types.add(obj.type); + } + for (const value of Object.values(obj)) { + walk(value); + } + } else if (Array.isArray(node)) { + for (const item of node) { + walk(item); + } + } + } + + walk(oxa); + return Array.from(types).sort(); +} + +/** + * Scan a category directory for test case files. + * Validates that each test case's declared category matches its folder location. + */ +function scanCategory(category: string): ManifestCase[] { + const categoryPath = join(CASES_PATH, category); + const cases: ManifestCase[] = []; + + let files: string[]; + try { + files = readdirSync(categoryPath).filter((f) => f.endsWith(".json")); + } catch { + // Directory doesn't exist yet + return cases; + } + + for (const file of files) { + const filePath = join(categoryPath, file); + const content = readFileSync(filePath, "utf-8"); + const testCase = JSON.parse(content) as TestCase; + + // Validate category matches folder location + if (testCase.category !== category) { + throw new Error( + `Category mismatch in ${filePath}: ` + + `file declares "${testCase.category}" but is located in "${category}/" folder. ` + + `Move the file or update the category field.`, + ); + } + + const id = basename(file, ".json"); + const nodeTypes = extractNodeTypes(testCase.formats.oxa); + + cases.push({ + id, + path: `cases/${category}/${file}`, + category, + nodeTypes, + }); + } + + return cases; +} + +export async function generateConformance(): Promise { + // Get version from oxa-conformance package.json + const pkgPath = join(CONFORMANCE_PATH, "package.json"); + const pkg = JSON.parse(readFileSync(pkgPath, "utf-8")); + const version = pkg.version; + + // Scan all category directories + const categories = ["inline", "block", "document"]; + const allCases: ManifestCase[] = []; + + for (const category of categories) { + const cases = scanCategory(category); + allCases.push(...cases); + } + + // Sort cases by category then id for deterministic output + allCases.sort((a, b) => { + if (a.category !== b.category) { + return categories.indexOf(a.category) - categories.indexOf(b.category); + } + return a.id.localeCompare(b.id); + }); + + const manifest: Manifest = { + $schema: "./schemas/manifest.schema.json", + version, + formats: ["oxa", "myst", "pandoc", "stencila", "markdown", "html", "jats"], + cases: allCases, + }; + + const json = JSON.stringify(manifest, null, 2); + const formatted = await prettier.format(json, { + parser: "json", + filepath: OUTPUT_PATH, + }); + + writeFileSync(OUTPUT_PATH, formatted); + console.log(`Generated ${OUTPUT_PATH} with ${allCases.length} test cases`); +} From 0fd9ba4f195182f5982fa56f1659a4418babc89a Mon Sep 17 00:00:00 2001 From: Nokome Bentley Date: Thu, 22 Jan 2026 05:22:24 +0000 Subject: [PATCH 02/14] chore(*): update formatted/generated files [skip ci] --- packages/oxa-conformance/manifest.json | 6 ++++++ pnpm-lock.yaml | 2 ++ scripts/codegen.ts | 6 +++++- 3 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 packages/oxa-conformance/manifest.json diff --git a/packages/oxa-conformance/manifest.json b/packages/oxa-conformance/manifest.json new file mode 100644 index 0000000..2df1151 --- /dev/null +++ b/packages/oxa-conformance/manifest.json @@ -0,0 +1,6 @@ +{ + "$schema": "./schemas/manifest.schema.json", + "version": "0.1.0", + "formats": ["oxa", "myst", "pandoc", "stencila", "markdown", "html", "jats"], + "cases": [] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f3d8695..74407f7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -45,6 +45,8 @@ importers: specifier: workspace:* version: link:../oxa-core + packages/oxa-conformance: {} + packages/oxa-core: dependencies: ajv: diff --git a/scripts/codegen.ts b/scripts/codegen.ts index c79389d..9b84534 100644 --- a/scripts/codegen.ts +++ b/scripts/codegen.ts @@ -25,7 +25,11 @@ const generators: Generator[] = [ { name: "py", label: "Python Pydantic models", fn: generatePy }, { name: "rs", label: "Rust types", fn: generateRs }, { name: "ts", label: "TypeScript types", fn: generateTs }, - { name: "conformance", label: "Conformance suite manifest", fn: generateConformance }, + { + name: "conformance", + label: "Conformance suite manifest", + fn: generateConformance, + }, { name: "docs", label: "Schema documentation", fn: generateDocs }, ]; From 498e608e5e07e08867138df2bbb332a498ac0229 Mon Sep 17 00:00:00 2001 From: Nokome Bentley Date: Thu, 22 Jan 2026 18:26:01 +1300 Subject: [PATCH 03/14] refactor(conformance): rename to @oxa/conformance --- CONTRIBUTING.md | 6 +++--- packages/oxa-conformance/README.md | 16 ++++++++-------- packages/oxa-conformance/package.json | 5 ++++- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fb5382a..1e9384d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -114,13 +114,13 @@ Commit the generated changeset file with your PR. ## Conformance Suite -The `oxa-conformance` package provides test cases for validating OXA format conversion implementations. If you're building a tool that converts between OXA and other formats, you can use these test cases to validate your implementation. +The `@oxa/conformance` package provides test cases for validating OXA format conversion implementations. If you're building a tool that converts between OXA and other formats, you can use these test cases to validate your implementation. To add new test cases to the conformance suite: 1. Create a new JSON file in the appropriate directory: - - `packages/oxa-conformance/cases/inline/` for inline node tests - - `packages/oxa-conformance/cases/block/` for block node tests + - `packages/@oxa/conformance/cases/inline/` for inline node tests + - `packages/@oxa/conformance/cases/block/` for block node tests 2. Follow the test case format: diff --git a/packages/oxa-conformance/README.md b/packages/oxa-conformance/README.md index 71be502..9f53786 100644 --- a/packages/oxa-conformance/README.md +++ b/packages/oxa-conformance/README.md @@ -9,13 +9,13 @@ The OXA Conformance Suite provides test cases containing OXA JSON alongside equi ## Installation ```bash -npm install oxa-conformance +npm install @oxa/conformance ``` Or include it as a dev dependency: ```bash -npm install -D oxa-conformance +npm install -D @oxa/conformance ``` ## Usage @@ -26,10 +26,10 @@ The package exports a manifest listing all available test cases: ```javascript // ESM (recommended) -import manifest from "oxa-conformance"; +import manifest from "@oxa/conformance"; // CommonJS -const manifest = require("oxa-conformance"); +const manifest = require("@oxa/conformance"); console.log(manifest.cases); // Array of test case metadata ``` @@ -42,7 +42,7 @@ import { dirname, resolve } from "path"; import { fileURLToPath } from "url"; // Get the path to the conformance package -const conformancePath = dirname(fileURLToPath(import.meta.resolve("oxa-conformance"))); +const conformancePath = dirname(fileURLToPath(import.meta.resolve("@oxa/conformance"))); // Load a specific test case const textBasic = JSON.parse( @@ -61,7 +61,7 @@ Here's an example of using the conformance suite with a testing framework: ```javascript import { describe, it, expect } from "vitest"; -import manifest from "oxa-conformance"; +import manifest from "@oxa/conformance"; import { readFileSync } from "fs"; import { dirname, resolve } from "path"; import { fileURLToPath } from "url"; @@ -70,7 +70,7 @@ import { fileURLToPath } from "url"; import { oxaToMarkdown, markdownToOxa } from "./your-converter"; // Get path to conformance package -const conformancePath = dirname(fileURLToPath(import.meta.resolve("oxa-conformance"))); +const conformancePath = dirname(fileURLToPath(import.meta.resolve("@oxa/conformance"))); describe("OXA Conformance", () => { for (const testCase of manifest.cases) { @@ -162,7 +162,7 @@ The `manifest.json` file provides an index of all test cases: ## Directory Structure ``` -oxa-conformance/ +@oxa/conformance/ ├── manifest.json # Index of all test cases ├── cases/ │ ├── inline/ # Inline node test cases diff --git a/packages/oxa-conformance/package.json b/packages/oxa-conformance/package.json index 7211bb2..3243f5f 100644 --- a/packages/oxa-conformance/package.json +++ b/packages/oxa-conformance/package.json @@ -1,5 +1,5 @@ { - "name": "oxa-conformance", + "name": "@oxa/conformance", "version": "0.1.0", "description": "Conformance test suite for OXA schema implementations", "type": "module", @@ -29,6 +29,9 @@ "validation" ], "license": "MIT", + "publishConfig": { + "access": "public" + }, "repository": { "type": "git", "url": "git+https://github.com/oxa-dev/oxa.git", From cf5c5b2b229bdf1287be565b604b3df4db21149b Mon Sep 17 00:00:00 2001 From: Nokome Bentley Date: Thu, 22 Jan 2026 18:47:37 +1300 Subject: [PATCH 04/14] feat(conformance): add schemas and generated typedefs --- package.json | 1 + packages/oxa-conformance/manifest.json | 33 ++++++- packages/oxa-conformance/package.json | 3 + .../schemas/manifest.schema.json | 59 +++++++++++++ .../schemas/test-case.schema.json | 75 ++++++++++++++++ pnpm-lock.yaml | 53 +++++++++++ scripts/lib/generate-conformance.ts | 88 +++++++++++++++++-- 7 files changed, 303 insertions(+), 9 deletions(-) create mode 100644 packages/oxa-conformance/schemas/manifest.schema.json create mode 100644 packages/oxa-conformance/schemas/test-case.schema.json diff --git a/package.json b/package.json index f52d004..236d602 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "@changesets/cli": "^2.29.8", "@eslint/js": "^9.39.2", "eslint": "^9.39.2", + "json-schema-to-typescript": "^15.0.4", "prettier": "^3.7.4", "tsx": "^4.21.0", "turbo": "^2.0.0", diff --git a/packages/oxa-conformance/manifest.json b/packages/oxa-conformance/manifest.json index 2df1151..2c1acb7 100644 --- a/packages/oxa-conformance/manifest.json +++ b/packages/oxa-conformance/manifest.json @@ -2,5 +2,36 @@ "$schema": "./schemas/manifest.schema.json", "version": "0.1.0", "formats": ["oxa", "myst", "pandoc", "stencila", "markdown", "html", "jats"], - "cases": [] + "cases": [ + { + "id": "emphasis-basic", + "path": "cases/inline/emphasis-basic.json", + "category": "inline", + "nodeTypes": ["Emphasis", "Text"] + }, + { + "id": "strong-basic", + "path": "cases/inline/strong-basic.json", + "category": "inline", + "nodeTypes": ["Strong", "Text"] + }, + { + "id": "text-basic", + "path": "cases/inline/text-basic.json", + "category": "inline", + "nodeTypes": ["Text"] + }, + { + "id": "heading-basic", + "path": "cases/block/heading-basic.json", + "category": "block", + "nodeTypes": ["Heading", "Text"] + }, + { + "id": "paragraph-basic", + "path": "cases/block/paragraph-basic.json", + "category": "block", + "nodeTypes": ["Paragraph", "Text"] + } + ] } diff --git a/packages/oxa-conformance/package.json b/packages/oxa-conformance/package.json index 3243f5f..11e0718 100644 --- a/packages/oxa-conformance/package.json +++ b/packages/oxa-conformance/package.json @@ -4,8 +4,10 @@ "description": "Conformance test suite for OXA schema implementations", "type": "module", "main": "./index.js", + "types": "./index.d.ts", "exports": { ".": { + "types": "./index.d.ts", "import": "./index.js", "require": "./index.cjs" }, @@ -16,6 +18,7 @@ "files": [ "index.js", "index.cjs", + "index.d.ts", "manifest.json", "cases", "schemas", diff --git a/packages/oxa-conformance/schemas/manifest.schema.json b/packages/oxa-conformance/schemas/manifest.schema.json new file mode 100644 index 0000000..69620d5 --- /dev/null +++ b/packages/oxa-conformance/schemas/manifest.schema.json @@ -0,0 +1,59 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://oxa.dev/schemas/conformance/manifest.schema.json", + "title": "OXA Conformance Suite Manifest", + "description": "Index of all test cases in the OXA Conformance Suite", + "type": "object", + "properties": { + "$schema": { + "type": "string", + "description": "Reference to this schema" + }, + "version": { + "type": "string", + "description": "Version of the conformance suite", + "pattern": "^\\d+\\.\\d+\\.\\d+$" + }, + "formats": { + "type": "array", + "description": "List of supported formats in canonical order", + "items": { + "type": "string", + "enum": ["oxa", "myst", "pandoc", "stencila", "markdown", "html", "jats"] + } + }, + "cases": { + "type": "array", + "description": "List of all test cases", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Unique identifier for the test case (derived from filename)" + }, + "path": { + "type": "string", + "description": "Relative path to the test case file" + }, + "category": { + "type": "string", + "enum": ["inline", "block", "document", "edge-case"], + "description": "Category of the test case" + }, + "nodeTypes": { + "type": "array", + "description": "OXA node types covered by this test case", + "items": { + "type": "string" + } + } + }, + "required": ["id", "path", "category", "nodeTypes"], + "additionalProperties": false + } + } + }, + "required": ["version", "formats", "cases"], + "additionalProperties": false +} diff --git a/packages/oxa-conformance/schemas/test-case.schema.json b/packages/oxa-conformance/schemas/test-case.schema.json new file mode 100644 index 0000000..9c7c1b9 --- /dev/null +++ b/packages/oxa-conformance/schemas/test-case.schema.json @@ -0,0 +1,75 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://oxa.dev/schemas/conformance/test-case.schema.json", + "title": "OXA Conformance Test Case", + "description": "A test case for validating OXA format conversions", + "type": "object", + "properties": { + "$schema": { + "type": "string", + "description": "Reference to this schema" + }, + "title": { + "type": "string", + "description": "Short title describing the test case" + }, + "description": { + "type": "string", + "description": "Detailed description of what the test case validates" + }, + "category": { + "type": "string", + "enum": ["inline", "block", "document"], + "description": "Category of the test case" + }, + "formats": { + "type": "object", + "description": "Format representations of the same content. OXA is always required and should be first.", + "properties": { + "oxa": { + "type": "object", + "description": "OXA JSON representation (required, authoritative)", + "additionalProperties": true + }, + "myst": { + "type": "object", + "description": "MyST Markdown AST representation", + "additionalProperties": true + }, + "pandoc": { + "type": "object", + "description": "Pandoc AST JSON representation", + "additionalProperties": true + }, + "stencila": { + "type": "object", + "description": "Stencila Schema JSON representation", + "additionalProperties": true + }, + "markdown": { + "type": "string", + "description": "CommonMark/GFM Markdown representation" + }, + "html": { + "type": "string", + "description": "Semantic HTML5 representation" + }, + "jats": { + "type": "string", + "description": "JATS Publishing XML representation" + } + }, + "required": ["oxa"], + "additionalProperties": false + }, + "notes": { + "type": "object", + "description": "Format-specific notes and considerations", + "additionalProperties": { + "type": "string" + } + } + }, + "required": ["title", "category", "formats"], + "additionalProperties": false +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 74407f7..e692a4b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,6 +20,9 @@ importers: eslint: specifier: ^9.39.2 version: 9.39.2 + json-schema-to-typescript: + specifier: ^15.0.4 + version: 15.0.4 prettier: specifier: ^3.7.4 version: 3.7.4 @@ -68,6 +71,9 @@ importers: specifier: workspace:* version: link:../oxa-types-ts devDependencies: + '@oxa/conformance': + specifier: workspace:* + version: link:../oxa-conformance '@types/js-yaml': specifier: ^4.0.9 version: 4.0.9 @@ -107,6 +113,10 @@ importers: packages: + '@apidevtools/json-schema-ref-parser@11.9.3': + resolution: {integrity: sha512-60vepv88RwcJtSHrD6MjIL6Ta3SOYbgfnkHb+ppAVK+o9mXprRtulx7VlRl3lN3bbvysAfCS7WMVfhUYemB0IQ==} + engines: {node: '>= 16'} + '@babel/code-frame@7.27.1': resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} engines: {node: '>=6.9.0'} @@ -562,6 +572,9 @@ packages: '@jridgewell/sourcemap-codec@1.5.5': resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + '@jsdevtools/ono@7.1.3': + resolution: {integrity: sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==} + '@manypkg/find-root@1.1.0': resolution: {integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==} @@ -715,6 +728,9 @@ packages: '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + '@types/lodash@4.17.23': + resolution: {integrity: sha512-RDvF6wTulMPjrNdCoYRC8gNR880JNGT8uB+REUpC2Ns4pRqQJhGz90wh7rgdXDPpCczF3VGktDuFGVnz8zP7HA==} + '@types/node@12.20.55': resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} @@ -1206,6 +1222,11 @@ packages: json-buffer@3.0.1: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + json-schema-to-typescript@15.0.4: + resolution: {integrity: sha512-Su9oK8DR4xCmDsLlyvadkXzX6+GGXJpbhwoLtOGArAG61dvbW4YQmSEno2y66ahpIdmLMg6YUf/QHLgiwvkrHQ==} + engines: {node: '>=16.0.0'} + hasBin: true + json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} @@ -1247,6 +1268,9 @@ packages: lodash.startcase@4.4.0: resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==} + lodash@4.17.23: + resolution: {integrity: sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==} + magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} @@ -1265,6 +1289,9 @@ packages: resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} engines: {node: '>=16 || 14 >=14.17'} + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + mri@1.2.0: resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} engines: {node: '>=4'} @@ -1710,6 +1737,12 @@ packages: snapshots: + '@apidevtools/json-schema-ref-parser@11.9.3': + dependencies: + '@jsdevtools/ono': 7.1.3 + '@types/json-schema': 7.0.15 + js-yaml: 4.1.1 + '@babel/code-frame@7.27.1': dependencies: '@babel/helper-validator-identifier': 7.28.5 @@ -2103,6 +2136,8 @@ snapshots: '@jridgewell/sourcemap-codec@1.5.5': {} + '@jsdevtools/ono@7.1.3': {} + '@manypkg/find-root@1.1.0': dependencies: '@babel/runtime': 7.28.4 @@ -2216,6 +2251,8 @@ snapshots: '@types/json-schema@7.0.15': {} + '@types/lodash@4.17.23': {} + '@types/node@12.20.55': {} '@types/node@22.19.3': @@ -2783,6 +2820,18 @@ snapshots: json-buffer@3.0.1: {} + json-schema-to-typescript@15.0.4: + dependencies: + '@apidevtools/json-schema-ref-parser': 11.9.3 + '@types/json-schema': 7.0.15 + '@types/lodash': 4.17.23 + is-glob: 4.0.3 + js-yaml: 4.1.1 + lodash: 4.17.23 + minimist: 1.2.8 + prettier: 3.7.4 + tinyglobby: 0.2.15 + json-schema-traverse@0.4.1: {} json-schema-traverse@1.0.0: {} @@ -2818,6 +2867,8 @@ snapshots: lodash.startcase@4.4.0: {} + lodash@4.17.23: {} + magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -2837,6 +2888,8 @@ snapshots: dependencies: brace-expansion: 2.0.2 + minimist@1.2.8: {} + mri@1.2.0: {} ms@2.1.3: {} diff --git a/scripts/lib/generate-conformance.ts b/scripts/lib/generate-conformance.ts index 54c8de3..b5d96e5 100644 --- a/scripts/lib/generate-conformance.ts +++ b/scripts/lib/generate-conformance.ts @@ -1,12 +1,13 @@ /** - * Generate conformance suite manifest from test case files. + * Generate conformance suite manifest and TypeScript types. * - * Scans the cases directory and generates manifest.json with metadata - * about all test cases. + * - Generates manifest.json from test case files + * - Generates index.d.ts from JSON schemas */ import { readdirSync, readFileSync, writeFileSync } from "fs"; import { basename, join } from "path"; +import { compile } from "json-schema-to-typescript"; import prettier from "prettier"; const CONFORMANCE_PATH = join( @@ -14,7 +15,9 @@ const CONFORMANCE_PATH = join( "../../packages/oxa-conformance", ); const CASES_PATH = join(CONFORMANCE_PATH, "cases"); -const OUTPUT_PATH = join(CONFORMANCE_PATH, "manifest.json"); +const SCHEMAS_PATH = join(CONFORMANCE_PATH, "schemas"); +const MANIFEST_OUTPUT_PATH = join(CONFORMANCE_PATH, "manifest.json"); +const TYPES_OUTPUT_PATH = join(CONFORMANCE_PATH, "index.d.ts"); interface TestCase { title: string; @@ -108,7 +111,69 @@ function scanCategory(category: string): ManifestCase[] { return cases; } -export async function generateConformance(): Promise { +/** + * Generate TypeScript types from JSON schemas. + */ +async function generateTypes(): Promise { + const testCaseSchema = JSON.parse( + readFileSync(join(SCHEMAS_PATH, "test-case.schema.json"), "utf-8"), + ); + const manifestSchema = JSON.parse( + readFileSync(join(SCHEMAS_PATH, "manifest.schema.json"), "utf-8"), + ); + + // Generate types from schemas + const testCaseTypes = await compile(testCaseSchema, "TestCase", { + bannerComment: "", + additionalProperties: false, + }); + + const manifestTypes = await compile(manifestSchema, "Manifest", { + bannerComment: "", + additionalProperties: false, + }); + + // Extract the generated interface names from the output + // json-schema-to-typescript uses the schema title for the interface name + const testCaseMatch = testCaseTypes.match(/export interface (\w+)/); + const manifestMatch = manifestTypes.match(/export interface (\w+)/); + const testCaseInterfaceName = testCaseMatch?.[1] ?? "TestCase"; + const manifestInterfaceName = manifestMatch?.[1] ?? "Manifest"; + + // Combine into a single declaration file with friendly type aliases + const output = `/** + * Type declarations for @oxa/conformance + * + * AUTO-GENERATED from JSON schemas - do not edit directly. + * Run \`pnpm codegen conformance\` to regenerate. + */ + +${testCaseTypes} + +${manifestTypes} + +// Friendly type aliases +export type TestCase = ${testCaseInterfaceName}; +export type Manifest = ${manifestInterfaceName}; +export type ManifestCase = Manifest["cases"][number]; + +declare const manifest: Manifest; +export default manifest; +`; + + const formatted = await prettier.format(output, { + parser: "typescript", + filepath: TYPES_OUTPUT_PATH, + }); + + writeFileSync(TYPES_OUTPUT_PATH, formatted); + console.log(`Generated ${TYPES_OUTPUT_PATH}`); +} + +/** + * Generate manifest.json from test case files. + */ +async function generateManifest(): Promise { // Get version from oxa-conformance package.json const pkgPath = join(CONFORMANCE_PATH, "package.json"); const pkg = JSON.parse(readFileSync(pkgPath, "utf-8")); @@ -141,9 +206,16 @@ export async function generateConformance(): Promise { const json = JSON.stringify(manifest, null, 2); const formatted = await prettier.format(json, { parser: "json", - filepath: OUTPUT_PATH, + filepath: MANIFEST_OUTPUT_PATH, }); - writeFileSync(OUTPUT_PATH, formatted); - console.log(`Generated ${OUTPUT_PATH} with ${allCases.length} test cases`); + writeFileSync(MANIFEST_OUTPUT_PATH, formatted); + console.log( + `Generated ${MANIFEST_OUTPUT_PATH} with ${allCases.length} test cases`, + ); +} + +export async function generateConformance(): Promise { + await generateTypes(); + await generateManifest(); } From c58885db493b9ece2efed3af0408293b0e464e90 Mon Sep 17 00:00:00 2001 From: Nokome Bentley Date: Thu, 22 Jan 2026 19:06:07 +1300 Subject: [PATCH 05/14] test(conformance): add initial test cases with testing --- .../cases/block/heading-basic.json | 45 ++++++ .../cases/block/paragraph-basic.json | 35 +++++ .../cases/inline/emphasis-basic.json | 38 +++++ .../cases/inline/strong-basic.json | 38 +++++ .../cases/inline/text-basic.json | 27 ++++ packages/oxa-conformance/index.d.ts | 130 ++++++++++++++++++ packages/oxa-core/package.json | 1 + packages/oxa-core/src/conformance.test.ts | 123 +++++++++++++++++ 8 files changed, 437 insertions(+) create mode 100644 packages/oxa-conformance/cases/block/heading-basic.json create mode 100644 packages/oxa-conformance/cases/block/paragraph-basic.json create mode 100644 packages/oxa-conformance/cases/inline/emphasis-basic.json create mode 100644 packages/oxa-conformance/cases/inline/strong-basic.json create mode 100644 packages/oxa-conformance/cases/inline/text-basic.json create mode 100644 packages/oxa-conformance/index.d.ts create mode 100644 packages/oxa-core/src/conformance.test.ts diff --git a/packages/oxa-conformance/cases/block/heading-basic.json b/packages/oxa-conformance/cases/block/heading-basic.json new file mode 100644 index 0000000..38ac247 --- /dev/null +++ b/packages/oxa-conformance/cases/block/heading-basic.json @@ -0,0 +1,45 @@ +{ + "$schema": "../../schemas/test-case.schema.json", + "title": "Basic heading", + "description": "A level 1 heading containing text", + "category": "block", + "formats": { + "oxa": { + "type": "Heading", + "level": 1, + "children": [ + { "type": "Text", "value": "Introduction" } + ] + }, + "myst": { + "type": "heading", + "depth": 1, + "children": [ + { "type": "text", "value": "Introduction" } + ] + }, + "pandoc": { + "t": "Header", + "c": [ + 1, + ["", [], []], + [{ "t": "Str", "c": "Introduction" }] + ] + }, + "stencila": { + "type": "Heading", + "depth": 1, + "content": [ + { "type": "Text", "value": "Introduction" } + ] + }, + "markdown": "# Introduction", + "html": "

Introduction

", + "jats": "Introduction" + }, + "notes": { + "myst": "MyST uses 'depth' instead of 'level'", + "stencila": "Stencila uses 'depth' instead of 'level'", + "jats": "JATS uses for section headings within <sec> elements" + } +} diff --git a/packages/oxa-conformance/cases/block/paragraph-basic.json b/packages/oxa-conformance/cases/block/paragraph-basic.json new file mode 100644 index 0000000..ce2e98e --- /dev/null +++ b/packages/oxa-conformance/cases/block/paragraph-basic.json @@ -0,0 +1,35 @@ +{ + "$schema": "../../schemas/test-case.schema.json", + "title": "Basic paragraph", + "description": "A simple paragraph containing text", + "category": "block", + "formats": { + "oxa": { + "type": "Paragraph", + "children": [ + { "type": "Text", "value": "This is a paragraph." } + ] + }, + "myst": { + "type": "paragraph", + "children": [ + { "type": "text", "value": "This is a paragraph." } + ] + }, + "pandoc": { + "t": "Para", + "c": [ + { "t": "Str", "c": "This is a paragraph." } + ] + }, + "stencila": { + "type": "Paragraph", + "content": [ + { "type": "Text", "value": "This is a paragraph." } + ] + }, + "markdown": "This is a paragraph.", + "html": "<p>This is a paragraph.</p>", + "jats": "<p>This is a paragraph.</p>" + } +} diff --git a/packages/oxa-conformance/cases/inline/emphasis-basic.json b/packages/oxa-conformance/cases/inline/emphasis-basic.json new file mode 100644 index 0000000..c24d578 --- /dev/null +++ b/packages/oxa-conformance/cases/inline/emphasis-basic.json @@ -0,0 +1,38 @@ +{ + "$schema": "../../schemas/test-case.schema.json", + "title": "Basic emphasis", + "description": "Emphasized text (typically rendered as italic)", + "category": "inline", + "formats": { + "oxa": { + "type": "Emphasis", + "children": [ + { "type": "Text", "value": "emphasized text" } + ] + }, + "myst": { + "type": "emphasis", + "children": [ + { "type": "text", "value": "emphasized text" } + ] + }, + "pandoc": { + "t": "Emph", + "c": [ + { "t": "Str", "c": "emphasized text" } + ] + }, + "stencila": { + "type": "Emphasis", + "content": [ + { "type": "Text", "value": "emphasized text" } + ] + }, + "markdown": "*emphasized text*", + "html": "<em>emphasized text</em>", + "jats": "<italic>emphasized text</italic>" + }, + "notes": { + "markdown": "Also accepts _emphasized text_ as input" + } +} diff --git a/packages/oxa-conformance/cases/inline/strong-basic.json b/packages/oxa-conformance/cases/inline/strong-basic.json new file mode 100644 index 0000000..8eb765f --- /dev/null +++ b/packages/oxa-conformance/cases/inline/strong-basic.json @@ -0,0 +1,38 @@ +{ + "$schema": "../../schemas/test-case.schema.json", + "title": "Basic strong emphasis", + "description": "Strongly emphasized text (typically rendered as bold)", + "category": "inline", + "formats": { + "oxa": { + "type": "Strong", + "children": [ + { "type": "Text", "value": "strong text" } + ] + }, + "myst": { + "type": "strong", + "children": [ + { "type": "text", "value": "strong text" } + ] + }, + "pandoc": { + "t": "Strong", + "c": [ + { "t": "Str", "c": "strong text" } + ] + }, + "stencila": { + "type": "Strong", + "content": [ + { "type": "Text", "value": "strong text" } + ] + }, + "markdown": "**strong text**", + "html": "<strong>strong text</strong>", + "jats": "<bold>strong text</bold>" + }, + "notes": { + "markdown": "Also accepts __strong text__ as input" + } +} diff --git a/packages/oxa-conformance/cases/inline/text-basic.json b/packages/oxa-conformance/cases/inline/text-basic.json new file mode 100644 index 0000000..9d1b2a6 --- /dev/null +++ b/packages/oxa-conformance/cases/inline/text-basic.json @@ -0,0 +1,27 @@ +{ + "$schema": "../../schemas/test-case.schema.json", + "title": "Basic text node", + "description": "A simple text node containing a string value", + "category": "inline", + "formats": { + "oxa": { + "type": "Text", + "value": "Hello, world!" + }, + "myst": { + "type": "text", + "value": "Hello, world!" + }, + "pandoc": { + "t": "Str", + "c": "Hello, world!" + }, + "stencila": { + "type": "Text", + "value": "Hello, world!" + }, + "markdown": "Hello, world!", + "html": "Hello, world!", + "jats": "Hello, world!" + } +} diff --git a/packages/oxa-conformance/index.d.ts b/packages/oxa-conformance/index.d.ts new file mode 100644 index 0000000..215f5ce --- /dev/null +++ b/packages/oxa-conformance/index.d.ts @@ -0,0 +1,130 @@ +/** + * Type declarations for @oxa/conformance + * + * AUTO-GENERATED from JSON schemas - do not edit directly. + * Run `pnpm codegen conformance` to regenerate. + */ + +/** + * A test case for validating OXA format conversions + */ +export interface OXAConformanceTestCase { + /** + * Reference to this schema + */ + $schema?: string; + /** + * Short title describing the test case + */ + title: string; + /** + * Detailed description of what the test case validates + */ + description?: string; + /** + * Category of the test case + */ + category: "inline" | "block" | "document"; + /** + * Format representations of the same content. OXA is always required and should be first. + */ + formats: { + /** + * OXA JSON representation (required, authoritative) + */ + oxa: { + [k: string]: unknown; + }; + /** + * MyST Markdown AST representation + */ + myst?: { + [k: string]: unknown; + }; + /** + * Pandoc AST JSON representation + */ + pandoc?: { + [k: string]: unknown; + }; + /** + * Stencila Schema JSON representation + */ + stencila?: { + [k: string]: unknown; + }; + /** + * CommonMark/GFM Markdown representation + */ + markdown?: string; + /** + * Semantic HTML5 representation + */ + html?: string; + /** + * JATS Publishing XML representation + */ + jats?: string; + }; + /** + * Format-specific notes and considerations + */ + notes?: { + [k: string]: string; + }; +} + +/** + * Index of all test cases in the OXA Conformance Suite + */ +export interface OXAConformanceSuiteManifest { + /** + * Reference to this schema + */ + $schema?: string; + /** + * Version of the conformance suite + */ + version: string; + /** + * List of supported formats in canonical order + */ + formats: ( + | "oxa" + | "myst" + | "pandoc" + | "stencila" + | "markdown" + | "html" + | "jats" + )[]; + /** + * List of all test cases + */ + cases: { + /** + * Unique identifier for the test case (derived from filename) + */ + id: string; + /** + * Relative path to the test case file + */ + path: string; + /** + * Category of the test case + */ + category: "inline" | "block" | "document" | "edge-case"; + /** + * OXA node types covered by this test case + */ + nodeTypes: string[]; + }[]; +} + +// Friendly type aliases +export type TestCase = OXAConformanceTestCase; +export type Manifest = OXAConformanceSuiteManifest; +export type ManifestCase = Manifest["cases"][number]; + +declare const manifest: Manifest; +export default manifest; diff --git a/packages/oxa-core/package.json b/packages/oxa-core/package.json index 89ca201..28804a5 100644 --- a/packages/oxa-core/package.json +++ b/packages/oxa-core/package.json @@ -36,6 +36,7 @@ "oxa-types": "workspace:*" }, "devDependencies": { + "@oxa/conformance": "workspace:*", "@types/js-yaml": "^4.0.9", "@types/node": "^22.0.0", "esbuild": "^0.25.0", diff --git a/packages/oxa-core/src/conformance.test.ts b/packages/oxa-core/src/conformance.test.ts new file mode 100644 index 0000000..8b9c1b8 --- /dev/null +++ b/packages/oxa-core/src/conformance.test.ts @@ -0,0 +1,123 @@ +/** + * Tests that all OXA conformance test cases validate against the OXA schema. + * + * This ensures that: + * 1. All test cases in the conformance suite are valid OXA documents + * 2. The test cases stay in sync with schema changes + */ + +import { describe, it, expect } from "vitest"; +import { readFileSync } from "fs"; +import { dirname, join } from "path"; +import { fileURLToPath } from "url"; +import manifest from "@oxa/conformance"; +import { validate } from "./validate.js"; + +// Resolve the path to the conformance package for loading individual test case files +const CONFORMANCE_PATH = dirname( + fileURLToPath(import.meta.resolve("@oxa/conformance")), +); + +interface TestCase { + title: string; + category: string; + formats: { + oxa: Record<string, unknown>; + }; +} + +describe("OXA Conformance Suite", () => { + describe("manifest", () => { + it("has valid version", () => { + expect(manifest.version).toMatch(/^\d+\.\d+\.\d+$/); + }); + + it("has expected formats in canonical order", () => { + expect(manifest.formats).toEqual([ + "oxa", + "myst", + "pandoc", + "stencila", + "markdown", + "html", + "jats", + ]); + }); + + it("has at least one test case", () => { + expect(manifest.cases.length).toBeGreaterThan(0); + }); + }); + + describe("test cases", () => { + for (const testCaseMeta of manifest.cases) { + describe(`${testCaseMeta.category}/${testCaseMeta.id}`, () => { + const casePath = join(CONFORMANCE_PATH, testCaseMeta.path); + const testCase: TestCase = JSON.parse(readFileSync(casePath, "utf-8")); + + it("has required fields", () => { + expect(testCase.title).toBeDefined(); + expect(testCase.category).toBeDefined(); + expect(testCase.formats).toBeDefined(); + expect(testCase.formats.oxa).toBeDefined(); + }); + + it("category matches manifest", () => { + expect(testCase.category).toBe(testCaseMeta.category); + }); + + it("OXA format validates against schema", () => { + // Determine the root type from the OXA content + const oxa = testCase.formats.oxa; + const rootType = oxa.type as string; + + const result = validate(oxa, { type: rootType }); + + if (!result.valid) { + // Provide helpful error message + const errors = result.errors.map((e) => e.message).join("\n "); + expect.fail( + `OXA content failed validation:\n ${errors}\n\nContent: ${JSON.stringify(oxa, null, 2)}`, + ); + } + + expect(result.valid).toBe(true); + }); + + it("nodeTypes in manifest match actual types in OXA", () => { + // Compare as sorted arrays to avoid Set iteration order issues + const actualTypes = Array.from(extractTypes(testCase.formats.oxa)).sort(); + const manifestTypes = [...testCaseMeta.nodeTypes].sort(); + + expect(actualTypes).toEqual(manifestTypes); + }); + }); + } + }); +}); + +/** + * Extract all type values from an OXA node tree. + */ +function extractTypes(node: unknown): Set<string> { + const types = new Set<string>(); + + function walk(n: unknown): void { + if (n && typeof n === "object" && !Array.isArray(n)) { + const obj = n as Record<string, unknown>; + if (typeof obj.type === "string") { + types.add(obj.type); + } + for (const value of Object.values(obj)) { + walk(value); + } + } else if (Array.isArray(n)) { + for (const item of n) { + walk(item); + } + } + } + + walk(node); + return types; +} From ad788b163ce4755ff2b7f63d6127ace4a054bbdb Mon Sep 17 00:00:00 2001 From: Nokome Bentley <nokome@stenci.la> Date: Thu, 22 Jan 2026 06:07:04 +0000 Subject: [PATCH 06/14] chore(*): update formatted/generated files [skip ci] --- packages/oxa-core/src/conformance.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/oxa-core/src/conformance.test.ts b/packages/oxa-core/src/conformance.test.ts index 8b9c1b8..a9b4b0d 100644 --- a/packages/oxa-core/src/conformance.test.ts +++ b/packages/oxa-core/src/conformance.test.ts @@ -86,7 +86,9 @@ describe("OXA Conformance Suite", () => { it("nodeTypes in manifest match actual types in OXA", () => { // Compare as sorted arrays to avoid Set iteration order issues - const actualTypes = Array.from(extractTypes(testCase.formats.oxa)).sort(); + const actualTypes = Array.from( + extractTypes(testCase.formats.oxa), + ).sort(); const manifestTypes = [...testCaseMeta.nodeTypes].sort(); expect(actualTypes).toEqual(manifestTypes); From 9ed3efd84cf8fb091203d9ec12059660aab8ad81 Mon Sep 17 00:00:00 2001 From: Nokome Bentley <nokome@stenci.la> Date: Thu, 22 Jan 2026 21:50:28 +1300 Subject: [PATCH 07/14] docs(schema): use conformance test cases in documentation --- docs/schema/emphasis.md | 54 +++++++++++++++++++++ docs/schema/heading.md | 54 +++++++++++++++++++++ docs/schema/paragraph.md | 54 +++++++++++++++++++++ docs/schema/strong.md | 54 +++++++++++++++++++++ docs/schema/text.md | 54 +++++++++++++++++++++ pnpm-lock.yaml | 3 ++ scripts/lib/generate-docs.ts | 94 ++++++++++++++++++++++++++++++++++-- scripts/package.json | 1 + 8 files changed, 365 insertions(+), 3 deletions(-) diff --git a/docs/schema/emphasis.md b/docs/schema/emphasis.md index 581683d..625b63d 100644 --- a/docs/schema/emphasis.md +++ b/docs/schema/emphasis.md @@ -26,3 +26,57 @@ __children__: __array__ ("Inline") : The inline content to emphasize. : See @oxa:inline + +### Example + +````{tab-set} +```{tab-item} OXA +:sync: oxa +```json +{"type":"Emphasis","children":[{"type":"Text","value":"emphasized text"}]} +``` +``` + +```{tab-item} MyST +:sync: myst +```json +{"type":"emphasis","children":[{"type":"text","value":"emphasized text"}]} +``` +``` + +```{tab-item} Pandoc +:sync: pandoc +```json +{"t":"Emph","c":[{"t":"Str","c":"emphasized text"}]} +``` +``` + +```{tab-item} Stencila +:sync: stencila +```json +{"type":"Emphasis","content":[{"type":"Text","value":"emphasized text"}]} +``` +``` + +```{tab-item} Markdown +:sync: markdown +```markdown +*emphasized text* +``` +``` + +```{tab-item} HTML +:sync: html +```html +<em>emphasized text</em> +``` +``` + +```{tab-item} JATS +:sync: jats +```xml +<italic>emphasized text</italic> +``` +``` + +```` \ No newline at end of file diff --git a/docs/schema/heading.md b/docs/schema/heading.md index cd6a33d..5236407 100644 --- a/docs/schema/heading.md +++ b/docs/schema/heading.md @@ -30,3 +30,57 @@ __children__: __array__ ("Inline") : The inline content of the heading. : See @oxa:inline + +### Example + +````{tab-set} +```{tab-item} OXA +:sync: oxa +```json +{"type":"Heading","level":1,"children":[{"type":"Text","value":"Introduction"}]} +``` +``` + +```{tab-item} MyST +:sync: myst +```json +{"type":"heading","depth":1,"children":[{"type":"text","value":"Introduction"}]} +``` +``` + +```{tab-item} Pandoc +:sync: pandoc +```json +{"t":"Header","c":[1,["",[],[]],[{"t":"Str","c":"Introduction"}]]} +``` +``` + +```{tab-item} Stencila +:sync: stencila +```json +{"type":"Heading","depth":1,"content":[{"type":"Text","value":"Introduction"}]} +``` +``` + +```{tab-item} Markdown +:sync: markdown +```markdown +# Introduction +``` +``` + +```{tab-item} HTML +:sync: html +```html +<h1>Introduction</h1> +``` +``` + +```{tab-item} JATS +:sync: jats +```xml +<title>Introduction +``` +``` + +```` \ No newline at end of file diff --git a/docs/schema/paragraph.md b/docs/schema/paragraph.md index 7e11cd7..e3fe4fe 100644 --- a/docs/schema/paragraph.md +++ b/docs/schema/paragraph.md @@ -26,3 +26,57 @@ __children__: __array__ ("Inline") : The inline content of the paragraph. : See @oxa:inline + +### Example + +````{tab-set} +```{tab-item} OXA +:sync: oxa +```json +{"type":"Paragraph","children":[{"type":"Text","value":"This is a paragraph."}]} +``` +``` + +```{tab-item} MyST +:sync: myst +```json +{"type":"paragraph","children":[{"type":"text","value":"This is a paragraph."}]} +``` +``` + +```{tab-item} Pandoc +:sync: pandoc +```json +{"t":"Para","c":[{"t":"Str","c":"This is a paragraph."}]} +``` +``` + +```{tab-item} Stencila +:sync: stencila +```json +{"type":"Paragraph","content":[{"type":"Text","value":"This is a paragraph."}]} +``` +``` + +```{tab-item} Markdown +:sync: markdown +```markdown +This is a paragraph. +``` +``` + +```{tab-item} HTML +:sync: html +```html +

This is a paragraph.

+``` +``` + +```{tab-item} JATS +:sync: jats +```xml +

This is a paragraph.

+``` +``` + +```` \ No newline at end of file diff --git a/docs/schema/strong.md b/docs/schema/strong.md index 8aa14de..1f967da 100644 --- a/docs/schema/strong.md +++ b/docs/schema/strong.md @@ -26,3 +26,57 @@ __children__: __array__ ("Inline") : The inline content to emphasize. : See @oxa:inline + +### Example + +````{tab-set} +```{tab-item} OXA +:sync: oxa +```json +{"type":"Strong","children":[{"type":"Text","value":"strong text"}]} +``` +``` + +```{tab-item} MyST +:sync: myst +```json +{"type":"strong","children":[{"type":"text","value":"strong text"}]} +``` +``` + +```{tab-item} Pandoc +:sync: pandoc +```json +{"t":"Strong","c":[{"t":"Str","c":"strong text"}]} +``` +``` + +```{tab-item} Stencila +:sync: stencila +```json +{"type":"Strong","content":[{"type":"Text","value":"strong text"}]} +``` +``` + +```{tab-item} Markdown +:sync: markdown +```markdown +**strong text** +``` +``` + +```{tab-item} HTML +:sync: html +```html +strong text +``` +``` + +```{tab-item} JATS +:sync: jats +```xml +strong text +``` +``` + +```` \ No newline at end of file diff --git a/docs/schema/text.md b/docs/schema/text.md index 4da7a92..2abdfe2 100644 --- a/docs/schema/text.md +++ b/docs/schema/text.md @@ -25,3 +25,57 @@ __data__: __object__ __value__: __string__ : The text content. + +### Example + +````{tab-set} +```{tab-item} OXA +:sync: oxa +```json +{"type":"Text","value":"Hello, world!"} +``` +``` + +```{tab-item} MyST +:sync: myst +```json +{"type":"text","value":"Hello, world!"} +``` +``` + +```{tab-item} Pandoc +:sync: pandoc +```json +{"t":"Str","c":"Hello, world!"} +``` +``` + +```{tab-item} Stencila +:sync: stencila +```json +{"type":"Text","value":"Hello, world!"} +``` +``` + +```{tab-item} Markdown +:sync: markdown +```markdown +Hello, world! +``` +``` + +```{tab-item} HTML +:sync: html +```html +Hello, world! +``` +``` + +```{tab-item} JATS +:sync: jats +```xml +Hello, world! +``` +``` + +```` \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e692a4b..358e81d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -91,6 +91,9 @@ importers: scripts: dependencies: + '@oxa/conformance': + specifier: workspace:* + version: link:../packages/oxa-conformance ajv: specifier: ^8.17.1 version: 8.17.1 diff --git a/scripts/lib/generate-docs.ts b/scripts/lib/generate-docs.ts index 9300aa6..2ac84b8 100644 --- a/scripts/lib/generate-docs.ts +++ b/scripts/lib/generate-docs.ts @@ -6,13 +6,20 @@ */ import { mkdirSync, rmSync, writeFileSync, existsSync, readFileSync } from "fs"; -import { join } from "path"; +import { createRequire } from "module"; +import { join, dirname } from "path"; + +import manifest, { type TestCase } from "@oxa/conformance"; import { loadMergedSchema } from "./schema.js"; const OUTPUT_DIR = join(import.meta.dirname, "../../docs/schema"); const INDEX_FILE = join(OUTPUT_DIR, "index.md"); +// Resolve the conformance package directory for loading test case files +const require = createRequire(import.meta.url); +const CONFORMANCE_DIR = dirname(require.resolve("@oxa/conformance")); + interface SchemaProperty { type?: string; const?: string; @@ -54,11 +61,12 @@ export async function generateDocs(): Promise { const schema = loadMergedSchema(); const definitions = schema.definitions as Record; + const testCases = loadTestCases(); // Generate documentation for object types (non-union types) for (const [name, def] of Object.entries(definitions)) { if (!def.anyOf && def.type === "object") { - const content = generateDocContent(name, def); + const content = generateDocContent(name, def, testCases); const filePath = join(OUTPUT_DIR, `${name.toLowerCase()}.md`); writeFileSync(filePath, content); console.log(`Generated ${filePath}`); @@ -76,7 +84,11 @@ export async function generateDocs(): Promise { } } -function generateDocContent(name: string, def: SchemaDefinition): string { +function generateDocContent( + name: string, + def: SchemaDefinition, + testCases: Map, +): string { const lines: string[] = []; // Frontmatter @@ -127,6 +139,12 @@ function generateDocContent(name: string, def: SchemaDefinition): string { lines.push(""); } + // Add test case example if available + const testCase = testCases.get(name.toLowerCase()); + if (testCase) { + lines.push(generateTestCaseSection(testCase)); + } + return lines.join("\n"); } @@ -199,3 +217,73 @@ function getArrayItemType(items: { $ref?: string; type?: string }): string { } return "unknown"; } + +function loadTestCases(): Map { + const testCases = new Map(); + + // Filter for *-basic test cases and load them + for (const caseInfo of manifest.cases) { + if (!caseInfo.id.endsWith("-basic")) continue; + + // Primary node type is first in nodeTypes array + const primaryType = caseInfo.nodeTypes[0]; + if (!primaryType) continue; + + const filePath = join(CONFORMANCE_DIR, caseInfo.path); + const content = readFileSync(filePath, "utf-8"); + const testCase = JSON.parse(content) as TestCase; + testCases.set(primaryType.toLowerCase(), testCase); + } + + return testCases; +} + +const FORMAT_LABELS: Record = { + oxa: "OXA", + myst: "MyST", + pandoc: "Pandoc", + stencila: "Stencila", + markdown: "Markdown", + html: "HTML", + jats: "JATS", +}; + +const FORMAT_LANGUAGES: Record = { + oxa: "json", + myst: "json", + pandoc: "json", + stencila: "json", + markdown: "markdown", + html: "html", + jats: "xml", +}; + +function generateTestCaseSection(testCase: TestCase): string { + const lines: string[] = []; + + lines.push("### Example"); + lines.push(""); + lines.push("````{tab-set}"); + + for (const format of manifest.formats) { + const value = testCase.formats[format as keyof typeof testCase.formats]; + if (value === undefined) continue; + + const label = FORMAT_LABELS[format]; + const lang = FORMAT_LANGUAGES[format]; + const content = + typeof value === "string" ? value : JSON.stringify(value); + + lines.push(`\`\`\`{tab-item} ${label}`); + lines.push(`:sync: ${format}`); + lines.push(`\`\`\`${lang}`); + lines.push(content); + lines.push("```"); + lines.push("```"); + lines.push(""); + } + + lines.push("````"); + + return lines.join("\n"); +} diff --git a/scripts/package.json b/scripts/package.json index 73c1d2a..de97d69 100644 --- a/scripts/package.json +++ b/scripts/package.json @@ -11,6 +11,7 @@ "lint:fix": "eslint --fix ." }, "dependencies": { + "@oxa/conformance": "workspace:*", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "commander": "^14.0.2", From 948564a6ba6096442caea7e5fa8d266dfc02a7c8 Mon Sep 17 00:00:00 2001 From: Nokome Bentley Date: Thu, 22 Jan 2026 08:51:22 +0000 Subject: [PATCH 08/14] chore(*): update formatted/generated files [skip ci] --- scripts/lib/generate-docs.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/scripts/lib/generate-docs.ts b/scripts/lib/generate-docs.ts index 2ac84b8..c19c2a8 100644 --- a/scripts/lib/generate-docs.ts +++ b/scripts/lib/generate-docs.ts @@ -271,8 +271,7 @@ function generateTestCaseSection(testCase: TestCase): string { const label = FORMAT_LABELS[format]; const lang = FORMAT_LANGUAGES[format]; - const content = - typeof value === "string" ? value : JSON.stringify(value); + const content = typeof value === "string" ? value : JSON.stringify(value); lines.push(`\`\`\`{tab-item} ${label}`); lines.push(`:sync: ${format}`); From fe41bd5fa095194911423d4a72162702a42df9bb Mon Sep 17 00:00:00 2001 From: Nokome Bentley Date: Tue, 27 Jan 2026 12:29:29 +1300 Subject: [PATCH 09/14] refactor(conformance): add markdown flavors --- docs/schema/emphasis.md | 57 +++++++++++++------ docs/schema/heading.md | 57 +++++++++++++------ docs/schema/paragraph.md | 57 +++++++++++++------ docs/schema/strong.md | 57 +++++++++++++------ docs/schema/text.md | 57 +++++++++++++------ .../cases/block/heading-basic.json | 16 ++++-- .../cases/block/paragraph-basic.json | 14 ++++- .../cases/inline/emphasis-basic.json | 14 +++-- .../cases/inline/strong-basic.json | 14 +++-- .../cases/inline/text-basic.json | 14 ++++- packages/oxa-conformance/index.d.ts | 27 +++++++-- packages/oxa-conformance/manifest.json | 13 ++++- .../schemas/manifest.schema.json | 2 +- .../schemas/test-case.schema.json | 18 +++++- packages/oxa-core/src/conformance.test.ts | 9 ++- scripts/lib/generate-conformance.ts | 2 +- scripts/lib/generate-docs.ts | 26 +++++---- 17 files changed, 320 insertions(+), 134 deletions(-) diff --git a/docs/schema/emphasis.md b/docs/schema/emphasis.md index 625b63d..e5a24af 100644 --- a/docs/schema/emphasis.md +++ b/docs/schema/emphasis.md @@ -29,54 +29,75 @@ __children__: __array__ ("Inline") ### Example -````{tab-set} -```{tab-item} OXA +`````{tab-set} +````{tab-item} OXA :sync: oxa ```json {"type":"Emphasis","children":[{"type":"Text","value":"emphasized text"}]} ``` -``` +```` -```{tab-item} MyST -:sync: myst +````{tab-item} MyST AST +:sync: myst-ast ```json {"type":"emphasis","children":[{"type":"text","value":"emphasized text"}]} ``` -``` +```` -```{tab-item} Pandoc -:sync: pandoc +````{tab-item} Pandoc Types +:sync: pandoc-types ```json {"t":"Emph","c":[{"t":"Str","c":"emphasized text"}]} ``` -``` +```` -```{tab-item} Stencila -:sync: stencila +````{tab-item} Stencila Schema +:sync: stencila-schema ```json {"type":"Emphasis","content":[{"type":"Text","value":"emphasized text"}]} ``` -``` +```` -```{tab-item} Markdown +````{tab-item} Markdown :sync: markdown ```markdown *emphasized text* ``` +```` + +````{tab-item} MyST Markdown +:sync: myst-markdown +```markdown +*emphasized text* ``` +```` -```{tab-item} HTML +````{tab-item} Stencila Markdown +:sync: stencila-markdown +```markdown +*emphasized text* +``` +```` + +````{tab-item} Quarto Markdown +:sync: quarto-markdown +```markdown +*emphasized text* +``` +```` + +````{tab-item} HTML :sync: html ```html emphasized text ``` -``` +```` -```{tab-item} JATS +````{tab-item} JATS :sync: jats ```xml emphasized text ``` -``` +```` -```` \ No newline at end of file +````` \ No newline at end of file diff --git a/docs/schema/heading.md b/docs/schema/heading.md index 5236407..4d5808e 100644 --- a/docs/schema/heading.md +++ b/docs/schema/heading.md @@ -33,54 +33,75 @@ __children__: __array__ ("Inline") ### Example -````{tab-set} -```{tab-item} OXA +`````{tab-set} +````{tab-item} OXA :sync: oxa ```json {"type":"Heading","level":1,"children":[{"type":"Text","value":"Introduction"}]} ``` -``` +```` -```{tab-item} MyST -:sync: myst +````{tab-item} MyST AST +:sync: myst-ast ```json {"type":"heading","depth":1,"children":[{"type":"text","value":"Introduction"}]} ``` -``` +```` -```{tab-item} Pandoc -:sync: pandoc +````{tab-item} Pandoc Types +:sync: pandoc-types ```json {"t":"Header","c":[1,["",[],[]],[{"t":"Str","c":"Introduction"}]]} ``` -``` +```` -```{tab-item} Stencila -:sync: stencila +````{tab-item} Stencila Schema +:sync: stencila-schema ```json {"type":"Heading","depth":1,"content":[{"type":"Text","value":"Introduction"}]} ``` -``` +```` -```{tab-item} Markdown +````{tab-item} Markdown :sync: markdown ```markdown # Introduction ``` +```` + +````{tab-item} MyST Markdown +:sync: myst-markdown +```markdown +# Introduction ``` +```` -```{tab-item} HTML +````{tab-item} Stencila Markdown +:sync: stencila-markdown +```markdown +# Introduction +``` +```` + +````{tab-item} Quarto Markdown +:sync: quarto-markdown +```markdown +# Introduction +``` +```` + +````{tab-item} HTML :sync: html ```html

Introduction

``` -``` +```` -```{tab-item} JATS +````{tab-item} JATS :sync: jats ```xml Introduction ``` -``` +```` -```` \ No newline at end of file +````` \ No newline at end of file diff --git a/docs/schema/paragraph.md b/docs/schema/paragraph.md index e3fe4fe..c9c26ce 100644 --- a/docs/schema/paragraph.md +++ b/docs/schema/paragraph.md @@ -29,54 +29,75 @@ __children__: __array__ ("Inline") ### Example -````{tab-set} -```{tab-item} OXA +`````{tab-set} +````{tab-item} OXA :sync: oxa ```json {"type":"Paragraph","children":[{"type":"Text","value":"This is a paragraph."}]} ``` -``` +```` -```{tab-item} MyST -:sync: myst +````{tab-item} MyST AST +:sync: myst-ast ```json {"type":"paragraph","children":[{"type":"text","value":"This is a paragraph."}]} ``` -``` +```` -```{tab-item} Pandoc -:sync: pandoc +````{tab-item} Pandoc Types +:sync: pandoc-types ```json {"t":"Para","c":[{"t":"Str","c":"This is a paragraph."}]} ``` -``` +```` -```{tab-item} Stencila -:sync: stencila +````{tab-item} Stencila Schema +:sync: stencila-schema ```json {"type":"Paragraph","content":[{"type":"Text","value":"This is a paragraph."}]} ``` -``` +```` -```{tab-item} Markdown +````{tab-item} Markdown :sync: markdown ```markdown This is a paragraph. ``` +```` + +````{tab-item} MyST Markdown +:sync: myst-markdown +```markdown +This is a paragraph. ``` +```` -```{tab-item} HTML +````{tab-item} Stencila Markdown +:sync: stencila-markdown +```markdown +This is a paragraph. +``` +```` + +````{tab-item} Quarto Markdown +:sync: quarto-markdown +```markdown +This is a paragraph. +``` +```` + +````{tab-item} HTML :sync: html ```html

This is a paragraph.

``` -``` +```` -```{tab-item} JATS +````{tab-item} JATS :sync: jats ```xml

This is a paragraph.

``` -``` +```` -```` \ No newline at end of file +````` \ No newline at end of file diff --git a/docs/schema/strong.md b/docs/schema/strong.md index 1f967da..fab4254 100644 --- a/docs/schema/strong.md +++ b/docs/schema/strong.md @@ -29,54 +29,75 @@ __children__: __array__ ("Inline") ### Example -````{tab-set} -```{tab-item} OXA +`````{tab-set} +````{tab-item} OXA :sync: oxa ```json {"type":"Strong","children":[{"type":"Text","value":"strong text"}]} ``` -``` +```` -```{tab-item} MyST -:sync: myst +````{tab-item} MyST AST +:sync: myst-ast ```json {"type":"strong","children":[{"type":"text","value":"strong text"}]} ``` -``` +```` -```{tab-item} Pandoc -:sync: pandoc +````{tab-item} Pandoc Types +:sync: pandoc-types ```json {"t":"Strong","c":[{"t":"Str","c":"strong text"}]} ``` -``` +```` -```{tab-item} Stencila -:sync: stencila +````{tab-item} Stencila Schema +:sync: stencila-schema ```json {"type":"Strong","content":[{"type":"Text","value":"strong text"}]} ``` -``` +```` -```{tab-item} Markdown +````{tab-item} Markdown :sync: markdown ```markdown **strong text** ``` +```` + +````{tab-item} MyST Markdown +:sync: myst-markdown +```markdown +**strong text** ``` +```` -```{tab-item} HTML +````{tab-item} Stencila Markdown +:sync: stencila-markdown +```markdown +**strong text** +``` +```` + +````{tab-item} Quarto Markdown +:sync: quarto-markdown +```markdown +**strong text** +``` +```` + +````{tab-item} HTML :sync: html ```html strong text ``` -``` +```` -```{tab-item} JATS +````{tab-item} JATS :sync: jats ```xml strong text ``` -``` +```` -```` \ No newline at end of file +````` \ No newline at end of file diff --git a/docs/schema/text.md b/docs/schema/text.md index 2abdfe2..5be534a 100644 --- a/docs/schema/text.md +++ b/docs/schema/text.md @@ -28,54 +28,75 @@ __value__: __string__ ### Example -````{tab-set} -```{tab-item} OXA +`````{tab-set} +````{tab-item} OXA :sync: oxa ```json {"type":"Text","value":"Hello, world!"} ``` -``` +```` -```{tab-item} MyST -:sync: myst +````{tab-item} MyST AST +:sync: myst-ast ```json {"type":"text","value":"Hello, world!"} ``` -``` +```` -```{tab-item} Pandoc -:sync: pandoc +````{tab-item} Pandoc Types +:sync: pandoc-types ```json {"t":"Str","c":"Hello, world!"} ``` -``` +```` -```{tab-item} Stencila -:sync: stencila +````{tab-item} Stencila Schema +:sync: stencila-schema ```json {"type":"Text","value":"Hello, world!"} ``` -``` +```` -```{tab-item} Markdown +````{tab-item} Markdown :sync: markdown ```markdown Hello, world! ``` +```` + +````{tab-item} MyST Markdown +:sync: myst-markdown +```markdown +Hello, world! ``` +```` -```{tab-item} HTML +````{tab-item} Stencila Markdown +:sync: stencila-markdown +```markdown +Hello, world! +``` +```` + +````{tab-item} Quarto Markdown +:sync: quarto-markdown +```markdown +Hello, world! +``` +```` + +````{tab-item} HTML :sync: html ```html Hello, world! ``` -``` +```` -```{tab-item} JATS +````{tab-item} JATS :sync: jats ```xml Hello, world! ``` -``` +```` -```` \ No newline at end of file +````` \ No newline at end of file diff --git a/packages/oxa-conformance/cases/block/heading-basic.json b/packages/oxa-conformance/cases/block/heading-basic.json index 38ac247..1963537 100644 --- a/packages/oxa-conformance/cases/block/heading-basic.json +++ b/packages/oxa-conformance/cases/block/heading-basic.json @@ -11,14 +11,14 @@ { "type": "Text", "value": "Introduction" } ] }, - "myst": { + "myst-ast": { "type": "heading", "depth": 1, "children": [ { "type": "text", "value": "Introduction" } ] }, - "pandoc": { + "pandoc-types": { "t": "Header", "c": [ 1, @@ -26,7 +26,7 @@ [{ "t": "Str", "c": "Introduction" }] ] }, - "stencila": { + "stencila-schema": { "type": "Heading", "depth": 1, "content": [ @@ -34,12 +34,18 @@ ] }, "markdown": "# Introduction", + "myst-markdown": "# Introduction", + "stencila-markdown": "# Introduction", + "quarto-markdown": "# Introduction", "html": "

Introduction

", "jats": "Introduction" }, "notes": { - "myst": "MyST uses 'depth' instead of 'level'", - "stencila": "Stencila uses 'depth' instead of 'level'", + "myst-ast": "MyST uses 'depth' instead of 'level'", + "stencila-schema": "Stencila uses 'depth' instead of 'level'", + "myst-markdown": "Equivalent to markdown for this node type", + "stencila-markdown": "Equivalent to markdown for this node type", + "quarto-markdown": "Equivalent to markdown for this node type", "jats": "JATS uses for section headings within <sec> elements" } } diff --git a/packages/oxa-conformance/cases/block/paragraph-basic.json b/packages/oxa-conformance/cases/block/paragraph-basic.json index ce2e98e..447e2a3 100644 --- a/packages/oxa-conformance/cases/block/paragraph-basic.json +++ b/packages/oxa-conformance/cases/block/paragraph-basic.json @@ -10,26 +10,34 @@ { "type": "Text", "value": "This is a paragraph." } ] }, - "myst": { + "myst-ast": { "type": "paragraph", "children": [ { "type": "text", "value": "This is a paragraph." } ] }, - "pandoc": { + "pandoc-types": { "t": "Para", "c": [ { "t": "Str", "c": "This is a paragraph." } ] }, - "stencila": { + "stencila-schema": { "type": "Paragraph", "content": [ { "type": "Text", "value": "This is a paragraph." } ] }, "markdown": "This is a paragraph.", + "myst-markdown": "This is a paragraph.", + "stencila-markdown": "This is a paragraph.", + "quarto-markdown": "This is a paragraph.", "html": "<p>This is a paragraph.</p>", "jats": "<p>This is a paragraph.</p>" + }, + "notes": { + "myst-markdown": "Equivalent to markdown for this node type", + "stencila-markdown": "Equivalent to markdown for this node type", + "quarto-markdown": "Equivalent to markdown for this node type" } } diff --git a/packages/oxa-conformance/cases/inline/emphasis-basic.json b/packages/oxa-conformance/cases/inline/emphasis-basic.json index c24d578..4af956c 100644 --- a/packages/oxa-conformance/cases/inline/emphasis-basic.json +++ b/packages/oxa-conformance/cases/inline/emphasis-basic.json @@ -10,29 +10,35 @@ { "type": "Text", "value": "emphasized text" } ] }, - "myst": { + "myst-ast": { "type": "emphasis", "children": [ { "type": "text", "value": "emphasized text" } ] }, - "pandoc": { + "pandoc-types": { "t": "Emph", "c": [ { "t": "Str", "c": "emphasized text" } ] }, - "stencila": { + "stencila-schema": { "type": "Emphasis", "content": [ { "type": "Text", "value": "emphasized text" } ] }, "markdown": "*emphasized text*", + "myst-markdown": "*emphasized text*", + "stencila-markdown": "*emphasized text*", + "quarto-markdown": "*emphasized text*", "html": "<em>emphasized text</em>", "jats": "<italic>emphasized text</italic>" }, "notes": { - "markdown": "Also accepts _emphasized text_ as input" + "markdown": "Also accepts _emphasized text_ as input", + "myst-markdown": "Equivalent to markdown for this node type", + "stencila-markdown": "Equivalent to markdown for this node type", + "quarto-markdown": "Equivalent to markdown for this node type" } } diff --git a/packages/oxa-conformance/cases/inline/strong-basic.json b/packages/oxa-conformance/cases/inline/strong-basic.json index 8eb765f..7eff2f3 100644 --- a/packages/oxa-conformance/cases/inline/strong-basic.json +++ b/packages/oxa-conformance/cases/inline/strong-basic.json @@ -10,29 +10,35 @@ { "type": "Text", "value": "strong text" } ] }, - "myst": { + "myst-ast": { "type": "strong", "children": [ { "type": "text", "value": "strong text" } ] }, - "pandoc": { + "pandoc-types": { "t": "Strong", "c": [ { "t": "Str", "c": "strong text" } ] }, - "stencila": { + "stencila-schema": { "type": "Strong", "content": [ { "type": "Text", "value": "strong text" } ] }, "markdown": "**strong text**", + "myst-markdown": "**strong text**", + "stencila-markdown": "**strong text**", + "quarto-markdown": "**strong text**", "html": "<strong>strong text</strong>", "jats": "<bold>strong text</bold>" }, "notes": { - "markdown": "Also accepts __strong text__ as input" + "markdown": "Also accepts __strong text__ as input", + "myst-markdown": "Equivalent to markdown for this node type", + "stencila-markdown": "Equivalent to markdown for this node type", + "quarto-markdown": "Equivalent to markdown for this node type" } } diff --git a/packages/oxa-conformance/cases/inline/text-basic.json b/packages/oxa-conformance/cases/inline/text-basic.json index 9d1b2a6..5343084 100644 --- a/packages/oxa-conformance/cases/inline/text-basic.json +++ b/packages/oxa-conformance/cases/inline/text-basic.json @@ -8,20 +8,28 @@ "type": "Text", "value": "Hello, world!" }, - "myst": { + "myst-ast": { "type": "text", "value": "Hello, world!" }, - "pandoc": { + "pandoc-types": { "t": "Str", "c": "Hello, world!" }, - "stencila": { + "stencila-schema": { "type": "Text", "value": "Hello, world!" }, "markdown": "Hello, world!", + "myst-markdown": "Hello, world!", + "stencila-markdown": "Hello, world!", + "quarto-markdown": "Hello, world!", "html": "Hello, world!", "jats": "Hello, world!" + }, + "notes": { + "myst-markdown": "Equivalent to markdown for this node type", + "stencila-markdown": "Equivalent to markdown for this node type", + "quarto-markdown": "Equivalent to markdown for this node type" } } diff --git a/packages/oxa-conformance/index.d.ts b/packages/oxa-conformance/index.d.ts index 215f5ce..c3dbae9 100644 --- a/packages/oxa-conformance/index.d.ts +++ b/packages/oxa-conformance/index.d.ts @@ -38,25 +38,37 @@ export interface OXAConformanceTestCase { /** * MyST Markdown AST representation */ - myst?: { + "myst-ast"?: { [k: string]: unknown; }; /** * Pandoc AST JSON representation */ - pandoc?: { + "pandoc-types"?: { [k: string]: unknown; }; /** * Stencila Schema JSON representation */ - stencila?: { + "stencila-schema"?: { [k: string]: unknown; }; /** * CommonMark/GFM Markdown representation */ markdown?: string; + /** + * MyST Markdown representation + */ + "myst-markdown"?: string; + /** + * Stencila Markdown representation + */ + "stencila-markdown"?: string; + /** + * Quarto Markdown representation + */ + "quarto-markdown"?: string; /** * Semantic HTML5 representation */ @@ -91,10 +103,13 @@ export interface OXAConformanceSuiteManifest { */ formats: ( | "oxa" - | "myst" - | "pandoc" - | "stencila" + | "myst-ast" + | "pandoc-types" + | "stencila-schema" | "markdown" + | "myst-markdown" + | "stencila-markdown" + | "quarto-markdown" | "html" | "jats" )[]; diff --git a/packages/oxa-conformance/manifest.json b/packages/oxa-conformance/manifest.json index 2c1acb7..8b59f80 100644 --- a/packages/oxa-conformance/manifest.json +++ b/packages/oxa-conformance/manifest.json @@ -1,7 +1,18 @@ { "$schema": "./schemas/manifest.schema.json", "version": "0.1.0", - "formats": ["oxa", "myst", "pandoc", "stencila", "markdown", "html", "jats"], + "formats": [ + "oxa", + "myst-ast", + "pandoc-types", + "stencila-schema", + "markdown", + "myst-markdown", + "stencila-markdown", + "quarto-markdown", + "html", + "jats" + ], "cases": [ { "id": "emphasis-basic", diff --git a/packages/oxa-conformance/schemas/manifest.schema.json b/packages/oxa-conformance/schemas/manifest.schema.json index 69620d5..41badbd 100644 --- a/packages/oxa-conformance/schemas/manifest.schema.json +++ b/packages/oxa-conformance/schemas/manifest.schema.json @@ -19,7 +19,7 @@ "description": "List of supported formats in canonical order", "items": { "type": "string", - "enum": ["oxa", "myst", "pandoc", "stencila", "markdown", "html", "jats"] + "enum": ["oxa", "myst-ast", "pandoc-types", "stencila-schema", "markdown", "myst-markdown", "stencila-markdown", "quarto-markdown", "html", "jats"] } }, "cases": { diff --git a/packages/oxa-conformance/schemas/test-case.schema.json b/packages/oxa-conformance/schemas/test-case.schema.json index 9c7c1b9..05c7182 100644 --- a/packages/oxa-conformance/schemas/test-case.schema.json +++ b/packages/oxa-conformance/schemas/test-case.schema.json @@ -31,17 +31,17 @@ "description": "OXA JSON representation (required, authoritative)", "additionalProperties": true }, - "myst": { + "myst-ast": { "type": "object", "description": "MyST Markdown AST representation", "additionalProperties": true }, - "pandoc": { + "pandoc-types": { "type": "object", "description": "Pandoc AST JSON representation", "additionalProperties": true }, - "stencila": { + "stencila-schema": { "type": "object", "description": "Stencila Schema JSON representation", "additionalProperties": true @@ -50,6 +50,18 @@ "type": "string", "description": "CommonMark/GFM Markdown representation" }, + "myst-markdown": { + "type": "string", + "description": "MyST Markdown representation" + }, + "stencila-markdown": { + "type": "string", + "description": "Stencila Markdown representation" + }, + "quarto-markdown": { + "type": "string", + "description": "Quarto Markdown representation" + }, "html": { "type": "string", "description": "Semantic HTML5 representation" diff --git a/packages/oxa-core/src/conformance.test.ts b/packages/oxa-core/src/conformance.test.ts index a9b4b0d..407f65e 100644 --- a/packages/oxa-core/src/conformance.test.ts +++ b/packages/oxa-core/src/conformance.test.ts @@ -35,10 +35,13 @@ describe("OXA Conformance Suite", () => { it("has expected formats in canonical order", () => { expect(manifest.formats).toEqual([ "oxa", - "myst", - "pandoc", - "stencila", + "myst-ast", + "pandoc-types", + "stencila-schema", "markdown", + "myst-markdown", + "stencila-markdown", + "quarto-markdown", "html", "jats", ]); diff --git a/scripts/lib/generate-conformance.ts b/scripts/lib/generate-conformance.ts index b5d96e5..35cd13d 100644 --- a/scripts/lib/generate-conformance.ts +++ b/scripts/lib/generate-conformance.ts @@ -199,7 +199,7 @@ async function generateManifest(): Promise<void> { const manifest: Manifest = { $schema: "./schemas/manifest.schema.json", version, - formats: ["oxa", "myst", "pandoc", "stencila", "markdown", "html", "jats"], + formats: ["oxa", "myst-ast", "pandoc-types", "stencila-schema", "markdown", "myst-markdown", "stencila-markdown", "quarto-markdown", "html", "jats"], cases: allCases, }; diff --git a/scripts/lib/generate-docs.ts b/scripts/lib/generate-docs.ts index c19c2a8..b4b6149 100644 --- a/scripts/lib/generate-docs.ts +++ b/scripts/lib/generate-docs.ts @@ -240,20 +240,26 @@ function loadTestCases(): Map<string, TestCase> { const FORMAT_LABELS: Record<string, string> = { oxa: "OXA", - myst: "MyST", - pandoc: "Pandoc", - stencila: "Stencila", + "myst-ast": "MyST AST", + "pandoc-types": "Pandoc Types", + "stencila-schema": "Stencila Schema", markdown: "Markdown", + "myst-markdown": "MyST Markdown", + "stencila-markdown": "Stencila Markdown", + "quarto-markdown": "Quarto Markdown", html: "HTML", jats: "JATS", }; const FORMAT_LANGUAGES: Record<string, string> = { oxa: "json", - myst: "json", - pandoc: "json", - stencila: "json", + "myst-ast": "json", + "pandoc-types": "json", + "stencila-schema": "json", markdown: "markdown", + "myst-markdown": "markdown", + "stencila-markdown": "markdown", + "quarto-markdown": "markdown", html: "html", jats: "xml", }; @@ -263,7 +269,7 @@ function generateTestCaseSection(testCase: TestCase): string { lines.push("### Example"); lines.push(""); - lines.push("````{tab-set}"); + lines.push("`````{tab-set}"); for (const format of manifest.formats) { const value = testCase.formats[format as keyof typeof testCase.formats]; @@ -273,16 +279,16 @@ function generateTestCaseSection(testCase: TestCase): string { const lang = FORMAT_LANGUAGES[format]; const content = typeof value === "string" ? value : JSON.stringify(value); - lines.push(`\`\`\`{tab-item} ${label}`); + lines.push(`\`\`\`\`{tab-item} ${label}`); lines.push(`:sync: ${format}`); lines.push(`\`\`\`${lang}`); lines.push(content); lines.push("```"); - lines.push("```"); + lines.push("````"); lines.push(""); } - lines.push("````"); + lines.push("`````"); return lines.join("\n"); } From 985af224e2fbe4ae3d803463e9235188adcc8c17 Mon Sep 17 00:00:00 2001 From: Nokome Bentley <nokome@stenci.la> Date: Tue, 27 Jan 2026 12:48:13 +1300 Subject: [PATCH 10/14] fix(conformance): export test cases --- packages/oxa-conformance/index.cjs | 12 ++++++++++-- packages/oxa-conformance/index.d.ts | 5 +++-- packages/oxa-conformance/index.js | 7 +++++-- packages/oxa-core/src/conformance.test.ts | 21 ++------------------- scripts/lib/generate-docs.ts | 2 +- 5 files changed, 21 insertions(+), 26 deletions(-) diff --git a/packages/oxa-conformance/index.cjs b/packages/oxa-conformance/index.cjs index 8fe109e..457cacd 100644 --- a/packages/oxa-conformance/index.cjs +++ b/packages/oxa-conformance/index.cjs @@ -3,5 +3,13 @@ */ const manifest = require("./manifest.json"); -module.exports = manifest; -module.exports.default = manifest; + +/** + * All test cases keyed by id + */ +const cases = Object.fromEntries( + manifest.cases.map((c) => [c.id, require(`./${c.path}`)]) +); + +module.exports.manifest = manifest; +module.exports.cases = cases; diff --git a/packages/oxa-conformance/index.d.ts b/packages/oxa-conformance/index.d.ts index c3dbae9..7c7876c 100644 --- a/packages/oxa-conformance/index.d.ts +++ b/packages/oxa-conformance/index.d.ts @@ -141,5 +141,6 @@ export type TestCase = OXAConformanceTestCase; export type Manifest = OXAConformanceSuiteManifest; export type ManifestCase = Manifest["cases"][number]; -declare const manifest: Manifest; -export default manifest; +export declare const manifest: Manifest; + +export declare const cases: Record<string, TestCase>; diff --git a/packages/oxa-conformance/index.js b/packages/oxa-conformance/index.js index 552e36a..831c0ab 100644 --- a/packages/oxa-conformance/index.js +++ b/packages/oxa-conformance/index.js @@ -8,5 +8,8 @@ import { createRequire } from "module"; const require = createRequire(import.meta.url); -const manifest = require("./manifest.json"); -export default manifest; +export const manifest = require("./manifest.json"); + +export const cases = Object.fromEntries( + manifest.cases.map((c) => [c.id, require(`./${c.path}`)]) +); diff --git a/packages/oxa-core/src/conformance.test.ts b/packages/oxa-core/src/conformance.test.ts index 407f65e..3665f17 100644 --- a/packages/oxa-core/src/conformance.test.ts +++ b/packages/oxa-core/src/conformance.test.ts @@ -7,25 +7,9 @@ */ import { describe, it, expect } from "vitest"; -import { readFileSync } from "fs"; -import { dirname, join } from "path"; -import { fileURLToPath } from "url"; -import manifest from "@oxa/conformance"; +import { manifest, cases } from "@oxa/conformance"; import { validate } from "./validate.js"; -// Resolve the path to the conformance package for loading individual test case files -const CONFORMANCE_PATH = dirname( - fileURLToPath(import.meta.resolve("@oxa/conformance")), -); - -interface TestCase { - title: string; - category: string; - formats: { - oxa: Record<string, unknown>; - }; -} - describe("OXA Conformance Suite", () => { describe("manifest", () => { it("has valid version", () => { @@ -55,8 +39,7 @@ describe("OXA Conformance Suite", () => { describe("test cases", () => { for (const testCaseMeta of manifest.cases) { describe(`${testCaseMeta.category}/${testCaseMeta.id}`, () => { - const casePath = join(CONFORMANCE_PATH, testCaseMeta.path); - const testCase: TestCase = JSON.parse(readFileSync(casePath, "utf-8")); + const testCase = cases[testCaseMeta.id]; it("has required fields", () => { expect(testCase.title).toBeDefined(); diff --git a/scripts/lib/generate-docs.ts b/scripts/lib/generate-docs.ts index b4b6149..374b932 100644 --- a/scripts/lib/generate-docs.ts +++ b/scripts/lib/generate-docs.ts @@ -9,7 +9,7 @@ import { mkdirSync, rmSync, writeFileSync, existsSync, readFileSync } from "fs"; import { createRequire } from "module"; import { join, dirname } from "path"; -import manifest, { type TestCase } from "@oxa/conformance"; +import { manifest, type TestCase } from "@oxa/conformance"; import { loadMergedSchema } from "./schema.js"; From d04c3747ad236296b230a8b6d12d48f553c74e81 Mon Sep 17 00:00:00 2001 From: Nokome Bentley <nokome@stenci.la> Date: Tue, 27 Jan 2026 13:04:22 +1300 Subject: [PATCH 11/14] fix(scripts): formatting --- scripts/lib/generate-conformance.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/scripts/lib/generate-conformance.ts b/scripts/lib/generate-conformance.ts index 35cd13d..4d54522 100644 --- a/scripts/lib/generate-conformance.ts +++ b/scripts/lib/generate-conformance.ts @@ -199,7 +199,18 @@ async function generateManifest(): Promise<void> { const manifest: Manifest = { $schema: "./schemas/manifest.schema.json", version, - formats: ["oxa", "myst-ast", "pandoc-types", "stencila-schema", "markdown", "myst-markdown", "stencila-markdown", "quarto-markdown", "html", "jats"], + formats: [ + "oxa", + "myst-ast", + "pandoc-types", + "stencila-schema", + "markdown", + "myst-markdown", + "stencila-markdown", + "quarto-markdown", + "html", + "jats", + ], cases: allCases, }; From ea50a1ac06e1b1b996f40a8819ec8deae8ccb979 Mon Sep 17 00:00:00 2001 From: Nokome Bentley <nokome@stenci.la> Date: Tue, 27 Jan 2026 13:13:53 +1300 Subject: [PATCH 12/14] fix(scripts): fix up imports --- scripts/lib/generate-docs.ts | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/scripts/lib/generate-docs.ts b/scripts/lib/generate-docs.ts index 374b932..8601c4e 100644 --- a/scripts/lib/generate-docs.ts +++ b/scripts/lib/generate-docs.ts @@ -6,20 +6,15 @@ */ import { mkdirSync, rmSync, writeFileSync, existsSync, readFileSync } from "fs"; -import { createRequire } from "module"; -import { join, dirname } from "path"; +import { join } from "path"; -import { manifest, type TestCase } from "@oxa/conformance"; +import { manifest, cases, type TestCase } from "@oxa/conformance"; import { loadMergedSchema } from "./schema.js"; const OUTPUT_DIR = join(import.meta.dirname, "../../docs/schema"); const INDEX_FILE = join(OUTPUT_DIR, "index.md"); -// Resolve the conformance package directory for loading test case files -const require = createRequire(import.meta.url); -const CONFORMANCE_DIR = dirname(require.resolve("@oxa/conformance")); - interface SchemaProperty { type?: string; const?: string; @@ -221,7 +216,7 @@ function getArrayItemType(items: { $ref?: string; type?: string }): string { function loadTestCases(): Map<string, TestCase> { const testCases = new Map<string, TestCase>(); - // Filter for *-basic test cases and load them + // Filter for *-basic test cases for (const caseInfo of manifest.cases) { if (!caseInfo.id.endsWith("-basic")) continue; @@ -229,9 +224,7 @@ function loadTestCases(): Map<string, TestCase> { const primaryType = caseInfo.nodeTypes[0]; if (!primaryType) continue; - const filePath = join(CONFORMANCE_DIR, caseInfo.path); - const content = readFileSync(filePath, "utf-8"); - const testCase = JSON.parse(content) as TestCase; + const testCase = cases[caseInfo.id]; testCases.set(primaryType.toLowerCase(), testCase); } From 4d88e5309901a8fe53f252951e96ab3ee95152c3 Mon Sep 17 00:00:00 2001 From: Nokome Bentley <nokome@stenci.la> Date: Tue, 27 Jan 2026 14:11:00 +1300 Subject: [PATCH 13/14] chore: add changeset --- .changeset/sparkly-cows-show.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/sparkly-cows-show.md diff --git a/.changeset/sparkly-cows-show.md b/.changeset/sparkly-cows-show.md new file mode 100644 index 0000000..ccda95f --- /dev/null +++ b/.changeset/sparkly-cows-show.md @@ -0,0 +1,6 @@ +--- +"@oxa/conformance": patch +"@oxa/core": patch +--- + +Add @oxa/conformance package for conformance testing From 7bdf635ff729dc6bc9eca98405f524402d3a80a1 Mon Sep 17 00:00:00 2001 From: Nokome Bentley <nokome@stenci.la> Date: Tue, 27 Jan 2026 14:25:22 +1300 Subject: [PATCH 14/14] fix(scripts): correct export for conformance generated code --- scripts/lib/generate-conformance.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/scripts/lib/generate-conformance.ts b/scripts/lib/generate-conformance.ts index 4d54522..81acda8 100644 --- a/scripts/lib/generate-conformance.ts +++ b/scripts/lib/generate-conformance.ts @@ -157,8 +157,9 @@ export type TestCase = ${testCaseInterfaceName}; export type Manifest = ${manifestInterfaceName}; export type ManifestCase = Manifest["cases"][number]; -declare const manifest: Manifest; -export default manifest; +export declare const manifest: Manifest; + +export declare const cases: Record<string, TestCase>; `; const formatted = await prettier.format(output, {