fix: validate Gitea conversation caches and safe commit messages
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: gitea-issue-rework
|
||||
description: Gitea PR 검토 지적을 계약에 맞춰 재작업하고 검증한다.
|
||||
version: 0.2.15
|
||||
version: 0.2.16
|
||||
author: genichin, Hermes Agent
|
||||
license: MIT
|
||||
platforms: [linux]
|
||||
@@ -178,7 +178,7 @@ Reviewer가 rework head를 새 snapshot으로 다시 검토해야 하며 이 com
|
||||
- `correlation`은 `owner`, `repository`, base-10 Issue 번호, base-10 PR 번호, base-10 review ID, lowercase reviewed head, lowercase final head와 lowercase contract digest를 이 순서로 NUL 하나씩 구분한 UTF-8 bytes의 SHA-256이다. `result-sha256`은 correlation 입력에 넣지 않는다.
|
||||
- 게시 전 exact PR top-level comment endpoint target과 fresh PR metadata binding을 검증하고 그 scoped collection만 complete pagination한 뒤 같은 correlation을 찾는다. Source Issue comment endpoint, inline-comment·review-reply endpoint와 native review collection은 검색 집합에서 제외하며 endpoint scope와 PR relation을 구분할 수 없으면 fail-closed한다. 0개면 exact body를 한 번 생성하고, 1개이며 author stable account ID·body·marker·digest가 모두 같으면 write 없는 no-op으로 재사용한다. 같은 correlation의 body가 다르거나 2개 이상이면 중복을 만들지 않고 fail-closed한다.
|
||||
- Comment create timeout·오류 뒤에는 blind retry하지 않는다. Complete comment collection을 한 번 authoritative하게 reconcile해 exact result 하나가 확인될 때만 성공으로 판정한다.
|
||||
- 게시 또는 재사용 후 exact comment ID, author stable account ID, body, created·updated timestamp, marker, digest, verified endpoint target, fresh PR metadata binding과 PR top-level collection cardinality를 readback한다. 신규 게시에서는 scoped PR top-level comment collection에 exact comment 하나만 추가되어야 한다. Provider의 aggregate comment-count field는 fresh top-level·review·inline comment cardinality로 설명 가능해야 하며 설명할 수 없으면 fail-closed한다. 신규 게시에서 PR·underlying PR-Issue의 `updated_at`은 불변이거나 exact 신규 comment 생성 timestamp와 같을 때만 허용한다. 이는 다른 native metadata가 불변인 상태의 state-delta/exact-timestamp admission이며, GET readback만으로 독립적인 causal event attestation을 제공하지 않는다. Source Issue `<issue-number>`는 `updated_at`을 포함한 전체 native metadata와 comment collection이 완전히 불변이어야 하며, provider relation을 이유로 timestamp 예외를 허용하지 않는다. Existing exact comment 재사용에서는 PR top-level collection cardinality, PR·underlying issue와 Source Issue field가 모두 불변이어야 한다. 기존 review collection 또는 그 밖의 PR field 변경은 허용하지 않는다.
|
||||
- 게시 또는 재사용 후 exact comment ID, author stable account ID, body, created·updated timestamp, marker, digest, verified endpoint target, fresh PR metadata binding과 PR top-level collection cardinality를 readback한다. 신규 게시에서는 scoped PR top-level comment collection에 exact comment 하나만 추가되어야 한다. Provider의 aggregate comment-count field는 명시된 version-specific 계약으로 설명 가능해야 하며 설명할 수 없으면 fail-closed한다. `gitea-conversation-v1` cache opt-in은 [cached conversation admission](references/readback.md#cached-conversation-admission)에 따라 snapshot의 bounded stale lag만 허용하고, 신규 게시 후에는 실제 전체 conversation 개수와 exact-match를 요구한다. Cached baseline에 무조건 `+1`을 적용하지 않으며 reuse의 counter·metadata 불변 조건은 유지한다. 신규 게시에서 PR·underlying PR-Issue의 `updated_at`은 불변이거나 exact 신규 comment 생성 timestamp와 같을 때만 허용한다. 이는 다른 native metadata가 불변인 상태의 state-delta/exact-timestamp admission이며, GET readback만으로 독립적인 causal event attestation을 제공하지 않는다. Source Issue `<issue-number>`는 `updated_at`을 포함한 전체 native metadata와 comment collection이 완전히 불변이어야 하며, provider relation을 이유로 timestamp 예외를 허용하지 않는다. Existing exact comment 재사용에서는 PR top-level collection cardinality, PR·underlying issue와 Source Issue field가 모두 불변이어야 한다. 기존 review collection 또는 그 밖의 PR field 변경은 허용하지 않는다.
|
||||
|
||||
완료 조건: exact final head와 finding matrix에 결합된 canonical comment가 impl role author로 정확히 하나 존재하고 authoritative readback을 통과한다.
|
||||
|
||||
@@ -277,7 +277,7 @@ pytest를 이미 사용하는 프로젝트의 evidence command에만 적용한
|
||||
3. Worktree의 staged·unstaged·untracked state와 미완료 Git operation을 확인한다.
|
||||
4. Contract Preservation Inventory가 필요한 경우 final tree에 다시 대응해 baseline 항목 수와 실제 상태를 검증하고 `missing=0`, `ambiguous=0`이 아니면 stage하거나 commit하지 않는다.
|
||||
5. Exact allowlist만 stage하고 staged diff를 다시 읽는다.
|
||||
6. Repository convention에 맞는 새 non-amended rework commit을 생성한다. Review별 traceability가 필요하면 commit message나 검증 기록에 PR과 review ID를 포함하되 review body를 그대로 복제하지 않는다.
|
||||
6. Commit 전에 subject와 body를 공통 [commit-message reference safety](../../../../operations/repository-cache.md#commit-message-reference-safety) 규칙에 따라 확인한다. Protected Issue metadata가 immutable이어야 하면 hash-number auto-reference와 parser-triggering closing/reopening 문구를 피하고 `Issue 18 / PR 19 / review 76` 또는 evidence로 추적한다. 기본은 새 non-amended commit이며, amend는 명시적 사용자 승인·미게시 증명·tree 동일성 증명과 새 SHA exact verification이 모두 있을 때만 허용된다. 일반 amend 권한으로 해석하지 않는다.
|
||||
7. Commit 후 clean HEAD에서 최종 formatter, test와 필요한 runtime·browser 검증을 다시 실행한다. 이 뒤 repository file을 바꾸면 영향을 받은 검증을 다시 실행한다.
|
||||
8. 최종 검증 기록은 exact HEAD 또는 검증한 source tree, command, exit code, pass·skip·fail 수치, 환경, relevant output과 reviewer가 접근 가능한 evidence location을 포함하며, PR body·결과 comment의 각 실행 주장과 수치는 해당 location에 일대일 대응시킨다. Hermes 내부 tool transcript, delegate 자기보고, 삭제 예정 임시 파일, reviewer가 접근할 수 없는 local path와 요약 수치만 있는 기록은 evidence로 인정하지 않고 `evidence_gap` 또는 `partial`로 유지한다. 새 evidence는 checkout 밖 정본 verification root에 보존하여 final HEAD를 기록하며 evidence-only descendant를 만들지 않는다. 과거 tracked evidence의 archived 분석은 fresh 실행·원격 head 검증과 구분한다.
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ Initial intake validates collected rows, endpoint/pagination provenance, duplica
|
||||
|
||||
The receipt adds `counter_diagnostics`, with keys `inline`, `pr` and `pr_issue`. Each entry records the declared `field`/`components`, unmodified `observed` value, collection-derived `expected` value, and `status`: `match`, `mismatch`, or `unverified` (missing, negative or non-integer observed counter, including booleans). A match means only that the explicitly declared formula matches this observation; it does not attest deployed-provider semantics or counter freshness. Diagnostics never auto-select a projection, modify raw counts, or grant edit/publication permission. Missing/malformed PR counters do not bypass malformed native records, failed GETs, incomplete lists or drift checks.
|
||||
|
||||
Provider schema/version binding and explicit declarations are still required. Rework/no-op/blocked decisions retain their role/head/contract/state rules regardless of diagnostics. `snapshot`/`verify-live` publication readback continues to require strict provider counter crosschecks and its existing explicitly declared fallback; an initial receipt cannot substitute for that verification.
|
||||
Initial diagnostic intake remains unchanged. Publication requires strict counter validation and explicit version-specific cache opt-in only: optional aggregate semantics are supported for `cache_mode: "gitea-conversation-v1"`, version `1.27.3`, with `comments` components exactly `["top_level", "reviews"]`; omission of the mode retains strict legacy behavior. No numeric fitting or automatic profile/provider adjustment. Rework/no-op/blocked decisions retain their role/head/contract/state rules regardless of diagnostics. `snapshot`/`verify-live` publication readback continues to require strict provider counter crosschecks and its existing explicitly declared fallback; an initial receipt cannot substitute for that verification.
|
||||
|
||||
## Derived decisions
|
||||
|
||||
|
||||
@@ -36,28 +36,52 @@ pre-write delegation admission.
|
||||
Schema: `gitea-agent.rework-provider/v1`. The contract explicitly declares the
|
||||
aggregate semantics for PR and underlying PR-Issue `comments` fields. Each
|
||||
`components` array contains unique values from `top_level`, `reviews`, `inline`
|
||||
and must include `top_level`. Choose the reviewed deployment semantics, not the
|
||||
and must include `top_level`. An optional `cache_mode: "gitea-conversation-v1"`
|
||||
on each aggregate scope is accepted only for provider version `1.27.3`, field
|
||||
`comments`, and components consisting of exactly `top_level` and `reviews`.
|
||||
Omission retains strict legacy behavior. Choose the reviewed deployment semantics, not the
|
||||
combination that happens to fit a current response. There is no guessed version
|
||||
or automatic capability downgrade.
|
||||
|
||||
Shape example only, **not a claim about this deployment**:
|
||||
Shape example only: upstream v1.27.3 source supports this shape; it does not attest that a custom build matches upstream:
|
||||
|
||||
```json
|
||||
{
|
||||
"schema": "gitea-agent.rework-provider/v1",
|
||||
"version": "<independently-observed-version>",
|
||||
"version": "1.27.3",
|
||||
"aggregate": {
|
||||
"pr": {"field": "comments", "components": ["top_level"]},
|
||||
"pr_issue": {"field": "comments", "components": ["top_level"]}
|
||||
"pr": {"field": "comments", "components": ["top_level", "reviews"], "cache_mode": "gitea-conversation-v1"},
|
||||
"pr_issue": {"field": "comments", "components": ["top_level", "reviews"], "cache_mode": "gitea-conversation-v1"}
|
||||
},
|
||||
"inline": {
|
||||
"mode": "per_review",
|
||||
"field": "review_comments",
|
||||
"components": ["inline"]
|
||||
"components": ["reviews"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Cached conversation admission
|
||||
|
||||
For the explicit cache mode, snapshot counters must be present non-boolean,
|
||||
nonnegative integers no greater than the complete collection-derived conversation
|
||||
count. This admits bounded stale lag, not arbitrary mismatch; counters ahead of
|
||||
the collected count, missing values and malformed scalars remain BLOCK.
|
||||
Upstream `v1.27.3` `services/convert/issue.go` reads stored `Issue.NumComments`.
|
||||
`models/issues/comment.go` counts plain comments and review-summary comments;
|
||||
review creation bypasses the recomputation that plain-comment creation performs.
|
||||
This source pin explains the opt-in policy, not a deployed custom-build attestation.
|
||||
|
||||
For `create`, fresh readback must prove exactly one canonical top-level addition
|
||||
and the recomputed aggregate must equal the complete post-write conversation
|
||||
count. A stale baseline counter `0` with one review can become `2` after the new
|
||||
plain comment; `before counter + 1` is not the cache-mode rule. All old rows,
|
||||
reviews, inline evidence, Source Issue metadata/comments, identity, body, head
|
||||
and unrelated native fields retain their existing protections. For `reuse`, no
|
||||
addition or counter/metadata refresh is admitted: even a legitimate-looking
|
||||
cache change without the authorized create action is BLOCK. Strict contracts
|
||||
without cache opt-in retain exact snapshot projections and the original delta.
|
||||
|
||||
For `inline.components`, the explicit projection modes are `["inline"]`,
|
||||
`["reviews"]`, and `["reviews", "inline"]`. The review-only mode projects
|
||||
only the number of reviews into `review_comments`; it does not add per-review
|
||||
|
||||
@@ -138,11 +138,16 @@ class ReworkReadbackAdapter:
|
||||
for scope in ('pr','pr_issue'):
|
||||
spec = object_value(aggregate[scope])
|
||||
parts = spec.get('components')
|
||||
if (set(spec) != {'field','components'} or spec.get('field') != 'comments'
|
||||
mode = spec.get('cache_mode')
|
||||
allowed_keys = {'field','components'} if mode is None and 'cache_mode' not in spec else {'field','components','cache_mode'}
|
||||
if (set(spec) != allowed_keys or spec.get('field') != 'comments'
|
||||
or type(parts) is not list or not parts
|
||||
or any(type(part) is not str or part not in ('top_level','reviews','inline') for part in parts)
|
||||
or len(parts) != len(set(parts)) or 'top_level' not in parts):
|
||||
raise ReadbackError('invalid aggregate count declaration')
|
||||
if 'cache_mode' in spec and (mode != 'gitea-conversation-v1' or contract['version'] != '1.27.3'
|
||||
or set(parts) != {'top_level','reviews'}):
|
||||
raise ReadbackError('invalid aggregate cache declaration')
|
||||
inline = object_value(contract.get('inline'))
|
||||
if (set(inline) != {'mode','field','components'}
|
||||
or inline.get('mode') not in ('per_review','per_review_or_pr_count')
|
||||
@@ -392,7 +397,12 @@ class ReworkReadbackAdapter:
|
||||
raise ReadbackError('inline provider counter crosscheck failed')
|
||||
for scope in ('pr','pr_issue'):
|
||||
spec = self.provider['aggregate'][scope]
|
||||
if integer(raw[scope].get(spec['field'])) != sum(counts[name] for name in spec['components']):
|
||||
observed = raw[scope].get(spec['field'])
|
||||
computed = sum(counts[name] for name in spec['components'])
|
||||
if spec.get('cache_mode') == 'gitea-conversation-v1':
|
||||
if integer(observed) > computed:
|
||||
raise ReadbackError('cached provider aggregate count exceeds collection')
|
||||
elif integer(observed) != computed:
|
||||
raise ReadbackError('provider aggregate count mismatch')
|
||||
return counts
|
||||
|
||||
@@ -586,8 +596,17 @@ class ReworkReadbackAdapter:
|
||||
raise ReadbackError('source Issue metadata drift')
|
||||
else:
|
||||
field = self.provider['aggregate'][name]['field']
|
||||
if integer(now.pop(field)) != integer(before.pop(field)) + (1 if action == 'create' else 0):
|
||||
spec = self.provider['aggregate'][name]
|
||||
before_value, now_value = integer(before.get(field)), integer(now.get(field))
|
||||
if spec.get('cache_mode') == 'gitea-conversation-v1':
|
||||
desired = (sum(self.counts(a)[part] for part in spec['components'])
|
||||
if action == 'create' else before_value)
|
||||
if now_value != desired:
|
||||
raise ReadbackError('cached PR counter causality mismatch')
|
||||
elif now_value != before_value + (1 if action == 'create' else 0):
|
||||
raise ReadbackError('PR counter delta causality mismatch')
|
||||
before.pop(field)
|
||||
now.pop(field)
|
||||
if name != 'source_issue' and action == 'create' and now['updated_at'] != before['updated_at']:
|
||||
if now['updated_at'] != matched['created_at']:
|
||||
raise ReadbackError('uncausal Issue or PR timestamp')
|
||||
|
||||
Reference in New Issue
Block a user