From 4a8268bb1202d35dff8ec1496866cb10fe9b74a4 Mon Sep 17 00:00:00 2001 From: Oliver-Marcus Eder Date: Thu, 27 Aug 2026 22:58:40 +0200 Subject: [PATCH 1/8] feat(schema): add reserved skeleton action types (tackle/clear/faceoff/check) for invasion sports --- schema/v1.json | 73 ++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 71 insertions(+), 2 deletions(-) diff --git a/schema/v1.json b/schema/v1.json index 6a97cb2..b748184 100644 --- a/schema/v1.json +++ b/schema/v1.json @@ -211,7 +211,7 @@ "action_ref": { "type": "string", "description": "Reference to another action in the same frame: '.'.", - "pattern": "^((offense|defense)_[1-9]|coach|(cone|station)_[1-9][0-9]*)\\.(move|cut|screen|defend|dribble|pass|shoot|rebound|pickup)$" + "pattern": "^((offense|defense)_[1-9]|coach|(cone|station)_[1-9][0-9]*)\\.(move|cut|screen|defend|dribble|pass|shoot|rebound|pickup|tackle|clear|faceoff|check)$" }, "movement_intensity": { @@ -402,6 +402,71 @@ "additionalProperties": false }, + "action_tackle": { + "type": "object", + "$comment": "Reserved for invasion sports (soccer/futsal/hockey). No variants defined yet; must be specified before promotion.", + "required": ["player", "type"], + "properties": { + "player": { "$ref": "#/definitions/entity_ref" }, + "type": { "const": "tackle" }, + "physicality": { "$ref": "#/definitions/physicality" }, + "tags": { "type": "array", "items": { "type": "string" } }, + "after": { "$ref": "#/definitions/action_ref" }, + "with": { "$ref": "#/definitions/action_ref" }, + "on_catch": { "type": "boolean" } + }, + "additionalProperties": false + }, + + "action_clear": { + "type": "object", + "$comment": "Reserved for invasion sports (soccer/futsal/hockey). No variants defined yet.", + "required": ["player", "type"], + "properties": { + "player": { "$ref": "#/definitions/entity_ref" }, + "type": { "const": "clear" }, + "ball_id": { "$ref": "#/definitions/ball_ref" }, + "intensity": { "$ref": "#/definitions/ball_intensity" }, + "tags": { "type": "array", "items": { "type": "string" } }, + "after": { "$ref": "#/definitions/action_ref" }, + "with": { "$ref": "#/definitions/action_ref" }, + "on_catch": { "type": "boolean" } + }, + "additionalProperties": false + }, + + "action_faceoff": { + "type": "object", + "$comment": "Reserved for hockey. No variants defined yet.", + "required": ["player", "type"], + "properties": { + "player": { "$ref": "#/definitions/entity_ref" }, + "type": { "const": "faceoff" }, + "tags": { "type": "array", "items": { "type": "string" } }, + "after": { "$ref": "#/definitions/action_ref" }, + "with": { "$ref": "#/definitions/action_ref" }, + "on_catch": { "type": "boolean" } + }, + "additionalProperties": false + }, + + "action_check": { + "type": "object", + "$comment": "Reserved for hockey. No variants defined yet.", + "required": ["player", "type"], + "properties": { + "player": { "$ref": "#/definitions/entity_ref" }, + "type": { "const": "check" }, + "on_player": { "$ref": "#/definitions/entity_ref" }, + "physicality": { "$ref": "#/definitions/physicality" }, + "tags": { "type": "array", "items": { "type": "string" } }, + "after": { "$ref": "#/definitions/action_ref" }, + "with": { "$ref": "#/definitions/action_ref" }, + "on_catch": { "type": "boolean" } + }, + "additionalProperties": false + }, + "action": { "description": "Any action. Discriminated by 'type'.", "oneOf": [ @@ -413,7 +478,11 @@ { "$ref": "#/definitions/action_pass" }, { "$ref": "#/definitions/action_shoot" }, { "$ref": "#/definitions/action_rebound" }, - { "$ref": "#/definitions/action_pickup" } + { "$ref": "#/definitions/action_pickup" }, + { "$ref": "#/definitions/action_tackle" }, + { "$ref": "#/definitions/action_clear" }, + { "$ref": "#/definitions/action_faceoff" }, + { "$ref": "#/definitions/action_check" } ] }, From a526822d2c1293205264efb652bd34537f34c493 Mon Sep 17 00:00:00 2001 From: Oliver-Marcus Eder Date: Thu, 27 Aug 2026 23:01:27 +0200 Subject: [PATCH 2/8] feat(schema): add optional sport field + per-sport action whitelist (RFC 0003) --- schema/v1.json | 147 +++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 137 insertions(+), 10 deletions(-) diff --git a/schema/v1.json b/schema/v1.json index b748184..44caceb 100644 --- a/schema/v1.json +++ b/schema/v1.json @@ -642,6 +642,13 @@ "$schema": { "type": "string", "format": "uri" }, + "sport": { + "type": "string", + "enum": ["basketball", "soccer", "handball", "hockey", "futsal"], + "default": "basketball", + "$comment": "Optional, default basketball (back-compat). Intended to become required in v2.0.0. basketball is fully defined; soccer/handball/hockey/futsal carry provisional minimal action whitelists pending sport-expert review." + }, + "meta": { "type": "object", "required": ["id", "title"], @@ -726,17 +733,137 @@ "additionalProperties": false, - "if": { - "type": "object", - "properties": { "court": { "type": "object", "properties": { "ruleset": { "const": "custom" } } } } - }, - "then": { - "type": "object", - "properties": { - "court": { + "allOf": [ + { + "if": { + "type": "object", + "properties": { "court": { "type": "object", "properties": { "ruleset": { "const": "custom" } } } } + }, + "then": { + "type": "object", + "properties": { "court": { "type": "object", "required": ["custom_dimensions"] } } + } + }, + { + "$comment": "sport absent OR basketball -> basketball action whitelist (default is a non-validating annotation, so absence must be handled explicitly for back-compat)", + "if": { + "anyOf": [ + { "not": { "required": ["sport"] } }, + { "properties": { "sport": { "const": "basketball" } } } + ] + }, + "then": { + "type": "object", + "properties": { + "frames": { + "type": "array", + "items": { + "type": "object", + "properties": { + "actions": { + "type": "array", + "items": { + "type": "object", + "properties": { "type": { "enum": ["move","cut","screen","defend","dribble","pass","shoot","rebound","pickup"] } } + } + } + } + } + } + } + } + }, + { + "if": { "required": ["sport"], "properties": { "sport": { "const": "soccer" } } }, + "then": { "type": "object", - "required": ["custom_dimensions"] + "properties": { + "frames": { + "type": "array", + "items": { + "type": "object", + "properties": { + "actions": { + "type": "array", + "items": { + "type": "object", + "properties": { "type": { "enum": ["move","pass","shoot","defend","dribble","tackle","clear"] } } + } + } + } + } + } + } + } + }, + { + "if": { "required": ["sport"], "properties": { "sport": { "const": "handball" } } }, + "then": { + "type": "object", + "properties": { + "frames": { + "type": "array", + "items": { + "type": "object", + "properties": { + "actions": { + "type": "array", + "items": { + "type": "object", + "properties": { "type": { "enum": ["move","pass","shoot","defend","cut","screen","pickup"] } } + } + } + } + } + } + } + } + }, + { + "if": { "required": ["sport"], "properties": { "sport": { "const": "hockey" } } }, + "then": { + "type": "object", + "properties": { + "frames": { + "type": "array", + "items": { + "type": "object", + "properties": { + "actions": { + "type": "array", + "items": { + "type": "object", + "properties": { "type": { "enum": ["move","pass","shoot","defend","dribble","clear","faceoff","check"] } } + } + } + } + } + } + } + } + }, + { + "if": { "required": ["sport"], "properties": { "sport": { "const": "futsal" } } }, + "then": { + "type": "object", + "properties": { + "frames": { + "type": "array", + "items": { + "type": "object", + "properties": { + "actions": { + "type": "array", + "items": { + "type": "object", + "properties": { "type": { "enum": ["move","pass","shoot","defend","dribble","tackle","clear"] } } + } + } + } + } + } + } } } - } + ] } From 45dadf114c0cc8c31eafeff3eed6746359266bc2 Mon Sep 17 00:00:00 2001 From: Oliver-Marcus Eder Date: Thu, 27 Aug 2026 23:06:29 +0200 Subject: [PATCH 3/8] test(schema): add valid fixtures for sport-tagged basketball and soccer Co-Authored-By: Claude Opus 4.8 --- examples/sport-basketball.ocf.json | 16 ++++++++++++++++ examples/sport-soccer.ocf.json | 16 ++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 examples/sport-basketball.ocf.json create mode 100644 examples/sport-soccer.ocf.json diff --git a/examples/sport-basketball.ocf.json b/examples/sport-basketball.ocf.json new file mode 100644 index 0000000..06eb9dc --- /dev/null +++ b/examples/sport-basketball.ocf.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://opencoachingformat.org/schema/v1.json", + "sport": "basketball", + "meta": { "id": "a1b2c3d4-1111-4a6e-8f0c-2d5e9a1b3c7f", "title": "Sport-tagged basketball", "created": "2026-08-27T10:00:00Z", "source_format": "open" }, + "court": { "ruleset": "fiba", "type": "half_court", "drill_focus": "offense" }, + "entities": [ + { "type": "offense", "nr": 1, "x": 0.0, "y": 5.68 }, + { "type": "offense", "nr": 4, "x": -2.45, "y": 8.2 } + ], + "balls": [ { "id": "ball_1", "carried_by": "offense_1" } ], + "frames": [ + { "id": "f1", "label": "Screen", "description": "big sets a ball screen", + "actions": [ { "player": "offense_4", "type": "screen", "for_player": "offense_1", "variant": "ball_screen" } ], + "end_state": { "offense_1": { "named": "top_of_the_key" }, "offense_4": { "x": -0.5, "y": 6.7 } } } + ] +} diff --git a/examples/sport-soccer.ocf.json b/examples/sport-soccer.ocf.json new file mode 100644 index 0000000..afeb122 --- /dev/null +++ b/examples/sport-soccer.ocf.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://opencoachingformat.org/schema/v1.json", + "sport": "soccer", + "meta": { "id": "b2c3d4e5-2222-4a6e-8f0c-2d5e9a1b3c7f", "title": "Sport-tagged soccer (provisional)", "created": "2026-08-27T10:00:00Z", "source_format": "open" }, + "court": { "ruleset": "custom", "type": "full_court", "custom_dimensions": { "unit": "m", "length": 105, "width": 68, "basket_from_baseline": 0, "three_point_distance": 0, "paint_width": 40.3, "paint_depth": 16.5, "free_throw_distance": 11 } }, + "entities": [ + { "type": "offense", "nr": 9, "x": 0.0, "y": 30.0 }, + { "type": "defense", "nr": 4, "x": 0.0, "y": 40.0 } + ], + "balls": [ { "id": "ball_1", "carried_by": "offense_9" } ], + "frames": [ + { "id": "f1", "label": "Tackle", "description": "defender challenges", + "actions": [ { "player": "defense_4", "type": "tackle" }, { "player": "offense_9", "type": "pass", "to_player": "offense_9" } ], + "end_state": { "offense_9": { "x": 0.0, "y": 30.0 }, "defense_4": { "x": 0.0, "y": 38.0 } } } + ] +} From 4e8945095ab46d80cbb3964f4d0a0eff88fa3cd4 Mon Sep 17 00:00:00 2001 From: Oliver-Marcus Eder Date: Thu, 27 Aug 2026 23:09:14 +0200 Subject: [PATCH 4/8] test(schema): add invalid fixtures for sport whitelist enforcement Co-Authored-By: Claude Opus 4.8 --- examples/invalid/sport-basketball-tackle.json | 7 +++++++ examples/invalid/sport-soccer-screen.json | 7 +++++++ examples/invalid/sport-unknown.json | 6 ++++++ 3 files changed, 20 insertions(+) create mode 100644 examples/invalid/sport-basketball-tackle.json create mode 100644 examples/invalid/sport-soccer-screen.json create mode 100644 examples/invalid/sport-unknown.json diff --git a/examples/invalid/sport-basketball-tackle.json b/examples/invalid/sport-basketball-tackle.json new file mode 100644 index 0000000..2f59dc2 --- /dev/null +++ b/examples/invalid/sport-basketball-tackle.json @@ -0,0 +1,7 @@ +{ + "sport": "basketball", + "meta": { "id": "d4e5f6a7-4444-4a6e-8f0c-2d5e9a1b3c7f", "title": "Tackle under basketball" }, + "court": { "ruleset": "fiba", "type": "half_court" }, + "entities": [ { "type": "defense", "nr": 4, "x": 0, "y": 8 } ], + "frames": [ { "id": "f", "actions": [ { "player": "defense_4", "type": "tackle" } ], "end_state": {} } ] +} diff --git a/examples/invalid/sport-soccer-screen.json b/examples/invalid/sport-soccer-screen.json new file mode 100644 index 0000000..38cc126 --- /dev/null +++ b/examples/invalid/sport-soccer-screen.json @@ -0,0 +1,7 @@ +{ + "sport": "soccer", + "meta": { "id": "e5f6a7b8-5555-4a6e-8f0c-2d5e9a1b3c7f", "title": "Screen under soccer" }, + "court": { "ruleset": "custom", "type": "full_court", "custom_dimensions": { "unit": "m", "length": 105, "width": 68, "basket_from_baseline": 0, "three_point_distance": 0, "paint_width": 40.3, "paint_depth": 16.5, "free_throw_distance": 11 } }, + "entities": [ { "type": "offense", "nr": 4, "x": 0, "y": 8 } ], + "frames": [ { "id": "f", "actions": [ { "player": "offense_4", "type": "screen", "for_player": "offense_4" } ], "end_state": {} } ] +} diff --git a/examples/invalid/sport-unknown.json b/examples/invalid/sport-unknown.json new file mode 100644 index 0000000..a11df12 --- /dev/null +++ b/examples/invalid/sport-unknown.json @@ -0,0 +1,6 @@ +{ + "sport": "volleyball", + "meta": { "id": "c3d4e5f6-3333-4a6e-8f0c-2d5e9a1b3c7f", "title": "Unknown sport" }, + "court": { "ruleset": "fiba", "type": "half_court" }, + "entities": [], "frames": [ { "id": "f", "actions": [], "end_state": {} } ] +} From d0bfa17015223f87ae2305973e1ba5c966c2b7d8 Mon Sep 17 00:00:00 2001 From: Oliver-Marcus Eder Date: Thu, 27 Aug 2026 23:11:07 +0200 Subject: [PATCH 5/8] test(schema): assert every sport enum value has a whitelist branch + back-compat branch --- package.json | 3 ++- test/sport-branches.test.mjs | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 test/sport-branches.test.mjs diff --git a/package.json b/package.json index 54e973d..72fbad9 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,8 @@ "validate": "ajv validate --spec=draft7 -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": "npm run validate && npm run test:invalid && npm run test:positions" + "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" }, "devDependencies": { "ajv-cli": "^5.0.0", diff --git a/test/sport-branches.test.mjs b/test/sport-branches.test.mjs new file mode 100644 index 0000000..4de384d --- /dev/null +++ b/test/sport-branches.test.mjs @@ -0,0 +1,34 @@ +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), "utf-8")); + +// Collect the sports each allOf branch handles (via if.properties.sport.const, +// or the basketball anyOf branch that also matches absent sport). +function branchSports(allOf) { + const handled = new Set(); + for (const b of allOf) { + const c = b.if?.properties?.sport?.const; + if (c) handled.add(c); + const anyOf = b.if?.anyOf; + if (anyOf?.some((x) => x.properties?.sport?.const === "basketball")) handled.add("basketball"); + } + return handled; +} + +test("every sport enum value has a whitelist branch", () => { + const enumVals = schema.properties.sport.enum; + assert.ok(Array.isArray(enumVals) && enumVals.length > 0, "sport enum present"); + const handled = branchSports(schema.allOf); + for (const s of enumVals) { + assert.ok(handled.has(s), `sport "${s}" has no if/then whitelist branch`); + } +}); + +test("basketball branch also matches an absent sport (back-compat)", () => { + const bbBranch = schema.allOf.find((b) => + b.if?.anyOf?.some((x) => x.not?.required?.includes("sport")), + ); + assert.ok(bbBranch, "a branch matches when sport is absent (default-annotation back-compat)"); +}); From 879bd0d4040015b481870177c05808ec112f12f4 Mon Sep 17 00:00:00 2001 From: Oliver-Marcus Eder Date: Thu, 27 Aug 2026 23:13:35 +0200 Subject: [PATCH 6/8] feat(sports): seed basketball vocabulary + provisional sport skeletons --- package.json | 8 +++++++- sports/basketball-v1.json | 17 +++++++++++++++++ sports/futsal-v0.0.1.json | 10 ++++++++++ sports/handball-v0.0.1.json | 10 ++++++++++ sports/hockey-v0.0.1.json | 10 ++++++++++ sports/soccer-v0.0.1.json | 10 ++++++++++ test/sport-branches.test.mjs | 24 +++++++++++++++++++++++- 7 files changed, 87 insertions(+), 2 deletions(-) create mode 100644 sports/basketball-v1.json create mode 100644 sports/futsal-v0.0.1.json create mode 100644 sports/handball-v0.0.1.json create mode 100644 sports/hockey-v0.0.1.json create mode 100644 sports/soccer-v0.0.1.json diff --git a/package.json b/package.json index 72fbad9..fe1de1d 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "files": [ "schema", "positions", + "sports", "README.md", "LICENSE" ], @@ -24,7 +25,12 @@ "./positions/fiba-v1.json": "./positions/fiba-v1.json", "./positions/nba-v1.json": "./positions/nba-v1.json", "./positions/ncaa-v1.json": "./positions/ncaa-v1.json", - "./positions/nfhs-v1.json": "./positions/nfhs-v1.json" + "./positions/nfhs-v1.json": "./positions/nfhs-v1.json", + "./sports/basketball-v1.json": "./sports/basketball-v1.json", + "./sports/soccer-v0.0.1.json": "./sports/soccer-v0.0.1.json", + "./sports/handball-v0.0.1.json": "./sports/handball-v0.0.1.json", + "./sports/hockey-v0.0.1.json": "./sports/hockey-v0.0.1.json", + "./sports/futsal-v0.0.1.json": "./sports/futsal-v0.0.1.json" }, "publishConfig": { "access": "public" diff --git a/sports/basketball-v1.json b/sports/basketball-v1.json new file mode 100644 index 0000000..2df2280 --- /dev/null +++ b/sports/basketball-v1.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://opencoachingformat.org/registry/sports/sport-schema-v1.json", + "sport": "basketball", + "version": "1.0.0", + "status": "defined", + "action_types": ["move","cut","screen","defend","dribble","pass","shoot","rebound","pickup"], + "variants": { + "cut": ["backdoor","give_and_go","flash","v_cut","l_cut","curl","flare","fade","basket"], + "screen": ["ball_screen","back_screen","down_screen","flare_screen","cross_screen","pin_down"], + "defend": ["on_ball","deny","help","hedge","switch","box_out"], + "pass": ["chest","bounce","overhead","lob","baseball","hand_off","outlet"], + "shoot": ["jumper","three","layup","floater","dunk","hook","free_throw"], + "rebound": ["offensive","defensive"] + }, + "outcomes": ["make","miss","turnover","steal","foul"], + "rulesets": ["fiba","nba","ncaa","nfhs"] +} diff --git a/sports/futsal-v0.0.1.json b/sports/futsal-v0.0.1.json new file mode 100644 index 0000000..55af8aa --- /dev/null +++ b/sports/futsal-v0.0.1.json @@ -0,0 +1,10 @@ +{ + "$schema": "https://opencoachingformat.org/registry/sports/sport-schema-v1.json", + "sport": "futsal", + "version": "0.0.1", + "status": "provisional", + "action_types": ["move","pass","shoot","defend","dribble","tackle","clear"], + "variants": {}, + "outcomes": [], + "rulesets": [] +} diff --git a/sports/handball-v0.0.1.json b/sports/handball-v0.0.1.json new file mode 100644 index 0000000..891b8c7 --- /dev/null +++ b/sports/handball-v0.0.1.json @@ -0,0 +1,10 @@ +{ + "$schema": "https://opencoachingformat.org/registry/sports/sport-schema-v1.json", + "sport": "handball", + "version": "0.0.1", + "status": "provisional", + "action_types": ["move","pass","shoot","defend","cut","screen","pickup"], + "variants": {}, + "outcomes": [], + "rulesets": [] +} diff --git a/sports/hockey-v0.0.1.json b/sports/hockey-v0.0.1.json new file mode 100644 index 0000000..9398b56 --- /dev/null +++ b/sports/hockey-v0.0.1.json @@ -0,0 +1,10 @@ +{ + "$schema": "https://opencoachingformat.org/registry/sports/sport-schema-v1.json", + "sport": "hockey", + "version": "0.0.1", + "status": "provisional", + "action_types": ["move","pass","shoot","defend","dribble","clear","faceoff","check"], + "variants": {}, + "outcomes": [], + "rulesets": [] +} diff --git a/sports/soccer-v0.0.1.json b/sports/soccer-v0.0.1.json new file mode 100644 index 0000000..afaaeca --- /dev/null +++ b/sports/soccer-v0.0.1.json @@ -0,0 +1,10 @@ +{ + "$schema": "https://opencoachingformat.org/registry/sports/sport-schema-v1.json", + "sport": "soccer", + "version": "0.0.1", + "status": "provisional", + "action_types": ["move","pass","shoot","defend","dribble","tackle","clear"], + "variants": {}, + "outcomes": [], + "rulesets": [] +} diff --git a/test/sport-branches.test.mjs b/test/sport-branches.test.mjs index 4de384d..2455128 100644 --- a/test/sport-branches.test.mjs +++ b/test/sport-branches.test.mjs @@ -1,6 +1,6 @@ import { test } from "node:test"; import assert from "node:assert/strict"; -import { readFileSync } from "node:fs"; +import { readFileSync, readdirSync } from "node:fs"; const schema = JSON.parse(readFileSync(new URL("../schema/v1.json", import.meta.url), "utf-8")); @@ -32,3 +32,25 @@ test("basketball branch also matches an absent sport (back-compat)", () => { ); assert.ok(bbBranch, "a branch matches when sport is absent (default-annotation back-compat)"); }); + +test("each sport skeleton's action_types matches its schema whitelist branch", () => { + const dir = new URL("../sports/", import.meta.url); + const files = readdirSync(dir).filter((f) => f.endsWith(".json")); + const branchWhitelist = (sport) => { + for (const b of schema.allOf) { + const isBB = b.if?.anyOf?.some((x) => x.properties?.sport?.const === "basketball"); + const c = b.if?.properties?.sport?.const; + if ((sport === "basketball" && isBB) || c === sport) { + return b.then.properties.frames.items.properties.actions.items.properties.type.enum; + } + } + return null; + }; + for (const f of files) { + const data = JSON.parse(readFileSync(new URL(f, dir), "utf-8")); + const wl = branchWhitelist(data.sport); + assert.ok(wl, `no schema branch for sport ${data.sport}`); + assert.deepEqual([...data.action_types].sort(), [...wl].sort(), + `${f} action_types must match the schema whitelist for ${data.sport}`); + } +}); From 7cb162d816126491df6baf6693670caefbcc2c41 Mon Sep 17 00:00:00 2001 From: Oliver-Marcus Eder Date: Thu, 27 Aug 2026 23:17:00 +0200 Subject: [PATCH 7/8] docs: document sport scoping, reconcile framing, bump to 1.2.0, accept RFC 0003 --- README.md | 6 +++--- docs/specification-v1.adoc | 31 ++++++++++++++++++++++++++++++- package.json | 4 ++-- schema/v1.json | 4 ++-- 4 files changed, 37 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 1505db1..3fe6106 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,16 @@ # Open Coaching Format (OCF) -**An open standard for representing sports coaching diagrams, drill animations, and playbooks as structured data.** +**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.1.1-blue)](schema/v1.json) +[![Schema Version](https://img.shields.io/badge/schema-v1.2.0-blue)](schema/v1.json) [![Status: Draft](https://img.shields.io/badge/status-draft-yellow)]() --- ## What is OCF? -The Open Coaching Format is a JSON-based standard for encoding sports coaching content — drills, plays, playbooks, and animated diagrams — in a structured, interoperable format. +The Open Coaching Format is a JSON-based standard for encoding team-sport coaching content — drills, plays, playbooks, and animated diagrams — in a structured, interoperable format. It targets **invasion team sports** (basketball, soccer, handball, hockey, futsal), with **basketball fully defined first** and the others reserved with provisional vocabularies. **No open standard for this currently exists.** All major coaching tools (FIBA Europe Coaching App, FastDraw, Sportplan.net) use proprietary formats with image-only export. OCF changes that. diff --git a/docs/specification-v1.adoc b/docs/specification-v1.adoc index 42a67b5..bb61b8a 100644 --- a/docs/specification-v1.adoc +++ b/docs/specification-v1.adoc @@ -1,5 +1,5 @@ = Open Coaching Format — Open Standard for Basketball Drill Diagrams -:version: 1.1.1 +:version: 1.2.0 :doctype: book :toc: left :toc-title: Table of Contents @@ -527,6 +527,28 @@ Rendered as a numbered marker with optional description. Used for circuit traini --- +== Sport + +OCF targets **invasion team sports** — two teams sharing a field, ball/puck +possession, and off-ball movement (basketball, soccer, handball, hockey, +futsal). Basketball is fully defined; the others are reserved with provisional +action vocabularies. + +The optional `sport` field declares the sport: + +[source,json] +---- +"sport": "basketball" +---- + +`sport` is optional and defaults to `basketball` for backwards compatibility; +it is planned to become required in v2. The declared sport determines which +action types (and variants) are valid — e.g. `screen`/`rebound` are valid under +`basketball`, `tackle`/`clear` under `soccer`. Documents with no `sport` field +are treated as basketball. + +--- + == Actions Movement and ball events are expressed as semantic *actions*, not geometric @@ -1413,6 +1435,13 @@ npx ajv-cli validate \ [appendix] == Changelog +=== v1.2.0 + +Adds the optional `sport` field and per-sport action-type scoping (RFC 0003). +Basketball is fully defined; soccer/handball/hockey/futsal are reserved with +provisional vocabularies. Additive and backwards-compatible — documents without +a `sport` field validate as basketball. + === v1.1.1 Tooling and distribution fixes only; no schema shape change. Publishes the diff --git a/package.json b/package.json index fe1de1d..bab0600 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,8 @@ { "name": "@opencoachingformat/spec", - "version": "1.1.1", + "version": "1.2.0", "type": "module", - "description": "Open standard for basketball drill diagrams and animations.", + "description": "Open standard for team-sport coaching diagrams and animations (invasion team sports; basketball first).", "repository": { "type": "git", "url": "git+https://github.com/opencoachingformat/spec.git" diff --git a/schema/v1.json b/schema/v1.json index 44caceb..1d4265e 100644 --- a/schema/v1.json +++ b/schema/v1.json @@ -1,9 +1,9 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "$id": "https://opencoachingformat.org/schema/v1.json", - "$comment": "Schema version 1.1.1. The $id stays v1 across all backwards-compatible 1.x releases; version-pinned copies live at //ocf-action-v1.json.", + "$comment": "Schema version 1.2.0. The $id stays v1 across all backwards-compatible 1.x releases; version-pinned copies live at //ocf-action-v1.json.", "title": "Open Coaching Format", - "description": "Open standard for basketball drill diagrams and animations. Semantic action model.", + "description": "Open standard for team-sport coaching diagrams and animations (invasion team sports; basketball first). Semantic action model.", "type": "object", "required": ["meta", "court", "entities", "frames"], From 846c2c62420c08096f120efee99ebbe678bdc8d4 Mon Sep 17 00:00:00 2001 From: Oliver-Marcus Eder Date: Thu, 27 Aug 2026 23:18:25 +0200 Subject: [PATCH 8/8] chore: sync package-lock to 1.2.0 --- package-lock.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6ab5f18..833f4ce 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,13 @@ { "name": "@opencoachingformat/spec", - "version": "1.1.0", + "version": "1.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@opencoachingformat/spec", - "version": "1.1.0", + "version": "1.2.0", + "license": "CC-BY-4.0", "devDependencies": { "ajv-cli": "^5.0.0", "ajv-formats": "^3.0.1"