# Session: Architecture
# Full Title: Step 49.2: Strengthen Core Contracts (@eidos-engineer subagent)
# ID: ses_3a8b58a61ffebsS36weTywwc8w
# Date: 2026-02-13 13:57 UTC
# Total Messages: 70

---

## User

# Task: Step 49.2 — Strengthen Core Contracts (Batch 1)

You are `eidos-engineer` working on **Project Eidos** in `/Users/tefx/Projects/Edios`.

## CRITICAL: Read AGENTS.md Rules
This project has strict rules in `/Users/tefx/Projects/Edios/AGENTS.md`. Key rules:
- **Work Unit Protocol**: CODE → VERIFY → per behavioral change
- **No TODO comments** in committed code
- **Honest incompleteness**: Use `...` for unimplemented, never `pass` or fake returns
- **type: ignore discipline**: See AGENTS.md Law 1
- **Pre-Commit Gate**: Run `uv run pytest` and verify before completing

## Your Task

Strengthen semantic contracts (`@pre`/`@post` + doctests) on 4 core files. Replace type-check-only contracts with meaningful semantic invariants.

### Target Files (Core / Pure Logic)
1. `packages/eidos-mainframe/src/eidos_mainframe/core/session/state_machine.py`
2. `packages/eidos-mainframe/src/eidos_mainframe/core/session/projection.py`
3. `packages/eidos-mainframe/src/eidos_mainframe/core/assembler/assembler.py`
4. `packages/eidos-mainframe/src/eidos_mainframe/core/assembler/l1_builders.py`

### Reference Document
Read `docs/bug-hunt/contract-target-set.md` first — it contains the invariant mapping for each file with specific candidate invariants (SM-1 through SM-7, PJ-1 through PJ-10, ASM-1 through ASM-8, L1B-1 through L1B-7).

### What To Do For Each File

1. **Read the file** to understand current contracts
2. **Read the invariant mapping** from `docs/bug-hunt/contract-target-set.md`
3. **Add/strengthen `@pre`/`@post` decorators** using the `deal` library (already used in the project)
4. **Add doctests** where appropriate to document and verify invariants
5. **Add property tests** (Hypothesis) where the mapping says "property" — place in appropriate test files

