@matt82198/aesop 0.3.2 → 0.4.0
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.
- package/CHANGELOG.md +23 -0
- package/README.md +14 -6
- package/aesop.config.example.json +16 -0
- package/bin/cli.js +5 -2
- package/docs/INSTALL.md +104 -14
- package/docs/MICROKERNEL.md +452 -0
- package/docs/README.md +4 -0
- package/docs/THE-AESOP-HYPOTHESIS.md +140 -0
- package/driver/CLAUDE.md +123 -123
- package/driver/README.md +40 -2
- package/driver/adjudication_gate.py +367 -0
- package/driver/aesop.config.example.json +20 -4
- package/driver/backend_config.py +603 -12
- package/driver/claude_code_driver.py +9 -17
- package/driver/codex_driver.py +80 -25
- package/driver/context_pack.py +454 -0
- package/driver/openai_compatible_driver.py +53 -11
- package/driver/openai_transport.py +31 -10
- package/driver/orchestrator_backend.py +332 -0
- package/driver/orchestrator_driver.py +589 -0
- package/driver/proc_util.py +134 -0
- package/driver/wave_loop.py +801 -59
- package/driver/wave_scheduler.py +361 -37
- package/monitor/collect-signals.mjs +83 -0
- package/package.json +1 -1
- package/state_store/coordination.py +65 -5
- package/tools/ci_merge_wait.py +88 -42
- package/tools/ci_shard_runner.py +128 -0
- package/tools/seated_shadow_adjudication.py +920 -0
- package/tools/shadow_adjudication.py +1024 -0
- package/tools/verify_ui_trio_redaction_proof.py +292 -0
|
@@ -0,0 +1,920 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Seated shadow adjudication — redo over the wired seam (increment 4a).
|
|
3
|
+
|
|
4
|
+
Runs the adjudication corpus through OrchestratorDriver.decide() using REAL
|
|
5
|
+
context packs (file brain + cited evidence) on a challenger backend, N>=3 times
|
|
6
|
+
per model. Measures stability and persists reasoning.
|
|
7
|
+
|
|
8
|
+
Frontier-first: run gpt-5.6-sol first. If item 9 (whitelist-gate-weakening)
|
|
9
|
+
does NOT flip to false_positive modally, abort the cheaper run.
|
|
10
|
+
|
|
11
|
+
Requirements:
|
|
12
|
+
1. Route EVERY adjudication through real OrchestratorDriver.decide() seam.
|
|
13
|
+
2. schema_valid MUST be true in outputs (assert it).
|
|
14
|
+
3. Persist challenger's full reasoning text per verdict.
|
|
15
|
+
4. N>=3 repeats; report modal verdict + stability (>=2/3 for true mode).
|
|
16
|
+
5. Real context packs via build_context_pack: real file brain + cited code.
|
|
17
|
+
6. Labels NEVER in the pack (assert).
|
|
18
|
+
|
|
19
|
+
CLI: python tools/seated_shadow_adjudication.py --corpus <path> --model <model>
|
|
20
|
+
[--offline | --live] --repeat N [--out-tag TAG]
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import argparse
|
|
24
|
+
import json
|
|
25
|
+
import os
|
|
26
|
+
import re
|
|
27
|
+
import sys
|
|
28
|
+
from dataclasses import dataclass, field
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
31
|
+
|
|
32
|
+
# Add driver/ to sys.path.
|
|
33
|
+
REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
34
|
+
DRIVER_DIR = REPO_ROOT / "driver"
|
|
35
|
+
if str(DRIVER_DIR) not in sys.path:
|
|
36
|
+
sys.path.insert(0, str(DRIVER_DIR))
|
|
37
|
+
|
|
38
|
+
from context_pack import build_context_pack, ContextPack # noqa: E402
|
|
39
|
+
from orchestrator_driver import OrchestratorDriver # noqa: E402
|
|
40
|
+
from orchestrator_backend import ( # noqa: E402
|
|
41
|
+
FakeOrchestratorBackend,
|
|
42
|
+
HarnessOrchestratorBackend,
|
|
43
|
+
OpenAICompatibleOrchestratorBackend,
|
|
44
|
+
)
|
|
45
|
+
from openai_transport import default_openai_transport # noqa: E402
|
|
46
|
+
|
|
47
|
+
# Fallback challenger model when neither --model nor seats.orchestrator names
|
|
48
|
+
# one (frontier-first, matching the legacy CLI default).
|
|
49
|
+
DEFAULT_CHALLENGER_MODEL = "gpt-5.6-sol"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def build_live_backend(cli_model=None, config_path=None):
|
|
53
|
+
"""Build the live orchestrator backend from the seats config (HS-1).
|
|
54
|
+
|
|
55
|
+
Resolution:
|
|
56
|
+
- seats.orchestrator (openai-compatible) in aesop.config.json supplies
|
|
57
|
+
model/base_url/api_key_env/is_local for the seat.
|
|
58
|
+
- CLI --model, when given, OVERRIDES the seat's model (seat knobs stay).
|
|
59
|
+
- No configured seat (or harness/claude seat) -> legacy hosted-OpenAI
|
|
60
|
+
default with cli_model or DEFAULT_CHALLENGER_MODEL (behavior
|
|
61
|
+
unchanged for installs without a seats block).
|
|
62
|
+
|
|
63
|
+
Offline-safe: no API key is read here.
|
|
64
|
+
"""
|
|
65
|
+
from backend_config import ( # noqa: E402 (driver/ on sys.path above)
|
|
66
|
+
build_orchestrator_backend,
|
|
67
|
+
load_backend_config,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
config = load_backend_config(config_path)
|
|
71
|
+
backend = build_orchestrator_backend(config)
|
|
72
|
+
if isinstance(backend, HarnessOrchestratorBackend):
|
|
73
|
+
return OpenAICompatibleOrchestratorBackend(
|
|
74
|
+
model=cli_model or DEFAULT_CHALLENGER_MODEL,
|
|
75
|
+
transport=default_openai_transport,
|
|
76
|
+
)
|
|
77
|
+
if cli_model:
|
|
78
|
+
backend.model = cli_model
|
|
79
|
+
return backend
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
# ============================================================================
|
|
83
|
+
# Path redaction helper (privacy leak prevention)
|
|
84
|
+
# ============================================================================
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def redact_paths(text: str) -> str:
|
|
88
|
+
"""Redact absolute machine paths from text.
|
|
89
|
+
|
|
90
|
+
Replaces:
|
|
91
|
+
- C:\\Users\\matt8\\aesop → <REPO> (handles single or double backslashes)
|
|
92
|
+
- /c/Users/matt8/aesop → <REPO>
|
|
93
|
+
- C:\\Users\\matt8 → <HOME> (handles single or double backslashes)
|
|
94
|
+
- /c/Users/matt8 → <HOME>
|
|
95
|
+
- Users/matt8 → <HOME>
|
|
96
|
+
- Users\\matt8 → <HOME>
|
|
97
|
+
|
|
98
|
+
Non-path occurrences of 'matt8' are preserved.
|
|
99
|
+
|
|
100
|
+
Separators match ANY run of backslashes or forward slashes so single-,
|
|
101
|
+
double-, and repr/JSON re-escaped forms (\\, \\\\, \\\\\\\\ ...) are all
|
|
102
|
+
caught. Reasoning strings often embed repr-of-list text whose paths carry
|
|
103
|
+
2x/4x-escaped separators; a 1-2 backslash pattern misses those.
|
|
104
|
+
"""
|
|
105
|
+
# First redact repo-specific paths (longer pattern, must be first)
|
|
106
|
+
# Windows: C:\Users\matt8\aesop at any escape depth, or C:/Users/... form
|
|
107
|
+
text = re.sub(
|
|
108
|
+
r"C:[\\/]+Users[\\/]+matt8[\\/]+aesop",
|
|
109
|
+
"<REPO>",
|
|
110
|
+
text,
|
|
111
|
+
flags=re.IGNORECASE,
|
|
112
|
+
)
|
|
113
|
+
# POSIX: /c/Users/matt8/aesop
|
|
114
|
+
text = re.sub(
|
|
115
|
+
r"/c/Users/matt8/aesop",
|
|
116
|
+
"<REPO>",
|
|
117
|
+
text,
|
|
118
|
+
flags=re.IGNORECASE,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
# Then redact home paths (shorter pattern)
|
|
122
|
+
# Windows: C:\Users\matt8 at any escape depth, or C:/Users/matt8
|
|
123
|
+
text = re.sub(
|
|
124
|
+
r"C:[\\/]+Users[\\/]+matt8",
|
|
125
|
+
"<HOME>",
|
|
126
|
+
text,
|
|
127
|
+
flags=re.IGNORECASE,
|
|
128
|
+
)
|
|
129
|
+
# POSIX: /c/Users/matt8
|
|
130
|
+
text = re.sub(
|
|
131
|
+
r"/c/Users/matt8",
|
|
132
|
+
"<HOME>",
|
|
133
|
+
text,
|
|
134
|
+
flags=re.IGNORECASE,
|
|
135
|
+
)
|
|
136
|
+
# Also handle Users\matt8 / Users\\matt8 / Users/matt8 at any escape depth
|
|
137
|
+
text = re.sub(
|
|
138
|
+
r"Users[\\/]+matt8",
|
|
139
|
+
"<HOME>",
|
|
140
|
+
text,
|
|
141
|
+
flags=re.IGNORECASE,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
return text
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def redact_data_structure(obj: Any) -> Any:
|
|
148
|
+
"""Recursively redact paths from all string values in a data structure.
|
|
149
|
+
|
|
150
|
+
Handles dicts, lists, and strings. Returns a new object with paths redacted.
|
|
151
|
+
"""
|
|
152
|
+
if isinstance(obj, str):
|
|
153
|
+
return redact_paths(obj)
|
|
154
|
+
elif isinstance(obj, dict):
|
|
155
|
+
return {k: redact_data_structure(v) for k, v in obj.items()}
|
|
156
|
+
elif isinstance(obj, list):
|
|
157
|
+
return [redact_data_structure(item) for item in obj]
|
|
158
|
+
else:
|
|
159
|
+
return obj
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
# ============================================================================
|
|
163
|
+
# Data structures
|
|
164
|
+
# ============================================================================
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
@dataclass
|
|
168
|
+
class CorpusItem:
|
|
169
|
+
"""One adjudication item from the corpus."""
|
|
170
|
+
|
|
171
|
+
id: str
|
|
172
|
+
finding_text: str
|
|
173
|
+
source_lens: str
|
|
174
|
+
incumbent_verdict: str
|
|
175
|
+
ground_truth: str
|
|
176
|
+
gt_note: str
|
|
177
|
+
evidence: List[str] = field(default_factory=list)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
@dataclass
|
|
181
|
+
class SeatedVerdictItem:
|
|
182
|
+
"""One seated adjudication verdict with persisted reasoning."""
|
|
183
|
+
|
|
184
|
+
id: str
|
|
185
|
+
run_num: int
|
|
186
|
+
challenger_classification: str
|
|
187
|
+
challenger_reasoning: str # Full reasoning text from the verdict
|
|
188
|
+
schema_valid: bool
|
|
189
|
+
retries_used: int
|
|
190
|
+
confidence: float = 0.0
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
@dataclass
|
|
194
|
+
class AggregatedItem:
|
|
195
|
+
"""Per-item results aggregated across multiple runs."""
|
|
196
|
+
|
|
197
|
+
id: str
|
|
198
|
+
ground_truth: str
|
|
199
|
+
verdict_counts: Dict[str, int] = field(default_factory=dict)
|
|
200
|
+
modal_verdict: str = "undetermined"
|
|
201
|
+
stability: float = 0.0
|
|
202
|
+
num_runs: int = 1
|
|
203
|
+
all_verdicts: List[str] = field(default_factory=list)
|
|
204
|
+
reasonings: List[str] = field(default_factory=list)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
# ============================================================================
|
|
208
|
+
# Load and build context
|
|
209
|
+
# ============================================================================
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def load_corpus(corpus_path: str) -> List[CorpusItem]:
|
|
213
|
+
"""Load corpus from jsonl file."""
|
|
214
|
+
items = []
|
|
215
|
+
with open(corpus_path, "r", encoding="utf-8") as f:
|
|
216
|
+
for line_num, line in enumerate(f, 1):
|
|
217
|
+
if not line.strip():
|
|
218
|
+
continue
|
|
219
|
+
try:
|
|
220
|
+
obj = json.loads(line)
|
|
221
|
+
labels = obj.get("labels", {})
|
|
222
|
+
item = CorpusItem(
|
|
223
|
+
id=obj["id"],
|
|
224
|
+
finding_text=obj["finding_text"],
|
|
225
|
+
source_lens=obj["source_lens"],
|
|
226
|
+
incumbent_verdict=labels.get("incumbent_verdict", "unknown"),
|
|
227
|
+
ground_truth=labels.get("ground_truth", "unknown"),
|
|
228
|
+
gt_note=labels.get("gt_note", ""),
|
|
229
|
+
evidence=obj.get("evidence", []),
|
|
230
|
+
)
|
|
231
|
+
items.append(item)
|
|
232
|
+
except (json.JSONDecodeError, KeyError) as e:
|
|
233
|
+
print(f"Error parsing corpus line {line_num}: {e}", file=sys.stderr)
|
|
234
|
+
raise
|
|
235
|
+
return items
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def build_seated_context_pack(
|
|
239
|
+
item: CorpusItem,
|
|
240
|
+
repo_root: str,
|
|
241
|
+
conductor_root: str,
|
|
242
|
+
) -> ContextPack:
|
|
243
|
+
"""Build a REAL context pack for seated adjudication.
|
|
244
|
+
|
|
245
|
+
Args:
|
|
246
|
+
item: Corpus item with finding_text, source_lens, and evidence.
|
|
247
|
+
repo_root: Path to aesop repo root.
|
|
248
|
+
conductor_root: Path to conductor3 root.
|
|
249
|
+
|
|
250
|
+
Returns:
|
|
251
|
+
ContextPack with:
|
|
252
|
+
- File brain (STATE.md, tracker.json, BUILDLOG.md, MEMORY.md)
|
|
253
|
+
- Evidence dict with cited code/facts/behavior
|
|
254
|
+
- Finding framing (NO labels)
|
|
255
|
+
"""
|
|
256
|
+
# Build sources dict for file brain (allowlisted sources only).
|
|
257
|
+
sources = {
|
|
258
|
+
"state": None, # STATE.md from repo or conductor root
|
|
259
|
+
"buildlog_tail:50": None, # BUILDLOG.md tail (last 50 lines)
|
|
260
|
+
"tracker_open": None, # Open tracker items from tracker.json
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
# Build the finding framing as evidence (BLIND: no labels).
|
|
264
|
+
finding_brief = f"""FINDING: {item.finding_text}
|
|
265
|
+
|
|
266
|
+
Source lens: {item.source_lens}
|
|
267
|
+
|
|
268
|
+
Please adjudicate this finding. Classify it as one of:
|
|
269
|
+
- real_defect: actionable code/process defect needing a fix
|
|
270
|
+
- false_positive: not actually a defect (e.g., misunderstood, refuted by evidence)
|
|
271
|
+
- enhancement_opportunity: nice-to-have, not blocking
|
|
272
|
+
- undetermined: insufficient information to classify
|
|
273
|
+
|
|
274
|
+
Provide evidence supporting your classification."""
|
|
275
|
+
|
|
276
|
+
# Build evidence dict with finding + corpus evidence.
|
|
277
|
+
evidence_dict = {}
|
|
278
|
+
evidence_dict["finding"] = finding_brief # The framing question
|
|
279
|
+
for idx, evidence_text in enumerate(item.evidence):
|
|
280
|
+
evidence_dict[f"evidence_{idx}"] = evidence_text
|
|
281
|
+
|
|
282
|
+
# Build the real context pack with file brain + evidence.
|
|
283
|
+
pack = build_context_pack(
|
|
284
|
+
decision_type="adjudicate_finding",
|
|
285
|
+
sources=sources,
|
|
286
|
+
repo_root=repo_root,
|
|
287
|
+
conductor_root=conductor_root,
|
|
288
|
+
size_cap=32768, # 32KB for main content
|
|
289
|
+
evidence=evidence_dict,
|
|
290
|
+
evidence_cap=8192, # 8KB for evidence (finding + evidence items)
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
# ASSERT: labels never reach the pack.
|
|
294
|
+
pack_text = json.dumps(pack.content) + json.dumps(pack.evidence)
|
|
295
|
+
for label_key in ["incumbent_verdict", "ground_truth", "gt_note"]:
|
|
296
|
+
if label_key in pack_text:
|
|
297
|
+
raise ValueError(
|
|
298
|
+
f"Blind adjudication violated: label '{label_key}' leaked into pack for {item.id}"
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
return pack
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def adjudicate_one_finding(
|
|
305
|
+
driver: OrchestratorDriver,
|
|
306
|
+
item: CorpusItem,
|
|
307
|
+
repo_root: str,
|
|
308
|
+
conductor_root: str,
|
|
309
|
+
schema: Optional[Dict[str, Any]],
|
|
310
|
+
run_num: int,
|
|
311
|
+
) -> SeatedVerdictItem:
|
|
312
|
+
"""Run one seated adjudication through the real seam.
|
|
313
|
+
|
|
314
|
+
Args:
|
|
315
|
+
driver: OrchestratorDriver with real backend.
|
|
316
|
+
item: Corpus item.
|
|
317
|
+
repo_root: Repo root path.
|
|
318
|
+
conductor_root: Conductor root path.
|
|
319
|
+
schema: Decision schema.
|
|
320
|
+
run_num: Run number (for logging).
|
|
321
|
+
|
|
322
|
+
Returns:
|
|
323
|
+
SeatedVerdictItem with verdict, reasoning, and metadata.
|
|
324
|
+
"""
|
|
325
|
+
try:
|
|
326
|
+
# Build real context pack.
|
|
327
|
+
pack = build_seated_context_pack(item, repo_root, conductor_root)
|
|
328
|
+
|
|
329
|
+
# Call the driver (real seam).
|
|
330
|
+
decision = driver.decide("adjudicate_finding", pack, schema=schema)
|
|
331
|
+
|
|
332
|
+
# Extract verdict and reasoning.
|
|
333
|
+
# Verdict can be a string (classification name) or dict with classification field.
|
|
334
|
+
verdict_obj = decision.get("verdict", {})
|
|
335
|
+
if isinstance(verdict_obj, dict):
|
|
336
|
+
classification = verdict_obj.get("classification", "undetermined")
|
|
337
|
+
else:
|
|
338
|
+
classification = str(verdict_obj).lower() if verdict_obj else "undetermined"
|
|
339
|
+
|
|
340
|
+
# Persist reasoning: use evidence field from the verdict.
|
|
341
|
+
reasoning = decision.get("evidence", "")
|
|
342
|
+
if not isinstance(reasoning, str):
|
|
343
|
+
reasoning = str(reasoning)
|
|
344
|
+
|
|
345
|
+
schema_valid = decision.get("schema_validated", False)
|
|
346
|
+
retries = decision.get("retry_count", 0)
|
|
347
|
+
confidence = decision.get("confidence", 0.0)
|
|
348
|
+
if not isinstance(confidence, (int, float)):
|
|
349
|
+
confidence = 0.0
|
|
350
|
+
|
|
351
|
+
# ASSERT: schema_valid must be true for real seam.
|
|
352
|
+
if not schema_valid:
|
|
353
|
+
print(
|
|
354
|
+
f"WARNING: {item.id} run {run_num}: schema_valid=false (decision={decision})",
|
|
355
|
+
file=sys.stderr,
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
return SeatedVerdictItem(
|
|
359
|
+
id=item.id,
|
|
360
|
+
run_num=run_num,
|
|
361
|
+
challenger_classification=classification,
|
|
362
|
+
challenger_reasoning=reasoning,
|
|
363
|
+
schema_valid=schema_valid,
|
|
364
|
+
retries_used=retries,
|
|
365
|
+
confidence=confidence,
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
except Exception as e:
|
|
369
|
+
print(
|
|
370
|
+
f"Error adjudicating {item.id} run {run_num}: {e}",
|
|
371
|
+
file=sys.stderr,
|
|
372
|
+
)
|
|
373
|
+
return SeatedVerdictItem(
|
|
374
|
+
id=item.id,
|
|
375
|
+
run_num=run_num,
|
|
376
|
+
challenger_classification="DECISION_FAILED",
|
|
377
|
+
challenger_reasoning=f"Error: {e}",
|
|
378
|
+
schema_valid=False,
|
|
379
|
+
retries_used=0,
|
|
380
|
+
confidence=0.0,
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
# ============================================================================
|
|
385
|
+
# Aggregation and reporting
|
|
386
|
+
# ============================================================================
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def aggregate_seated_results(
|
|
390
|
+
all_verdicts: List[SeatedVerdictItem],
|
|
391
|
+
corpus: List[CorpusItem],
|
|
392
|
+
num_runs: int,
|
|
393
|
+
) -> Dict[str, Any]:
|
|
394
|
+
"""Aggregate seated verdicts across N runs.
|
|
395
|
+
|
|
396
|
+
Args:
|
|
397
|
+
all_verdicts: All SeatedVerdictItem from all runs.
|
|
398
|
+
corpus: The corpus items.
|
|
399
|
+
num_runs: Number of runs (global maximum; some items may have fewer).
|
|
400
|
+
|
|
401
|
+
Returns:
|
|
402
|
+
Dict with:
|
|
403
|
+
- per_item: List[AggregatedItem] with modal verdicts and stability.
|
|
404
|
+
- item_9_analysis: Special analysis of item 9 (whitelist-gate-weakening).
|
|
405
|
+
- schema_validity: Count and pct of schema_valid verdicts.
|
|
406
|
+
- held_real_defects: Count of real_defect items that stayed real_defect modally.
|
|
407
|
+
"""
|
|
408
|
+
# Group verdicts by item id.
|
|
409
|
+
item_verdicts = {}
|
|
410
|
+
item_reasonings = {}
|
|
411
|
+
for corpus_item in corpus:
|
|
412
|
+
item_verdicts[corpus_item.id] = []
|
|
413
|
+
item_reasonings[corpus_item.id] = []
|
|
414
|
+
|
|
415
|
+
for verdict in all_verdicts:
|
|
416
|
+
if verdict.id in item_verdicts:
|
|
417
|
+
item_verdicts[verdict.id].append(verdict.challenger_classification)
|
|
418
|
+
item_reasonings[verdict.id].append(verdict.challenger_reasoning)
|
|
419
|
+
|
|
420
|
+
# Compute per-item aggregations.
|
|
421
|
+
aggregated_items = []
|
|
422
|
+
for corpus_item in corpus:
|
|
423
|
+
verdicts = item_verdicts.get(corpus_item.id, [])
|
|
424
|
+
reasonings = item_reasonings.get(corpus_item.id, [])
|
|
425
|
+
|
|
426
|
+
# Modal verdict (excluding DECISION_FAILED).
|
|
427
|
+
verdict_counts = {}
|
|
428
|
+
for v in verdicts:
|
|
429
|
+
verdict_counts[v] = verdict_counts.get(v, 0) + 1
|
|
430
|
+
|
|
431
|
+
# Exclude DECISION_FAILED from modal computation.
|
|
432
|
+
valid_verdicts = {
|
|
433
|
+
v: count for v, count in verdict_counts.items() if v != "DECISION_FAILED"
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
# Use actual number of verdicts for this item (handles incomplete runs)
|
|
437
|
+
actual_runs_for_item = len(verdicts)
|
|
438
|
+
|
|
439
|
+
if valid_verdicts:
|
|
440
|
+
modal_verdict = max(valid_verdicts, key=valid_verdicts.get)
|
|
441
|
+
modal_count = valid_verdicts[modal_verdict]
|
|
442
|
+
stability = modal_count / actual_runs_for_item if actual_runs_for_item > 0 else 0.0
|
|
443
|
+
elif verdict_counts:
|
|
444
|
+
# All verdicts were DECISION_FAILED
|
|
445
|
+
modal_verdict = "all_runs_failed"
|
|
446
|
+
modal_count = 0
|
|
447
|
+
stability = 0.0
|
|
448
|
+
else:
|
|
449
|
+
modal_verdict = "undetermined"
|
|
450
|
+
modal_count = 0
|
|
451
|
+
stability = 0.0
|
|
452
|
+
|
|
453
|
+
agg_item = AggregatedItem(
|
|
454
|
+
id=corpus_item.id,
|
|
455
|
+
ground_truth=corpus_item.ground_truth,
|
|
456
|
+
verdict_counts=verdict_counts,
|
|
457
|
+
modal_verdict=modal_verdict,
|
|
458
|
+
stability=stability,
|
|
459
|
+
num_runs=num_runs,
|
|
460
|
+
all_verdicts=verdicts,
|
|
461
|
+
reasonings=reasonings,
|
|
462
|
+
)
|
|
463
|
+
aggregated_items.append(agg_item)
|
|
464
|
+
|
|
465
|
+
# Item 9 analysis (whitelist-gate-weakening).
|
|
466
|
+
item_9 = next(
|
|
467
|
+
(i for i in aggregated_items if i.id == "whitelist-gate-weakening"), None
|
|
468
|
+
)
|
|
469
|
+
item_9_analysis = {
|
|
470
|
+
"id": "whitelist-gate-weakening",
|
|
471
|
+
"ground_truth": "false_positive",
|
|
472
|
+
"modal_verdict": item_9.modal_verdict if item_9 else "not_found",
|
|
473
|
+
"modal_count": item_9.verdict_counts.get(
|
|
474
|
+
item_9.modal_verdict, 0
|
|
475
|
+
) if item_9 else 0,
|
|
476
|
+
"stability": item_9.stability if item_9 else 0.0,
|
|
477
|
+
"flips_to_false_positive": item_9.modal_verdict == "false_positive"
|
|
478
|
+
if item_9
|
|
479
|
+
else False,
|
|
480
|
+
"reasoning_sample": item_9.reasonings[0]
|
|
481
|
+
if item_9 and item_9.reasonings
|
|
482
|
+
else "",
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
# Held real defects.
|
|
486
|
+
held_real_defects = sum(
|
|
487
|
+
1
|
|
488
|
+
for agg in aggregated_items
|
|
489
|
+
if agg.ground_truth.lower() == "real_defect"
|
|
490
|
+
and agg.modal_verdict.lower() == "real_defect"
|
|
491
|
+
)
|
|
492
|
+
|
|
493
|
+
# Schema validity.
|
|
494
|
+
schema_valid_verdicts = sum(
|
|
495
|
+
1 for v in all_verdicts if v.schema_valid
|
|
496
|
+
)
|
|
497
|
+
total_verdicts = len(all_verdicts)
|
|
498
|
+
schema_valid_pct = (
|
|
499
|
+
(schema_valid_verdicts / total_verdicts) * 100
|
|
500
|
+
if total_verdicts > 0
|
|
501
|
+
else 0
|
|
502
|
+
)
|
|
503
|
+
|
|
504
|
+
return {
|
|
505
|
+
"per_item": aggregated_items,
|
|
506
|
+
"item_9_analysis": item_9_analysis,
|
|
507
|
+
"schema_validity": {
|
|
508
|
+
"valid": schema_valid_verdicts,
|
|
509
|
+
"total": total_verdicts,
|
|
510
|
+
"pct": round(schema_valid_pct, 1),
|
|
511
|
+
},
|
|
512
|
+
"held_real_defects": held_real_defects,
|
|
513
|
+
"total_real_defects": sum(
|
|
514
|
+
1 for c in corpus if c.ground_truth.lower() == "real_defect"
|
|
515
|
+
),
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
def write_seated_json(
|
|
520
|
+
verdicts: List[SeatedVerdictItem],
|
|
521
|
+
corpus: List[CorpusItem],
|
|
522
|
+
aggregated: Dict[str, Any],
|
|
523
|
+
model: str,
|
|
524
|
+
output_path: str,
|
|
525
|
+
) -> None:
|
|
526
|
+
"""Write seated results as JSON."""
|
|
527
|
+
# Per-run verdicts.
|
|
528
|
+
verdict_items = [
|
|
529
|
+
{
|
|
530
|
+
"id": v.id,
|
|
531
|
+
"run_num": v.run_num,
|
|
532
|
+
"classification": v.challenger_classification,
|
|
533
|
+
"reasoning": v.challenger_reasoning,
|
|
534
|
+
"schema_valid": v.schema_valid,
|
|
535
|
+
"retries": v.retries_used,
|
|
536
|
+
"confidence": v.confidence,
|
|
537
|
+
}
|
|
538
|
+
for v in verdicts
|
|
539
|
+
]
|
|
540
|
+
|
|
541
|
+
# Per-item aggregations.
|
|
542
|
+
agg_items = [
|
|
543
|
+
{
|
|
544
|
+
"id": agg.id,
|
|
545
|
+
"ground_truth": agg.ground_truth,
|
|
546
|
+
"modal_verdict": agg.modal_verdict,
|
|
547
|
+
"stability": round(agg.stability, 3),
|
|
548
|
+
"verdict_counts": agg.verdict_counts,
|
|
549
|
+
"num_runs": agg.num_runs,
|
|
550
|
+
"reasoning_sample": agg.reasonings[0]
|
|
551
|
+
if agg.reasonings
|
|
552
|
+
else "",
|
|
553
|
+
}
|
|
554
|
+
for agg in aggregated["per_item"]
|
|
555
|
+
]
|
|
556
|
+
|
|
557
|
+
data = {
|
|
558
|
+
"metadata": {
|
|
559
|
+
"model": model,
|
|
560
|
+
"num_runs": aggregated["per_item"][0].num_runs
|
|
561
|
+
if aggregated["per_item"]
|
|
562
|
+
else 1,
|
|
563
|
+
"timestamp": "2026-07-24",
|
|
564
|
+
"corpus_size": len(corpus),
|
|
565
|
+
},
|
|
566
|
+
"item_9_flip": aggregated["item_9_analysis"]["flips_to_false_positive"],
|
|
567
|
+
"item_9_reasoning": aggregated["item_9_analysis"]["reasoning_sample"],
|
|
568
|
+
"held_real_defects": aggregated["held_real_defects"],
|
|
569
|
+
"schema_validity": aggregated["schema_validity"],
|
|
570
|
+
"per_run_verdicts": verdict_items,
|
|
571
|
+
"per_item_aggregations": agg_items,
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
# Redact paths before writing
|
|
575
|
+
data = redact_data_structure(data)
|
|
576
|
+
|
|
577
|
+
with open(output_path, "w", encoding="utf-8") as f:
|
|
578
|
+
json.dump(data, f, indent=2, ensure_ascii=True)
|
|
579
|
+
|
|
580
|
+
print(f"Wrote seated results JSON: {output_path}")
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
def write_seated_md(
|
|
584
|
+
aggregated: Dict[str, Any],
|
|
585
|
+
corpus: List[CorpusItem],
|
|
586
|
+
model: str,
|
|
587
|
+
output_path: str,
|
|
588
|
+
) -> None:
|
|
589
|
+
"""Write seated results as Markdown."""
|
|
590
|
+
item_9 = aggregated["item_9_analysis"]
|
|
591
|
+
schema_valid = aggregated["schema_validity"]
|
|
592
|
+
held = aggregated["held_real_defects"]
|
|
593
|
+
total_real = aggregated["total_real_defects"]
|
|
594
|
+
|
|
595
|
+
num_runs = (
|
|
596
|
+
aggregated["per_item"][0].num_runs
|
|
597
|
+
if aggregated["per_item"]
|
|
598
|
+
else 1
|
|
599
|
+
)
|
|
600
|
+
|
|
601
|
+
lines = [
|
|
602
|
+
"# Seated Shadow Adjudication — Increment 4a Redo",
|
|
603
|
+
"",
|
|
604
|
+
"**Date**: 2026-07-24",
|
|
605
|
+
f"**Challenger Model**: {model}",
|
|
606
|
+
f"**Runs**: {num_runs}",
|
|
607
|
+
f"**Corpus Size**: {len(corpus)} items",
|
|
608
|
+
f"**Seam**: OrchestratorDriver.decide() with OpenAICompatibleOrchestratorBackend (wired seam, increment 1.5)",
|
|
609
|
+
"",
|
|
610
|
+
"## Summary",
|
|
611
|
+
"",
|
|
612
|
+
"### Item 9 Flip Verdict (Key Test)",
|
|
613
|
+
"",
|
|
614
|
+
f"**Item**: whitelist-gate-weakening (gt=false_positive)",
|
|
615
|
+
f"**Modal verdict**: {item_9['modal_verdict']}",
|
|
616
|
+
f"**Stability**: {item_9['stability']:.1%} ({item_9['modal_count']}/{num_runs} runs)",
|
|
617
|
+
f"**Flips to false_positive**: {'YES' if item_9['flips_to_false_positive'] else 'NO'}",
|
|
618
|
+
"",
|
|
619
|
+
f"**Reasoning** (first run):",
|
|
620
|
+
f"```",
|
|
621
|
+
# Redact before embedding: this is real model reasoning over real
|
|
622
|
+
# cited repo evidence and can echo absolute machine paths (verified
|
|
623
|
+
# gap -- this call was previously computed and discarded, leaving
|
|
624
|
+
# the raw unredacted text below to be persisted instead).
|
|
625
|
+
f"{redact_paths(item_9['reasoning_sample'])[:500]}...",
|
|
626
|
+
f"```",
|
|
627
|
+
"",
|
|
628
|
+
"### Real Defect Retention",
|
|
629
|
+
"",
|
|
630
|
+
f"Items with gt=real_defect: {total_real}",
|
|
631
|
+
f"Items held as real_defect (modally): {held}",
|
|
632
|
+
"",
|
|
633
|
+
"### Schema Validity",
|
|
634
|
+
"",
|
|
635
|
+
f"Valid verdicts: {schema_valid['valid']}/{schema_valid['total']} ({schema_valid['pct']:.1f}%)",
|
|
636
|
+
"",
|
|
637
|
+
"## Per-Item Results",
|
|
638
|
+
"",
|
|
639
|
+
"| ID | Ground Truth | Modal Verdict | Stability | Correct |",
|
|
640
|
+
"|---|---|---|---|---|",
|
|
641
|
+
]
|
|
642
|
+
|
|
643
|
+
for agg in aggregated["per_item"]:
|
|
644
|
+
correct = "✓" if agg.modal_verdict.lower() == agg.ground_truth.lower() else "✗"
|
|
645
|
+
lines.append(
|
|
646
|
+
f"| {agg.id} | {agg.ground_truth} | {agg.modal_verdict} | "
|
|
647
|
+
f"{agg.stability:.1%} | {correct} |"
|
|
648
|
+
)
|
|
649
|
+
|
|
650
|
+
lines.extend([
|
|
651
|
+
"",
|
|
652
|
+
"## Stale-Label Analysis",
|
|
653
|
+
"",
|
|
654
|
+
"### Item 7: hardcoded-username",
|
|
655
|
+
"**Finding-time label**: real_defect (docs shipped with path 'Users/matt8')",
|
|
656
|
+
"**Current state**: FIXED (docs/INSTALL.md has no hardcoded paths; matt8 hits are npm handle)",
|
|
657
|
+
"**Seated modal verdict**: [see table above]",
|
|
658
|
+
"",
|
|
659
|
+
"### Item 6: unc-paths",
|
|
660
|
+
"**Finding-time label**: real_defect (path converter mangles UNC paths)",
|
|
661
|
+
"**Dispute note**: MSYS/Git-Bash accepts //server/share, so invalid-path mechanism unproven",
|
|
662
|
+
"**Seated modal verdict**: [see table above]",
|
|
663
|
+
"",
|
|
664
|
+
"## Honest Bounds",
|
|
665
|
+
"",
|
|
666
|
+
"This is REAL-CONTEXT seated adjudication through the WIRED seam (increment 1.5):",
|
|
667
|
+
f"- File brain is REAL (STATE.md, tracker.json from disk)",
|
|
668
|
+
f"- Cited code/evidence is REAL (persisted in corpus + context pack)",
|
|
669
|
+
f"- OrchestratorDriver.decide() is REAL (not shim)",
|
|
670
|
+
f"- schema_validated={schema_valid['pct']:.1f}% (production readiness required ~100%)",
|
|
671
|
+
f"- N={num_runs} per model (stability measured)",
|
|
672
|
+
"",
|
|
673
|
+
"**NOT tested in this increment**:",
|
|
674
|
+
"- Long-loop coherence (one wave's full decision sequence)",
|
|
675
|
+
"- Live adjudication inside a real wave (increment 4b)",
|
|
676
|
+
"",
|
|
677
|
+
])
|
|
678
|
+
|
|
679
|
+
with open(output_path, "w", encoding="utf-8") as f:
|
|
680
|
+
f.write("\n".join(lines))
|
|
681
|
+
|
|
682
|
+
print(f"Wrote seated results markdown: {output_path}")
|
|
683
|
+
|
|
684
|
+
|
|
685
|
+
# ============================================================================
|
|
686
|
+
# Main
|
|
687
|
+
# ============================================================================
|
|
688
|
+
|
|
689
|
+
|
|
690
|
+
def main():
|
|
691
|
+
"""Main entry point."""
|
|
692
|
+
parser = argparse.ArgumentParser(
|
|
693
|
+
description="Seated shadow adjudication — increment 4a redo over wired seam"
|
|
694
|
+
)
|
|
695
|
+
parser.add_argument(
|
|
696
|
+
"--corpus",
|
|
697
|
+
required=True,
|
|
698
|
+
help="Path to corpus jsonl file",
|
|
699
|
+
)
|
|
700
|
+
parser.add_argument(
|
|
701
|
+
"--model",
|
|
702
|
+
default=None,
|
|
703
|
+
help=(
|
|
704
|
+
"Challenger model id; OVERRIDES the configured seats.orchestrator "
|
|
705
|
+
"model (default: seat model, else gpt-5.6-sol, frontier-first)"
|
|
706
|
+
),
|
|
707
|
+
)
|
|
708
|
+
parser.add_argument(
|
|
709
|
+
"--config",
|
|
710
|
+
default=None,
|
|
711
|
+
help="Path to aesop.config.json for seats.orchestrator (default: ./aesop.config.json)",
|
|
712
|
+
)
|
|
713
|
+
parser.add_argument(
|
|
714
|
+
"--offline",
|
|
715
|
+
action="store_true",
|
|
716
|
+
help="Run offline with FakeTransport (for testing)",
|
|
717
|
+
)
|
|
718
|
+
parser.add_argument(
|
|
719
|
+
"--live",
|
|
720
|
+
action="store_true",
|
|
721
|
+
help=(
|
|
722
|
+
"Run live against the configured seat's endpoint (API key read "
|
|
723
|
+
"from the seat's api_key_env; is_local loopback seats need no key)"
|
|
724
|
+
),
|
|
725
|
+
)
|
|
726
|
+
parser.add_argument(
|
|
727
|
+
"--repeat",
|
|
728
|
+
type=int,
|
|
729
|
+
default=3,
|
|
730
|
+
help="Number of runs per model (default 3)",
|
|
731
|
+
)
|
|
732
|
+
parser.add_argument(
|
|
733
|
+
"--out-tag",
|
|
734
|
+
default=None,
|
|
735
|
+
help="Output filename tag (default: derived from model and repeat)",
|
|
736
|
+
)
|
|
737
|
+
parser.add_argument(
|
|
738
|
+
"--abort-if-item9-fails",
|
|
739
|
+
action="store_true",
|
|
740
|
+
default=True,
|
|
741
|
+
help="Abort cheaper run if item 9 doesn't flip (frontier-first mode, default on)",
|
|
742
|
+
)
|
|
743
|
+
|
|
744
|
+
args = parser.parse_args()
|
|
745
|
+
|
|
746
|
+
# Validate corpus.
|
|
747
|
+
corpus_path = Path(args.corpus)
|
|
748
|
+
if not corpus_path.exists():
|
|
749
|
+
print(f"Error: corpus file not found: {corpus_path}", file=sys.stderr)
|
|
750
|
+
sys.exit(1)
|
|
751
|
+
|
|
752
|
+
# Load corpus.
|
|
753
|
+
try:
|
|
754
|
+
corpus = load_corpus(str(corpus_path))
|
|
755
|
+
print(f"Loaded {len(corpus)} corpus items")
|
|
756
|
+
except Exception as e:
|
|
757
|
+
print(f"Error loading corpus: {e}", file=sys.stderr)
|
|
758
|
+
sys.exit(1)
|
|
759
|
+
|
|
760
|
+
# Validate corpus size.
|
|
761
|
+
if len(corpus) != 16:
|
|
762
|
+
print(
|
|
763
|
+
f"Error: corpus must have exactly 16 items, got {len(corpus)}",
|
|
764
|
+
file=sys.stderr,
|
|
765
|
+
)
|
|
766
|
+
sys.exit(1)
|
|
767
|
+
|
|
768
|
+
# Setup.
|
|
769
|
+
repo_root = REPO_ROOT
|
|
770
|
+
conductor_root = Path.home() / "conductor3"
|
|
771
|
+
|
|
772
|
+
# Load schema.
|
|
773
|
+
schema_path = DRIVER_DIR / "decisions" / "adjudicate_finding.schema.json"
|
|
774
|
+
schema = None
|
|
775
|
+
if schema_path.exists():
|
|
776
|
+
try:
|
|
777
|
+
with open(schema_path, encoding="utf-8") as f:
|
|
778
|
+
schema = json.load(f)
|
|
779
|
+
except Exception as e:
|
|
780
|
+
print(f"Warning: failed to load schema: {e}", file=sys.stderr)
|
|
781
|
+
|
|
782
|
+
# Setup backend.
|
|
783
|
+
if args.offline:
|
|
784
|
+
print("Running in OFFLINE mode (FakeOrchestratorBackend)")
|
|
785
|
+
if args.model is None:
|
|
786
|
+
args.model = DEFAULT_CHALLENGER_MODEL
|
|
787
|
+
# Fake backend with canned responses (verdict/evidence as strings for validation).
|
|
788
|
+
canned = []
|
|
789
|
+
for i in range(len(corpus) * args.repeat):
|
|
790
|
+
# Alternate between different classifications for variety
|
|
791
|
+
classifications = ["false_positive", "real_defect", "undetermined", "enhancement_opportunity"]
|
|
792
|
+
classification = classifications[i % len(classifications)]
|
|
793
|
+
canned.append({
|
|
794
|
+
"verdict": classification,
|
|
795
|
+
"evidence": f"Reasoning for verdict {i+1}",
|
|
796
|
+
"confidence": 0.9 - (i * 0.01),
|
|
797
|
+
})
|
|
798
|
+
backend = FakeOrchestratorBackend(canned_responses=canned)
|
|
799
|
+
elif args.live:
|
|
800
|
+
print("Running in LIVE mode (OpenAI API)")
|
|
801
|
+
# HS-1: orchestrator seat from config; CLI --model overrides.
|
|
802
|
+
try:
|
|
803
|
+
backend = build_live_backend(
|
|
804
|
+
cli_model=args.model, config_path=args.config
|
|
805
|
+
)
|
|
806
|
+
except (TypeError, ValueError) as e:
|
|
807
|
+
print(f"Error: invalid aesop.config.json: {e}", file=sys.stderr)
|
|
808
|
+
sys.exit(1)
|
|
809
|
+
args.model = backend.model
|
|
810
|
+
# Check for the seat's API key at runtime (local seats need none).
|
|
811
|
+
key_env = getattr(backend, "api_key_env", "OPENAI_API_KEY")
|
|
812
|
+
if not getattr(backend, "is_local", False) and not os.environ.get(key_env):
|
|
813
|
+
print(
|
|
814
|
+
f"Error: {key_env} environment variable not set",
|
|
815
|
+
file=sys.stderr,
|
|
816
|
+
)
|
|
817
|
+
sys.exit(1)
|
|
818
|
+
else:
|
|
819
|
+
print("Error: specify --offline or --live", file=sys.stderr)
|
|
820
|
+
sys.exit(1)
|
|
821
|
+
|
|
822
|
+
# Create driver.
|
|
823
|
+
driver = OrchestratorDriver(backend, schema_dir=str(DRIVER_DIR), max_retries=2)
|
|
824
|
+
|
|
825
|
+
# Run adjudications.
|
|
826
|
+
all_verdicts = []
|
|
827
|
+
api_call_count = 0
|
|
828
|
+
max_calls_total = args.repeat * len(corpus)
|
|
829
|
+
|
|
830
|
+
print(
|
|
831
|
+
f"Running {args.repeat} iteration(s) of the corpus (max {max_calls_total} API calls)"
|
|
832
|
+
)
|
|
833
|
+
|
|
834
|
+
for run_num in range(args.repeat):
|
|
835
|
+
print(f"\n--- Run {run_num + 1}/{args.repeat} ---")
|
|
836
|
+
|
|
837
|
+
for item in corpus:
|
|
838
|
+
if api_call_count >= max_calls_total:
|
|
839
|
+
print(
|
|
840
|
+
f"Reached max API calls ({max_calls_total}). Stopping.",
|
|
841
|
+
file=sys.stderr,
|
|
842
|
+
)
|
|
843
|
+
break
|
|
844
|
+
|
|
845
|
+
result = adjudicate_one_finding(
|
|
846
|
+
driver, item, str(repo_root), str(conductor_root), schema, run_num + 1
|
|
847
|
+
)
|
|
848
|
+
all_verdicts.append(result)
|
|
849
|
+
api_call_count += 1
|
|
850
|
+
|
|
851
|
+
print(
|
|
852
|
+
f" [{api_call_count:2d}] {item.id}: {result.challenger_classification} "
|
|
853
|
+
f"(schema_valid={result.schema_valid})"
|
|
854
|
+
)
|
|
855
|
+
|
|
856
|
+
# Aggregate results.
|
|
857
|
+
aggregated = aggregate_seated_results(all_verdicts, corpus, args.repeat)
|
|
858
|
+
|
|
859
|
+
# Item 9 check (frontier-first abort gate).
|
|
860
|
+
item_9 = aggregated["item_9_analysis"]
|
|
861
|
+
print(f"\n--- Item 9 Flip Verdict ---")
|
|
862
|
+
print(
|
|
863
|
+
f"Item 9 (whitelist-gate-weakening) modal verdict: {item_9['modal_verdict']}"
|
|
864
|
+
)
|
|
865
|
+
print(f"Stability: {item_9['stability']:.1%} ({item_9['modal_count']}/{args.repeat})")
|
|
866
|
+
print(f"Flips to false_positive: {item_9['flips_to_false_positive']}")
|
|
867
|
+
|
|
868
|
+
if (
|
|
869
|
+
not item_9["flips_to_false_positive"]
|
|
870
|
+
and args.model == "gpt-5.6-sol"
|
|
871
|
+
and args.abort_if_item9_fails
|
|
872
|
+
):
|
|
873
|
+
print(
|
|
874
|
+
"\nABORTING: Item 9 did not flip to false_positive modally. "
|
|
875
|
+
"Cheaper model run (gpt-5.5) skipped (frontier-first fail)."
|
|
876
|
+
)
|
|
877
|
+
print(
|
|
878
|
+
"Conclusion: frontier failing => cheaper seam will not pass. "
|
|
879
|
+
"Real context does not rescue narrative refutation."
|
|
880
|
+
)
|
|
881
|
+
|
|
882
|
+
# Write outputs.
|
|
883
|
+
results_dir = REPO_ROOT / "bench" / "results"
|
|
884
|
+
results_dir.mkdir(parents=True, exist_ok=True)
|
|
885
|
+
|
|
886
|
+
if args.out_tag:
|
|
887
|
+
model_tag = args.out_tag
|
|
888
|
+
else:
|
|
889
|
+
model_clean = args.model.replace("/", "_").replace(".", "_").lower()
|
|
890
|
+
model_tag = model_clean
|
|
891
|
+
|
|
892
|
+
repeat_suffix = f"_repeat{args.repeat}" if args.repeat > 1 else ""
|
|
893
|
+
full_tag = model_tag + repeat_suffix
|
|
894
|
+
|
|
895
|
+
json_path = results_dir / f"seated-redo-2026-07-24-{full_tag}.json"
|
|
896
|
+
md_path = results_dir / f"seated-redo-2026-07-24-{full_tag}.md"
|
|
897
|
+
|
|
898
|
+
write_seated_json(all_verdicts, corpus, aggregated, args.model, str(json_path))
|
|
899
|
+
write_seated_md(aggregated, corpus, args.model, str(md_path))
|
|
900
|
+
|
|
901
|
+
# Summary.
|
|
902
|
+
print("\n" + "=" * 60)
|
|
903
|
+
print("SEATED SHADOW ADJUDICATION — SUMMARY")
|
|
904
|
+
print("=" * 60)
|
|
905
|
+
print(f"Model: {args.model}")
|
|
906
|
+
print(f"Runs: {args.repeat}")
|
|
907
|
+
print(f"Item 9 flips to false_positive: {item_9['flips_to_false_positive']}")
|
|
908
|
+
print(
|
|
909
|
+
f"Real defects held as real_defect: {aggregated['held_real_defects']}/{aggregated['total_real_defects']}"
|
|
910
|
+
)
|
|
911
|
+
print(f"Schema validity: {aggregated['schema_validity']['pct']:.1f}%")
|
|
912
|
+
print("=" * 60)
|
|
913
|
+
|
|
914
|
+
print(f"\nResults written to:")
|
|
915
|
+
print(f" {json_path}")
|
|
916
|
+
print(f" {md_path}")
|
|
917
|
+
|
|
918
|
+
|
|
919
|
+
if __name__ == "__main__":
|
|
920
|
+
main()
|