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
4 changes: 4 additions & 0 deletions .github/workflows/contract-compatibility.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ on:
- "asyncapi/**"
- "schemas/**"
- "scripts/check_openapi_compatibility.rb"
- "scripts/lint_contracts.rb"
- "postman/**"
- ".github/workflows/contract-compatibility.yml"
push:
branches: [main]
Expand All @@ -30,3 +32,5 @@ jobs:
run: |
git show "origin/${{ github.base_ref }}:openapi/corelink-public-v1.yaml" > /tmp/base-public.yaml || true
ruby scripts/check_openapi_compatibility.rb /tmp/base-public.yaml openapi/corelink-public-v1.yaml
- name: Lint public contract and examples
run: ruby scripts/lint_contracts.rb
23 changes: 23 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Changelog

All notable contract releases are recorded here. Contract tags are immutable;
corrections require a new patch tag and must not be moved in place.

## [1.0.0-draft] - 2026-07-25

Initial reviewed draft for the proven Device and Command public slice.

### Added

- canonical public device and command identifiers;
- tenant-scoped device listing, lookup, creation and command operations;
- `application/problem+json` error responses with correlation IDs;
- canonical event envelope and device lifecycle event definitions;
- compatibility policy and public OpenAPI compatibility gate.

### Explicitly not released

Tenant provisioning, partner credentials, webhooks, telemetry, billing and
privileged administration remain outside this draft until their contracts and
runtime parity evidence are reviewed.

4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ administration remain out of public v1 until they have their own reviewed
contract. SDKs and the mock server may consume this draft only in prerelease
channels; it is not a release claim until runtime parity and CI checks land.

The immutable baseline tag is `v1.0.0-draft`. See the

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Publish the baseline tag before advertising it

The reviewed repository has no refs/tags/v1.0.0-draft, and the target commit is not pointed to by such a tag. SDK, CLI, and mock-server consumers directed here to pin the immutable baseline therefore cannot resolve it; publish the tag as part of this release or describe the draft by an existing commit until the tag is available.

Useful? React with 👍 / 👎.

[changelog](CHANGELOG.md) and [compatibility matrix](docs/compatibility-matrix.md)
for the exact release boundary and consumer status.

## Contract rules

- Public device identity is `corelink_device_id`; integration IDs remain
Expand Down
23 changes: 23 additions & 0 deletions docs/compatibility-matrix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Contract compatibility matrix

This matrix records the contract version and verification boundary for each
consumer. A draft tag is immutable, but it is not a production support claim.

| Surface | Contract source | Version | Compatibility gate | Current status |
| --- | --- | --- | --- | --- |
| Public HTTP API | `openapi/corelink-public-v1.yaml` | `1.0.0-draft` | OpenAPI syntax + public diff checker | Draft reviewed; runtime parity is a separate gate |
| Admin HTTP API | `openapi/corelink-admin-v1.yaml` | `1.0.0-draft` | OpenAPI syntax + authorization review | Internal draft; not a public release |
| Internal HTTP API | `openapi/corelink-internal-v1.yaml` | `1.0.0-draft` | OpenAPI syntax + service ownership review | Internal draft; not a public release |
Comment on lines +9 to +10

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Trigger checks for admin and internal contract changes

These rows state that the admin and internal contracts have OpenAPI syntax gates, but the pull-request path filters in .github/workflows/contract-compatibility.yml:4-12 only match public OpenAPI files. A PR that changes only corelink-admin-v1.yaml or corelink-internal-v1.yaml therefore skips the workflow entirely and can merge malformed YAML despite the documented gate; add those files to the trigger or do not report the gate as active.

Useful? React with 👍 / 👎.

| Events | `asyncapi/corelink-events-v1.yaml` | `1.0.0-draft` | AsyncAPI validation + event envelope review | Draft; delivery/replay evidence remains platform-owned |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Do not claim AsyncAPI validation before it runs

