refactor: 문서 개선 중
This commit is contained in:
@@ -26,6 +26,7 @@ from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import datetime as dt
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
@@ -34,6 +35,11 @@ import sys
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from techlog import Report # noqa: E402
|
||||
from command_pedagogy import ( # noqa: E402
|
||||
analyze_commands,
|
||||
validate_command_patch_set,
|
||||
validate_command_plan,
|
||||
)
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
SKILLS = os.path.join(ROOT, ".agents", "skills")
|
||||
@@ -87,11 +93,27 @@ LEGACY_RUNBY = "subagent"
|
||||
# 쓰였나**를 읽으면 커밋 전후로 판정이 흔들리지 않는다.
|
||||
AGENT_RUNBY_SCHEMA = 2
|
||||
|
||||
# v3부터 S3 안의 command-pedagogy 보조 흐름과 S6 뒤 독립 검토를 원장에 남긴다.
|
||||
# 과거 v1/v2 원장은 그때 없던 영수증을 소급해 요구하지 않는다.
|
||||
QUALITY_REVIEW_SCHEMA = 3
|
||||
COMMAND_ARTIFACT_SCHEMA = 4
|
||||
EVIDENCE_RECONCILIATION_SCHEMA = 5
|
||||
CURRENT_RUN_SCHEMA = EVIDENCE_RECONCILIATION_SCHEMA
|
||||
COMMAND_REVIEW_SKILL = "writing-practitioner-guides"
|
||||
COMMAND_REVIEW_AGENTS = {
|
||||
"planner": "command-pedagogy-planner",
|
||||
"editor": "command-pedagogy-editor",
|
||||
"reviewer": "command-pedagogy-reviewer",
|
||||
}
|
||||
FACT_REVIEW_AGENT = "fact-reviewer"
|
||||
|
||||
# 측정 관문 — 돌았다는 것은 요구하지만 종료 코드 0 은 요구하지 않는다.
|
||||
# 문서 계약이 「error 0」을 붙인 것은 check_prose 뿐이고 style_profile 은 문체 수치를 보여 주는
|
||||
# 측정이다 (stage-contracts.md:178·:252). 여기에 0 을 요구하면 정직하게 적은 원장이 실패하고,
|
||||
# 0 으로 고쳐 적으면 그건 지어낸 것이 된다.
|
||||
MEASUREMENT_GATES = ("style_profile.mjs",)
|
||||
EVIDENCE_GATE_STAGES = {"S3", "S5", "S6"}
|
||||
EVIDENCE_GATE_ID = "evidence-repo"
|
||||
|
||||
ORDER = ["S1", "S2", "S3", "S4", "S5", "S6", "S7"]
|
||||
|
||||
@@ -422,6 +444,442 @@ def _side_proof(rep: Report, st: dict, sid: str, spec: dict, where: str) -> int:
|
||||
return unverifiable
|
||||
|
||||
|
||||
def _evidence_gate_v5(rep: Report, run: dict, sid: str, gates: list[dict], where: str) -> dict | None:
|
||||
"""v5부터 live source reconciliation을 이름 있는 관문으로 검증한다.
|
||||
|
||||
예전 원장은 command 문자열에 `check_evidence.mjs`만 있으면 통과했다. 그러면 `--repo`를
|
||||
빼서 live source 대조를 하지 않은 명령도 exit 0만 적으면 같은 초록색이 된다. v5에서는
|
||||
S3/S5/S6마다 semanticId가 `evidence-repo`인 관문을 하나 요구하고 실제 `--repo` 호출인지
|
||||
확인한다.
|
||||
|
||||
source checkout이 현재 기계에 없을 수 있다. 그 경우 실패를 0으로 바꾸지 않는다. 실제
|
||||
exit 3을 `UNVERIFIABLE`로 적고 이유와 프로젝트 리뷰 수용 여부를 남긴 경우에만 절차를
|
||||
정직하게 수행한 것으로 인정한다.
|
||||
"""
|
||||
schema = run.get("schemaVersion")
|
||||
if not isinstance(schema, int) or schema < EVIDENCE_RECONCILIATION_SCHEMA:
|
||||
return None
|
||||
if sid not in EVIDENCE_GATE_STAGES:
|
||||
return None
|
||||
|
||||
matches = [g for g in gates if g.get("semanticId") == EVIDENCE_GATE_ID]
|
||||
if len(matches) != 1:
|
||||
rep.error("필수 evidence semantic gate가 정확히 하나가 아니다",
|
||||
f"{where} — semanticId={EVIDENCE_GATE_ID!r} · count={len(matches)}")
|
||||
return None
|
||||
gate = matches[0]
|
||||
cmd = str(gate.get("cmd") or "")
|
||||
if "check_evidence.mjs" not in cmd:
|
||||
rep.error("evidence semantic gate가 check_evidence를 실행하지 않았다",
|
||||
f"{where} — {cmd[:90]}")
|
||||
if not re.search(r"(?:^|\s)--repo(?:\s|$)", cmd):
|
||||
rep.error("live source evidence gate에서 --repo가 빠졌다",
|
||||
f"{where} — {cmd[:90]}")
|
||||
project = str(run.get("project") or "")
|
||||
if project and not re.search(rf"(?<![\w/.-]){re.escape(project)}(?![\w/.-])", cmd):
|
||||
rep.error("evidence semantic gate가 현재 프로젝트를 가리키지 않는다",
|
||||
f"{where} — project={project} · {cmd[:90]}")
|
||||
|
||||
status = gate.get("status")
|
||||
exit_code = gate.get("exit")
|
||||
if status == "PASS":
|
||||
if exit_code not in (0, "0"):
|
||||
rep.error("PASS evidence gate의 종료 코드가 0이 아니다",
|
||||
f"{where} — exit={exit_code}")
|
||||
elif status == "UNVERIFIABLE":
|
||||
if exit_code not in (3, "3"):
|
||||
rep.error("UNVERIFIABLE evidence gate는 실제 대조 불가 exit 3이어야 한다",
|
||||
f"{where} — exit={exit_code}")
|
||||
if not str(gate.get("reason") or "").strip():
|
||||
rep.error("UNVERIFIABLE evidence gate에 이유가 없다", where)
|
||||
if gate.get("acceptedByProjectReview") is not True:
|
||||
rep.error("UNVERIFIABLE evidence gate가 프로젝트 리뷰에서 수용되지 않았다", where)
|
||||
rep.facts.setdefault("live source UNVERIFIABLE", []).append(sid)
|
||||
else:
|
||||
rep.error("evidence semantic gate status가 계약 밖이다",
|
||||
f"{where} — status={status!r} · PASS|UNVERIFIABLE만 허용")
|
||||
return gate
|
||||
|
||||
|
||||
def _sha256_file(path: str) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with open(path, "rb") as fh:
|
||||
for chunk in iter(lambda: fh.read(1024 * 1024), b""):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def _publication_sha256(run: dict) -> str | None:
|
||||
rel = str(run.get("record") or "")
|
||||
if not rel or os.path.isabs(rel):
|
||||
return None
|
||||
full = os.path.realpath(os.path.join(ROOT, rel))
|
||||
try:
|
||||
if os.path.commonpath([ROOT, full]) != os.path.realpath(ROOT):
|
||||
return None
|
||||
except ValueError:
|
||||
return None
|
||||
if not os.path.isfile(full):
|
||||
return None
|
||||
return _sha256_file(full)
|
||||
|
||||
|
||||
def _command_artifact(
|
||||
rep: Report,
|
||||
receipt,
|
||||
where: str,
|
||||
*,
|
||||
missing_error: str = "command artifact 영수증이 없다",
|
||||
) -> dict | None:
|
||||
"""repo-relative path + sha256 영수증을 실제 JSON artifact와 대조한다."""
|
||||
if not isinstance(receipt, dict):
|
||||
rep.error(missing_error, where)
|
||||
return None
|
||||
rel = receipt.get("path")
|
||||
expected = receipt.get("sha256")
|
||||
if not isinstance(rel, str) or not rel.strip() or os.path.isabs(rel):
|
||||
rep.error("command artifact path가 계약 밖이다", f"{where} — {rel!r}")
|
||||
return None
|
||||
if not isinstance(expected, str) or not re.fullmatch(r"[0-9a-f]{64}", expected):
|
||||
rep.error("command artifact sha256 형식이 잘못됐다", f"{where} — {expected!r}")
|
||||
return None
|
||||
full = os.path.realpath(os.path.join(ROOT, rel))
|
||||
try:
|
||||
inside = os.path.commonpath([os.path.realpath(ROOT), full]) == os.path.realpath(ROOT)
|
||||
except ValueError:
|
||||
inside = False
|
||||
if not inside:
|
||||
rep.error("command artifact가 repository 밖을 가리킨다", f"{where} — {rel}")
|
||||
return None
|
||||
if not os.path.isfile(full):
|
||||
rep.error("command artifact 파일이 없다", f"{where} — {rel}")
|
||||
return None
|
||||
actual = _sha256_file(full)
|
||||
if actual != expected:
|
||||
rep.error(
|
||||
"command artifact sha256이 실제 파일과 다르다",
|
||||
f"{where} — expected={expected[:12]} actual={actual[:12]} · {rel}",
|
||||
)
|
||||
return None
|
||||
try:
|
||||
with open(full, encoding="utf-8") as fh:
|
||||
value = json.load(fh)
|
||||
except (OSError, json.JSONDecodeError) as exc:
|
||||
rep.error("command artifact JSON을 읽지 못했다", f"{where} — {rel} — {exc}")
|
||||
return None
|
||||
if not isinstance(value, dict):
|
||||
rep.error("command artifact가 JSON object가 아니다", f"{where} — {rel}")
|
||||
return None
|
||||
return value
|
||||
|
||||
|
||||
def _review_agent(rep: Report, role: dict, expected: str, where: str) -> None:
|
||||
"""품질 검토 역할이 지정된 독립 에이전트를 실제로 가리키는지 본다."""
|
||||
actual = role.get("runBy")
|
||||
if actual != expected:
|
||||
rep.error("품질 검토 agent가 계약과 다르다",
|
||||
f"{where} — runBy={actual!r} · 계약은 {expected!r}")
|
||||
if not os.path.exists(os.path.join(AGENTS_DIR, f"{expected}.md")):
|
||||
rep.error("품질 검토 agent 정의가 없다",
|
||||
f"{where} — .claude/agents/{expected}.md 가 없다")
|
||||
|
||||
|
||||
def _analysis_counts(
|
||||
rep: Report, value, where: str, *, require_artifact: bool
|
||||
) -> dict[str, object] | None:
|
||||
"""결정론적 command 분석 영수증과 frozen JSON artifact를 함께 검증한다."""
|
||||
if not isinstance(value, dict):
|
||||
rep.error("command analysis 영수증이 없다", where)
|
||||
return None
|
||||
cmd = str(value.get("cmd") or "")
|
||||
if "check-command-pedagogy.py" not in cmd:
|
||||
rep.error("command analysis가 결정론적 검사기를 쓰지 않았다",
|
||||
f"{where} — {cmd or 'cmd 없음'}")
|
||||
if value.get("exit") not in (0, "0"):
|
||||
rep.error("command analysis를 끝내지 못했다",
|
||||
f"{where} — exit {value.get('exit')}")
|
||||
|
||||
out: dict[str, object] = {}
|
||||
for key in ("shellBlocks", "findings", "majorFindings"):
|
||||
raw = value.get(key)
|
||||
if isinstance(raw, bool) or not isinstance(raw, int) or raw < 0:
|
||||
rep.error("command analysis 수치가 계약 밖이다",
|
||||
f"{where} — {key}={raw!r}")
|
||||
return None
|
||||
out[key] = raw
|
||||
if int(out["majorFindings"]) > int(out["findings"]):
|
||||
rep.error("major command finding 수가 전체 finding보다 크다", where)
|
||||
|
||||
receipt = value.get("artifact")
|
||||
artifact = (
|
||||
_command_artifact(rep, receipt, where)
|
||||
if require_artifact or isinstance(receipt, dict)
|
||||
else None
|
||||
)
|
||||
out["artifact"] = artifact
|
||||
if artifact is not None:
|
||||
blocks = artifact.get("blocks")
|
||||
findings = artifact.get("findings")
|
||||
if not isinstance(blocks, list) or not isinstance(findings, list):
|
||||
rep.error("command analysis artifact 구조가 잘못됐다", where)
|
||||
else:
|
||||
actual_major = sum(
|
||||
1 for finding in findings
|
||||
if isinstance(finding, dict) and finding.get("severity") == "major"
|
||||
)
|
||||
expected_counts = (len(blocks), len(findings), actual_major)
|
||||
receipt_counts = (
|
||||
int(out["shellBlocks"]), int(out["findings"]), int(out["majorFindings"])
|
||||
)
|
||||
if expected_counts != receipt_counts:
|
||||
rep.error(
|
||||
"command analysis 영수증과 artifact 수치가 다르다",
|
||||
f"{where} — receipt={receipt_counts} artifact={expected_counts}",
|
||||
)
|
||||
if artifact.get("authority") != "deterministic":
|
||||
rep.error("command analysis artifact authority가 deterministic이 아니다", where)
|
||||
return out
|
||||
|
||||
|
||||
def _command_role(
|
||||
rep: Report,
|
||||
role,
|
||||
*,
|
||||
name: str,
|
||||
required: bool,
|
||||
required_error: str,
|
||||
verdict: bool = False,
|
||||
artifact_kind: str | None = None,
|
||||
analysis: dict | None = None,
|
||||
publication_sha256: str | None = None,
|
||||
require_artifact: bool = True,
|
||||
) -> int:
|
||||
"""planner/editor/reviewer receipt와 first-class artifact를 검사한다."""
|
||||
where = f"qualityReviews.commandPedagogy.{name}"
|
||||
if not isinstance(role, dict):
|
||||
rep.error(required_error if required else "command role 영수증이 없다", where)
|
||||
return 0
|
||||
expected = COMMAND_REVIEW_AGENTS[name]
|
||||
_review_agent(rep, role, expected, where)
|
||||
status = role.get("status")
|
||||
if required and status != "DONE":
|
||||
rep.error(required_error, f"{where} — status={status!r}")
|
||||
elif not required and status not in ("DONE", "SKIPPED"):
|
||||
rep.error("선택적 command role의 상태가 끝나지 않았다",
|
||||
f"{where} — status={status!r}")
|
||||
if status == "SKIPPED":
|
||||
if not str(role.get("skipReason") or "").strip():
|
||||
rep.error("command role을 건너뛴 사유가 없다", where)
|
||||
return 0
|
||||
if status != "DONE":
|
||||
return 0
|
||||
|
||||
if role.get("skill") != COMMAND_REVIEW_SKILL:
|
||||
rep.error("command role이 다른 스킬을 썼다",
|
||||
f"{where} — {role.get('skill')!r} · 계약은 {COMMAND_REVIEW_SKILL!r}")
|
||||
echo = _norm(role.get("skillEcho") or "")
|
||||
unverifiable = 0
|
||||
if not echo:
|
||||
rep.error("command role에 스킬 영수증이 없다", where)
|
||||
elif _skill_text(COMMAND_REVIEW_SKILL) is None:
|
||||
rep.error("command role의 스킬 폴더가 없다", COMMAND_REVIEW_SKILL)
|
||||
else:
|
||||
unverifiable += _judge_echo(
|
||||
rep,
|
||||
COMMAND_REVIEW_SKILL,
|
||||
echo,
|
||||
role.get(REVISION_FIELD),
|
||||
where,
|
||||
"품질 검토의 ",
|
||||
)
|
||||
if verdict and role.get("verdict") != "PASS":
|
||||
rep.error("command-pedagogy review가 통과하지 못했다",
|
||||
f"{where} — verdict={role.get('verdict')!r}")
|
||||
|
||||
artifact_receipt = role.get("artifact")
|
||||
artifact = (
|
||||
_command_artifact(
|
||||
rep,
|
||||
artifact_receipt,
|
||||
where,
|
||||
missing_error="command role artifact 영수증이 없다",
|
||||
)
|
||||
if require_artifact or isinstance(artifact_receipt, dict)
|
||||
else None
|
||||
)
|
||||
if artifact is not None and artifact_kind == "plan":
|
||||
try:
|
||||
validate_command_plan(artifact, analysis=analysis)
|
||||
except ValueError as exc:
|
||||
rep.error("CommandPlan artifact가 계약과 다르다", f"{where} — {exc}")
|
||||
elif artifact is not None and artifact_kind == "patch":
|
||||
try:
|
||||
validate_command_patch_set(artifact, analysis=analysis)
|
||||
except ValueError as exc:
|
||||
rep.error("CommandPatchSet artifact가 계약과 다르다", f"{where} — {exc}")
|
||||
elif artifact is not None and artifact_kind == "review":
|
||||
if artifact.get("reviewer") != expected or artifact.get("verdict") != role.get("verdict"):
|
||||
rep.error("command review artifact와 reviewer 영수증이 다르다", where)
|
||||
if publication_sha256 and artifact.get("source_sha256") != publication_sha256:
|
||||
rep.error("command review artifact가 최종 publication hash와 다르다", where)
|
||||
|
||||
if require_artifact and artifact_kind == "review" and publication_sha256:
|
||||
if role.get("sourceSha256") != publication_sha256:
|
||||
rep.error("command review가 최종 publication hash와 다르다", where)
|
||||
return unverifiable
|
||||
|
||||
|
||||
def _verify_quality_reviews(
|
||||
rep: Report,
|
||||
run: dict,
|
||||
*,
|
||||
check_current_publication: bool = True,
|
||||
) -> int:
|
||||
"""v3의 command-pedagogy + 최종 technical-evidence review 계약을 검사한다.
|
||||
|
||||
historical/superseded run도 당시 artifact 자체의 영수증과 stage 계약은 계속 검증한다.
|
||||
다만 같은 Record에 더 최신 run이 있으면 그 옛 run을 *현재* publication hash와 다시
|
||||
맞추지는 않는다. 현재 publication 대조는 authoritative latest run 하나가 맡는다.
|
||||
"""
|
||||
schema = run.get("schemaVersion")
|
||||
if not isinstance(schema, int) or schema < QUALITY_REVIEW_SCHEMA:
|
||||
return 0
|
||||
|
||||
reviews = run.get("qualityReviews")
|
||||
if not isinstance(reviews, dict):
|
||||
rep.error("품질 검토 원장이 없다", "schemaVersion 3부터 qualityReviews가 필요하다")
|
||||
return 0
|
||||
command = reviews.get("commandPedagogy")
|
||||
if not isinstance(command, dict):
|
||||
rep.error("command-pedagogy 원장이 없다", "qualityReviews.commandPedagogy")
|
||||
command = {}
|
||||
|
||||
publication_sha = _publication_sha256(run)
|
||||
artifact_required = schema >= COMMAND_ARTIFACT_SCHEMA
|
||||
initial = _analysis_counts(
|
||||
rep, command.get("initialAnalysis"), "command initial analysis",
|
||||
require_artifact=artifact_required,
|
||||
)
|
||||
final = _analysis_counts(
|
||||
rep, command.get("finalAnalysis"), "command final analysis",
|
||||
require_artifact=artifact_required,
|
||||
)
|
||||
unverifiable = 0
|
||||
initial_findings = int(initial["findings"]) if initial else 0
|
||||
final_blocks = int(final["shellBlocks"]) if final else 0
|
||||
initial_artifact = initial.get("artifact") if initial else None
|
||||
final_artifact = final.get("artifact") if final else None
|
||||
|
||||
if initial and final:
|
||||
if int(initial["shellBlocks"]) > 0 and int(final["shellBlocks"]) == 0:
|
||||
rep.error("command repair가 모든 shell block을 없앴다",
|
||||
f"initial={initial['shellBlocks']} · final=0")
|
||||
if int(final["majorFindings"]) > 0:
|
||||
rep.error("major command finding이 남았다",
|
||||
f"final major findings={final['majorFindings']}")
|
||||
|
||||
if artifact_required and publication_sha and isinstance(final_artifact, dict):
|
||||
if final_artifact.get("source_sha256") != publication_sha:
|
||||
rep.error("final command analysis가 최종 publication hash와 다르다", "command final analysis")
|
||||
record = str(run.get("record") or "")
|
||||
try:
|
||||
with open(os.path.join(ROOT, record), encoding="utf-8") as fh:
|
||||
publication_text = fh.read()
|
||||
mode = str(final_artifact.get("mode") or "operator")
|
||||
rerun = analyze_commands(
|
||||
str(final_artifact.get("section_id") or record), publication_text, mode=mode
|
||||
)
|
||||
if (
|
||||
rerun.get("source_sha256") != final_artifact.get("source_sha256")
|
||||
or len(rerun.get("blocks", [])) != len(final_artifact.get("blocks", []))
|
||||
or len(rerun.get("findings", [])) != len(final_artifact.get("findings", []))
|
||||
):
|
||||
rep.error("final command analysis artifact를 현재 publication에서 재현할 수 없다", record)
|
||||
except (OSError, ValueError) as exc:
|
||||
rep.error("final command analysis를 재검증하지 못했다", str(exc))
|
||||
|
||||
needs_edit = initial_findings > 0
|
||||
unverifiable += _command_role(
|
||||
rep,
|
||||
command.get("planner"),
|
||||
name="planner",
|
||||
required=needs_edit,
|
||||
required_error="명령 finding이 있는데 planner가 끝나지 않았다",
|
||||
artifact_kind="plan" if needs_edit else None,
|
||||
analysis=initial_artifact if isinstance(initial_artifact, dict) else None,
|
||||
require_artifact=artifact_required,
|
||||
)
|
||||
unverifiable += _command_role(
|
||||
rep,
|
||||
command.get("editor"),
|
||||
name="editor",
|
||||
required=needs_edit,
|
||||
required_error="명령 finding이 있는데 editor가 끝나지 않았다",
|
||||
artifact_kind="patch" if needs_edit else None,
|
||||
analysis=initial_artifact if isinstance(initial_artifact, dict) else None,
|
||||
require_artifact=artifact_required,
|
||||
)
|
||||
unverifiable += _command_role(
|
||||
rep,
|
||||
command.get("reviewer"),
|
||||
name="reviewer",
|
||||
required=final_blocks > 0,
|
||||
required_error="shell/CLI가 있는데 command reviewer가 끝나지 않았다",
|
||||
verdict=True,
|
||||
artifact_kind="review" if final_blocks > 0 else None,
|
||||
publication_sha256=publication_sha,
|
||||
require_artifact=artifact_required,
|
||||
)
|
||||
|
||||
fact = reviews.get("technicalEvidence")
|
||||
where = "qualityReviews.technicalEvidence"
|
||||
if not isinstance(fact, dict):
|
||||
rep.error("technical-evidence review 영수증이 없다", where)
|
||||
else:
|
||||
_review_agent(rep, fact, FACT_REVIEW_AGENT, where)
|
||||
if fact.get("status") != "DONE":
|
||||
rep.error("technical-evidence review가 끝나지 않았다",
|
||||
f"{where} — status={fact.get('status')!r}")
|
||||
if fact.get("verdict") != "PASS":
|
||||
rep.error("technical-evidence review가 통과하지 못했다",
|
||||
f"{where} — verdict={fact.get('verdict')!r}")
|
||||
if artifact_required and publication_sha and fact.get("sourceSha256") != publication_sha:
|
||||
rep.error("technical-evidence review가 최종 publication hash와 다르다", where)
|
||||
schema = run.get("schemaVersion")
|
||||
if isinstance(schema, int) and schema >= EVIDENCE_RECONCILIATION_SCHEMA:
|
||||
evidence_gates = []
|
||||
for st in run.get("stages") or []:
|
||||
if st.get("id") not in EVIDENCE_GATE_STAGES:
|
||||
continue
|
||||
evidence_gates.extend(
|
||||
g for g in (st.get("gates") or [])
|
||||
if g.get("semanticId") == EVIDENCE_GATE_ID
|
||||
)
|
||||
states = {g.get("status") for g in evidence_gates}
|
||||
expected = "UNVERIFIABLE" if "UNVERIFIABLE" in states else "VERIFIED"
|
||||
if fact.get("liveSourceReconciliation") != expected:
|
||||
rep.error("technical-evidence review의 live source 상태가 stage evidence와 다르다",
|
||||
f"{where} — expected={expected} · got={fact.get('liveSourceReconciliation')!r}")
|
||||
if expected == "UNVERIFIABLE":
|
||||
if not str(fact.get("liveSourceReason") or "").strip():
|
||||
rep.error("technical-evidence review에 live source 대조 불가 이유가 없다", where)
|
||||
if fact.get("acceptedByProjectReview") is not True:
|
||||
rep.error("technical-evidence review의 UNVERIFIABLE이 프로젝트 리뷰에서 수용되지 않았다",
|
||||
where)
|
||||
|
||||
identities = []
|
||||
for name in ("planner", "editor", "reviewer"):
|
||||
role = command.get(name)
|
||||
if isinstance(role, dict) and role.get("status") == "DONE":
|
||||
identities.append(role.get("runBy"))
|
||||
if isinstance(fact, dict) and fact.get("status") == "DONE":
|
||||
identities.append(fact.get("runBy"))
|
||||
present = [identity for identity in identities if identity]
|
||||
if len(present) != len(set(present)):
|
||||
rep.error("품질 검토 역할은 독립된 agent여야 한다", " · ".join(map(str, present)))
|
||||
return unverifiable
|
||||
|
||||
|
||||
def verify(path: str) -> Report:
|
||||
rel = os.path.relpath(path, ROOT)
|
||||
rep = Report(rel)
|
||||
@@ -492,6 +950,7 @@ def verify(path: str) -> Report:
|
||||
st.get(REVISION_FIELD), where)
|
||||
|
||||
gates = st.get("gates") or []
|
||||
evidence_gate = _evidence_gate_v5(rep, run, sid, gates, where)
|
||||
cmds = " ; ".join(str(g.get("cmd") or "") for g in gates)
|
||||
for g in gates:
|
||||
other = _wrong_target(str(g.get("cmd") or ""), project, known_projects)
|
||||
@@ -512,6 +971,9 @@ def verify(path: str) -> Report:
|
||||
rep.error("관문이 빠졌다", f"{where} — {token}")
|
||||
for g in gates:
|
||||
cmd = str(g.get("cmd") or "")
|
||||
if evidence_gate is g:
|
||||
# semantic evidence gate는 위에서 PASS/UNVERIFIABLE 두 상태를 따로 검증했다.
|
||||
continue
|
||||
if any(tok in cmd for tok in MEASUREMENT_GATES):
|
||||
# 측정 관문 — 돌았는지만 본다. exit 칸이 아예 없으면 안 돌린 것이다
|
||||
if g.get("exit") is None:
|
||||
@@ -528,6 +990,9 @@ def verify(path: str) -> Report:
|
||||
# 한 단계를 두 번 돌렸으면 두 번째 것도 같은 잣대로 본다
|
||||
unverifiable += _side_proof(rep, st, sid, spec, where)
|
||||
|
||||
# v3부터는 S3의 command repair와 S6 이후 독립 review도 같은 원장에서 검증한다.
|
||||
unverifiable += _verify_quality_reviews(rep, run)
|
||||
|
||||
rep.facts["stages"] = counts
|
||||
if unverifiable:
|
||||
# 「위조가 아니다」와 「맞다」는 다른 말이다. 대조를 못 한 것은 수로 남긴다
|
||||
|
||||
Reference in New Issue
Block a user