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
20 changes: 20 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,26 @@ non-Anthropic model families remain out of scope.
`[bench] ledger: implement chain verification`
`[bench] constitution: add C-009 logging constraint`

## Code Navigation: graphify

A knowledge graph of this repo lives in `graphify-out/` (`graph.json`,
`GRAPH_REPORT.md`, `graph.html`), built by the graphify skill. For structural
questions (what calls X, what depends on Y, trace a data flow), query the
graph before reaching for grep:

```
graphify query "<question>" # BFS context; --dfs to trace, --budget N to raise the output cap
graphify path "A" "B" # shortest path between two symbols, edges tagged with provenance
graphify explain "<node>" # plain-language explanation of one node
```

The graph replaces exploratory grepping, not verification: read the cited
files before editing, and use grep for exact strings or anything newer than
the last build. After a stretch of commits, refresh with `/graphify . --update`.
Do not use `graphify claude install` here; it writes this file directly,
bypassing governance. Edit this section through governed tools like any other
change.

## Constitution Reference

The constitution lives in bench.json. Current constraints:
Expand Down
77 changes: 23 additions & 54 deletions ledger/chain.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,12 @@
# must agree on which project a run belongs to; if they could disagree, a
# change could be judged against one project's constitution while being
# recorded in another project's ledger. utils.project is the single definition
# all three resolve through. This alias preserves the existing internal name.
# all three resolve through.
_BENCH_ROOT: Path = BENCH_ROOT
_DEFAULT_LEDGER_PATH: str = str(_BENCH_ROOT / "ledger" / "bench-ledger.json")
_PROJECT_LEDGER_DIRNAME: str = ".bench"
# Frozen pin on the legacy segment; never written here. verify.py imports
# this name to check the pin.
META_FILENAME: str = "ledger-meta.json"
_GENESIS_MARKER: str = "GENESIS"
ANCHOR_VERDICT: str = "ANCHOR"
Expand All @@ -62,12 +64,9 @@
ENTRIES_DIRNAME: str = "entries"
"""Directory holding one JSON file per entry, named ``<entry_hash>.json``.

A single JSON array had to be rewritten in full on every append, so two
branches that both recorded verdicts produced divergent chains that could not
be merged: interleaving breaks the hash links and rebasing rewrites hashes,
which C-008 forbids. One file per entry means different branches write
different filenames, so a merge is conflict-free, and because the filename is
the content hash a merge cannot yield two files claiming the same identity.
One file per entry makes branch merges conflict-free (the module docstring
covers why the single array could not be), and because the filename is the
content hash a merge cannot yield two files claiming the same identity.

``verify.py`` re-declares this name locally rather than importing it, keeping
the auditor independent of the write path.
Expand All @@ -88,16 +87,12 @@ class LedgerReadError(RuntimeError):
def _project_root() -> Path:
"""The root of the project currently being governed.

Ledger routing and out-of-project classification both anchor here, so a
change can never be routed to one project's ledger while being judged
against a different project's boundary. A working directory anywhere
inside the Bench repo counts as Bench governing itself, which is why
editing ``utils/api.py`` while sitting in ``tests/`` is still in-project.

Delegates to ``utils.project.project_root``, which the constitution
resolver also uses, so the two can never drift apart. The behavior is
unchanged, including the loud fallback to Bench's own root when the
working directory cannot be resolved (C-001).
A working directory anywhere inside the Bench repo counts as Bench
governing itself, which is why editing ``utils/api.py`` while sitting in
``tests/`` is still in-project. Delegates to
``utils.project.project_root`` (see the module-level note on why ledger
routing, external-change classification, and constitution resolution
share one root definition).
"""
return project_root()

Expand Down Expand Up @@ -156,9 +151,7 @@ def resolve_entries_dir(path: str | None = None) -> str:
def _is_external_change(file_ref: str) -> bool:
"""True when the governed file lies outside the project being governed.

Anchored on ``_project_root()``, the same root ledger routing uses, so a
change cannot be written to one project's ledger while being classified
against another's boundary.
Anchored on ``_project_root()``, the same root ledger routing uses.