The checked workflow only calls YAML.safe_load for asyncapi/corelink-events-v1.yaml at .github/workflows/contract-compatibility.yml:28; neither the new lint nor another repository check validates the document against the AsyncAPI specification. An AsyncAPI-invalid but syntactically valid YAML document therefore passes while this matrix tells release consumers that AsyncAPI validation is a gate, so this should say YAML syntax parsing or the workflow should add a real validator.

Useful? React with 👍 / 👎.

| Python/TypeScript/Java SDKs | Generated or hand-written consumers | N/A | Contract version pinned per release | No stable SDK release claim for this draft |
| CLI, mock server and MCP server | Consumer repositories | N/A | Runtime parity and examples | Must consume a reviewed tag before beta |

## Release rules

1. Each released row must point to an immutable Git tag.
2. Additive changes within the same major version require a new minor or patch
tag and a changelog entry.
3. Breaking changes require a new major contract document and migration notes.
4. A contract tag is not a runtime release until the corresponding consumer
parity checks and operational evidence are attached to the release record.

4 changes: 4 additions & 0 deletions docs/runtime-parity.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,7 @@ No TypeScript or Python package may be promoted beyond prerelease until these
items, a contract-diff check and generated-client compatibility tests are
green. This gate preserves current consumers while the public boundary is
normalized.

The repository workflow also runs `scripts/lint_contracts.rb` and parses the
versioned Postman collection/environment. This is an early structural gate; it
does not replace runtime parity or generated-client tests.
45 changes: 45 additions & 0 deletions scripts/lint_contracts.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
#!/usr/bin/env ruby
# frozen_string_literal: true

# Small dependency-free contract lint. It catches incomplete operations before
# a full OpenAPI/AsyncAPI validator is introduced in the contract toolchain.

require "json"
require "yaml"

PUBLIC_SPEC = "openapi/corelink-public-v1.yaml"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Lint every versioned public contract

When a breaking change is correctly introduced in a new document such as corelink-public-v2.yaml, the workflow runs because its path filter matches corelink-public-v*.yaml, but this constant still points only to v1; the parse step in .github/workflows/contract-compatibility.yml is also hard-coded to v1. Consequently, the new major document receives no YAML or operation lint and the job can pass even if that document is malformed or lacks the required operation metadata.

Useful? React with 👍 / 👎.

OPERATIONS = %w[get put post patch delete head options].freeze

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Include TRACE in the operation lint

OpenAPI 3.1.1 permits trace operations, but this allowlist omits that method. If a public path adds a TRACE operation, the lint reports success even when the operation lacks operationId, x-corelink-stability, or responses, bypassing the checks this gate is intended to enforce.

Useful? React with 👍 / 👎.


spec = YAML.safe_load(File.read(PUBLIC_SPEC), permitted_classes: [], aliases: false)
errors = []
paths = spec.fetch("paths", {})
errors << "public contract must declare paths" if paths.empty?

paths.each do |path, path_item|
OPERATIONS.each do |method|
operation = path_item[method]
Comment on lines +18 to +20

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Dereference reusable path items before linting

OpenAPI 3.1 permits a path entry to reference a reusable Path Item, but this loop only inspects HTTP-method keys directly present on the entry. If an operation is moved under a $ref such as one targeting components.pathItems, no operation is visited and the lint passes even when the referenced operation lacks the required ID, stability marker, or responses; resolve path-item references before iterating or reject them explicitly.

Useful? React with 👍 / 👎.

next unless operation

location = "#{method.upcase} #{path}"
errors << "#{location}: missing operationId" if operation["operationId"].to_s.empty?
errors << "#{location}: missing x-corelink-stability" if operation["x-corelink-stability"].to_s.empty?
errors << "#{location}: missing responses" if operation.fetch("responses", {}).empty?
end
end

%w[
postman/corelink-public-v1.postman_collection.json
postman/corelink-public-v1.postman_environment.json
].each do |path|
JSON.parse(File.read(path))
rescue JSON::ParserError => error
errors << "#{path}: invalid JSON (#{error.message})"
end

if errors.empty?
puts "Contract lint passed."
else
warn errors.join("\n")
exit 1
end

Loading