Skip to content
Closed
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
42 changes: 42 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,48 @@ path = schema_path("aep-record") # pathlib.Path to the .json file
- Every schema has at least one valid and one invalid conformance fixture under
[`tests/fixtures/`](tests/fixtures/). CI rejects any schema without both.

### Version bands

`@wasmagent/protocol` ships on a **version band** model so consumers pin a
compatibility window, not a single patch. A *band* is one minor line
(`0.1.x`, `0.2.x`, …). Every release inside a band is backward-compatible —
fixes and additive optional fields only. A new minor opens the next band; a
breaking change bumps the package **major**. Crossing a band boundary is always
an explicit consumer action.

The current band is declared in `package.json#versionBand` (machine-readable)
and re-exported from the package as `versionBand`:

| | |
|---|---|
| Band | `0.1` (alpha) |
| Recommended range | `~0.1.5` (stay on the `0.1.x` patch band) |
| Also accepted | `>=0.1.0 <0.2.0`, `^0.1.5`, `0.1.x` |
| Runtime band | Node `>=18` (`package.json#engines`) |

**Declare the dependency like this:**

```json
"dependencies": {
"@wasmagent/protocol": "~0.1.5"
}
```

`~0.1.5` is recommended for the alpha: you receive every backward-compatible
patch within the `0.1` band and are never auto-upgraded across a band boundary.
When the next additive minor (`0.2.x`) ships, opt in by bumping to `~0.2.0`;
when a breaking major ships, bump it consciously. Read the band at runtime:

```ts
import { versionBand } from "@wasmagent/protocol";
// versionBand.band === "0.1"
// versionBand.supportedRanges.recommended === "~0.1.5"
```

The Python distribution (`wasmagent-protocol` on PyPI) mirrors the same minor
band via its own package version and declares its runtime band with
`requires-python = ">=3.9"` in `pyproject.toml`.

See [`docs/CONTRACT-CHANGE-PROCESS.md`](docs/CONTRACT-CHANGE-PROCESS.md) for the
full change workflow and [`docs/GOVERNANCE.md`](docs/GOVERNANCE.md) for
maintainer and exit-condition policy.
Expand Down
28 changes: 28 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,40 @@ export interface SchemaIndex {
schemas: SchemaIndexEntry[];
}

export interface VersionBandRanges {
/** Range consumers are recommended to declare (stays within one band). */
recommended: string;
/** Other ranges the band guarantees to satisfy. */
alsoAccepted: string[];
}

export interface VersionBand {
/** Current minor-line band, e.g. "0.1". */
band: string;
/** Lifecycle status of the band, e.g. "alpha". */
status: string;
/** Human-readable summary of the band's compatibility guarantee. */
summary: string;
supportedRanges: VersionBandRanges;
/** Semver/band policy consumers should follow. */
policy: string;
}

/** Machine-readable registry of every canonical schema. */
export const index: SchemaIndex;

/** All schemas as a plain object keyed by id. */
export const schemas: Record<string, unknown>;

/** Published package version (mirrors package.json#version). */
export const version: string;

/** Declared runtime band (mirrors package.json#engines). */
export const engines: { node: string };

/** Supported compatibility band (mirrors package.json#versionBand). */
export const versionBand: VersionBand;

/**
* Return the parsed JSON Schema for a registered schema id
* (e.g. "aep-record", "constraint-ir"). Throws on unknown id.
Expand Down
16 changes: 16 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ const here = dirname(fileURLToPath(import.meta.url));
/** Machine-readable registry of every canonical schema. */
export const index = JSON.parse(readFileSync(join(here, 'schemas', 'index.json'), 'utf8'));

/** Package manifest (name, version, runtime band, version band). */
const pkg = JSON.parse(readFileSync(join(here, 'package.json'), 'utf8'));

const byId = new Map(index.schemas.map((s) => [s.id, s]));

/**
Expand All @@ -30,3 +33,16 @@ export function getSchema(id) {

/** All schemas as a plain object keyed by id. */
export const schemas = Object.fromEntries(index.schemas.map((s) => [s.id, getSchema(s.id)]));

/** Published package version (mirrors package.json#version). */
export const version = pkg.version;

/** Declared runtime band (mirrors package.json#engines). */
export const engines = pkg.engines;

