diff --git a/.gitignore b/.gitignore index b2b198d..b49260d 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,9 @@ release-source/ .tmp/ *.log .DS_Store + +.tmp-gh-*.ps1 + +.tmp-venv*/ + +TestResults/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 39f5ef0..19c9d90 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ All notable changes follow Semantic Versioning and Keep a Changelog conventions. - Conservative automated JSON Schema compatibility classification in pull-request CI. - Deterministic schema registry packaging and release-triggered GitHub Pages distribution. - Stable major/minor and immutable patch-version schema URL contracts with SHA-256 manifests. +- `failure-state.schema.json` for typed failure-state payloads in the v1.1 contract line. +- Shared `failureClass` enum in `schemas/v1.1/common.schema.json` for cross-runtime failure classification. ## [1.1.1] - 2026-07-05 diff --git a/docs/VERSIONING.md b/docs/VERSIONING.md index cef129e..5a6af70 100644 --- a/docs/VERSIONING.md +++ b/docs/VERSIONING.md @@ -56,3 +56,13 @@ The classifier reports: - `review_required`: changed semantics that cannot be classified safely by the automated rules. Both `breaking` and `review_required` exit nonzero. A `review_required` result must be resolved by simplifying the change, extending classifier coverage with tests, or obtaining explicit maintainer review. Automation never treats an unknown semantic change as compatible. + +## v1.1 FailureState Addition + +The `FailureState` contract added to the v1.1 line is a minor, additive change. + +- It introduces a new schema file: `schemas/v1.1/failure-state.schema.json`. +- It adds a shared `failureClass` enum definition to `schemas/v1.1/common.schema.json`. +- It does not remove or rename any existing schema, required property, or accepted enum value in previously published contracts. + +Existing v1.1 payloads remain valid, and consumers can adopt `FailureState` incrementally. diff --git a/examples/v1.1/failure-state.json b/examples/v1.1/failure-state.json new file mode 100644 index 0000000..fb02d6e --- /dev/null +++ b/examples/v1.1/failure-state.json @@ -0,0 +1,27 @@ +{ + "kind": "FailureState", + "correlationId": "corr-20260707-001", + "promptId": "prompt-typed-failure-001", + "runId": "run-typed-failure-001", + "repo": "Coding-Autopilot-System/gsd-orchestrator", + "actor": { "id": "gsd-orchestrator", "type": "workflow" }, + "timestamp": "2026-07-07T09:45:00Z", + "schemaVersion": "1.1.0", + "traceContext": { "traceparent": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01" }, + "failureClass": "transient", + "component": "gsd-orchestrator.LoopCoordinator", + "message": "MAF worker process exited with code 1", + "retryable": true, + "exceptionType": "System.InvalidOperationException", + "causeChain": [ + "MAF worker failed with exit code 1: connection refused", + "ConnectionRefusedError: [Errno 111] Connection refused" + ], + "retryAfterSeconds": 5, + "evidence": [ + { + "kind": "log", + "uri": "https://coding-autopilot-system.github.io/cas-contracts/examples/v1.1/failure-state.json" + } + ] +} diff --git a/schemas/v1.1/common.schema.json b/schemas/v1.1/common.schema.json index b23a790..5ee6af1 100644 --- a/schemas/v1.1/common.schema.json +++ b/schemas/v1.1/common.schema.json @@ -98,6 +98,16 @@ "waiting", "terminal" ] + }, + "failureClass": { + "type": "string", + "enum": [ + "transient", + "deterministic", + "policy", + "cancellation", + "unrecoverable" + ] } } } diff --git a/schemas/v1.1/failure-state.schema.json b/schemas/v1.1/failure-state.schema.json new file mode 100644 index 0000000..3b902c6 --- /dev/null +++ b/schemas/v1.1/failure-state.schema.json @@ -0,0 +1,37 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://schemas.coding-autopilot.dev/v1.1/failure-state.schema.json", + "title": "FailureState", + "type": "object", + "allOf": [ + { "$ref": "common.schema.json#/$defs/lifecycleMetadata" }, + { + "type": "object", + "required": [ + "kind", + "failureClass", + "component", + "message", + "retryable" + ], + "properties": { + "kind": { "const": "FailureState" }, + "failureClass": { "$ref": "common.schema.json#/$defs/failureClass" }, + "component": { "type": "string", "minLength": 1, "maxLength": 256 }, + "message": { "type": "string", "minLength": 1, "maxLength": 5000 }, + "retryable": { "type": "boolean" }, + "exceptionType": { "type": "string", "maxLength": 256 }, + "causeChain": { + "type": "array", + "items": { "type": "string", "maxLength": 1000 } + }, + "retryAfterSeconds": { "type": "number", "minimum": 0 }, + "evidence": { + "type": "array", + "items": { "$ref": "common.schema.json#/$defs/evidence" } + } + } + } + ], + "unevaluatedProperties": false +} diff --git a/tests/contracts.test.mjs b/tests/contracts.test.mjs index 8e97583..275ae76 100644 --- a/tests/contracts.test.mjs +++ b/tests/contracts.test.mjs @@ -105,3 +105,23 @@ test("v1.1 SDLC request rejects missing profile version", () => { assert.equal(validate(request), false); assert.match(ajv.errorsText(validate.errors), /profileVersion/); }); + +test("v1.1 FailureState example satisfies the authoritative schema", () => { + const examplePath = `${exampleRoot}/v1.1/failure-state.json`; + const validate = ajv.getSchema(schemaIdForExample(examplePath)); + const example = examples.find((record) => record.kind === "FailureState" && record.schemaVersion === "1.1.0"); + + assert.ok(validate, "v1.1 failure-state schema is registered"); + assert.ok(example, "v1.1 FailureState example exists"); + assert.equal(validate(example), true, ajv.errorsText(validate.errors)); +}); + +test("v1.1 FailureState rejects missing failureClass", () => { + const example = structuredClone(examples.find((record) => record.kind === "FailureState" && record.schemaVersion === "1.1.0")); + const validate = ajv.getSchema(schemaId("v1.1", "failure-state")); + delete example.failureClass; + + assert.ok(validate, "v1.1 failure-state schema is registered"); + assert.equal(validate(example), false); + assert.match(ajv.errorsText(validate.errors), /failureClass/); +}); diff --git a/tests/registry.test.mjs b/tests/registry.test.mjs index 7b9abb2..f376186 100644 --- a/tests/registry.test.mjs +++ b/tests/registry.test.mjs @@ -88,5 +88,5 @@ test("registry all-mode publishes all lines without replacing any", async () => assert.deepEqual(index.lines, { "v0.1": "0.1.0", "v1.0": "1.0.0", "v1.1": "1.1.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); - assert.equal((await readJson(path.join(output, "v1.1", "manifest.json"))).schemas.length, 6); + assert.equal((await readJson(path.join(output, "v1.1", "manifest.json"))).schemas.length, 7); });