Relative paths are normalized against the project by the hook, so only
absolute paths can escape. A path that cannot be compared to the project
Expand Down Expand Up @@ -283,9 +276,8 @@ def _load_legacy_strict(file_path: Path) -> list[dict]:
"""Read the frozen legacy array, raising rather than degrading.

An absent file is normal and returns ``[]``. Every other failure raises
``LedgerReadError``. Treating a corrupt array as an empty chain is exactly
what let the next append restart from GENESIS and overwrite the damaged
file, destroying the evidence (C-008).
``LedgerReadError`` (see that class for why degrading to an empty chain
is forbidden).
"""
if not file_path.exists():
return []
Expand Down Expand Up @@ -456,8 +448,7 @@ def load_ledger(path: str | None = None) -> list[dict]:

Returns the union of the frozen legacy array and the per-entry files
beside it, deduplicated by ``entry_hash`` (the array wins a collision), in
deterministic order. The signature and return type are unchanged, so the
CLI, the viewer, and stats consume this exactly as before.
deterministic order.

Read failures are logged to stderr and the readable remainder is returned,
without touching anything on disk — a damaged ledger is preserved for
Expand Down Expand Up @@ -491,12 +482,11 @@ def append_entry(
pipeline_result: dict,
path: str | None = None,
) -> dict:
"""Append a governance verdict to the ledger and update ledger-meta.json.
"""Append a governance verdict to the ledger.

``path`` defaults to ``resolve_ledger_path()``, which routes the verdict
to the ledger of the project being governed rather than always to
Bench's own. ``ledger-meta.json`` is written alongside whichever ledger
is selected, so each chain carries its own anchor.
Bench's own.

