diff --git a/.github/workflows/release-spec.yml b/.github/workflows/release-spec.yml index d0ec7f4..588bf86 100644 --- a/.github/workflows/release-spec.yml +++ b/.github/workflows/release-spec.yml @@ -123,6 +123,23 @@ jobs: cp schema/v1.json dist/schema/v1.json echo "Published schema/v1.json -> dist/schema/v1.json (canonical \$id)" + # Assemble /schema/versions.json from the v* schema dirs already on gh-pages + # plus the current tag. gh-pages deploys with keep_files:true, so prior + # versions persist; we read them back to build the full list. On a manual + # run (no tag) VERSION is a branch name and gets filtered out by the + # generator's semver check, leaving the manifest built from existing dirs. + - name: Publish versions manifest (Step A3) + env: + VERSION: ${{ github.ref_name }} + run: | + mkdir -p dist/schema + git fetch origin gh-pages --depth=1 || true + EXISTING=$(git ls-tree -r origin/gh-pages --name-only 2>/dev/null \ + | grep -oE '^v[0-9]+\.[0-9]+\.[0-9]+/ocf-action-v1.json' \ + | sed 's#/ocf-action-v1.json##' | sort -u | tr '\n' ' ') + node scripts/build-versions-manifest.mjs "$EXISTING" "$VERSION" > dist/schema/versions.json + echo "Published versions.json:"; cat dist/schema/versions.json + - name: Deploy to gh-pages (Step C) uses: peaceiris/actions-gh-pages@v4 with: diff --git a/README.md b/README.md index 0211e02..2135329 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ **An open standard for representing team-sport coaching diagrams, drill animations, and playbooks as structured data — for invasion team sports, basketball first.** [![License: CC BY 4.0](https://img.shields.io/badge/License-CC%20BY%204.0-lightgrey.svg)](https://creativecommons.org/licenses/by/4.0/) -[![Schema Version](https://img.shields.io/badge/schema-v1.3.0-blue)](schema/v1.json) +[![Schema Version](https://img.shields.io/badge/schema-v1.4.0-blue)](schema/v1.json) [![Status: Draft](https://img.shields.io/badge/status-draft-yellow)]() --- diff --git a/docs/specification-v1.adoc b/docs/specification-v1.adoc index ee663b1..3881315 100644 --- a/docs/specification-v1.adoc +++ b/docs/specification-v1.adoc @@ -1,5 +1,5 @@ = Open Coaching Format — Open Standard for Team-Sport Coaching Diagrams -:version: 1.3.0 +:version: 1.4.0 :doctype: book :toc: left :toc-title: Table of Contents @@ -1144,6 +1144,20 @@ is the derivative. A single optional reference (not an array). } ---- +=== `meta.min_schema_version` + +`min_schema_version` (optional, SemVer) declares the minimum schema version +needed to validate the document correctly. Set it when the document uses a field +introduced in a specific minor, so a validator whose bundled schema is older can +warn that it may be out of date rather than reporting the newer fields as errors. +Omit it otherwise. It never changes how a current validator validates a document +— it is a signal for version-skew diagnostics. + +[source,json] +---- +"meta": { "id": "...", "title": "...", "min_schema_version": "1.4.0" } +---- + --- == Schema Reference @@ -1181,6 +1195,7 @@ class Meta { +difficulty: enum? +created: datetime? +source_format: enum? + +min_schema_version: semver? } class Court { @@ -1444,6 +1459,13 @@ npx ajv-cli validate \ [appendix] == Changelog +=== v1.4.0 + +Adds schema version signals (RFC 0005): optional `min_schema_version` in `meta`, +a machine-readable `x-ocf-version` keyword, and a published `versions.json` +manifest. All additive and backwards-compatible; documents without the new field +are unchanged. + === v1.3.0 Adds optional `side` (`left`/`right`) and `arc` (`tight`/`normal`/`wide`) fields diff --git a/docs/superpowers/plans/2026-08-28-plan1-schema-version-signals.md b/docs/superpowers/plans/2026-08-28-plan1-schema-version-signals.md new file mode 100644 index 0000000..6a2be72 --- /dev/null +++ b/docs/superpowers/plans/2026-08-28-plan1-schema-version-signals.md @@ -0,0 +1,514 @@ +# Plan 1 — Schema Version Signals (Strang A) Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add the document/schema version signals a version-aware validator needs — optional `meta.min_schema_version`, a machine-readable `x-ocf-version` on the schema, and a published `versions.json` manifest — released as spec v1.4.0. + +**Architecture:** Purely additive JSON-Schema draft-07 changes plus one CI step. `min_schema_version` reuses the exact SemVer `pattern` already in the schema (`based_on_formation.source_version`). `x-ocf-version` is a custom root keyword (ignored by validation). `versions.json` is assembled at release time from the existing `v*` gh-pages dirs plus the new tag. RFC 0005 gates it. + +**Tech Stack:** JSON Schema draft-07, ajv-cli + ajv-formats, GitHub Actions, `node --test`. + +**Design ref:** `docs/superpowers/specs/2026-08-28-version-aware-validator-design.md` (Strand A). + +--- + +### Task 1: RFC 0005 — Schema Version Signals + +**Files:** +- Create: `rfcs/0005-schema-version-signals.md` + +- [ ] **Step 1: Write the RFC** + +Copy the structure from `rfcs/0004-around-player-arc.md`. Content: + +```markdown +# RFC 0005 — Schema Version Signals (meta.min_schema_version, x-ocf-version, versions.json) + +| Field | Value | +|-------------|-------------------------------------------| +| RFC Number | 0005 | +| Title | Schema Version Signals | +| Author(s) | opencoachingformat maintainers | +| Created | 2026-08-28 | +| Status | Draft | +| Affects | Schema + Spec + Release CI | +| Version | Targets OCF v1.4.0 | + +## Summary + +Adds the version signals a version-aware validator needs, all additive: +- meta.min_schema_version (optional SemVer string): the minimum schema version + required to validate the document correctly. Lets a validator tell "the + document is wrong" from "I am out of date." +- x-ocf-version (root custom keyword): a machine-readable schema version, so + tools read it instead of parsing the free-text $comment. +- versions.json: a published manifest of all released schema versions + latest, + the single authority for update checks. + +Additive -> minor (v1.4.0). + +## Motivation + +The published playground silently served an outdated validator after v1.3.0, and +even once fixed, a validator cannot today distinguish a document using a +newer-minor field from a genuinely invalid document — additionalProperties:false +rejects both identically. These signals give tooling the information to diagnose +version skew instead of guessing. + +## Detailed Design + +### meta.min_schema_version +Optional string on meta, validated by the same SemVer pattern already used for +based_on_formation.source_version. Absent -> no minimum (today's behavior). + +### x-ocf-version +A string keyword at the schema root carrying the exact version (e.g. "1.4.0"). +JSON Schema ignores unknown keywords, so validation is unchanged. + +### versions.json +Emitted by the release workflow to /schema/versions.json: +{ latest, major, versions[], schema_url }. + +## Backwards Compatibility +- [x] No breaking changes (additive only). Documents without the new field are + unchanged; x-ocf-version does not affect validation. + +## Testing / Regression +- valid: a doc with meta.min_schema_version "1.4.0" validates. +- invalid: meta.min_schema_version "1.4" (bad SemVer) is rejected. +- x-ocf-version is present and equals the release version. +- versions.json has the documented shape and includes the new version. + +## References +- Design: docs/superpowers/specs/2026-08-28-version-aware-validator-design.md +- Related: RFC 0002 (SemVer pattern reused), the v0.1.0->0.1.1 validator skew incident. +``` + +- [ ] **Step 2: Commit** + +```bash +git add rfcs/0005-schema-version-signals.md +git commit -m "rfc: 0005 schema version signals (min_schema_version, x-ocf-version, versions.json)" +``` + +--- + +### Task 2: Add `meta.min_schema_version` to the schema + +**Files:** +- Modify: `schema/v1.json` (`meta` definition, ~line 656–674) +- Create: `examples/min-schema-version.ocf.json` + +- [ ] **Step 1: Write the valid fixture first (TDD)** + +Create `examples/min-schema-version.ocf.json`: + +```json +{ + "$schema": "https://opencoachingformat.org/schema/v1.json", + "sport": "basketball", + "meta": { + "id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d", + "title": "Declares a minimum schema version", + "min_schema_version": "1.4.0" + }, + "court": { "ruleset": "fiba", "type": "half_court" }, + "entities": [ + { "type": "offense", "nr": 2, "x": -6.75, "y": 13.98 } + ], + "frames": [ + { + "id": "f1", + "label": "Static", + "actions": [ + { "player": "offense_2", "type": "move", "moves": [ { "to": { "named": "right_wing" } } ] } + ], + "end_state": { "offense_2": { "named": "right_wing" } } + } + ] +} +``` + +- [ ] **Step 2: Run validate — expect FAIL (field not yet allowed)** + +Run: `npx ajv validate --spec=draft7 -s schema/v1.json -d examples/min-schema-version.ocf.json --all-errors -c ajv-formats` +Expected: FAIL — `meta must NOT have additional properties`. + +- [ ] **Step 3: Add the property to `meta`** + +In `schema/v1.json`, inside the `meta` properties (change the `based_on_play` +line to add `min_schema_version` after it): + +```json + "based_on_play": { "$ref": "#/definitions/based_on_play" }, + "min_schema_version": { + "type": "string", + "description": "Minimum OCF schema version required to validate this document correctly. Optional; when present MUST be valid SemVer. A validator whose bundled schema is older SHOULD warn that it may be out of date rather than treat unknown newer fields as errors.", + "pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$" + } +``` + +(Exact SemVer pattern already used by `based_on_formation.source_version`.) + +- [ ] **Step 4: Run validate — expect PASS** + +Run: `npx ajv validate --spec=draft7 -s schema/v1.json -d examples/min-schema-version.ocf.json --all-errors -c ajv-formats` +Expected: PASS — `examples/min-schema-version.ocf.json valid`. + +- [ ] **Step 5: Compile-check schema stays strict-clean** + +Run: `npx ajv compile --spec=draft7 -s schema/v1.json -c ajv-formats` +Expected: `schema schema/v1.json is valid`, no new strict warnings. + +- [ ] **Step 6: Commit** + +```bash +git add schema/v1.json examples/min-schema-version.ocf.json +git commit -m "feat(schema): add optional meta.min_schema_version (RFC 0005)" +``` + +--- + +### Task 3: Invalid fixture — bad SemVer is rejected + +**Files:** +- Create: `examples/invalid/min-schema-version-bad.json` + +- [ ] **Step 1: Write the invalid fixture** (`"1.4"` is not full SemVer): + +```json +{ + "sport": "basketball", + "meta": { "id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d", "title": "Bad min version", "min_schema_version": "1.4" }, + "court": { "ruleset": "fiba", "type": "half_court" }, + "entities": [ { "type": "offense", "nr": 2, "x": -6, "y": 13 } ], + "frames": [ { "id": "f", "actions": [ { "player": "offense_2", "type": "move", "moves": [ { "to": { "named": "right_wing" } } ] } ], "end_state": { "offense_2": { "named": "right_wing" } } } ] +} +``` + +- [ ] **Step 2: Confirm it rejects on the pattern** + +Run: `npx ajv validate --spec=draft7 -s schema/v1.json -d examples/invalid/min-schema-version-bad.json --all-errors -c ajv-formats` +Expected: FAIL; error set includes `.../meta/min_schema_version must match pattern`. + +- [ ] **Step 3: Run the invalid-fixtures suite** + +Run: `npm run test:invalid` +Expected: includes `ok: examples/invalid/min-schema-version-bad.json correctly rejected.` + +- [ ] **Step 4: Commit** + +```bash +git add examples/invalid/min-schema-version-bad.json +git commit -m "test(schema): reject malformed meta.min_schema_version" +``` + +--- + +### Task 4: Machine-readable `x-ocf-version` + +**Files:** +- Modify: `schema/v1.json` (root, near `$comment` ~line 4) +- Create: `test/schema-version.test.mjs` + +- [ ] **Step 1: Write the failing test** + +`test/schema-version.test.mjs`: + +```js +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; + +const schema = JSON.parse(readFileSync(new URL("../schema/v1.json", import.meta.url))); +const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url))); + +test("schema carries a machine-readable x-ocf-version", () => { + assert.equal(typeof schema["x-ocf-version"], "string"); +}); + +test("x-ocf-version matches package.json version", () => { + assert.equal(schema["x-ocf-version"], pkg.version); +}); + +test("x-ocf-version matches the $comment prose version", () => { + const m = /Schema version (\d+\.\d+\.\d+)/.exec(schema.$comment ?? ""); + assert.ok(m, "$comment should state 'Schema version X.Y.Z'"); + assert.equal(schema["x-ocf-version"], m[1]); +}); +``` + +- [ ] **Step 2: Run it — expect FAIL** + +Run: `node --test test/schema-version.test.mjs` +Expected: FAIL — `x-ocf-version` undefined; version mismatch (package.json still 1.3.0). + +- [ ] **Step 3: Add `x-ocf-version` to the schema root** + +In `schema/v1.json`, immediately after the `$comment` line (line 4), add a line: + +```json + "x-ocf-version": "1.3.0", +``` + +Keep its value equal to the `$comment` version at every step. Task 6 bumps +`$comment`, `x-ocf-version`, and `package.json` together to 1.4.0. + +- [ ] **Step 4: Run it — partial pass expected mid-plan** + +Run: `node --test test/schema-version.test.mjs` +Expected: the "is a string" and "$comment prose" tests PASS; the +"matches package.json" test still FAILS until Task 6 bumps package.json. This is +expected; Task 6 Step 4 re-runs it fully green. + +- [ ] **Step 5: Commit** + +```bash +git add schema/v1.json test/schema-version.test.mjs +git commit -m "feat(schema): add machine-readable x-ocf-version keyword (RFC 0005)" +``` + +--- + +### Task 5: `versions.json` manifest at release time + +**Files:** +- Create: `scripts/build-versions-manifest.mjs` +- Modify: `.github/workflows/release-spec.yml` (after Step A2, ~line 124) +- Create: `test/versions-manifest.test.mjs` + +- [ ] **Step 1: Write the failing test for the generator** + +`test/versions-manifest.test.mjs`: + +```js +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { buildVersionsManifest } from "../scripts/build-versions-manifest.mjs"; + +test("manifest lists sorted versions with latest + major + schema_url", () => { + const m = buildVersionsManifest({ + existing: ["v1.0.0", "v1.2.0", "v1.10.0"], + current: "v1.4.0", + }); + assert.equal(m.major, "v1"); + assert.equal(m.schema_url, "https://opencoachingformat.org/schema/v1.json"); + assert.deepEqual(m.versions, ["1.0.0", "1.2.0", "1.4.0", "1.10.0"]); + assert.equal(m.latest, "1.10.0"); +}); + +test("current version is included even if not in existing", () => { + const m = buildVersionsManifest({ existing: ["v1.0.0"], current: "v1.4.0" }); + assert.ok(m.versions.includes("1.4.0")); + assert.equal(m.latest, "1.4.0"); +}); +``` + +- [ ] **Step 2: Run it — expect FAIL (module missing)** + +Run: `node --test test/versions-manifest.test.mjs` +Expected: FAIL — cannot find the module. + +- [ ] **Step 3: Implement the generator** + +`scripts/build-versions-manifest.mjs`: + +```js +// Pure, testable manifest builder. The workflow feeds it the existing v* dirs +// from gh-pages plus the current release tag. +const SCHEMA_URL = "https://opencoachingformat.org/schema/v1.json"; + +function semverKey(v) { + return v.split(".").map((n) => parseInt(n, 10)); +} +function cmp(a, b) { + const [aa, ab, ac] = semverKey(a); + const [ba, bb, bc] = semverKey(b); + return aa - ba || ab - bb || ac - bc; +} + +export function buildVersionsManifest({ existing, current }) { + const all = [...existing, current] + .map((t) => t.replace(/^v/, "")) + .filter((v) => /^\d+\.\d+\.\d+$/.test(v)); + const unique = [...new Set(all)].sort(cmp); + const latest = unique[unique.length - 1]; + const major = "v" + semverKey(latest)[0]; + return { latest, major, versions: unique, schema_url: SCHEMA_URL }; +} + +const invokedDirectly = import.meta.url === `file://${process.argv[1]}`; +if (invokedDirectly) { + const existing = (process.argv[2] || "").split(/\s+/).filter(Boolean); + const current = process.argv[3]; + process.stdout.write(JSON.stringify(buildVersionsManifest({ existing, current }), null, 2) + "\n"); +} +``` + +- [ ] **Step 4: Run it — expect PASS** + +Run: `node --test test/versions-manifest.test.mjs` +Expected: PASS (both tests). + +- [ ] **Step 5: Wire it into the release workflow** + +In `.github/workflows/release-spec.yml`, after the "Publish canonical schema +(Step A2)" step (~line 124), add: + +```yaml + - name: Publish versions manifest (Step A3) + env: + VERSION: ${{ github.ref_name }} + run: | + mkdir -p dist/schema + git fetch origin gh-pages --depth=1 || true + EXISTING=$(git ls-tree -r origin/gh-pages --name-only 2>/dev/null \ + | grep -oE '^v[0-9]+\.[0-9]+\.[0-9]+/ocf-action-v1.json' \ + | sed 's#/ocf-action-v1.json##' | sort -u | tr '\n' ' ') + node scripts/build-versions-manifest.mjs "$EXISTING" "$VERSION" > dist/schema/versions.json + echo "Published versions.json:"; cat dist/schema/versions.json +``` + +- [ ] **Step 6: Commit** + +```bash +git add scripts/build-versions-manifest.mjs test/versions-manifest.test.mjs .github/workflows/release-spec.yml +git commit -m "feat(release): publish schema/versions.json manifest (RFC 0005)" +``` + +--- + +### Task 6: Docs, version bump to 1.4.0, RFC accept + +**Files:** +- Modify: `docs/specification-v1.adoc` (`:version:`, changelog, meta-field docs) +- Modify: `package.json` (version → 1.4.0) +- Modify: `schema/v1.json` (`$comment` + `x-ocf-version` → 1.4.0) +- Modify: `rfcs/0005-schema-version-signals.md` (Status → Accepted) +- Modify: `README.md` (badge → v1.4.0) + +- [ ] **Step 1: Document `meta.min_schema_version` in the spec doc** + +Find the meta field section in `docs/specification-v1.adoc` (search for +`source_format`). Add: + +```asciidoc +`min_schema_version` (optional, SemVer) declares the minimum schema version +needed to validate the document correctly. A validator whose bundled schema is +older should warn that it may be out of date rather than reporting the newer +fields as errors. Omit it unless the document uses a field newer than the base +version it targets. +``` + +- [ ] **Step 2: Bump version to 1.4.0 in all five places** + +- `package.json`: `"version": "1.4.0"` +- `schema/v1.json` `$comment`: `Schema version 1.4.0. ...` +- `schema/v1.json` `x-ocf-version`: `"1.4.0"` +- `docs/specification-v1.adoc` `:version:`: `1.4.0` +- `README.md` badge: `schema-v1.4.0-blue` + +- [ ] **Step 3: Add changelog entry at the TOP of the changelog** + +```asciidoc +=== v1.4.0 + +Adds schema version signals (RFC 0005): optional `min_schema_version` in meta, a +machine-readable `x-ocf-version` keyword, and a published `versions.json` +manifest. All additive and backwards-compatible; documents without the new field +are unchanged. +``` + +- [ ] **Step 4: Set RFC 0005 Status to Accepted; run full suites** + +Change `rfcs/0005-schema-version-signals.md` Status `Draft` → `Accepted`. + +Run: `node --test test/schema-version.test.mjs` +Expected: PASS (package.json == x-ocf-version == 1.4.0 now). + +Run: `npm test` +Expected: validate + test:invalid + test:positions + test:sport all pass. + +Run: `node --test test/versions-manifest.test.mjs` +Expected: PASS. + +- [ ] **Step 5: Verify version consistency** + +Run: +```bash +grep -H '"version"' package.json; grep -H 'x-ocf-version' schema/v1.json; grep -H 'Schema version' schema/v1.json; grep -H ':version:' docs/specification-v1.adoc; grep -H 'schema-v1' README.md +``` +Expected: every line shows 1.4.0. + +- [ ] **Step 6: Commit** + +```bash +git add docs/specification-v1.adoc package.json schema/v1.json rfcs/0005-schema-version-signals.md README.md +git commit -m "docs: document version signals; bump to 1.4.0; accept RFC 0005" +``` + +--- + +### Task 7: Site build + package-lock + PR + release + +**Files:** +- Modify: `package-lock.json` (version sync) + +- [ ] **Step 1: Sync package-lock version** + +Run: `npm install --package-lock-only` +Verify: `grep -m2 '"version": "1.4.0"' package-lock.json` shows both root + `""` entries. + +- [ ] **Step 2: Build the site (asciidoctor parses new prose + changelog)** + +Run: `cd site && npm ci && npm run build:adoc && cd ..` +Expected: `Generated site/src/generated/spec.html ...` with no error. + +- [ ] **Step 3: Commit the lockfile** + +```bash +git add package-lock.json +git commit -m "chore: sync package-lock to 1.4.0" +``` + +- [ ] **Step 4: Push branch, open PR, merge after CI green, tag release** + +```bash +git push -u origin +gh pr create --repo opencoachingformat/spec --base main --title "RFC 0005: schema version signals (v1.4.0)" --body "Implements RFC 0005 — meta.min_schema_version, x-ocf-version, versions.json. Additive, minor." +gh pr merge --repo opencoachingformat/spec --squash --delete-branch +``` + +Then on synced main: +```bash +git checkout main && git pull origin main +git tag -a v1.4.0 -m "v1.4.0 — schema version signals (RFC 0005)" +git push origin v1.4.0 +``` + +- [ ] **Step 5: Verify the release published live** + +```bash +npm view @opencoachingformat/spec version +curl -s https://opencoachingformat.org/schema/v1.json | grep -o 'x-ocf-version[^,]*' +curl -s https://opencoachingformat.org/schema/versions.json | python3 -m json.tool +curl -s -o /dev/null -w '%{http_code}\n' https://opencoachingformat.org/v1.4.0/ocf-action-v1.json +``` +Expected: 1.4.0 / `"x-ocf-version": "1.4.0"` / manifest latest 1.4.0 / 200. + +- [ ] **Step 6: Confirm the validator auto-sync PR opened** + +```bash +gh pr list --repo opencoachingformat/ocf-validator --state open +``` +Expected: `auto/schema-sync-v1.4.0`. (Handled by Plan 2 — carries the auto-republish bump added there.) + +--- + +## Self-Review Notes + +- **Spec coverage (Strand A):** A1 → Task 2/3; A2 → Task 4; A3 → Task 5; RFC → Task 1/6; release → Task 7. +- **Reused SemVer pattern** (DRY): identical to `based_on_formation.source_version`. +- **Version consistency:** five locations in Task 6 + lockfile in Task 7; `x-ocf-version` kept equal to package.json/`$comment` at every step. +- **`versions.json` generator is pure + unit-tested** (Task 5) apart from the workflow, verifying sort/dedup (incl. 1.10.0 > 1.2.0) without CI. +- **Additive only:** no field removed/required; `x-ocf-version` is a no-op for validation; `$id`/filename stays v1.json. diff --git a/docs/superpowers/specs/2026-08-28-version-aware-validator-design.md b/docs/superpowers/specs/2026-08-28-version-aware-validator-design.md new file mode 100644 index 0000000..e4dce6a --- /dev/null +++ b/docs/superpowers/specs/2026-08-28-version-aware-validator-design.md @@ -0,0 +1,244 @@ +# Design: Version-Aware Validator, Auto-Republish & Playground Transparency + +| Field | Value | +|----------|--------------------------------------------------------------| +| Date | 2026-08-28 | +| Status | Draft (design) | +| Repos | `opencoachingformat/spec`, `opencoachingformat/ocf-validator` | +| Sibling | Site multi-sport redesign — *separate*, later spec (Strang C) | + +## Problem + +The playground on opencoachingformat.org silently served an outdated validator +(`@opencoachingformat/validator@0.1.0`, bundling schema v1.2.0) after spec +v1.3.0 shipped, so every play using the new `side`/`arc` fields was rejected via +`additionalProperties: false`. A patch release (0.1.1) fixed the acute case, but +the underlying architecture has three gaps: + +1. **The validator has no version awareness.** It statically imports exactly one + schema copy (`import schema from ".../ocf-action-v1.json"`), has zero + `version`/`major`/`schemaVersion` handling, and its semantic rules hard-code + the v1 document shape (`.frames`, `.actions`, `.end_state`, `.balls`, + `.carried_by`, …). It is stable *within* the additive v1 line but is **not** + major-stable: a v2 document would be silently mis-validated as v1. + +2. **The "outdated validator" case is indistinguishable from a bad document.** + If a document uses a field from a *newer* minor (e.g. a hypothetical 1.4.0 + `move_step.spin`) but the loaded validator only knows 1.3.0, + `additionalProperties: false` rejects it with a misleading "`spin` is not + allowed" — when the real cause is that the validator is stale. The validator + cannot tell "document is wrong" from "I am out of date." + +3. **No forcing function keeps the published validator current.** The spec + release does not bump/republish the validator, and there is no version + manifest for anyone (validator or playground) to check against. + +## Goals + +- The validator is **honest** about which schema version it validated against and + whether that matches what the document declares/requires. +- A document that requires a **newer minor** than the bundled schema triggers a + clear warning (not a misleading hard-fail) and, when reachable, an on-demand + fetch of the current schema so it can validate correctly anyway. +- A document targeting a **different major** is deliberately rejected with a + dedicated code instead of silently mis-validated. +- Spec releases **auto-republish** the validator as a patch, so the playground + never silently goes stale again. +- The playground **always demonstrates the latest** validator and surfaces the + active schema version + an "update available" hint. + +## Non-Goals + +- Real multi-major validation (loading v2 semantics). v2 does not exist; building + for its unknown shape is speculative (YAGNI). We build **v1-only but + major-ready**: a thin boundary that refuses non-v1 documents cleanly. +- Per-minor historical validation (validating against 1.1.0 vs 1.2.0 + individually). Within v1 everything is additive, so an old document already + validates against the newest v1 schema — granular minor selection has no real + payoff and would force bundling every historical schema. +- The site multi-sport content redesign — tracked as its own later spec. + +--- + +## Strand A — Spec / schema changes (`opencoachingformat/spec`) + +All additive → **MINOR bump** (targets v1.4.0), gated by an RFC. + +### A1. `meta.min_schema_version` (new, optional) + +An optional SemVer string on the existing `meta` object: + +```json +"meta": { "min_schema_version": "1.4.0", ... } +``` + +Semantics: "correctly validating this document requires schema **>= this +version**." Absent → no minimum (today's behavior; fully backwards-compatible). +An authoring tool sets it when it emits a field introduced in a specific minor. +This is the signal that lets a validator distinguish "I am outdated" from "the +document is wrong." + +Schema: add to `meta.properties` as `{ "type": "string", "pattern": "" }`, +`meta` keeps `additionalProperties: false` (the key is now listed). + +### A2. Machine-readable schema version + +Today the version lives only as free text in the root `$comment` +("Schema version 1.3.0. …"). Add a robust machine-readable keyword at the schema +root: + +```json +"x-ocf-version": "1.4.0" +``` + +JSON Schema ignores unknown keywords, so validation is unaffected. The validator +reads its bundled version from `x-ocf-version` instead of parsing prose. The +release version-consistency sweep gains a 5th location (`x-ocf-version`) beside +`package.json`, `$comment`, `:version:`, and the changelog. + +### A3. `versions.json` manifest + +`release-spec.yml` generates/updates `dist/schema/versions.json`, deployed to +`https://opencoachingformat.org/schema/versions.json`: + +```json +{ + "latest": "1.4.0", + "major": "v1", + "versions": ["1.0.0","1.0.1","1.1.0","1.1.1","1.2.0","1.3.0","1.4.0"], + "schema_url": "https://opencoachingformat.org/schema/v1.json" +} +``` + +CORS is already `access-control-allow-origin: *` on the schema host, so both the +validator and the browser playground can fetch it. This is the single authority +for update checks. + +--- + +## Strand B — Validator + site (`ocf-validator`, `spec/site`) + +### B1. `schemaInfo` export + +The validator reads `x-ocf-version` from its bundled schema and exports: + +```ts +export const schemaInfo = { version: "1.4.0", major: "v1", id: "https://opencoachingformat.org/schema/v1.json" }; +``` + +### B2. Major guard + +On validate, read `doc.$schema`. If it points at a **different major** than the +bundled schema (e.g. `.../schema/v2.json` under a v1 validator), stop with a +dedicated error `SCHEMA_MAJOR_UNSUPPORTED` — never silently run v1 semantics on a +v2 document. Absent `$schema` or matching major → proceed as today. + +### B3. Minor-gap handling (the core scenario) + +When `meta.min_schema_version` is **higher** than the bundled version: + +- **Sync path (`validate`)**: emit warning `VALIDATOR_MAYBE_OUTDATED` + ("document requires >= 1.4.0; this validator bundles 1.3.0 — the errors below + may be caused by an out-of-date validator") and best-effort validate against + the bundled schema. Nothing is silently swallowed. No network in the sync path. +- **Async path (`validateAsync`)**: attempt a **one-time** fetch of the current + schema from the `$schema` URL. + - Fetch OK → validate against the fetched newer schema; `Result.schema.validatedAgainst` = fetched version. + - Fetch fails / offline → fall back to the sync behavior (warning + best-effort). + +### B4. API shape (backwards-compatible) + +- `validate(doc): Result` stays **synchronous** and offline — unchanged + signature. Minor-gap → warning, never a fetch. +- `validateAsync(doc, { fetchLatestSchema = true }): Promise` is new and + may fetch on demand (B3). The existing sync API is not broken. + +### B5. `Result.schema` (additive) + +```ts +Result.schema = { + validatedAgainst: "1.3.0", // version actually used + documentDeclared: "https://.../schema/v1.json" | null, // doc's $schema + requiredByDoc: "1.4.0" | null, // doc's meta.min_schema_version + match: true // major matches AND validatedAgainst >= requiredByDoc +}; +``` + +`match` is computed against `validatedAgainst` (the version actually used), not +against the bundled version — so after a successful on-demand fetch (B3), +`validatedAgainst` may exceed the bundled version and `match` is `true`. + +Existing `valid`/`errors`/`warnings`/`summary` fields are untouched. + +### B6. New codes + +- `SCHEMA_MAJOR_UNSUPPORTED` (error) — document targets a different schema major. +- `VALIDATOR_MAYBE_OUTDATED` (warning) — document requires a newer minor than + bundled and the current schema could not be used. + +Both added to `shared/error-codes.json` (TS + Python mirrors) with spec_refs. + +### B7. Auto-republish (CI) + +Extend `sync-from-spec.yml`: after syncing the schema, **also** bump the patch +version of `packages/ts/package.json` (+ lockfile) in the same PR. This is a +**patch** because a pure schema sync changes only the bundled (additive) schema +bytes, not validator logic. On merge of the `auto/schema-sync-*` PR, a workflow +**auto-tags** `vX.Y.Z`, which triggers the existing `release-ts.yml` → npm +publish. So: spec release ⇒ schema-sync PR (human-reviewed) ⇒ merge ⇒ auto-tag ⇒ +auto npm release, with no manual step. + +**Boundary:** auto-republish covers only the pure-patch case (schema bytes +changed, validator code unchanged). If a sync ever needs a validator *code* +change (e.g. a new error code, a breaking schema shape), CI surfaces that (the +bundle/tests) and a human handles the non-patch release deliberately. + +### B8. Playground transparency (`spec/site`) + +- Show "validated against **1.3.0**" + "document declares **v1** ✓/✗" from + `Result.schema`. +- On load, compare the validator version to `versions.json`'s `latest`; if newer, + show a discreet "newer schema version X available" hint. +- Change the pin from exact `0.1.1` to **`@latest`** (`OCF_VALIDATOR_VERSION` + becomes `latest`) so the playground always demonstrates the newest validator, + backed by the update hint. + +--- + +## Data flow (validate, async path) + +``` +doc ──> read $schema (major?) ──mismatch──> SCHEMA_MAJOR_UNSUPPORTED (stop) + └─match/absent─> read meta.min_schema_version + ├─ <= bundled ──> validate against bundled schema + └─ > bundled ──> try fetch current schema + ├─ ok ──> validate against fetched schema + └─ fail ──> VALIDATOR_MAYBE_OUTDATED + best-effort bundled + ──> assemble Result (+ Result.schema block) +``` + +## Testing + +- **A (schema):** valid fixture with `meta.min_schema_version`; invalid fixture + with a malformed version string; `x-ocf-version` present & machine-read; + `versions.json` shape asserted in the release build. +- **B2 guard:** a doc with `$schema` = `.../v2.json` → `SCHEMA_MAJOR_UNSUPPORTED`, + no v1 semantic errors leaking. +- **B3 sync:** doc with `min_schema_version` above bundled → `VALIDATOR_MAYBE_OUTDATED` + warning + best-effort; **async** with a stubbed fetch returning a newer schema + → validates clean; stubbed fetch failure → falls back to the warning. +- **B4 compat:** existing `validate(doc)` call sites unchanged; TS + Python + conformance parity maintained. +- **B7 CI:** the sync workflow produces a patch bump; the auto-tag triggers a + release (dry-run/asserted in CI where possible). +- **B8:** playground renders the schema block + update hint; `@latest` resolves. + +## Rollout order + +1. Strand A RFC + schema/manifest (spec v1.4.0), released. +2. Strand B validator (schemaInfo, guard, gap handling, codes, API) → validator + minor release (new public API = a minor, e.g. 0.2.0). +3. B7 auto-republish wiring (verified end-to-end on the next sync). +4. B8 playground transparency + `@latest` pin. + +Python mirror (`packages/py`) tracks B2/B3/B5/B6 for conformance parity. diff --git a/examples/invalid/min-schema-version-bad.json b/examples/invalid/min-schema-version-bad.json new file mode 100644 index 0000000..fe9ef19 --- /dev/null +++ b/examples/invalid/min-schema-version-bad.json @@ -0,0 +1,7 @@ +{ + "sport": "basketball", + "meta": { "id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d", "title": "Bad min version", "min_schema_version": "1.4" }, + "court": { "ruleset": "fiba", "type": "half_court" }, + "entities": [ { "type": "offense", "nr": 2, "x": -6, "y": 13 } ], + "frames": [ { "id": "f", "actions": [ { "player": "offense_2", "type": "move", "moves": [ { "to": { "named": "right_wing" } } ] } ], "end_state": { "offense_2": { "named": "right_wing" } } } ] +} diff --git a/examples/min-schema-version.ocf.json b/examples/min-schema-version.ocf.json new file mode 100644 index 0000000..a83ee5f --- /dev/null +++ b/examples/min-schema-version.ocf.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://opencoachingformat.org/schema/v1.json", + "sport": "basketball", + "meta": { + "id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d", + "title": "Declares a minimum schema version", + "min_schema_version": "1.4.0" + }, + "court": { "ruleset": "fiba", "type": "half_court" }, + "entities": [ + { "type": "offense", "nr": 2, "x": -6.75, "y": 13.98 } + ], + "frames": [ + { + "id": "f1", + "label": "Static", + "actions": [ + { "player": "offense_2", "type": "move", "moves": [ { "to": { "named": "right_wing" } } ] } + ], + "end_state": { "offense_2": { "named": "right_wing" } } + } + ] +} diff --git a/package-lock.json b/package-lock.json index ec827b4..d86f915 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@opencoachingformat/spec", - "version": "1.3.0", + "version": "1.4.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@opencoachingformat/spec", - "version": "1.3.0", + "version": "1.4.0", "license": "CC-BY-4.0", "devDependencies": { "ajv-cli": "^5.0.0", diff --git a/package.json b/package.json index 0de03ab..fea2fb4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@opencoachingformat/spec", - "version": "1.3.0", + "version": "1.4.0", "type": "module", "description": "Open standard for team-sport coaching diagrams and animations (invasion team sports; basketball first).", "repository": { @@ -36,11 +36,13 @@ "access": "public" }, "scripts": { - "validate": "ajv validate --spec=draft7 -s schema/v1.json -d \"examples/*.ocf.json\" --all-errors --verbose -c ajv-formats", + "validate": "ajv validate --spec=draft7 --strict-schema=false -s schema/v1.json -d \"examples/*.ocf.json\" --all-errors --verbose -c ajv-formats", "test:invalid": "node scripts/check-invalid.mjs", "test:positions": "node --test test/positions-anchors.test.mjs", "test:sport": "node --test test/sport-branches.test.mjs", - "test": "npm run validate && npm run test:invalid && npm run test:positions && npm run test:sport" + "test:version": "node --test test/schema-version.test.mjs", + "test:manifest": "node --test test/versions-manifest.test.mjs", + "test": "npm run validate && npm run test:invalid && npm run test:positions && npm run test:sport && npm run test:version && npm run test:manifest" }, "devDependencies": { "ajv-cli": "^5.0.0", diff --git a/rfcs/0005-schema-version-signals.md b/rfcs/0005-schema-version-signals.md new file mode 100644 index 0000000..205ce26 --- /dev/null +++ b/rfcs/0005-schema-version-signals.md @@ -0,0 +1,143 @@ +# RFC 0005 — Schema Version Signals (`meta.min_schema_version`, `x-ocf-version`, `versions.json`) + +| Field | Value | +|-------------|-------------------------------------------| +| RFC Number | 0005 | +| Title | Schema Version Signals | +| Author(s) | opencoachingformat maintainers | +| Created | 2026-08-28 | +| Status | Accepted | +| Affects | Schema + Spec + Release CI | +| Version | Targets OCF v1.4.0 | + +--- + +## Summary + +Adds the version signals a version-aware validator needs, all additive: + +- **`meta.min_schema_version`** (optional SemVer string): the minimum schema + version required to validate the document correctly. Lets a validator + distinguish *"the document is wrong"* from *"I am out of date."* +- **`x-ocf-version`** (root custom keyword): a machine-readable schema version, so + tools read it instead of parsing the free-text `$comment`. +- **`versions.json`**: a published manifest of all released schema versions plus + `latest`, the single authority for update checks. + +Additive → **minor (v1.4.0)**. + +--- + +## Motivation + +The published playground silently served an outdated validator after v1.3.0 +shipped (`@opencoachingformat/validator@0.1.0` bundled the v1.2.0 schema), so +every play using the new `side`/`arc` fields was rejected via +`additionalProperties: false`. A patch release fixed the acute case, but a deeper +gap remains: a validator today cannot tell a document that uses a *newer-minor* +field from a genuinely invalid document — `additionalProperties: false` rejects +both identically, with an identically misleading "field not allowed" message. + +These three signals give tooling the information to diagnose version skew instead +of guessing: + +- `min_schema_version` lets the validator say *"this document needs ≥ 1.4.0, I + bundle 1.3.0 — I may be out of date"* instead of blaming the document. +- `x-ocf-version` gives the validator a reliable read of its own bundled version. +- `versions.json` lets the validator/playground check whether a newer version + exists at all. + +--- + +## Detailed Design + +### `meta.min_schema_version` + +Optional string on `meta`, validated by the **same SemVer pattern already used** +for `based_on_formation.source_version` / `based_on_play.source_version` (RFC +0002). Absent → no minimum (today's behavior, fully backwards-compatible). An +authoring tool sets it when it emits a field introduced in a specific minor. + +`meta` keeps `additionalProperties: false` (the new key is listed). + +### `x-ocf-version` + +A string keyword at the schema root carrying the exact version (e.g. `"1.4.0"`). +JSON Schema ignores unknown keywords, so validation behavior is unchanged. The +release version-consistency sweep gains a fifth location beside `package.json`, +`$comment`, `:version:`, and the changelog. + +### `versions.json` + +Emitted by the release workflow to `https://opencoachingformat.org/schema/versions.json`: + +```json +{ + "latest": "1.4.0", + "major": "v1", + "versions": ["1.0.0", "1.0.1", "1.1.0", "1.1.1", "1.2.0", "1.3.0", "1.4.0"], + "schema_url": "https://opencoachingformat.org/schema/v1.json" +} +``` + +Assembled from the existing `v*` schema directories already on gh-pages plus the +current release tag. CORS is already `access-control-allow-origin: *` on the +schema host, so validators and browsers can fetch it. + +--- + +## Drawbacks + +- Two more optional/metadata additions to understand. Mitigated: both are inert + for existing documents (`min_schema_version` is optional; `x-ocf-version` does + not affect validation). +- `versions.json` is a new published artifact the release must keep correct; + covered by a unit-tested generator. + +--- + +## Alternatives Considered + +- **Parse the version from `$comment` prose.** Rejected: brittle string parsing + of a human-facing comment; `x-ocf-version` is explicit and stable. +- **A required `schema_version` on every document.** Rejected: breaking, and + redundant with `$schema` (which already pins the major line). The minimum-version + signal is only needed when a document uses newer-than-base fields, so it is + optional. +- **`min_minor_version` as a bare integer.** Rejected: less self-describing and + harder to extend; a full SemVer string reuses the existing pattern and tooling. + +--- + +## Backwards Compatibility + +- [x] No breaking changes (additive only) +- [ ] Breaking change — requires major version bump +- [ ] Deprecates existing fields + +Documents without `min_schema_version` are unchanged; `x-ocf-version` is a no-op +for validation; `$id`/filename stays `v1.json`. **Version impact: MINOR → v1.4.0.** + +--- + +## Testing / Regression + +- **valid:** a doc with `meta.min_schema_version: "1.4.0"` validates. +- **invalid:** `meta.min_schema_version: "1.4"` (not full SemVer) is rejected on + the pattern. +- **schema:** `x-ocf-version` is present, a string, and equals both `package.json` + and the `$comment` prose version. +- **manifest:** `versions.json` has the documented shape, sorts SemVer correctly + (e.g. `1.10.0` after `1.2.0`), and includes the new version. + +Validator-side consumption of these signals (major guard, minor-gap warning + +on-demand fetch, `Result.schema`) is a **separate** effort (Plan 2), not part of +this schema RFC. + +--- + +## References + +- Design: `docs/superpowers/specs/2026-08-28-version-aware-validator-design.md` +- Related: RFC 0002 (External References — SemVer pattern reused); the + v0.1.0 → 0.1.1 validator skew incident that motivated this. diff --git a/schema/v1.json b/schema/v1.json index 8605667..eb9fbda 100644 --- a/schema/v1.json +++ b/schema/v1.json @@ -1,7 +1,8 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "$id": "https://opencoachingformat.org/schema/v1.json", - "$comment": "Schema version 1.3.0. The $id stays v1 across all backwards-compatible 1.x releases; version-pinned copies live at //ocf-action-v1.json.", + "$comment": "Schema version 1.4.0. The $id stays v1 across all backwards-compatible 1.x releases; version-pinned copies live at //ocf-action-v1.json.", + "x-ocf-version": "1.4.0", "title": "Open Coaching Format", "description": "Open standard for team-sport coaching diagrams and animations (invasion team sports; basketball first). Semantic action model.", "type": "object", @@ -668,7 +669,12 @@ "source_format": { "type": "string", "enum": ["fiba", "fastdraw", "open", "custom"] }, "source_url": { "type": "string", "format": "uri" }, "based_on_formation": { "$ref": "#/definitions/based_on_formation" }, - "based_on_play": { "$ref": "#/definitions/based_on_play" } + "based_on_play": { "$ref": "#/definitions/based_on_play" }, + "min_schema_version": { + "type": "string", + "description": "Minimum OCF schema version required to validate this document correctly. Optional; when present MUST be valid SemVer. A validator whose bundled schema is older SHOULD warn that it may be out of date rather than treat unknown newer fields as errors.", + "pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$" + } }, "additionalProperties": false }, diff --git a/scripts/build-versions-manifest.mjs b/scripts/build-versions-manifest.mjs new file mode 100644 index 0000000..d0a26d1 --- /dev/null +++ b/scripts/build-versions-manifest.mjs @@ -0,0 +1,33 @@ +// Pure, testable manifest builder. The release workflow feeds it the existing v* +// schema dirs from gh-pages plus the current release tag, and writes the result +// to dist/schema/versions.json. +const SCHEMA_URL = "https://opencoachingformat.org/schema/v1.json"; + +function semverKey(v) { + return v.split(".").map((n) => parseInt(n, 10)); +} +function cmp(a, b) { + const [aa, ab, ac] = semverKey(a); + const [ba, bb, bc] = semverKey(b); + return aa - ba || ab - bb || ac - bc; +} + +export function buildVersionsManifest({ existing, current }) { + const all = [...existing, current] + .map((t) => t.replace(/^v/, "")) + .filter((v) => /^\d+\.\d+\.\d+$/.test(v)); + const unique = [...new Set(all)].sort(cmp); + const latest = unique[unique.length - 1]; + const major = "v" + semverKey(latest)[0]; + return { latest, major, versions: unique, schema_url: SCHEMA_URL }; +} + +// CLI: node build-versions-manifest.mjs "" +const invokedDirectly = import.meta.url === `file://${process.argv[1]}`; +if (invokedDirectly) { + const existing = (process.argv[2] || "").split(/\s+/).filter(Boolean); + const current = process.argv[3]; + process.stdout.write( + JSON.stringify(buildVersionsManifest({ existing, current }), null, 2) + "\n" + ); +} diff --git a/scripts/check-invalid.mjs b/scripts/check-invalid.mjs index 443b0c8..152e6cb 100644 --- a/scripts/check-invalid.mjs +++ b/scripts/check-invalid.mjs @@ -19,7 +19,7 @@ for (const file of files) { try { execFileSync( "npx", - ["ajv", "validate", "--spec=draft7", "-s", "schema/v1.json", "-d", filePath, "-c", "ajv-formats"], + ["ajv", "validate", "--spec=draft7", "--strict-schema=false", "-s", "schema/v1.json", "-d", filePath, "-c", "ajv-formats"], { stdio: "pipe" } ); validated = true; // ajv exited 0 => it accepted the bad fixture diff --git a/test/schema-version.test.mjs b/test/schema-version.test.mjs new file mode 100644 index 0000000..2e1b32c --- /dev/null +++ b/test/schema-version.test.mjs @@ -0,0 +1,21 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; + +const schema = JSON.parse(readFileSync(new URL("../schema/v1.json", import.meta.url))); +const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url))); + +test("schema carries a machine-readable x-ocf-version", () => { + assert.equal(typeof schema["x-ocf-version"], "string"); +}); + +test("x-ocf-version matches package.json version", () => { + assert.equal(schema["x-ocf-version"], pkg.version); +}); + +test("x-ocf-version matches the $comment prose version", () => { + const comment = schema.$comment ?? ""; + const m = comment.match(/Schema version (\d+\.\d+\.\d+)/); + assert.ok(m, "$comment should state 'Schema version X.Y.Z'"); + assert.equal(schema["x-ocf-version"], m[1]); +}); diff --git a/test/versions-manifest.test.mjs b/test/versions-manifest.test.mjs new file mode 100644 index 0000000..1a14735 --- /dev/null +++ b/test/versions-manifest.test.mjs @@ -0,0 +1,20 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { buildVersionsManifest } from "../scripts/build-versions-manifest.mjs"; + +test("manifest lists sorted versions with latest + major + schema_url", () => { + const m = buildVersionsManifest({ + existing: ["v1.0.0", "v1.2.0", "v1.10.0"], + current: "v1.4.0", + }); + assert.equal(m.major, "v1"); + assert.equal(m.schema_url, "https://opencoachingformat.org/schema/v1.json"); + assert.deepEqual(m.versions, ["1.0.0", "1.2.0", "1.4.0", "1.10.0"]); + assert.equal(m.latest, "1.10.0"); +}); + +test("current version is included even if not in existing", () => { + const m = buildVersionsManifest({ existing: ["v1.0.0"], current: "v1.4.0" }); + assert.ok(m.versions.includes("1.4.0")); + assert.equal(m.latest, "1.4.0"); +});