Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand All @@ -71,5 +72,8 @@
"ajv": "^8.17.1",
"ajv-errors": "^3.0.0",
"ajv-formats": "^3.0.1"
},
"publishConfig": {
"access": "public"
}
}
73 changes: 6 additions & 67 deletions scripts/validate-all.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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");
Expand All @@ -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");
Expand Down Expand Up @@ -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");
}

Expand Down
93 changes: 15 additions & 78 deletions scripts/validate-examples.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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.");
16 changes: 0 additions & 16 deletions tsconfig.json

This file was deleted.

Loading