From a6ce477d6abc52a32fac0a351b889136bf04d6f3 Mon Sep 17 00:00:00 2001 From: pos Date: Tue, 6 Oct 2026 21:01:39 +0200 Subject: [PATCH 1/5] M2.24: qualify packaged StoryForge CLI in OCI --- .github/workflows/oci.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/.github/workflows/oci.yml b/.github/workflows/oci.yml index e26dc11..9801dbb 100644 --- a/.github/workflows/oci.yml +++ b/.github/workflows/oci.yml @@ -43,3 +43,19 @@ jobs: push: ${{ github.event_name == 'push' }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} + + + qualify: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Build qualification image + run: docker build -t storyforge:qualification . + - name: CLI starts as packaged + run: docker run --rm storyforge:qualification --help + - name: Validate Lighthouse in container + run: docker run --rm -v "$PWD:/work:ro" storyforge:qualification validate examples/lighthouse/story.yaml + - name: Solve Lighthouse in container + run: docker run --rm -v "$PWD:/work:ro" storyforge:qualification solve examples/lighthouse/story.yaml + - name: Analyze Lighthouse in container + run: docker run --rm -v "$PWD:/work:ro" storyforge:qualification analyze examples/lighthouse/story.yaml From c526e3fd4fc97b1172256900e8f82bee1b9bc128 Mon Sep 17 00:00:00 2001 From: pos Date: Tue, 6 Oct 2026 21:01:42 +0200 Subject: [PATCH 2/5] M2.24: document OCI qualification contract --- docs/oci-qualification.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 docs/oci-qualification.md diff --git a/docs/oci-qualification.md b/docs/oci-qualification.md new file mode 100644 index 0000000..d6e226a --- /dev/null +++ b/docs/oci-qualification.md @@ -0,0 +1,15 @@ +# OCI qualification + +The OCI workflow does more than prove that the image can be assembled. A separate qualification job builds the packaged image and executes StoryForge inside it. + +The qualification currently proves: + +- the installed CLI starts through the image entrypoint; +- the Lighthouse reference story validates; +- the deterministic solver completes against Lighthouse; +- deterministic analysis completes against Lighthouse; +- the repository is mounted read-only during reference-story checks. + +This catches packaging errors that Python unit tests cannot see, including missing package data, broken console entrypoints, dependency omissions, and container-only path problems. + +AI-backed network providers are intentionally excluded from OCI qualification. Their nondeterminism, credentials, and external availability should not decide whether the deterministic StoryForge runtime image is healthy. From caf7d8aa8ca1759b8e6c31a8c23fa3329772686a Mon Sep 17 00:00:00 2001 From: pos Date: Tue, 6 Oct 2026 21:07:24 +0200 Subject: [PATCH 3/5] M2.24: package StoryForge schema with runtime --- .../schemas/storyforge-v0.schema.json | 1030 +++++++++++++++++ 1 file changed, 1030 insertions(+) create mode 100644 src/storyforge/schemas/storyforge-v0.schema.json diff --git a/src/storyforge/schemas/storyforge-v0.schema.json b/src/storyforge/schemas/storyforge-v0.schema.json new file mode 100644 index 0000000..545cf41 --- /dev/null +++ b/src/storyforge/schemas/storyforge-v0.schema.json @@ -0,0 +1,1030 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ploos.no/storyforge/schema/v0", + "title": "StoryForge IR v0", + "type": "object", + "required": [ + "storyforge", + "start", + "scenes", + "endings" + ], + "properties": { + "storyforge": { + "const": "0" + }, + "metadata": { + "type": "object", + "properties": { + "title": { + "type": "string", + "minLength": 1 + }, + "profile": { + "enum": [ + "visual-novel", + "point-and-click", + "text-adventure" + ] + }, + "id": { + "type": "string", + "pattern": "^[a-z0-9][a-z0-9._-]*$" + }, + "version": { + "type": "string", + "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+(?:[-+][0-9A-Za-z.-]+)?$" + } + }, + "additionalProperties": true + }, + "start": { + "type": "string", + "minLength": 1 + }, + "characters": { + "type": "object" + }, + "locations": { + "type": "object" + }, + "items": { + "type": "object" + }, + "variables": { + "type": "object", + "additionalProperties": { + "type": [ + "boolean", + "integer", + "number", + "string" + ] + } + }, + "scenes": { + "type": "object", + "minProperties": 1, + "additionalProperties": { + "type": "object", + "properties": { + "text": { + "type": "string" + }, + "choices": { + "type": "array", + "items": { + "type": "object", + "required": [ + "text" + ], + "properties": { + "text": { + "type": "string", + "minLength": 1 + }, + "goto": { + "type": "string", + "minLength": 1 + }, + "ending": { + "type": "string", + "minLength": 1 + }, + "requires": { + "type": "array", + "items": { + "type": "object", + "properties": { + "variable": { + "type": "string", + "minLength": 1 + }, + "equals": { + "type": [ + "boolean", + "integer", + "number", + "string" + ] + }, + "has_item": { + "type": "string", + "minLength": 1 + }, + "lacks_item": { + "type": "string", + "minLength": 1 + }, + "owned_by": { + "type": "object", + "required": [ + "item", + "character" + ], + "properties": { + "item": { + "type": "string", + "minLength": 1 + }, + "character": { + "type": "string", + "minLength": 1 + } + }, + "additionalProperties": false + }, + "character": { + "type": "object", + "required": [ + "id", + "attribute", + "equals" + ], + "properties": { + "id": { + "type": "string", + "minLength": 1 + }, + "attribute": { + "type": "string", + "minLength": 1 + }, + "equals": { + "type": [ + "boolean", + "integer", + "number", + "string" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false, + "oneOf": [ + { + "required": [ + "variable", + "equals" + ], + "not": { + "anyOf": [ + { + "required": [ + "has_item" + ] + }, + { + "required": [ + "lacks_item" + ] + } + ] + } + }, + { + "required": [ + "has_item" + ], + "not": { + "anyOf": [ + { + "required": [ + "variable" + ] + }, + { + "required": [ + "equals" + ] + }, + { + "required": [ + "lacks_item" + ] + } + ] + } + }, + { + "required": [ + "lacks_item" + ], + "not": { + "anyOf": [ + { + "required": [ + "variable" + ] + }, + { + "required": [ + "equals" + ] + }, + { + "required": [ + "has_item" + ] + } + ] + } + }, + { + "required": [ + "owned_by" + ], + "not": { + "anyOf": [ + { + "required": [ + "variable" + ] + }, + { + "required": [ + "equals" + ] + }, + { + "required": [ + "has_item" + ] + }, + { + "required": [ + "lacks_item" + ] + } + ] + } + }, + { + "required": [ + "character" + ], + "not": { + "anyOf": [ + { + "required": [ + "variable" + ] + }, + { + "required": [ + "equals" + ] + }, + { + "required": [ + "has_item" + ] + }, + { + "required": [ + "lacks_item" + ] + }, + { + "required": [ + "owned_by" + ] + } + ] + } + } + ] + } + }, + "effects": { + "type": "array", + "items": { + "type": "object", + "properties": { + "variable": { + "type": "string", + "minLength": 1 + }, + "set": { + "type": [ + "boolean", + "integer", + "number", + "string" + ] + }, + "increment": { + "type": "number" + }, + "decrement": { + "type": "number" + }, + "take_item": { + "type": "string", + "minLength": 1 + }, + "drop_item": { + "type": "string", + "minLength": 1 + }, + "give_item": { + "type": "object", + "required": [ + "item", + "character" + ], + "properties": { + "item": { + "type": "string", + "minLength": 1 + }, + "character": { + "type": "string", + "minLength": 1 + } + }, + "additionalProperties": false + }, + "character": { + "type": "object", + "required": [ + "id", + "attribute" + ], + "properties": { + "id": { + "type": "string", + "minLength": 1 + }, + "attribute": { + "type": "string", + "minLength": 1 + }, + "set": { + "type": [ + "boolean", + "integer", + "number", + "string" + ] + }, + "increment": { + "type": "number" + }, + "decrement": { + "type": "number" + } + }, + "oneOf": [ + { + "required": [ + "set" + ] + }, + { + "required": [ + "increment" + ] + }, + { + "required": [ + "decrement" + ] + } + ], + "additionalProperties": false + } + }, + "oneOf": [ + { + "required": [ + "variable", + "set" + ], + "not": { + "anyOf": [ + { + "required": [ + "increment" + ] + }, + { + "required": [ + "decrement" + ] + }, + { + "required": [ + "take_item" + ] + }, + { + "required": [ + "drop_item" + ] + } + ] + } + }, + { + "required": [ + "variable", + "increment" + ], + "not": { + "anyOf": [ + { + "required": [ + "set" + ] + }, + { + "required": [ + "decrement" + ] + }, + { + "required": [ + "take_item" + ] + }, + { + "required": [ + "drop_item" + ] + } + ] + } + }, + { + "required": [ + "variable", + "decrement" + ], + "not": { + "anyOf": [ + { + "required": [ + "set" + ] + }, + { + "required": [ + "increment" + ] + }, + { + "required": [ + "take_item" + ] + }, + { + "required": [ + "drop_item" + ] + } + ] + } + }, + { + "required": [ + "take_item" + ], + "not": { + "anyOf": [ + { + "required": [ + "variable" + ] + }, + { + "required": [ + "set" + ] + }, + { + "required": [ + "increment" + ] + }, + { + "required": [ + "decrement" + ] + }, + { + "required": [ + "drop_item" + ] + } + ] + } + }, + { + "required": [ + "drop_item" + ], + "not": { + "anyOf": [ + { + "required": [ + "variable" + ] + }, + { + "required": [ + "set" + ] + }, + { + "required": [ + "increment" + ] + }, + { + "required": [ + "decrement" + ] + }, + { + "required": [ + "take_item" + ] + } + ] + } + }, + { + "required": [ + "give_item" + ], + "not": { + "anyOf": [ + { + "required": [ + "variable" + ] + }, + { + "required": [ + "set" + ] + }, + { + "required": [ + "increment" + ] + }, + { + "required": [ + "decrement" + ] + }, + { + "required": [ + "take_item" + ] + }, + { + "required": [ + "drop_item" + ] + } + ] + } + }, + { + "required": [ + "character" + ], + "not": { + "anyOf": [ + { + "required": [ + "variable" + ] + }, + { + "required": [ + "set" + ] + }, + { + "required": [ + "increment" + ] + }, + { + "required": [ + "decrement" + ] + }, + { + "required": [ + "take_item" + ] + }, + { + "required": [ + "drop_item" + ] + }, + { + "required": [ + "give_item" + ] + } + ] + } + } + ], + "additionalProperties": false + } + }, + "action": { + "oneOf": [ + { + "type": "object", + "required": [ + "type", + "item" + ], + "properties": { + "type": { + "const": "take" + }, + "item": { + "type": "string", + "minLength": 1 + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "type", + "item" + ], + "properties": { + "type": { + "const": "drop" + }, + "item": { + "type": "string", + "minLength": 1 + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "type", + "item" + ], + "properties": { + "type": { + "const": "use" + }, + "item": { + "type": "string", + "minLength": 1 + }, + "effects": { + "type": "array", + "items": { + "type": "object", + "properties": { + "variable": { + "type": "string", + "minLength": 1 + }, + "set": { + "type": [ + "boolean", + "integer", + "number", + "string" + ] + }, + "increment": { + "type": "number" + }, + "decrement": { + "type": "number" + }, + "take_item": { + "type": "string", + "minLength": 1 + }, + "drop_item": { + "type": "string", + "minLength": 1 + } + }, + "oneOf": [ + { + "required": [ + "variable", + "set" + ], + "not": { + "anyOf": [ + { + "required": [ + "increment" + ] + }, + { + "required": [ + "decrement" + ] + }, + { + "required": [ + "take_item" + ] + }, + { + "required": [ + "drop_item" + ] + } + ] + } + }, + { + "required": [ + "variable", + "increment" + ], + "not": { + "anyOf": [ + { + "required": [ + "set" + ] + }, + { + "required": [ + "decrement" + ] + }, + { + "required": [ + "take_item" + ] + }, + { + "required": [ + "drop_item" + ] + } + ] + } + }, + { + "required": [ + "variable", + "decrement" + ], + "not": { + "anyOf": [ + { + "required": [ + "set" + ] + }, + { + "required": [ + "increment" + ] + }, + { + "required": [ + "take_item" + ] + }, + { + "required": [ + "drop_item" + ] + } + ] + } + }, + { + "required": [ + "take_item" + ], + "not": { + "anyOf": [ + { + "required": [ + "variable" + ] + }, + { + "required": [ + "set" + ] + }, + { + "required": [ + "increment" + ] + }, + { + "required": [ + "decrement" + ] + }, + { + "required": [ + "drop_item" + ] + } + ] + } + }, + { + "required": [ + "drop_item" + ], + "not": { + "anyOf": [ + { + "required": [ + "variable" + ] + }, + { + "required": [ + "set" + ] + }, + { + "required": [ + "increment" + ] + }, + { + "required": [ + "decrement" + ] + }, + { + "required": [ + "take_item" + ] + } + ] + } + } + ], + "additionalProperties": false + } + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "type", + "item", + "character" + ], + "properties": { + "type": { + "const": "give" + }, + "item": { + "type": "string", + "minLength": 1 + }, + "character": { + "type": "string", + "minLength": 1 + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "type", + "left", + "right", + "result" + ], + "properties": { + "type": { + "const": "combine" + }, + "left": { + "type": "string", + "minLength": 1 + }, + "right": { + "type": "string", + "minLength": 1 + }, + "result": { + "type": "string", + "minLength": 1 + } + }, + "additionalProperties": false + } + ] + }, + "commands": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + }, + "uniqueItems": true + }, + "id": { + "type": "string", + "pattern": "^[A-Za-z][A-Za-z0-9_-]*$" + } + }, + "oneOf": [ + { + "required": [ + "goto" + ], + "not": { + "required": [ + "ending" + ] + } + }, + { + "required": [ + "ending" + ], + "not": { + "required": [ + "goto" + ] + } + } + ], + "additionalProperties": false + } + } + }, + "additionalProperties": false + } + }, + "endings": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "text": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "inventory": { + "type": "array", + "uniqueItems": true, + "items": { + "type": "string", + "minLength": 1 + } + }, + "ownership": { + "type": "object", + "additionalProperties": { + "type": "string", + "minLength": 1 + } + }, + "character_state": { + "type": "object", + "additionalProperties": { + "type": "object", + "additionalProperties": { + "type": [ + "boolean", + "integer", + "number", + "string" + ] + } + } + } + }, + "additionalProperties": false +} From 5d52ded43b542d7ec706cae8c7a8272171c99a04 Mon Sep 17 00:00:00 2001 From: pos Date: Tue, 6 Oct 2026 21:07:27 +0200 Subject: [PATCH 4/5] M2.24: load schema from installed package resources --- src/storyforge/schema.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/storyforge/schema.py b/src/storyforge/schema.py index 9f13e98..b0ef188 100644 --- a/src/storyforge/schema.py +++ b/src/storyforge/schema.py @@ -1,15 +1,16 @@ import json -from pathlib import Path +from importlib.resources import files + from jsonschema import Draft202012Validator -def _schema_path() -> Path: - return Path(__file__).resolve().parents[2] / "schemas" / "storyforge-v0.schema.json" +def _load_schema() -> dict: + resource = files("storyforge").joinpath("schemas/storyforge-v0.schema.json") + return json.loads(resource.read_text(encoding="utf-8")) def validate_schema(story: dict) -> list[str]: - schema = json.loads(_schema_path().read_text(encoding="utf-8")) - validator = Draft202012Validator(schema) + validator = Draft202012Validator(_load_schema()) errors = sorted(validator.iter_errors(story), key=lambda error: list(error.path)) return [ "schema: " + (".".join(map(str, error.path)) + ": " if error.path else "") + error.message From 416d669231e8ca3ed335cba5af4032f8f9202e19 Mon Sep 17 00:00:00 2001 From: pos Date: Tue, 6 Oct 2026 21:07:29 +0200 Subject: [PATCH 5/5] M2.24: include JSON schema in wheel --- pyproject.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index c87ed03..07067f7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,3 +19,6 @@ storyforge = "storyforge.cli:main" [tool.pytest.ini_options] pythonpath = ["src"] testpaths = ["tests"] + +[tool.setuptools.package-data] +storyforge = ["schemas/*.json"]