Skip to content

Commit bbb2302

Browse files
Add contract hardening, oracle regression, and repair audit trail.
Ship P1 JSON repair, P2 semantic policies, versioned oracle snapshots with CI semantic changelog, repair_audit/unstable_repair gates, and live-baseline tooling so release decisions stay auditable in CI. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 7213806 commit bbb2302

57 files changed

Lines changed: 4949 additions & 171 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.ci/live_quality_kpi_baseline.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"profile": "live_baseline",
3+
"recorded_at": null,
4+
"batch_report": null,
5+
"inference_backend": null,
6+
"observed": {},
7+
"notes": "Run: bash scripts/run_live_kpi_baseline.sh — then inspect or commit after review."
8+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"profile": "live_baseline",
3+
"description": "Loose ceilings for local live runs. Tighten after record_live_kpi_baseline.py --propose-thresholds.",
4+
"max_fallback_ratio": 0.35,
5+
"max_review_rate": 1.0,
6+
"max_pass_rate_variance": 0.15,
7+
"max_image_score_variance": 0.15,
8+
"max_json_repair_exhausted_count": 10,
9+
"max_json_repair_exhausted_ratio": 0.25,
10+
"max_semantic_assert_fail_count": 50,
11+
"max_semantic_code_mismatch_count": 50,
12+
"max_strict_contract_violation_count": 5,
13+
"max_llm_judge_calls": 20,
14+
"max_llm_judge_overrides": 20,
15+
"max_unstable_repair_count": 10
16+
}

.ci/quality_kpi_thresholds.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,11 @@
22
"max_fallback_ratio": 0.01,
33
"max_review_rate": 1.0,
44
"max_pass_rate_variance": 0.05,
5-
"max_image_score_variance": 0.05
5+
"max_image_score_variance": 0.05,
6+
"max_json_repair_exhausted_count": 0,
7+
"max_json_repair_exhausted_ratio": 0.0,
8+
"max_semantic_assert_fail_count": 50,
9+
"max_semantic_code_mismatch_count": 50,
10+
"max_strict_contract_violation_count": 0,
11+
"max_unstable_repair_count": 0
612
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"max_fallback_ratio": 0.01,
3+
"max_review_rate": 1.0,
4+
"max_json_repair_exhausted_count": 0,
5+
"max_json_repair_exhausted_ratio": 0.0,
6+
"max_semantic_assert_fail_count": 0,
7+
"max_semantic_code_mismatch_count": 0,
8+
"max_strict_contract_violation_count": 0
9+
}

