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: 16 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<p align="center"><img src=".github/assets/zentropy-banner.png" alt="crucible: A judgment engine: register a thesis, steelman each claim, measure against a substrate, refine the weakest axis." width="100%"></p>
<p align="center"><img src="docs/art/crucible-header.svg" alt="crucible: register a thesis, measure each claim, name the weakest axis." width="100%"></p>

**A judgment engine: register a thesis, steelman each claim, measure against a substrate, refine the weakest axis.**

Expand Down Expand Up @@ -27,6 +27,21 @@ crucible turns a thesis into a set of claims, each paired with the observation t
- **Batch manifests, Markdown reports, creative measurement gates, native MCP.** `crucible batch` runs a manifest of theses into one registry, `crucible report` renders deterministic Markdown, `crucible measurement-gate` verifies Telos creative measurement packets, and `crucible mcp` serves 13 tools over stdio.
- **Zero third-party runtime dependencies.** The core is pure standard library, Python 3.11+.

## How a thesis run works

`crucible run` takes one thesis and produces a packet a reviewer can check
without rerunning anything, and without trusting the run that made it.

<p align="center"><img src="docs/art/thesis-lifecycle.svg" alt="Eight stages from a registered thesis to a self-contained review packet: thesis, steelman, measure, cohesion, verdict, registry, recheck, packet. A round that is not cohesive reflects on the weakest axis and re-measures it. The verdict is a pure function of the measurement and ends as match, drift or unverifiable." width="100%"></p>

Two edges carry the design. The one that loops back is the refine step: grading
every claim yields a margin, the harmonic mean of those margins is the cohesion,
and a round that is not cohesive names its weakest axis and measures that axis
again rather than reporting a thesis that only half held. The one that does not
loop is `verdict_for`, which is a pure function of a deviation and a tolerance.
No model sits in it, so a fluent assertion has no route into a rechecked result,
and an axis that could not be measured reads UNVERIFIABLE instead of holding.

## Install

```bash
Expand Down
1 change: 1 addition & 0 deletions docs/art/crucible-header.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
36 changes: 36 additions & 0 deletions docs/art/crucible.art.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"$comment": "Source of truth for the artwork on this repository's front page. Edit a sentence here and re-run scripts/render_repo_art.py; tests/test_repo_art.py fails if the committed SVG stops matching.",
"header": {
"name": "crucible",
"role": "adversarial claim measurement",
"tagline": "Register a thesis, measure each claim, name the weakest axis.",
"words": ["thesis", "steelman", "measure", "verdict", "refine"]
},
"flows": [
{
"file": "thesis-lifecycle.svg",
"kicker": "one thesis, end to end",
"title": "How a claim earns its standing, and how it loses it",
"alt": "Eight stages from a registered thesis to a self-contained review packet. Each claim is steelmanned, measured against a substrate, and graded; a round that is not cohesive reflects on the weakest axis and re-measures it. The verdict is a pure function of the measurement and ends as match, drift or unverifiable.",
"footnote": "The verdict recomputes from the stored record, so a confident assertion cannot move it.",
"stages": [
{"title": "Thesis", "note": "A claim set under one seal, each with what would refute it."},
{"title": "Steelman", "note": "The strongest test is proposed before anything is measured."},
{"title": "Measure", "note": "Each claim meets a substrate: a deviation, and its tolerance."},
{"title": "Cohesion", "note": "The harmonic mean of margins, so one weak axis drags it down."},
{"title": "Verdict", "note": "A pure function of deviation and tolerance. No model here."},
{"title": "Registry", "note": "The assessment is stored under a sha256 receipt with the seal."},
{"title": "Recheck", "note": "Rows are re-read from disk and the verdicts recompute."},
{"title": "Packet", "note": "A bundle that carries its own zero-import verifier."}
],
"returns": [
{"from": 3, "to": 2, "label": "NOT COHESIVE: THE WEAKEST AXIS IS RE-MEASURED"}
],
"outcomes": [
{"label": "MATCH", "note": "the measurement sits inside tolerance", "tone": "verified"},
{"label": "DRIFT", "note": "the measurement sits outside it", "tone": "drift"},
{"label": "UNVERIFIABLE", "note": "the axis could not be measured at all", "tone": "none"}
]
}
]
}
19 changes: 19 additions & 0 deletions docs/art/thesis-lifecycle.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
73 changes: 73 additions & 0 deletions scripts/render_repo_art.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
"""Render a repository's front-page artwork from its spec.

python scripts/render_repo_art.py # write the SVGs
python scripts/render_repo_art.py --check # fail if any is stale

The check mode is the point. Committed artwork drifts from the words it
illustrates the moment someone edits one and not the other, and nobody
notices, because a picture in a README is never diffed. Here the picture is
a pure function of a spec that IS diffable, so a test can re-render and
compare bytes.
"""
from __future__ import annotations

import argparse
import json
import sys
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parent))

from repo_art import header_svg # noqa: E402
from repo_flow import flow_svg # noqa: E402

ART = Path(__file__).resolve().parents[1] / "docs" / "art"


def rendered(spec_path: Path) -> dict[Path, str]:
"""Every file one spec produces, as path to text."""
spec = json.loads(spec_path.read_text(encoding="utf-8"))
stem = spec_path.name.removesuffix(".art.json")
out = {spec_path.parent / f"{stem}-header.svg": header_svg(spec["header"])}
for flow in spec.get("flows", []):
out[spec_path.parent / flow["file"]] = flow_svg(flow)
return out


def specs() -> list[Path]:
return sorted(ART.glob("*.art.json"))


def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--check", action="store_true",
help="report stale artwork instead of rewriting it")
args = parser.parse_args(argv)

stale: list[str] = []
for spec_path in specs():
for path, text in rendered(spec_path).items():
body = text + "\n"
if args.check:
current = path.read_text(encoding="utf-8") if path.exists() else ""
if current != body:
stale.append(str(path.relative_to(ART.parents[1])))
continue
# newline="" so a Windows run writes the same bytes a Linux
# run does. The whole point of this file is that committed
# artwork and a fresh render are comparable.
path.write_text(body, encoding="utf-8", newline="")
print(f"wrote {path.relative_to(ART.parents[1])} ({len(body)} bytes)")

if stale:
print("stale artwork, re-run scripts/render_repo_art.py:", file=sys.stderr)
for name in stale:
print(f" {name}", file=sys.stderr)
return 1
if args.check:
print(f"artwork matches its spec ({len(specs())} spec files)")
return 0


if __name__ == "__main__":
raise SystemExit(main())
Loading