enterprise-agent-designer 0.34.1
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/.codebuddy-plugin/plugin.json +66 -0
- package/CHANGELOG.md +729 -0
- package/DESIGN_NOTE.md +101 -0
- package/LICENSE +21 -0
- package/PACKAGE.yaml +209 -0
- package/README.md +109 -0
- package/RETROSPECTIVE_v0.1-v0.10.md +67 -0
- package/RUNTIME_ASSEMBLY.md +134 -0
- package/SYSTEM_PROMPT.md +139 -0
- package/agents/agent-designer.md +151 -0
- package/avatars/.gitkeep +0 -0
- package/avatars/expert.png +0 -0
- package/evaluation/README.md +60 -0
- package/evaluation/cases.json +2045 -0
- package/evaluation/document-reviewer-holdout.md +24 -0
- package/package.json +33 -0
- package/references/optional-host-workflow.md +105 -0
- package/scripts/check_agent_delivery.py +202 -0
- package/scripts/optional/workflow_controller.py +478 -0
- package/scripts/validate.py +437 -0
- package/scripts/verify_v0321_guards.py +410 -0
- package/skills/design-enterprise-agent/SKILL.md +131 -0
- package/skills/design-enterprise-agent/references/41-performance-worked-example.md +199 -0
- package/skills/design-enterprise-agent/references/cold-start-and-writing.md +163 -0
- package/skills/design-enterprise-agent/references/requirements-grilling.md +40 -0
- package/skills/design-enterprise-agent/references/runtime-and-integration.md +102 -0
- package/skills/design-enterprise-agent/references/task-adaptive-runtime.md +70 -0
- package/skills/design-enterprise-agent/scripts/finalize_agent_delivery.py +748 -0
- package/skills/grill-with-docs/SKILL.md +58 -0
- package/skills/grill-with-docs/references/design-context-format.md +101 -0
- package/skills/grilling/SKILL.md +62 -0
- package/skills/review-enterprise-agent/SKILL.md +86 -0
- package/skills/review-enterprise-agent/references/isolated-review-contract.md +154 -0
- package/skills/review-enterprise-agent/scripts/validate_review_receipt.py +338 -0
|
@@ -0,0 +1,748 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Validate and package a designed Agent as portable UTF-8 source assets.
|
|
3
|
+
|
|
4
|
+
This tool checks deterministic delivery facts only. It does not score professional
|
|
5
|
+
design, prompt quality, model behavior, platform assembly, or business value.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
import hashlib
|
|
12
|
+
import json
|
|
13
|
+
import re
|
|
14
|
+
import sys
|
|
15
|
+
import zipfile
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from urllib.parse import unquote
|
|
18
|
+
|
|
19
|
+
MARKDOWN_LINK = re.compile(r"(?<!!)\[[^\]]+\]\(([^)]+)\)")
|
|
20
|
+
DECLARED_SKILL = re.compile(r"(?<![\w.-])(skills/[A-Za-z0-9._-]+/SKILL\.md)")
|
|
21
|
+
SKILL_NAME = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*$")
|
|
22
|
+
DYNAMIC_README_STATUS = re.compile(
|
|
23
|
+
r"(?im)^\s*(?:[-*]\s*)?(?:package_status|handoff_status|"
|
|
24
|
+
r"source_review_status|grounding_review_status)\s*[::]"
|
|
25
|
+
)
|
|
26
|
+
DYNAMIC_GATE_STATUS = re.compile(
|
|
27
|
+
r"(?im)^\s*(?:[-*]\s*)?(?:source-gate|grounding-gate)\s*[::]\s*"
|
|
28
|
+
r"(?:pass|通过|失败|未运行|not[-_ ]?run|pending|revision_required)"
|
|
29
|
+
)
|
|
30
|
+
REVIEW_RECEIPT_FIELDS = {
|
|
31
|
+
"review_mode",
|
|
32
|
+
"review_scope",
|
|
33
|
+
"review_round",
|
|
34
|
+
"verdict",
|
|
35
|
+
"lowest_failed_layer",
|
|
36
|
+
"decisive_reason",
|
|
37
|
+
"evidence",
|
|
38
|
+
"preserve",
|
|
39
|
+
"return_to",
|
|
40
|
+
"next_action",
|
|
41
|
+
"change_condition",
|
|
42
|
+
"reviewed_snapshot_id",
|
|
43
|
+
"grounding_audit",
|
|
44
|
+
"source_audit",
|
|
45
|
+
"materiality_audit",
|
|
46
|
+
"authority_audit",
|
|
47
|
+
}
|
|
48
|
+
SOURCE_AUDIT_FIELDS = {
|
|
49
|
+
"normal_case_replay",
|
|
50
|
+
"highest_risk_case_replay",
|
|
51
|
+
"finding_classification_consistent",
|
|
52
|
+
"unsupported_scope_expansion_found",
|
|
53
|
+
"skill_dependency_closure",
|
|
54
|
+
}
|
|
55
|
+
MATERIALITY_AUDIT_FIELDS = {
|
|
56
|
+
"overall_goal",
|
|
57
|
+
"consumer_decision",
|
|
58
|
+
"first_release_boundary",
|
|
59
|
+
"material_blocker_found",
|
|
60
|
+
"material_effect",
|
|
61
|
+
"same_failure_family_scope",
|
|
62
|
+
"deferred_non_blocking",
|
|
63
|
+
"late_round_blocking_justification",
|
|
64
|
+
}
|
|
65
|
+
LATE_ROUND_BLOCKING_JUSTIFICATIONS = {
|
|
66
|
+
"not_applicable",
|
|
67
|
+
"new-independent-failure-class",
|
|
68
|
+
"material-same-family-missed-instance",
|
|
69
|
+
"patch-introduced-failure",
|
|
70
|
+
"previously-unobservable-failure",
|
|
71
|
+
}
|
|
72
|
+
LOWEST_FAILED_LAYERS = {
|
|
73
|
+
"requirements",
|
|
74
|
+
"professional-task",
|
|
75
|
+
"role",
|
|
76
|
+
"principles",
|
|
77
|
+
"skill-topology",
|
|
78
|
+
"tool-knowledge",
|
|
79
|
+
"output-handoff",
|
|
80
|
+
"trace-runtime",
|
|
81
|
+
"none",
|
|
82
|
+
}
|
|
83
|
+
RETURN_TARGETS = {
|
|
84
|
+
"grilling",
|
|
85
|
+
"grill-with-docs",
|
|
86
|
+
"design-enterprise-agent",
|
|
87
|
+
"delivery-finalizer",
|
|
88
|
+
"user",
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def parse_args() -> argparse.Namespace:
|
|
93
|
+
parser = argparse.ArgumentParser(
|
|
94
|
+
description="Validate source assets and optionally create a portable UTF-8 ZIP."
|
|
95
|
+
)
|
|
96
|
+
parser.add_argument("target_root", type=Path, help="Root directory of the delivered Agent")
|
|
97
|
+
parser.add_argument(
|
|
98
|
+
"--entry", default="SYSTEM_PROMPT.md", help="Entrypoint relative to target_root"
|
|
99
|
+
)
|
|
100
|
+
parser.add_argument(
|
|
101
|
+
"--input-material",
|
|
102
|
+
action="append",
|
|
103
|
+
default=[],
|
|
104
|
+
type=Path,
|
|
105
|
+
help="Input artifact path; repeat to ensure it was not reused as the output entry",
|
|
106
|
+
)
|
|
107
|
+
parser.add_argument(
|
|
108
|
+
"--output-zip",
|
|
109
|
+
type=Path,
|
|
110
|
+
help="Create this ZIP after source validation, then reopen and verify it",
|
|
111
|
+
)
|
|
112
|
+
parser.add_argument(
|
|
113
|
+
"--prepare-source-review",
|
|
114
|
+
type=Path,
|
|
115
|
+
metavar="MANIFEST",
|
|
116
|
+
help="Validate and freeze the current source into an external review manifest",
|
|
117
|
+
)
|
|
118
|
+
parser.add_argument(
|
|
119
|
+
"--source-review-manifest",
|
|
120
|
+
type=Path,
|
|
121
|
+
help="Frozen manifest produced by --prepare-source-review",
|
|
122
|
+
)
|
|
123
|
+
parser.add_argument(
|
|
124
|
+
"--source-review-receipt",
|
|
125
|
+
type=Path,
|
|
126
|
+
help="External JSON receipt returned by an isolated source-gate reviewer",
|
|
127
|
+
)
|
|
128
|
+
parser.add_argument(
|
|
129
|
+
"--delivery-receipt",
|
|
130
|
+
type=Path,
|
|
131
|
+
help="Write the deterministic packaging receipt to this external JSON path",
|
|
132
|
+
)
|
|
133
|
+
parser.add_argument(
|
|
134
|
+
"--require-evaluation",
|
|
135
|
+
action="store_true",
|
|
136
|
+
help="Require actual evaluation artifacts in a conventional or declared path",
|
|
137
|
+
)
|
|
138
|
+
parser.add_argument(
|
|
139
|
+
"--evaluation-path",
|
|
140
|
+
action="append",
|
|
141
|
+
default=[],
|
|
142
|
+
type=Path,
|
|
143
|
+
help="Evaluation artifact or directory relative to target_root; repeat for custom layouts",
|
|
144
|
+
)
|
|
145
|
+
parser.add_argument(
|
|
146
|
+
"--forbid-evaluation-term",
|
|
147
|
+
action="append",
|
|
148
|
+
default=[],
|
|
149
|
+
help="Literal term explicitly forbidden by the user or authoritative brief in runtime evaluation assets; repeat as needed",
|
|
150
|
+
)
|
|
151
|
+
parser.add_argument(
|
|
152
|
+
"--require-path",
|
|
153
|
+
action="append",
|
|
154
|
+
default=[],
|
|
155
|
+
type=Path,
|
|
156
|
+
help="Required source file relative to target_root; repeat for committed Skills or contracts",
|
|
157
|
+
)
|
|
158
|
+
return parser.parse_args()
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def is_within(path: Path, root: Path) -> bool:
|
|
162
|
+
return path == root or root in path.parents
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def local_target(raw: str) -> str | None:
|
|
166
|
+
target = raw.strip().strip("<>")
|
|
167
|
+
if target.startswith(("http://", "https://", "mailto:", "#")):
|
|
168
|
+
return None
|
|
169
|
+
target = unquote(target.split("#", 1)[0])
|
|
170
|
+
return target or None
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def read_utf8(path: Path, root: Path, errors: list[str]) -> str | None:
|
|
174
|
+
try:
|
|
175
|
+
return path.read_text(encoding="utf-8")
|
|
176
|
+
except UnicodeDecodeError as exc:
|
|
177
|
+
errors.append(f"文件不是有效 UTF-8:{path.relative_to(root)} ({exc})")
|
|
178
|
+
return None
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def parse_frontmatter(text: str) -> dict[str, str] | None:
|
|
182
|
+
lines = text.splitlines()
|
|
183
|
+
if not lines or lines[0].strip() != "---":
|
|
184
|
+
return None
|
|
185
|
+
try:
|
|
186
|
+
end = next(index for index, line in enumerate(lines[1:], start=1) if line.strip() == "---")
|
|
187
|
+
except StopIteration:
|
|
188
|
+
return None
|
|
189
|
+
|
|
190
|
+
values: dict[str, str] = {}
|
|
191
|
+
for line in lines[1:end]:
|
|
192
|
+
if not line.strip() or line.lstrip().startswith("#") or ":" not in line:
|
|
193
|
+
continue
|
|
194
|
+
key, value = line.split(":", 1)
|
|
195
|
+
values[key.strip()] = value.strip().strip('"\'')
|
|
196
|
+
return values
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def validate_skill(path: Path, root: Path, text: str, errors: list[str]) -> None:
|
|
200
|
+
relative = path.relative_to(root)
|
|
201
|
+
folder_name = path.parent.name
|
|
202
|
+
metadata = parse_frontmatter(text)
|
|
203
|
+
if metadata is None:
|
|
204
|
+
errors.append(f"Skill 缺少完整 YAML frontmatter:{relative}")
|
|
205
|
+
return
|
|
206
|
+
|
|
207
|
+
name = metadata.get("name", "")
|
|
208
|
+
description = metadata.get("description", "")
|
|
209
|
+
if not name:
|
|
210
|
+
errors.append(f"Skill frontmatter 缺少 name:{relative}")
|
|
211
|
+
elif not SKILL_NAME.fullmatch(name):
|
|
212
|
+
errors.append(f"Skill name 必须使用 hyphen-case:{relative} -> {name}")
|
|
213
|
+
elif folder_name != name:
|
|
214
|
+
errors.append(f"Skill 文件夹名必须与 name 一致:{relative} -> folder={folder_name}, name={name}")
|
|
215
|
+
if not description:
|
|
216
|
+
errors.append(f"Skill frontmatter 缺少 description:{relative}")
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def discover_evaluations(root: Path, declared: list[Path], errors: list[str]) -> list[Path]:
|
|
220
|
+
files: list[Path] = []
|
|
221
|
+
if declared:
|
|
222
|
+
candidates = [(root / path).resolve() for path in declared]
|
|
223
|
+
else:
|
|
224
|
+
candidates = [
|
|
225
|
+
root / dirname
|
|
226
|
+
for dirname in ("evaluation", "evaluations", "eval", "evals", "tests", "fixtures")
|
|
227
|
+
if (root / dirname).exists()
|
|
228
|
+
]
|
|
229
|
+
|
|
230
|
+
for path in candidates:
|
|
231
|
+
if root not in path.parents and path != root:
|
|
232
|
+
errors.append(f"评测路径越出源码根目录:{path}")
|
|
233
|
+
elif path.is_file() and path.stat().st_size > 0:
|
|
234
|
+
files.append(path)
|
|
235
|
+
elif path.is_dir():
|
|
236
|
+
files.extend(item for item in path.rglob("*") if item.is_file() and item.stat().st_size > 0)
|
|
237
|
+
else:
|
|
238
|
+
errors.append(f"评测资产不存在或为空:{path}")
|
|
239
|
+
return sorted(set(files))
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def count_evaluation_cases(files: list[Path], root: Path, errors: list[str]) -> int:
|
|
243
|
+
"""Count executable case artifacts without treating README/holdout as tests."""
|
|
244
|
+
count = 0
|
|
245
|
+
for path in files:
|
|
246
|
+
if path.name.lower() == "readme.md":
|
|
247
|
+
continue
|
|
248
|
+
if path.suffix.lower() == ".md":
|
|
249
|
+
text = read_utf8(path, root, errors)
|
|
250
|
+
if text is not None and "NOT_FOR_RUNTIME_REFERENCE" not in text:
|
|
251
|
+
count += 1
|
|
252
|
+
continue
|
|
253
|
+
if path.suffix.lower() == ".json":
|
|
254
|
+
text = read_utf8(path, root, errors)
|
|
255
|
+
if text is None:
|
|
256
|
+
continue
|
|
257
|
+
try:
|
|
258
|
+
payload = json.loads(text)
|
|
259
|
+
except json.JSONDecodeError as exc:
|
|
260
|
+
errors.append(f"评测 JSON 无法解析:{path.relative_to(root)} ({exc})")
|
|
261
|
+
continue
|
|
262
|
+
if isinstance(payload, dict) and isinstance(payload.get("cases"), list):
|
|
263
|
+
count += len(payload["cases"])
|
|
264
|
+
elif isinstance(payload, dict) and (
|
|
265
|
+
"business_claims" in payload
|
|
266
|
+
or "load_bearing_facts" in payload
|
|
267
|
+
or payload.get("registry_scope") == "current_task"
|
|
268
|
+
):
|
|
269
|
+
# Non-behavior support fixtures are not target-model cases.
|
|
270
|
+
continue
|
|
271
|
+
elif isinstance(payload, (dict, list)) and payload:
|
|
272
|
+
count += 1
|
|
273
|
+
continue
|
|
274
|
+
count += 1
|
|
275
|
+
return count
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def validate_forbidden_evaluation_terms(
|
|
279
|
+
files: list[Path], root: Path, terms: list[str], errors: list[str]
|
|
280
|
+
) -> None:
|
|
281
|
+
for path in files:
|
|
282
|
+
if path.name.lower() == "readme.md":
|
|
283
|
+
continue
|
|
284
|
+
text = read_utf8(path, root, errors)
|
|
285
|
+
if text is None or "NOT_FOR_RUNTIME_REFERENCE" in text:
|
|
286
|
+
continue
|
|
287
|
+
for term in terms:
|
|
288
|
+
if term in text:
|
|
289
|
+
errors.append(
|
|
290
|
+
f"评测资产包含明确禁区词:{path.relative_to(root)} -> {term}"
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def normalize_forbidden_evaluation_terms(terms: list[str]) -> list[str]:
|
|
295
|
+
"""Return the single canonical term list used by scanning and snapshot identity."""
|
|
296
|
+
return sorted({term.strip() for term in terms if term and term.strip()})
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def source_files(root: Path, output_zip: Path | None) -> list[Path]:
|
|
300
|
+
excluded = {"Thumbs.db", ".DS_Store"}
|
|
301
|
+
result: list[Path] = []
|
|
302
|
+
resolved_zip = output_zip.resolve() if output_zip else None
|
|
303
|
+
for path in root.rglob("*"):
|
|
304
|
+
if not path.is_file() or path.name in excluded or "__pycache__" in path.parts:
|
|
305
|
+
continue
|
|
306
|
+
if resolved_zip and path.resolve() == resolved_zip:
|
|
307
|
+
continue
|
|
308
|
+
result.append(path)
|
|
309
|
+
return sorted(result)
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def create_zip(root: Path, output_zip: Path, files: list[Path]) -> None:
|
|
313
|
+
output_zip.parent.mkdir(parents=True, exist_ok=True)
|
|
314
|
+
with zipfile.ZipFile(output_zip, "w", compression=zipfile.ZIP_DEFLATED) as archive:
|
|
315
|
+
for path in files:
|
|
316
|
+
relative = path.relative_to(root).as_posix()
|
|
317
|
+
archive.write(path, arcname=f"{root.name}/{relative}")
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def validate_zip(
|
|
321
|
+
root: Path,
|
|
322
|
+
output_zip: Path,
|
|
323
|
+
files: list[Path],
|
|
324
|
+
entry: str,
|
|
325
|
+
errors: list[str],
|
|
326
|
+
) -> None:
|
|
327
|
+
if not output_zip.is_file():
|
|
328
|
+
errors.append(f"ZIP 未生成:{output_zip}")
|
|
329
|
+
return
|
|
330
|
+
|
|
331
|
+
expected = {path.relative_to(root).as_posix() for path in files}
|
|
332
|
+
prefix = f"{root.name}/"
|
|
333
|
+
try:
|
|
334
|
+
with zipfile.ZipFile(output_zip) as archive:
|
|
335
|
+
corrupt = archive.testzip()
|
|
336
|
+
if corrupt:
|
|
337
|
+
errors.append(f"ZIP 条目损坏:{corrupt}")
|
|
338
|
+
items = [item for item in archive.infolist() if not item.is_dir()]
|
|
339
|
+
actual: set[str] = set()
|
|
340
|
+
for item in items:
|
|
341
|
+
normalized = item.filename.replace("\\", "/")
|
|
342
|
+
if any(ord(char) > 127 for char in normalized) and not (item.flag_bits & 0x800):
|
|
343
|
+
errors.append(f"ZIP 非 ASCII 路径未标记 UTF-8:{normalized}")
|
|
344
|
+
if not normalized.startswith(prefix):
|
|
345
|
+
errors.append(f"ZIP 条目不在唯一源码根目录下:{normalized}")
|
|
346
|
+
continue
|
|
347
|
+
relative = normalized[len(prefix) :]
|
|
348
|
+
actual.add(relative)
|
|
349
|
+
source = root / relative
|
|
350
|
+
if source.is_file() and archive.read(item) != source.read_bytes():
|
|
351
|
+
errors.append(f"ZIP 内容与源码不一致:{relative}")
|
|
352
|
+
if expected != actual:
|
|
353
|
+
missing = sorted(expected - actual)
|
|
354
|
+
extra = sorted(actual - expected)
|
|
355
|
+
if missing:
|
|
356
|
+
errors.append(f"ZIP 缺少源码文件:{missing}")
|
|
357
|
+
if extra:
|
|
358
|
+
errors.append(f"ZIP 存在源码目录外的额外文件:{extra}")
|
|
359
|
+
if entry not in actual:
|
|
360
|
+
errors.append(f"ZIP 中找不到入口:{entry}")
|
|
361
|
+
except zipfile.BadZipFile as exc:
|
|
362
|
+
errors.append(f"ZIP 无法读取:{exc}")
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def validate_readme_status(root: Path, texts: dict[Path, str], errors: list[str]) -> None:
|
|
366
|
+
readme = root / "README.md"
|
|
367
|
+
text = texts.get(readme)
|
|
368
|
+
if text is None:
|
|
369
|
+
return
|
|
370
|
+
if DYNAMIC_README_STATUS.search(text) or DYNAMIC_GATE_STATUS.search(text):
|
|
371
|
+
errors.append(
|
|
372
|
+
"README 不得保存会在评审/打包后失真的动态门禁或交付状态;"
|
|
373
|
+
"这些状态只写入外部复审/交付收据"
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def build_manifest(
|
|
378
|
+
root: Path,
|
|
379
|
+
entry: str,
|
|
380
|
+
files: list[Path],
|
|
381
|
+
evaluation_count: int,
|
|
382
|
+
forbidden_evaluation_terms: list[str],
|
|
383
|
+
) -> dict[str, object]:
|
|
384
|
+
records: list[dict[str, object]] = []
|
|
385
|
+
snapshot = hashlib.sha256()
|
|
386
|
+
for path in files:
|
|
387
|
+
relative = path.relative_to(root).as_posix()
|
|
388
|
+
content = path.read_bytes()
|
|
389
|
+
digest = hashlib.sha256(content).hexdigest()
|
|
390
|
+
records.append({"path": relative, "bytes": len(content), "sha256": digest})
|
|
391
|
+
snapshot.update(relative.encode("utf-8"))
|
|
392
|
+
snapshot.update(b"\0")
|
|
393
|
+
snapshot.update(digest.encode("ascii"))
|
|
394
|
+
snapshot.update(b"\0")
|
|
395
|
+
snapshot.update(b"forbidden_evaluation_terms\0")
|
|
396
|
+
for term in forbidden_evaluation_terms:
|
|
397
|
+
snapshot.update(term.encode("utf-8"))
|
|
398
|
+
snapshot.update(b"\0")
|
|
399
|
+
return {
|
|
400
|
+
"manifest_version": "1.2",
|
|
401
|
+
"target_root": str(root),
|
|
402
|
+
"entry": entry,
|
|
403
|
+
"source_snapshot_id": f"sha256:{snapshot.hexdigest()}",
|
|
404
|
+
"evaluation_count": evaluation_count,
|
|
405
|
+
"forbidden_evaluation_terms": forbidden_evaluation_terms,
|
|
406
|
+
"files": records,
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def write_json(path: Path, payload: dict[str, object]) -> None:
|
|
411
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
412
|
+
path.write_text(
|
|
413
|
+
json.dumps(payload, ensure_ascii=False, indent=2) + "\n",
|
|
414
|
+
encoding="utf-8",
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def read_json(path: Path, label: str, errors: list[str]) -> dict[str, object] | None:
|
|
419
|
+
if not path.is_file():
|
|
420
|
+
errors.append(f"{label}不存在:{path}")
|
|
421
|
+
return None
|
|
422
|
+
try:
|
|
423
|
+
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
424
|
+
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
|
|
425
|
+
errors.append(f"{label}不是有效 UTF-8 JSON:{path} ({exc})")
|
|
426
|
+
return None
|
|
427
|
+
if not isinstance(payload, dict):
|
|
428
|
+
errors.append(f"{label}必须是 JSON 对象:{path}")
|
|
429
|
+
return None
|
|
430
|
+
return payload
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
def validate_review_chain(
|
|
434
|
+
root: Path,
|
|
435
|
+
current_manifest: dict[str, object],
|
|
436
|
+
manifest_path: Path,
|
|
437
|
+
receipt_path: Path,
|
|
438
|
+
errors: list[str],
|
|
439
|
+
) -> None:
|
|
440
|
+
if is_within(manifest_path, root) or is_within(receipt_path, root):
|
|
441
|
+
errors.append("送审清单与隔离复审收据必须位于目标源码根目录之外")
|
|
442
|
+
return
|
|
443
|
+
|
|
444
|
+
frozen = read_json(manifest_path, "送审清单", errors)
|
|
445
|
+
receipt = read_json(receipt_path, "隔离 source-gate 收据", errors)
|
|
446
|
+
if frozen is None or receipt is None:
|
|
447
|
+
return
|
|
448
|
+
|
|
449
|
+
if frozen.get("manifest_version") != "1.2":
|
|
450
|
+
errors.append("送审清单版本不受支持")
|
|
451
|
+
if frozen.get("target_root") != str(root):
|
|
452
|
+
errors.append("送审清单绑定的源码根目录与当前目标不一致")
|
|
453
|
+
if frozen.get("source_snapshot_id") != current_manifest.get("source_snapshot_id"):
|
|
454
|
+
errors.append("源码在 source-gate 送审后发生变化;旧复审收据失效")
|
|
455
|
+
if frozen.get("files") != current_manifest.get("files"):
|
|
456
|
+
errors.append("当前源码文件集合或内容与送审清单不一致")
|
|
457
|
+
if frozen.get("forbidden_evaluation_terms") != current_manifest.get(
|
|
458
|
+
"forbidden_evaluation_terms"
|
|
459
|
+
):
|
|
460
|
+
errors.append("最终打包使用的评测禁区与冻结送审清单不一致")
|
|
461
|
+
|
|
462
|
+
missing = REVIEW_RECEIPT_FIELDS - set(receipt)
|
|
463
|
+
if missing:
|
|
464
|
+
errors.append(f"隔离 source-gate 收据缺少字段:{sorted(missing)}")
|
|
465
|
+
return
|
|
466
|
+
if receipt.get("review_mode") != "source-gate":
|
|
467
|
+
errors.append("复审收据 review_mode 不是 source-gate")
|
|
468
|
+
if receipt.get("review_scope") != "isolated-subagent":
|
|
469
|
+
errors.append("复审收据不是隔离子智能体范围")
|
|
470
|
+
review_round = receipt.get("review_round")
|
|
471
|
+
if not isinstance(review_round, int) or isinstance(review_round, bool) or review_round < 1:
|
|
472
|
+
errors.append("复审收据 review_round 必须是从 1 开始的正整数")
|
|
473
|
+
if receipt.get("verdict") != "pass":
|
|
474
|
+
errors.append("source-gate 未通过,禁止打包")
|
|
475
|
+
if receipt.get("lowest_failed_layer") not in LOWEST_FAILED_LAYERS:
|
|
476
|
+
errors.append("复审收据 lowest_failed_layer 不在允许枚举中")
|
|
477
|
+
elif receipt.get("verdict") == "pass" and receipt.get("lowest_failed_layer") != "none":
|
|
478
|
+
errors.append("source-gate pass 的 lowest_failed_layer 必须为 none")
|
|
479
|
+
if receipt.get("return_to") not in RETURN_TARGETS:
|
|
480
|
+
errors.append("复审收据 return_to 不在允许枚举中")
|
|
481
|
+
elif receipt.get("verdict") == "pass" and receipt.get("return_to") != "delivery-finalizer":
|
|
482
|
+
errors.append("source-gate pass 必须返回 delivery-finalizer")
|
|
483
|
+
if receipt.get("reviewed_snapshot_id") != frozen.get("source_snapshot_id"):
|
|
484
|
+
errors.append("复审收据没有绑定本次冻结源码快照")
|
|
485
|
+
|
|
486
|
+
authority = receipt.get("authority_audit")
|
|
487
|
+
if not isinstance(authority, dict):
|
|
488
|
+
errors.append("复审收据 authority_audit 必须是对象")
|
|
489
|
+
else:
|
|
490
|
+
unresolved = authority.get("unresolved_or_unauthorized")
|
|
491
|
+
reviewed = authority.get("stable_rules_reviewed")
|
|
492
|
+
if not isinstance(unresolved, list):
|
|
493
|
+
errors.append("authority_audit.unresolved_or_unauthorized 必须是数组")
|
|
494
|
+
elif unresolved:
|
|
495
|
+
errors.append("source-gate 仍发现无权或未解决的稳定规则,禁止打包")
|
|
496
|
+
if not isinstance(reviewed, list):
|
|
497
|
+
errors.append("authority_audit.stable_rules_reviewed 必须是数组")
|
|
498
|
+
|
|
499
|
+
source_audit = receipt.get("source_audit")
|
|
500
|
+
if not isinstance(source_audit, dict):
|
|
501
|
+
errors.append("source-gate pass 收据缺少 source_audit")
|
|
502
|
+
else:
|
|
503
|
+
missing_source_audit = SOURCE_AUDIT_FIELDS - set(source_audit)
|
|
504
|
+
if missing_source_audit:
|
|
505
|
+
errors.append(f"source_audit 缺少字段:{sorted(missing_source_audit)}")
|
|
506
|
+
if source_audit.get("normal_case_replay") != "pass":
|
|
507
|
+
errors.append("source-gate 未确认正常案例回放通过")
|
|
508
|
+
if source_audit.get("highest_risk_case_replay") != "pass":
|
|
509
|
+
errors.append("source-gate 未确认最高风险案例回放通过")
|
|
510
|
+
if source_audit.get("finding_classification_consistent") is not True:
|
|
511
|
+
errors.append("source-gate 未确认评测发现分类一致")
|
|
512
|
+
if source_audit.get("unsupported_scope_expansion_found") is not False:
|
|
513
|
+
errors.append("source-gate 仍发现或未排除未经依据的范围扩张")
|
|
514
|
+
if source_audit.get("skill_dependency_closure") != "pass":
|
|
515
|
+
errors.append("source-gate 未确认 Skill 数据依赖闭合")
|
|
516
|
+
materiality = receipt.get("materiality_audit")
|
|
517
|
+
if not isinstance(materiality, dict):
|
|
518
|
+
errors.append("source-gate pass 收据缺少 materiality_audit")
|
|
519
|
+
else:
|
|
520
|
+
missing_materiality = MATERIALITY_AUDIT_FIELDS - set(materiality)
|
|
521
|
+
if missing_materiality:
|
|
522
|
+
errors.append(f"materiality_audit 缺少字段:{sorted(missing_materiality)}")
|
|
523
|
+
if materiality.get("material_blocker_found") is not False:
|
|
524
|
+
errors.append("source-gate pass 仍声明存在 material blocker")
|
|
525
|
+
for name in (
|
|
526
|
+
"overall_goal",
|
|
527
|
+
"consumer_decision",
|
|
528
|
+
"first_release_boundary",
|
|
529
|
+
"material_effect",
|
|
530
|
+
):
|
|
531
|
+
if not isinstance(materiality.get(name), str) or not str(
|
|
532
|
+
materiality.get(name)
|
|
533
|
+
).strip():
|
|
534
|
+
errors.append(f"materiality_audit.{name} 必须是非空字符串")
|
|
535
|
+
for name in ("same_failure_family_scope", "deferred_non_blocking"):
|
|
536
|
+
if not isinstance(materiality.get(name), list):
|
|
537
|
+
errors.append(f"materiality_audit.{name} 必须是数组")
|
|
538
|
+
if materiality.get("late_round_blocking_justification") not in LATE_ROUND_BLOCKING_JUSTIFICATIONS:
|
|
539
|
+
errors.append(
|
|
540
|
+
"materiality_audit.late_round_blocking_justification 不在允许枚举中"
|
|
541
|
+
)
|
|
542
|
+
elif materiality.get("late_round_blocking_justification") != "not_applicable":
|
|
543
|
+
errors.append(
|
|
544
|
+
"source-gate pass 的 late_round_blocking_justification 必须为 not_applicable"
|
|
545
|
+
)
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
def main() -> int:
|
|
549
|
+
args = parse_args()
|
|
550
|
+
root = args.target_root.resolve()
|
|
551
|
+
errors: list[str] = []
|
|
552
|
+
if args.prepare_source_review and args.output_zip:
|
|
553
|
+
errors.append("--prepare-source-review 与 --output-zip 不能在同一次调用中使用")
|
|
554
|
+
if args.output_zip and not (args.source_review_manifest and args.source_review_receipt):
|
|
555
|
+
errors.append("生成最终 ZIP 必须同时提供冻结送审清单和隔离 source-gate 收据")
|
|
556
|
+
if args.delivery_receipt and not args.output_zip:
|
|
557
|
+
errors.append("--delivery-receipt 只可与 --output-zip 一起使用")
|
|
558
|
+
if not root.is_dir():
|
|
559
|
+
return report([f"目标源码根目录不存在:{root}"])
|
|
560
|
+
|
|
561
|
+
output_zip = args.output_zip.resolve() if args.output_zip else None
|
|
562
|
+
entry = (root / args.entry).resolve()
|
|
563
|
+
if not entry.is_file() or root not in entry.parents:
|
|
564
|
+
errors.append(f"独立入口不存在或越出源码根目录:{args.entry}")
|
|
565
|
+
|
|
566
|
+
inputs = {path.resolve() for path in args.input_material}
|
|
567
|
+
if entry in inputs or root in inputs:
|
|
568
|
+
errors.append("目标入口/源码根目录与输入材料是同一对象")
|
|
569
|
+
|
|
570
|
+
markdown_files = sorted(root.rglob("*.md"))
|
|
571
|
+
if not markdown_files:
|
|
572
|
+
errors.append("目标目录没有 Markdown 源码")
|
|
573
|
+
|
|
574
|
+
texts: dict[Path, str] = {}
|
|
575
|
+
for path in markdown_files:
|
|
576
|
+
text = read_utf8(path, root, errors)
|
|
577
|
+
if text is not None:
|
|
578
|
+
texts[path] = text
|
|
579
|
+
|
|
580
|
+
for path, text in texts.items():
|
|
581
|
+
for raw in MARKDOWN_LINK.findall(text):
|
|
582
|
+
target = local_target(raw)
|
|
583
|
+
if target is None:
|
|
584
|
+
continue
|
|
585
|
+
resolved = (path.parent / target).resolve()
|
|
586
|
+
if root not in resolved.parents and resolved != root:
|
|
587
|
+
errors.append(f"相对引用越出源码根目录:{path.relative_to(root)} -> {target}")
|
|
588
|
+
elif not resolved.exists():
|
|
589
|
+
errors.append(f"相对引用不存在:{path.relative_to(root)} -> {target}")
|
|
590
|
+
|
|
591
|
+
for declared in DECLARED_SKILL.findall(text.replace("\\", "/")):
|
|
592
|
+
skill_path = (root / declared).resolve()
|
|
593
|
+
if not skill_path.is_file():
|
|
594
|
+
errors.append(f"声明的 Skill 不存在:{path.relative_to(root)} -> {declared}")
|
|
595
|
+
|
|
596
|
+
validate_readme_status(root, texts, errors)
|
|
597
|
+
|
|
598
|
+
skill_files = sorted(root.glob("skills/*/SKILL.md"))
|
|
599
|
+
if (root / "skills").is_dir() and not skill_files:
|
|
600
|
+
errors.append("skills 目录存在但没有可装载的 skills/<name>/SKILL.md")
|
|
601
|
+
for path in skill_files:
|
|
602
|
+
text = texts.get(path)
|
|
603
|
+
if text is not None:
|
|
604
|
+
validate_skill(path, root, text, errors)
|
|
605
|
+
|
|
606
|
+
for relative in args.require_path:
|
|
607
|
+
path = (root / relative).resolve()
|
|
608
|
+
if root not in path.parents:
|
|
609
|
+
errors.append(f"必需资产越出源码根目录:{relative}")
|
|
610
|
+
elif not path.is_file() or path.stat().st_size == 0:
|
|
611
|
+
errors.append(f"必需资产不存在或为空:{relative}")
|
|
612
|
+
|
|
613
|
+
forbidden_evaluation_terms = normalize_forbidden_evaluation_terms(
|
|
614
|
+
args.forbid_evaluation_term
|
|
615
|
+
)
|
|
616
|
+
evaluation_count = 0
|
|
617
|
+
if args.require_evaluation:
|
|
618
|
+
evaluation_files = discover_evaluations(root, args.evaluation_path, errors)
|
|
619
|
+
evaluation_count = count_evaluation_cases(evaluation_files, root, errors)
|
|
620
|
+
validate_forbidden_evaluation_terms(
|
|
621
|
+
evaluation_files, root, forbidden_evaluation_terms, errors
|
|
622
|
+
)
|
|
623
|
+
if evaluation_count < 2:
|
|
624
|
+
errors.append("完整 Agent 至少需要一个正常任务和一个最高风险压力事件评测资产")
|
|
625
|
+
|
|
626
|
+
files = source_files(root, output_zip)
|
|
627
|
+
if errors:
|
|
628
|
+
return report(errors)
|
|
629
|
+
|
|
630
|
+
current_manifest = build_manifest(
|
|
631
|
+
root,
|
|
632
|
+
args.entry,
|
|
633
|
+
files,
|
|
634
|
+
evaluation_count,
|
|
635
|
+
forbidden_evaluation_terms,
|
|
636
|
+
)
|
|
637
|
+
|
|
638
|
+
if args.prepare_source_review:
|
|
639
|
+
manifest_path = args.prepare_source_review.resolve()
|
|
640
|
+
if is_within(manifest_path, root):
|
|
641
|
+
return report(["送审清单必须写在目标源码根目录之外"])
|
|
642
|
+
write_json(manifest_path, current_manifest)
|
|
643
|
+
return report(
|
|
644
|
+
[], root=root, file_count=len(files), skill_count=len(skill_files),
|
|
645
|
+
evaluation_count=evaluation_count, output_zip=None,
|
|
646
|
+
prepared_manifest=manifest_path,
|
|
647
|
+
snapshot_id=str(current_manifest["source_snapshot_id"]),
|
|
648
|
+
)
|
|
649
|
+
|
|
650
|
+
if output_zip:
|
|
651
|
+
validate_review_chain(
|
|
652
|
+
root,
|
|
653
|
+
current_manifest,
|
|
654
|
+
args.source_review_manifest.resolve(),
|
|
655
|
+
args.source_review_receipt.resolve(),
|
|
656
|
+
errors,
|
|
657
|
+
)
|
|
658
|
+
if errors:
|
|
659
|
+
return report(errors)
|
|
660
|
+
create_zip(root, output_zip, files)
|
|
661
|
+
validate_zip(root, output_zip, files, args.entry, errors)
|
|
662
|
+
if errors:
|
|
663
|
+
return report(errors)
|
|
664
|
+
delivery_receipt = args.delivery_receipt.resolve() if args.delivery_receipt else None
|
|
665
|
+
if delivery_receipt:
|
|
666
|
+
if is_within(delivery_receipt, root):
|
|
667
|
+
return report(["交付收据必须写在目标源码根目录之外"])
|
|
668
|
+
write_json(
|
|
669
|
+
delivery_receipt,
|
|
670
|
+
{
|
|
671
|
+
"receipt_version": "1.0",
|
|
672
|
+
"package_status": "packaged_verified",
|
|
673
|
+
"handoff_status": "ready_for_handoff",
|
|
674
|
+
"target_root": str(root),
|
|
675
|
+
"source_snapshot_id": current_manifest["source_snapshot_id"],
|
|
676
|
+
"source_files": len(files),
|
|
677
|
+
"skills": len(skill_files),
|
|
678
|
+
"evaluation_cases": evaluation_count,
|
|
679
|
+
"forbidden_evaluation_terms": current_manifest[
|
|
680
|
+
"forbidden_evaluation_terms"
|
|
681
|
+
],
|
|
682
|
+
"output_zip": str(output_zip),
|
|
683
|
+
},
|
|
684
|
+
)
|
|
685
|
+
return report(
|
|
686
|
+
[], root=root, file_count=len(files), skill_count=len(skill_files),
|
|
687
|
+
evaluation_count=evaluation_count, output_zip=output_zip,
|
|
688
|
+
snapshot_id=str(current_manifest["source_snapshot_id"]),
|
|
689
|
+
delivery_receipt=delivery_receipt,
|
|
690
|
+
)
|
|
691
|
+
|
|
692
|
+
return report(
|
|
693
|
+
[], root=root, file_count=len(files), skill_count=len(skill_files),
|
|
694
|
+
evaluation_count=evaluation_count, output_zip=None,
|
|
695
|
+
)
|
|
696
|
+
|
|
697
|
+
|
|
698
|
+
def report(
|
|
699
|
+
errors: list[str],
|
|
700
|
+
root: Path | None = None,
|
|
701
|
+
file_count: int = 0,
|
|
702
|
+
skill_count: int = 0,
|
|
703
|
+
evaluation_count: int = 0,
|
|
704
|
+
output_zip: Path | None = None,
|
|
705
|
+
prepared_manifest: Path | None = None,
|
|
706
|
+
snapshot_id: str | None = None,
|
|
707
|
+
delivery_receipt: Path | None = None,
|
|
708
|
+
) -> int:
|
|
709
|
+
if errors:
|
|
710
|
+
print(f"DELIVERY_FAIL:{len(errors)} 个确定性资产问题")
|
|
711
|
+
for error in errors:
|
|
712
|
+
print(f"- {error}")
|
|
713
|
+
print("source_status=written_unverified")
|
|
714
|
+
print("说明:该结果不评价职业设计、模型行为、平台装配或业务效果。")
|
|
715
|
+
return 1
|
|
716
|
+
|
|
717
|
+
if prepared_manifest:
|
|
718
|
+
print("SOURCE_REVIEW_PREPARED:源码已校验并冻结为送审快照")
|
|
719
|
+
print("source_review_status=prepared")
|
|
720
|
+
print(f"source_review_manifest={prepared_manifest}")
|
|
721
|
+
print(f"source_snapshot_id={snapshot_id}")
|
|
722
|
+
elif output_zip:
|
|
723
|
+
print("DELIVERY_PASS:源码校验、Skill 可装载性、UTF-8 打包和 ZIP 回读通过")
|
|
724
|
+
print("package_status=packaged_verified")
|
|
725
|
+
print("handoff_status=ready_for_handoff")
|
|
726
|
+
print(f"output_zip={output_zip}")
|
|
727
|
+
print(f"source_snapshot_id={snapshot_id}")
|
|
728
|
+
if delivery_receipt:
|
|
729
|
+
print(f"delivery_receipt={delivery_receipt}")
|
|
730
|
+
else:
|
|
731
|
+
print("SOURCE_PASS:源码与 Skill 可装载性检查通过,未生成 ZIP")
|
|
732
|
+
print("source_status=source_verified")
|
|
733
|
+
if root:
|
|
734
|
+
print(f"target_root={root}")
|
|
735
|
+
print(f"source_files={file_count}")
|
|
736
|
+
print(f"skills={skill_count}")
|
|
737
|
+
print(f"evaluation_cases={evaluation_count}")
|
|
738
|
+
if prepared_manifest:
|
|
739
|
+
print("说明:该结果只证明源码送审快照已生成,不表示 source-gate 已通过或 ZIP 已打包。")
|
|
740
|
+
elif output_zip:
|
|
741
|
+
print("说明:该收据只证明确定性资产事实和 ZIP 已准备交接;最终回答可以提供此处验证的可访问路径,但不得把收据改写为 complete。它不评价职业设计、模型行为、平台装配或业务效果。")
|
|
742
|
+
else:
|
|
743
|
+
print("说明:该结果只证明源码结构事实,未生成 ZIP;不评价职业设计、模型行为、平台装配或业务效果。")
|
|
744
|
+
return 0
|
|
745
|
+
|
|
746
|
+
|
|
747
|
+
if __name__ == "__main__":
|
|
748
|
+
sys.exit(main())
|