.github/workflows/ci.yml

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,17 @@ jobs:
5050
run: |
5151
PYTHONPATH=src pytest
5252
53+
- name: Oracle historical regression
54+
id: oracle_regression
55+
run: |
56+
PYTHONPATH=src pytest tests/test_oracle_regression.py -q
57+
58+
- name: Oracle semantic changelog (job summary)
59+
if: always()
60+
env:
61+
CI_ORACLE_SNAPSHOT_ENFORCE: ${{ github.event_name == 'pull_request' && '1' || '0' }}
62+
run: bash scripts/ci_oracle_semantic_summary.sh
63+
5364
- name: Coverage baseline gate (PR only)
5465
if: github.event_name == 'pull_request'
5566
run: |
@@ -65,8 +76,7 @@ jobs:
6576
--loopback-planner simulated \
6677
--planner-skip-health-check
6778
python scripts/check_quality_kpis.py --enforce \
68-
--max-fallback-ratio 0.01 \
69-
--max-review-rate 1.0
79+
--thresholds-file .ci/quality_kpi_thresholds.json
7080
7181
- name: Repeatability KPI smoke
7282
run: |
@@ -76,8 +86,7 @@ jobs:
7686
--loopback-planner simulated \
7787
--planner-skip-health-check
7888
python scripts/check_quality_kpis.py --enforce \
79-
--max-pass-rate-variance 0.05 \
80-
--max-image-score-variance 0.05
89+
--thresholds-file .ci/quality_kpi_thresholds.json
8190
8291
generate-reports:
8392
runs-on: ubuntu-latest

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,5 +40,6 @@ docs/InterviewNarratives.md
4040
docs/InterviewLanguagePrep.md
4141
docs/linkedin-self-healing-vision-qa.md
4242
docs/CollaborationPlan.md
43+
docs/ContractHardening.md
4344
docs/*Collaboration*.md
4445
docs/*Plan*.md

README.md

Lines changed: 81 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,65 @@ python3 src/ai_quality_agent.py --profile dev --replay-mode record --replay-file
202202
python3 src/ai_quality_agent.py --profile dev --replay-mode replay --replay-file results/replay_trace.jsonl
203203
```
204204

205+
Inference JSON contract (self-repair + strict mode):
206+
207+
Default is **off** (`max_json_repair_attempts: 0`) so CI and simulated batches stay deterministic. Enable in config under `model_settings.inference.contract`:
208+
209+
```json
210+
{
211+
"model_settings": {
212+
"inference": {
213+
"backend": "ollama_vision",
214+
"fallback_to_simulated": true,
215+
"contract": {
216+
"max_json_repair_attempts": 2,
217+
"strict_contract": false,
218+
"repair_on_empty_dict": true,
219+
"repair_prompt_suffix": "Return ONLY a single JSON object with keys decision, code, msg."
220+
}
221+
}
222+
}
223+
}
224+
```
225+
226+
Behavior:
227+
228+
- Parse/validate failures trigger up to N extra LLM calls with validation feedback (`contract_validator.py`).
229+
- Success attaches `contract_meta.repair_attempts` on the inference dict.
230+
- After exhausted repair: `ERR_MODEL_RESPONSE_422` + `repair_exhausted` in `msg`; may fall back to `simulated` unless `strict_contract` is true.
231+
- `runtime.replay_mode=replay` disables repair (same as CI replay smoke).
232+
233+
Semantic asserts (P2) run by default via `eval_settings.semantic_asserts_enabled` (default true). Row-level issues appear in `contract.semantic_errors`; batch summary includes `semantic_assert_fail_count` and `review_breakdown.SEMANTIC_ASSERT_MISMATCH` when arbitration input is overridden.
234+
235+
Oracle historical regression (frozen release semantics):
236+
237+
```bash
238+
PYTHONPATH=src pytest tests/test_oracle_regression.py -q
239+
```
240+
241+
Import a row from a past batch report into the oracle corpus:
242+
243+
```bash
244+
PYTHONPATH=src python scripts/append_oracle_case_from_batch.py \
245+
--batch-report results/dev/batch_report_YYYYMMDD_HHMMSS.json \
246+
--file your_image.jpg \
247+
--id hist-NNN-short-name \
248+
--description "What this incident was"
249+
```
250+
251+
See `tests/regression/README.md` and `docs/RegressionVersioning.md`. Rule-change drift vs committed snapshots:
252+
253+
```bash
254+
python scripts/diff_oracle_semantics.py
255+
python scripts/refresh_oracle_snapshot.py # after intentional policy change
256+
```
257+
258+
CI posts a **semantic changelog** to the GitHub job summary on every run; PRs also enforce snapshot parity (`scripts/ci_oracle_semantic_summary.sh`).
259+
260+
**JSON repair audit:** when `contract.max_json_repair_attempts > 0`, each row gets `contract_meta.repair_audit` and `unstable_repair` if decision flips across repair rounds. Default `contract.unstable_repair_release: REVIEW` downgrades release for audit. See `docs/RepairAudit.md`, `docs/FailureTaxonomy.md` (triage **IN**).
261+
262+
Replay CI uses stricter KPI thresholds (`.ci/replay_quality_kpi_thresholds.json`: `max_semantic_assert_fail_count=0`).
263+
205264
</details>
206265

207266
<details>
@@ -226,12 +285,29 @@ docker run --rm \
226285

227286
</details>
228287

288+
<details>
289+
<summary><strong>Live KPI baseline (optional, not CI)</strong></summary>
290+
291+
Profile `live_baseline` runs real inference + contract repair + adaptive backoff + LLM judge (Ollama). Requires a local server (`llama.cpp` on `:8080` or Ollama on `:11434`).
292+
293+
```bash
294+
bash scripts/run_live_kpi_baseline.sh
295+
INFERENCE_BACKEND=ollama_vision bash scripts/run_live_kpi_baseline.sh
296+
PROPOSE_THRESHOLDS=1 bash scripts/run_live_kpi_baseline.sh # tighten .ci/live_quality_kpi_thresholds.json
297+
```
298+
299+
Writes `.ci/live_quality_kpi_baseline.json` and checks loose ceilings with `--warn-only`. See `docs/LiveKPIBaseline.md`.
300+
301+
</details>
302+
229303
<details>
230304
<summary><strong>CI / local tests</strong></summary>
231305

232306
```bash
233307
python -m pip install -U pip
234308
pip install -e ".[dev]"
309+
PYTHONPATH=src pytest tests/test_oracle_regression.py -q
310+
python scripts/check_quality_kpis.py --enforce --thresholds-file .ci/quality_kpi_thresholds.json
235311
bash scripts/dev_prepush_check.sh
236312
```
237313

@@ -265,6 +341,9 @@ Workflow reference: `.github/workflows/ci.yml`
265341
- [x] Batch ranking + release arbitration
266342
- [x] Repeatability / performance / overhead analysis
267343
- [x] Automated JSON error reporting with retention
344+
- [x] Deterministic replay (JSONL planner trace + CI smoke)
345+
- [x] Contract hardening (JSON repair, semantic asserts, oracle regression)
346+
- [x] Adaptive backoff module for async HTTP (config-gated, see `docs/AdaptiveBackoff.md`)
268347

269348
**Backlog (intentionally deferred)**
270349

@@ -273,14 +352,8 @@ Workflow reference: `.github/workflows/ci.yml`
273352

274353
**Future roadmap (agentic testing hardening)**
275354

276-
- **Deterministic replay mode (VCR-style)**
277-
Priority: **P0** | Effort: **M** | Impact: **High**
278-
Record planner prompts/responses and decision-state transitions on a known-good run, then support playback-only regression mode to eliminate flaky LLM variance and reduce token spend.
279-
280-
- **Adaptive backoff + dynamic concurrency**
281-
Priority: **P1** | Effort: **M-L** | Impact: **High**
282-
Evolve from fixed semaphore limits to runtime-aware rate control (429/503 detection, exponential backoff with jitter, and temporary permit reduction) so test loops remain stable under service pressure.
283-
355+
- **LLM judge on REVIEW rows (P2.1)** — shipped (simulated, cost-capped); enable via `eval_settings.llm_judge.enabled`.
356+
- **Critique Agent** — outline in `docs/CritiqueAgent.md` (assertion strength / schema coverage recommendations, not a CI gate).
284357
- **Schema-driven auto assertion generation**
285358
Priority: **P2** | Effort: **L** | Impact: **High**
286359
Use observed response samples and Pydantic contracts to infer boundary/type assertions and scaffold `tests/test_generated_*.py` candidates, reducing manual test-authoring overhead for newly explored paths.

configs/base.json

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@
5252
"loopback_planner": {
5353
"mode": "simulated",
5454
"require_healthy_on_startup": true,
55+
"contract": {
56+
"max_json_repair_attempts": 0,
57+
"unstable_repair_release": "REVIEW"
58+
},
5559
"llm": {
5660
"host": "http://127.0.0.1:8080",
5761
"endpoint": "/v1/chat/completions",
@@ -60,6 +64,14 @@
6064
"temperature": 0.0,
6165
"max_tokens": 200
6266
}
67+
},
68+
"adaptive_backoff": {
69+
"enabled": false,
70+
"max_retries": 2,
71+
"base_delay_s": 0.25,
72+
"max_delay_s": 8.0,
73+
"jitter_ratio": 0.2,
74+
"concurrency_floor": 1
6375
}
6476
},
6577
"quality_gate": {
@@ -71,6 +83,18 @@
7183
"engine_weight": 0.6,
7284
"model_weight": 0.4
7385
},
74-
"auto_tag_conflicts": true
86+
"auto_tag_conflicts": true,
87+
"semantic_asserts_enabled": true,
88+
"semantic_policy": {
89+
"invalid_label_release": "NO_GO",
90+
"confidence_violation_policy": "review",
91+
"inference_error_release": "NO_GO"
92+
},
93+
"llm_judge": {
94+
"enabled": false,
95+
"max_calls_per_batch": 5,
96+
"mode": "simulated",
97+
"tie_break": "conservative"
98+
}
7599
}
76100
}

configs/live_baseline.json

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
{
2+
"version": "1.2.0",
3+
"environment": "live_baseline",
4+
"metadata": {
5+
"purpose": "One-off / periodic live inference KPI baseline (not used in CI)",
6+
"notes": "Requires llama.cpp server :8080 or Ollama :11434. See docs/LiveKPIBaseline.md"
7+
},
8+
"model_settings": {
9+
"name": "Agentic Testing Framework - Live KPI",
10+
"inference": {
11+
"backend": "llama_cpp",
12+
"fallback_to_simulated": true,
13+
"contract": {
14+
"max_json_repair_attempts": 2,
15+
"strict_contract": false,
16+
"repair_on_empty_dict": true
17+
},
18+
"ollama": {
19+
"host": "http://localhost:11434",
20+
"model": "llava:7b",
21+
"timeout_s": 60
22+
},
23+
"llama_cpp": {
24+
"host": "http://127.0.0.1:8080",
25+
"endpoint": "/v1/chat/completions",
26+
"model": "local-model",
27+
"timeout_s": 60,
28+
"temperature": 0.0,
29+
"max_tokens": 256,
30+
"use_response_format": true
31+
}
32+
}
33+
},
34+
"thresholds": {
35+
"min_sharpness": 20.0,
36+
"min_brightness": 40.0,
37+
"max_brightness": 220.0,
38+
"timeout_ms": 8000
39+
},
40+
"folders": {
41+
"input": "test_images",
42+
"output": "results/live_baseline",
43+
"logs": "logs/live_baseline"
44+
},
45+
"runtime": {
46+
"oom_probability": 0.0,
47+
"async_per_image_timeout_s": 45,
48+
"async_backend_health_check": true,
49+
"async_backend_health_timeout_s": 3.0,
50+
"adaptive_backoff": {
51+
"enabled": true,
52+
"max_retries": 3,
53+
"base_delay_s": 0.5,
54+
"max_delay_s": 12.0,
55+
"jitter_ratio": 0.2,
56+
"concurrency_floor": 1
57+
},
58+
"loopback_planner": {
59+
"mode": "simulated",
60+
"require_healthy_on_startup": false,
61+
"contract": {
62+
"max_json_repair_attempts": 1
63+
}
64+
}
65+
},
66+
"quality_gate": {
67+
"target_pass_rate": 75.0
68+
},
69+
"eval_settings": {
70+
"conflict_strategy": "conservative",
71+
"semantic_asserts_enabled": true,
72+
"semantic_policy": {
73+
"invalid_label_release": "NO_GO",
74+
"confidence_violation_policy": "review",
75+
"inference_error_release": "NO_GO"
76+
},
77+
"llm_judge": {
78+
"enabled": true,
79+
"max_calls_per_batch": 8,
80+
"mode": "ollama",
81+
"tie_break": "conservative",
82+
"host": "http://localhost:11434",
83+
"model": "llama3.2",
84+
"timeout_s": 45
85+
}
86+
}
87+
}

docs/AdaptiveBackoff.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Adaptive Backoff (async inference)
2+
3+
Implemented in `src/util/adaptive_backoff.py` and wired into `src/models/async_inference.py`.
4+
5+
## Purpose
6+
7+
When live inference backends return **429/5xx**, async batch runs can retry with exponential backoff + jitter instead of immediately falling back to simulated results.
8+
9+
## Config (`runtime.adaptive_backoff`)
10+
11+
| Key | Default | Meaning |
12+
|-----|---------|---------|
13+
| `enabled` | `false` | Master switch |
14+
| `max_retries` | `2` | Extra attempts after pressure response |
15+
| `base_delay_s` | `0.25` | Initial backoff |
16+
| `max_delay_s` | `8.0` | Cap per sleep |
17+
| `jitter_ratio` | `0.2` | Randomized delay fraction |
18+
| `concurrency_floor` | `1` | Future hook for lowering async concurrency under pressure |
19+
20+
## Enable (example)
21+
22+
```json
23+
"runtime": {
24+
"adaptive_backoff": {
25+
"enabled": true,
26+
"max_retries": 3,
27+
"base_delay_s": 0.5,
28+
"max_delay_s": 10.0
29+
}
30+
}
31+
```
32+
33+
## Notes
34+
35+
- Retries are **HTTP-layer only**; they do not re-run vision metrics.
36+
- Simulated backend is unaffected (no HTTP).
37+
- Pair with `--async-concurrency` tuning on live profiles after measuring 429 rates.

0 commit comments

Comments
 (0)