Skip to content

Commit 0e07f4b

Browse files
authored
Tighten validation and package metadata (#24)
1 parent 5b4e299 commit 0e07f4b

File tree

5 files changed

+32
-162
lines changed

5 files changed

+32
-162
lines changed

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@ This README is a repo-wide orientation document for the current release line and
3030

3131
`v1.0.0` is historical only. Its older nested `requests/` and `receipts/` directories remain published for compatibility and audit, not as current teaching.
3232

33+
## Schema identity and trust
34+
35+
- `https://commandlayer.org/...` is the canonical schema namespace and the normative `$id` base for this release line.
36+
- This Git repository and its published package contents are the source of truth for those artifacts.
37+
- 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.
38+
3339
## Relationship to the stack
3440

3541
| Layer | Current line | Responsibility |

package.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@
4949
"SECURITY.md",
5050
"SECURITY_PROVENANCE.md",
5151
"COMPLIANCE.md",
52-
"ONBOARDING.md"
52+
"ONBOARDING.md",
53+
"INTEGRATOR.md"
5354
],
5455
"main": "schemas/v1.1.0/index.json",
5556
"exports": {
@@ -71,5 +72,8 @@
7172
"ajv": "^8.17.1",
7273
"ajv-errors": "^3.0.0",
7374
"ajv-formats": "^3.0.1"
75+
},
76+
"publishConfig": {
77+
"access": "public"
7478
}
7579
}

scripts/validate-all.mjs

Lines changed: 6 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import path from "path";
44
import Ajv2020 from "ajv/dist/2020.js";
55
import addFormats from "ajv-formats";
66
import ajvErrors from "ajv-errors";
7+
import { loadJsonStrict } from "./load-json-strict.mjs";
78

89
const ROOT_DIR = process.cwd();
910
const CURRENT_VERSION = "1.1.0";
@@ -52,68 +53,6 @@ function assert(condition, message) {
5253
if (!condition) throw new Error(message);
5354
}
5455

55-
function assertNoDuplicateObjectKeys(source, filePath) {
56-
const objectKeySets = [];
57-
let inString = false;
58-
let escape = false;
59-
60-
for (let index = 0; index < source.length; index += 1) {
61-
const char = source[index];
62-
63-
if (inString) {
64-
if (escape) {
65-
escape = false;
66-
continue;
67-
}
68-
if (char === "\\") {
69-
escape = true;
70-
continue;
71-
}
72-
if (char === '"') inString = false;
73-
continue;
74-
}
75-
76-
if (char === '"') {
77-
let end = index + 1;
78-
let stringEscape = false;
79-
while (end < source.length) {
80-
const nextChar = source[end];
81-
if (stringEscape) {
82-
stringEscape = false;
83-
} else if (nextChar === "\\") {
84-
stringEscape = true;
85-
} else if (nextChar === '"') {
86-
break;
87-
}
88-
end += 1;
89-
}
90-
91-
const raw = source.slice(index, end + 1);
92-
let cursor = end + 1;
93-
while (cursor < source.length && /\s/.test(source[cursor])) cursor += 1;
94-
if (cursor < source.length && source[cursor] === ':' && objectKeySets.length > 0) {
95-
const key = JSON.parse(raw);
96-
const currentKeys = objectKeySets[objectKeySets.length - 1];
97-
if (currentKeys.has(key)) {
98-
throw new Error(`${filePath} contains a duplicate JSON object key: ${key}`);
99-
}
100-
currentKeys.add(key);
101-
}
102-
103-
index = end;
104-
continue;
105-
}
106-
107-
if (char === '{') objectKeySets.push(new Set());
108-
if (char === '}') objectKeySets.pop();
109-
}
110-
}
111-
112-
async function loadJson(filePath) {
113-
const source = await fs.readFile(filePath, "utf8");
114-
assertNoDuplicateObjectKeys(source, filePath);
115-
return JSON.parse(source);
116-
}
11756

