Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,23 @@ jobs:
/tmp/charactercheck-cold/bin/charactercheck --schema > /tmp/charactercheck-schema.json
grep -q '"snapshot"' /tmp/charactercheck-schema.json
grep -q '"internal_error"' /tmp/charactercheck-schema.json
grep -q '"table_evaluation"' /tmp/charactercheck-schema.json

- name: Installed shared result is value-free and fail-closed
working-directory: /tmp
env: *offline-network
run: |
sample=/tmp/charactercheck-cold/lib/python3.12/site-packages/charactercheck/sample-character.json
set +e
/tmp/charactercheck-cold/bin/charactercheck derive "$sample" \
--table-evaluation > /tmp/charactercheck-evaluation.json
code=$?
set -e
test $code -eq 2 || { echo "expected unsupported exit 2, got $code"; exit 1; }
grep -q '"schema_version": "table.evaluation/1.0"' /tmp/charactercheck-evaluation.json
grep -q '"status": "unsupported"' /tmp/charactercheck-evaluation.json
grep -q '"authority_status": "self_attested"' /tmp/charactercheck-evaluation.json
grep -qv 'Torvald Brightmantle' /tmp/charactercheck-evaluation.json

- name: Installed MCP initializes and lists tools without network
working-directory: /tmp
Expand Down
6 changes: 6 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ Run the offline smoke test first:
```console
python3 -m charactercheck selftest
python3 -m charactercheck derive examples/sample-character.json --brief
python3 -m charactercheck derive examples/sample-character.json --table-evaluation
```

The example is project-authored synthetic data. A smoke-test pass proves only
Expand Down Expand Up @@ -84,6 +85,11 @@ The local `--for-dm` projection marks every duplicate of current HP/slots,
stance, resources, and inventory as `player-authority`; it is a redaction mode,
not authentication or proof of table role.

`derive` and `report` accept `--table-evaluation` for a deterministic,
value-free `table.evaluation/1.0` projection. Treat mutable player-authority
fields as out-of-scope advisories, not evaluator-owned facts. Every envelope is
`self_attested` and remains pre-session character context only.

## Privacy and capability boundary

Default mechanical output omits account identifiers, linked images, notes,
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## 0.7.0 - 2026-08-02

- Add a deterministic, value-free `table.evaluation/1.0` projection for
`derive`/`report`. It preserves canonical field trust, treats mutable
player-authority state as an out-of-scope advisory, maps gaps and input
failures to typed refusals, and remains explicitly self-attested.
- Pair every Coverage Inventory answer with its numbered 2024 character-sheet
question, and include deduplicated sheet-specific lint questions in `quiz`
without supplying answer keys for non-trusted mechanics.
Expand Down
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ collection, inventory, modifier, and container-traversal limits.
|---|---|
| `derive <ref>` | Selected derived fields, provenance, findings, and trust routing |
| `derive <ref> --brief` | Deterministic chat-sized summary; still subject to findings |
| `derive <ref> --table-evaluation` | Value-free, deterministic, self-attested `table.evaluation/1.0` assessment envelope |
| `stance <ref>` | Envelope containing stance, canonical assessment, trust, fields, and observation metadata |
| `report <ref>` | Canonical field/trust assessments plus findings and identified feature names |
| `qa <ref> [--full]` | 100-question **Coverage Inventory** pairing each answer with a closed field state; not a validity score |
Expand All @@ -142,6 +143,14 @@ collection, inventory, modifier, and container-traversal limits.
`--pipe` reads refs from standard input. `charactercheck --schema` emits the
machine-readable CLI contract.

`--table-evaluation` is also accepted by `report`. It projects canonical field
states and source/policy digests, never character values. Trusted fields count
as evaluated; unsupported, unknown, invalid, and non-authority confirmation
states fail closed. Mutable player-authority fields are outside the evaluator's
scope and become advisories requiring player/session-host reconciliation. The
envelope is always `self_attested`; it is not proof of source identity,
freshness, table role, encounter state, or action legality.

### Question catalog

The Coverage Inventory pairs each of its 100 answers with the corresponding
Expand Down Expand Up @@ -317,6 +326,12 @@ Exit status is command-specific:
| `selftest` | smoke test passed | smoke test failed | — | — |
| `doctor` | all checks passed | — | — | at least one diagnostic check failed |

With explicit `--table-evaluation`, the shared contract's exit codes apply:
`0` checked clean, `1` complete with advisories/findings, and `2` incomplete,
unsupported, invalid, or internal error. Native command behavior is unchanged
when the flag is absent. `--brief` and `--table-evaluation` are mutually
exclusive.

The CLI rejects every unsupported command/flag combination with structured
`bad_flag` exit 2; notably `--for-dm` is valid only for `seatpack` and `intake`.
Missing arguments,
Expand Down
4 changes: 3 additions & 1 deletion charactercheck/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,6 @@
from .engine import derive, fetch, stance

