@givolt/i3-composer-agent 2.3.7 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/plugins/marketplace.json +1 -1
- package/.claude/commands/i3-sop.md +19 -0
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +2 -2
- package/.gemini/commands/i3-sop.toml +19 -0
- package/README.md +37 -3
- package/commands/i3-sop.toml +19 -0
- package/docs/codebuddy-setup.md +3 -3
- package/docs/codex-setup.md +2 -2
- package/docs/other-hosts.md +2 -2
- package/docs/workbuddy-setup.md +3 -3
- package/evals/cases/i3-enterprise-sop.json +64 -0
- package/evals/fixtures/i3-enterprise-sop/brief.md +11 -0
- package/evals/zh-CN/cases/i3-enterprise-sop.json +61 -0
- package/package.json +7 -1
- package/plugin.json +1 -1
- package/references/engineering-skeleton-resources/README.md +52 -0
- package/references/engineering-skeleton-resources/adoption.md +200 -0
- package/references/engineering-skeleton-resources/catalog.json +17 -0
- package/references/engineering-skeleton-resources/directory-standard.md +119 -0
- package/references/enterprise-sop-resources/README.md +165 -0
- package/references/enterprise-sop-resources/ad/README.md +70 -0
- package/references/enterprise-sop-resources/ad/assets-manifest.json +106 -0
- package/references/enterprise-sop-resources/ad/prompts/01-generate.md +58 -0
- package/references/enterprise-sop-resources/ad/prompts/02-review.md +54 -0
- package/references/enterprise-sop-resources/ad/prompts/03-revise.md +54 -0
- package/references/enterprise-sop-resources/ad/prompts/04-recheck.md +54 -0
- package/references/enterprise-sop-resources/ad/templates/AD.md +337 -0
- package/references/enterprise-sop-resources/assets-manifest.json +199 -0
- package/references/enterprise-sop-resources/brd/README.md +50 -0
- package/references/enterprise-sop-resources/brd/assets-manifest.json +94 -0
- package/references/enterprise-sop-resources/brd/prompts/01-generate.md +51 -0
- package/references/enterprise-sop-resources/brd/prompts/02-review.md +47 -0
- package/references/enterprise-sop-resources/brd/prompts/03-revise.md +47 -0
- package/references/enterprise-sop-resources/brd/prompts/04-recheck.md +47 -0
- package/references/enterprise-sop-resources/brd/templates/BRD.md +169 -0
- package/references/enterprise-sop-resources/codebuddy-project-guide.md +34 -0
- package/references/enterprise-sop-resources/controlled-handoff.md +41 -0
- package/references/enterprise-sop-resources/controlled_handoff.py +334 -0
- package/references/enterprise-sop-resources/handoff-contract.md +196 -0
- package/references/enterprise-sop-resources/handoff.py +277 -0
- package/references/enterprise-sop-resources/lifecycle-contract.md +91 -0
- package/references/enterprise-sop-resources/plan-handoff.md +73 -0
- package/references/enterprise-sop-resources/prd/README.md +137 -0
- package/references/enterprise-sop-resources/prd/assets-manifest.json +115 -0
- package/references/enterprise-sop-resources/prd/prompts/01-generate.md +64 -0
- package/references/enterprise-sop-resources/prd/prompts/02-review.md +49 -0
- package/references/enterprise-sop-resources/prd/prompts/03-revise.md +48 -0
- package/references/enterprise-sop-resources/prd/prompts/04-recheck.md +47 -0
- package/references/enterprise-sop-resources/prd/template-contracts.md +39 -0
- package/references/enterprise-sop-resources/prd/templates/AD.md +17 -0
- package/references/enterprise-sop-resources/prd/templates/PRD.md +257 -0
- package/references/enterprise-sop-resources/recovery-contract.md +65 -0
- package/references/enterprise-sop-resources/review-handoff.md +105 -0
- package/references/enterprise-sop-resources/selection-handoff.md +55 -0
- package/references/enterprise-sop-resources/selection_handoff.py +225 -0
- package/references/enterprise-sop-resources/stages.json +262 -0
- package/references/enterprise-sop-resources/technical-baseline/README.md +132 -0
- package/references/enterprise-sop-resources/technical-baseline/assets-manifest.json +101 -0
- package/references/enterprise-sop-resources/technical-baseline/baselines/PROVENANCE.md +40 -0
- package/references/enterprise-sop-resources/technical-baseline/baselines//344/274/201/344/270/232/345/272/224/347/224/250-/346/212/200/346/234/257/350/247/204/347/272/246_v0.0.2.md +295 -0
- package/references/enterprise-sop-resources/technical-baseline/references/engineering-directory-v0.2.0/adoption.md +112 -0
- package/references/enterprise-sop-resources/technical-baseline/references/engineering-directory-v0.2.0/directory-standard.md +119 -0
- package/references/enterprise-sop-resources/technical-baseline/templates/TECHNICAL-BASELINE.md +133 -0
- package/references/enterprise-sop-resources/template-contracts.md +142 -0
- package/references/enterprise-sop-resources/testcase/README.md +61 -0
- package/references/enterprise-sop-resources/testcase/assets-manifest.json +111 -0
- package/references/enterprise-sop-resources/testcase/prompts/01-generate.md +53 -0
- package/references/enterprise-sop-resources/testcase/prompts/02-review.md +51 -0
- package/references/enterprise-sop-resources/testcase/prompts/03-revise.md +49 -0
- package/references/enterprise-sop-resources/testcase/prompts/04-recheck.md +51 -0
- package/references/enterprise-sop-resources/testcase/templates/TestCase.md +269 -0
- package/references/enterprise-sop-resources/verification/README.md +149 -0
- package/references/enterprise-sop-resources/verification/check_execution.py +185 -0
- package/references/enterprise-sop-resources/verification/check_native_execution.py +282 -0
- package/references/enterprise-sop-resources/verification/check_reads.py +348 -0
- package/references/enterprise-sop-resources/verification/review-output.schema.json +155 -0
- package/references/enterprise-sop-resources/verification/review_handoff.py +329 -0
- package/skills/i3-enterprise-sop/SKILL.md +173 -0
- package/skills/i3-enterprise-sop/agents/openai.yaml +2 -0
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
"""Read-only checker for CodeBuddy stream-json evidence. No project or report writes."""
|
|
2
|
+
import json,re
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
def assess(project_root, scope, events):
|
|
6
|
+
root=Path(project_root).resolve()
|
|
7
|
+
def relative(raw):
|
|
8
|
+
p=Path(raw);p=(p if p.is_absolute() else root/p).resolve()
|
|
9
|
+
return str(p.relative_to(root)) if p.is_relative_to(root) else str(p)
|
|
10
|
+
calls=[];results={};finals=[]
|
|
11
|
+
for seq,event in enumerate(events):
|
|
12
|
+
if event.get('type')=='result':finals.append(event)
|
|
13
|
+
message=event.get('message',{})
|
|
14
|
+
if not isinstance(message,dict):continue
|
|
15
|
+
for block in message.get('content',[]):
|
|
16
|
+
if not isinstance(block,dict):continue
|
|
17
|
+
if event.get('type')=='assistant' and block.get('type')=='tool_use':
|
|
18
|
+
calls.append({**block,'event':seq})
|
|
19
|
+
if event.get('type')=='user' and block.get('type')=='tool_result':
|
|
20
|
+
results[block['tool_use_id']]={**block,'receiptEvent':seq}
|
|
21
|
+
reads=[];writes=[];errors=[]
|
|
22
|
+
for call in calls:
|
|
23
|
+
inp=call.get('input',{});result=results.get(call['id'])
|
|
24
|
+
raw=inp.get('file_path') or inp.get('path')
|
|
25
|
+
path=relative(raw) if raw else None
|
|
26
|
+
success=result is not None and not result.get('is_error',False)
|
|
27
|
+
item={'tool':call['name'],'path':path,'event':call['event'],
|
|
28
|
+
'receiptEvent':result['receiptEvent'] if result else float('inf'),'successful':success}
|
|
29
|
+
if call['name']=='Read':
|
|
30
|
+
data=result.get('content',[]) if result else []
|
|
31
|
+
text=data if isinstance(data,str) else '\n'.join(x.get('text','') for x in data if isinstance(x,dict))
|
|
32
|
+
reads.append({**item,'lines':[int(n) for n in re.findall(r'^\s*(\d+)→',text,re.M)]})
|
|
33
|
+
elif call['name'] in ['Write','Edit','Bash','PowerShell','NotebookEdit','Skill','Task','Agent']:
|
|
34
|
+
writes.append(item)
|
|
35
|
+
if result and result.get('is_error'):errors.append(item)
|
|
36
|
+
allowed={relative(p) for p in scope.get('allowedWrites',[])}
|
|
37
|
+
first_write=min((x['event'] for x in writes if x['successful']),default=float('inf'))
|
|
38
|
+
coverage=[]
|
|
39
|
+
for raw in scope['requiredReads']:
|
|
40
|
+
path=relative(raw);p=root/path
|
|
41
|
+
count=scope.get('beforeLineCounts',{}).get(raw)
|
|
42
|
+
if count is None:count=len(p.read_text().splitlines()) if p.is_file() else None
|
|
43
|
+
observed=[x for x in reads if x['path']==path and x['successful'] and x['receiptEvent']<first_write]
|
|
44
|
+
lines={n for x in observed for n in x['lines']}
|
|
45
|
+
full=count is not None and bool(observed) and all(n in lines for n in range(1,count+1))
|
|
46
|
+
coverage.append({'path':path,'exists':p.is_file(),'expectedLines':count,'successfulRead':bool(observed),'fullRead':full})
|
|
47
|
+
missing=[x['path'] for x in coverage if not x['successfulRead']]
|
|
48
|
+
partial=[x['path'] for x in coverage if x['successfulRead'] and not x['fullRead']]
|
|
49
|
+
explicit_external={relative(p) for p in scope['requiredReads'] if Path(relative(p)).is_absolute()}
|
|
50
|
+
forbidden=[]
|
|
51
|
+
for read in reads:
|
|
52
|
+
p=read['path']
|
|
53
|
+
if read['successful'] and ((Path(p).is_absolute() and p not in explicit_external) or any(p==relative(x) or p.startswith(relative(x).rstrip('/')+'/') for x in scope.get('forbiddenAccessPrefixes',[]))):forbidden.append(p)
|
|
54
|
+
targets=[]
|
|
55
|
+
for path in sorted(allowed):
|
|
56
|
+
p=root/path;changes=[x for x in writes if x['path']==path and x['successful'] and x['tool'] in ['Write','Edit']]
|
|
57
|
+
last=max((x['receiptEvent'] for x in changes),default=float('inf'))
|
|
58
|
+
final_reads=[x for x in reads if x['path']==path and x['successful'] and x['event']>last and x['receiptEvent']>last]
|
|
59
|
+
count=len(p.read_text().splitlines()) if p.is_file() else None
|
|
60
|
+
lines={n for x in final_reads for n in x['lines']}
|
|
61
|
+
full=bool(changes and final_reads) and count is not None and all(n in lines for n in range(1,count+1))
|
|
62
|
+
targets.append({'path':path,'lastWriteEvent':last if changes else None,'finalLines':count,'fullReadAfterLastEdit':full})
|
|
63
|
+
failures=[]
|
|
64
|
+
if not finals or finals[-1].get('is_error',False):failures.append('native_final_not_proven')
|
|
65
|
+
if missing:failures.append('required_read_missing')
|
|
66
|
+
if partial:failures.append('required_read_incomplete')
|
|
67
|
+
if forbidden:failures.append('forbidden_source_read')
|
|
68
|
+
if any(x['tool'] not in ['Write','Edit'] or x['path'] not in allowed for x in writes):failures.append('tool_or_write_boundary')
|
|
69
|
+
if any(not x['fullReadAfterLastEdit'] for x in targets):failures.append('final_full_read_incomplete')
|
|
70
|
+
return {'format':'i3-read-evidence/v1','passed':not failures,'failures':failures,'requiredReadCoverage':coverage,
|
|
71
|
+
'missingReads':missing,'partialReads':partial,'forbiddenReads':sorted(set(forbidden)),'targetChecks':targets,
|
|
72
|
+
'observedReads':reads,'observedWrites':writes,'toolErrors':errors,
|
|
73
|
+
'proofScope':'actual native reads and final read coverage only; not semantic correctness, approval or runtime interception'}
|
|
74
|
+
|
|
75
|
+
def _schema_errors(value, node, schema, location='$'):
|
|
76
|
+
"""Validate only the keywords used by our bundled contract, without dependencies.
|
|
77
|
+
|
|
78
|
+
This is not a general JSON Schema engine. Unknown keywords fail closed.
|
|
79
|
+
"""
|
|
80
|
+
supported = {'$schema', '$id', 'title', 'description', 'type', 'required', 'properties',
|
|
81
|
+
'additionalProperties', 'items', 'minItems', 'minimum', 'minLength',
|
|
82
|
+
'enum', '$ref', '$defs'}
|
|
83
|
+
if set(node) - supported:
|
|
84
|
+
raise ValueError('unsupported bundled schema keyword at ' + location)
|
|
85
|
+
if '$ref' in node:
|
|
86
|
+
prefix = '#/$defs/'
|
|
87
|
+
ref = node['$ref']
|
|
88
|
+
if not ref.startswith(prefix):
|
|
89
|
+
raise ValueError('only bundled local schema references are supported')
|
|
90
|
+
return _schema_errors(value, schema['$defs'][ref[len(prefix):]], schema, location)
|
|
91
|
+
types = {'object': lambda x: isinstance(x, dict), 'array': lambda x: isinstance(x, list),
|
|
92
|
+
'string': lambda x: isinstance(x, str), 'integer': lambda x: type(x) is int}
|
|
93
|
+
kind = node.get('type')
|
|
94
|
+
if kind and (kind not in types or not types[kind](value)):
|
|
95
|
+
return [location + ': expected ' + str(kind)]
|
|
96
|
+
errors = []
|
|
97
|
+
if 'enum' in node and value not in node['enum']:
|
|
98
|
+
errors.append(location + ': value outside enum')
|
|
99
|
+
if kind == 'object':
|
|
100
|
+
properties = node.get('properties', {})
|
|
101
|
+
errors += [location + '.' + key + ': missing' for key in node.get('required', []) if key not in value]
|
|
102
|
+
if node.get('additionalProperties') is False:
|
|
103
|
+
errors += [location + '.' + key + ': unknown field' for key in value if key not in properties]
|
|
104
|
+
for key in value.keys() & properties.keys():
|
|
105
|
+
errors += _schema_errors(value[key], properties[key], schema, location + '.' + key)
|
|
106
|
+
elif kind == 'array':
|
|
107
|
+
if len(value) < node.get('minItems', 0):
|
|
108
|
+
errors.append(location + ': too few items')
|
|
109
|
+
for index, item in enumerate(value):
|
|
110
|
+
errors += _schema_errors(item, node.get('items', {}), schema, f'{location}[{index}]')
|
|
111
|
+
elif kind == 'string' and len(value.strip()) < node.get('minLength', 0):
|
|
112
|
+
errors.append(location + ': empty string')
|
|
113
|
+
elif kind == 'integer' and value < node.get('minimum', value):
|
|
114
|
+
errors.append(location + ': below minimum')
|
|
115
|
+
return errors
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _source_role_bindings(scope):
|
|
119
|
+
"""One caller-owned role table for both schema generation and evidence checks."""
|
|
120
|
+
if not isinstance(scope, dict):
|
|
121
|
+
raise ValueError('review scope must be an object')
|
|
122
|
+
roles = scope.get('sourceRoles', {})
|
|
123
|
+
required = scope.get('requiredSourceRoles', [])
|
|
124
|
+
if (not isinstance(roles, dict)
|
|
125
|
+
or not all(isinstance(k, str) and isinstance(v, str) and k.strip() and v.strip() for k, v in roles.items())
|
|
126
|
+
or not isinstance(required, list)
|
|
127
|
+
or any(not isinstance(k, str) or k not in roles for k in required)):
|
|
128
|
+
raise ValueError('sourceRoles/requiredSourceRoles must be explicit caller bindings')
|
|
129
|
+
return dict(roles), list(required)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def build_review_schema(scope):
|
|
133
|
+
"""Build a fresh CLI-compatible job schema from the bundled schema and scope.
|
|
134
|
+
|
|
135
|
+
Role enums are bound before generation. No project, scope or report writes.
|
|
136
|
+
The evidence validator uses the same _source_role_bindings function.
|
|
137
|
+
"""
|
|
138
|
+
roles, _ = _source_role_bindings(scope)
|
|
139
|
+
schema = json.loads(Path(__file__).with_name('review-output.schema.json').read_text())
|
|
140
|
+
properties = schema['$defs']['reference']['properties']
|
|
141
|
+
if roles:
|
|
142
|
+
properties['role']['enum'] = sorted(roles)
|
|
143
|
+
else:
|
|
144
|
+
properties.pop('role', None)
|
|
145
|
+
|
|
146
|
+
def inline(node):
|
|
147
|
+
if isinstance(node, list):
|
|
148
|
+
return [inline(x) for x in node]
|
|
149
|
+
if not isinstance(node, dict):
|
|
150
|
+
return node
|
|
151
|
+
if '$ref' in node:
|
|
152
|
+
prefix = '#/$defs/'
|
|
153
|
+
if not node['$ref'].startswith(prefix):
|
|
154
|
+
raise ValueError('only bundled local schema references are supported')
|
|
155
|
+
return inline(schema['$defs'][node['$ref'][len(prefix):]])
|
|
156
|
+
return {k: inline(v) for k, v in node.items() if k not in ['$defs', '$schema', '$id']}
|
|
157
|
+
return inline(schema)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _unique_object(pairs):
|
|
161
|
+
result = {}
|
|
162
|
+
for key, value in pairs:
|
|
163
|
+
if key in result:
|
|
164
|
+
raise ValueError('duplicate JSON key: ' + key)
|
|
165
|
+
result[key] = value
|
|
166
|
+
return result
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _invalid_constant(value):
|
|
170
|
+
raise ValueError('nonstandard JSON constant: ' + value)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def check_review(project_root, events, assessment, scope=None):
|
|
174
|
+
scope = scope or {}
|
|
175
|
+
issues, checked = [], []
|
|
176
|
+
def fail(code, detail):
|
|
177
|
+
issues.append({'code': code, 'detail': detail})
|
|
178
|
+
def result(**extra):
|
|
179
|
+
return {'passed': not issues, 'errors': [x['detail'] for x in issues],
|
|
180
|
+
'errorCodes': sorted({x['code'] for x in issues}), 'issues': issues,
|
|
181
|
+
'quotes': checked, 'contract': 'i3-enterprise-sop-review-output/v1', **extra}
|
|
182
|
+
finals = [x for x in events if x.get('type') == 'result']
|
|
183
|
+
text = finals[-1].get('result', '') if finals else ''
|
|
184
|
+
carrier = scope.get('reviewCarrier', 'text')
|
|
185
|
+
if carrier not in ['text', 'structured_output']:
|
|
186
|
+
fail('review_carrier_invalid', 'unsupported explicitly selected review carrier')
|
|
187
|
+
return result()
|
|
188
|
+
if carrier == 'structured_output':
|
|
189
|
+
if not finals or 'structured_output' not in finals[-1]:
|
|
190
|
+
fail('review_output_missing', 'requested native structured_output missing; no text fallback')
|
|
191
|
+
return result()
|
|
192
|
+
payload = finals[-1]['structured_output']
|
|
193
|
+
calls, successful = {}, set()
|
|
194
|
+
for event in events:
|
|
195
|
+
message = event.get('message', {})
|
|
196
|
+
content = message.get('content', []) if isinstance(message, dict) else []
|
|
197
|
+
if not isinstance(content, list):
|
|
198
|
+
continue
|
|
199
|
+
for block in content:
|
|
200
|
+
if not isinstance(block, dict):
|
|
201
|
+
continue
|
|
202
|
+
if block.get('type') == 'tool_use' and block.get('name') == 'StructuredOutput':
|
|
203
|
+
calls[block.get('id')] = block.get('input')
|
|
204
|
+
elif block.get('type') == 'tool_result' and not block.get('is_error', False):
|
|
205
|
+
successful.add(block.get('tool_use_id'))
|
|
206
|
+
if not any(tool_id in successful and value == payload for tool_id, value in calls.items()):
|
|
207
|
+
fail('review_structured_output_unproven', 'no matching successful native StructuredOutput tool/result pair')
|
|
208
|
+
return result()
|
|
209
|
+
try:
|
|
210
|
+
raw = json.dumps(payload, ensure_ascii=False, allow_nan=False)
|
|
211
|
+
except (ValueError, TypeError) as error:
|
|
212
|
+
fail('review_json_invalid', str(error))
|
|
213
|
+
return result()
|
|
214
|
+
else:
|
|
215
|
+
if not isinstance(text, str) or not text.strip():
|
|
216
|
+
fail('review_output_missing', 'native final report text missing')
|
|
217
|
+
return result()
|
|
218
|
+
blocks = re.findall(r'```json[ \t]*\r?\n(.*?)\r?\n```', text, re.S)
|
|
219
|
+
if len(blocks) > 1:
|
|
220
|
+
fail('review_json_ambiguous', 'expected one JSON object, found multiple JSON blocks')
|
|
221
|
+
return result()
|
|
222
|
+
raw = blocks[0] if blocks else text.strip()
|
|
223
|
+
try:
|
|
224
|
+
report = json.loads(raw, object_pairs_hook=_unique_object, parse_constant=_invalid_constant)
|
|
225
|
+
except (ValueError, TypeError) as error:
|
|
226
|
+
fail('review_json_invalid', str(error))
|
|
227
|
+
return result(parseError={'message': str(error), 'line': getattr(error, 'lineno', None),
|
|
228
|
+
'column': getattr(error, 'colno', None), 'position': getattr(error, 'pos', None)})
|
|
229
|
+
checked_report = check_report(project_root, report, assessment, scope)
|
|
230
|
+
checked_report['carrier'] = carrier
|
|
231
|
+
checked_report['nativeCarrierProven'] = carrier == 'structured_output'
|
|
232
|
+
return checked_report
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def check_report(project_root, report, assessment, scope=None):
|
|
236
|
+
"""Check report content against source/read evidence; no native-origin claim.
|
|
237
|
+
|
|
238
|
+
The caller must record the origin of a separately corrected report. Calling
|
|
239
|
+
this function does not turn that report into a new native model result.
|
|
240
|
+
"""
|
|
241
|
+
root = Path(project_root).resolve()
|
|
242
|
+
scope = scope or {}
|
|
243
|
+
carrier = 'report-object'
|
|
244
|
+
issues, checked = [], []
|
|
245
|
+
def fail(code, detail):
|
|
246
|
+
issues.append({'code': code, 'detail': detail})
|
|
247
|
+
def result(**extra):
|
|
248
|
+
return {'passed': not issues, 'errors': [x['detail'] for x in issues],
|
|
249
|
+
'errorCodes': sorted({x['code'] for x in issues}), 'issues': issues,
|
|
250
|
+
'quotes': checked, 'contract': 'i3-enterprise-sop-review-output/v1', **extra}
|
|
251
|
+
try:
|
|
252
|
+
schema = json.loads(Path(__file__).with_name('review-output.schema.json').read_text())
|
|
253
|
+
schema_errors = _schema_errors(report, schema, schema)
|
|
254
|
+
except (ValueError, KeyError, OSError) as error:
|
|
255
|
+
fail('review_contract_invalid', str(error))
|
|
256
|
+
return result()
|
|
257
|
+
if not schema_errors:
|
|
258
|
+
for group in ['findings', 'checkedFacts']:
|
|
259
|
+
ids = [x['id'] for x in report[group]]
|
|
260
|
+
if len(ids) != len(set(ids)):
|
|
261
|
+
schema_errors.append(group + ': duplicate stable ID')
|
|
262
|
+
if schema_errors:
|
|
263
|
+
for error in schema_errors:
|
|
264
|
+
fail('review_schema_invalid', error)
|
|
265
|
+
return result()
|
|
266
|
+
|
|
267
|
+
def relative(raw_path):
|
|
268
|
+
p = Path(raw_path)
|
|
269
|
+
p = (p if p.is_absolute() else root / p).resolve()
|
|
270
|
+
return str(p.relative_to(root)) if p.is_relative_to(root) else str(p)
|
|
271
|
+
try:
|
|
272
|
+
roles, required_roles = _source_role_bindings(scope)
|
|
273
|
+
except ValueError as error:
|
|
274
|
+
fail('review_role_scope_invalid', str(error))
|
|
275
|
+
return result()
|
|
276
|
+
roles = {name: relative(path) for name, path in roles.items()}
|
|
277
|
+
read_lines = {}
|
|
278
|
+
for item in assessment['observedReads']:
|
|
279
|
+
if item['successful']:
|
|
280
|
+
read_lines.setdefault(item['path'], set()).update(item['lines'])
|
|
281
|
+
proven_roles = set()
|
|
282
|
+
for group in ['findings', 'checkedFacts']:
|
|
283
|
+
for index, item in enumerate(report[group]):
|
|
284
|
+
for side in ['target', 'source']:
|
|
285
|
+
ref = item[side]
|
|
286
|
+
label = f'{group}[{index}].{side}'
|
|
287
|
+
path = relative(ref['path'])
|
|
288
|
+
start, end = ref['startLine'], ref.get('endLine', ref['startLine'])
|
|
289
|
+
bound_role = ref.get('role')
|
|
290
|
+
role_matches = bound_role is None
|
|
291
|
+
if bound_role is not None:
|
|
292
|
+
if bound_role not in roles:
|
|
293
|
+
fail('review_source_role_unbound', label + ': unbound role ' + bound_role)
|
|
294
|
+
elif roles[bound_role] != path:
|
|
295
|
+
fail('review_source_role_mismatch', label + ': role does not match caller-bound path')
|
|
296
|
+
else:
|
|
297
|
+
role_matches = True
|
|
298
|
+
observed = start <= end and all(n in read_lines.get(path, set()) for n in range(start, end + 1))
|
|
299
|
+
quote_matches = False
|
|
300
|
+
if start > end:
|
|
301
|
+
fail('review_reference_range_invalid', label + ': endLine precedes startLine')
|
|
302
|
+
else:
|
|
303
|
+
# Do not open an arbitrary external path merely because the model cites it.
|
|
304
|
+
p = root / path
|
|
305
|
+
explicit_external = {relative(x) for x in scope.get('requiredReads', [])}
|
|
306
|
+
permitted = p.is_relative_to(root) or path in explicit_external
|
|
307
|
+
try:
|
|
308
|
+
lines = p.read_text().splitlines() if permitted and p.is_file() else []
|
|
309
|
+
quote_matches = end <= len(lines) and ref['quote'] in '\n'.join(lines[start-1:end])
|
|
310
|
+
except (OSError, UnicodeError):
|
|
311
|
+
pass
|
|
312
|
+
if not quote_matches:
|
|
313
|
+
fail('review_quote_mismatch', label + ': quote does not match referenced current file/lines')
|
|
314
|
+
if not observed:
|
|
315
|
+
fail('review_reference_not_read', label + ': cited lines lack successful native Read evidence')
|
|
316
|
+
if bound_role and role_matches and observed and quote_matches:
|
|
317
|
+
proven_roles.add(bound_role)
|
|
318
|
+
checked.append({'group': group, 'index': index, 'role': side, 'path': path,
|
|
319
|
+
'sourceRole': bound_role, 'quoteMatches': quote_matches,
|
|
320
|
+
'citedLinesActuallyRead': observed, 'sourceRoleMatches': role_matches})
|
|
321
|
+
for name in required_roles:
|
|
322
|
+
if name not in proven_roles:
|
|
323
|
+
fail('review_source_role_missing', 'required role lacks proven reference: ' + name)
|
|
324
|
+
return result(verdict=report['verdict'], carrier=carrier, sourceRoleEvidence=sorted(proven_roles),
|
|
325
|
+
scope='structure, exact quotes, actual Read ranges and caller-bound roles only; not semantic correctness or approval')
|
|
326
|
+
|
|
327
|
+
def main():
|
|
328
|
+
import argparse,sys
|
|
329
|
+
parser=argparse.ArgumentParser(description=__doc__)
|
|
330
|
+
parser.add_argument('--project-root',required=True)
|
|
331
|
+
parser.add_argument('--scope',required=True,help='Verification scope only; not business handoff.json')
|
|
332
|
+
parser.add_argument('--trace',required=True,help='CodeBuddy stream-json trace')
|
|
333
|
+
parser.add_argument('--review-quotes',action='store_true')
|
|
334
|
+
args=parser.parse_args()
|
|
335
|
+
try:
|
|
336
|
+
scope=json.loads(Path(args.scope).read_text())
|
|
337
|
+
events=[json.loads(line) for line in Path(args.trace).read_text().splitlines() if line.strip()]
|
|
338
|
+
if not isinstance(scope.get('requiredReads'),list):raise ValueError('scope.requiredReads must be a list')
|
|
339
|
+
result=assess(args.project_root,scope,events)
|
|
340
|
+
if args.review_quotes:
|
|
341
|
+
quote_check=check_review(args.project_root,events,result,scope);result['reviewQuoteCheck']=quote_check
|
|
342
|
+
if not quote_check['passed']:result['failures'].extend(quote_check['errorCodes']);result['passed']=False
|
|
343
|
+
except (ValueError,KeyError,TypeError,OSError,UnicodeError) as error:
|
|
344
|
+
result={'format':'i3-read-evidence/v1','passed':False,'failures':['invalid_or_unsupported_evidence'],'detail':str(error)}
|
|
345
|
+
print(json.dumps(result,ensure_ascii=False,indent=2))
|
|
346
|
+
return 0 if result['passed'] else 1
|
|
347
|
+
|
|
348
|
+
if __name__=='__main__':raise SystemExit(main())
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "urn:i3:i3-enterprise-sop:review-output:v1",
|
|
4
|
+
"title": "企业 SOP 只读文档审查会话输出 v1",
|
|
5
|
+
"description": "只约束审查报告结构;不是业务 handoff.json、批准或测试结果。",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": [
|
|
8
|
+
"verdict",
|
|
9
|
+
"findings",
|
|
10
|
+
"checkedFacts",
|
|
11
|
+
"unread"
|
|
12
|
+
],
|
|
13
|
+
"additionalProperties": false,
|
|
14
|
+
"properties": {
|
|
15
|
+
"verdict": {
|
|
16
|
+
"type": "string",
|
|
17
|
+
"enum": [
|
|
18
|
+
"交人工审阅",
|
|
19
|
+
"可交人工审阅",
|
|
20
|
+
"需修订",
|
|
21
|
+
"无法判断"
|
|
22
|
+
]
|
|
23
|
+
},
|
|
24
|
+
"findings": {
|
|
25
|
+
"type": "array",
|
|
26
|
+
"items": {
|
|
27
|
+
"$ref": "#/$defs/finding"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"checkedFacts": {
|
|
31
|
+
"type": "array",
|
|
32
|
+
"minItems": 1,
|
|
33
|
+
"items": {
|
|
34
|
+
"$ref": "#/$defs/fact"
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"unread": {
|
|
38
|
+
"type": "array",
|
|
39
|
+
"items": {
|
|
40
|
+
"type": "string",
|
|
41
|
+
"minLength": 1
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"$defs": {
|
|
46
|
+
"reference": {
|
|
47
|
+
"type": "object",
|
|
48
|
+
"required": [
|
|
49
|
+
"path",
|
|
50
|
+
"startLine",
|
|
51
|
+
"quote"
|
|
52
|
+
],
|
|
53
|
+
"additionalProperties": false,
|
|
54
|
+
"properties": {
|
|
55
|
+
"path": {
|
|
56
|
+
"type": "string",
|
|
57
|
+
"minLength": 1
|
|
58
|
+
},
|
|
59
|
+
"startLine": {
|
|
60
|
+
"type": "integer",
|
|
61
|
+
"minimum": 1
|
|
62
|
+
},
|
|
63
|
+
"endLine": {
|
|
64
|
+
"type": "integer",
|
|
65
|
+
"minimum": 1
|
|
66
|
+
},
|
|
67
|
+
"quote": {
|
|
68
|
+
"type": "string",
|
|
69
|
+
"minLength": 1
|
|
70
|
+
},
|
|
71
|
+
"role": {
|
|
72
|
+
"type": "string",
|
|
73
|
+
"minLength": 1,
|
|
74
|
+
"description": "可选的机器来源标识。实际作业必须由build_review_schema(scope)注入sourceRoles键的枚举;普通资料引用省略role,资料性质写入reason或conclusion。"
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
"finding": {
|
|
79
|
+
"type": "object",
|
|
80
|
+
"required": [
|
|
81
|
+
"id",
|
|
82
|
+
"severity",
|
|
83
|
+
"target",
|
|
84
|
+
"source",
|
|
85
|
+
"reason",
|
|
86
|
+
"suggestion"
|
|
87
|
+
],
|
|
88
|
+
"additionalProperties": false,
|
|
89
|
+
"properties": {
|
|
90
|
+
"id": {
|
|
91
|
+
"type": "string",
|
|
92
|
+
"minLength": 1
|
|
93
|
+
},
|
|
94
|
+
"severity": {
|
|
95
|
+
"type": "string",
|
|
96
|
+
"enum": [
|
|
97
|
+
"low",
|
|
98
|
+
"medium",
|
|
99
|
+
"high",
|
|
100
|
+
"critical"
|
|
101
|
+
]
|
|
102
|
+
},
|
|
103
|
+
"kind": {
|
|
104
|
+
"type": "string",
|
|
105
|
+
"enum": [
|
|
106
|
+
"contradiction",
|
|
107
|
+
"evidence-gap",
|
|
108
|
+
"clarification",
|
|
109
|
+
"optional"
|
|
110
|
+
]
|
|
111
|
+
},
|
|
112
|
+
"target": {
|
|
113
|
+
"$ref": "#/$defs/reference"
|
|
114
|
+
},
|
|
115
|
+
"source": {
|
|
116
|
+
"$ref": "#/$defs/reference"
|
|
117
|
+
},
|
|
118
|
+
"reason": {
|
|
119
|
+
"type": "string",
|
|
120
|
+
"minLength": 1
|
|
121
|
+
},
|
|
122
|
+
"suggestion": {
|
|
123
|
+
"type": "string",
|
|
124
|
+
"minLength": 1
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
"fact": {
|
|
129
|
+
"type": "object",
|
|
130
|
+
"required": [
|
|
131
|
+
"id",
|
|
132
|
+
"target",
|
|
133
|
+
"source",
|
|
134
|
+
"conclusion"
|
|
135
|
+
],
|
|
136
|
+
"additionalProperties": false,
|
|
137
|
+
"properties": {
|
|
138
|
+
"id": {
|
|
139
|
+
"type": "string",
|
|
140
|
+
"minLength": 1
|
|
141
|
+
},
|
|
142
|
+
"target": {
|
|
143
|
+
"$ref": "#/$defs/reference"
|
|
144
|
+
},
|
|
145
|
+
"source": {
|
|
146
|
+
"$ref": "#/$defs/reference"
|
|
147
|
+
},
|
|
148
|
+
"conclusion": {
|
|
149
|
+
"type": "string",
|
|
150
|
+
"minLength": 1
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|