@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.
Files changed (81) hide show
  1. package/.agents/plugins/marketplace.json +1 -1
  2. package/.claude/commands/i3-sop.md +19 -0
  3. package/.claude-plugin/marketplace.json +1 -1
  4. package/.claude-plugin/plugin.json +1 -1
  5. package/.codex-plugin/plugin.json +2 -2
  6. package/.gemini/commands/i3-sop.toml +19 -0
  7. package/README.md +37 -3
  8. package/commands/i3-sop.toml +19 -0
  9. package/docs/codebuddy-setup.md +3 -3
  10. package/docs/codex-setup.md +2 -2
  11. package/docs/other-hosts.md +2 -2
  12. package/docs/workbuddy-setup.md +3 -3
  13. package/evals/cases/i3-enterprise-sop.json +64 -0
  14. package/evals/fixtures/i3-enterprise-sop/brief.md +11 -0
  15. package/evals/zh-CN/cases/i3-enterprise-sop.json +61 -0
  16. package/package.json +7 -1
  17. package/plugin.json +1 -1
  18. package/references/engineering-skeleton-resources/README.md +52 -0
  19. package/references/engineering-skeleton-resources/adoption.md +200 -0
  20. package/references/engineering-skeleton-resources/catalog.json +17 -0
  21. package/references/engineering-skeleton-resources/directory-standard.md +119 -0
  22. package/references/enterprise-sop-resources/README.md +165 -0
  23. package/references/enterprise-sop-resources/ad/README.md +70 -0
  24. package/references/enterprise-sop-resources/ad/assets-manifest.json +106 -0
  25. package/references/enterprise-sop-resources/ad/prompts/01-generate.md +58 -0
  26. package/references/enterprise-sop-resources/ad/prompts/02-review.md +54 -0
  27. package/references/enterprise-sop-resources/ad/prompts/03-revise.md +54 -0
  28. package/references/enterprise-sop-resources/ad/prompts/04-recheck.md +54 -0
  29. package/references/enterprise-sop-resources/ad/templates/AD.md +337 -0
  30. package/references/enterprise-sop-resources/assets-manifest.json +199 -0
  31. package/references/enterprise-sop-resources/brd/README.md +50 -0
  32. package/references/enterprise-sop-resources/brd/assets-manifest.json +94 -0
  33. package/references/enterprise-sop-resources/brd/prompts/01-generate.md +51 -0
  34. package/references/enterprise-sop-resources/brd/prompts/02-review.md +47 -0
  35. package/references/enterprise-sop-resources/brd/prompts/03-revise.md +47 -0
  36. package/references/enterprise-sop-resources/brd/prompts/04-recheck.md +47 -0
  37. package/references/enterprise-sop-resources/brd/templates/BRD.md +169 -0
  38. package/references/enterprise-sop-resources/codebuddy-project-guide.md +34 -0
  39. package/references/enterprise-sop-resources/controlled-handoff.md +41 -0
  40. package/references/enterprise-sop-resources/controlled_handoff.py +334 -0
  41. package/references/enterprise-sop-resources/handoff-contract.md +196 -0
  42. package/references/enterprise-sop-resources/handoff.py +277 -0
  43. package/references/enterprise-sop-resources/lifecycle-contract.md +91 -0
  44. package/references/enterprise-sop-resources/plan-handoff.md +73 -0
  45. package/references/enterprise-sop-resources/prd/README.md +137 -0
  46. package/references/enterprise-sop-resources/prd/assets-manifest.json +115 -0
  47. package/references/enterprise-sop-resources/prd/prompts/01-generate.md +64 -0
  48. package/references/enterprise-sop-resources/prd/prompts/02-review.md +49 -0
  49. package/references/enterprise-sop-resources/prd/prompts/03-revise.md +48 -0
  50. package/references/enterprise-sop-resources/prd/prompts/04-recheck.md +47 -0
  51. package/references/enterprise-sop-resources/prd/template-contracts.md +39 -0
  52. package/references/enterprise-sop-resources/prd/templates/AD.md +17 -0
  53. package/references/enterprise-sop-resources/prd/templates/PRD.md +257 -0
  54. package/references/enterprise-sop-resources/recovery-contract.md +65 -0
  55. package/references/enterprise-sop-resources/review-handoff.md +105 -0
  56. package/references/enterprise-sop-resources/selection-handoff.md +55 -0
  57. package/references/enterprise-sop-resources/selection_handoff.py +225 -0
  58. package/references/enterprise-sop-resources/stages.json +262 -0
  59. package/references/enterprise-sop-resources/technical-baseline/README.md +132 -0
  60. package/references/enterprise-sop-resources/technical-baseline/assets-manifest.json +101 -0
  61. package/references/enterprise-sop-resources/technical-baseline/baselines/PROVENANCE.md +40 -0
  62. 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
  63. package/references/enterprise-sop-resources/technical-baseline/references/engineering-directory-v0.2.0/adoption.md +112 -0
  64. package/references/enterprise-sop-resources/technical-baseline/references/engineering-directory-v0.2.0/directory-standard.md +119 -0
  65. package/references/enterprise-sop-resources/technical-baseline/templates/TECHNICAL-BASELINE.md +133 -0
  66. package/references/enterprise-sop-resources/template-contracts.md +142 -0
  67. package/references/enterprise-sop-resources/testcase/README.md +61 -0
  68. package/references/enterprise-sop-resources/testcase/assets-manifest.json +111 -0
  69. package/references/enterprise-sop-resources/testcase/prompts/01-generate.md +53 -0
  70. package/references/enterprise-sop-resources/testcase/prompts/02-review.md +51 -0
  71. package/references/enterprise-sop-resources/testcase/prompts/03-revise.md +49 -0
  72. package/references/enterprise-sop-resources/testcase/prompts/04-recheck.md +51 -0
  73. package/references/enterprise-sop-resources/testcase/templates/TestCase.md +269 -0
  74. package/references/enterprise-sop-resources/verification/README.md +149 -0
  75. package/references/enterprise-sop-resources/verification/check_execution.py +185 -0
  76. package/references/enterprise-sop-resources/verification/check_native_execution.py +282 -0
  77. package/references/enterprise-sop-resources/verification/check_reads.py +348 -0
  78. package/references/enterprise-sop-resources/verification/review-output.schema.json +155 -0
  79. package/references/enterprise-sop-resources/verification/review_handoff.py +329 -0
  80. package/skills/i3-enterprise-sop/SKILL.md +173 -0
  81. 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
+ }