Expects ``pipeline_result`` to include the standard runner keys
(``verdict``, ``pipeline_error``, ``constitution_hash``, ``challenger``,
Expand All @@ -516,9 +506,8 @@ def append_entry(
directory: Path = file_path.parent
directory.mkdir(parents=True, exist_ok=True)

# Strict on the write path. Appending onto a ledger that cannot be fully
# read risks a second genesis or a lost parent, and the old behaviour of
# treating an unreadable array as empty is what overwrote corrupt files.
# Strict on the write path: appending onto a ledger that cannot be fully
# read risks a second genesis or a lost parent.
entries_dir: Path = Path(resolve_entries_dir(resolved))
legacy: list[dict] = _load_legacy_strict(file_path)
existing_new: list[dict] = _load_entry_files(entries_dir, strict=True)
Expand Down Expand Up @@ -578,19 +567,9 @@ def append_entry(
}
entry["entry_hash"] = compute_entry_hash(entry)

# Two-segment storage, described in CLAUDE.md under Architecture.
#
# What this code does: writes the entry to its own file named by its hash,
# refuses to write if that file already exists, and does not touch
# bench-ledger.json or ledger-meta.json. Those two are read on every append
# (above, via _load_legacy_strict) and are never rewritten, so the frozen
# segment and its pinned tip stay byte-identical.
#
# What the auditor does: verify_chain enumerates this directory itself,
# recomputes every hash, requires each filename to equal the hash it
# contains, and fails closed on MISSING_PARENT, ORPHAN_ENTRY,
# DUPLICATE_ENTRY and MULTIPLE_GENESIS, walking parent links across the
# frozen tip into these files. The legacy array keeps its positional walk.
# Two-segment storage (see module docstring): the entry gets its own file
# named by its hash; bench-ledger.json and ledger-meta.json are read above
# but never rewritten, so the frozen segment stays byte-identical.
entries_dir.mkdir(parents=True, exist_ok=True)
entry_file: Path = entries_dir / f"{entry['entry_hash']}.json"
if entry_file.exists():
Expand Down Expand Up @@ -631,13 +610,3 @@ def _atomic_write_json(target: Path, data: Any) -> None:
file=sys.stderr,
)
raise


# _update_meta was removed here. ledger-meta.json is now frozen alongside the
# legacy array (see CLAUDE.md, Architecture): it permanently pins that
# segment's tip hash and entry count, and rewriting it on every append would
# have reintroduced exactly the single-file conflict this change removes.
# verify_chain still checks the pin against the legacy segment, so the
# auditability signal is retained rather than dropped; it is now an assertion
# about a fixed segment instead of a rolling one. META_FILENAME is kept because
# verify.py imports it.
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
{
"entry_id": "3134dcf7-7b34-4e5f-9656-990a0d326e84",
"timestamp": "2026-08-07T07:22:36.994133+00:00",
"previous_hash": [
"5abf012ee422ac43afd3fe3b094f26d8f05e0f27703af245922cb94ce2e8aa77"
],
"constitution_hash": "06ce31ba8f0cf1ebbab17172a8b41be1162dabcf3d473b83317e21e7d2893fd3",
"constitution_sources": [
{
"layer": "core",
"path": "C:\\Users\\mstar\\Bench\\bench.json",
"sha256": "06ce31ba8f0cf1ebbab17172a8b41be1162dabcf3d473b83317e21e7d2893fd3"
}
],
"verdict": "PASS",
"pipeline_error": false,
"change": {
"file": "ledger\\chain.py",
"tool": "Edit",
"diff_summary": {
"file_path": "ledger\\chain.py",
"change_type": "modify",
"old_string": " \"\"\"Append a governance verdict to the ledger and update ledger-meta.json.\n\n ``path`` defaults to ``resolve_ledger_path()``, which routes the verdict\n to the ledger of the project being governed rather than always to\n Bench's own. ``ledger-meta.json`` is written alongside whichever ledger\n is selected, so each chain carries its own anchor.",
"new_string": " \"\"\"Append a governance verdict to the ledger.\n\n ``path`` defaults to ``resolve_ledger_path()``, which routes the verdict\n to the ledger of the project being governed rather than always to\n Bench's own."
}
},
"challenger": {
"status": "FINDINGS",
"findings": [
{
"constraint_id": "C-007",
"severity": "OBSERVATION",
"location": "ledger/chain.py: append_entry docstring",
"evidence": "Removed: \"and update ledger-meta.json.\" and \"``ledger-meta.json`` is written alongside whichever ledger is selected, so each chain carries its own anchor.\"",
"reasoning": "This is a docstring-only edit to a ledger/ file, so it falls within the governance pipeline scope C-007 covers. The diff provided contains no corresponding change to the function body, so it cannot be independently confirmed whether append_entry's actual behavior around ledger-meta.json changed or whether this is purely a documentation correction. The project's own CLAUDE.md states ledger-meta.json is now frozen ('permanent pin on that segment's tip and entry count') and that new entries are written to the entries/ directory instead, which is consistent with this docstring update simply removing a stale claim rather than describing a weakening of enforcement. Flagging as an observation only because the code diff itself is not visible to confirm the docstring still accurately reflects behavior."
},
{
"constraint_id": "C-002",
"severity": "OBSERVATION",
"location": "ledger/chain.py",
"evidence": "Single docstring edit, no other files touched",
"reasoning": "The change is narrow and coherent (one docstring in one function), well within scope boundaries. No concern here, noted only for completeness."
}
],
"_tokens": {
"input": 9037,
"output": 1841
}
},
"defender": {
"status": "REBUTTAL",
"rebuttals": [
{
"finding_index": 0,
"position": "REBUT",
"argument": "The Challenger correctly flags this as needing scrutiny but its own hedge resolves in the change's favor once the file_context is applied. CLAUDE.md states explicitly and unambiguously that 'ledger-meta.json is frozen with it as a permanent pin on that segment's tip and entry count' and 'All new entries are written one per file to <ledger_dir>/entries/'. That means the old docstring text the diff removes -- 'update ledger-meta.json... is written alongside whichever ledger is selected, so each chain carries its own anchor' -- was describing behavior that no longer exists in this codebase. A docstring asserting that append_entry writes ledger-meta.json on every call is not a neutral-but-outdated claim; it is affirmatively wrong given the frozen-segment architecture, and a governance-pipeline docstring that misstates what the function does is itself a small governance liability (it would mislead a future maintainer into assuming meta-file writes still happen). Removing the false claim brings the docstring in line with actual, already-existing behavior. Nothing about C-007 requires preserving stale documentation; it requires that changes not weaken enforcement, bypass verification, or disable logging. This diff touches zero executable code -- no function body, no control flow, no logging call -- so there is nothing for it to weaken.",
"evidence": "CLAUDE.md: 'ledger-meta.json is frozen with it as a permanent pin on that segment's tip and entry count. All new entries are written one per file to <ledger_dir>/entries/<entry_hash>.json'. The diff's old_string/new_string pair shows only docstring text removed; the function signature and (by omission from the diff) the function body are untouched."
},
{
"constraint_id": "C-002",
"finding_index": 1,
"position": "CONCEDE",
"argument": "The Challenger's own characterization is correct: a single docstring edit in one function, no other files touched, is unambiguously within scope. I agree with this observation and add nothing to it beyond confirming there is no bundling, no unrelated edit, and no scope-boundary concern of any kind here.",
"evidence": "The diff contains exactly one old_string/new_string pair, both confined to the append_entry docstring in ledger/chain.py."
}
],
"summary": "This is a narrow, accurate docstring correction that removes a claim about ledger-meta.json writes which the project's own CLAUDE.md confirms is no longer true (the file is frozen), touches no executable code, and stays fully within scope.",
"_tokens": {
"input": 9731,
"output": 1340
}
},
"oracle": {
"verdict": "PASS",
"reasoning": "The change is a docstring-only edit to ledger/chain.py's append_entry that removes two stale claims: that the function updates ledger-meta.json on every call and that ledger-meta.json 'is written alongside whichever ledger is selected, so each chain carries its own anchor.' The Challenger raised two OBSERVATIONS (not violations). On C-007, the Challenger explicitly hedged, noting the diff shows no function-body change and that CLAUDE.md's frozen-segment architecture is 'consistent with this docstring update simply removing a stale claim rather than describing a weakening of enforcement.' The Defender's rebuttal resolves this: the file_context (CLAUDE.md) confirms 'ledger-meta.json is frozen with it as a permanent pin on that segment's tip and entry count' and 'All new entries are written one per file to <ledger_dir>/entries/.' The removed docstring text described behavior that no longer exists, so removing it aligns documentation with actual, already-existing behavior. C-007 requires that pipeline changes not weaken enforcement, bypass verification, or disable logging; a docstring edit touches zero executable code, no control flow, and no logging, so there is nothing to weaken. Because ledger-meta.json is documented as frozen rather than actively written, correcting the docstring reduces rather than increases governance liability. On C-002, both parties agree the change is a single coherent docstring edit in one function with no bundling; the Defender conceded and the Challenger noted no scope concern. I independently reviewed the change against all constraints: C-001 (no catch blocks touched), C-003 (no imports), C-004 (no type annotations altered), C-005 (no new logic), C-006 (no secrets), and C-008 (no ledger entry modified; documentation about a frozen pin is not a ledger mutation) are all satisfied or not applicable.",
"constraint_citations": [
{
"constraint_id": "C-007",
"disposition": "SATISFIED",
"note": "Docstring-only edit to a ledger/ pipeline file. Touches no executable code, control flow, or logging. Removes a stale claim about ledger-meta.json writes that CLAUDE.md confirms no longer reflects behavior (segment is frozen). Enforcement is neither weakened nor bypassed."
},
{
"constraint_id": "C-002",
"disposition": "SATISFIED",
"note": "Single coherent docstring edit in one function, no other files touched. Challenger and Defender agree there is no bundling or scope-boundary concern."
},
{
"constraint_id": "C-008",
"disposition": "NOT_APPLICABLE",
"note": "No ledger entry is modified, deleted, or reordered. The edit concerns a function docstring, not any stored entry or the frozen ledger-meta.json file itself."
},
{
"constraint_id": "C-001",
"disposition": "NOT_APPLICABLE",
"note": "No error-handling code is present in the diff."
},
{
"constraint_id": "C-003",
"disposition": "NOT_APPLICABLE",
"note": "No imports or dependencies added."
},
{
"constraint_id": "C-004",
"disposition": "NOT_APPLICABLE",
"note": "No type annotations added, removed, or weakened."
},
{
"constraint_id": "C-006",
"disposition": "NOT_APPLICABLE",
"note": "No secrets or credentials involved."
}
],
"advisories": [
"The diff does not include the function body, so the Oracle relies on file_context (CLAUDE.md) to confirm that ledger-meta.json is in fact frozen and no longer written by append_entry. This is corroborated by the documented frozen-segment architecture, but authors should ensure the docstring continues to accurately describe append_entry's actual behavior."
],
"remediation": null,
"confidence": "HIGH",
"_tokens": {
"input": 10442,
"output": 1502
}
},
"entry_hash": "0188ca33ac4e9c82991091785db09456e70a749f39f1168a9c5aa9d10df9b58f"
}
Loading