diff --git a/Makefile b/Makefile index 4f3777d6..96098f06 100644 --- a/Makefile +++ b/Makefile @@ -52,9 +52,12 @@ install: # Sync dependencies AND rebuild the Rust VM extension. # ALWAYS use this instead of bare `uv sync` when Rust sources changed. +# ADR-DOE-ENFORCE-001 R4 (B3 裁定 2026-07-14): dev ビルドは VM conformance oracle +# (invariant-checks) を常時有効にする。tests/test_vm_invariant_checks_enabled.py が +# フラグを hard-fail で検査する(skip 禁止)。 sync: uv sync --group dev - cd packages/doeff-vm && maturin develop --release + cd packages/doeff-vm && maturin develop --release --features invariant-checks pre-commit-install: uv run pre-commit install @@ -129,6 +132,10 @@ PYTEST_MEM_GUARD_POLL_INTERVAL ?= 1.0 PYTEST_MEMORY_ENV := PYTEST_MEM_GUARD_MB=$(PYTEST_MEM_GUARD_MB) \ PYTEST_MEM_GUARD_POLL_INTERVAL=$(PYTEST_MEM_GUARD_POLL_INTERVAL) +# ADR-DOE-ENFORCE-001 R4: VM conformance oracle の Rust 側テスト(invariant-checks 有効)。 +test-vm-invariants: + cd packages/doeff-vm-core && cargo test --features "invariant-checks python_bridge" + test: bench-smoke $(PYTEST_MEMORY_ENV) uv run pytest diff --git a/docs/22-capability-classes.md b/docs/22-capability-classes.md new file mode 100644 index 00000000..3948114f --- /dev/null +++ b/docs/22-capability-classes.md @@ -0,0 +1,117 @@ +# Capability Classes: Predicting Where Effects Pay Off + +[Why Effects Over DI](20-why-effects-over-di.md) argues the case for algebraic effects by example. This document gives the underlying theory: a classification that predicts — *before you write any code* — whether a proposed handler is trivial, cheap, powerful, or impossible in doeff. It was distilled from an evidence audit across every downstream project consuming doeff, including the places where adoption paid off and the places where it was pure ceremony. + +## The Classification Axis + +Everything a handler can be classified by one question: + +> **What does the handler demand of the continuation `k`?** + +Four classes emerge. Each class strictly contains the previous one, and each step up buys new power and new cost. + +| Class | What happens to `k` | Members | DI equivalent? | +|-------|--------------------|---------|----------------| +| 0 — Dispatch | Resumed once, immediately; operation performed as-is | provider routing, mocks, secret backends | Yes | +| 1 — Data middleware | Resumed once, immediately; effect inspected/transformed/elided/repeated first | memo, replay, budget, retry, tracing, dry-run, rewrite | Mostly (decorators + contextvars) | +| 2 — Scheduling | `k` escapes the handler: parked, reordered, interleaved | Spawn/Wait, virtual time, rate limiting, batching, dedup, hedging | **No** | +| 3 — Multi-shot | `k` resumed more than once | backtracking, nondeterminism, probabilistic branching | No — **and not doeff either** | + +## Class 0 — Dispatch + +The handler performs the operation and resumes `k` once with the result. Routing by model prefix, swapping a mock for a production client, choosing a secret backend — all class 0. + +Be honest about this class: **it is the same runtime power as dependency injection.** No continuation is needed at all; a function call would do. What doeff adds is *composition-site* selection — the same Program runs under different stacks per run, per test, per dynamic scope (`Local`) — instead of one object graph fixed at construction time. That is valuable, but if class 0 is all you need, DI is a legitimate and simpler alternative. + +## Class 1 — Data Middleware + +Same contract as class 0 — `k` is resumed exactly once, immediately — but the handler does something *around* the single resume: + +- **inspect** the effect as data (cache keys, structured logs), +- **accumulate state** across heterogeneous effects (budget/cost tracking), +- **elide** the operation and substitute a value (cache hit, dry-run), +- **transform** the effect before delegating (model rewriting), +- **repeat** the operation before resuming (retry). + +The required machinery is: effects-as-data (frozen dataclasses give canonical keys for free), handler state, and *re-dispatch* — performing an effect against the rest of the stack and capturing the result as a value. Re-dispatch is not hypothetical; it is the production-proven core of the memo handler ("on memo miss: re-perform effect → outer handler handles it → store result"). + +**Consequence: the implementability of any class 1 handler is a corollary, not a research question.** A generic retry handler, a budget cap, a replay handler — if you can state it as "do X around a single resume," it is buildable with existing primitives. + +### Retry under one-shot continuations (the correct formulation) + +doeff continuations are one-shot: each `k` resumes exactly once. A retry handler must therefore **never** loop over `Resume(k, ...)`. The correct shape is: + +```python +@do +def retry_handler(effect: Effect, k): + last_err = None + for attempt in range(3): + safe = yield Try(effect) # re-YIELD the effect: fresh continuation per attempt + if safe.is_ok(): + return (yield Resume(k, safe.value)) # k resumed exactly once + last_err = safe.error + raise last_err +``` + +Retrying the *operation* is class 1. Retrying the *continuation* (re-running the rest of the program after a downstream failure) would require multi-shot and is impossible by design — see class 3 for the sanctioned approximation. + +### Honest comparison: class 1 vs decorators + +Python already has a class 1 mechanism: decorator stacks, with `contextvars` bolted on when dynamic scoping is needed (the ecosystem converged on `contextvars` precisely because wrapping alone can't scope state to an execution). Three deltas remain in doeff's favor: + +1. **Matching unit.** A decorator wraps one callable, by name, at definition time. A handler matches an effect *vocabulary* (a type) across the whole dynamic extent — including operations performed by code that didn't exist when the handler was written. "One budget across all LLM calls" needs every call site decorated, or all calls funneled through one function; with effects it is one handler. +2. **Composition scope.** Decorator composition is fixed per process. Handler stacks recompose per run and per subtree. +3. **Observable seam.** What happened inside a decorator is invisible to tests without monkeypatching. Effects are data: a test asserts cache behavior by counting intercepted effects. + +If none of these three deltas matters for your system, decorators are the cheaper tool. + +## Class 2 — Scheduling + +Here the power jumps: **`k` escapes the handler's dynamic extent.** It is stored as a value, parked, resumed later, out of order, interleaved with other continuations. Virtual time, deterministic simulation, structured concurrency (`Spawn`/`Wait`), and cooperative schedulers all live here. + +This is the class dependency injection **structurally cannot reach** — DI has no continuations at all; the "program" is the call stack itself and evaporates as it runs. If your system needs class 2, effects (or an equivalent runtime) are not a preference but a requirement. One-shot continuations are sufficient: each `k` is still resumed exactly once, just *later*. + +### Non-obvious members of class 2 + +The classification earns its keep by making non-obvious predictions. These features look like "caching relatives" but are actually schedulers, because they must park a continuation: + +- **Batching / coalescing**: effect A's continuation waits until B and C arrive so one batched call can serve all three. +- **In-flight deduplication**: the second identical request parks until the first completes, then both resume with the same value. +- **Hedged requests**: launch two, resume with the winner, cancel the loser. +- **Rate limiting done right**: a token-bucket handler that parks continuations until the bucket refills — *preventing* 429s upstream instead of retrying them (class 1) after the fact. + +Treat these with scheduler-grade care: class 2 is also where the runtime's cost concentrates. Parked continuations are live objects; their lifecycle (leaks, cancellation, ordering semantics) is the hardest part of the system. Budget review time accordingly. + +### The function-color dividend + +A class 2 side benefit: because time and concurrency are effects, a Program does not know whether it runs on a real clock or a simulated one, on asyncio or synchronously. The sync/async color distinction — Python's most viral type split — is externalized into the interpreter, and one sequencing syntax (`yield` / `<-` in Hy) expresses everything from a config read to a spawned task. + +Be honest about the trade: doeff does not erase colors, it **unifies them into one new color** — Program vs plain value. Forgetting to bind a Program builds a computation that silently never runs; over-binding a plain value fails at runtime. Unlike `async`/`await`, this color is not enforced by the interpreter's syntax. In Hy, the macro layer closes most of this gap: `defk`/`do!`/`defp`/`deftest` wrap statement-position expressions in a runtime guard that raises immediately (with a fix template) when a bare Program/EffectBase is about to be silently discarded (ADR-DOE-HY-001). Return-position composition remains legal; nested control-form bodies are not yet guarded — treat those as a review point. + +## Class 3 — Multi-shot (deliberately absent) + +Resuming `k` more than once enables backtracking search, nondeterminism, and probabilistic branching. doeff excludes this class by design (one-shot continuations, unlike Koka or Eff). There is no hidden reserve of power here; the ceiling is explicit. + +The sanctioned approximation composes classes you already have: **re-execute the whole Program with a memoized prefix.** Program-as-value means the computation can always be run again; a class 1 memo/replay handler makes the re-run deterministic and fast up to the branch point. What-if analysis and branch exploration are therefore expressible — as re-execution, not as continuation copying. + +## When Adoption Pays: The Evidence Rule + +Across every audited downstream project, one rule explains the outcomes: + +> **Effects pay when (a) you need to manipulate call boundaries at composition/run time, and (b) the per-call cost of those boundaries dwarfs the interception overhead.** + +Concretely: + +- **Strong fits** (both factors high): running one Program under simulated/paper/live interpreters; record/replay of expensive calls (LLM, market data) with fail-on-miss guarantees; hermetic test suites over I/O-heavy pipelines. These were, empirically, the load-bearing wins. +- **Poor fit — small single-context apps** (factor *a* low): if the code will only ever run one way, the effect vocabulary is ceremony. Projects that adopted doeff as a template, without an interpreter-swapping problem, ended up deleting or bypassing it. +- **Poor fit — CPU-bound hot loops** (factor *b* inverted): interception overhead is per-effect. Historical note: an earlier doeff version captured creation context on every effect and was measured at a multiple-x slowdown in a tight simulation loop; current doeff captures trace context only via opt-in effects (`GetTraceback`/`GetExecutionContext`) and on error paths, with line-text resolution deferred to render time — a regression guard pins this property (ADR-DOE-CORE-002). The residual per-effect cost is dispatch itself (VM core plus `@do` generator re-construction glue), so the guidance stands: move sub-millisecond hot paths below the effect boundary (e.g., into a native extension), keeping effects for orchestration. + +## An Adoption Anti-Pattern: Building Power Faster Than Using It + +The recurring failure mode observed downstream is not misuse of effects but **unexercised capability**: test machinery wired but never adopted, enforcement rules written but never run in CI, documented handlers with zero call sites. Capability that isn't exercised silently decays and gives false confidence. + +Practical guidance: + +1. Adopt by *consuming* existing handlers before writing new ones. Most needs are class 0/1 and already covered. +2. If you write a class 1 handler, port at least one real call site to it in the same change — a handler with no consumer is a claim, not a feature. +3. If you write a class 2 handler, wire its invariant checks into CI *first*. Scheduler-grade power without scheduler-grade guards is how production incidents happen. diff --git a/docs/adr/conftest.py b/docs/adr/conftest.py new file mode 100644 index 00000000..95ed7182 --- /dev/null +++ b/docs/adr/conftest.py @@ -0,0 +1,25 @@ +"""docs/adr 用 doeff_interpreter fixture — doeff-adr deftest enforcement の実行時。 + +責務境界(doeff-adr の方針): 収集は doeff-adr pytest plugin が所有し、 +実行時 fixture は消費リポジトリ(ここでは doeff 自身)が所有する。 + +この fixture は ADR-DOE-HY-002 R3 の参照実装を兼ねる: +- deftest の :env は reader ハンドラ経由で必ず反映する(黙って無視しない)。 +- エラーは再送出する(RunResult 吸収による偽緑を作らない)。 +""" + +import pytest + + +@pytest.fixture +def doeff_interpreter(): + def run_program(program, *, env=None): + from doeff import run + + if env: + from doeff_core_effects.handlers import reader + + program = reader(dict(env))(program) + return run(program) + + return run_program diff --git a/docs/adr/defadr_doeff_core_002_lazy_creation_trace.hy b/docs/adr/defadr_doeff_core_002_lazy_creation_trace.hy new file mode 100644 index 00000000..c4cf22d5 --- /dev/null +++ b/docs/adr/defadr_doeff_core_002_lazy_creation_trace.hy @@ -0,0 +1,76 @@ +;;; Executable ADR: effect 生成ホットパスでの作成文脈捕捉は遅延・最小化する。 +;;; トレーシング(class 1 ミドルウェア)の per-effect コストが hot path を +;;; 侵食してはならない。enforcement は C1 spike(捕捉サイト特定)後に追加する。 + +(require doeff-adr.macros [defadr defsemgrep rule law]) +(require doeff-hy.macros [deftest defk <-]) +(import doeff-adr.macros [fact interpretation counterexample]) +(import doeff [Ask run]) +(import linecache) + + +;; R1 回帰ガード用のホットパス・プローブ: エラーなしの Ask dispatch ループ。 +(defk _probe-hot-loop [] + {:pre [] :post [(: % int)]} + (setv total 0) + (for [_ (range 50)] + (<- v (Ask "x")) + (setv total (+ total v))) + total) + + +(defadr ADR-DOE-CORE-002 + :title "lazy creation trace(回帰ガード): effect 生成・dispatch のホットパスでは行テキスト解決(linecache)・文字列整形・スタック walk を行わない。捕捉の発火点は opt-in effect(GetTraceback / GetExecutionContext)とエラー経路のみ — C1 spike(2026-07-14)で現行 main が既にこの性質を満たすと確認済みのため、本 ADR はこれを不変量として固定し将来の侵食から守る" + :status "proposed" + :scope ["doeff/traceback.py" + "packages/doeff-core-effects" + "docs/adr/defadr_doeff_core_002_lazy_creation_trace.hy"] + :problem + [(fact + "下流実測(proboscis-ema、2026-06): 当時のシミュレーション interpreter において effect 生成毎の作成文脈捕捉(スタック検査+行テキスト解決)が実行時間の約32%を占め、全体で 3.7x 減速・関数呼び出し 4.1x。同 issue の結論は Rust 化を否定(interpreter core は全体の ~9% に過ぎない)。" + :evidence "proboscis-ema VAULT issue『doeff Tracing Overhead Causes 3.7x Slowdown』(open, 2026-07-14 時点)") + (fact + "C1 spike(2026-07-14)の判定: 現行 main にこの捕捉コストは存在しない。捕捉は opt-in effect(GetTraceback / GetExecutionContext、step.rs:320-341)とエラー経路(step_raise step.rs:115、unhandled_effect dispatch.rs:66-74)のみで発火し、通常の Perform→dispatch→Resume/Transfer では一切走らない。行テキスト解決(linecache)は doeff/traceback.py:105-112 のみで、呼び出し元は run() の except 節(doeff/run.py:16-34)= 描画時限定。100k 回の Ask ループ実測でも linecache / stack-walk / 文字列整形は 0 呼び出し(内訳: Rust VM core 48% / @do の generator 再構築グルー ~39% / effect 構築 ~7% / ユーザーコード ~7%)。" + :evidence "scratchpad bench_ask_loop.py + ask_loop.pstats(2026-07-14 C1 spike); packages/doeff-vm/src/python_generator_stream.rs:371-409; packages/doeff-vm-core/src/vm.rs:76-213") + (fact + "残る字義上の非遅延は1箇所のみ: run() の例外経路 _enrich_exception_traceback / _merge_python_frames(doeff/run.py:38-100)が traceback.extract_tb を lookup_line=False なしで呼び、未捕捉例外1回につき linecache を先読みする。hot path 外だが R1 の字義には反する。" + :evidence "doeff/run.py:38-100(C1 spike 2026-07-14)") + (fact + "一般則としての位置づけ: 『class 1 ミドルウェア(トレーシング)の代償は per-effect オーバーヘッド』の実例。現行 main は設計としてこれを回避済みであり、本 ADR の役割は修正ではなく回帰ガード(この性質を将来の変更から守る)である。下流の 3.7x は旧版に対する計測であり、pin 更新で解消される見込み — 下流 issue の再検証が残作業。" + :evidence "docs/22-capability-classes.md『When Adoption Pays: The Evidence Rule』; C1 spike 2026-07-14")] + :context + [(interpretation + "『call tree が自動で・タダで手に入る』(docs/20-why-effects-over-di.md の利点 #3)は生成時捕捉が常時 on である限り成立しない — タダなのはコード上であって実行時ではない。遅延できるものは遅延し(行テキスト・整形は描画時)、遅延できないもの(スタック情報はフレーム消滅前に取るしかない)は最小の生データに絞る。") + (interpretation + "構造的な終着点(C3、要裁定): 作成文脈捕捉はトレーシングという observability の関心事であり、効果哲学に従えば handler スタックが opt-in する構成要素であるべきで、effect 生成側の無条件動作であるべきではない。本番障害の事後デバッグは『memo replay 下で tracing を有効にして再実行』を正規経路とする(Program-as-Value + class 1 replay による multi-shot 近似)。") + (interpretation + "C1 spike の帰結による再スコープ: R1 は現行 main で既に成立している(意図的な遅延化ではなく、捕捉が opt-in effect + エラー経路限定として設計されていたため)。よって本 ADR の enforcement は『直す』テストではなく『この性質が侵食されたら fail する』回帰ガードである。旧 R2(observability handler への opt-in 移行)は前提を失い縮小 — 残る C3 論点は『GetTraceback / GetExecutionContext を VM 組み込み命令のままにするか observability handler 経由に載せ替えるか』という独立の設計判断のみ。")] + :decision + [(rule R1 "effect 生成・dispatch のホットパスでは linecache 読み・文字列整形・行テキスト解決・スタック walk を行わない。捕捉の発火点は opt-in effect(GetTraceback / GetExecutionContext)とエラー経路のみに限る(現行 main の性質を不変量として固定する)。") + (rule R2 "run() の例外経路 _merge_python_frames は traceback.extract_tb を lookup_line=False で呼び、行テキスト解決を描画時(format 時)まで遅延する(字義ギャップの解消、hot path 外の小修正)。") + (rule R3 "『GetTraceback / GetExecutionContext を observability handler 経由に載せ替えるか』(旧 C3)は独立の結合核判断として frontier + 人間で裁定する。perf 根拠は消滅したため、裁定は可観測性の設計論のみで行う。@do の generator 再構築グルー(実測 ~39%)は本 ADR の対象外 — 別 issue として起票する。")] + :laws + [(law effect-creation-is-cheap + :statement "for_all effect_creation: linecache_calls == 0 AND string_formatting == 0; human_readable_resolution happens_at render_time only" + :counterexamples + [(counterexample "effect 生成毎にスタック walk + linecache.getline で行テキストを即時解決する(下流で 3.7x 減速を実測した形)") + (counterexample "トレース無効時にも捕捉コードが走る(フラグが表示だけを止め、捕捉を止めない)")])] + :enforcement + [(deftest test-adr-doe-core-002-no-linecache-on-hot-path + ;; 回帰ガード(C1 spike 2026-07-14 で確認済みの性質を固定): + ;; エラーなしの effect dispatch ループで行テキスト解決(linecache.getline)ゼロ。 + ;; per-effect の作成文脈捕捉が将来復活したら、ここが red になる。 + (import doeff_core_effects.handlers [reader]) + (setv calls []) + (setv orig linecache.getline) + (defn _counting [#* a #** k] + (.append calls a) + (orig #* a #** k)) + (setv (. linecache getline) _counting) + (try + (setv result (run ((reader {"x" 1}) (_probe-hot-loop)))) + (finally (setv (. linecache getline) orig))) + (assert (= result 50) f"probe の結果が不正: {result}") + (assert (= (len calls) 0) + f"hot path で linecache.getline が {(len calls)} 回呼ばれた — ADR-DOE-CORE-002 R1"))] + :plans ["docs/doeff-2026-07-14-agent-first-investment-architecture-plan.md"]) diff --git a/docs/adr/defadr_doeff_domain_001_vocabulary_cohesion.hy b/docs/adr/defadr_doeff_domain_001_vocabulary_cohesion.hy new file mode 100644 index 00000000..b8d5aed2 --- /dev/null +++ b/docs/adr/defadr_doeff_domain_001_vocabulary_cohesion.hy @@ -0,0 +1,52 @@ +;;; Executable ADR: FP の「純粋だが所属不明の小関数の森」問題への doeff の答えは、 +;;; 凝集を「語彙 + 法」の宣言レベルで結合する defdomain と、エージェントが照会できる +;;; SEDA(CLI/MCP)、そして適合検査である。エージェントにとってのドキュメントとは +;;; 失敗する検査のことである。 + +(require doeff-adr.macros [defadr defsemgrep rule law]) +(require doeff-hy.macros [deftest]) +(import doeff-adr.macros [fact interpretation counterexample]) + + +(defadr ADR-DOE-DOMAIN-001 + :title "vocabulary cohesion: doeff-hy は凝集単位の宣言 defdomain(effect 型の束 + laws + 期待 handler 面)を提供し、SEDA は CLI/MCP としてエージェントから照会可能にし、適合検査(handler の domain 被覆・Program 使用 effect 集合 ⊆ interpreter 処理集合・domain 外の同義語彙定義の禁止)を defsemgrep/deftest で提供する。IDE 面は非優先 — 書き手はエージェントである" + :status "proposed" + :scope ["packages/doeff-hy/src/doeff_hy/macros.hy" + "packages/doeff-effect-analyzer" + "docs/adr/defadr_doeff_domain_001_vocabulary_cohesion.hy"] + :problem + [(fact + "実害の実例(姉妹リポジトリ ACP、核#8): エージェント swarm が `_issue-terminal?` 述語を 7 定義、terminal-set を 6 定義(値も発散)、failure classifier を 3 系統まで増殖させた — 既存の 1 本が見つからないまま書き足される FP 凝集性欠如の典型。何も fail しなかった。ADR 0031 R2/R4(2026-07-03)で単一正典 predicates.hy に統合して是正。" + :evidence "agent-control-plane docs/erosion-audit-2026-07-02.md 核#8; ACP ADR 0031; apps/hypha/shared/predicates.hy") + (fact + "SEDA(doeff-effect-analyzer)は『静的 effect 依存解析・incremental tree-structured reports・PyO3』を掲げる WIP。凝集の照会面(『この Program はどの effect を実行しうるか』『この effect の handler はどこか』)の種は既に社内にある。" + :evidence "packages/doeff-effect-analyzer/README.md; specs/effect-analyzer/") + (fact + "書き手エージェントは IDE 補完を使わない。grep・ファイル読取・スキル・失敗する検査に反応する。よって凝集の担保は探索性の改善(IDE のドット体験)ではなく、照会可能なインデックスと fail する適合検査に置く。" + :evidence "2026-07-14 maintainer 前提(agent-first); ACP 核#8 の増殖主体が swarm だった事実")] + :context + [(interpretation + "OOP の凝集の正体は実行時機構(クラス)ではなく発見可能性である。doeff には effect 型族=インターフェース、handler=実装、handler stack=オブジェクトグラフという暗黙の凝集単位が既にあるが、宣言も強制もされていない。defdomain はこれを宣言に昇格する — 結合するのは語彙と法だけで、実装も状態も結合しない(OOP の密結合の原因を除いた凝集の輸入)。") + (interpretation + "エージェントにとってのドキュメントとは失敗する検査のことである。domain 宣言は (1) エージェントに最初に読ませる 1 ファイルであり、(2) 適合検査の照合対象である。docs は読み飛ばされるが red は必ず読まれる。")] + :decision + [(rule R1 "doeff-hy は凝集単位の宣言マクロ defdomain を提供する: effect 型の束、domain laws、期待される handler 面(どの handler 群がこの語彙を被覆すべきか)を 1 宣言にまとめる。") + (rule R2 "SEDA は CLI および MCP tool としてエージェントから照会可能にする: 『この Program が実行しうる effect 集合』『この effect 型の handler 所在』『この interpreter の処理集合』。IDE プラグイン面は非優先とする。") + (rule R3 "適合検査を defsemgrep / deftest で提供する: (a) handler が宣言 domain の全 effect を被覆する、(b) Program の使用 effect 集合 ⊆ 実行 interpreter の処理集合、(c) domain 宣言の外での同義語彙(同一意味論の述語・effect)の定義を禁止する。") + (rule R4 "本 ADR の実装は計画 Stage E であり、Track B(pytest 正典ゲート)の完了を前提とする — 適合検査は走るゲートがあって初めて意味を持つ。")] + :laws + [(law vocabulary-has-single-home + :statement "for_all semantic_predicate_or_effect v: canonical_declaration_count(v) == 1 AND declared_in_some_domain(v); duplicate_definitions_detected_by_conformance_check" + :counterexamples + [(counterexample "ACP 核#8: terminal 述語 7 定義・terminal-set 6 定義が並存し、判定が呼び出しサイトごとに発散(2026-07-02 監査で検出、何も fail していなかった)") + (counterexample "新規エージェントが既存 domain 語彙を発見できず、同義の effect 型を別パッケージに追加する — 照会面(SEDA)不在時の既定挙動")])] + :enforcement + [(deftest test-adr-doe-domain-001-defdomain-exists + ;; RED(2026-07-14): defdomain マクロは未実装。R1 実装で green。 + ;; ソーステキストのピン(マクロは module 属性にならないため)。実装後は挙動ピンに昇格する。 + (import pathlib [Path]) + (setv root (get (. (Path __file__) parents) 2)) + (setv macros-src (.read-text (/ root "packages" "doeff-hy" "src" "doeff_hy" "macros.hy"))) + (assert (in "defmacro defdomain" macros-src) + "defdomain マクロが doeff-hy に存在しない — ADR-DOE-DOMAIN-001 R1"))] + :plans ["docs/doeff-2026-07-14-agent-first-investment-architecture-plan.md"]) diff --git a/docs/adr/defadr_doeff_enforce_001_pytest_canonical_gate.hy b/docs/adr/defadr_doeff_enforce_001_pytest_canonical_gate.hy new file mode 100644 index 00000000..a61d8759 --- /dev/null +++ b/docs/adr/defadr_doeff_enforce_001_pytest_canonical_gate.hy @@ -0,0 +1,72 @@ +;;; Executable ADR: enforcement の正典ゲートは既定のローカル pytest である。 +;;; GitHub CI には依存しない(予算により停止中)。doeff は自分の invariant を +;;; 自分の機構(defadr / deftest / defsemgrep)で、既定実行の中で守る。 + +(require doeff-adr.macros [defadr defsemgrep rule law]) +(require doeff-hy.macros [deftest]) +(import doeff-adr.macros [fact interpretation counterexample]) + + +(defadr ADR-DOE-ENFORCE-001 + :title "pytest canonical gate: doeff の全 enforcement(defadr 収集・defsemgrep 静的検査・VM conformance oracle・台帳 ratchet)は既定の `uv run pytest` で収集・実行される。手動起動のみの検査、skip で緑になる検査、testpaths 外で沈黙する検査を禁止する" + :status "proposed" + :scope ["pyproject.toml" + "Makefile" + ".semgrep.yaml" + "packages/doeff-adr/src/doeff_adr" + "packages/doeff-vm-core/Cargo.toml" + "docs/adr/defadr_doeff_enforce_001_pytest_canonical_gate.hy"] + :problem + [(fact + "侵食監査 2026-07-02 の判定: doeff の宣言済み不変量は ENFORCED 7 / PARTIAL 4 / EXISTS-NOT-WIRED 10 / DOC-ONLY 6。監査の結語は『現状、デフォルト実行で自分の invariant を defadr 機構で守れている箇所はゼロ』。" + :evidence "docs/crystallization/erosion-audit-2026-07-02.md") + (fact + "VM conformance oracle(runtime invariants I1–I8 — 3度のリビルドから抽出された教訓の機械化)は doeff-vm-core の cargo feature `invariant-checks`(+ `python_bridge`)配下に実装済みだが、Makefile にも既定テストにも起動経路が存在しない(`grep invariant Makefile` → 0件、2026-07-14 実測)。" + :evidence "packages/doeff-vm-core/Cargo.toml:14-20; Makefile(2026-07-14); docs/crystallization/invariants.md") + (fact + ".semgrep.yaml の 229 ルール(2026-07-14 実測)は `make lint-semgrep` の手動起動のみで、どの自動ゲートも実行しない。semgrep は dev 依存ですらない。姉妹リポジトリ ACP では 2026-07-11 の初回ゲート実行で 16 検査が即 red になった(semgrep バイナリ不在)— 『書かれたが走らない』検査は牙があっても偽の安心を生む。" + :evidence ".semgrep.yaml; docs/crystallization/erosion-audit-2026-07-02.md; agent-control-plane .github/workflows/ci.yml 初回実行 2026-07-11") + (fact + "pyproject の testpaths は tests + 4 パッケージ tests のみで docs/adr を含まない。既存 defadr 10本は既定 pytest で不可視。doeff / proboscis-ema / agent-control-plane の3リポジトリが同一の罠(defadr が testpaths 外で黙って収集されない)に同時に嵌った — 各リポジトリの不注意ではなく所有層(doeff-adr)で潰すべき系統的欠陥。" + :evidence "pyproject.toml:80-86(2026-07-14); erosion-audit 横断所見 #3; doeff issue doeff-adr-wiring-selfcheck") + (fact + "GitHub Actions は予算制約により停止中であり、再有効化は選択肢にない(2026-07-14 maintainer)。ゲートは開発者・エージェントが常に走らせるローカル実行に置くしかない。" + :evidence "2026-07-14 doeff 投資計画議論")] + :context + [(interpretation + "支配的な故障モードは『検査の不在』ではなく『配線の不在』(侵食監査の横断所見 #1)。検査は良質に書かれている — 走らないだけ。よって本 ADR の仕事は新しい検査を書くことではなく、既存の検査を『pytest が緑 = enforcement が走った』が構造的に成立する場所へ移すこと。") + (interpretation + "書き手がエージェントである以上、ゲートの正典は『エージェントが自分のループで必ず走らせるもの』でなければならない。それは pytest である。GitHub CI は(予算以前に)エージェントのループの外にある。") + (interpretation + "skip は偽緑の温床である。semgrep バイナリ不在・fixture 不在・feature 未ビルドは、skip ではなく hard fail として現れなければならない — fail-fast はこのリポジトリ群の基本方針である。")] + :decision + [(rule R1 "enforcement の正典ゲートは既定の `uv run pytest`(testpaths 収集)である。GitHub CI には依存しない。pre-commit / make はこのゲートの別名であってよいが、代替ではない。") + (rule R2 "testpaths は docs/adr を含む。さらに defadr 収集自己検査(defadr_*.hy のファイル数と収集された ADR モジュール数の一致検査)を doeff-adr パッケージが所有・提供し、全消費リポジトリが継承する(issue doeff-adr-wiring-selfcheck の根本対処)。") + (rule R3 ".semgrep.yaml のルールは defsemgrep(installed-rule 形式)経由で既定 pytest 収集に載せる。semgrep バイナリ不在は skip ではなく hard fail。semgrep は dev 依存として `make sync` で必ず入る。") + (rule R4 "dev ビルドは doeff-vm-core を feature `invariant-checks` + `python_bridge` 有効でビルドする(2026-07-14 B3 裁定)。VM conformance oracle(I1–I8)は pytest から起動される。invariant-checks 無効ビルドでの oracle テストは hard fail(skip 禁止)。") + (rule R5 "anti-drop ratchet: enforcement 台帳(defadr 数・law 数・defsemgrep 数・deftest enforcement 数)が黙って減ったら fail するメタテストを既定収集に置く(orch SpecInventorySpec の pytest 版)。台帳の意図的な削減は台帳ファイルの明示的更新を伴う。")] + :laws + [(law default-pytest-sees-all-enforcement + :statement "for_all declared_enforcement e: collected_by(default_pytest, e) AND (missing_dependency(e) => hard_fail, not skip)" + :counterexamples + [(counterexample "defadr 10本が testpaths 外で沈黙している現状 — `uv run pytest` は enforcement ゼロのまま緑") + (counterexample "semgrep 不在の環境で defsemgrep が skip され、229 ルール全滅のままスイートが緑") + (counterexample "invariant-checks 無効のリリースビルドに対して oracle テストが skip され、VM 不変量が未検証のまま緑")])] + :enforcement + [(deftest test-adr-doe-enforce-001-docs-adr-in-default-testpaths + ;; RED(2026-07-14): pyproject testpaths は docs/adr を含まない。R2 実装で green。 + (import tomllib) + (import pathlib [Path]) + (setv root (get (. (Path __file__) parents) 2)) + (setv cfg (tomllib.loads (.read-text (/ root "pyproject.toml")))) + (setv testpaths (get cfg "tool" "pytest" "ini_options" "testpaths")) + (assert (in "docs/adr" testpaths) + f"docs/adr が testpaths に無い: {testpaths} — ADR-DOE-ENFORCE-001 R2")) + (deftest test-adr-doe-enforce-001-vm-oracle-wired + ;; RED(2026-07-14): Makefile に invariant-checks の起動経路が無い。R4 実装で green。 + (import pathlib [Path]) + (setv root (get (. (Path __file__) parents) 2)) + (setv makefile (.read-text (/ root "Makefile"))) + (assert (in "invariant-checks" makefile) + "Makefile に invariant-checks の配線が無い — ADR-DOE-ENFORCE-001 R4(B3 裁定 2026-07-14)"))] + :plans ["docs/doeff-2026-07-14-agent-first-investment-architecture-plan.md"]) diff --git a/docs/adr/defadr_doeff_hy_001_statement_bind_guard.hy b/docs/adr/defadr_doeff_hy_001_statement_bind_guard.hy new file mode 100644 index 00000000..094ca5d0 --- /dev/null +++ b/docs/adr/defadr_doeff_hy_001_statement_bind_guard.hy @@ -0,0 +1,67 @@ +;;; Executable ADR: defk/do! 本体の statement 位置で評価された Program/EffectBase を +;;; 黙って破棄することを禁止する(guard-error)。auto-bind は採用しない。 +;;; エラーメッセージはエージェント書き手向けの修正プロンプトとして設計する。 + +(require doeff-adr.macros [defadr defsemgrep rule law]) +(import doeff-adr.macros [fact interpretation counterexample]) +(require doeff-hy.macros [deftest defk]) +(import doeff [run]) +(import pytest) + + +;; ADR-DOE-HY-001 反例の生きた再現体(enforcement が run する)。 +;; `(_probe-inner)` は statement 位置の bare kleisli 呼び出し — Program を生成して破棄する。 +(defk _probe-inner [] + {:pre [] :post [(: % int)]} + 1) + +(defk _probe-outer [] + {:pre [] :post [(: % int)]} + (_probe-inner) ;; ← 罠: 現行マクロは「Plain statement — emit as-is」で素通しする + 2) + + +(defadr ADR-DOE-HY-001 + :title "statement-position bind guard: defk/do! 本体の statement 位置で評価された値が Program(DoExpr)/EffectBase であるとき、マクロが挿入する runtime guard が即時 RuntimeError を送出する(guard-error)。auto-bind(Haskell do 記法式の自動束縛)は採用しない — 書き手はエージェントであり、必要なのは書き味ではなく決定的で行動可能な赤である" + :status "proposed" + :scope ["packages/doeff-hy/src/doeff_hy/macros.hy" + "docs/adr/defadr_doeff_hy_001_statement_bind_guard.hy"] + :problem + [(fact + "現行マクロは defk/do! 本体の非最終 statement 位置の式を『Plain statement (setv, when, for, etc.) — emit as-is』として素通しする。bare kleisli 呼び出しは Program を生成して黙って捨て、実行も例外も起きない。" + :evidence "packages/doeff-hy/src/doeff_hy/macros.hy:623, 748, 1069(2026-07-14 時点)") + (fact + "既存 guard `_guard-performed` の守備範囲は『最終式が bare EffectBase』のみ・実行時のみ。DoExpr/Program は docstring で明示的に対象外(『The Program-return composition pattern returns a DoExpr ... so this never fires on it』)。" + :evidence "packages/doeff-hy/src/doeff_hy/macros.hy:300-317") + (fact + "下流実害: agent-control-plane の deff→defk 一斉移行計画は silent-passthrough(bind 忘れ → Program が走らず raise もしない)を非自明 gotcha として列挙。逆方向の色エラー(Program-as-value の過剰 bind)は 2026-07-13 に deftest/CI green のまま本番 supervisor を crash-loop させた。" + :evidence "agent-control-plane ADR 0056 移行計画(facts a–p); ACP hotfix commit 2026-07-13『K2 潜伏欠陥: Program-as-value の過剰 bind を復元 — dogfood 停止 incident』") + (fact + "本リポジトリの開発様式: コードの書き手は常にエージェントであり、人間はレビュアーとしてのみ関与する(2026-07-14 maintainer 裁定)。エージェントは沈黙の意味論から学習せず、行番号と修正テンプレを含む決定的な赤に最も確実に反応する。" + :evidence "2026-07-14 doeff 投資計画議論(docs/doeff-2026-07-14-agent-first-investment-architecture-plan.md)")] + :context + [(interpretation + "doeff は sync/async の色を消したのではなく『Program か値か』という新しい1色に統一した。bind 忘れ・過剰 bind はこの色の色エラーであり、async/await と違い構文が強制しないため、マクロ層(構文木を見られる唯一の層)が守るのが所有レイヤとして正しい。Python 側 linter での検出は型推論を要し偽陰性を作る — Hy が第一級言語である以上、防衛線はマクロに置く。") + (interpretation + "auto-bind は書き味では優るが二重に不適: (1) 休眠していた bare form が導入日に『そのファイルに一切 diff がないまま』動き出す挙動変更であり、(2) エージェント書き手に教育信号を一切与えない。guard-error はエージェントの実行ループ内で失敗し、そのメッセージがそのまま修正プロンプトになる。") + (interpretation + "静的(展開時)検出はリテラルな EffectBase コンストラクタ等の狭いケースにしか健全でない。動的言語で偽陰性の混じる静的検査より、一様で決定的な runtime guard(statement 1つあたり isinstance 1回)を選ぶ。")] + :decision + [(rule R1 "defk/do! 本体の statement 位置(束縛・return・既知の制御形以外)で評価された値が DoExpr/Program または EffectBase であるとき、マクロが挿入する runtime guard が即時 RuntimeError を送出する。") + (rule R2 "auto-bind は採用しない。値を捨てて実行したい場合の正式な形は明示 discard-bind `(<- _ expr)` である。") + (rule R3 "guard のエラーメッセージは修正プロンプト形式とする: 関数名、ファイル:行、評価された型名、修正テンプレ『(<- _ ())』を必ず含む。エラーメッセージは人間向け説明ではなくエージェント向け修正指示である。") + (rule R4 "setv/print 等、Program でも EffectBase でもない値の statement は無傷で素通しする(guard は isinstance 検査のみ)。") + (rule R5 "消費リポジトリへの展開は guard 有効化 → 休眠 discard の全数洗い出し・修正(計画 A2)を経る。洗い出し完了までは guard を doeff 本体と opt-in 消費者に限定してよい。")] + :laws + [(law statement-discard-impossible + :statement "for_all defk_or_do!_body: evaluated_at_statement_position(v) AND isinstance(v, DoExpr | EffectBase) => raises(RuntimeError); silent_discard_count == 0" + :counterexamples + [(counterexample "中間 statement の bare kleisli 呼び出し `(notify-user order receipt)` — 通知 Program が生成・破棄され、通知は永遠に飛ばず、テストも例外も出ない(現行挙動)") + (counterexample "do! の最終式手前の bare `(LaunchEffect ...)` — _guard-performed は最終式しか見ないため素通り")])] + :enforcement + [(deftest test-adr-doe-hy-001-bare-statement-program-raises + ;; RED(2026-07-14): 現行マクロは _probe-outer の bare (_probe-inner) を黙って捨て、 + ;; run は 2 を返して成功する。R1 実装後、guard が RuntimeError を送出して green。 + (with [(pytest.raises RuntimeError)] + (run (_probe-outer))))] + :plans ["docs/doeff-2026-07-14-agent-first-investment-architecture-plan.md"]) diff --git a/docs/adr/defadr_doeff_hy_002_deftest_expressiveness.hy b/docs/adr/defadr_doeff_hy_002_deftest_expressiveness.hy new file mode 100644 index 00000000..2b151ac5 --- /dev/null +++ b/docs/adr/defadr_doeff_hy_002_deftest_expressiveness.hy @@ -0,0 +1,48 @@ +;;; Executable ADR: deftest の :interpreters / :env 語彙は「部分スタック合成 + +;;; handler 差し替え + env 注入」を消費側シムなしで表現できなければならない。 +;;; エージェントが検証を deftest で忠実に表現できないことは、レビュアー(人間)への +;;; 検証報告が歪むことと同義である。 + +(require doeff-adr.macros [defadr defsemgrep rule law]) +(require doeff-hy.macros [deftest <-]) +(import doeff-adr.macros [fact interpretation counterexample]) +(import doeff [Ask]) + + +(defadr ADR-DOE-HY-002 + :title "deftest expressiveness contract: doeff-hy の deftest は :interpreters / :env / :marks を fixture へ忠実に受け渡し、その語彙だけで『部分スタック合成 + 単一 handler 差し替え + env 注入』が表現できることを契約とする。消費リポジトリが deftest を回避して自前 run_test シムを再発明する状態は本契約の違反シグナルである" + :status "proposed" + :scope ["packages/doeff-hy/src/doeff_hy" + "packages/doeff-adr/src/doeff_adr/pytest_plugin.py" + "docs/adr/conftest.py" + "docs/adr/defadr_doeff_hy_002_deftest_expressiveness.hy"] + :problem + [(fact + "最大級の Python 消費者 mediagen は conftest で doeff_interpreter fixture を配線済みにもかかわらず、260 のテスト関数中 deftest 使用は 0。代わりに tests/support/run_test.py が interpreter スタックの一部を再実装し、手動 with_handlers 差し替えを行う。fixture の :env 経路は NotImplementedError(『deftest :env override is not wired to mediagen_interpreter yet』)のまま。" + :evidence "mediagen conftest.py / tests/support/run_test.py / mediagen ADR-008 §5 residual(2026-07-14 調査)") + (fact + "書き手エージェントに対する検証契約の観点: エージェントが issue の Verification 項目を deftest で忠実に表現できないと、検証の沈黙弱体化(2026-06-12 の PR #472 / #474 事案 — mock 置換・表示のみ検査が green のまま出荷)の再発面になる。deftest の表現力は品質改善ではなく、エージェント主導開発の成立条件である。" + :evidence "doeff CLAUDE.md『Verification Contract (CRITICAL)』; 2026-07-14 投資計画議論")] + :context + [(interpretation + "消費者がフレームワークの看板テスト機構を配線までして使わないのは、需要の不在ではなく表現力の不足のシグナルである(mediagen はシムを書いてまで同じことをしている)。シムが表現している要求 — 部分スタック + 差し替え + env — を deftest の語彙に吸収するのが所有レイヤの仕事。") + (interpretation + "責務境界: doeff-hy は params の忠実な受け渡しを所有し、doeff-adr は収集を所有し、消費リポジトリは doeff_interpreter fixture(env の反映義務を含む)を所有する。doeff 自身の docs/adr conftest fixture はこの fixture 契約の参照実装を兼ねる。")] + :decision + [(rule R1 "deftest の :interpreters / :env 語彙で『部分スタック合成 + 単一 handler 差し替え + env 注入』を、消費側の自前シムなしで表現できる。受け入れ基準は mediagen の tests/support/run_test.py が deftest 語彙で置換可能になること。") + (rule R2 "doeff-hy は deftest params(:env / :interpreters / :marks)を fixture に忠実に受け渡す。fixture 側契約(:env の反映義務、未対応 params の hard fail)を文書化する。") + (rule R3 "doeff 自身の docs/adr conftest の doeff_interpreter fixture は :env を反映する参照実装とする。fixture が :env を黙って無視することを禁止する(NotImplementedError による fail は許容 — 沈黙よりよい)。")] + :laws + [(law deftest-params-are-honored + :statement "for_all deftest t: env_declared(t, k, v) => Ask(k) == v under_fixture; params_silently_dropped == 0" + :counterexamples + [(counterexample "fixture が :env を受け取りながら反映せず、テストが本番 env で走って偶然 green になる") + (counterexample "deftest を回避した自前シムが interpreter スタックの一部を再実装し、スタック合成の変更(handler 順序等)への追従が漏れる")])] + :enforcement + [(deftest test-adr-doe-hy-002-env-roundtrip + {:env {"adr.doe.hy.002.probe" 42}} + ;; R2/R3 のピン: deftest の :env が fixture 経由で Ask に届くこと。 + ;; red の場合は params 受け渡しか fixture 反映のどちらかが未配線(どちらも本 ADR の対象)。 + (<- v (Ask "adr.doe.hy.002.probe")) + (assert (= v 42) f"deftest :env が Ask に届いていない: {v} — ADR-DOE-HY-002 R2/R3"))] + :plans ["docs/doeff-2026-07-14-agent-first-investment-architecture-plan.md"]) diff --git a/docs/adr/enforcement-ledger.json b/docs/adr/enforcement-ledger.json new file mode 100644 index 00000000..61d8441a --- /dev/null +++ b/docs/adr/enforcement-ledger.json @@ -0,0 +1,8 @@ +{ + "_comment": "ADR-DOE-ENFORCE-001 R5 anti-drop ratchet の台帳。enforcement 資産の数が黙って減る(または黙って増える)ことを tests/test_enforcement_ledger.py が禁止する。数を変える変更は、この台帳の明示的な更新を同じ変更セットに含めること。", + "defadr_files": 15, + "semgrep_rules": 229, + "adr_deftest_enforcements": 8, + "adr_defsemgrep_enforcements": 19, + "adr_laws": 49 +} diff --git a/docs/adr/semgrep-baseline.json b/docs/adr/semgrep-baseline.json new file mode 100644 index 00000000..5dbf3bdd --- /dev/null +++ b/docs/adr/semgrep-baseline.json @@ -0,0 +1,13 @@ +{ + "_comment": "ADR-DOE-ENFORCE-001 R3 の暫定 baseline。2026-07-14 の初回ゲート実行で検出された既存違反数。tests/test_semgrep_gate.py が「これより増えたら fail(新規違反)・減ったら fail(baseline を下げる記帳を強制)」の等値 ratchet を張る。目標は 0(T-B2 残作業: 違反解消の codex バッチ + 個別ルールの defsemgrep fixture 化)。", + "_breakdown_2026_07_14": { + "no-future-annotations": 24, + "doeff-no-typing-any-in-public-api": 10, + "doeff-no-print-in-core": 4, + "doeff-no-datetime-now-in-do": 4, + "doeff-no-sleep-in-tests": 2, + "no-silent-except-in-traceback": 1, + "no-silent-except-return-none": 1 + }, + "findings": 46 +} diff --git a/docs/doeff-2026-07-14-agent-first-investment-architecture-plan.md b/docs/doeff-2026-07-14-agent-first-investment-architecture-plan.md new file mode 100644 index 00000000..7104cb41 --- /dev/null +++ b/docs/doeff-2026-07-14-agent-first-investment-architecture-plan.md @@ -0,0 +1,162 @@ +# doeff 投資計画 2026-07-14 — agent-first 前提の能力消化と自己 enforcement + +> 出自: 2026-07-14 の doeff 価値評価(消費 14 リポジトリを 7 体の調査エージェントで横断監査)と、 +> それに続く maintainer との設計議論。前提は **書き手は常にエージェント、人間はレビュアーのみ**。 +> 反復パターン「力を作る速度が使う速度を上回る」(retry/budget/replay 文書化済み未使用、 +> deftest 配線済み未使用、VM oracle 実装済み未配線、semgrep 229 ルール未実行)への対処として、 +> 本計画は **新しい力ではなく、既にある力を安全に・安く使える状態にする**投資を優先する。 +> +> 裁定済み(2026-07-14 maintainer): +> - **A3 = guard-error 確定**(auto-bind 不採用。エラーメッセージは修正プロンプト形式) +> - **B3 = dev ビルドは doeff-vm-core feature `invariant-checks` + `python_bridge` 常時有効** +> - **ゲート正典はローカル pytest**(GitHub CI は予算により停止中 — 再有効化は選択肢にない) + +## Source of truth + +| 種別 | パス | 内容 | +|---|---|---| +| defadr | `docs/adr/defadr_doeff_hy_001_statement_bind_guard.hy` | ADR-DOE-HY-001: statement 位置 bind guard(Track A) | +| defadr | `docs/adr/defadr_doeff_enforce_001_pytest_canonical_gate.hy` | ADR-DOE-ENFORCE-001: pytest 正典ゲート(Track B) | +| defadr | `docs/adr/defadr_doeff_core_002_lazy_creation_trace.hy` | ADR-DOE-CORE-002: 生成トレース遅延化(Track C、enforcement は C1 後) | +| defadr | `docs/adr/defadr_doeff_hy_002_deftest_expressiveness.hy` | ADR-DOE-HY-002: deftest 表現力契約(Track D) | +| defadr | `docs/adr/defadr_doeff_domain_001_vocabulary_cohesion.hy` | ADR-DOE-DOMAIN-001: defdomain / SEDA / 適合検査(Track E) | +| 理論文書 | `docs/22-capability-classes.md` | class 0–3 権力分類・採用損益規則・anti-pattern(2026-07-14 新規) | +| 既存主張 | `docs/20-why-effects-over-di.md` | DI 対比の主張(22 番が理論で補完) | +| 監査 | `docs/crystallization/erosion-audit-2026-07-02.md` | enforcement 侵食監査(本計画 Track B の根拠) | +| 既存 issue | `ci-wire-enforcement-layer` / `doeff-adr-wiring-selfcheck` | 監査由来の配線 issue(Track B に吸収) | +| WIP 資産 | `packages/doeff-effect-analyzer/README.md`, `specs/effect-analyzer/` | SEDA(Track E2 の土台) | +| 下流証拠 | proboscis-ema VAULT『doeff Tracing Overhead … 3.7x』/ ISSUE-TRD-185/186 | Track C の実測根拠、scheduler リーク事故 | +| 下流証拠 | mediagen `tests/support/run_test.py` / ADR-008 §5 | Track D の違反シグナル(シム再発明) | +| 下流証拠 | agent-control-plane ADR 0056 / 0031 / erosion-audit 核#8 | Track A の gotcha 実証、Track E の増殖実害 | + +## Current state(観測事実と Gap) + +| # | 事実 / Gap | 根拠 | +|---|---|---| +| F-1 | ~~statement 位置素通し~~ **解消(T-A1、2026-07-14)**: 4 emit サイトに guard 実装、HY-001 green、1094 テスト無回帰 | macros.hy(_wrap-statement-guard) | +| F-2 | pyproject testpaths に docs/adr 不在 → defadr 15 本は既定 pytest で不可視。**flip は E1 まで保留**(DOMAIN-001 の意図的 red のため) | pyproject.toml:80-86 | +| F-3 | ~~oracle 起動経路ゼロ~~ **解消(T-B3、2026-07-14)**: dev ビルド常時有効 + hard-fail pytest 常駐(実測 True) | Makefile / tests/test_vm_invariant_checks_enabled.py | +| F-4 | ~~semgrep 手動のみ~~ **ゲート稼働(T-B2、2026-07-14)**: 既定 pytest で fail-closed 実行 + baseline 46 等値 ratchet。残: 違反解消・fixture 化・dev 依存化 | tests/test_semgrep_gate.py / docs/adr/semgrep-baseline.json | +| F-5 | deftest は最大 Python 消費者(mediagen)で使用 0 / 260。**:env 経路は doeff 側では機能する**と実証済み(HY-002 roundtrip green)— 残ギャップは消費側配線と語彙拡張(D2) | mediagen conftest / docs/adr/conftest.py | +| Gap-1 | ~~docs/adr に conftest.py が無い~~ **解消済み(Stage 0)**: `docs/adr/conftest.py` を新設(:env を reader で反映・エラー再送出。ADR-DOE-HY-002 R3 の参照実装) | 2026-07-14 実装 | +| Gap-2 | 新規 enforcement の実測(2026-07-14): **red 4**(HY-001 guard = DID NOT RAISE / ENFORCE-001 testpaths / ENFORCE-001 oracle / DOMAIN-001 defdomain)+ **green 6**(adr_contract ×5 + **HY-002 :env roundtrip green** — deftest→fixture→Ask の受け渡しは doeff 側で機能しており、mediagen の NotImplementedError は消費側配線の問題と確定)。収集は docs/adr 全 15 ファイル 39 テスト・エラーゼロ。テストパス配線(T-B1)までは `uv run pytest docs/adr` の明示実行でのみ見える | `uv run pytest docs/adr -q` 2026-07-14 | +| Gap-3 | ~~CORE-002 enforcement blocked~~ **解消(T-C1、2026-07-14)**: 現行 main に per-effect 捕捉は存在しないと確定。回帰ガード deftest green。下流 TRD-002 は旧版への計測 — pin 更新後の再検証を下流へ差し戻し(issue に追記済み) | C1 spike / CORE-002 enforcement | +| Gap-4 | `doeff-main-with-handler-stack`(489MB 姉妹 checkout)は 2026-07-11 に main へ完全マージ済みの残骸と判明 — 計画対象外、削除は maintainer 判断 | git merge-base 検証 2026-07-14 | + +## 依存関係(ASCII) + +``` + [B: pytest 正典ゲート] ←—— ゲートが無ければ全 enforcement は飾り + B1 testpaths+自己検査 + B2 defsemgrep 化 B3 VM oracle 配線(裁定済) + B4 台帳 ratchet + │ (ゲート成立) + ┌──────────────┼──────────────────┬───────────────┐ + ▼ ▼ ▼ ▼ +[A: bind guard] [C: trace(再スコープ)] [D: deftest 契約] [E: defdomain/SEDA/適合] + A1 ✅ 実装済み C1 ✅ 税は不存在 D1 ✅ 大半完了 E1 defdomain(設計=frontier) + A2 下流洗い出し C2 lookup_line 一行 D2 シム語彙吸収 E2 SEDA CLI/MCP + (A3 裁定済み) C3 裁定(優先度低) D3 mediagen 移行 E3 適合検査 + (下流) (B 完了が前提: R4) + │ + ▼ +[将来(本計画外): class 2 新プリミティブ — rate-limit scheduler / batching / hedging] +``` + +## Completion gates(計画全体の完了条件) + +1. `uv run pytest`(既定 testpaths)が docs/adr の全 defadr enforcement を収集・実行する(ENFORCE-001 law)。 +2. 本計画で追加した red enforcement 4 本が green(HY-001 guard / ENFORCE-001 testpaths+oracle / DOMAIN-001 defdomain)。 +3. ~~ADR-DOE-CORE-002 に実測に基づく enforcement を追加~~ **達成(2026-07-14)**: 回帰ガード green。残: T-C2 の一行修正と下流 TRD-002 の再検証。 +4. ADR-DOE-HY-002 の :env roundtrip が green、かつ mediagen の run_test.py が deftest 語彙で置換可能なことを下流で実証。 +5. 全 5 ADR の :status が "accepted" に昇格(enforcement green が条件)。 +6. semgrep 229 ルール+VM oracle が「バイナリ/feature 不在 = hard fail」で既定ゲートに常駐。 + +## Master TODO + +| ID | 出典 ADR | 作業 | red 反例(現状) | green 機構 | 状態 | +|---|---|---|---|---|---| +| T-B1 | ENFORCE-001 R2 | testpaths に docs/adr 追加 + defadr 収集自己検査を doeff-adr に実装 | test-…-docs-adr-in-default-testpaths が red | 同 deftest green + 自己検査テスト | 自己検査=codex 委譲待ち。**flip は E1 まで保留**(DOMAIN-001 の意図的 red が既定ゲートに入るため。代替: xfail(strict) マーキングの裁定) | +| T-B2 | ENFORCE-001 R3 | .semgrep.yaml を既定 pytest ゲートで実行 | 手動 make のみ・skip 可能 | **ゲート稼働(2026-07-14)**: tests/test_semgrep_gate.py が 229 ルールを fail-closed で実行。初回実行で**既存違反 46 件検出** → baseline 等値 ratchet(docs/adr/semgrep-baseline.json に内訳)。残: 46 件解消バッチ(codex — no-future-annotations 24 は前方参照リスクあり)、defsemgrep fixture 化、semgrep の dev 依存化 | ゲート稼働・残作業 codex | +| T-B3 | ENFORCE-001 R4 | dev ビルド invariant-checks 常時有効 + oracle の pytest 起動 | test-…-vm-oracle-wired が red | **完了(2026-07-14)**: doeff-vm に feature 転送、make sync に --features、PyO3 で invariant_checks_enabled() 公開(doeff_vm/__init__.py 再輸出込み)、tests/test_vm_invariant_checks_enabled.py が hard-fail 常駐(実測 True)。oracle は全 VM 実行で常時演習される。make test-vm-invariants 追加 | **完了** | +| T-B4 | ENFORCE-001 R5 | enforcement 台帳 ratchet メタテスト | 台帳の黙減が検出されない | **完了(2026-07-14)**: docs/adr/enforcement-ledger.json(defadr 15 / semgrep 229 / deftest 8 / defsemgrep 19 / law 49)+ tests/test_enforcement_ledger.py 等値 ratchet green | **完了** | +| T-A1 | HY-001 R1-R4 | statement 位置 runtime guard 実装(修正プロンプト形式メッセージ) | test-…-bare-statement-program-raises が red | **完了(2026-07-14)**: macros.hy に _guard-statement-value / _wrap-statement-guard を実装、defk(_build-fn-with-contracts)/do!/defp(_build-defp)/deftest の4 emit サイトに適用。HY-001 green、既定スイート 1094 passed 無回帰 | **完了** | +| T-A2 | HY-001 R5 | 下流(ema/ACP/mediagen/reactor 系)で guard 有効化 → 休眠 discard 全数洗い出し・修正 | 休眠 bare form 数 未知 | 各リポジトリのスイート green + 洗い出し報告。**⚠️ Hy バイトコード罠**: マクロ変更はソース mtime に映らないため、掃引は必ず `PYTHONPYCACHEPREFIX=` か touch で全再コンパイルして走らせること(doeff 自身の検証でこの罠を実踏) | 未着手(codex、リポジトリ毎) | +| T-A4 | HY-001 付随 | CPS の鋭い縁(ネスト try 内 `<-`、closure 内 bind)の解消 or 明示エラー化 | ACP ADR0056 facts の回避イディオム | マクロ改善 or 展開時エラー + テスト | 未着手 | +| T-C1 | CORE-002 R3 | profiling spike: 現行 main の捕捉サイト・コスト内訳の確定、ADR facts 更新 | Gap-3(サイト未特定) | **完了(2026-07-14)**: 現行 main に per-effect 捕捉は存在しない(opt-in effect + エラー経路のみ、linecache は描画時限定。100k Ask 実測で linecache/stack-walk/整形 = 0 呼び出し)。ADR facts 更新済み + 回帰ガード deftest green。**下流 3.7x は旧版への計測 — pin 更新後の再検証を下流 issue に**。副産物: @do の generator 再構築グルーが Python 側時間の ~39%(別 issue 種、本 ADR 対象外) | **完了** | +| T-C2 | CORE-002 R2(再スコープ) | run.py の _merge_python_frames に lookup_line=False(例外経路の字義ギャップ解消、一行) | 例外1回につき linecache 先読み | 一行修正 + 既存テスト green | 未着手(codex、小) | +| T-C3 | CORE-002 R3(再スコープ) | GetTraceback/GetExecutionContext の VM 組み込み vs observability handler 化 — perf 根拠は消滅、可観測性設計論のみで裁定 | — | frontier+人間の裁定 | 裁定待ち(優先度低下) | +| T-D1 | HY-002 R2/R3 | deftest params 忠実受け渡し + docs/adr conftest fixture(:env 反映の参照実装) | — | :env roundtrip **green 済み(Stage 0 実測)**。残作業 = fixture 契約の文書化 + 未対応 params の hard fail | 大半完了 | +| T-D2 | HY-002 R1 | mediagen run_test.py の要求(部分スタック+差し替え)を deftest 語彙へ吸収 | シムの存在自体 | 語彙追加 + 等価性デモテスト | T-D1 後 | +| T-D3 | HY-002 R1 受入 | mediagen テストのパイロット deftest 移行(下流 dogfood) | mediagen deftest 0/260 | 下流 PR(mediagen ADR-008 residual 消化) | T-D2 後 | +| T-E1 | DOMAIN-001 R1 | defdomain マクロ設計・実装 | test-…-defdomain-exists が red | 同 deftest green(挙動ピンに昇格) | 設計=frontier | +| T-E2 | DOMAIN-001 R2 | SEDA を CLI/MCP としてエージェント照会可能に(WIP 完成) | 照会面なし | seda CLI + MCP tool + テスト | T-E1 と並行可 | +| T-E3 | DOMAIN-001 R3 | 適合検査 3 種(被覆・部分集合・同義語彙禁止) | ACP 核#8 型の増殖が素通り | 適合検査テスト green | T-E1+B 完了後 | + +## 段階計画 + +- **Stage 0(完了 / 本セッション)**: 価値評価・理論文書(22 番)・本計画・5 defadr(red enforcement 4 本 + blocked 1 件)作成。A3/B3 裁定記録。 +- **Stage 1(即時並行、codex 委譲可)**: T-B1 → T-B2 → T-B4、T-C1、T-D1。B1 完了時点で red 4 本が既定ゲートに入るため、**B1 の着地は T-A1/T-B3 の green 化と同一変更セットにするか、直前に順序付ける**(既定ゲートを恒常 red にしない)。 +- **Stage 2**: T-A1(frontier 設計 → codex 実装)→ T-A2(下流展開、リポジトリ毎に codex)。 +- **Stage 3**: T-D2 → T-D3(下流 PR)。T-C2(C1 の結果を受けて)。 +- **Stage 4(設計セッション)**: T-E1 defdomain 設計 + T-C3 裁定を同一 frontier セッションで実施 → T-E2/T-E3 実装。 +- **Stage 5(本計画外・次期計画)**: class 2 新プリミティブ(rate-limit token-bucket scheduler → batching/dedup/hedging)。前提 = 本計画の Gate 1-6。 + +## Subagent spawn strategy + +| 役割 | タスク | スコープ | 並列組 | 期待出力 | 検証コマンド | 権限 | model | +|---|---|---|---|---|---|---|---| +| worker | T-B1 testpaths+自己検査 | pyproject.toml, packages/doeff-adr | G1 | diff + green 自己検査 | `uv run pytest docs/adr -q` | edit | sonnet | +| worker | T-B2 defsemgrep 化 | .semgrep.yaml, docs/adr, pyproject dev-deps | G1 | defsemgrep 群 + hard-fail 挙動 | `uv run pytest docs/adr -q`(semgrep 有/無 両方) | edit | sonnet | +| worker | T-B4 台帳 ratchet | docs/adr, tests | G1 | ratchet メタテスト | seeded-violation で red 確認 | edit | sonnet | +| worker | T-C1 profiling spike | doeff/, packages/doeff-core-effects(read)+ scratch | G1 | 捕捉サイト特定レポート + コスト内訳 | py-spy / cProfile 数値 | read+scratch | sonnet | +| worker | T-D1 params 受け渡し+fixture | packages/doeff-hy, docs/adr/conftest.py | G1 | :env roundtrip green | `uv run pytest docs/adr -q` | edit | sonnet | +| 設計 | T-A1 guard 設計(メッセージ仕様込み) | macros.hy | G2 | 設計メモ → 実装指示 | — | read | **frontier(inline)** | +| worker | T-A1 実装 | packages/doeff-hy | G2 | guard + green enforcement | `uv run pytest docs/adr packages/doeff-hy -q` | edit | sonnet | +| worker | T-A2 下流洗い出し(リポジトリ毎 1 体) | 各消費リポジトリ | G3 | 休眠 discard 一覧 + 修正 PR | 各リポジトリのスイート | edit(worktree) | sonnet | +| 設計 | T-E1 defdomain / T-C3 裁定 | macros.hy, SEDA specs | G4 | ADR 更新 + 実装仕様 | — | read | **frontier+人間** | +| verifier | 各 Stage 末の cross-link / 収集確認 | docs/, pyproject | 各 G 末 | 収集数・red/green 表 | `uv run pytest --collect-only -q docs/adr` | read | sonnet | + +制約: worker は commit 禁止(親が diff をレビューして単一変更セットで確定)。doeff への着地は orch 経由 +(CLAUDE.md 書込権限規則)。マクロ層(macros.hy)と VM は結合核 — sonnet worker は決定済み仕様の実装のみ、 +方針判断が必要になったら改変せず報告で戻す。 + +## Non-goals + +- GitHub Actions の再有効化(予算制約、maintainer 裁定 2026-07-14)。 +- auto-bind の導入(A3 裁定で不採用。再検討するとしても T-A2 完了後の別 ADR)。 +- class 2 新プリミティブ(rate-limit/batching/dedup/hedging)の実装 — 本計画の Gate 達成が前提の次期計画。 +- 新規サブパッケージの追加、Haskell client の拡張、multi-shot 方向の検討。 +- `doeff-main-with-handler-stack` checkout の削除(マージ済み残骸と確認済みだが、削除は maintainer 自身が行う)。 +- 本番・下流リポジトリのデプロイ/ライブ系操作。 + +## 進捗記録 2026-07-14(Stage 1–2 実施) + +同日の goal セッションで以下を実施(すべてローカル編集 — 着地は orch 経由): + +- **T-A1 完了**: statement bind guard 実装(macros.hy)。HY-001 red→green、既定スイート 1094 passed 無回帰。 + エラーメッセージは修正プロンプト形式(`Fix: bind it — (<- _ ) — ...`)。 +- **T-B3 完了**: VM conformance oracle を dev 既定に(B3 裁定の実装)。`invariant_checks_enabled()` 実測 True。 +- **T-B4 完了**: enforcement 台帳 + 等値 ratchet 稼働。 +- **T-B2 ゲート稼働**: 229 ルールが既定 pytest で fail-closed 実行。初回実行が**既存違反 46 件**を検出 + (no-future-annotations 24 / no-typing-any 10 / no-print-in-core 4 / **no-datetime-now-in-do 4** / + no-sleep-in-tests 2 / silent-except 2)— baseline 等値 ratchet で封じ込め、解消は codex バッチへ。 +- **T-C1 完了・C トラック再スコープ**: 現行 main にトレース税は存在しない(捕捉は opt-in effect + + エラー経路のみ)。CORE-002 は「修正」から「回帰ガード」へ書き換え済み、ガード green。 + 下流の 3.7x issue は旧版への計測 — pin 更新後の再検証を下流に差し戻す。 +- **docs/adr 全体**: 38 green + 意図した red 2(ENFORCE-001 testpaths = flip 保留、DOMAIN-001 defdomain = E1 待ち)。 + +新規・変更ファイル: packages/doeff-hy/src/doeff_hy/macros.hy(guard)、packages/doeff-vm/Cargo.toml・ +src/lib.rs・doeff_vm/__init__.py(oracle flag)、Makefile(sync features / test-vm-invariants)、 +tests/test_vm_invariant_checks_enabled.py、tests/test_enforcement_ledger.py、tests/test_semgrep_gate.py、 +docs/adr/enforcement-ledger.json、docs/adr/semgrep-baseline.json、docs/adr/conftest.py、defadr 5本、本計画。 + +## Immediate next action(/goal 実行者へ) + +1. Stage 1 の G1 バッチ(T-B1/T-B2/T-B4/T-C1/T-D1)を並行着手。ただし **T-B1 の着地順序に注意**: + docs/adr を testpaths に入れると red 4 本が既定ゲートに入るため、T-A1/T-B3 の green 化を同一 + 変更セットに含めるか、自己検査のみ先行させ testpaths 反映を最後に置く。 +2. 各変更は doeff の TDD+Semgrep 3 フェーズ手順(CLAUDE.md)に従い、着地は orch issue/run 経由。 +3. T-C1 の結果で ADR-DOE-CORE-002 の facts と enforcement を必ず更新する(blocked 解消)。 +4. 検証: `uv run pytest docs/adr -q` と `uv run pytest --collect-only -q docs/adr` を各ステップで実行し、 + red→green の遷移を PR 本文の Verification 表(issue 項目 1:1 対応)に記録する。 diff --git a/docs/index.md b/docs/index.md index 018e8664..782cc94d 100644 --- a/docs/index.md +++ b/docs/index.md @@ -56,6 +56,7 @@ Welcome to the comprehensive documentation for doeff - an algebraic effects syst ### Design Notes - **[Why Effects Over DI?](20-why-effects-over-di.md)** - Real-world use cases where algebraic effects beat dependency injection +- **[Capability Classes](22-capability-classes.md)** - A four-class taxonomy predicting where effects pay off, retry under one-shot continuations, and adoption anti-patterns - **[CLI Architecture](cli-run-command-architecture.md)** - Run command pipeline design - **[Filesystem Effects](filesystem-effect-architecture.md)** - Filesystem effect design (draft) - **[Abstraction Concern](abstraction_concern.md)** - Interpreter design discussion diff --git a/packages/doeff-hy/src/doeff_hy/macros.hy b/packages/doeff-hy/src/doeff_hy/macros.hy index f672a73b..b125a5ac 100644 --- a/packages/doeff-hy/src/doeff_hy/macros.hy +++ b/packages/doeff-hy/src/doeff_hy/macros.hy @@ -317,6 +317,56 @@ defk {name}: :post type annotation cannot be an empty string. result) +;; ADR-DOE-HY-001: statement-position bind guard -------------------------------- +;; +;; defk/do!/defp/deftest の本体で、束縛されず statement 位置に置かれた式が +;; Program(DoExpr)/EffectBase に評価された場合、それは「作られたが決して走らない」 +;; 計算であり、ほぼ確実にバグである(silent-passthrough)。マクロは statement 位置の +;; 式形フォームを _guard-statement-value でラップし、初回実行で決定的に fail させる。 +;; auto-bind は採用しない(ADR-DOE-HY-001 R2)。エラーメッセージは書き手エージェント +;; 向けの修正プロンプトである(R3)。 + +(setv _STATEMENT-FORM-HEADS + #{"setv" "setx" "import" "require" "assert" "del" "raise" "return" "global" + "nonlocal" "yield" "await" "for" "while" "with" "with/a" "when" "unless" + "if" "do" "cond" "try" "let" "match" "defn" "defn/a" "defclass" "defmacro" + "quote" "quasiquote" "unquote" "annotate" "<-" "!" "When" "lazy" "lazy-val" + "lazy-var" "set!"}) + +(defn _guard-statement-value [value owner line source] + "ADR-DOE-HY-001 R1 の実行時 guard。statement 位置で評価された値が + Program(DoExpr)/EffectBase なら RuntimeError を送出する。それ以外の値は + そのまま返す(R4: 非 Program 文は無傷)。" + (import doeff [DoExpr EffectBase]) + (when (isinstance value #(DoExpr EffectBase)) + (raise (RuntimeError + (+ owner " (line " (str line) "): statement-position expression evaluated to an " + "unperformed " (. (type value) __name__) " — a bare Program/EffectBase in " + "statement position never runs. " + "Fix: bind it — (<- _ " source ") — or make it the final body expression. " + "[ADR-DOE-HY-001]")))) + value) + +(defn _wrap-statement-guard [form owner] + "statement 位置の式形フォーム(関数呼び出し・シンボル)を _guard-statement-value で + ラップして返す。文形式(setv/for/with 等、_STATEMENT-FORM-HEADS)と非式フォームは + そのまま返す。owner はエラーメッセージ用の defk/do!/defp/deftest 名。" + (setv head (if (and (isinstance form hy.models.Expression) (> (len form) 0)) + (str (get form 0)) + None)) + (setv eligible + (or (isinstance form hy.models.Symbol) + (and (is-not head None) (not (in head _STATEMENT-FORM-HEADS))))) + (if eligible + (do + (setv line (or (getattr form "start_line" None) -1)) + (setv source + (try (.lstrip (hy.repr form) "'") + (except [Exception] (str form)))) + `(_guard-statement-value ~form ~(str owner) ~line ~source)) + form)) + + (defn _build-fn-with-contracts [decorators name params pre-checks post-checks real-body] "Build a defn form with pre/post assertion wrappers. Works for both plain functions (deff) and generator/kleisli functions (defk). @@ -340,7 +390,10 @@ defk {name}: :post type annotation cannot be an empty string. (if post-checks (let [post-asserts (lfor check post-checks (_expand-check check name "post-condition")) - init-forms (cut real-body 0 -1) + ;; ADR-DOE-HY-001: kleisli 本体の statement 位置を guard(最終式=返り値は対象外) + init-forms (if kleisli? + (lfor f (cut real-body 0 -1) (_wrap-statement-guard f name)) + (list (cut real-body 0 -1))) last-form (get real-body -1)] `(defn ~decorators ~name ~params ~@docstring-forms @@ -531,7 +584,7 @@ defk {name}: {{:post [...]}} is required. `(do))) `(do (import doeff.do [do :as _doeff_do]) - (import doeff-hy.macros [_guard-performed]) + (import doeff-hy.macros [_guard-performed _guard-statement-value]) ~lazy-imports ~fn-form (setv (. ~name __doeff_body__) '~real-body) @@ -620,8 +673,8 @@ defk {name}: {{:post [...]}} is required. (_expand-check check "do!" "pre-condition")) expanded (lfor bind bindings (if (and (isinstance bind tuple) (= (get bind 0) "__plain__")) - ;; Plain statement (setv, when, for, etc.) — emit as-is - (get bind 1) + ;; Plain statement — ADR-DOE-HY-001 guard(式形のみラップ) + (_wrap-statement-guard (get bind 1) "do!") ;; Effect binding — yield (let [#(name expr) (_bind-parts bind)] (if (is name None) @@ -631,7 +684,7 @@ defk {name}: {{:post [...]}} is required. (let [post-asserts (lfor check post-checks (_expand-check check "do!" "post-condition"))] `(do (import doeff.do [do :as _doeff-do]) - (import doeff-hy.macros [_guard-performed]) + (import doeff-hy.macros [_guard-performed _guard-statement-value]) ((_doeff-do (fn [] ~@pre-code ~@expanded @@ -641,7 +694,7 @@ defk {name}: {{:post [...]}} is required. ~@post-asserts) (return _contract_result)))))) `(do (import doeff.do [do :as _doeff-do]) - (import doeff-hy.macros [_guard-performed]) + (import doeff-hy.macros [_guard-performed _guard-statement-value]) ((_doeff-do (fn [] ~@pre-code ~@expanded @@ -1066,8 +1119,8 @@ defk {name}: {{:post [...]}} is required. (setv #(bindings body-expr) (_parse-do-body expanded-forms macro-name)) (setv expanded (lfor bind bindings (if (and (isinstance bind tuple) (= (get bind 0) "__plain__")) - ;; Plain statement (import, setv, when, etc.) — emit as-is - (get bind 1) + ;; Plain statement — ADR-DOE-HY-001 guard(式形のみラップ) + (_wrap-statement-guard (get bind 1) (str name)) ;; Effect binding — yield (let [#(bname expr) (_bind-parts bind)] (if (is bname None) @@ -1078,6 +1131,7 @@ defk {name}: {{:post [...]}} is required. `(do (import inspect) (import doeff.do [do :as _doeff_do]) + (import doeff-hy.macros [_guard-statement-value]) (defn _doeff_check_program_return [v msg mode] "Check Program return value. Raises TypeError on violation. Returns False (for assert)." (setv is-program (inspect.isgenerator v)) @@ -1239,9 +1293,9 @@ defk {name}: {{:post [...]}} is required. (if (is bname None) (.append gen-body `(yield ~expr)) (.append gen-body `(setv ~bname (yield ~expr))))) - ;; Everything else (assert, setv, when, for, print, etc.) — pass through + ;; Everything else — ADR-DOE-HY-001 guard(式形のみラップ、文はそのまま) True - (.append gen-body form))) + (.append gen-body (_wrap-statement-guard form (str name))))) ;; Build the test function (setv fn-params (+ [(hy.models.Symbol "doeff_interpreter")] fixture-params)) @@ -1291,9 +1345,11 @@ defk {name}: {{:post [...]}} is required. `(do (import pytest) (import doeff.do [do :as _doeff_do]) + (import doeff-hy.macros [_guard-statement-value]) (defn [~@decorators] ~name [~@fn-params] ~fn-body)) `(do (import doeff.do [do :as _doeff_do]) + (import doeff-hy.macros [_guard-statement-value]) (defn ~name [~@fn-params] ~fn-body)))) diff --git a/packages/doeff-vm/Cargo.toml b/packages/doeff-vm/Cargo.toml index 5aa23c3e..b1549f06 100644 --- a/packages/doeff-vm/Cargo.toml +++ b/packages/doeff-vm/Cargo.toml @@ -15,6 +15,9 @@ crate-type = ["cdylib"] [features] default = [] vm_debug_logs = [] +# ADR-DOE-ENFORCE-001 R4: VM conformance oracle (per-step runtime invariants). +# Dev builds enable this via `make sync`; release wheels ship without it. +invariant-checks = ["doeff-vm-core/invariant-checks"] [dependencies] pyo3 = { version = "0.28", features = ["py-clone"] } diff --git a/packages/doeff-vm/doeff_vm/__init__.py b/packages/doeff-vm/doeff_vm/__init__.py index d9172205..e6e6a997 100644 --- a/packages/doeff-vm/doeff_vm/__init__.py +++ b/packages/doeff-vm/doeff_vm/__init__.py @@ -33,3 +33,4 @@ TailEval = _ext.TailEval vm_live_counts = _ext.vm_live_counts +invariant_checks_enabled = _ext.invariant_checks_enabled diff --git a/packages/doeff-vm/src/lib.rs b/packages/doeff-vm/src/lib.rs index b0f83858..192ce2fe 100644 --- a/packages/doeff-vm/src/lib.rs +++ b/packages/doeff-vm/src/lib.rs @@ -34,5 +34,13 @@ fn doeff_vm(m: &Bound<'_, PyModule>) -> PyResult<()> { (c.live_segments, c.live_continuations, c.live_ir_streams) } + /// True when the VM was compiled with doeff-vm-core's `invariant-checks` + /// feature (the per-step runtime conformance oracle). Dev builds must + /// enable it (ADR-DOE-ENFORCE-001 R4); release wheels ship without it. + #[pyfn(m)] + fn invariant_checks_enabled() -> bool { + cfg!(feature = "invariant-checks") + } + Ok(()) } diff --git a/tests/test_enforcement_ledger.py b/tests/test_enforcement_ledger.py new file mode 100644 index 00000000..c374504b --- /dev/null +++ b/tests/test_enforcement_ledger.py @@ -0,0 +1,41 @@ +"""ADR-DOE-ENFORCE-001 R5: enforcement 台帳の anti-drop ratchet。 + +orch の SpecInventorySpec の pytest 版。enforcement 資産(defadr ファイル数・ +.semgrep.yaml ルール数・ADR 内 deftest/defsemgrep/law 数)の実数が台帳 +docs/adr/enforcement-ledger.json と厳密一致しなければ fail する。 + +- 実数 < 台帳: enforcement の黙った喪失(侵食)— 削減の意図があるなら台帳を + 同じ変更セットで下げ、理由を ADR に記録すること。 +- 実数 > 台帳: 追加の記帳漏れ — 台帳を上げること(増加も明示的に)。 +""" + +import json +import re +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +LEDGER = ROOT / "docs" / "adr" / "enforcement-ledger.json" + + +def _actual_counts() -> dict: + adr_files = sorted((ROOT / "docs" / "adr").glob("defadr_*.hy")) + adr_text = "".join(p.read_text() for p in adr_files) + semgrep_text = (ROOT / ".semgrep.yaml").read_text() + return { + "defadr_files": len(adr_files), + "semgrep_rules": len(re.findall(r"^ - id:", semgrep_text, re.MULTILINE)), + "adr_deftest_enforcements": adr_text.count("(deftest "), + "adr_defsemgrep_enforcements": adr_text.count("(defsemgrep "), + "adr_laws": adr_text.count("(law "), + } + + +def test_enforcement_inventory_matches_ledger(): + ledger = {k: v for k, v in json.loads(LEDGER.read_text()).items() if not k.startswith("_")} + actual = _actual_counts() + assert actual == ledger, ( + f"enforcement 台帳と実数が不一致 — ADR-DOE-ENFORCE-001 R5。\n" + f" 台帳: {ledger}\n 実数: {actual}\n" + f"減少 = 侵食(意図的なら台帳とADRを同時更新)、増加 = 記帳漏れ(台帳を上げる)。" + f" 台帳: {LEDGER}" + ) diff --git a/tests/test_semgrep_gate.py b/tests/test_semgrep_gate.py new file mode 100644 index 00000000..ab46127f --- /dev/null +++ b/tests/test_semgrep_gate.py @@ -0,0 +1,55 @@ +"""ADR-DOE-ENFORCE-001 R3: .semgrep.yaml の全ルールを既定 pytest ゲートで実行する。 + +`make lint-semgrep` と同一の走査(--config .semgrep.yaml doeff/ packages/)を pytest から +起動する集約ゲート。Makefile 版との違い: +1. fail-closed — semgrep バイナリ不在は skip ではなく hard fail(偽緑の禁止 — ACP で + 「初回実行時に 16 検査が即 red(バイナリ不在)」が起きた失敗様式の再発防止)。 +2. baseline 等値 ratchet — 2026-07-14 の初回実行で既存違反 46 件が見つかったため + (docs/adr/semgrep-baseline.json に内訳)、違反数が baseline より「増えたら fail + (新規違反)・減ったら fail(baseline を下げる記帳を強制)」。目標は 0。 + +個別ルールの hit/clean fixture 化(defsemgrep installed-rule 形式)と既存違反の解消 +バッチは T-B2 の残作業(codex 委譲)— 本ゲートはその間も 229 ルールが実際に走り、 +後退しないことを保証する。 +""" + +import json +import shutil +import subprocess +from pathlib import Path + +import pytest + +ROOT = Path(__file__).resolve().parents[1] +BASELINE = ROOT / "docs" / "adr" / "semgrep-baseline.json" + + +@pytest.mark.semgrep +@pytest.mark.slow +def test_semgrep_findings_match_baseline_ratchet(): + binary = shutil.which("semgrep") + assert binary is not None, ( + "semgrep バイナリが見つからない — ADR-DOE-ENFORCE-001 R3 は skip を禁止する。" + "`uv tool install semgrep` でインストールすること。" + ) + proc = subprocess.run( + [binary, "--config", ".semgrep.yaml", "doeff/", "packages/", "--error", "--quiet", "--json"], + cwd=ROOT, + capture_output=True, + text=True, + timeout=600, + check=False, # 違反有無は returncode でなく results 数で判定する(baseline ratchet) + ) + results = json.loads(proc.stdout)["results"] + baseline = json.loads(BASELINE.read_text())["findings"] + actual = len(results) + if actual > baseline: + newest = [f"{r['check_id']} {r['path']}:{r['start']['line']}" for r in results][-10:] + raise AssertionError( + f"semgrep 違反が baseline を超過: {actual} > {baseline} — 新規違反を修正すること" + f"(ADR-DOE-ENFORCE-001 R3)。直近の検出例:\n" + "\n".join(newest) + ) + assert actual == baseline, ( + f"semgrep 違反数 {actual} < baseline {baseline} — 改善を記帳すること: " + f"{BASELINE} の findings を {actual} に下げる(黙った基準緩みの防止)。" + ) diff --git a/tests/test_vm_invariant_checks_enabled.py b/tests/test_vm_invariant_checks_enabled.py new file mode 100644 index 00000000..e83ae430 --- /dev/null +++ b/tests/test_vm_invariant_checks_enabled.py @@ -0,0 +1,22 @@ +"""ADR-DOE-ENFORCE-001 R4: dev ビルドは VM conformance oracle(invariant-checks)を常時有効にする。 + +B3 裁定(2026-07-14): oracle は cargo feature `invariant-checks` 配下の per-step 実行時検査であり、 +このフラグが有効なら pytest スイート全体の VM 実行がそのまま oracle の演習になる。 + +このテストは skip しない — invariant-checks 無効ビルドに対しては hard fail する(偽緑の禁止、 +ADR-DOE-ENFORCE-001 law `default-pytest-sees-all-enforcement`)。release wheel を相手に +スイートを走らせた場合に落ちるのは意図した挙動である。 +""" + +import doeff_vm + + +def test_vm_built_with_invariant_checks(): + assert hasattr(doeff_vm, "invariant_checks_enabled"), ( + "doeff_vm.invariant_checks_enabled が存在しない — VM バイナリが古い。" + "`make sync` で再ビルドすること(stale Rust VM build; CLAUDE.md の警告参照)" + ) + assert doeff_vm.invariant_checks_enabled(), ( + "VM が invariant-checks 無効でビルドされている — ADR-DOE-ENFORCE-001 R4(B3 裁定 2026-07-14)。" + "`make sync`(= maturin develop --release --features invariant-checks)で再ビルドすること" + )