126 lines
5.0 KiB
Python
126 lines
5.0 KiB
Python
"""vote.py — cross-vendor 적대 검증 표 생성기 (wiki_quorum.py 워커).
|
|
|
|
N=3 quorum 의 표 2/3 을 외부 구독 CLI(codex/agy)로 — Claude 토큰 절감이면서
|
|
독립 실패 모드 덕에 falsification 은 강화된다 (rules/extraction-tiering.md).
|
|
출력은 ```wiki-verdict``` 블록 파일 — `wiki_quorum.py` 가 엔진 불가지로 그대로 집계.
|
|
|
|
CLI:
|
|
python3 -m deep_research.vote --backend codex|antigravity \
|
|
--findings findings.md [--context-files f1 f2 ...] --out /tmp/vote-codex.md
|
|
exit: 0 (표 파일 생성) / 1 (엔진 실패).
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import asyncio
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import Literal
|
|
|
|
from pydantic import BaseModel, Field
|
|
|
|
from deep_research.extract import make_backend, numbered_content
|
|
|
|
MAX_CONTEXT_CHARS = 40_000
|
|
|
|
|
|
class FindingVote(BaseModel):
|
|
id: str = Field(description="the finding's ID exactly as given (no whitespace)")
|
|
action: Literal["KEEP", "DOWNGRADE", "REJECT"]
|
|
reason: str = Field(description="one-sentence justification")
|
|
|
|
|
|
class VoteResult(BaseModel):
|
|
votes: list[FindingVote]
|
|
|
|
|
|
PROMPT = """You are an independent adversarial reviewer. Your KPI is finding weaknesses, \
|
|
not approving. For EACH finding below, try to FALSIFY it via three checks: \
|
|
(1) PRACTICALITY — is it actionable/real in this context? \
|
|
(2) OVERCLAIM — does the evidence actually support the stated severity? \
|
|
(3) ASSUMPTION — does it rest on an unstated assumption that may not hold?
|
|
|
|
Default-refute: if any check leaves you uncertain, do NOT answer KEEP — answer at least \
|
|
DOWNGRADE. KEEP only when all three checks actively pass. REJECT when the finding is wrong \
|
|
or unsupported. Vote on EVERY finding (same IDs, no whitespace in IDs). \
|
|
Respond ONLY with the JSON object matching the schema.
|
|
|
|
FINDINGS:
|
|
----------------------------------------
|
|
{findings}
|
|
----------------------------------------
|
|
{context}"""
|
|
|
|
|
|
def build_context(files: list[str]) -> str:
|
|
if not files:
|
|
return ""
|
|
parts = ["REFERENCE FILES (read-only evidence):"]
|
|
budget = MAX_CONTEXT_CHARS
|
|
for f in files:
|
|
p = Path(f)
|
|
try:
|
|
text = p.read_text(encoding="utf-8", errors="replace")
|
|
except Exception:
|
|
continue
|
|
numbered, _ = numbered_content(text, limit=min(budget, 20_000))
|
|
parts.append(f"--- {f} ---\n{numbered}")
|
|
budget -= len(numbered)
|
|
if budget <= 0:
|
|
parts.append("--- (context budget exhausted — remaining files omitted) ---")
|
|
break
|
|
return "\n".join(parts)
|
|
|
|
|
|
def render_vote_block(backend_name: str, result: VoteResult, truncated_chars: int = 0) -> str:
|
|
lines = [f"# Adversarial vote ({backend_name})", ""]
|
|
if truncated_chars:
|
|
# no-silent-caps (rules/extraction-tiering.md 계명 3): 절단 사실을 표 자체에 기록.
|
|
lines += [f"> ⚠ findings input {truncated_chars} chars 중 {MAX_CONTEXT_CHARS} 까지만 표결 — "
|
|
f"잘린 finding 은 이 표에서 누락 (Claude 표로 보완 필요)", ""]
|
|
lines += ["```wiki-verdict", f"agent: {backend_name}-adversarial-vote"]
|
|
for v in result.votes:
|
|
fid = re.sub(r"\s+", "-", v.id.strip())
|
|
lines.append(f"finding: {fid} action: {v.action}")
|
|
lines.append("```")
|
|
lines.append("")
|
|
for v in result.votes:
|
|
lines.append(f"- {v.id} → {v.action}: {v.reason}")
|
|
return "\n".join(lines)
|
|
|
|
|
|
async def run(args) -> int:
|
|
findings = Path(args.findings).read_text(encoding="utf-8", errors="replace")
|
|
truncated_chars = len(findings) if len(findings) > MAX_CONTEXT_CHARS else 0
|
|
if truncated_chars:
|
|
print(f"warning: findings {truncated_chars} chars > {MAX_CONTEXT_CHARS} — 절단됨. "
|
|
f"잘린 finding 은 이 표에서 누락되므로 분할 표결 또는 Claude 표 보완 필요 (no-silent-caps)",
|
|
file=sys.stderr)
|
|
prompt = PROMPT.format(findings=findings[:MAX_CONTEXT_CHARS],
|
|
context=build_context(args.context_files or []))
|
|
backend = make_backend(args.backend)
|
|
result = await backend.run_agent(prompt, VoteResult, label=f"vote:{args.backend}")
|
|
if result is None or not result.votes:
|
|
print(f"error: {args.backend} 표 생성 실패 — Claude 표로 대체하세요 (fallback 사다리)",
|
|
file=sys.stderr)
|
|
return 1
|
|
block = render_vote_block(args.backend, result, truncated_chars)
|
|
Path(args.out).write_text(block, encoding="utf-8")
|
|
print(f"vote → {args.out} ({len(result.votes)} findings)")
|
|
return 0
|
|
|
|
|
|
def main() -> None:
|
|
ap = argparse.ArgumentParser(description="cross-vendor 적대 검증 표 생성")
|
|
ap.add_argument("--backend", required=True, choices=["codex", "antigravity"])
|
|
ap.add_argument("--findings", required=True, help="findings 목록 파일 (ID 포함)")
|
|
ap.add_argument("--context-files", nargs="*", default=[])
|
|
ap.add_argument("--out", required=True)
|
|
args = ap.parse_args()
|
|
sys.exit(asyncio.run(run(args)))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|