11857
function expectedVerbEntry(verb) {
11958
return {
@@ -148,12 +87,12 @@ async function loadCurrentSchemas() {
14887
return Promise.all(schemaFiles.map(async (file) => ({
14988
file,
15089
rel: path.relative(ROOT_DIR, file).replace(/\\/g, "/"),
151-
schema: await loadJson(file)
90+
schema: await loadJsonStrict(file)
15291
})));
15392
}
15493

15594
async function validateManifest() {
156-
const manifest = await loadJson(path.join(ROOT_DIR, "manifest.json"));
95+
const manifest = await loadJsonStrict(path.join(ROOT_DIR, "manifest.json"));
15796
assert(!("$schema" in manifest), "manifest.json must not carry a decorative $schema field");
15897
assert(manifest.version === CURRENT_VERSION, `manifest version must be ${CURRENT_VERSION}`);
15998
assert(manifest.status === "current", "manifest status must be current");
@@ -165,7 +104,7 @@ async function validateManifest() {
165104
}
166105

167106
async function validatePackage() {
168-
const pkg = await loadJson(path.join(ROOT_DIR, "package.json"));
107+
const pkg = await loadJsonStrict(path.join(ROOT_DIR, "package.json"));
169108
assert(pkg.version === CURRENT_VERSION, `package version must be ${CURRENT_VERSION}`);
170109
assert(pkg.main === `schemas/v${CURRENT_VERSION}/index.json`, "package main drift");
171110
assert(pkg.exports['.'] === `./schemas/v${CURRENT_VERSION}/index.json`, "package exports current entry drift");
@@ -226,13 +165,13 @@ async function validateSchemaConsistency(currentSchemas) {
226165

227166
async function validateIndex() {
228167
const indexPath = path.join(SCHEMAS_ROOT, "index.json");
229-
const indexJson = await loadJson(indexPath);
168+
const indexJson = await loadJsonStrict(indexPath);
230169
assert(indexJson.version === CURRENT_VERSION, "index.json version drift");
231170
assert(indexJson.$id === `https://commandlayer.org/schemas/v${CURRENT_VERSION}/index.json`, "index.json $id drift");
232171
assert(indexJson.schemas_root === `https://commandlayer.org/schemas/v${CURRENT_VERSION}/`, "index.json schemas_root drift");
233172
assert(JSON.stringify(indexJson.verbs) === JSON.stringify(EXPECTED_VERBS.map(expectedVerbEntry)), "index.json verb inventory drift");
234173

235-
const manifest = await loadJson(path.join(ROOT_DIR, "manifest.json"));
174+
const manifest = await loadJsonStrict(path.join(ROOT_DIR, "manifest.json"));
236175
assert(JSON.stringify(indexJson.verbs) === JSON.stringify(manifest.verbs), "manifest/index verb inventory mismatch");
237176
}
238177

scripts/validate-examples.mjs

Lines changed: 15 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -7,102 +7,39 @@ import ajvErrors from "ajv-errors";
77
import { loadJsonStrict } from "./load-json-strict.mjs";
88

99
const ROOT_DIR = process.cwd();
10-
const CURRENT_VERSION = '1.1.0';
11-
const EXAMPLES_ROOT = path.join(ROOT_DIR, 'examples', `v${CURRENT_VERSION}`, 'commercial');
12-
const SCHEMAS_ROOT = path.join(ROOT_DIR, 'schemas', `v${CURRENT_VERSION}`, 'commercial');
13-
const VERBS = ['authorize', 'checkout', 'purchase', 'ship', 'verify'];
10+
const CURRENT_VERSION = "1.1.0";
11+
const EXAMPLES_ROOT = path.join(ROOT_DIR, "examples", `v${CURRENT_VERSION}`, "commercial");
12+
const SCHEMAS_ROOT = path.join(ROOT_DIR, "schemas", `v${CURRENT_VERSION}`, "commercial");
13+
const VERBS = ["authorize", "checkout", "purchase", "ship", "verify"];
1414

1515
const ajv = new Ajv2020({ strict: true, allErrors: true, allowUnionTypes: false });
1616
addFormats(ajv);
1717
ajvErrors(ajv);
1818

19-
function assertNoDuplicateObjectKeys(source, filePath) {
20-
const objectKeySets = [];
21-
let inString = false;
22-
let escape = false;
23-
24-
for (let index = 0; index < source.length; index += 1) {
25-
const char = source[index];
26-
27-
if (inString) {
28-
if (escape) {
29-
escape = false;
30-
continue;
31-
}
32-
if (char === "\\") {
33-
escape = true;
34-
continue;
35-
}
36-
if (char === '"') inString = false;
37-
continue;
38-
}
39-
40-
if (char === '"') {
41-
let end = index + 1;
42-
let stringEscape = false;
43-
while (end < source.length) {
44-
const nextChar = source[end];
45-
if (stringEscape) {
46-
stringEscape = false;
47-
} else if (nextChar === "\\") {
48-
stringEscape = true;
49-
} else if (nextChar === '"') {
50-
break;
51-
}
52-
end += 1;
53-
}
54-
55-
const raw = source.slice(index, end + 1);
56-
let cursor = end + 1;
57-
while (cursor < source.length && /\s/.test(source[cursor])) cursor += 1;
58-
if (cursor < source.length && source[cursor] === ':' && objectKeySets.length > 0) {
59-
const key = JSON.parse(raw);
60-
const currentKeys = objectKeySets[objectKeySets.length - 1];
61-
if (currentKeys.has(key)) {
62-
throw new Error(`${filePath} contains a duplicate JSON object key: ${key}`);
63-
}
64-
currentKeys.add(key);
65-
}
66-
67-
index = end;
68-
continue;
69-
}
70-
71-
if (char === '{') objectKeySets.push(new Set());
72-
if (char === '}') objectKeySets.pop();
73-
}
74-
}
75-
76-
async function loadJson(filePath) {
77-
const source = await fs.readFile(filePath, 'utf8');
78-
assertNoDuplicateObjectKeys(source, filePath);
79-
return JSON.parse(source);
80-
}
81-
8219
async function validateVerb(verb) {
83-
const requestSchema = await loadJson(path.join(SCHEMAS_ROOT, verb, `${verb}.request.schema.json`));
84-
const receiptSchema = await loadJson(path.join(SCHEMAS_ROOT, verb, `${verb}.receipt.schema.json`));
20+
const requestSchema = await loadJsonStrict(path.join(SCHEMAS_ROOT, verb, `${verb}.request.schema.json`));
21+
const receiptSchema = await loadJsonStrict(path.join(SCHEMAS_ROOT, verb, `${verb}.receipt.schema.json`));
8522
const validateRequest = ajv.compile(requestSchema);
8623
const validateReceipt = ajv.compile(receiptSchema);
8724

88-
for (const group of ['valid', 'invalid']) {
25+
for (const group of ["valid", "invalid"]) {
8926
const dir = path.join(EXAMPLES_ROOT, verb, group);
90-
const files = (await fs.readdir(dir)).filter(file => file.endsWith('.json')).sort();
91-
const requestFiles = files.filter(file => file.includes('request'));
92-
const receiptFiles = files.filter(file => file.includes('receipt'));
27+
const files = (await fs.readdir(dir)).filter((file) => file.endsWith(".json")).sort();
28+
const requestFiles = files.filter((file) => file.includes("request"));
29+
const receiptFiles = files.filter((file) => file.includes("receipt"));
9330
if (requestFiles.length === 0 || receiptFiles.length === 0) {
9431
throw new Error(`${verb} ${group} examples must include both request and receipt cases`);
9532
}
9633
for (const file of files) {
97-
const data = await loadJson(path.join(dir, file));
98-
const validate = file.includes('request') ? validateRequest : validateReceipt;
34+
const data = await loadJsonStrict(path.join(dir, file));
35+
const validate = file.includes("request") ? validateRequest : validateReceipt;
9936
const ok = validate(data);
100-
if (group === 'valid' && !ok) throw new Error(`${file} should be valid: ${ajv.errorsText(validate.errors)}`);
101-
if (group === 'invalid' && ok) throw new Error(`${file} should be invalid`);
37+
if (group === "valid" && !ok) throw new Error(`${file} should be valid: ${ajv.errorsText(validate.errors)}`);
38+
if (group === "invalid" && ok) throw new Error(`${file} should be invalid`);
10239
}
10340
}
10441
console.log(`✅ ${verb} examples validated.`);
10542
}
10643

10744
for (const verb of VERBS) await validateVerb(verb);
108-
console.log('✅ All current-line examples validated.');
45+
console.log("✅ All current-line examples validated.");

tsconfig.json

Lines changed: 0 additions & 16 deletions
This file was deleted.

0 commit comments

Comments
 (0)