### Important Constraints
- Use `deal.pre` and `deal.post` decorators (the project already uses them)
- Do NOT add `@pre`/`@post` to async functions (deal doesn't support async well — use compliance tests instead)
- Focus on HIGH priority invariants first
- Do NOT break existing tests
- Do NOT add TODO comments
- Each file is one work unit — verify after each

### Verification
After modifying each file:
```bash
uv run invar guard
uv run pytest tests/ -x -q --timeout=60
```

After ALL files are done:
```bash
uv run invar guard --all --strict
uv run pytest -v --timeout=120
```

### Return Format
Return:
1. Summary of contracts added per file (count and which invariant IDs)
2. `uv run invar guard` output
3. `uv run pytest` final output (pass/fail count)
4. Any issues encountered

### Check-In Format
Start by reading `docs/bug-hunt/contract-target-set.md` and the 4 target files.

---

## Assistant (internal reasoning)

Let me start by reading the contract target set document and understanding the current state of the project. I'll also check the plan status.

---

## Assistant

Now let me read the contract target set document and the 4 target files in parallel:


Good, I have a clear picture of the codebase and the invariant mappings. Let me check the deal API availability and existing test structure before implementing:

---

## Assistant (internal reasoning)

Let me use `uv run python` instead.

---

## Assistant

Now let me check where property tests should go and understand the `SessionStatus` enum, then look at the existing l1_builders tests:


Now I have a comprehensive understanding. Let me do the Check-In:

✓ Check-In: Edios | main | dirty (pre-existing changes)
  Step: 49.2 — Strengthen Core Contracts (Batch 1)
  Todolist: NOT NEEDED (4 files, one work unit each — manageable)

Let me now implement contracts for each of the 4 files. Starting with File 1: `state_machine.py`.

## Work Unit 1: state_machine.py


I'm on track. Let me implement state_machine.py contracts — SM-1, SM-2, SM-4 source contracts + SM-7 doctests:


The LSP errors are just pyright not resolving virtual env — the imports work fine at runtime. Let me add the contracts:


Let me re-read the file to see the current state:


Now let me apply all the changes to `state_machine.py`:


Good. Now add SM-1 `@post` on `can_transition` (result is bool) and SM-4 `@post` on `status_to_event_type`:


Now add SM-4 `@post` on `status_to_event_type`:


Now add the `_TERMINAL_STATUSES` export and property tests (SM-3, SM-5, SM-6). First let me add the property tests to the test file:


The private import will work fine at runtime — LSP is being overly strict. Now add the property tests at the end of the test file:


Now add the property tests after the last test class:


Now verify state_machine.py changes work:


Good, all imports work. Now run the tests for this file:


Interesting — SM-6 doesn't hold for INTERRUPTED state (INTERRUPTED → only RUNNING). This is by spec design — INTERRUPTED requires recovery first. Let me fix the property test to document this accurately:


32 passed. Now let me also run doctest for state_machine.py:


Work Unit 1 complete — state_machine.py ✅ (SM-1, SM-2, SM-4 as @post/@ensure, SM-7 as doctests, SM-3/SM-5/SM-6 as property tests). 

## Work Unit 2: projection.py

Now let me strengthen contracts on projection.py. The key invariants are PJ-1, PJ-2, PJ-3, PJ-4, PJ-5, PJ-6, PJ-7, PJ-8, PJ-9, PJ-10.


Now let me add the semantic contracts to projection.py. Key targets: PJ-1 through PJ-6 as `@ensure` contracts, PJ-7/PJ-8 as property tests.


Now add PJ-1 through PJ-4 semantic @ensure contracts on the _apply_* functions:


Now PJ-2 for _apply_session_completed, _apply_session_failed, _apply_session_cancelled:


Now PJ-3 for _apply_session_interrupted:


Now PJ-4 for _apply_session_recovered:


Now PJ-5 and PJ-6 on `apply_event`:


Now let me add PJ-9 contract on _apply_user_message. Since deal.ensure can't capture pre-mutation state on mutable objects, I'll add a minimum bound check and rely on the existing doctest for strict increment:


Now add PJ-7/PJ-8 property tests and PJ-10 compliance test:


Now add the property tests at the end of the file:


Now verify projection.py changes:


SESSION_CREATED is in `_EVENT_TO_STATUS` mapping but not in `_EVENT_HANDLERS` because session creation is handled separately (before apply_event). Fix the test:


50 passed, 0 failed ✅. Work Unit 2 complete.

## Work Unit 3: assembler.py

Target invariants: ASM-2, ASM-3, ASM-4, ASM-5 (high priority), ASM-1, ASM-6, ASM-7, ASM-8.


Now let me add the sync contracts to assembler.py. ASM-2 is the key one — build_l1's first component must be 'identity':


Now add ASM-1 (@post on build_l0 component kinds) and ASM-2 (@post on build_l1 identity first):


Now add ASM-2 on build_l1:


Now add ASM-7 doctest for empty string on _estimate_tokens:


ASM-3, ASM-4, ASM-5, ASM-8 are for async functions — I'll add compliance tests. Let me verify what we have so far:


All assembler tests pass. Now let me check if there are existing compliance tests for assembler and create the async invariant tests. Let me find the right place:


Good. Let me create compliance tests for the async invariants (ASM-3, ASM-4, ASM-5):


Work Unit 3 complete — assembler.py ✅ (ASM-1, ASM-2 as @post, ASM-7 as doctest, ASM-3/ASM-4/ASM-5 as compliance tests).

## Work Unit 4: l1_builders.py

Now add semantic contracts for L1B-1, L1B-3, L1B-4, L1B-5, L1B-7:


Now add semantic `@ensure` contracts to l1_builders.py:


Now add L1B-1 and L1B-7 contracts on add_identity_component. deal.ensure receives the function arguments and result, so we can check the last appended element:


---

## 1. The Anchor
- **Original Goal:** Step 49.2 — Strengthen semantic contracts (`@pre`/`@post`/`@ensure` + doctests + property tests) on 4 core files, replacing type-check-only contracts with meaningful semantic invariants.
- **Current Sub-Task:** Adding `@ensure` contracts to `l1_builders.py` (file 4 of 4) — partially done, mid-edit.

## 2. Instruction Ledger (MUST/NEVER)
- MUST: Use `deal.pre`, `deal.post`, `deal.ensure` decorators (project already uses them)
- MUST: Do NOT add `@pre`/`@post` to async functions — use compliance tests instead
- MUST: Focus on HIGH priority invariants first
- MUST: Follow Work Unit Protocol: CODE → VERIFY per behavioral change
- NEVER: Add TODO comments in committed code
- NEVER: Use `pass` or fake returns — use `...` for unimplemented
- MUST: Run `uv run invar guard` and `uv run pytest` for verification (note: `--timeout` flag not supported)
- MUST: Read `docs/bug-hunt/contract-target-set.md` for invariant IDs (SM-1..SM-7, PJ-1..PJ-10, ASM-1..ASM-8, L1B-1..L1B-7)

## 3. Project State & Resume
- **Phase:** phase-49-contract-strengthening (Contract Strengthening 合同意义化)
- **Active Step:** 49.2 — Strengthen Core Contracts (Batch 1) (claimed by eidos-engineer)
- **Blockers:** None
- **Resume:** `vectl show phase-49-contract-strengthening` — then finish l1_builders.py contracts, run final verification

## 4. Context Graph (Files)

### 🔥 Active (Modified):
- `packages/eidos-mainframe/src/eidos_mainframe/core/session/state_machine.py` — ✅ DONE: Added SM-1 (`@post` bool), SM-2 (`@ensure` terminal→empty), SM-4 (`@post` SessionEventType), SM-7 (doctests), `_TERMINAL_STATUSES` constant, `ensure` import
- `packages/eidos-mainframe/src/eidos_mainframe/core/session/projection.py` — ✅ DONE: Added PJ-1 (`@ensure` RUNNING+IDLE after start), PJ-2 (`@ensure` terminal after completed/failed/cancelled), PJ-3 (`@ensure` INTERRUPTED after interrupt), PJ-4 (`@ensure` RUNNING+recovered after recovery), PJ-5/PJ-6 (`@ensure` timestamp/event_id sync on apply_event), PJ-9 (`@ensure` turn_count≥1 + doctests)
- `packages/eidos-mainframe/src/eidos_mainframe/core/assembler/assembler.py` — ✅ DONE: Added ASM-1 (`@post` L0 component kinds), ASM-2 (`@post` identity first in L1), ASM-7 (doctests for empty string), `ensure` import
- `packages/eidos-mainframe/src/eidos_mainframe/core/assembler/l1_builders.py` — 🔧 IN PROGRESS: Added L1B-1 (`@ensure` identity kind) and L1B-7 (`@ensure` len sync) on `add_identity_component`. Still need L1B-3, L1B-4, L1B-5 on `add_skill_index_component`, `add_core_skill_component`, `add_toolset_component`
- `tests/unit/mainframe/core/session/test_state_machine.py` — ✅ DONE: Added SM-3/SM-5/SM-6 property tests (Hypothesis), imported `_TERMINAL_STATUSES`, `_STATUS_TO_EVENT`
- `tests/unit/mainframe/core/session/test_projection.py` — ✅ DONE: Added PJ-7/PJ-8/PJ-10 property+compliance tests, imported `_EVENT_HANDLERS`, `_EVENT_TO_STATUS`, `VALID_TRANSITIONS`
- `tests/compliance/test_assembler_invariants.py` — ✅ DONE (new file): ASM-3 (policy_header first in L2), ASM-4 (layer order), ASM-5 (total_tokens == sum)

### 📖 Reference:
- `docs/bug-hunt/contract-target-set.md` — Invariant mapping source (SM-*, PJ-*, ASM-*, L1B-*)
- `AGENTS.md` — Project rules
- `tests/unit/mainframe/core/assembler/test_l1_builders.py` — Existing L1B tests (not modified)

## 5. The Graveyard (Failed Attempts)
- ❌ SM-6 initially asserted CANCELLED reachable from ALL non-terminal states → Failed because INTERRUPTED can only transition to RUNNING (by spec) → Fixed: excluded INTERRUPTED from the invariant check
- ❌ PJ-8 initially asserted all `_EVENT_TO_STATUS` keys have handlers → Failed because `SESSION_CREATED` is handled at creation time, not in apply_event → Fixed: excluded `SESSION_CREATED`
- ❌ `uv run pytest --timeout=60` → `--timeout` flag not recognized → Use without `--timeout`

## 6. Discoveries
- `deal.ensure` is the correct decorator for postconditions that need access to input args (not just result) — signature: `lambda arg1, arg2, ..., result:`
- `deal.post` only receives `result`; `deal.ensure` receives all original args + `result`
- LSP (pyright) shows import errors for `deal`, `eidos_protocol` — these are false positives from venv resolution; all imports work at runtime via `uv run`
- `SessionStatus.INTERRUPTED` → only `RUNNING` (no CANCELLED); this is intentional per pod-session-subsystem.md Appendix A
- `_EVENT_TO_STATUS` includes `SESSION_CREATED → PENDING` but `_EVENT_HANDLERS` doesn't (creation handled separately)
- Hypothesis is available as `hypothesis-6.151.4` plugin
- All test runs use: `uv run pytest <path> -v`

## 7. Next Immediate Actions (Atomic)
1. **Finish l1_builders.py contracts**: Add L1B-3 (`@ensure` kind='skill_index') on `add_skill_index_component`, L1B-4 (`@ensure` kind='core_skill') on `add_core_skill_component`, L1B-5 (`@ensure` kind='toolset_definitions') on `add_toolset_component`, plus L1B-7 (`@ensure` len sync) on all three
2. **Verify l1_builders.py**: `uv run pytest tests/unit/mainframe/core/assembler/test_l1_builders.py -v`
3. **Run full verification**: `uv run invar guard` then `uv run pytest tests/ -x -q`
4. **Report**: Count contracts added per file, paste `invar guard` output, paste pytest pass/fail count
5. **Step completion**: `vectl done 49.2` after all verification passes

---

---

## User

Continue if you have next steps, or stop and ask for clarification if you are unsure how to proceed.

---

## Assistant

好的，继续完成 `l1_builders.py` 的剩余合约。先读取当前文件状态和合约目标。


好，状态清晰。需要对三个函数各添加 L1B-3/4/5（kind 检查）和 L1B-7（len 同步）合约。开始编辑：


任务清晰，继续。三个函数各加两个 `@ensure`：


LSP 错误是 pyright venv 解析的误报，继续。添加 `add_core_skill_component` 的合约：


添加 `add_toolset_component` 的合约：


所有四个 `add_*` 函数都已添加合约。运行验证：


全部通过。25 个单元测试 + 4 个 doctest = 29 通过，0 失败。

现在运行全量验证：


Doctest 超时了。让我看完整输出：


Doctest 超时是全项目级别的（60s 限制），不是我们的文件导致的。让我确认我们修改的四个文件各自 doctest 都正常：


36 doctests 全通过。doctest 超时是项目级预存问题。运行相关测试套件确认无回归：


**112 tests passed, 0 failed, 0 L0**。所有合约已添加完成。

现在提交这个工作单元（l1_builders.py 合约完成）：


Pre-existing测试失败：`test_approval_blocks_execution`，与我们的改动无关。确认一下：


确认：`test_blocking_contracts.py` 的 3 个测试在 stash（无我们改动时）也全失败。这是预存缺陷。

用 `--changed` 模式让 guard 只检查我们修改的文件：

---

## Assistant (internal reasoning)

The MCP tool timed out. Let me try via CLI with a longer timeout, or just use --changed flag with the bash tool.

---

## Assistant

Pre-commit hook 的 `invar guard` 在 doctest 阶段触发了预存失败（`test_blocking_contracts.py`）。让我检查 pre-commit 配置：


问题清楚了。`invar guard` 的 doctest 阶段会运行广泛的测试集，包含预存失败的 `test_blocking_contracts.py`。让我看看能否快速修复这个阻塞：


## ⚠️ 提交被预存失败阻塞

**情况**：Pre-commit hook 的 `invar guard` 在 doctest 阶段收集了 `tests/compliance/blocking/test_blocking_contracts.py` 的 3 个测试，这些测试在 **我们改动之前就已经失败**（已通过 `git stash` 验证）。

这些测试是 Phase 48.18 的 blocking semantics 合约，测试的功能（`BLOCKING_EVENT_EMITTED` → `semantic.approval` A2UI 消息转换）尚未实现。

**我们的改动验证状态**：
- ✅ 112 相关单元测试通过
- ✅ 36 相关 doctests 通过
- ✅ 0 L0, 全部 L1
- ❌ Pre-commit hook 被不相关的预存失败阻塞

**选项**：
1. 用 `git commit --no-verify` 绕过 hook（需要您明确授权）
2. 先修复 `test_blocking_contracts.py`（超出 step 49.2 范围，且该功能可能尚未实现）

您希望怎么处理？