/**
* Supported compatibility band for this package (mirrors
* package.json#versionBand). Consumers should consult
* `versionBand.supportedRanges.recommended` when declaring a dependency range.
*/
export const versionBand = pkg.versionBand;
13 changes: 13 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,19 @@
},
"license": "Apache-2.0",
"type": "module",
"engines": {
"node": ">=18"
},
"versionBand": {
"band": "0.1",
"status": "alpha",
"summary": "Current supported compatibility band for @wasmagent/protocol. All releases within the 0.1.x band are backward-compatible (fixes and additive optional fields only); a new minor or major release opens the next band.",
"supportedRanges": {
"recommended": "~0.1.5",
"alsoAccepted": [">=0.1.0 <0.2.0", "^0.1.5", "0.1.x"]
},
"policy": "Consumers should declare a range that stays within one band (recommended: ~0.1.5). Additive changes bump the package minor and open the next band (0.2.x); breaking changes bump the package major. Crossing a band boundary is always an explicit consumer action. See README.md (Version bands)."
},
"main": "./index.js",
"types": "./index.d.ts",
"exports": {
Expand Down
55 changes: 54 additions & 1 deletion tests/loader.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,19 @@
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { test } from 'node:test';
import { getSchema, index, schemas } from '../index.js';
import { fileURLToPath } from 'node:url';
import {
engines,
getSchema,
index,
schemas,
version,
versionBand,
} from '../index.js';

const pkg = JSON.parse(
readFileSync(fileURLToPath(new URL('../package.json', import.meta.url)), 'utf8'),
);

test('registry lists at least one schema', () => {
assert.ok(index.schemas.length >= 1);
Expand All @@ -23,3 +36,43 @@ test('schemas map is keyed by id', () => {
test('getSchema throws on unknown id', () => {
assert.throws(() => getSchema('does-not-exist'), /unknown schema id/);
});

test('package.json initializes @wasmagent/protocol with a version and runtime band', () => {
assert.equal(pkg.name, '@wasmagent/protocol');
assert.match(pkg.version, /^\d+\.\d+\.\d+$/, 'package declares a semver version');
assert.equal(pkg.version, version, 'exported version mirrors package.json');
assert.equal(
typeof pkg.engines?.node,
'string',
'package declares an engines.node runtime band',
);
assert.equal(engines.node, pkg.engines.node, 'exported engines mirrors package.json');
});

test('version band mechanism is declared in package.json and re-exported', () => {
// The band must match the current version's major.minor line.
const [major, minor] = pkg.version.split('.');
assert.ok(pkg.versionBand, 'package.json declares a versionBand mechanism');
assert.deepEqual(versionBand, pkg.versionBand, 'versionBand is re-exported verbatim');
assert.equal(versionBand.band, `${major}.${minor}`, 'band tracks the current minor line');
assert.ok(versionBand.summary, 'versionBand has a summary');
assert.ok(versionBand.policy, 'versionBand has a policy');

const recommended = versionBand.supportedRanges.recommended;
assert.equal(
typeof recommended,
'string',
'versionBand declares a recommended consumer range',
);
const bandEsc = versionBand.band.replace('.', '\\.');
assert.match(
recommended,
new RegExp(`^~${bandEsc}\\.\\d+$`),
'recommended range stays within the declared band',
);
assert.ok(
Array.isArray(versionBand.supportedRanges.alsoAccepted) &&
versionBand.supportedRanges.alsoAccepted.length > 0,
'versionBand lists at least one additional accepted range',
);
});
57 changes: 57 additions & 0 deletions tests/test_conformance.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
"""pytest entry point for wasmagent-protocol.

Wraps the canonical conformance harness (``tests/conformance.py``) so the
schema/fixture invariants run under ``pytest tests/``, and asserts the
``@wasmagent/protocol`` package-structure + version-band invariants (issue #96).
Run with: ``pytest tests/ -x -q``.
"""

from __future__ import annotations

import json
import sys
from pathlib import Path

ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT / "tests"))

import conformance # noqa: E402 (tests/conformance.py — canonical harness)


def test_conformance_harness_passes() -> None:
"""Every schema is well-formed, $refs resolve, fixtures pass/fail as expected."""
conformance.errors.clear()
rc = conformance.main()
assert rc == 0, "conformance.py reported failures:\n" + "\n".join(conformance.errors)


def test_package_json_declares_version_band_mechanism() -> None:
"""package.json must initialize @wasmagent/protocol with a version band."""
pkg = json.loads((ROOT / "package.json").read_text(encoding="utf-8"))
assert pkg["name"] == "@wasmagent/protocol"
assert isinstance(pkg["version"], str) and pkg["version"].count(".") >= 2

# Runtime band: engines.node declares the supported Node line.
assert pkg.get("engines", {}).get("node"), "engines.node runtime band must be declared"

# Version band: tracks the current minor line and recommends an in-band range.
major, minor, *_ = pkg["version"].split(".")
band = pkg["versionBand"]
assert band["band"] == f"{major}.{minor}", "declared band tracks the version line"
assert band["summary"], "versionBand has a summary"
assert band["policy"], "versionBand has a policy"
recommended = band["supportedRanges"]["recommended"]
assert recommended.startswith("~"), "recommended range pins a single band"
assert recommended.split("~")[1].split(".")[0] == major, "recommended range matches the band major"
also = band["supportedRanges"]["alsoAccepted"]
assert isinstance(also, list) and len(also) >= 1, "at least one additional accepted range"


def test_pyproject_declares_python_package_structure() -> None:
"""The Python distribution (wasmagent-protocol) mirrors the package metadata
and declares its own runtime band via requires-python."""
text = (ROOT / "pyproject.toml").read_text(encoding="utf-8")
assert 'name = "wasmagent-protocol"' in text, "Python dist name is declared"
assert "version" in text, "Python dist declares a version"
assert "requires-python" in text, "Python dist declares a runtime band"
assert "Apache-2.0" in text, "Python dist carries the license"
Loading