__version__ = "0.7.0"
__all__ = ["derive", "fetch", "stance"]
from .table_evaluation import project_table_evaluation

__all__ = ["derive", "fetch", "stance", "project_table_evaluation"]
55 changes: 49 additions & 6 deletions charactercheck/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import sys

from . import derive, engine, errors, qa
from .table_evaluation import input_digest, project_table_error, project_table_evaluation


def _json(value):
Expand Down Expand Up @@ -55,6 +56,13 @@ def _json(value):
"for credentials."),
"kinds": list(errors.PUBLIC_ERROR_KINDS),
},
"table_evaluation": {
"flag": "--table-evaluation on derive/report",
"schema_version": "table.evaluation/1.0",
"authority_status": "self_attested",
"privacy": "field assessments and digests only; no character values",
"exit_codes": "0 checked_clean; 1 complete with advisories/findings; 2 refused",
},
}


Expand Down Expand Up @@ -85,12 +93,25 @@ def main(argv=None):
ap.add_argument("--brief", action="store_true", help="derive/report: chat-sized summary")
ap.add_argument("--json", dest="json_out", action="store_true", help="doctor: machine-readable output")
ap.add_argument("--schema", action="store_true", help="print the I/O contract and exit")
ap.add_argument("--table-evaluation", action="store_true",
help="derive/report: emit a self-attested table.evaluation/1.0 envelope")
a = ap.parse_args(argv)

if a.schema:
print(_json(SCHEMA))
return 0

if a.table_evaluation and a.brief:
print(_json({
"ok": False,
"error": "bad_flag",
"message": "--brief and --table-evaluation are mutually exclusive.",
"action": "Drop --brief when requesting the shared JSON envelope.",
"retryable": False,
"exit_code": 2,
}), file=sys.stderr)
return 2

flag_contract = (
(a.brief, "--brief", {"derive", "report"},
"Use --brief with `derive` or `report`, or drop it."),
Expand All @@ -102,6 +123,8 @@ def main(argv=None):
(a.full, "--full", {"qa"}, "Use it only with qa."),
(bool(a.baseline), "--baseline", {"diff"}, "Use it only with diff."),
(a.json_out, "--json", {"doctor"}, "Use it only with doctor."),
(a.table_evaluation, "--table-evaluation", {"derive", "report"},
"Use it only with derive or report."),
(a.pipe, "--pipe",
{"derive", "stance", "qa", "report", "snapshot", "diff", "quiz",
"seatpack", "intake"},
Expand Down Expand Up @@ -137,8 +160,13 @@ def main(argv=None):
try:
if a.command == "derive":
r = derive(ref)
print(engine.render_brief(r) if a.brief else _json(r))
code = max(code, _exit_code(r))
if a.table_evaluation:
out = project_table_evaluation(r)
print(_json(out))
code = max(code, out["exit_code"])
else:
print(engine.render_brief(r) if a.brief else _json(r))
code = max(code, _exit_code(r))
elif a.command == "intake":
print(_json(engine.intake(ref, for_dm=a.for_dm,
include_persona=a.include_persona)))
Expand Down Expand Up @@ -167,6 +195,11 @@ def main(argv=None):
print(_json(engine.quiz(ref)))
elif a.command == "report":
r = derive(ref)
if a.table_evaluation:
out = project_table_evaluation(r)
print(_json(out))
code = max(code, out["exit_code"])
continue
if a.brief:
print(engine.render_report_brief(r))
code = max(code, _exit_code(r))
Expand All @@ -177,15 +210,25 @@ def main(argv=None):
except errors.CharacterCheckError as e:
# Structured, actionable, and on stdout so a piping agent
# sees it. Never a traceback: see charactercheck/errors.py.
print(_json(e.as_dict()))
code = max(code, e.exit_code)
if a.table_evaluation:
out = project_table_error(e.as_dict(), input_digest(ref))
print(_json(out))
code = max(code, out["exit_code"])
else:
print(_json(e.as_dict()))
code = max(code, e.exit_code)
except Exception as exc: # noqa: BLE001 - redact at the process boundary
correlation_id = secrets.token_hex(8)
print(f"charactercheck internal_error correlation_id={correlation_id} "
f"exception={type(exc).__name__}", file=sys.stderr)
e = errors.internal_error(correlation_id)
print(_json(e.as_dict()))
code = max(code, e.exit_code)
if a.table_evaluation:
out = project_table_error(e.as_dict(), input_digest(ref))
print(_json(out))
code = max(code, out["exit_code"])
else:
print(_json(e.as_dict()))
code = max(code, e.exit_code)
return code


Expand Down
Loading