diff --git a/README.md b/README.md index 7166d9f..7a5d82d 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,12 @@ This README is a repo-wide orientation document for the current release line and `v1.0.0` is historical only. Its older nested `requests/` and `receipts/` directories remain published for compatibility and audit, not as current teaching. +## Schema identity and trust + +- `https://commandlayer.org/...` is the canonical schema namespace and the normative `$id` base for this release line. +- This Git repository and its published package contents are the source of truth for those artifacts. +- External resolution of `$id` URLs is a convenience, not a trust requirement; consumers should vendor, mirror, or package-pin the repository artifacts they validate against. + ## Relationship to the stack | Layer | Current line | Responsibility | diff --git a/package.json b/package.json index 5ecf023..6bce5fe 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,8 @@ "SECURITY.md", "SECURITY_PROVENANCE.md", "COMPLIANCE.md", - "ONBOARDING.md" + "ONBOARDING.md", + "INTEGRATOR.md" ], "main": "schemas/v1.1.0/index.json", "exports": { @@ -71,5 +72,8 @@ "ajv": "^8.17.1", "ajv-errors": "^3.0.0", "ajv-formats": "^3.0.1" + }, + "publishConfig": { + "access": "public" } } diff --git a/scripts/validate-all.mjs b/scripts/validate-all.mjs index db53edc..a523786 100644 --- a/scripts/validate-all.mjs +++ b/scripts/validate-all.mjs @@ -4,6 +4,7 @@ import path from "path"; import Ajv2020 from "ajv/dist/2020.js"; import addFormats from "ajv-formats"; import ajvErrors from "ajv-errors"; +import { loadJsonStrict } from "./load-json-strict.mjs"; const ROOT_DIR = process.cwd(); const CURRENT_VERSION = "1.1.0"; @@ -52,68 +53,6 @@ function assert(condition, message) { if (!condition) throw new Error(message); } -function assertNoDuplicateObjectKeys(source, filePath) { - const objectKeySets = []; - let inString = false; - let escape = false; - - for (let index = 0; index < source.length; index += 1) { - const char = source[index]; - - if (inString) { - if (escape) { - escape = false; - continue; - } - if (char === "\\") { - escape = true; - continue; - } - if (char === '"') inString = false; - continue; - } - - if (char === '"') { - let end = index + 1; - let stringEscape = false; - while (end < source.length) { - const nextChar = source[end]; - if (stringEscape) { - stringEscape = false; - } else if (nextChar === "\\") { - stringEscape = true; - } else if (nextChar === '"') { - break; - } - end += 1; - } - - const raw = source.slice(index, end + 1); - let cursor = end + 1; - while (cursor < source.length && /\s/.test(source[cursor])) cursor += 1; - if (cursor < source.length && source[cursor] === ':' && objectKeySets.length > 0) { - const key = JSON.parse(raw); - const currentKeys = objectKeySets[objectKeySets.length - 1]; - if (currentKeys.has(key)) { - throw new Error(`${filePath} contains a duplicate JSON object key: ${key}`); - } - currentKeys.add(key); - } - - index = end; - continue; - } - - if (char === '{') objectKeySets.push(new Set()); - if (char === '}') objectKeySets.pop(); - } -} - -async function loadJson(filePath) { - const source = await fs.readFile(filePath, "utf8"); - assertNoDuplicateObjectKeys(source, filePath); - return JSON.parse(source); -} function expectedVerbEntry(verb) { return { @@ -148,12 +87,12 @@ async function loadCurrentSchemas() { return Promise.all(schemaFiles.map(async (file) => ({ file, rel: path.relative(ROOT_DIR, file).replace(/\\/g, "/"), - schema: await loadJson(file) + schema: await loadJsonStrict(file) }))); } async function validateManifest() { - const manifest = await loadJson(path.join(ROOT_DIR, "manifest.json")); + const manifest = await loadJsonStrict(path.join(ROOT_DIR, "manifest.json")); assert(!("$schema" in manifest), "manifest.json must not carry a decorative $schema field"); assert(manifest.version === CURRENT_VERSION, `manifest version must be ${CURRENT_VERSION}`); assert(manifest.status === "current", "manifest status must be current"); @@ -165,7 +104,7 @@ async function validateManifest() { } async function validatePackage() { - const pkg = await loadJson(path.join(ROOT_DIR, "package.json")); + const pkg = await loadJsonStrict(path.join(ROOT_DIR, "package.json")); assert(pkg.version === CURRENT_VERSION, `package version must be ${CURRENT_VERSION}`); assert(pkg.main === `schemas/v${CURRENT_VERSION}/index.json`, "package main drift"); assert(pkg.exports['.'] === `./schemas/v${CURRENT_VERSION}/index.json`, "package exports current entry drift"); @@ -226,13 +165,13 @@ async function validateSchemaConsistency(currentSchemas) { async function validateIndex() { const indexPath = path.join(SCHEMAS_ROOT, "index.json"); - const indexJson = await loadJson(indexPath); + const indexJson = await loadJsonStrict(indexPath); assert(indexJson.version === CURRENT_VERSION, "index.json version drift"); assert(indexJson.$id === `https://commandlayer.org/schemas/v${CURRENT_VERSION}/index.json`, "index.json $id drift"); assert(indexJson.schemas_root === `https://commandlayer.org/schemas/v${CURRENT_VERSION}/`, "index.json schemas_root drift"); assert(JSON.stringify(indexJson.verbs) === JSON.stringify(EXPECTED_VERBS.map(expectedVerbEntry)), "index.json verb inventory drift"); - const manifest = await loadJson(path.join(ROOT_DIR, "manifest.json")); + const manifest = await loadJsonStrict(path.join(ROOT_DIR, "manifest.json")); assert(JSON.stringify(indexJson.verbs) === JSON.stringify(manifest.verbs), "manifest/index verb inventory mismatch"); } diff --git a/scripts/validate-examples.mjs b/scripts/validate-examples.mjs index d05116e..271286f 100644 --- a/scripts/validate-examples.mjs +++ b/scripts/validate-examples.mjs @@ -7,102 +7,39 @@ import ajvErrors from "ajv-errors"; import { loadJsonStrict } from "./load-json-strict.mjs"; const ROOT_DIR = process.cwd(); -const CURRENT_VERSION = '1.1.0'; -const EXAMPLES_ROOT = path.join(ROOT_DIR, 'examples', `v${CURRENT_VERSION}`, 'commercial'); -const SCHEMAS_ROOT = path.join(ROOT_DIR, 'schemas', `v${CURRENT_VERSION}`, 'commercial'); -const VERBS = ['authorize', 'checkout', 'purchase', 'ship', 'verify']; +const CURRENT_VERSION = "1.1.0"; +const EXAMPLES_ROOT = path.join(ROOT_DIR, "examples", `v${CURRENT_VERSION}`, "commercial"); +const SCHEMAS_ROOT = path.join(ROOT_DIR, "schemas", `v${CURRENT_VERSION}`, "commercial"); +const VERBS = ["authorize", "checkout", "purchase", "ship", "verify"]; const ajv = new Ajv2020({ strict: true, allErrors: true, allowUnionTypes: false }); addFormats(ajv); ajvErrors(ajv); -function assertNoDuplicateObjectKeys(source, filePath) { - const objectKeySets = []; - let inString = false; - let escape = false; - - for (let index = 0; index < source.length; index += 1) { - const char = source[index]; - - if (inString) { - if (escape) { - escape = false; - continue; - } - if (char === "\\") { - escape = true; - continue; - } - if (char === '"') inString = false; - continue; - } - - if (char === '"') { - let end = index + 1; - let stringEscape = false; - while (end < source.length) { - const nextChar = source[end]; - if (stringEscape) { - stringEscape = false; - } else if (nextChar === "\\") { - stringEscape = true; - } else if (nextChar === '"') { - break; - } - end += 1; - } - - const raw = source.slice(index, end + 1); - let cursor = end + 1; - while (cursor < source.length && /\s/.test(source[cursor])) cursor += 1; - if (cursor < source.length && source[cursor] === ':' && objectKeySets.length > 0) { - const key = JSON.parse(raw); - const currentKeys = objectKeySets[objectKeySets.length - 1]; - if (currentKeys.has(key)) { - throw new Error(`${filePath} contains a duplicate JSON object key: ${key}`); - } - currentKeys.add(key); - } - - index = end; - continue; - } - - if (char === '{') objectKeySets.push(new Set()); - if (char === '}') objectKeySets.pop(); - } -} - -async function loadJson(filePath) { - const source = await fs.readFile(filePath, 'utf8'); - assertNoDuplicateObjectKeys(source, filePath); - return JSON.parse(source); -} - async function validateVerb(verb) { - const requestSchema = await loadJson(path.join(SCHEMAS_ROOT, verb, `${verb}.request.schema.json`)); - const receiptSchema = await loadJson(path.join(SCHEMAS_ROOT, verb, `${verb}.receipt.schema.json`)); + const requestSchema = await loadJsonStrict(path.join(SCHEMAS_ROOT, verb, `${verb}.request.schema.json`)); + const receiptSchema = await loadJsonStrict(path.join(SCHEMAS_ROOT, verb, `${verb}.receipt.schema.json`)); const validateRequest = ajv.compile(requestSchema); const validateReceipt = ajv.compile(receiptSchema); - for (const group of ['valid', 'invalid']) { + for (const group of ["valid", "invalid"]) { const dir = path.join(EXAMPLES_ROOT, verb, group); - const files = (await fs.readdir(dir)).filter(file => file.endsWith('.json')).sort(); - const requestFiles = files.filter(file => file.includes('request')); - const receiptFiles = files.filter(file => file.includes('receipt')); + const files = (await fs.readdir(dir)).filter((file) => file.endsWith(".json")).sort(); + const requestFiles = files.filter((file) => file.includes("request")); + const receiptFiles = files.filter((file) => file.includes("receipt")); if (requestFiles.length === 0 || receiptFiles.length === 0) { throw new Error(`${verb} ${group} examples must include both request and receipt cases`); } for (const file of files) { - const data = await loadJson(path.join(dir, file)); - const validate = file.includes('request') ? validateRequest : validateReceipt; + const data = await loadJsonStrict(path.join(dir, file)); + const validate = file.includes("request") ? validateRequest : validateReceipt; const ok = validate(data); - if (group === 'valid' && !ok) throw new Error(`${file} should be valid: ${ajv.errorsText(validate.errors)}`); - if (group === 'invalid' && ok) throw new Error(`${file} should be invalid`); + if (group === "valid" && !ok) throw new Error(`${file} should be valid: ${ajv.errorsText(validate.errors)}`); + if (group === "invalid" && ok) throw new Error(`${file} should be invalid`); } } console.log(`✅ ${verb} examples validated.`); } for (const verb of VERBS) await validateVerb(verb); -console.log('✅ All current-line examples validated.'); +console.log("✅ All current-line examples validated."); diff --git a/tsconfig.json b/tsconfig.json deleted file mode 100644 index 3b23de1..0000000 --- a/tsconfig.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2020", - "module": "ESNext", - "moduleResolution": "node", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "resolveJsonModule": true, - "allowSyntheticDefaultImports": true, - "noEmit": true - }, - "include": [ - "endpoints/**/*.ts" - ] -}