[verified] Check trusted-template AC verification classification
This commit is contained in:
@@ -252,7 +252,7 @@ terminal(command="python3 harness/agents/spec/skills/gitea-implementation-spec-r
|
||||
8. Same-repository PR이면 head object도 cache에서 검증하고 exact base-to-head tree를 Gitea changed-file set·diff와 대조한다. Fork PR이면 target cache에 head object가 있다고 가정하지 않고 authoritative head repository·branch SHA와 Gitea changed-file set·diff를 대조한다. Diff 직렬화의 byte 차이만으로 실패시키지 않고 변경 경로와 exact base/head blob 및 hunk가 동일 변경을 나타내는지 검증한다. 임의 ref, checkout, clone 또는 worktree를 만들지 않는다.
|
||||
9. `release_repository` request를 보내되 Issue lease는 유지한다.
|
||||
10. Acceptance matrix 작성 전에 구현자 실행 보고와 상세 실행 기록의 출처 및 확인 범위를 구분한다. Revision이 식별된 기존 evidence는 source revision과 reviewed head 사이 전체 diff·실행 입력 변경을 확인하고, 필수 검증별 재사용 가능 범위와 현재 head에 새로 결합해야 할 근거를 먼저 요약한다. 상세 기록이나 revision 정보의 부재만으로 새 검증 gate를 만들지 않는다. 재사용 여부는 위 Acceptance matrix의 기존 evidence binding 기준으로 판단하며 새 gate나 일괄 재실행 요구를 추가하지 않는다. 재검토에서는 이전 finding별 승인 계약 근거와 최소 보완 요청을 새 구현·evidence의 확인 위치에 대응하고, 해소 또는 잔여 사유를 기존 criterion의 `evidence`와 `reason`에 기록한다. 이미 충족된 검증을 source revision 차이만으로 다시 요구하지 않되, 변경 영향으로 새로 판정할 수 없어진 필수 조건은 그 근거와 최소 보완 범위를 특정한다. 이 초기 요약만으로 검토를 끝내지 않고 Approved Discovery에서 acceptance matrix를 만들고 모든 changed file, 문서, source, test와 exact-head-bound status evidence를 대응시킨다.
|
||||
11. 문서 검토 입력과 구조·의미 분리 절차로 trusted 계약·산출물별 의무표를 준비하고 모든 문서 의무를 대조한다. 위임 시 해당 입력 전체를 제공한다. 독립 code-review worker 결과가 있으면 exact head에 바인딩됐고 authoritative하게 읽을 수 있을 때만 보조 evidence로 사용한다. 그 결과를 spec verdict로 그대로 복사하지 않는다.
|
||||
11. 문서 검토 입력과 구조·의미 분리 절차로 trusted 계약·산출물별 의무표를 준비하고 모든 문서 의무를 대조한다. 정본 형식 요구사항의 AC 검증 분류는 아래 read-only 구조 검사로 보조 확인하고 비지원 형식·의미 판단은 main이 직접 대조한다. 위임 시 해당 입력 전체를 제공한다. 독립 code-review worker 결과가 있으면 exact head에 바인딩됐고 authoritative하게 읽을 수 있을 때만 보조 evidence로 사용한다. 그 결과를 spec verdict로 그대로 복사하지 않는다.
|
||||
12. Verdict 규칙으로 하나의 strict result artifact와 canonical review body를 trusted workflow directory에 생성한다.
|
||||
13. Exact key/type, duplicate key, marker field, digest, deterministic correlation, criterion cardinality, path binding, verdict/state 조합, credential 부재와 render → parse → render byte equality를 검증한다.
|
||||
14. 여기까지는 read-only preflight다. Exact review 요청이 native review write 권한을 부여하지 않았거나 preflight가 완전하지 않으면 종료하며 review create 또는 다른 mutation을 수행하지 않는다.
|
||||
@@ -281,6 +281,14 @@ terminal(command="python3 harness/agents/spec/skills/gitea-implementation-spec-r
|
||||
- 이는 호출 형태의 확인일 뿐 인증·provider wire schema·pagination completeness·callback 내부 동작이나 향후 네트워크 성공의 증명이 아니다. Write 뒤 readback이 실패하면 응답과 정확한 대상 근거를 보존하고 기존 read-only reconciliation으로 확인하거나 uncertain/partial로 중단한다. Signature 문제를 이유로 native create를 반복하지 않는다.
|
||||
- 새 runtime gate·범용 interface 검사기·adapter·schema·journal을 추가하지 않는다. 문서 계약 회귀는 지침의 존재만 확인하고, synthetic binding/collector 회귀는 로컬 호출 동작만 확인한다. 이를 실제 agent 실행이나 live publication 검증으로 보고하지 않는다.
|
||||
|
||||
### AC 검증 분류의 read-only 구조 검사
|
||||
|
||||
Trusted requirements README/TEMPLATE와 exact reviewed-head 요구사항 snapshot을 준비한 뒤 `scripts/review_support.py check-ac-classification --document <immutable-req-snapshot> --expected-document-sha256 <independently-pinned-document-digest> --expected-template-sha256 <independently-pinned-trusted-template-digest>`를 실행한다. Expected digest는 검사 대상 파일을 그 자리에서 다시 hash해 승인하는 값이 아니라 기존 trusted input·source revision에서 독립 고정한 값이어야 한다. 정본 TEMPLATE는 helper와 같은 harness의 `docs/requirements/TEMPLATE.md`만 읽으며 제품 TEMPLATE나 임의 파일로 대체하지 않는다. CLI는 document·template의 exact UTF-8 bytes digest를 대조하고 원본을 수정하지 않는다. Digest 일치는 파일 결속일 뿐 reviewed-head 실행 provenance나 의미 인증을 대신하지 않는다.
|
||||
|
||||
- 현재 정본 형식의 `## 7. 인수 기준`에서 `## 8. 오류 및 충돌 계약`까지 flat H3 `REQ-NNN.XX-ACYY — 제목`별 `**검증 분류**` 하나와 수준·백엔드·케이스의 단일 행 필드를 확인한다. 빈 값·중복 필드·중복 AC ID·필드의 미치환 placeholder는 차단한다. 다른 AC 또는 코드 fence 안의 example로 누락 필드를 충족하지 않는다. `없음`은 기존 계약이 허용하는 값이며 새 backend enum·검증 수준 정책을 만들지 않는다.
|
||||
- 지원 범위는 canonical flat block과 LF/CRLF에 한정한다. Multiline 분류값·들여쓴/중첩 heading·HTML/HTML comment·불완전 fence·모호한 section 구조는 자동 보정하지 않고 unsupported/미확인으로 남겨 main이 exact source와 trusted 계약을 직접 대조한다. 비지원 형식의 실패를 의미 결함으로 자동 판정하지 않는다. 입력은 1MiB 이하 regular UTF-8 파일만 읽으며 최종 symlink·FIFO는 거부한다. Ancestor 경로 admission과 authenticated source·revision binding은 기존 main 책임이다.
|
||||
- Exit 0과 AC ID·checked fields·digest 출력은 해당 분류의 구조 검사 통과만 뜻한다. 전체 문서 적합성, AC ID 연속성·traceability, 필드값의 의미·검증 수준 적절성·실제 backend/test 실행·AC 충족은 기존 main 검토에 남긴다. Exit 2는 sanitized 오류이며 raw 입력이나 credential을 출력하지 않는다. 기존 result의 `evidence`·`reason`에 검사 범위·결과·미확인 이유를 기록하고 새 schema·제품 문서 자동 수정·publisher gate를 추가하지 않는다.
|
||||
|
||||
## 문서 검토 입력과 구조·의미 분리
|
||||
|
||||
문서 검토를 main이 수행하거나 delegate에 위임하기 전에 다음 입력을 trusted workflow에 준비한다. 이 준비는 제품 저장소의 새 산출물이나 result schema 변경을 요구하지 않는다.
|
||||
|
||||
+122
@@ -642,6 +642,119 @@ def verify_publication(before, after, exact_review, result, allowed_paths, revie
|
||||
return {"review_id": rid, "state": expected, "review_records_added": 1,
|
||||
"timeline_events_added": 1, "projected_review_ids": projected, "read_only": True}
|
||||
|
||||
def _classification_bytes(path):
|
||||
"""Bounded regular-file read; caller independently admits source provenance."""
|
||||
fd = None
|
||||
try:
|
||||
fd = os.open(path, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK)
|
||||
if not stat.S_ISREG(os.fstat(fd).st_mode):
|
||||
_fail("classification input unavailable")
|
||||
with os.fdopen(fd, "rb") as stream:
|
||||
fd = None
|
||||
raw = stream.read(1048577)
|
||||
if len(raw) > 1048576:
|
||||
_fail("classification input too large")
|
||||
raw.decode("utf-8")
|
||||
return raw
|
||||
except (OSError, UnicodeError, ValueError):
|
||||
_fail("classification input unavailable")
|
||||
finally:
|
||||
if fd is not None:
|
||||
os.close(fd)
|
||||
|
||||
|
||||
def _classification_lines(text):
|
||||
if type(text) is not str or len(text.encode("utf-8")) > 1048576:
|
||||
_fail("invalid classification text")
|
||||
text = text.replace("\r\n", "\n")
|
||||
if any(c in text for c in ("\r", "\x00", "\u2028", "\u2029")):
|
||||
_fail("unsupported classification text")
|
||||
visible, fence = [], None
|
||||
for line in text.split("\n"):
|
||||
match = re.fullmatch(r" {0,3}(`{3,}|~{3,})(.*)", line)
|
||||
if fence is not None:
|
||||
if match and match[1][0] == fence[0] and len(match[1]) >= fence[1] and not match[2].strip():
|
||||
fence = None
|
||||
continue
|
||||
if match:
|
||||
if match[1][0] == "`" and "`" in match[2]:
|
||||
_fail("ambiguous classification fence")
|
||||
fence = (match[1][0], len(match[1]))
|
||||
continue
|
||||
if '<!--' in line or '-->' in line or re.search(r'</?[A-Za-z][^>]*>', line):
|
||||
_fail("unsupported classification HTML")
|
||||
visible.append(line)
|
||||
if fence is not None:
|
||||
_fail("unterminated classification fence")
|
||||
return visible
|
||||
|
||||
|
||||
def check_ac_classification(body, expected_template_sha256):
|
||||
"""Check only canonical section-7 AC classification, not full compliance."""
|
||||
if type(expected_template_sha256) is not str or not HEX64.fullmatch(expected_template_sha256):
|
||||
_fail("invalid trusted template digest")
|
||||
template = _classification_bytes(Path(__file__).resolve().parents[5] / "docs/requirements/TEMPLATE.md")
|
||||
if hashlib.sha256(template).hexdigest() != expected_template_sha256:
|
||||
_fail("trusted template drift")
|
||||
template_text = template.decode("utf-8")
|
||||
try:
|
||||
template_block = template_text.split("## 7. 인수 기준\n", 1)[1].split("## 8.", 1)[0]
|
||||
classification = template_block.split("**검증 분류**\n", 1)[1].split("**상태 불변조건**", 1)[0]
|
||||
except IndexError:
|
||||
_fail("unsupported trusted template")
|
||||
fields = re.findall(r"^- ([^:]+): \$\{[a-z_]+\}$", classification, flags=re.M)
|
||||
if fields != ["수준", "백엔드", "케이스"]:
|
||||
_fail("unsupported trusted template")
|
||||
lines = _classification_lines(body)
|
||||
section = "## 7. 인수 기준"
|
||||
if lines.count(section) != 1:
|
||||
_fail("ambiguous AC section")
|
||||
start = lines.index(section) + 1
|
||||
end = next((i for i in range(start, len(lines)) if lines[i].startswith("## ")), len(lines))
|
||||
if end == len(lines) or lines[end] != "## 8. 오류 및 충돌 계약":
|
||||
_fail("unsupported AC section boundary")
|
||||
blocks, current = [], None
|
||||
for line in lines[start:end]:
|
||||
if line.startswith("### "):
|
||||
match = re.fullmatch(r"### (REQ-[0-9]{3}\.[0-9]{2}-AC[0-9]{2}) — (\S.*)", line)
|
||||
if not match:
|
||||
_fail("unsupported AC heading")
|
||||
current = (match[1], [])
|
||||
blocks.append(current)
|
||||
elif current is None:
|
||||
if line.strip():
|
||||
_fail("unsupported AC preamble")
|
||||
else:
|
||||
if line.lstrip().startswith("#"):
|
||||
_fail("ambiguous AC heading")
|
||||
current[1].append(line)
|
||||
ids = [item[0] for item in blocks]
|
||||
if not ids or len(ids) != len(set(ids)):
|
||||
_fail("missing or duplicate AC")
|
||||
for _, block in blocks:
|
||||
marker = "**검증 분류**"
|
||||
if block.count(marker) != 1:
|
||||
_fail("missing or duplicate AC classification")
|
||||
pos = block.index(marker) + 1
|
||||
stop = next((i for i in range(pos, len(block)) if block[i].startswith("**")), len(block))
|
||||
values = {}
|
||||
for line in block[pos:stop]:
|
||||
if not line.strip():
|
||||
continue
|
||||
match = re.fullmatch(r"- ([^:]+): (.*)", line)
|
||||
if not match or match[1] not in fields or match[1] in values:
|
||||
_fail("ambiguous AC classification field")
|
||||
value = match[2].strip()
|
||||
if not value or "${" in value:
|
||||
_fail("empty or placeholder AC classification")
|
||||
values[match[1]] = value
|
||||
if set(values) != set(fields):
|
||||
_fail("missing AC classification field")
|
||||
return {"ac_ids": ids, "checked_fields": fields, "read_only": True,
|
||||
"template_sha256": expected_template_sha256,
|
||||
"document_sha256": hashlib.sha256(body.encode("utf-8")).hexdigest()}
|
||||
|
||||
|
||||
def _read(path):
|
||||
try:
|
||||
with open(path, encoding="utf-8", newline="") as stream:
|
||||
@@ -670,6 +783,10 @@ def main(argv=None):
|
||||
q=sub.add_parser("check-cache"); q.add_argument("--contract-path",required=True); q.add_argument("--owner",required=True); q.add_argument("--repository",required=True); q.add_argument("--issue",required=True,type=int)
|
||||
q=sub.add_parser("check-pages"); q.add_argument("--pages",required=True); q.add_argument("--key",required=True)
|
||||
q.add_argument("--page-size", type=int, default=50); q.add_argument("--max-pages", type=int, default=10000)
|
||||
q=sub.add_parser("check-ac-classification")
|
||||
q.add_argument("--document", required=True)
|
||||
q.add_argument("--expected-document-sha256", required=True)
|
||||
q.add_argument("--expected-template-sha256", required=True)
|
||||
q=sub.add_parser("check-unchanged"); q.add_argument("--before",required=True); q.add_argument("--after",required=True)
|
||||
q=sub.add_parser("verify-publication");
|
||||
for n in ("before","after","exact-review","result","allowed-paths","inline-comments"): q.add_argument("--"+n,required=True)
|
||||
@@ -703,6 +820,11 @@ def main(argv=None):
|
||||
items = collect_pages(f, a.key, a.page_size, a.max_pages)
|
||||
if len(used) != len(pages): _fail("extra saved pages")
|
||||
print(json.dumps(items,ensure_ascii=False,sort_keys=True))
|
||||
elif a.cmd=="check-ac-classification":
|
||||
raw = _classification_bytes(a.document)
|
||||
if type(a.expected_document_sha256) is not str or not HEX64.fullmatch(a.expected_document_sha256) or hashlib.sha256(raw).hexdigest() != a.expected_document_sha256:
|
||||
_fail("classification document drift")
|
||||
print(json.dumps(check_ac_classification(raw.decode("utf-8"), a.expected_template_sha256), ensure_ascii=False, sort_keys=True))
|
||||
elif a.cmd=="check-unchanged": print(json.dumps({"unchanged":verify_unchanged(_json_file(a.before),_json_file(a.after))}))
|
||||
else:
|
||||
print(json.dumps(verify_publication(_json_file(a.before),_json_file(a.after),_json_file(a.exact_review),_json_file(a.result),_json_file(a.allowed_paths),a.reviewer_id,_json_file(a.inline_comments),_json_file(a.provider_contract) if a.provider_contract else None, expected_pr_url=a.expected_pr_url),sort_keys=True))
|
||||
|
||||
Reference in New Issue
Block a user