Add opt-in admission manifest: per-decision JSONL audit ledger (#381)#382
Add opt-in admission manifest: per-decision JSONL audit ledger (#381)#382OtotaO wants to merge 2 commits into
Conversation
…a-ai#381) Implements the run_dir admission_manifest.jsonl proposed in gepa-ai#381: one JSON line per candidate-admission decision (accepted or rejected), keyed by candidate hash and parent hashes, so a run can be audited and reproduced from local files alone. Every field is derivable from facts GEPA already holds, so the manifest is replayable for free. Design (per the discussion on gepa-ai#381): - Opt-in via EngineConfig(write_admission_manifest=True) / optimize(...), which requires run_dir. Off by default and zero-cost when off, so existing runs never start emitting a new artifact unexpectedly. - Append-once, no in-place backfill: the decision snapshot is built at the subsample-acceptance seam; for an accepted candidate the validation fields are finalized after _run_full_eval_and_add and the complete record is appended once; rejects append immediately with validation null and candidate_idx null. One shared finalization helper covers the reflective and merge accept paths. - The recorded decision is the subsample acceptance decision; the validation block is post-decision enrichment, not a second admission gate. - components_changed is derived from a candidate-vs-parent diff (replayable), not from proposer metadata (which can be absent for custom adapters/proposers). - A dedicated single-record JSONL append helper (serialize one line, append, flush), not _atomic_write_json which replaces a whole document. read_manifest tolerates a truncated final line from an interrupted append. - cache hit/miss counters left null in v1; signatures / record chaining / tamper-evidence intentionally left out as a possible separate follow-up. verify_record() recomputes candidate and parent hashes and re-derives the decision from the recorded subsample scores. Tests cover accept/reject/merge records, the truncated-final-line case, replay verification, and an end-to-end optimize_anything run whose accepted records replay against candidates.json. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Changed Files
|
|
Thanks, this matches the v1 shape well. I checked the diff and the focused tests. One small replayability issue I found: with Since the feature is meant to be replayable from local artifacts, I think the manifest should record the acceptance criterion/operator, and the decision reason should use the actual operator. Minor doc nit: Otherwise this looks aligned with the v1 spec from #381. |
…a-ai#382 review) Addresses @sunghunkwag's review of gepa-ai#382: - Every admission record now stores acceptance_criterion (the public "strict_improvement" / "improvement_or_equal" / "merge_gate" name) and the acceptance_operator (">" / ">=", or null for a custom criterion). The record is therefore replayable from local files alone: verify_record() re-derives the decision from the recorded operator and no longer defaults to strict improvement or requires the caller to re-supply the acceptance strategy. - Fix the reflective-accept decision_reason: it hardcoded ">" even under improvement_or_equal (an equal-score accept wrote "0.0 > 0.0"). It now uses the criterion's actual operator, and a custom criterion gets a neutral phrasing. - Document write_admission_manifest in the api.optimize() parameter list. New tests cover the equal-score accept under improvement_or_equal replaying True with no caller override, the record being authoritative over a stale override, a custom criterion leaving the score decision out of scope (hashes still checked), and an end-to-end improvement_or_equal run recording ">=". Full suite: no new failures vs the branch baseline; +8 new passing tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Good catch, and you are right that this is a real replayability gap, not just a cosmetic reason string. Both halves trace to the same root: the record did not carry the rule that produced the decision, so a verifier could not re-derive an equal-score acceptance without being told the strategy out of band. Fixed in the latest push. What changed:
Tests: added the exact case you flagged (equal-score accept under One note: I kept Open questions from the PR description still stand (the opt-in surface, and |
|
Confirmed on the latest head. The equal-score I think keeping both the public criterion name and the literal operator is the right shape: the name preserves semantic context, while the operator keeps replay independent of GEPA class names. Keeping From the #381 perspective, this resolves my replayability concern. I only see the latest GitHub Actions runs as |
|
Thanks, glad it lines up on your end too. Agreed on all three points: keeping both the public criterion name and the literal operator gives the semantic context plus replay that does not depend on GEPA's class names, and holding schema_version at 1 makes sense while this is pre-merge. On CI: the runs are now green across every job (Build Package 3.10 through 3.14, Run Tests 3.10 through 3.14, Check Ruff Fix, and Type Check), so that gate looks cleared from my side too. Nothing pending on my end. Happy to rebase or adjust if anything comes up before you merge. Thanks again for the careful review. |
|
Hi @LakshyAAAgrawal, flagging this one for a merge look whenever you have a moment. Where it stands:
Two open questions from the PR description are still yours to call: the opt-in surface, and Thanks for the library, it has been a pleasure to build on. |
Implements the
admission_manifest.jsonlproposed in #381: one JSON line per candidate-admission decision (accepted or rejected), keyed by candidate and parent hashes, so a run can be audited and reproduced from local files alone. Every field is derivable from facts GEPA already holds, so the manifest is replayable for free. Opt-in, off by default, zero-cost when off.Follows the design we converged on in the issue thread.
What it does
run_dir.write_admission_manifest: bool = FalseonEngineConfig, mirrored as a flat kwarg inoptimize(); raises ifrun_diris unset, so existing runs never start emitting a new artifact unexpectedly._accept_reflective_proposal); for an accepted candidate the validation fields are finalized after_run_full_eval_and_addand the complete record is appended once; rejects append immediately withvalidation: nullandcandidate_idx: null. One shared finalization helper covers the reflective and merge accept paths.decisionis the subsample acceptance decision (decision_basis: "subsample_acceptance"); thevalidationblock is post-decision enrichment, not a second admission gate.components_changedfrom a candidate-versus-parent diff, not metadata parsing (metadata can be absent for custom adapters/proposers, while a diff stays replayable)._atomic_write_json.read_manifest()tolerates a truncated final line from an interrupted append.verify_record()recomputes the candidate and parent hashes and re-derives the decision from the recorded subsample scores.Example record (real output from the test run)
An accepted reflective decision:
{ "schema_version": 1, "iteration": 1, "proposal_kind": "reflective_mutation", "decision": "accepted", "decision_reason": "subsample score 1.0 > 0.0 (accepted by StrictImprovementAcceptance)", "decision_basis": "subsample_acceptance", "candidate_hash": "f554b8d7...098adf", "candidate_idx": 1, "parent_candidate_ids": [0], "parent_candidate_hashes": ["0abb9c2d...8bb4a1"], "components_changed": ["current_candidate"], "subsample_ids": [0], "subsample_score_before": 0.0, "subsample_score_after": 1.0, "metric_calls_before_decision": 3, "validation": { "val_average_score": 1.0, "evaluated_val_ids": [0], "is_best_program": true, "metric_calls_after_validation": 4, "metric_calls_charged_validation": 1 }, "cache_hits": null, "cache_misses": null }A rejected decision is the same shape with
decision: "rejected",candidate_idx: null, andvalidation: null.Files
src/gepa/logging/admission_manifest.py(new): the schema builder, the append and read helpers, andverify_record.src/gepa/core/engine.py: emit at the reflective and merge accept/reject seams via one shared finalization helper; the opt-in flag on__init__.src/gepa/optimize_anything.py,src/gepa/api.py: theEngineConfigfield and theoptimize()flat kwarg.tests/test_admission_manifest.py(new, 16 tests).Tests and checks
optimize_anythingrun whose accepted records replay againstcandidates.json.pytest tests/passes locally under thedevextra;pyrightis clean; no newrufffindings; no new dependencies (the module uses only the standard library and existing GEPA internals).Open questions for you
EngineConfigfield mirrored inoptimize(), per your preference over inferring fromrun_dir.gepa/logging/; happy to move it (for example tocore/) if you would rather it sit with the otherrun_dirartifacts.Opening as a draft for your review. @sunghunkwag
Addresses #381.