[verified] Separate rework intake from publication counter checks
This commit is contained in:
@@ -120,8 +120,8 @@ def _validate_raw(adapter, raw, inp):
|
||||
raise InputGateError('incomplete review comment endpoints')
|
||||
for rid, collected in raw['inline'].items():
|
||||
adapter.validate_collection(collected, prefix+'/pulls/'+str(target['pr'])+'/reviews/'+rid+'/comments',
|
||||
allow_missing=adapter.provider['inline']['mode'] == 'per_review_or_pr_count')
|
||||
adapter.counts(raw)
|
||||
allow_missing=False)
|
||||
adapter.collection_counts(raw)
|
||||
source = raw['source_issue']
|
||||
if source['body'].splitlines()[:1] != ['<!-- gitea-agent:discovery-draft/v2 -->']:
|
||||
raise InputGateError('invalid discovery draft marker')
|
||||
@@ -225,11 +225,12 @@ def derive_decision(raw, inp, impl_actor_id):
|
||||
if len(json.dumps(decision,ensure_ascii=False,sort_keys=True).encode()) > 1_000_000: raise InputGateError('decision artifact too large')
|
||||
return decision
|
||||
|
||||
def build_receipt(raw, inp, decision):
|
||||
def build_receipt(raw, inp, decision, counter_diagnostics):
|
||||
receipt = {'schema':RECEIPT_SCHEMA,'status':decision['status'],'reason_codes':decision['reason_codes'],
|
||||
'target':decision['target'],'head':decision['head'],'contract_sha256':decision['contract_sha256'],
|
||||
'actor':decision['actor'],'input_sha256':digest(inp),'raw_input_snapshot':copy.deepcopy(raw),
|
||||
'raw_input_sha256':digest(raw),'fresh_GET_provenance':{'kind':'fresh_authenticated_GET','read_only':True},
|
||||
'counter_diagnostics':copy.deepcopy(counter_diagnostics),
|
||||
'decision':decision}
|
||||
if len(json.dumps(receipt, ensure_ascii=False, sort_keys=True, allow_nan=False).encode('utf-8')) > 1_000_000:
|
||||
raise InputGateError('receipt artifact too large')
|
||||
|
||||
@@ -332,13 +332,15 @@ class ReworkReadbackAdapter:
|
||||
or review['user']['login'] not in ('gitea-review-agent', 'gitea-spec-agent')):
|
||||
raise ReadbackError('source review author or state mismatch')
|
||||
|
||||
def counts(self, raw):
|
||||
def _collection_counts(self, raw, *, allow_counter_fallback=False):
|
||||
inline = sum(integer(review.get('comments_count')) for review in raw['reviews']['rows'])
|
||||
fallback = False
|
||||
inline_ids = set()
|
||||
for review in raw['reviews']['rows']:
|
||||
collected = raw['inline'][str(review['id'])]
|
||||
if collected['pages'][0]['status'] in (404,405):
|
||||
if not allow_counter_fallback:
|
||||
raise ReadbackError('inline collection unavailable for input gate')
|
||||
if self.provider['inline']['mode'] != 'per_review_or_pr_count':
|
||||
raise ReadbackError('inline fallback not explicitly authorized')
|
||||
fallback = True
|
||||
@@ -347,29 +349,54 @@ class ReworkReadbackAdapter:
|
||||
for row in collected['rows']:
|
||||
if row['id'] in inline_ids: raise ReadbackError('duplicate cross-review inline ID')
|
||||
inline_ids.add(row['id'])
|
||||
parts = self.provider['inline']['components']
|
||||
computed = ((inline if 'inline' in parts else 0)
|
||||
+ (len(raw['reviews']['rows']) if 'reviews' in parts else 0))
|
||||
field = self.provider['inline']['field']
|
||||
if field not in raw['pr']:
|
||||
if (self.provider['inline']['mode'] != 'per_review' or fallback or computed != 0
|
||||
or len(inline_ids) != 0):
|
||||
raise ReadbackError('missing inline provider counter')
|
||||
elif integer(raw['pr'].get(field)) != computed:
|
||||
raise ReadbackError('inline provider counter crosscheck failed')
|
||||
if not fallback and len(inline_ids) != inline:
|
||||
raise ReadbackError('inline record cardinality mismatch')
|
||||
counts = {'top_level':len(raw['comments']['rows']),
|
||||
'reviews':len(raw['reviews']['rows']), 'inline':inline}
|
||||
if integer(raw['source_issue'].get('comments')) != len(raw['source_comments']['rows']):
|
||||
raise ReadbackError('source collection count mismatch')
|
||||
return counts, fallback
|
||||
|
||||
def collection_counts(self, raw):
|
||||
"""Initial intake requires real inline records, never a counter-only fallback."""
|
||||
return self._collection_counts(raw)[0]
|
||||
|
||||
def counter_diagnostics(self, raw):
|
||||
"""Declared PR projections are diagnostic only for complete initial intake."""
|
||||
counts = self.collection_counts(raw)
|
||||
diagnostics = {}
|
||||
projections = {'inline': ('pr', self.provider['inline'])}
|
||||
projections.update({scope: (scope, self.provider['aggregate'][scope])
|
||||
for scope in ('pr', 'pr_issue')})
|
||||
for name, (scope, spec) in projections.items():
|
||||
observed = raw[scope].get(spec['field'])
|
||||
expected = sum(counts[part] for part in spec['components'])
|
||||
valid = type(observed) is int and observed >= 0
|
||||
diagnostics[name] = {'field':spec['field'], 'components':list(spec['components']),
|
||||
'observed':observed, 'expected':expected,
|
||||
'status':('match' if observed == expected else 'mismatch')
|
||||
if valid else 'unverified'}
|
||||
return diagnostics
|
||||
|
||||
def counts(self, raw):
|
||||
"""Publication/readback retains strict provider counter crosschecks."""
|
||||
counts, fallback = self._collection_counts(raw, allow_counter_fallback=True)
|
||||
parts = self.provider['inline']['components']
|
||||
computed = sum(counts[part] for part in parts)
|
||||
field = self.provider['inline']['field']
|
||||
if field not in raw['pr']:
|
||||
if (self.provider['inline']['mode'] != 'per_review' or fallback or computed != 0
|
||||
or counts['inline'] != 0):
|
||||
raise ReadbackError('missing inline provider counter')
|
||||
elif integer(raw['pr'].get(field)) != computed:
|
||||
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']):
|
||||
raise ReadbackError('provider aggregate count mismatch')
|
||||
if integer(raw['source_issue'].get('comments')) != len(raw['source_comments']['rows']):
|
||||
raise ReadbackError('source collection count mismatch')
|
||||
return counts
|
||||
|
||||
def capture_input(self, target, *, require_default_branch=True):
|
||||
def _capture_raw(self, target, *, require_default_branch):
|
||||
identity, _ = self.get('/user')
|
||||
if integer(identity.get('id'), 1) != self.actor_id or identity.get('login') != 'gitea-impl-agent':
|
||||
raise ReadbackError('authenticated identity mismatch')
|
||||
@@ -389,11 +416,16 @@ class ReworkReadbackAdapter:
|
||||
raw['inline'] = {str(review['id']):self.collection(prefix+'/pulls/'+str(target['pr'])+
|
||||
'/reviews/'+str(review['id'])+'/comments', allow_missing=self.provider['inline']['mode'] == 'per_review_or_pr_count') for review in raw['reviews']['rows']}
|
||||
self.bind_input(target, raw, require_default_branch=require_default_branch)
|
||||
self.counts(raw)
|
||||
return raw
|
||||
|
||||
def capture_input(self, target, *, require_default_branch=True):
|
||||
raw = self._capture_raw(target, require_default_branch=require_default_branch)
|
||||
self.collection_counts(raw)
|
||||
return raw
|
||||
|
||||
def capture(self, target):
|
||||
raw = self.capture_input(target, require_default_branch=False)
|
||||
raw = self._capture_raw(target, require_default_branch=False)
|
||||
self.counts(raw)
|
||||
self.bind(target, raw)
|
||||
return raw
|
||||
|
||||
@@ -416,7 +448,7 @@ class ReworkReadbackAdapter:
|
||||
if digest(first) != digest(second):
|
||||
raise ReadbackError('concurrent provider drift')
|
||||
decision = derive_decision(first, value, self.actor_id)
|
||||
return build_receipt(first, value, decision)
|
||||
return build_receipt(first, value, decision, self.counter_diagnostics(first))
|
||||
|
||||
def snapshot(self, expected):
|
||||
target = self.target(expected)
|
||||
|
||||
Reference in New Issue
Block a user