tdd-cli 0.11.0__tar.gz → 0.12.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {tdd_cli-0.11.0 → tdd_cli-0.12.0}/CHANGELOG.md +19 -0
- {tdd_cli-0.11.0 → tdd_cli-0.12.0}/PKG-INFO +1 -1
- {tdd_cli-0.11.0 → tdd_cli-0.12.0}/docs/PRD.md +21 -2
- {tdd_cli-0.11.0 → tdd_cli-0.12.0}/src/tddcli/__init__.py +1 -1
- {tdd_cli-0.11.0 → tdd_cli-0.12.0}/src/tddcli/adapters/base.py +2 -3
- {tdd_cli-0.11.0 → tdd_cli-0.12.0}/src/tddcli/advance.py +1 -1
- {tdd_cli-0.11.0 → tdd_cli-0.12.0}/src/tddcli/ledger.py +8 -1
- {tdd_cli-0.11.0 → tdd_cli-0.12.0}/src/tddcli/machine.py +51 -15
- tdd_cli-0.12.0/tests/test_close_sweep_gates.py +361 -0
- {tdd_cli-0.11.0 → tdd_cli-0.12.0}/tests/test_release_surface.py +19 -0
- {tdd_cli-0.11.0 → tdd_cli-0.12.0}/.gitignore +0 -0
- {tdd_cli-0.11.0 → tdd_cli-0.12.0}/LICENSE +0 -0
- {tdd_cli-0.11.0 → tdd_cli-0.12.0}/README.md +0 -0
- {tdd_cli-0.11.0 → tdd_cli-0.12.0}/SECURITY.md +0 -0
- {tdd_cli-0.11.0 → tdd_cli-0.12.0}/docs/harness-integration.md +0 -0
- {tdd_cli-0.11.0 → tdd_cli-0.12.0}/examples/claude-code-hooks/README.md +0 -0
- {tdd_cli-0.11.0 → tdd_cli-0.12.0}/examples/claude-code-hooks/bash_hook.py +0 -0
- {tdd_cli-0.11.0 → tdd_cli-0.12.0}/examples/claude-code-hooks/stop_hook.py +0 -0
- {tdd_cli-0.11.0 → tdd_cli-0.12.0}/examples/plan.md +0 -0
- {tdd_cli-0.11.0 → tdd_cli-0.12.0}/examples/skills/tdd-drive/README.md +0 -0
- {tdd_cli-0.11.0 → tdd_cli-0.12.0}/examples/skills/tdd-drive/SKILL.md +0 -0
- {tdd_cli-0.11.0 → tdd_cli-0.12.0}/examples/skills/tdd-handoff/README.md +0 -0
- {tdd_cli-0.11.0 → tdd_cli-0.12.0}/examples/skills/tdd-handoff/SKILL.md +0 -0
- {tdd_cli-0.11.0 → tdd_cli-0.12.0}/pyproject.toml +0 -0
- {tdd_cli-0.11.0 → tdd_cli-0.12.0}/src/tddcli/adapters/__init__.py +0 -0
- {tdd_cli-0.11.0 → tdd_cli-0.12.0}/src/tddcli/adapters/cargo_adapter.py +0 -0
- {tdd_cli-0.11.0 → tdd_cli-0.12.0}/src/tddcli/adapters/exec_adapter.py +0 -0
- {tdd_cli-0.11.0 → tdd_cli-0.12.0}/src/tddcli/adapters/gradle_adapter.py +0 -0
- {tdd_cli-0.11.0 → tdd_cli-0.12.0}/src/tddcli/adapters/pytest_adapter.py +0 -0
- {tdd_cli-0.11.0 → tdd_cli-0.12.0}/src/tddcli/adapters/vitest_adapter.py +0 -0
- {tdd_cli-0.11.0 → tdd_cli-0.12.0}/src/tddcli/adapters/xctest_adapter.py +0 -0
- {tdd_cli-0.11.0 → tdd_cli-0.12.0}/src/tddcli/cli.py +0 -0
- {tdd_cli-0.11.0 → tdd_cli-0.12.0}/src/tddcli/config.py +0 -0
- {tdd_cli-0.11.0 → tdd_cli-0.12.0}/src/tddcli/contract.py +0 -0
- {tdd_cli-0.11.0 → tdd_cli-0.12.0}/src/tddcli/docs.py +0 -0
- {tdd_cli-0.11.0 → tdd_cli-0.12.0}/src/tddcli/envelope.py +0 -0
- {tdd_cli-0.11.0 → tdd_cli-0.12.0}/src/tddcli/fleet.py +0 -0
- {tdd_cli-0.11.0 → tdd_cli-0.12.0}/src/tddcli/gitutil.py +0 -0
- {tdd_cli-0.11.0 → tdd_cli-0.12.0}/src/tddcli/identity.py +0 -0
- {tdd_cli-0.11.0 → tdd_cli-0.12.0}/src/tddcli/leases.py +0 -0
- {tdd_cli-0.11.0 → tdd_cli-0.12.0}/src/tddcli/plan_paths.py +0 -0
- {tdd_cli-0.11.0 → tdd_cli-0.12.0}/src/tddcli/render.py +0 -0
- {tdd_cli-0.11.0 → tdd_cli-0.12.0}/src/tddcli/snapshot.py +0 -0
- {tdd_cli-0.11.0 → tdd_cli-0.12.0}/src/tddcli/staging.py +0 -0
- {tdd_cli-0.11.0 → tdd_cli-0.12.0}/src/tddcli/target_lint.py +0 -0
- {tdd_cli-0.11.0 → tdd_cli-0.12.0}/tests/conftest.py +0 -0
- {tdd_cli-0.11.0 → tdd_cli-0.12.0}/tests/test_advance_adoption.py +0 -0
- {tdd_cli-0.11.0 → tdd_cli-0.12.0}/tests/test_ancillary_files.py +0 -0
- {tdd_cli-0.11.0 → tdd_cli-0.12.0}/tests/test_artifact_regeneration.py +0 -0
- {tdd_cli-0.11.0 → tdd_cli-0.12.0}/tests/test_baseline_integrity.py +0 -0
- {tdd_cli-0.11.0 → tdd_cli-0.12.0}/tests/test_baseline_sanity.py +0 -0
- {tdd_cli-0.11.0 → tdd_cli-0.12.0}/tests/test_batch_collection.py +0 -0
- {tdd_cli-0.11.0 → tdd_cli-0.12.0}/tests/test_cargo_adapter.py +0 -0
- {tdd_cli-0.11.0 → tdd_cli-0.12.0}/tests/test_concurrent_advance.py +0 -0
- {tdd_cli-0.11.0 → tdd_cli-0.12.0}/tests/test_config_and_staging.py +0 -0
- {tdd_cli-0.11.0 → tdd_cli-0.12.0}/tests/test_config_drift.py +0 -0
- {tdd_cli-0.11.0 → tdd_cli-0.12.0}/tests/test_contract.py +0 -0
- {tdd_cli-0.11.0 → tdd_cli-0.12.0}/tests/test_docs_command.py +0 -0
- {tdd_cli-0.11.0 → tdd_cli-0.12.0}/tests/test_doctor_attribution.py +0 -0
- {tdd_cli-0.11.0 → tdd_cli-0.12.0}/tests/test_doctor_blockers.py +0 -0
- {tdd_cli-0.11.0 → tdd_cli-0.12.0}/tests/test_end_to_end.py +0 -0
- {tdd_cli-0.11.0 → tdd_cli-0.12.0}/tests/test_evidence_extraction.py +0 -0
- {tdd_cli-0.11.0 → tdd_cli-0.12.0}/tests/test_example_plan.py +0 -0
- {tdd_cli-0.11.0 → tdd_cli-0.12.0}/tests/test_exec_adapter.py +0 -0
- {tdd_cli-0.11.0 → tdd_cli-0.12.0}/tests/test_executor_attribution.py +0 -0
- {tdd_cli-0.11.0 → tdd_cli-0.12.0}/tests/test_executor_notes.py +0 -0
- {tdd_cli-0.11.0 → tdd_cli-0.12.0}/tests/test_failure_clipping.py +0 -0
- {tdd_cli-0.11.0 → tdd_cli-0.12.0}/tests/test_fleet.py +0 -0
- {tdd_cli-0.11.0 → tdd_cli-0.12.0}/tests/test_gradle_adapter.py +0 -0
- {tdd_cli-0.11.0 → tdd_cli-0.12.0}/tests/test_heartbeat.py +0 -0
- {tdd_cli-0.11.0 → tdd_cli-0.12.0}/tests/test_id_normalisation.py +0 -0
- {tdd_cli-0.11.0 → tdd_cli-0.12.0}/tests/test_init_detection.py +0 -0
- {tdd_cli-0.11.0 → tdd_cli-0.12.0}/tests/test_late_baseline.py +0 -0
- {tdd_cli-0.11.0 → tdd_cli-0.12.0}/tests/test_named_leases_and_timeout.py +0 -0
- {tdd_cli-0.11.0 → tdd_cli-0.12.0}/tests/test_pin_cycles.py +0 -0
- {tdd_cli-0.11.0 → tdd_cli-0.12.0}/tests/test_plan_paths.py +0 -0
- {tdd_cli-0.11.0 → tdd_cli-0.12.0}/tests/test_progress.py +0 -0
- {tdd_cli-0.11.0 → tdd_cli-0.12.0}/tests/test_project_commands.py +0 -0
- {tdd_cli-0.11.0 → tdd_cli-0.12.0}/tests/test_project_env.py +0 -0
- {tdd_cli-0.11.0 → tdd_cli-0.12.0}/tests/test_pytest_xdist_group.py +0 -0
- {tdd_cli-0.11.0 → tdd_cli-0.12.0}/tests/test_python_env_managers.py +0 -0
- {tdd_cli-0.11.0 → tdd_cli-0.12.0}/tests/test_refactor_cycles.py +0 -0
- {tdd_cli-0.11.0 → tdd_cli-0.12.0}/tests/test_run_claim.py +0 -0
- {tdd_cli-0.11.0 → tdd_cli-0.12.0}/tests/test_sensitivity_evidence.py +0 -0
- {tdd_cli-0.11.0 → tdd_cli-0.12.0}/tests/test_single_project_repo.py +0 -0
- {tdd_cli-0.11.0 → tdd_cli-0.12.0}/tests/test_snapshot_and_identity.py +0 -0
- {tdd_cli-0.11.0 → tdd_cli-0.12.0}/tests/test_stub_hint.py +0 -0
- {tdd_cli-0.11.0 → tdd_cli-0.12.0}/tests/test_suite_overrides.py +0 -0
- {tdd_cli-0.11.0 → tdd_cli-0.12.0}/tests/test_target_lint.py +0 -0
- {tdd_cli-0.11.0 → tdd_cli-0.12.0}/tests/test_target_validation.py +0 -0
- {tdd_cli-0.11.0 → tdd_cli-0.12.0}/tests/test_timing_visibility.py +0 -0
- {tdd_cli-0.11.0 → tdd_cli-0.12.0}/tests/test_undeclared_close_gate.py +0 -0
- {tdd_cli-0.11.0 → tdd_cli-0.12.0}/tests/test_undeclared_dedup.py +0 -0
- {tdd_cli-0.11.0 → tdd_cli-0.12.0}/tests/test_vitest_adapter.py +0 -0
- {tdd_cli-0.11.0 → tdd_cli-0.12.0}/tests/test_worker_leases.py +0 -0
- {tdd_cli-0.11.0 → tdd_cli-0.12.0}/tests/test_xctest_adapter.py +0 -0
|
@@ -6,6 +6,25 @@ and the project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.12.0] - 2026-09-17
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- **Close sweep runs all gates before any suite** (#129): lint and typecheck now run for every
|
|
14
|
+
project in the sweep set before any project's suite executes. A failing gate short-circuits the
|
|
15
|
+
sweep immediately — no suite runs, and `next_action` returns `fix_regression` with the failing
|
|
16
|
+
gate entry. Previously each project ran its suite first and only then checked its gates.
|
|
17
|
+
- **`Adapter._gate` stops at the first failing command** (#129): the first non-zero exit ends the
|
|
18
|
+
gate; subsequent commands in the same gate list are not executed.
|
|
19
|
+
- **Gate memoisation** (#129): a gate whose project tree hash is unchanged since it last passed
|
|
20
|
+
within the same run is skipped (no command executed) and recorded with `skipped = 1` in
|
|
21
|
+
`gate_result`. A failed gate or a changed tree always re-runs.
|
|
22
|
+
- **`fix_regression` detail wording** (#129): the reply for a gate failure now reads
|
|
23
|
+
`"Close sweep stopped before the suite: lint/typecheck gates failed."` (was `"Close sweep is
|
|
24
|
+
green but lint/typecheck gates failed."`).
|
|
25
|
+
- **Schema v11** (#129): `gate_result` gains `tree_hash TEXT` and
|
|
26
|
+
`skipped INTEGER NOT NULL DEFAULT 0`. Existing ledgers are migrated on open via `MIGRATIONS[10]`.
|
|
27
|
+
|
|
9
28
|
## [0.11.0] - 2026-09-16
|
|
10
29
|
|
|
11
30
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: tdd-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.12.0
|
|
4
4
|
Summary: Ledger-backed TDD process controller for autonomous coding agents
|
|
5
5
|
Project-URL: Homepage, https://github.com/geuben/tdd-cli
|
|
6
6
|
Project-URL: Repository, https://github.com/geuben/tdd-cli
|
|
@@ -195,6 +195,18 @@ Typed: `regression`, `target_unfixable`, `bad_red`, `plan_defect`, `tooling`, `c
|
|
|
195
195
|
Without it the only available label is `regression`, which records a defect the run introduced —
|
|
196
196
|
so the agent must either misreport its own work or leave the failure unfiled.
|
|
197
197
|
|
|
198
|
+
### GateResult
|
|
199
|
+
One execution (or memo-skip) of one project's lint or typecheck gate during a close sweep.
|
|
200
|
+
|
|
201
|
+
| Field | Notes |
|
|
202
|
+
|---|---|
|
|
203
|
+
| `id`, `run_id`, `cycle_id`, `project`, `kind` | `kind` is `lint` or `typecheck` |
|
|
204
|
+
| `ok` | `1` = passed (or skipped), `0` = failed |
|
|
205
|
+
| `output` | truncated stdout/stderr of the failing command; empty when `ok = 1` |
|
|
206
|
+
| `tree_hash` | hash of the project tree at the time the gate ran or was skipped (schema v11) |
|
|
207
|
+
| `skipped` | `1` if the gate was memo-skipped (tree unchanged, gate previously passed within this run); `0` otherwise (schema v11) |
|
|
208
|
+
| `at` | |
|
|
209
|
+
|
|
198
210
|
---
|
|
199
211
|
|
|
200
212
|
## 6. State machine
|
|
@@ -561,7 +573,9 @@ For the passed-on-arrival case, which occurred in 4 of 8 executed cycles in the
|
|
|
561
573
|
- **R9.2** On the transition out of `AWAITING_REFACTOR`, the close sweep runs: the cycle's own
|
|
562
574
|
projects, plus every project **downstream of an artifact the cycle modified** per the
|
|
563
575
|
`consumed_by` edges in §7.1, plus lint and typecheck for each. Projects with
|
|
564
|
-
`in_close_sweep = false` are always excluded.
|
|
576
|
+
`in_close_sweep = false` are always excluded. The close sweep runs lint and typecheck for
|
|
577
|
+
**every project in scope before any suite**; if any gate fails, the sweep returns immediately
|
|
578
|
+
without running any suite.
|
|
565
579
|
- **R9.3** A **full sweep** — every registered project, lint and typecheck, including
|
|
566
580
|
`in_close_sweep = false` suites — runs once at plan completion.
|
|
567
581
|
- **R9.4** Rationale: a per-cycle sweep of all four projects with `mypy` and `tsc` costs minutes,
|
|
@@ -664,7 +678,12 @@ For the passed-on-arrival case, which occurred in 4 of 8 executed cycles in the
|
|
|
664
678
|
|
|
665
679
|
### 9.4 Lint, typecheck and artifacts
|
|
666
680
|
- **R9.11** Lint and typecheck results are part of the close-sweep verdict, surfaced alongside
|
|
667
|
-
test failures rather than discovered later at commit time.
|
|
681
|
+
test failures rather than discovered later at commit time. The gate pass stops at the **first
|
|
682
|
+
failing gate**; `Adapter._gate` stops at the **first failing command** within a gate. A gate
|
|
683
|
+
whose project tree hash is unchanged since that gate last passed within the same run is
|
|
684
|
+
**skipped** (no command is executed) and recorded with `skipped = 1` in `gate_result`. Limit:
|
|
685
|
+
gate configuration that lives outside the project's declared roots (e.g. a top-level
|
|
686
|
+
`pyproject.toml`) does not affect the tree hash and therefore does not invalidate the memo.
|
|
668
687
|
- **R9.12** Before a run starts and at every cycle close, declared artifacts are checked for
|
|
669
688
|
staleness against their producer. When the tool cannot resolve staleness itself (no `regenerate`
|
|
670
689
|
command, or the regenerate hook produced no commit), it emits `stale_artifact` and blocks the
|
|
@@ -302,9 +302,8 @@ class Adapter:
|
|
|
302
302
|
return self._gate(self.project.typecheck)
|
|
303
303
|
|
|
304
304
|
def _gate(self, commands: list[str]) -> GateResult:
|
|
305
|
-
chunks = []
|
|
306
305
|
for cmd in commands:
|
|
307
306
|
code, out, err = run_command(cmd, self.root, label="gate")
|
|
308
307
|
if code != 0:
|
|
309
|
-
|
|
310
|
-
return GateResult(ok=
|
|
308
|
+
return GateResult(ok=False, output=f"$ {cmd}\n{out}\n{err}".strip()[:4000])
|
|
309
|
+
return GateResult(ok=True)
|
|
@@ -520,7 +520,7 @@ def _handle_refactor(engine: Engine, cycle, retried: bool) -> Envelope:
|
|
|
520
520
|
engine,
|
|
521
521
|
cycle,
|
|
522
522
|
Verb.FIX_REGRESSION,
|
|
523
|
-
"Close sweep
|
|
523
|
+
"Close sweep stopped before the suite: lint/typecheck gates failed.",
|
|
524
524
|
gates=gates,
|
|
525
525
|
commit=sha,
|
|
526
526
|
)
|
|
@@ -13,7 +13,7 @@ import sqlite3
|
|
|
13
13
|
from datetime import datetime, timedelta, timezone
|
|
14
14
|
from pathlib import Path
|
|
15
15
|
|
|
16
|
-
SCHEMA_VERSION =
|
|
16
|
+
SCHEMA_VERSION = 11
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
class LedgerVersionError(RuntimeError):
|
|
@@ -45,6 +45,11 @@ MIGRATIONS: dict[int, str] = {
|
|
|
45
45
|
8: "ALTER TABLE artifact_check ADD COLUMN regenerate_failed INTEGER NOT NULL DEFAULT 0;",
|
|
46
46
|
# v9 -> v10 added start_sha column to run; ALTER TABLE covers old ledgers.
|
|
47
47
|
9: "ALTER TABLE run ADD COLUMN start_sha TEXT;",
|
|
48
|
+
# v10 -> v11 added tree_hash and skipped columns to gate_result; ALTER TABLE covers old ledgers.
|
|
49
|
+
10: (
|
|
50
|
+
"ALTER TABLE gate_result ADD COLUMN tree_hash TEXT;"
|
|
51
|
+
" ALTER TABLE gate_result ADD COLUMN skipped INTEGER NOT NULL DEFAULT 0;"
|
|
52
|
+
),
|
|
48
53
|
}
|
|
49
54
|
|
|
50
55
|
SCHEMA = """
|
|
@@ -158,6 +163,8 @@ CREATE TABLE IF NOT EXISTS gate_result (
|
|
|
158
163
|
kind TEXT NOT NULL, -- lint | typecheck
|
|
159
164
|
ok INTEGER NOT NULL,
|
|
160
165
|
output TEXT,
|
|
166
|
+
tree_hash TEXT,
|
|
167
|
+
skipped INTEGER NOT NULL DEFAULT 0,
|
|
161
168
|
at TEXT NOT NULL
|
|
162
169
|
);
|
|
163
170
|
|
|
@@ -270,9 +270,58 @@ class Engine:
|
|
|
270
270
|
unbaselined: dict[str, list[str]] = {}
|
|
271
271
|
unobserved: dict[str, str] = {}
|
|
272
272
|
|
|
273
|
+
adapters_by_name = {
|
|
274
|
+
name: adapters.build(self.config.project(name), self.worktree) for name in names
|
|
275
|
+
}
|
|
276
|
+
|
|
273
277
|
for name in names:
|
|
274
|
-
|
|
275
|
-
|
|
278
|
+
adapter = adapters_by_name[name]
|
|
279
|
+
current_hash = self.tree_hash([name])
|
|
280
|
+
gate_failed = False
|
|
281
|
+
for kind, gate_fn in (("lint", adapter.lint), ("typecheck", adapter.typecheck)):
|
|
282
|
+
last = self.ledger.one(
|
|
283
|
+
"SELECT ok, tree_hash FROM gate_result"
|
|
284
|
+
" WHERE run_id = ? AND project = ? AND kind = ?"
|
|
285
|
+
" ORDER BY id DESC LIMIT 1",
|
|
286
|
+
(self.run["id"], name, kind),
|
|
287
|
+
)
|
|
288
|
+
if last and last["ok"] == 1 and last["tree_hash"] == current_hash:
|
|
289
|
+
self.ledger.insert(
|
|
290
|
+
"gate_result",
|
|
291
|
+
run_id=self.run["id"],
|
|
292
|
+
cycle_id=cycle_row["id"],
|
|
293
|
+
project=name,
|
|
294
|
+
kind=kind,
|
|
295
|
+
ok=1,
|
|
296
|
+
output="",
|
|
297
|
+
tree_hash=current_hash,
|
|
298
|
+
skipped=1,
|
|
299
|
+
at=now(),
|
|
300
|
+
)
|
|
301
|
+
continue
|
|
302
|
+
gate = gate_fn()
|
|
303
|
+
self.ledger.insert(
|
|
304
|
+
"gate_result",
|
|
305
|
+
run_id=self.run["id"],
|
|
306
|
+
cycle_id=cycle_row["id"],
|
|
307
|
+
project=name,
|
|
308
|
+
kind=kind,
|
|
309
|
+
ok=int(gate.ok),
|
|
310
|
+
output=gate.output,
|
|
311
|
+
tree_hash=current_hash,
|
|
312
|
+
at=now(),
|
|
313
|
+
)
|
|
314
|
+
if not gate.ok:
|
|
315
|
+
gates.append((name, kind, gate.output))
|
|
316
|
+
gate_failed = True
|
|
317
|
+
break
|
|
318
|
+
if gate_failed:
|
|
319
|
+
return SweepOutcome(
|
|
320
|
+
failures=failures, gates=gates, unbaselined=unbaselined, unobserved=unobserved
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
for name in names:
|
|
324
|
+
adapter = adapters_by_name[name]
|
|
276
325
|
started = time.monotonic()
|
|
277
326
|
verdict = adapter.run(None)
|
|
278
327
|
heartbeat(
|
|
@@ -350,19 +399,6 @@ class Engine:
|
|
|
350
399
|
tree_hash=self.tree_hash([name]),
|
|
351
400
|
started_at=now(),
|
|
352
401
|
)
|
|
353
|
-
for kind, gate in (("lint", adapter.lint()), ("typecheck", adapter.typecheck())):
|
|
354
|
-
self.ledger.insert(
|
|
355
|
-
"gate_result",
|
|
356
|
-
run_id=self.run["id"],
|
|
357
|
-
cycle_id=cycle_row["id"],
|
|
358
|
-
project=name,
|
|
359
|
-
kind=kind,
|
|
360
|
-
ok=int(gate.ok),
|
|
361
|
-
output=gate.output,
|
|
362
|
-
at=now(),
|
|
363
|
-
)
|
|
364
|
-
if not gate.ok:
|
|
365
|
-
gates.append((name, kind, gate.output))
|
|
366
402
|
return SweepOutcome(
|
|
367
403
|
failures=failures, gates=gates, unbaselined=unbaselined, unobserved=unobserved
|
|
368
404
|
)
|
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
"""Close-sweep gate ordering and short-circuit (issue #129)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from conftest import git, run_cli, write_plan
|
|
8
|
+
from tddcli import gitutil
|
|
9
|
+
from tddcli.ledger import Ledger
|
|
10
|
+
|
|
11
|
+
# ── inner plan used by every test to reach the close sweep ────────────────────
|
|
12
|
+
|
|
13
|
+
SIMPLE_PLAN = """---
|
|
14
|
+
cycles:
|
|
15
|
+
- n: 1
|
|
16
|
+
project: backend
|
|
17
|
+
title: "basic pass"
|
|
18
|
+
test: "tests/test_gate_probe.py::test_gate_probe"
|
|
19
|
+
stub_expected: ["app/probe.py"]
|
|
20
|
+
commit_red: "test: gate probe"
|
|
21
|
+
commit_green: "feat: gate probe"
|
|
22
|
+
---
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
_TEST = """\
|
|
26
|
+
from app.probe import probe
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_gate_probe():
|
|
30
|
+
assert probe() == 42
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
_STUB = "def probe():\n raise NotImplementedError\n"
|
|
34
|
+
_IMPL = "def probe():\n return 42\n"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _toml(lint_cmds: list[str], typecheck_cmds: list[str] | None = None) -> str:
|
|
38
|
+
tc = typecheck_cmds if typecheck_cmds is not None else []
|
|
39
|
+
return (
|
|
40
|
+
"[project.backend]\n"
|
|
41
|
+
'root = "backend"\n'
|
|
42
|
+
'adapter = "pytest"\n'
|
|
43
|
+
'test_paths = ["tests/"]\n'
|
|
44
|
+
f"lint = {lint_cmds!r}\n"
|
|
45
|
+
f"typecheck = {tc!r}\n"
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _drive_to_close(
|
|
50
|
+
repo: Path,
|
|
51
|
+
lint_cmds: list[str] | None = None,
|
|
52
|
+
typecheck_cmds: list[str] | None = None,
|
|
53
|
+
project: str = "backend",
|
|
54
|
+
) -> dict:
|
|
55
|
+
"""Drive a single-cycle inner plan through RED → GREEN and return the
|
|
56
|
+
close-sweep envelope (the advance at AWAITING_REFACTOR)."""
|
|
57
|
+
lint = lint_cmds if lint_cmds is not None else []
|
|
58
|
+
(repo / "tdd.toml").write_text(_toml(lint, typecheck_cmds))
|
|
59
|
+
git(repo, "add", "-A")
|
|
60
|
+
git(repo, "commit", "-q", "-m", "configure gates")
|
|
61
|
+
|
|
62
|
+
plan = write_plan(repo, SIMPLE_PLAN)
|
|
63
|
+
assert run_cli(repo, "plan", "register", plan)["ok"]
|
|
64
|
+
assert run_cli(repo, "run", "start", "--plan", plan)["ok"]
|
|
65
|
+
|
|
66
|
+
# RED: write test + stub so the test collects but fails
|
|
67
|
+
(repo / project / "tests" / "test_gate_probe.py").write_text(_TEST)
|
|
68
|
+
(repo / project / "app" / "probe.py").write_text(_STUB)
|
|
69
|
+
adv = run_cli(repo, "advance")
|
|
70
|
+
assert adv["next_action"]["verb"] == "write_implementation", adv
|
|
71
|
+
|
|
72
|
+
# GREEN: write real implementation
|
|
73
|
+
(repo / project / "app" / "probe.py").write_text(_IMPL)
|
|
74
|
+
adv = run_cli(repo, "advance")
|
|
75
|
+
assert adv["next_action"]["verb"] == "refactor_or_advance", adv
|
|
76
|
+
|
|
77
|
+
# Advance at AWAITING_REFACTOR → triggers the close sweep
|
|
78
|
+
return run_cli(repo, "advance")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# ── cycle 1 ── pin: a failing gate replies fix_regression with that gate's entry
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def test_failing_lint_replies_fix_regression_with_the_lint_gate(repo):
|
|
85
|
+
out = _drive_to_close(repo, lint_cmds=["sh -c 'exit 1'"])
|
|
86
|
+
verb = out["next_action"]["verb"]
|
|
87
|
+
gates = out.get("result", {}).get("gates", [])
|
|
88
|
+
assert (verb, [(g["project"], g["kind"]) for g in gates]) == (
|
|
89
|
+
"fix_regression",
|
|
90
|
+
[("backend", "lint")],
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
# ── cycle 2 ── pin: a gate that ran records a gate_result row
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def test_a_failing_lint_records_its_gate_result_row(repo):
|
|
98
|
+
out = _drive_to_close(repo, lint_cmds=["sh -c 'exit 1'"])
|
|
99
|
+
run_id = out["run"]["id"]
|
|
100
|
+
led = Ledger(gitutil.repo_identity(repo))
|
|
101
|
+
rows = led.all(
|
|
102
|
+
"SELECT * FROM gate_result WHERE run_id = ? AND project = 'backend' AND kind = 'lint'",
|
|
103
|
+
(run_id,),
|
|
104
|
+
)
|
|
105
|
+
assert any(r["ok"] == 0 for r in rows)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
# ── cycle 3 ── _gate stops at the first failing command
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def test_gate_stops_at_the_first_failing_command(repo):
|
|
112
|
+
lint_cmds = [
|
|
113
|
+
"sh -c 'echo LINT-BOOM; exit 1'",
|
|
114
|
+
"sh -c 'echo SECOND-RAN; exit 1'",
|
|
115
|
+
]
|
|
116
|
+
out = _drive_to_close(repo, lint_cmds=lint_cmds)
|
|
117
|
+
gates = out.get("result", {}).get("gates", [])
|
|
118
|
+
lint_gate = next((g for g in gates if g["kind"] == "lint"), None)
|
|
119
|
+
assert lint_gate is not None
|
|
120
|
+
assert "SECOND-RAN" not in lint_gate["output"]
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
# ── cycle 4 ── a failing lint means typecheck is never run
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def test_a_failing_lint_stops_the_gate_pass_before_typecheck(repo, tmp_path):
|
|
127
|
+
counter = tmp_path / "typecheck-ran"
|
|
128
|
+
typecheck_cmds = [f"sh -c 'echo ran >> {counter}; exit 1'"]
|
|
129
|
+
_drive_to_close(repo, lint_cmds=["sh -c 'exit 1'"], typecheck_cmds=typecheck_cmds)
|
|
130
|
+
assert not counter.exists()
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
# ── cycle 5 ── a failing gate short-circuits the sweep before any suite runs
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def test_a_failing_gate_runs_no_suite(repo):
|
|
137
|
+
out = _drive_to_close(repo, lint_cmds=["sh -c 'exit 1'"])
|
|
138
|
+
run_id = out["run"]["id"]
|
|
139
|
+
led = Ledger(gitutil.repo_identity(repo))
|
|
140
|
+
rows = led.all(
|
|
141
|
+
"SELECT * FROM invocation WHERE run_id = ? AND phase_at = 'CLOSE_SWEEP'",
|
|
142
|
+
(run_id,),
|
|
143
|
+
)
|
|
144
|
+
assert len(rows) == 0
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
# ── cycle 6 ── every sweep project's gates run before any project's suite
|
|
148
|
+
|
|
149
|
+
_THREE_PLAN = """\
|
|
150
|
+
---
|
|
151
|
+
cycles:
|
|
152
|
+
- n: 1
|
|
153
|
+
project: backend
|
|
154
|
+
title: "basic pass"
|
|
155
|
+
test: "tests/test_gate_probe.py::test_gate_probe"
|
|
156
|
+
stub_expected: ["app/probe.py"]
|
|
157
|
+
commit_red: "test: gate probe"
|
|
158
|
+
commit_green: "feat: gate probe"
|
|
159
|
+
---
|
|
160
|
+
"""
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def test_a_later_projects_failing_gate_runs_no_earlier_suite(repo_three):
|
|
164
|
+
# Rewrite tdd.toml: backend has no lint, svc has a failing lint.
|
|
165
|
+
(repo_three / "tdd.toml").write_text(
|
|
166
|
+
"[project.backend]\n"
|
|
167
|
+
'root = "backend"\n'
|
|
168
|
+
'adapter = "pytest"\n'
|
|
169
|
+
'test_paths = ["tests/"]\n'
|
|
170
|
+
"\n"
|
|
171
|
+
"[project.svc]\n"
|
|
172
|
+
'root = "svc"\n'
|
|
173
|
+
'adapter = "pytest"\n'
|
|
174
|
+
'test_paths = ["tests/"]\n'
|
|
175
|
+
"lint = [\"sh -c 'exit 1'\"]\n"
|
|
176
|
+
"typecheck = []\n"
|
|
177
|
+
"\n"
|
|
178
|
+
"[project.other]\n"
|
|
179
|
+
'root = "other"\n'
|
|
180
|
+
'adapter = "pytest"\n'
|
|
181
|
+
'test_paths = ["tests/"]\n'
|
|
182
|
+
"\n"
|
|
183
|
+
"[artifact.schema]\n"
|
|
184
|
+
'path = "backend/schema.json"\n'
|
|
185
|
+
'produced_by = "backend"\n'
|
|
186
|
+
'consumed_by = ["svc"]\n'
|
|
187
|
+
'regenerate = "true"\n'
|
|
188
|
+
)
|
|
189
|
+
git(repo_three, "add", "-A")
|
|
190
|
+
git(repo_three, "commit", "-q", "-m", "configure svc lint")
|
|
191
|
+
|
|
192
|
+
plan = write_plan(repo_three, _THREE_PLAN)
|
|
193
|
+
assert run_cli(repo_three, "plan", "register", plan)["ok"]
|
|
194
|
+
assert run_cli(repo_three, "run", "start", "--plan", plan)["ok"]
|
|
195
|
+
|
|
196
|
+
# RED: test + stub
|
|
197
|
+
(repo_three / "backend" / "tests" / "test_gate_probe.py").write_text(_TEST)
|
|
198
|
+
(repo_three / "backend" / "app" / "probe.py").write_text(_STUB)
|
|
199
|
+
adv = run_cli(repo_three, "advance")
|
|
200
|
+
assert adv["next_action"]["verb"] == "write_implementation", adv
|
|
201
|
+
|
|
202
|
+
# GREEN: real implementation
|
|
203
|
+
(repo_three / "backend" / "app" / "probe.py").write_text(_IMPL)
|
|
204
|
+
adv = run_cli(repo_three, "advance")
|
|
205
|
+
assert adv["next_action"]["verb"] == "refactor_or_advance", adv
|
|
206
|
+
|
|
207
|
+
# Modify backend/schema.json (dirty) → pulls svc into the close sweep
|
|
208
|
+
(repo_three / "backend" / "schema.json").write_text('{"version": 2}\n')
|
|
209
|
+
|
|
210
|
+
# Advance at AWAITING_REFACTOR → svc's lint fails → no suite runs
|
|
211
|
+
out = run_cli(repo_three, "advance")
|
|
212
|
+
|
|
213
|
+
run_id = out["run"]["id"]
|
|
214
|
+
led = Ledger(gitutil.repo_identity(repo_three))
|
|
215
|
+
rows = led.all(
|
|
216
|
+
"SELECT * FROM invocation WHERE run_id = ? AND phase_at = 'CLOSE_SWEEP' AND project = 'backend'",
|
|
217
|
+
(run_id,),
|
|
218
|
+
)
|
|
219
|
+
assert len(rows) == 0
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
# ── cycle 7 ── the gate-failure reply does not claim the sweep is green
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def test_the_gate_failure_reply_does_not_claim_the_sweep_is_green(repo):
|
|
226
|
+
out = _drive_to_close(repo, lint_cmds=["sh -c 'exit 1'"])
|
|
227
|
+
assert out["next_action"]["detail"] == (
|
|
228
|
+
"Close sweep stopped before the suite: lint/typecheck gates failed."
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
# ── cycle 8 ── pin: a gate that failed is re-run at the same tree
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def test_a_failed_gate_is_rerun_at_the_same_tree(repo, tmp_path):
|
|
236
|
+
counter = tmp_path / "lint-count"
|
|
237
|
+
lint_cmds = [f"sh -c 'echo x >> {counter}; exit 1'"]
|
|
238
|
+
_drive_to_close(repo, lint_cmds=lint_cmds)
|
|
239
|
+
# Advance again at AWAITING_REFACTOR with nothing edited (same tree) → gate re-runs
|
|
240
|
+
run_cli(repo, "advance")
|
|
241
|
+
assert counter.read_text().count("\n") == 2
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
# ── cycle 9 ── pin: a gate is re-run when its project tree changed
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def test_a_gate_is_rerun_when_the_tree_changed(repo, tmp_path):
|
|
248
|
+
"""Lint passes but suite fails first time; tree changes; lint runs again second time."""
|
|
249
|
+
counter = tmp_path / "lint-count"
|
|
250
|
+
lint_cmds = [f"sh -c 'echo x >> {counter}'"] # exits 0 (passes)
|
|
251
|
+
|
|
252
|
+
# Set up a failing test that will be in the close sweep
|
|
253
|
+
(repo / "tdd.toml").write_text(_toml(lint_cmds))
|
|
254
|
+
git(repo, "add", "-A")
|
|
255
|
+
git(repo, "commit", "-q", "-m", "configure gates")
|
|
256
|
+
|
|
257
|
+
plan = write_plan(repo, SIMPLE_PLAN)
|
|
258
|
+
assert run_cli(repo, "plan", "register", plan)["ok"]
|
|
259
|
+
assert run_cli(repo, "run", "start", "--plan", plan)["ok"]
|
|
260
|
+
|
|
261
|
+
# RED
|
|
262
|
+
(repo / "backend" / "tests" / "test_gate_probe.py").write_text(_TEST)
|
|
263
|
+
(repo / "backend" / "app" / "probe.py").write_text(_STUB)
|
|
264
|
+
adv = run_cli(repo, "advance")
|
|
265
|
+
assert adv["next_action"]["verb"] == "write_implementation", adv
|
|
266
|
+
|
|
267
|
+
# GREEN
|
|
268
|
+
(repo / "backend" / "app" / "probe.py").write_text(_IMPL)
|
|
269
|
+
adv = run_cli(repo, "advance")
|
|
270
|
+
assert adv["next_action"]["verb"] == "refactor_or_advance", adv
|
|
271
|
+
|
|
272
|
+
# First close: add a failing test → lint passes but suite fails
|
|
273
|
+
(repo / "backend" / "tests" / "test_fail.py").write_text("def test_fail():\n assert False\n")
|
|
274
|
+
out1 = run_cli(repo, "advance")
|
|
275
|
+
assert out1["next_action"]["verb"] == "fix_regression", out1
|
|
276
|
+
|
|
277
|
+
# Fix the failing test → tree changes → second close: lint re-runs
|
|
278
|
+
(repo / "backend" / "tests" / "test_fail.py").write_text("def test_fail():\n assert True\n")
|
|
279
|
+
out2 = run_cli(repo, "advance")
|
|
280
|
+
assert out2["next_action"]["verb"] != "fix_regression" or out2.get("result", {}).get("gates"), (
|
|
281
|
+
out2
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
assert counter.read_text().count("\n") == 2
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
# ── cycle 11 ── a gate that passed at this tree hash is not re-executed
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def test_a_passing_gate_is_not_reexecuted_at_an_unchanged_tree(repo, tmp_path):
|
|
291
|
+
"""Lint passes first close; suite fails first close; tree unchanged for second close →
|
|
292
|
+
lint should be skipped the second time."""
|
|
293
|
+
counter = tmp_path / "lint-count"
|
|
294
|
+
lint_cmds = [f"sh -c 'echo x >> {counter}'"] # exits 0 (passes)
|
|
295
|
+
|
|
296
|
+
(repo / "tdd.toml").write_text(_toml(lint_cmds))
|
|
297
|
+
git(repo, "add", "-A")
|
|
298
|
+
git(repo, "commit", "-q", "-m", "configure gates")
|
|
299
|
+
|
|
300
|
+
plan = write_plan(repo, SIMPLE_PLAN)
|
|
301
|
+
assert run_cli(repo, "plan", "register", plan)["ok"]
|
|
302
|
+
assert run_cli(repo, "run", "start", "--plan", plan)["ok"]
|
|
303
|
+
|
|
304
|
+
# RED/GREEN
|
|
305
|
+
(repo / "backend" / "tests" / "test_gate_probe.py").write_text(_TEST)
|
|
306
|
+
(repo / "backend" / "app" / "probe.py").write_text(_STUB)
|
|
307
|
+
assert run_cli(repo, "advance")["next_action"]["verb"] == "write_implementation"
|
|
308
|
+
(repo / "backend" / "app" / "probe.py").write_text(_IMPL)
|
|
309
|
+
assert run_cli(repo, "advance")["next_action"]["verb"] == "refactor_or_advance"
|
|
310
|
+
|
|
311
|
+
# First close: add failing test (dirty) → lint passes, suite fails
|
|
312
|
+
(repo / "backend" / "tests" / "test_fail.py").write_text("def test_fail():\n assert False\n")
|
|
313
|
+
out1 = run_cli(repo, "advance")
|
|
314
|
+
assert out1["next_action"]["verb"] == "fix_regression", out1
|
|
315
|
+
|
|
316
|
+
# Second close: no change to tree → lint should be skipped
|
|
317
|
+
run_cli(repo, "advance")
|
|
318
|
+
# (could be fix_regression again due to suite failure, but lint must NOT re-run)
|
|
319
|
+
|
|
320
|
+
assert counter.read_text().count("\n") == 1
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
# ── cycle 12 ── a skipped gate still records a gate_result row, marked skipped
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def test_a_skipped_gate_records_a_skipped_row(repo, tmp_path):
|
|
327
|
+
"""Same scenario as cycle 11: lint passes first close, skipped second close.
|
|
328
|
+
The second gate_result row must have skipped=1."""
|
|
329
|
+
counter = tmp_path / "lint-count"
|
|
330
|
+
lint_cmds = [f"sh -c 'echo x >> {counter}'"]
|
|
331
|
+
|
|
332
|
+
(repo / "tdd.toml").write_text(_toml(lint_cmds))
|
|
333
|
+
git(repo, "add", "-A")
|
|
334
|
+
git(repo, "commit", "-q", "-m", "configure gates")
|
|
335
|
+
|
|
336
|
+
plan = write_plan(repo, SIMPLE_PLAN)
|
|
337
|
+
assert run_cli(repo, "plan", "register", plan)["ok"]
|
|
338
|
+
assert run_cli(repo, "run", "start", "--plan", plan)["ok"]
|
|
339
|
+
|
|
340
|
+
# RED/GREEN
|
|
341
|
+
(repo / "backend" / "tests" / "test_gate_probe.py").write_text(_TEST)
|
|
342
|
+
(repo / "backend" / "app" / "probe.py").write_text(_STUB)
|
|
343
|
+
assert run_cli(repo, "advance")["next_action"]["verb"] == "write_implementation"
|
|
344
|
+
(repo / "backend" / "app" / "probe.py").write_text(_IMPL)
|
|
345
|
+
assert run_cli(repo, "advance")["next_action"]["verb"] == "refactor_or_advance"
|
|
346
|
+
|
|
347
|
+
# First close: failing test (dirty) → lint passes
|
|
348
|
+
(repo / "backend" / "tests" / "test_fail.py").write_text("def test_fail():\n assert False\n")
|
|
349
|
+
out1 = run_cli(repo, "advance")
|
|
350
|
+
run_id = out1["run"]["id"]
|
|
351
|
+
assert out1["next_action"]["verb"] == "fix_regression", out1
|
|
352
|
+
|
|
353
|
+
# Second close: no tree change → lint skipped
|
|
354
|
+
run_cli(repo, "advance")
|
|
355
|
+
|
|
356
|
+
led = Ledger(gitutil.repo_identity(repo))
|
|
357
|
+
rows = led.all(
|
|
358
|
+
"SELECT skipped FROM gate_result WHERE run_id = ? AND project = 'backend' AND kind = 'lint' ORDER BY id",
|
|
359
|
+
(run_id,),
|
|
360
|
+
)
|
|
361
|
+
assert [r["skipped"] for r in rows] == [0, 1]
|
|
@@ -197,3 +197,22 @@ def test_run_gains_start_sha_column(tmp_path, ledger_home):
|
|
|
197
197
|
reopened = Ledger(tmp_path / "somerepo")
|
|
198
198
|
cols2 = {r[1] for r in reopened.db.execute("PRAGMA table_info(run)").fetchall()}
|
|
199
199
|
assert "start_sha" in cols2
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def test_gate_result_gains_tree_hash_and_skipped_columns(tmp_path, ledger_home):
|
|
203
|
+
# Fresh ledger: both columns must exist in CREATE TABLE
|
|
204
|
+
ledger = Ledger(tmp_path / "somerepo")
|
|
205
|
+
cols = {r[1] for r in ledger.db.execute("PRAGMA table_info(gate_result)").fetchall()}
|
|
206
|
+
assert "tree_hash" in cols
|
|
207
|
+
assert "skipped" in cols
|
|
208
|
+
|
|
209
|
+
# Migration path: force back to v10 (before these columns), drop them, reopen
|
|
210
|
+
ledger._write("UPDATE meta SET value = '10' WHERE key = 'schema_version'", ())
|
|
211
|
+
ledger.db.execute("ALTER TABLE gate_result DROP COLUMN tree_hash")
|
|
212
|
+
ledger.db.execute("ALTER TABLE gate_result DROP COLUMN skipped")
|
|
213
|
+
ledger.db.close()
|
|
214
|
+
|
|
215
|
+
reopened = Ledger(tmp_path / "somerepo")
|
|
216
|
+
cols2 = {r[1] for r in reopened.db.execute("PRAGMA table_info(gate_result)").fetchall()}
|
|
217
|
+
assert "tree_hash" in cols2
|
|
218
|
+
assert "skipped" in cols2
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|