From d96bb3a22ae9fde2b9892f0eabe70afa5af77197 Mon Sep 17 00:00:00 2001 From: Daniel Strader Date: Sat, 1 Aug 2026 19:46:57 -0700 Subject: [PATCH 1/2] ci: enforce installed charactercheck conformance --- .github/workflows/ci.yml | 34 +++++ docs/CONTRACTS.md | 15 ++- scripts/check_charactercheck_conformance.py | 135 ++++++++++++++++++++ 3 files changed, 178 insertions(+), 6 deletions(-) create mode 100644 scripts/check_charactercheck_conformance.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c510165..66c5f9e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -154,3 +154,37 @@ jobs: "$GITHUB_WORKSPACE/table-kit-src/scripts/check_srdcheck_conformance.py" \ --tablekit "$RUNNER_TEMP/srdcheck-conformance/bin/tablekit" \ --srdcheck "$RUNNER_TEMP/srdcheck-conformance/bin/srdcheck" + + charactercheck-conformance: + runs-on: ubuntu-latest + steps: + - name: Check out table-kit candidate + uses: actions/checkout@v5 + with: + path: table-kit-src + - name: Check out pinned charactercheck candidate + uses: actions/checkout@v5 + with: + repository: chaoz23/charactercheck + ref: e6ef29f5beb8f7bae972865e724988c932ebf708 + path: charactercheck-src + - uses: actions/setup-python@v6 + with: + python-version: "3.12" + - name: Build both candidate wheels + run: | + python -m pip install build + mkdir wheels + python -m build --wheel --outdir "$GITHUB_WORKSPACE/wheels" table-kit-src + python -m build --wheel --outdir "$GITHUB_WORKSPACE/wheels" charactercheck-src + - name: Cold-install and run charactercheck conformance + run: | + python -m venv "$RUNNER_TEMP/charactercheck-conformance" + "$RUNNER_TEMP/charactercheck-conformance/bin/python" -m pip install jsonschema + "$RUNNER_TEMP/charactercheck-conformance/bin/python" -m pip install \ + --no-index --find-links "$GITHUB_WORKSPACE/wheels" tablekit charactercheck + cd "$RUNNER_TEMP" + "$RUNNER_TEMP/charactercheck-conformance/bin/python" \ + "$GITHUB_WORKSPACE/table-kit-src/scripts/check_charactercheck_conformance.py" \ + --tablekit "$RUNNER_TEMP/charactercheck-conformance/bin/tablekit" \ + --charactercheck "$RUNNER_TEMP/charactercheck-conformance/bin/charactercheck" diff --git a/docs/CONTRACTS.md b/docs/CONTRACTS.md index 55ffee9..48e8a09 100644 --- a/docs/CONTRACTS.md +++ b/docs/CONTRACTS.md @@ -68,13 +68,16 @@ schema enums with `python scripts/generate_contract_types.py`; CI rejects stale artifacts. Runtime remains dependency-free. JSON Schema validation is a test and integration concern. -CI also builds and cold-installs this candidate with the exact dmcheck and -srdcheck commits named in the workflow, outside their source checkouts. The +CI also builds and cold-installs this candidate with the exact dmcheck, +srdcheck, and charactercheck commits named in the workflow, outside their +source checkouts. The dmcheck gate exercises a complete clean result, an exact-evidence finding, the canonical transport gap, deterministic legacy migration, and redacted content. The srdcheck gate exercises successful and failed-but-adjudicated saves, an exact-evidence illegal action, missing facts, unsupported content, and invalid -input. Both validate installed evaluation envelopes with table-kit's schema and -semantic checks; dmcheck also validates events. Pinned commits are integration -locks, not release dependencies; advance them deliberately when a consumer -changes. +input. The charactercheck gate exercises unsupported sheet mechanics, +player-authority advisories, unknown coverage, invalid fields/references, +privacy-safe output, and deterministic replay. All three validate installed +evaluation envelopes with table-kit's schema and semantic checks; dmcheck also +validates events. Pinned commits are integration locks, not release +dependencies; advance them deliberately when a consumer changes. diff --git a/scripts/check_charactercheck_conformance.py b/scripts/check_charactercheck_conformance.py new file mode 100644 index 0000000..b4fa7c8 --- /dev/null +++ b/scripts/check_charactercheck_conformance.py @@ -0,0 +1,135 @@ +#!/usr/bin/env python3 +"""Cold-installed table-kit -> charactercheck result conformance gate.""" + +import argparse +import copy +from importlib import resources +import json +from pathlib import Path +import subprocess +import sys +import tempfile + +from jsonschema import Draft202012Validator, FormatChecker + +import charactercheck +import tablekit +from tablekit import contracts + + +def _run(command, expected, cwd): + result = subprocess.run(command, cwd=cwd, capture_output=True, text=True) + if result.returncode != expected: + raise AssertionError( + "%s exited %d, expected %d\nstdout:\n%s\nstderr:\n%s" % ( + " ".join(command), result.returncode, expected, + result.stdout, result.stderr)) + return json.loads(result.stdout) + + +def _assert_cold_install(module): + module_path = Path(module.__file__).resolve() + prefix = Path(sys.prefix).resolve() + if prefix not in module_path.parents: + raise AssertionError("%s was imported outside the test environment: %s" % ( + module.__name__, module_path)) + + +def _derive(command, validator, root, ref, expected, status): + value = _run([ + command, "derive", str(ref), "--table-evaluation", + ], expected, root) + validator.validate(value) + contracts.require_evaluation_semantics(value) + assert value["status"] == status + assert value["exit_code"] == expected + assert value["authority_status"] == "self_attested" + assert value["subject"]["kind"] == "character" + assert value["subject"]["entity_refs"] == [] + return value + + +def _write(root, name, value): + path = root / name + path.write_text(json.dumps(value, sort_keys=True), encoding="utf-8") + # macOS exposes /var through /private/var; CharacterCheck deliberately + # rejects aliasing paths, so exercise the same canonical path CI sees. + return path.resolve() + + +def main(argv=None): + parser = argparse.ArgumentParser() + parser.add_argument("--tablekit", default="tablekit") + parser.add_argument("--charactercheck", default="charactercheck") + args = parser.parse_args(argv) + + _assert_cold_install(tablekit) + _assert_cold_install(charactercheck) + schema = _run([args.tablekit, "contract", "evaluation"], 0, None) + validator = Draft202012Validator(schema, format_checker=FormatChecker()) + sample = json.loads(resources.files("charactercheck").joinpath( + "sample-character.json").read_text(encoding="utf-8")) + character_name = sample["data"]["name"] + + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + sample_path = _write(root, "sample.json", sample) + unsupported = _derive( + args.charactercheck, validator, root, sample_path, + 2, "unsupported") + assert unsupported["coverage"]["complete"] is False + assert unsupported["context"]["policy_digest"].startswith("sha256:") + assert character_name not in json.dumps(unsupported, sort_keys=True) + repeated = _derive( + args.charactercheck, validator, root, sample_path, + 2, "unsupported") + assert repeated == unsupported + + reconciled = copy.deepcopy(sample) + reconciled["data"]["inventory"] = [] + advisory = _derive( + args.charactercheck, validator, root, + _write(root, "advisory.json", reconciled), + 1, "checked_with_advisories") + assert advisory["coverage"]["complete"] is True + assert advisory["advisories"] + assert all(item["severity"] == "advisory" + for item in advisory["advisories"]) + assert advisory["context"]["session_descriptor_digest"] is None + + unknown = copy.deepcopy(reconciled) + unknown["data"]["futureMechanicalThing"] = {"value": 1} + incomplete = _derive( + args.charactercheck, validator, root, + _write(root, "unknown.json", unknown), + 2, "incomplete") + assert incomplete["errors"] + + invalid_source = copy.deepcopy(reconciled) + invalid_source["data"]["modifiers"]["race"].append({ + "type": "bonus", "subType": "armor-class", "isGranted": True, + }) + invalid = _derive( + args.charactercheck, validator, root, + _write(root, "invalid.json", invalid_source), + 2, "invalid") + assert invalid["errors"] + + bad_ref = _derive( + args.charactercheck, validator, root, "notanumber", + 2, "invalid") + assert bad_ref["errors"][0]["code"] == "charactercheck.bad_ref" + assert "notanumber" not in json.dumps(bad_ref, sort_keys=True) + + print(json.dumps({ + "status": "passed", "authority_status": "self_attested", + "tablekit_version": tablekit.__version__, + "charactercheck_version": charactercheck.__version__, + "cases": ["unsupported", "checked_with_advisories", "incomplete", + "invalid_field", "invalid_reference", "deterministic"], + }, sort_keys=True)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) From 481e88ee7ddea3cb514d568d83537898b7105b5f Mon Sep 17 00:00:00 2001 From: Daniel Strader Date: Mon, 3 Aug 2026 08:25:28 -0700 Subject: [PATCH 2/2] ci: pin merged charactercheck projection --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 66c5f9e..a14062e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -166,7 +166,7 @@ jobs: uses: actions/checkout@v5 with: repository: chaoz23/charactercheck - ref: e6ef29f5beb8f7bae972865e724988c932ebf708 + ref: db92352e67c6e7839a53ae059a0bf93f21a1e161 path: charactercheck-src - uses: actions/setup-python@v6 with: