diff --git a/README.md b/README.md index 1d053a0..a5e6ecd 100644 --- a/README.md +++ b/README.md @@ -42,8 +42,9 @@ npm run validate | Path | Purpose | |------|---------| -| `schemas/v0.1/` | Authoritative JSON Schemas | -| `examples/v0.1/` | Complete executable lifecycle fixtures | +| `schemas/v0.1/` | Foundation lifecycle schemas retained for existing consumers | +| `schemas/v1.0/` | Bounded goal and lifecycle schemas | +| `examples/v0.1/`, `examples/v1.0/` | Complete executable lifecycle fixtures | | `tests/` | Positive, negative, and lifecycle contract tests | | `docs/VERSIONING.md` | Compatibility and evolution policy | | `docs/DISTRIBUTION.md` | Stable and immutable schema registry URLs | @@ -57,9 +58,15 @@ The schemas are public APIs. Review [versioning and compatibility](docs/VERSIONI Released schemas are discoverable from `https://coding-autopilot-system.github.io/cas-contracts/index.json`. Use stable `vMAJOR.MINOR` URLs for compatible updates or immutable `releases/vMAJOR.MINOR.PATCH` URLs for reproducible builds. See [schema distribution](docs/DISTRIBUTION.md). +The v1 `WorkRequest` rejects dispatch unless objective, repositories, +measurable criteria, constraints, risk, approval and verification policies, +capabilities, bounded execution limits, and stop policy are explicit. Its +documented defaults are fan-out 3, iterations 3, attempts 3, runtime 1800 +seconds, model calls 20, and no-progress limit 2. + ## Status -`v0.1.0` is the useful foundation release. Compatibility automation and versioned registry distribution are available for subsequent releases. +`v0.1.0` remains the supported foundation line. `v1.0.0` adds the bounded goal contract as an explicit major-version opt-in. ## License diff --git a/docs/VERSIONING.md b/docs/VERSIONING.md index 17d4f86..cef129e 100644 --- a/docs/VERSIONING.md +++ b/docs/VERSIONING.md @@ -24,6 +24,18 @@ Major releases may remove or rename properties, add required properties, narrow - Consumers should ignore unknown optional properties only when their validator policy permits them. - Consumers must not infer compatibility solely from file paths; validate against the declared schema. +## v1.0 Goal Contract Migration + +The v1.0 line introduces required bounded-goal properties on `WorkRequest`. +This is intentionally a major-version boundary: v0.1 schemas and payloads remain +published and valid, while producers opt into v1.0 by emitting +`schemaVersion: 1.0.0` and supplying success criteria, constraints, policies, +capabilities, budget limits, and stop policy. + +Registry builds publish both stable lines. Consumers must select the schema ID +matching the record's explicit version; they must not validate a v0.1 payload +against v1.0 or silently synthesize missing limits. + ## Change Procedure Every contract change must include updated examples, tests, changelog entry, compatibility classification, and migration notes for breaking changes. diff --git a/examples/v1.0/artifact-manifest.json b/examples/v1.0/artifact-manifest.json new file mode 100644 index 0000000..a9f42af --- /dev/null +++ b/examples/v1.0/artifact-manifest.json @@ -0,0 +1,18 @@ +{ + "kind": "ArtifactManifest", + "correlationId": "corr-20260611-001", + "promptId": "prompt-001", + "runId": "run-001", + "repo": "Coding-Autopilot-System/cas-contracts", + "actor": { "id": "gsd-orchestrator", "type": "workflow" }, + "timestamp": "2026-06-11T09:00:04Z", + "schemaVersion": "1.0.0", + "traceContext": { "traceparent": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01" }, + "artifacts": [ + { + "kind": "schema", + "uri": "https://schemas.coding-autopilot.dev/v1.0/prompt-envelope.schema.json", + "sha256": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + } + ] +} diff --git a/examples/v1.0/evaluation-result.json b/examples/v1.0/evaluation-result.json new file mode 100644 index 0000000..3162ed1 --- /dev/null +++ b/examples/v1.0/evaluation-result.json @@ -0,0 +1,20 @@ +{ + "kind": "EvaluationResult", + "correlationId": "corr-20260611-001", + "promptId": "prompt-001", + "runId": "run-001", + "repo": "Coding-Autopilot-System/cas-contracts", + "actor": { "id": "cas-evals", "type": "service" }, + "timestamp": "2026-06-11T09:00:06Z", + "schemaVersion": "1.0.0", + "traceContext": { "traceparent": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01" }, + "evaluator": "contract-completeness-v1", + "outcome": "passed", + "metrics": { + "schemaValidity": 1, + "traceability": 1 + }, + "evidence": [ + { "kind": "test-report", "uri": "cas://evidence/evaluation-001" } + ] +} diff --git a/examples/v1.0/policy-decision.json b/examples/v1.0/policy-decision.json new file mode 100644 index 0000000..6a43d43 --- /dev/null +++ b/examples/v1.0/policy-decision.json @@ -0,0 +1,14 @@ +{ + "kind": "PolicyDecision", + "correlationId": "corr-20260611-001", + "promptId": "prompt-001", + "runId": "run-001", + "repo": "Coding-Autopilot-System/cas-contracts", + "actor": { "id": "policy-engine", "type": "service" }, + "timestamp": "2026-06-11T09:00:01Z", + "schemaVersion": "1.0.0", + "traceContext": { "traceparent": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01" }, + "decision": "allow", + "policyVersion": "2026.06", + "reasons": ["Requested paths are within the contract repository."] +} diff --git a/examples/v1.0/prompt-envelope.json b/examples/v1.0/prompt-envelope.json new file mode 100644 index 0000000..5ac4a56 --- /dev/null +++ b/examples/v1.0/prompt-envelope.json @@ -0,0 +1,14 @@ +{ + "kind": "PromptEnvelope", + "correlationId": "corr-20260611-001", + "promptId": "prompt-001", + "runId": "run-001", + "repo": "Coding-Autopilot-System/cas-contracts", + "actor": { "id": "kim", "type": "human", "displayName": "Repository owner" }, + "timestamp": "2026-06-11T09:00:00Z", + "schemaVersion": "1.0.0", + "traceContext": { "traceparent": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01" }, + "intent": "Add a versioned lifecycle contract", + "prompt": "Define and validate the CAS lifecycle contracts.", + "constraints": ["No secrets in examples", "Preserve backward compatibility"] +} diff --git a/examples/v1.0/run-event.json b/examples/v1.0/run-event.json new file mode 100644 index 0000000..ab9cf6e --- /dev/null +++ b/examples/v1.0/run-event.json @@ -0,0 +1,15 @@ +{ + "kind": "RunEvent", + "correlationId": "corr-20260611-001", + "promptId": "prompt-001", + "runId": "run-001", + "repo": "Coding-Autopilot-System/cas-contracts", + "actor": { "id": "gsd-orchestrator", "type": "workflow" }, + "timestamp": "2026-06-11T09:00:03Z", + "schemaVersion": "1.0.0", + "traceContext": { "traceparent": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01" }, + "eventType": "validation.started", + "sequence": 1, + "status": "running", + "message": "Validating lifecycle fixtures." +} diff --git a/examples/v1.0/verification-result.json b/examples/v1.0/verification-result.json new file mode 100644 index 0000000..c0de75a --- /dev/null +++ b/examples/v1.0/verification-result.json @@ -0,0 +1,19 @@ +{ + "kind": "VerificationResult", + "correlationId": "corr-20260611-001", + "promptId": "prompt-001", + "runId": "run-001", + "repo": "Coding-Autopilot-System/cas-contracts", + "actor": { "id": "ci", "type": "service" }, + "timestamp": "2026-06-11T09:00:05Z", + "schemaVersion": "1.0.0", + "traceContext": { "traceparent": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01" }, + "outcome": "passed", + "checks": [ + { + "name": "contract-tests", + "outcome": "passed", + "evidenceUri": "https://github.com/Coding-Autopilot-System/cas-contracts/actions" + } + ] +} diff --git a/examples/v1.0/work-request.json b/examples/v1.0/work-request.json new file mode 100644 index 0000000..3b3831a --- /dev/null +++ b/examples/v1.0/work-request.json @@ -0,0 +1,31 @@ +{ + "kind": "WorkRequest", + "correlationId": "corr-20260611-001", + "promptId": "prompt-001", + "runId": "run-001", + "repo": "Coding-Autopilot-System/cas-contracts", + "actor": { "id": "autogen", "type": "agent" }, + "timestamp": "2026-06-11T09:00:02Z", + "schemaVersion": "1.0.0", + "traceContext": { "traceparent": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01" }, + "workType": "change", + "objective": "Publish and validate the bounded v1 goal contract.", + "targetRef": "refs/heads/main", + "targetRepositories": ["Coding-Autopilot-System/cas-contracts"], + "successCriteria": [ + { "id": "contracts-pass", "description": "All contract examples and negative validation tests pass." } + ], + "constraints": ["Preserve all v0.1 payloads and schemas unchanged."], + "riskLevel": "medium", + "approvalPolicy": "risk-based", + "verificationProfile": "contracts-default", + "requiredCapabilities": ["json-schema", "node-test"], + "budget": { + "maxFanOut": 3, + "maxIterations": 3, + "maxAttemptsPerWorkItem": 3, + "maxRuntimeSeconds": 1800, + "maxModelCalls": 20 + }, + "stopPolicy": { "noProgressLimit": 2 } +} diff --git a/schemas/v1.0/artifact-manifest.schema.json b/schemas/v1.0/artifact-manifest.schema.json new file mode 100644 index 0000000..9473d89 --- /dev/null +++ b/schemas/v1.0/artifact-manifest.schema.json @@ -0,0 +1,31 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://schemas.coding-autopilot.dev/v1.0/artifact-manifest.schema.json", + "title": "ArtifactManifest", + "type": "object", + "allOf": [ + { + "$ref": "common.schema.json#/$defs/lifecycleMetadata" + }, + { + "type": "object", + "required": [ + "kind", + "artifacts" + ], + "properties": { + "kind": { + "const": "ArtifactManifest" + }, + "artifacts": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "common.schema.json#/$defs/evidence" + } + } + } + } + ], + "unevaluatedProperties": false +} diff --git a/schemas/v1.0/common.schema.json b/schemas/v1.0/common.schema.json new file mode 100644 index 0000000..3563af9 --- /dev/null +++ b/schemas/v1.0/common.schema.json @@ -0,0 +1,68 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://schemas.coding-autopilot.dev/v1.0/common.schema.json", + "title": "CAS Common Definitions", + "$defs": { + "actor": { + "type": "object", + "additionalProperties": false, + "required": ["id", "type"], + "properties": { + "id": { "type": "string", "minLength": 1, "maxLength": 256 }, + "type": { + "type": "string", + "enum": ["human", "agent", "service", "workflow"] + }, + "displayName": { "type": "string", "minLength": 1, "maxLength": 256 } + } + }, + "traceContext": { + "type": "object", + "additionalProperties": false, + "required": ["traceparent"], + "properties": { + "traceparent": { + "type": "string", + "pattern": "^[\\da-f]{2}-[\\da-f]{32}-[\\da-f]{16}-[\\da-f]{2}$" + }, + "tracestate": { "type": "string", "maxLength": 512 } + } + }, + "lifecycleMetadata": { + "type": "object", + "required": [ + "correlationId", + "promptId", + "runId", + "repo", + "actor", + "timestamp", + "schemaVersion", + "traceContext" + ], + "properties": { + "correlationId": { "type": "string", "minLength": 1, "maxLength": 128 }, + "promptId": { "type": "string", "minLength": 1, "maxLength": 128 }, + "runId": { "type": "string", "minLength": 1, "maxLength": 128 }, + "repo": { + "type": "string", + "pattern": "^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$" + }, + "actor": { "$ref": "#/$defs/actor" }, + "timestamp": { "type": "string", "format": "date-time" }, + "schemaVersion": { "const": "1.0.0" }, + "traceContext": { "$ref": "#/$defs/traceContext" } + } + }, + "evidence": { + "type": "object", + "additionalProperties": false, + "required": ["kind", "uri"], + "properties": { + "kind": { "type": "string", "minLength": 1, "maxLength": 64 }, + "uri": { "type": "string", "format": "uri" }, + "sha256": { "type": "string", "pattern": "^[\\da-f]{64}$" } + } + } + } +} diff --git a/schemas/v1.0/evaluation-result.schema.json b/schemas/v1.0/evaluation-result.schema.json new file mode 100644 index 0000000..fa6c93c --- /dev/null +++ b/schemas/v1.0/evaluation-result.schema.json @@ -0,0 +1,51 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://schemas.coding-autopilot.dev/v1.0/evaluation-result.schema.json", + "title": "EvaluationResult", + "type": "object", + "allOf": [ + { + "$ref": "common.schema.json#/$defs/lifecycleMetadata" + }, + { + "type": "object", + "required": [ + "kind", + "evaluator", + "outcome", + "metrics", + "evidence" + ], + "properties": { + "kind": { + "const": "EvaluationResult" + }, + "evaluator": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "outcome": { + "enum": [ + "passed", + "failed", + "inconclusive" + ] + }, + "metrics": { + "type": "object", + "minProperties": 1, + "additionalProperties": { + "type": "number" + } + }, + "evidence": { + "type": "array", + "minItems": 1, + "items": { "$ref": "common.schema.json#/$defs/evidence" } + } + } + } + ], + "unevaluatedProperties": false +} diff --git a/schemas/v1.0/policy-decision.schema.json b/schemas/v1.0/policy-decision.schema.json new file mode 100644 index 0000000..10c8628 --- /dev/null +++ b/schemas/v1.0/policy-decision.schema.json @@ -0,0 +1,47 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://schemas.coding-autopilot.dev/v1.0/policy-decision.schema.json", + "title": "PolicyDecision", + "type": "object", + "allOf": [ + { + "$ref": "common.schema.json#/$defs/lifecycleMetadata" + }, + { + "type": "object", + "required": [ + "kind", + "decision", + "policyVersion", + "reasons" + ], + "properties": { + "kind": { + "const": "PolicyDecision" + }, + "decision": { + "enum": [ + "allow", + "deny", + "require-approval" + ] + }, + "policyVersion": { + "type": "string", + "minLength": 1, + "maxLength": 64 + }, + "reasons": { + "type": "array", + "minItems": 1, + "items": { + "type": "string", + "minLength": 1, + "maxLength": 1000 + } + } + } + } + ], + "unevaluatedProperties": false +} diff --git a/schemas/v1.0/prompt-envelope.schema.json b/schemas/v1.0/prompt-envelope.schema.json new file mode 100644 index 0000000..576b9a2 --- /dev/null +++ b/schemas/v1.0/prompt-envelope.schema.json @@ -0,0 +1,44 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://schemas.coding-autopilot.dev/v1.0/prompt-envelope.schema.json", + "title": "PromptEnvelope", + "type": "object", + "allOf": [ + { + "$ref": "common.schema.json#/$defs/lifecycleMetadata" + }, + { + "type": "object", + "required": [ + "kind", + "intent", + "prompt" + ], + "properties": { + "kind": { + "const": "PromptEnvelope" + }, + "intent": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "prompt": { + "type": "string", + "minLength": 1, + "maxLength": 50000 + }, + "constraints": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 1000 + }, + "uniqueItems": true + } + } + } + ], + "unevaluatedProperties": false +} diff --git a/schemas/v1.0/run-event.schema.json b/schemas/v1.0/run-event.schema.json new file mode 100644 index 0000000..ff1c7fa --- /dev/null +++ b/schemas/v1.0/run-event.schema.json @@ -0,0 +1,48 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://schemas.coding-autopilot.dev/v1.0/run-event.schema.json", + "title": "RunEvent", + "type": "object", + "allOf": [ + { + "$ref": "common.schema.json#/$defs/lifecycleMetadata" + }, + { + "type": "object", + "required": [ + "kind", + "eventType", + "sequence", + "status" + ], + "properties": { + "kind": { + "const": "RunEvent" + }, + "eventType": { + "type": "string", + "minLength": 1, + "maxLength": 128 + }, + "sequence": { + "type": "integer", + "minimum": 0 + }, + "status": { + "enum": [ + "queued", + "running", + "succeeded", + "failed", + "cancelled" + ] + }, + "message": { + "type": "string", + "maxLength": 5000 + } + } + } + ], + "unevaluatedProperties": false +} diff --git a/schemas/v1.0/verification-result.schema.json b/schemas/v1.0/verification-result.schema.json new file mode 100644 index 0000000..5defd72 --- /dev/null +++ b/schemas/v1.0/verification-result.schema.json @@ -0,0 +1,63 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://schemas.coding-autopilot.dev/v1.0/verification-result.schema.json", + "title": "VerificationResult", + "type": "object", + "allOf": [ + { + "$ref": "common.schema.json#/$defs/lifecycleMetadata" + }, + { + "type": "object", + "required": [ + "kind", + "outcome", + "checks" + ], + "properties": { + "kind": { + "const": "VerificationResult" + }, + "outcome": { + "enum": [ + "passed", + "failed", + "inconclusive" + ] + }, + "checks": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "name", + "outcome", + "evidenceUri" + ], + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "outcome": { + "enum": [ + "passed", + "failed", + "skipped" + ] + }, + "evidenceUri": { + "type": "string", + "format": "uri" + } + } + } + } + } + } + ], + "unevaluatedProperties": false +} diff --git a/schemas/v1.0/work-request.schema.json b/schemas/v1.0/work-request.schema.json new file mode 100644 index 0000000..7f72c64 --- /dev/null +++ b/schemas/v1.0/work-request.schema.json @@ -0,0 +1,84 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://schemas.coding-autopilot.dev/v1.0/work-request.schema.json", + "title": "WorkRequest", + "type": "object", + "allOf": [ + { "$ref": "common.schema.json#/$defs/lifecycleMetadata" }, + { + "type": "object", + "required": [ + "kind", "workType", "objective", "targetRef", "targetRepositories", + "successCriteria", "constraints", "riskLevel", "approvalPolicy", + "verificationProfile", "requiredCapabilities", "budget", "stopPolicy" + ], + "properties": { + "kind": { "const": "WorkRequest" }, + "workType": { "enum": ["analyze", "change", "verify", "repair", "deploy"] }, + "objective": { "type": "string", "minLength": 1, "maxLength": 5000 }, + "targetRef": { "type": "string", "minLength": 1, "maxLength": 256 }, + "targetRepositories": { + "type": "array", + "minItems": 1, + "maxItems": 32, + "uniqueItems": true, + "items": { "type": "string", "pattern": "^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$" } + }, + "successCriteria": { + "type": "array", + "minItems": 1, + "maxItems": 100, + "items": { + "type": "object", + "additionalProperties": false, + "required": ["id", "description"], + "properties": { + "id": { "type": "string", "minLength": 1, "maxLength": 128 }, + "description": { "type": "string", "minLength": 1, "maxLength": 1000 } + } + } + }, + "constraints": { + "type": "array", + "minItems": 1, + "maxItems": 100, + "items": { "type": "string", "minLength": 1, "maxLength": 1000 } + }, + "riskLevel": { "enum": ["low", "medium", "high", "critical"] }, + "approvalPolicy": { "enum": ["none", "risk-based", "always"] }, + "verificationProfile": { "type": "string", "minLength": 1, "maxLength": 128 }, + "requiredCapabilities": { + "type": "array", + "minItems": 1, + "maxItems": 64, + "uniqueItems": true, + "items": { "type": "string", "minLength": 1, "maxLength": 128 } + }, + "budget": { + "type": "object", + "additionalProperties": false, + "required": ["maxFanOut", "maxIterations", "maxAttemptsPerWorkItem", "maxRuntimeSeconds", "maxModelCalls"], + "properties": { + "maxFanOut": { "type": "integer", "minimum": 1, "maximum": 16, "default": 3 }, + "maxIterations": { "type": "integer", "minimum": 1, "maximum": 100, "default": 3 }, + "maxAttemptsPerWorkItem": { "type": "integer", "minimum": 1, "maximum": 20, "default": 3 }, + "maxRuntimeSeconds": { "type": "integer", "minimum": 1, "maximum": 86400, "default": 1800 }, + "maxModelCalls": { "type": "integer", "minimum": 1, "maximum": 1000, "default": 20 }, + "maxProviderCost": { "type": "number", "exclusiveMinimum": 0 }, + "currency": { "type": "string", "pattern": "^[A-Z]{3}$" } + }, + "dependentRequired": { "maxProviderCost": ["currency"], "currency": ["maxProviderCost"] } + }, + "stopPolicy": { + "type": "object", + "additionalProperties": false, + "required": ["noProgressLimit"], + "properties": { + "noProgressLimit": { "type": "integer", "minimum": 1, "maximum": 20, "default": 2 } + } + } + } + } + ], + "unevaluatedProperties": false +} diff --git a/scripts/build-registry.mjs b/scripts/build-registry.mjs index bca2cbb..ec83c57 100644 --- a/scripts/build-registry.mjs +++ b/scripts/build-registry.mjs @@ -1,4 +1,4 @@ -import { copyFile, mkdir, readFile, rm, writeFile } from "node:fs/promises"; +import { copyFile, mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises"; import { createHash } from "node:crypto"; import path from "node:path"; import { pathToFileURL } from "node:url"; @@ -8,6 +8,7 @@ const versionPattern = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/; function parseArguments(args) { if (args.includes("--help")) return { help: true }; + if (args.length === 0 || args.includes("--all")) return { all: true, output: path.join(root, "registry") }; const result = { output: path.join(root, "registry"), append: false }; for (let index = 0; index < args.length; index += 1) { if (args[index] === "--append") result.append = true; @@ -85,14 +86,38 @@ export async function buildRegistry({ version, source, output, append = false, d return { index, immutable, stable, output: outputRoot }; } +export async function buildAllRegistry({ output = path.join(root, "registry"), domain } = {}) { + const lines = (await readdir(path.join(root, "schemas"), { withFileTypes: true })) + .filter((entry) => entry.isDirectory() && /^v\d+\.\d+$/.test(entry.name)) + .map((entry) => entry.name) + .sort((left, right) => left.localeCompare(right, undefined, { numeric: true })); + if (!lines.length) throw new Error("No versioned schema directories found"); + + let result; + for (let index = 0; index < lines.length; index += 1) { + const line = lines[index]; + const version = `${line.slice(1)}.0`; + result = await buildRegistry({ + version, + source: path.join(root, "schemas", line), + output, + append: index > 0, + domain: index === lines.length - 1 ? domain : undefined + }); + } + return result; +} + async function main() { const args = parseArguments(process.argv.slice(2)); if (args.help) { - console.log("Usage: node scripts/build-registry.mjs --version [--source ] [--output ] [--append]"); + console.log("Usage: node scripts/build-registry.mjs [--all] | --version [--source ] [--output ] [--append]"); return; } - const result = await buildRegistry(args); - console.log(`Built ${result.stable.schemas.length} schemas for ${args.version} at ${result.output}`); + const result = args.all ? await buildAllRegistry(args) : await buildRegistry(args); + console.log(args.all + ? `Built ${result.index.releases.length} contract releases at ${result.output}` + : `Built ${result.stable.schemas.length} schemas for ${args.version} at ${result.output}`); } if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { diff --git a/scripts/lib.mjs b/scripts/lib.mjs index b9f2194..69d1d98 100644 --- a/scripts/lib.mjs +++ b/scripts/lib.mjs @@ -4,8 +4,10 @@ import Ajv2020 from "ajv/dist/2020.js"; import addFormats from "ajv-formats"; export const root = path.resolve(import.meta.dirname, ".."); -export const schemaDirectory = path.join(root, "schemas", "v0.1"); -export const exampleDirectory = path.join(root, "examples", "v0.1"); +export const schemaRoot = path.join(root, "schemas"); +export const exampleRoot = path.join(root, "examples"); +export const schemaDirectory = path.join(schemaRoot, "v0.1"); +export const exampleDirectory = path.join(exampleRoot, "v0.1"); export async function readJson(filePath) { return JSON.parse(await readFile(filePath, "utf8")); @@ -27,11 +29,11 @@ export async function jsonFilesRecursive(directory) { return files.flat().sort(); } -export async function createValidator(directory = schemaDirectory) { +export async function createValidator(directory = schemaRoot) { const ajv = new Ajv2020({ allErrors: true, strict: true }); addFormats(ajv); - for (const schemaPath of (await jsonFiles(directory)).filter((file) => file.endsWith(".schema.json"))) { + for (const schemaPath of (await jsonFilesRecursive(directory)).filter((file) => file.endsWith(".schema.json"))) { ajv.addSchema(await readJson(schemaPath)); } @@ -40,5 +42,6 @@ export async function createValidator(directory = schemaDirectory) { export function schemaIdForExample(examplePath) { const name = path.basename(examplePath, ".json"); - return `https://schemas.coding-autopilot.dev/v0.1/${name}.schema.json`; + const version = path.basename(path.dirname(examplePath)); + return `https://schemas.coding-autopilot.dev/${version}/${name}.schema.json`; } diff --git a/tests/contracts.test.mjs b/tests/contracts.test.mjs index 55efb2a..ed6c634 100644 --- a/tests/contracts.test.mjs +++ b/tests/contracts.test.mjs @@ -1,9 +1,9 @@ import assert from "node:assert/strict"; import test from "node:test"; -import { createValidator, exampleDirectory, jsonFiles, readJson, schemaIdForExample } from "../scripts/lib.mjs"; +import { createValidator, exampleRoot, jsonFilesRecursive, readJson, schemaIdForExample } from "../scripts/lib.mjs"; const ajv = await createValidator(); -const examplePaths = await jsonFiles(exampleDirectory); +const examplePaths = await jsonFilesRecursive(exampleRoot); const examples = await Promise.all(examplePaths.map(readJson)); test("all published examples satisfy their authoritative schemas", () => { @@ -14,26 +14,23 @@ test("all published examples satisfy their authoritative schemas", () => { } }); -test("the complete lifecycle preserves correlation and W3C trace context", () => { - const expected = { - correlationId: examples[0].correlationId, - promptId: examples[0].promptId, - runId: examples[0].runId, - repo: examples[0].repo, - traceparent: examples[0].traceContext.traceparent - }; - - for (const record of examples) { - assert.equal(record.correlationId, expected.correlationId); - assert.equal(record.promptId, expected.promptId); - assert.equal(record.runId, expected.runId); - assert.equal(record.repo, expected.repo); - assert.equal(record.traceContext.traceparent, expected.traceparent); +test("each complete lifecycle preserves correlation and W3C trace context", () => { + for (const version of ["0.1.0", "1.0.0"]) { + const records = examples.filter((example) => example.schemaVersion === version); + assert.ok(records.length > 0, `${version} examples exist`); + const expected = records[0]; + for (const record of records) { + assert.equal(record.correlationId, expected.correlationId); + assert.equal(record.promptId, expected.promptId); + assert.equal(record.runId, expected.runId); + assert.equal(record.repo, expected.repo); + assert.equal(record.traceContext.traceparent, expected.traceContext.traceparent); + } } }); test("contracts reject missing mandatory lifecycle metadata", () => { - const invalid = structuredClone(examples.find((example) => example.kind === "PromptEnvelope")); + const invalid = structuredClone(examples.find((example) => example.kind === "PromptEnvelope" && example.schemaVersion === "0.1.0")); delete invalid.correlationId; const validate = ajv.getSchema("https://schemas.coding-autopilot.dev/v0.1/prompt-envelope.schema.json"); @@ -42,7 +39,7 @@ test("contracts reject missing mandatory lifecycle metadata", () => { }); test("contracts reject malformed W3C traceparent values", () => { - const invalid = structuredClone(examples.find((example) => example.kind === "RunEvent")); + const invalid = structuredClone(examples.find((example) => example.kind === "RunEvent" && example.schemaVersion === "0.1.0")); invalid.traceContext.traceparent = "not-a-traceparent"; const validate = ajv.getSchema("https://schemas.coding-autopilot.dev/v0.1/run-event.schema.json"); diff --git a/tests/goal-contract.test.mjs b/tests/goal-contract.test.mjs new file mode 100644 index 0000000..65f87ba --- /dev/null +++ b/tests/goal-contract.test.mjs @@ -0,0 +1,83 @@ +import assert from "node:assert/strict"; +import path from "node:path"; +import test from "node:test"; +import { createValidator, readJson, root } from "../scripts/lib.mjs"; + +const version = "v1.0"; +const schemaDirectory = path.join(root, "schemas", version); +const examplePath = path.join(root, "examples", version, "work-request.json"); +const schemaId = "https://schemas.coding-autopilot.dev/v1.0/work-request.schema.json"; + +const ajv = await createValidator(schemaDirectory); +const validGoal = await readJson(examplePath); +const validate = ajv.getSchema(schemaId); + +test("v1 goal fixture declares every bounded default", () => { + assert.ok(validate); + assert.equal(validate(validGoal), true, ajv.errorsText(validate.errors)); + assert.deepEqual(validGoal.budget, { + maxFanOut: 3, + maxIterations: 3, + maxAttemptsPerWorkItem: 3, + maxRuntimeSeconds: 1800, + maxModelCalls: 20 + }); + assert.equal(validGoal.stopPolicy.noProgressLimit, 2); +}); + +for (const field of [ + "objective", + "targetRepositories", + "successCriteria", + "constraints", + "riskLevel", + "approvalPolicy", + "verificationProfile", + "requiredCapabilities", + "budget", + "stopPolicy" +]) { + test(`v1 goal rejects missing ${field}`, () => { + const invalid = structuredClone(validGoal); + delete invalid[field]; + assert.equal(validate(invalid), false); + }); +} + +for (const field of [ + "maxFanOut", + "maxIterations", + "maxAttemptsPerWorkItem", + "maxRuntimeSeconds", + "maxModelCalls" +]) { + test(`v1 goal rejects non-positive ${field}`, () => { + const invalid = structuredClone(validGoal); + invalid.budget[field] = 0; + assert.equal(validate(invalid), false); + }); +} + +test("v1 lifecycle rejects incomplete metadata and malformed trace context", () => { + const missingRun = structuredClone(validGoal); + delete missingRun.runId; + assert.equal(validate(missingRun), false); + + const malformedTrace = structuredClone(validGoal); + malformedTrace.traceContext.traceparent = "invalid"; + assert.equal(validate(malformedTrace), false); +}); + +test("v1 verification and evaluation records require evidence URIs", async () => { + const verification = await readJson(path.join(root, "examples", version, "verification-result.json")); + const verificationValidator = ajv.getSchema("https://schemas.coding-autopilot.dev/v1.0/verification-result.schema.json"); + const noVerificationEvidence = structuredClone(verification); + delete noVerificationEvidence.checks[0].evidenceUri; + assert.equal(verificationValidator(noVerificationEvidence), false); + + const evaluation = await readJson(path.join(root, "examples", version, "evaluation-result.json")); + const evaluationValidator = ajv.getSchema("https://schemas.coding-autopilot.dev/v1.0/evaluation-result.schema.json"); + const noEvaluationEvidence = structuredClone(evaluation); + delete noEvaluationEvidence.evidence; + assert.equal(evaluationValidator(noEvaluationEvidence), false); +}); diff --git a/tests/registry.test.mjs b/tests/registry.test.mjs index f53b17f..826cb90 100644 --- a/tests/registry.test.mjs +++ b/tests/registry.test.mjs @@ -4,7 +4,7 @@ import { mkdtemp, readFile } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import test from "node:test"; -import { buildRegistry } from "../scripts/build-registry.mjs"; +import { buildAllRegistry, buildRegistry } from "../scripts/build-registry.mjs"; import { createValidator, jsonFiles, readJson, schemaDirectory } from "../scripts/lib.mjs"; import { validateRegistry } from "../scripts/validate-registry.mjs"; @@ -63,3 +63,14 @@ test("registry emits a custom-domain CNAME only when explicitly configured", asy await buildRegistry({ version: "0.1.0", source: schemaDirectory, output, domain: "schemas.example.com" }); assert.equal(await readFile(path.join(output, "CNAME"), "utf8"), "schemas.example.com\n"); }); + +test("registry all-mode publishes v0.1 and v1.0 without replacing either line", async () => { + const output = await mkdtemp(path.join(os.tmpdir(), "cas-registry-all-")); + await buildAllRegistry({ output }); + + const index = await validateRegistry(output); + assert.deepEqual(index.releases, ["0.1.0", "1.0.0"]); + assert.deepEqual(index.lines, { "v0.1": "0.1.0", "v1.0": "1.0.0" }); + assert.equal((await readJson(path.join(output, "v0.1", "manifest.json"))).schemas.length, 8); + assert.equal((await readJson(path.join(output, "v1.0", "manifest.json"))).schemas.length, 8); +});