🇯🇵 日本語版 README
End-to-end round-trip harness that verifies the Method section of a paper survives the full pipeline intact:
paper text T ──▶ contingency-procedure2dsl ──▶ DSL-AST ① (= ast1)
│
▼
contingency-dsl2procedure
│
▼
paper text P
│
▼
contingency-procedure2dsl ──▶ DSL-AST ② (= ast2)
Four judgements are recorded per paper. The overall PASS is determined by (a).
| Key | Judgement | PASS condition |
|---|---|---|
| a | AST semantic equivalence | normalize(ast1) == normalize(ast2) |
| b | Reparseability of compiled text P |
reader(P) returns a non-null AST |
| c | Text similarity between T and P |
Jaccard ≥ threshold (report-only, not gating) |
| d | DSL surface-syntax equivalence (form①=form②) | canonical_json(ast1) == canonical_json(ast2) |
Procedure kinds are discovered dynamically from
contingency-dsl/conformance/**/*.json. Each JSON file becomes one
procedure key (e.g. operant/atomic, operant/stateful/adjusting,
respondent/elementary). Adding or renaming conformance files on the DSL
side propagates automatically into this project's DOI-list layout (no
hardcoded list).
One markdown file per procedure key is generated under corpus/doi-lists/,
each DOI displayed as a checkbox status line:
- [x] 10.1901/jeab.2005.33-03 — PASS (a:✓ b:✓ c:0.82 d:✓)
- [ ] 10.1002/jaba.226 — FAIL (a:✗ b:✓ c:0.41 d:✗)
Every e2e run writes the three round-trip intermediates per DOI in their
human-readable surface forms under corpus/generated/:
corpus/generated/
├── dsl/<slug>.dsl # AST_1 rendered as canonical DSL (e.g. "FR5")
├── compiled/<slug>.txt # P — dsl2procedure paper text
└── dsl2/<slug>.dsl # AST_2 rendered as canonical DSL
This directory is gitignored (regenerable from corpus/extracted/).
ASTs that cannot be rendered into a valid Program (e.g. partial
Experiment / Paper nodes) fall back to a commented JSON sidecar so no
round-trip result is silently lost.
Procedure plain text is cached under corpus/extracted/<doi-slug>.txt.
This directory is gitignored. If a file exists, extraction is skipped.
Fallback order when the cache is missing. Steps 2–4 are used only when
E2E_PAPER_SOURCES_ROOT points at a directory of your own paper sources:
corpus/extracted/<doi-slug>.txt(this project)$E2E_PAPER_SOURCES_ROOT/reference/papers/extracted/<doi-slug>.md(whole-paper markdown) — slice the Method section from it$E2E_PAPER_SOURCES_ROOT/reference/papers/pdf/<doi-slug>.pdf(raw PDF) — slice the Method section$E2E_PAPER_SOURCES_ROOT/notes/papers/*.md(notes with adoi:front-matter field) — use the procedure section
<doi-slug> = doi.replace("/", "_").
The harness reads the procedure taxonomy from the
contingency-dsl
conformance tree, so clone it as well and point E2E_CONFORMANCE_ROOT at
its conformance/ directory:
git clone https://github.com/OperantKit/contingency-dsl-e2e-roundtrip
git clone https://github.com/OperantKit/contingency-dsl
cd contingency-dsl-e2e-roundtrip
python -m venv .venv
.venv/bin/python -m pip install -e '.[test]'
# Optional: Anthropic SDK classification backend (requires ANTHROPIC_API_KEY)
.venv/bin/python -m pip install -e '.[llm]'
export E2E_CONFORMANCE_ROOT="$PWD/../contingency-dsl/conformance" # required
.venv/bin/python -m pytest -qpip installs contingency-dsl-py from PyPI and contingency-procedure2dsl /
contingency-dsl2procedure from their GitHub tags, so no other checkout is
needed. Python 3.11 or later.
| Variable | Required | Meaning |
|---|---|---|
E2E_CONFORMANCE_ROOT |
yes | Path to contingency-dsl/conformance. A sibling ../contingency-dsl/conformance next to this checkout is used when it is unset. |
E2E_PAPER_SOURCES_ROOT |
no | Your own paper notes / markdown / PDFs, used as extraction fallbacks 2–4 above. Unset = only corpus/extracted/ is read. |
E2E_CLASSIFIER_BACKEND |
no | Force anthropic or claude-cli (see below). |
The paper texts behind corpus/state.json are not distributed;
corpus/extracted/ starts empty in a fresh clone. Supply Method text
yourself as shown in the next section.
Round-trip one Method passage. Write the text to the extraction cache
under a DOI of your choice (here a placeholder DOI in a scratch corpus, so
the tracked corpus/state.json is left untouched), then run it:
mkdir -p demo/corpus/extracted
cat > demo/corpus/extracted/10.0000_demo.txt <<'EOF'
Procedure
Lever presses were reinforced under a fixed-ratio 5 schedule: every fifth response produced a food pellet. Sessions were conducted daily and ended after 60 reinforcers or 60 min, whichever came first. Training continued until response rates were stable across five consecutive sessions.
EOF
.venv/bin/e2e --corpus demo/corpus run --doi 10.0000/demo
# [PASS] 10.0000/demo: a=True b=True c=0.2 d=True
cat demo/corpus/generated/dsl/10.0000_demo.dsl # FR5
cat demo/corpus/generated/compiled/10.0000_demo.txt # Responses were reinforced under a fixed-ratio (FR) 5 schedule.The cached text must contain at least 200 characters of prose; shorter
files are treated as empty templates and discarded. run needs no LLM:
the classification step (classify) is the only one that calls a model.
The classifier selects a backend automatically:
| Backend | Requirement | When used |
|---|---|---|
anthropic |
ANTHROPIC_API_KEY, [llm] extra |
Preferred when the env var is set |
claude-cli |
claude binary (Claude Code CLI) |
Fallback — reuses existing CLI session, no API key |
You can force-select via E2E_CLASSIFIER_BACKEND={anthropic,claude-cli}.
The CLI backend spawns claude -p per call with cwd=/tmp and
--no-session-persistence. Because it rebuilds the session cache each
time, it is slower than the SDK path and burns more tokens.
.venv/bin/e2e list-procedures # Show all DSL procedure keys
.venv/bin/e2e extract --doi <doi> # Populate extracted/<slug>.txt
.venv/bin/e2e classify --doi <doi> # Classify via LLM (needs ANTHROPIC_API_KEY or claude CLI)
.venv/bin/e2e classify-prepare # Write corpus/work/classify-queue.jsonl for subagent consumption
.venv/bin/e2e next-batch 15 # Cut the next 15 unclassified rows into corpus/work/pending/*.txt
.venv/bin/e2e ingest-batch <file> # Append {doi, procedures} lines ('-' = stdin) to corpus/work/classify-results.jsonl
.venv/bin/e2e classify-ingest [<results>] # Fold subagent results JSONL back into state
.venv/bin/e2e regenerate-agent # (Re)publish the procedure-classifier subagent md
.venv/bin/e2e run --doi <doi> # Round-trip a single DOI
.venv/bin/e2e run --all # Round-trip every classified paper
.venv/bin/e2e report # Regenerate doi-lists/*.md from stateWhen running inside an interactive Claude Code session, the fastest path
is to dispatch classification to a sub-agent via the Agent tool. No
ANTHROPIC_API_KEY and no claude -p subprocess required.
-
Publish / regenerate the sub-agent definition (written to
.claude/agents/procedure-classifier.mdinside this project):.venv/bin/e2e regenerate-agent
Then restart Claude Code once so the new agent type is discovered (Claude Code reads agent definitions only at session start).
-
Prepare the DOI queue to be classified:
.venv/bin/e2e classify-prepare # writes corpus/work/classify-queue.jsonl -
Inside Claude Code, ask the session to perform batch classification. The session cuts prompts with
e2e next-batch 15, callsAgent(subagent_type="procedure-classifier", ...)in parallel, and appends one JSON object per DOI —{"doi": "...", "procedures": [...]}— tocorpus/work/classify-results.jsonlwithe2e ingest-batch. -
Fold the results back into state and regenerate the per-procedure DOI lists:
.venv/bin/e2e classify-ingest # reads corpus/work/classify-results.jsonl .venv/bin/e2e report
Until the new agent is discovered (i.e. until step 1 and the restart
complete), you can fall back to subagent_type="general-purpose" with
the taxonomy pasted inline into the prompt.
For queues in the hundreds-of-DOIs range, the cheapest path is to keep
Haiku resident and call the
classify-queue skill.
Making both the orchestrator (the side driving the conversation) and the
sub-agents (Agent(model="haiku", ...)) Haiku cuts token cost
substantially compared with an Opus/Sonnet-driven orchestrator. The
skill ships with this repo, and the queue handling it relies on is the
e2e next-batch / e2e ingest-batch subcommands.
-
Refresh the sub-agent definition and restart Claude Code inside this project so the agent type is reloaded:
.venv/bin/e2e regenerate-agent
-
Generate the queue:
.venv/bin/e2e classify-prepare
-
Launch a Haiku-only session from this project root (skill discovery and helper paths are CWD-dependent; wrong directory = broken run):
cd <this-package-root> claude \ --model haiku \ --effort low \ --permission-mode bypassPermissions
Flag rationale:
--effort low— minimises Haiku's extended-thinking. Classification is deterministic, so high-effort reasoning is overkill (and adds several minutes of latency per batch). Success looks like a startup banner that names a Haiku model with low effort.--permission-mode bypassPermissions(alias--dangerously-skip-permissions) — skips the approval dialog that would otherwise fire on every shell pipe / subshell( ... ). The skill only touches project-local files, so no real blast radius. Claude Code still shows a trust prompt at startup — answer "Yes".- Use this session for this project only. Do not mix tasks that require external network access or system changes into the same session.
-
Invoke the skill inside that session:
/classify-queueWhat the skill does:
- Probe once on a single DOI and halt if the reply is not a JSON object
- Split the remaining un-classified DOIs into batches of 15 with
e2e next-batch 15(prompts land incorpus/work/pending/<slug>.txt) - Fire 15
Agent(subagent_type="procedure-classifier", model="haiku", ...)calls in parallel in a single message Write-tool the 15 result lines intocorpus/work/batch-results.jsonl, then.venv/bin/e2e ingest-batch corpus/work/batch-results.jsonlappends them tocorpus/work/classify-results.jsonl(single command, no shell pipe — so allowingBash(.venv/bin/e2e:*)in your local.claude/settings.local.jsonavoids per-batch approval dialogs)- Loop until
next-batchreportsremaining=0, then auto-runclassify-ingestandreportat the end
-
Resume / re-run behaviour
e2e next-batchexcludes DOIs already present incorpus/work/classify-results.jsonl, so a crashed / restarted session resumes from the diff- Running
/classify-queueagain after full completion exits immediately withremaining=0(no-op) - Empty
procedures: []lines count as "done" and are not re-classified automatically. To redo one, delete its row fromcorpus/work/classify-results.jsonland re-invoke the skill
-
Caveat on parallelism
Running two Haiku sessions against the same queue will double-pick the same 15 DOIs and classify them twice (no locking). Run a single session, or pre-split the queue before launch.
Per-DOI round-trip results are persisted as JSON in corpus/state.json
(tracked). Reports can be regenerated from state without re-running the
pipeline.