@hap-labs/human-agent-paradigm 0.1.0 → 0.2.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/CHANGELOG.md +2 -15
- package/README.md +18 -44
- package/README.zh-CN.md +5 -27
- package/assets/EN/CONFORMANCE_CHECKLIST.md +156 -161
- package/assets/EN/CONTRACT_TEMPLATE.md +108 -112
- package/assets/EN/DECISION_REQUEST_TEMPLATE.md +76 -81
- package/assets/EN/DELIVERY_REPORT_TEMPLATE.md +103 -83
- package/assets/EN/DERIVED_SPECIFICATION.md +73 -76
- package/assets/EN/HUMAN_AGENT_PARADIGM.md +26 -30
- package/assets/EN/TRANSLATION_GLOSSARY.md +79 -0
- package/assets/ZH_CN/TRANSLATION_GLOSSARY.md +72 -0
- package/package.json +6 -15
- package/scripts/python/repo_governance_check/__init__.py +0 -3
- package/scripts/python/repo_governance_check/__main__.py +0 -6
- package/scripts/python/repo_governance_check/checks.py +0 -359
- package/scripts/python/repo_governance_check/cli.py +0 -115
- package/scripts/python/repo_governance_check/config.py +0 -33
- package/scripts/python/repo_governance_check/governance_check_config.json +0 -166
- package/scripts/python/repo_governance_check/report.py +0 -79
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# 翻译术语表 — 治理术语(EN ↔ 中文)
|
|
2
|
+
|
|
3
|
+
> English version: [TRANSLATION_GLOSSARY.md](../EN/TRANSLATION_GLOSSARY.md)
|
|
4
|
+
|
|
5
|
+
治理层文档(`assets/` 下六份冻结文档及其官方译本、以及由它们驱动的流程记录)翻译与一致性审校的统一基准。翻译或审校任何治理材料时使用下列对应关系;新增行须说明理由,并保持表格按英文术语排序。
|
|
6
|
+
|
|
7
|
+
| English | 中文 | 语境/备注 |
|
|
8
|
+
|:----------------------------------|:-----------------------------------|:---------------------------------------------------------------------|
|
|
9
|
+
| Human-Agent Paradigm (HAP) | 人机协作范式 / 人与 Agent 协作范式 | 专有名词;产品名 |
|
|
10
|
+
| constitution | 宪法 | 唯一权威文档:`assets/ZH_CN/HUMAN_AGENT_PARADIGM.md` |
|
|
11
|
+
| derived specification | 派生规范 | `DERIVED_SPECIFICATION` |
|
|
12
|
+
| conformance self-check checklist | 符合性自检清单 | `CONFORMANCE_CHECKLIST` |
|
|
13
|
+
| contract | 契约 | 已签署的 C1–C10 契约 |
|
|
14
|
+
| contract template | 契约模板 | `CONTRACT_TEMPLATE` |
|
|
15
|
+
| decision request | 决策请求 | `DECISION_REQUEST_TEMPLATE`(Part A) |
|
|
16
|
+
| necessary information request | 必要信息补全请求 | 同一模板(Part B) |
|
|
17
|
+
| delivery report | 交付报告 | `DELIVERY_REPORT_TEMPLATE` |
|
|
18
|
+
| Owner | 人(所有者)/ 所有者 | 也称 “the human”;不译作“owner-company” |
|
|
19
|
+
| Agent | Agent | 全责公司;中文语境保留 “Agent” |
|
|
20
|
+
| decision-maker | 决策者 | 人的角色 |
|
|
21
|
+
| mandatory file | 强制文件 | P0–P7 阶段记录 |
|
|
22
|
+
| hard gate | 硬门 | 阻止推进的机器或人工门禁 |
|
|
23
|
+
| record | 记录 | 流程产物,如 P0_intent.md |
|
|
24
|
+
| evidence | 证据 | 可独立复核的证明 |
|
|
25
|
+
| evidence package | 证据包 | 交付用证据集 |
|
|
26
|
+
| multi-view review | 多视角审视 | 下述四个视角 |
|
|
27
|
+
| product perspective | 产品视角 | P6 视角词 |
|
|
28
|
+
| user perspective | 用户视角 | P6 视角词 |
|
|
29
|
+
| engineering perspective | 工程视角 | P6 视角词 |
|
|
30
|
+
| adversarial review / perspective | 对抗性(审查)视角 | P6 视角词 |
|
|
31
|
+
| five excellence dimensions | 优秀五维度 | 6.1–6.5(见下) |
|
|
32
|
+
| sound design | 设计合理 | 维度 6.1 |
|
|
33
|
+
| complete functionality | 功能完备 | 维度 6.2 |
|
|
34
|
+
| elegant implementation | 实现优雅 | 维度 6.3 |
|
|
35
|
+
| perfect experience | 体验完美 | 维度 6.4 |
|
|
36
|
+
| excellent performance | 性能优异 | 维度 6.5 |
|
|
37
|
+
| autonomy budget | 自治预算 | 契约 C7 |
|
|
38
|
+
| decision point | 决策点 | 流程术语 |
|
|
39
|
+
| proposal | 提案 | 可决策材料(P2) |
|
|
40
|
+
| intent | 意图 | P0 对象 |
|
|
41
|
+
| definition of done (DoD) | 完成定义 | 契约 C4 |
|
|
42
|
+
| acceptance | 验收 | P7 活动 |
|
|
43
|
+
| owner signature / signing | 签署 | 以用户语言副本为准 |
|
|
44
|
+
| reliability | 可靠 | 硬性要求一 |
|
|
45
|
+
| excellence | 优秀 | 硬性要求二 |
|
|
46
|
+
| safety and compliance bottom line | 安全与合规底线 | 宪法底线 |
|
|
47
|
+
| assets | 资产 | 沉淀的可复用成果 |
|
|
48
|
+
| product DNA | 产品 DNA | 沉淀的人偏好与价值观 |
|
|
49
|
+
| run | run / 流程实例 | `.hap/runs/<run-id>/`;中文记录中与 SKILL.md 一致使用 “run/流程实例” |
|
|
50
|
+
| governance root | 治理根 | `.hap/` 目录 |
|
|
51
|
+
| bilingual pair | 双语成对 | 用户语言(无后缀) |
|
|
52
|
+
|
|
53
|
+
## 规则
|
|
54
|
+
|
|
55
|
+
1. P0–P7、C1–C10、A1–A7…J1–J7、★ 标记与版本串一律不翻译。
|
|
56
|
+
2. 冻结文档正文内的文件名引用指向同一语言目录下的同名文件(例如 `assets/EN/DERIVED_SPECIFICATION.md` 内引用
|
|
57
|
+
`HUMAN_AGENT_PARADIGM`);冻结中文原文正文内嵌的规范小写文件名引用保持原样(内容冻结不可改),工具复制到 `.hap/docs/`
|
|
58
|
+
时使用规范小写名。
|
|
59
|
+
3. 已随代码注释、README(EN)与注入契约文本发布的既有英文措辞优先(如 “hard requirement”、“multi-view review”、“owner-decision
|
|
60
|
+
contract”)。
|
|
61
|
+
4. 记录的双语副本同轮书写同一事实——事实与 ID 必须一致,措辞可随语言调整; **英文副本供 Agent 读取与推理(Agent 读取记录事实一律读
|
|
62
|
+
`records/EN/`)**,中文副本供用户(所有者)审批阅读与签署。
|
|
63
|
+
|
|
64
|
+
## 语言目录与新增语言
|
|
65
|
+
|
|
66
|
+
仓库语言文档按语言分目录,语言码大写、复合码用下划线:`assets/ZH_CN/`(冻结中文原文)、`assets/EN/`(英文译本)、`docs/EN/`、
|
|
67
|
+
`docs/ZH_CN/`——目录内文件名为纯 `<NAME>.md`(大写+下划线连接,无语言后缀)。
|
|
68
|
+
|
|
69
|
+
新增语言:
|
|
70
|
+
|
|
71
|
+
1. 在 `assets/` 下建立同名语言目录(如 `JA/`;复合码用下划线),把文档译为目录内纯 `<NAME>.md`;
|
|
72
|
+
2. 使用前在本术语表与 README「语言规则」节登记。
|
package/package.json
CHANGED
|
@@ -1,36 +1,27 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hap-labs/human-agent-paradigm",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Human-Agent Paradigm (HAP)
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Human-Agent Paradigm (HAP) content package: the Constitution, the Derived Specification, the Conformance Checklist, and the Contract / Decision-Request / Delivery-Report templates in Simplified Chinese (ZH_CN) and English (EN).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
8
|
-
"url": "https://github.com/hap-labs/human-agent-paradigm
|
|
8
|
+
"url": "https://github.com/hap-labs/human-agent-paradigm.git"
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
11
11
|
"assets",
|
|
12
|
-
"scripts/python/repo_governance_check",
|
|
13
12
|
"README.md",
|
|
14
13
|
"README.zh-CN.md",
|
|
15
|
-
"CHANGELOG.md"
|
|
16
|
-
"!**/__pycache__",
|
|
17
|
-
"!**/__pycache__/**",
|
|
18
|
-
"!**/*.pyc"
|
|
14
|
+
"CHANGELOG.md"
|
|
19
15
|
],
|
|
20
16
|
"publishConfig": {
|
|
21
17
|
"access": "public"
|
|
22
18
|
},
|
|
23
19
|
"keywords": [
|
|
24
|
-
"
|
|
20
|
+
"constitution",
|
|
25
21
|
"ai",
|
|
26
22
|
"agent",
|
|
27
|
-
"governance"
|
|
28
|
-
"constitution",
|
|
29
|
-
"bilingual"
|
|
23
|
+
"governance"
|
|
30
24
|
],
|
|
31
|
-
"scripts": {
|
|
32
|
-
"check": "python3 -m scripts.python.repo_governance_check --root . --out-dir reports"
|
|
33
|
-
},
|
|
34
25
|
"packageManager": "pnpm@11.25.0",
|
|
35
26
|
"engines": {
|
|
36
27
|
"node": ">=18"
|
|
@@ -1,359 +0,0 @@
|
|
|
1
|
-
"""G01-G08 governance checks.
|
|
2
|
-
|
|
3
|
-
Every rule receives a CheckContext and returns a list of Finding objects.
|
|
4
|
-
Rules never modify files and only read files declared in the config.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
from dataclasses import dataclass
|
|
8
|
-
import re
|
|
9
|
-
from pathlib import Path
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
ITEM_RE = re.compile(r"^\|\s*([A-Z]\d+)\s*(★)?\s*\|")
|
|
13
|
-
SEPARATOR_CELL_RE = re.compile(r"^:?-{1,}:?$")
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
@dataclass(frozen=True)
|
|
17
|
-
class Finding:
|
|
18
|
-
rule_id: str
|
|
19
|
-
severity: str
|
|
20
|
-
file: str
|
|
21
|
-
line: int
|
|
22
|
-
message: str
|
|
23
|
-
evidence: str
|
|
24
|
-
|
|
25
|
-
def to_dict(self):
|
|
26
|
-
return {
|
|
27
|
-
"rule_id": self.rule_id,
|
|
28
|
-
"severity": self.severity,
|
|
29
|
-
"file": self.file,
|
|
30
|
-
"line": self.line,
|
|
31
|
-
"message": self.message,
|
|
32
|
-
"evidence": self.evidence,
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
def _upper_name(filename):
|
|
37
|
-
"""Language directories store documents under uppercase underscore names;
|
|
38
|
-
consumer copies keep the canonical lowercase names."""
|
|
39
|
-
stem, dot, ext = filename.rpartition(".")
|
|
40
|
-
return stem.replace("-", "_").upper() + dot + ext
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
class CheckContext:
|
|
44
|
-
def __init__(self, root, config, docs_dir=None):
|
|
45
|
-
self.root = Path(root)
|
|
46
|
-
self.config = config
|
|
47
|
-
self.docs_dir = Path(docs_dir) if docs_dir else self.root
|
|
48
|
-
|
|
49
|
-
def doc_file(self, filename):
|
|
50
|
-
"""First existing document path under docs_dir (canonical or
|
|
51
|
-
uppercase-underscore form); falls back to the canonical path."""
|
|
52
|
-
direct = self.docs_dir / filename
|
|
53
|
-
if direct.is_file():
|
|
54
|
-
return direct
|
|
55
|
-
upper = self.docs_dir / _upper_name(filename)
|
|
56
|
-
return upper if upper.is_file() else direct
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
def _read_lines(ctx, filename):
|
|
60
|
-
try:
|
|
61
|
-
return ctx.doc_file(filename).read_text(encoding="utf-8").splitlines()
|
|
62
|
-
except OSError:
|
|
63
|
-
return None
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
def _headings(lines):
|
|
67
|
-
for number, raw in enumerate(lines, 1):
|
|
68
|
-
stripped = raw.strip()
|
|
69
|
-
if stripped.startswith("#"):
|
|
70
|
-
yield number, stripped.lstrip("#").strip()
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
def _finding(ctx, rule_id, filename, line, message, evidence):
|
|
74
|
-
return Finding(rule_id, "error", filename, line, message, evidence)
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
def _table_blocks(lines):
|
|
78
|
-
blocks = []
|
|
79
|
-
current = []
|
|
80
|
-
for number, raw in enumerate(lines, 1):
|
|
81
|
-
if raw.strip().startswith("|"):
|
|
82
|
-
current.append((number, raw.strip()))
|
|
83
|
-
elif current:
|
|
84
|
-
blocks.append(current)
|
|
85
|
-
current = []
|
|
86
|
-
if current:
|
|
87
|
-
blocks.append(current)
|
|
88
|
-
return blocks
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
def _cell_count(row):
|
|
92
|
-
return max(row.count("|") - 1, 0)
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
def _is_separator(row):
|
|
96
|
-
cells = [cell.strip() for cell in row.strip("|").split("|")]
|
|
97
|
-
if not cells:
|
|
98
|
-
return False
|
|
99
|
-
return all(SEPARATOR_CELL_RE.fullmatch(cell) for cell in cells)
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
def check_required_files(ctx):
|
|
103
|
-
findings = []
|
|
104
|
-
for filename in ctx.config["required_files"]:
|
|
105
|
-
checked = ctx.doc_file(filename)
|
|
106
|
-
if not checked.is_file():
|
|
107
|
-
findings.append(
|
|
108
|
-
_finding(
|
|
109
|
-
ctx,
|
|
110
|
-
"G01",
|
|
111
|
-
filename,
|
|
112
|
-
0,
|
|
113
|
-
f"required file is missing: {filename}",
|
|
114
|
-
f"checked path: {checked}",
|
|
115
|
-
)
|
|
116
|
-
)
|
|
117
|
-
return findings
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
def check_upstream_references(ctx):
|
|
121
|
-
findings = []
|
|
122
|
-
for filename, expected in ctx.config["upstream_references"].items():
|
|
123
|
-
lines = _read_lines(ctx, filename)
|
|
124
|
-
if lines is None:
|
|
125
|
-
continue
|
|
126
|
-
content = "\n".join(lines)
|
|
127
|
-
for ref in expected:
|
|
128
|
-
if ref not in content:
|
|
129
|
-
findings.append(
|
|
130
|
-
_finding(
|
|
131
|
-
ctx,
|
|
132
|
-
"G02",
|
|
133
|
-
filename,
|
|
134
|
-
0,
|
|
135
|
-
f"missing upstream reference: {ref!r}",
|
|
136
|
-
"full-file substring check",
|
|
137
|
-
)
|
|
138
|
-
)
|
|
139
|
-
return findings
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
def check_required_headings(ctx):
|
|
143
|
-
findings = []
|
|
144
|
-
for filename, expected in ctx.config["required_headings"].items():
|
|
145
|
-
lines = _read_lines(ctx, filename)
|
|
146
|
-
if lines is None:
|
|
147
|
-
continue
|
|
148
|
-
present = {heading for _, heading in _headings(lines)}
|
|
149
|
-
for heading in expected:
|
|
150
|
-
if heading not in present:
|
|
151
|
-
findings.append(
|
|
152
|
-
_finding(
|
|
153
|
-
ctx,
|
|
154
|
-
"G03",
|
|
155
|
-
filename,
|
|
156
|
-
0,
|
|
157
|
-
f"missing required heading: {heading}",
|
|
158
|
-
"heading text parsed from markdown heading lines",
|
|
159
|
-
)
|
|
160
|
-
)
|
|
161
|
-
return findings
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
def check_markdown_tables(ctx):
|
|
165
|
-
findings = []
|
|
166
|
-
for filename in ctx.config["required_files"]:
|
|
167
|
-
lines = _read_lines(ctx, filename)
|
|
168
|
-
if lines is None:
|
|
169
|
-
continue
|
|
170
|
-
for block in _table_blocks(lines):
|
|
171
|
-
first_line, _ = block[0]
|
|
172
|
-
if len(block) < 2 or not _is_separator(block[1][1]):
|
|
173
|
-
findings.append(
|
|
174
|
-
_finding(
|
|
175
|
-
ctx,
|
|
176
|
-
"G04",
|
|
177
|
-
filename,
|
|
178
|
-
first_line,
|
|
179
|
-
"table has no valid separator row as its second row",
|
|
180
|
-
f"table starts at line {first_line}",
|
|
181
|
-
)
|
|
182
|
-
)
|
|
183
|
-
continue
|
|
184
|
-
expected_columns = _cell_count(block[1][1])
|
|
185
|
-
for line_number, row in block:
|
|
186
|
-
if _cell_count(row) != expected_columns:
|
|
187
|
-
findings.append(
|
|
188
|
-
_finding(
|
|
189
|
-
ctx,
|
|
190
|
-
"G04",
|
|
191
|
-
filename,
|
|
192
|
-
line_number,
|
|
193
|
-
"table row column count differs from separator row",
|
|
194
|
-
f"expected {expected_columns} columns, row has {_cell_count(row)}",
|
|
195
|
-
)
|
|
196
|
-
)
|
|
197
|
-
return findings
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
def _checklist_missing_sections(ctx, headings):
|
|
201
|
-
findings = []
|
|
202
|
-
checklist_config = ctx.config["checklist"]
|
|
203
|
-
for section in checklist_config["sections"]:
|
|
204
|
-
if section not in headings:
|
|
205
|
-
findings.append(
|
|
206
|
-
_finding(
|
|
207
|
-
ctx,
|
|
208
|
-
"G05",
|
|
209
|
-
"conformance_checklist.md",
|
|
210
|
-
0,
|
|
211
|
-
f"missing checklist section heading: {section}",
|
|
212
|
-
"heading inventory",
|
|
213
|
-
)
|
|
214
|
-
)
|
|
215
|
-
final_section = checklist_config["final_section"]
|
|
216
|
-
if final_section not in headings:
|
|
217
|
-
findings.append(
|
|
218
|
-
_finding(
|
|
219
|
-
ctx,
|
|
220
|
-
"G05",
|
|
221
|
-
"conformance_checklist.md",
|
|
222
|
-
0,
|
|
223
|
-
f"missing final section heading: {final_section}",
|
|
224
|
-
"heading inventory",
|
|
225
|
-
)
|
|
226
|
-
)
|
|
227
|
-
return findings
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
def _checklist_row_metrics(lines):
|
|
231
|
-
item_count = 0
|
|
232
|
-
star_count = 0
|
|
233
|
-
bad_star_rows = []
|
|
234
|
-
item_ids = []
|
|
235
|
-
for line_number, raw in enumerate(lines, 1):
|
|
236
|
-
match = ITEM_RE.match(raw.strip())
|
|
237
|
-
if not match:
|
|
238
|
-
continue
|
|
239
|
-
item_count += 1
|
|
240
|
-
item_ids.append(match.group(1))
|
|
241
|
-
if match.group(2):
|
|
242
|
-
star_count += 1
|
|
243
|
-
cells = [cell.strip() for cell in raw.strip().strip("|").split("|")]
|
|
244
|
-
if cells and cells[-1] == "不适用":
|
|
245
|
-
bad_star_rows.append(line_number)
|
|
246
|
-
return item_count, star_count, bad_star_rows, item_ids
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
def check_checklist_invariants(ctx):
|
|
250
|
-
lines = _read_lines(ctx, "conformance_checklist.md")
|
|
251
|
-
if lines is None:
|
|
252
|
-
return []
|
|
253
|
-
headings = {heading for _, heading in _headings(lines)}
|
|
254
|
-
findings = _checklist_missing_sections(ctx, headings)
|
|
255
|
-
item_count, star_count, bad_star_rows, item_ids = _checklist_row_metrics(lines)
|
|
256
|
-
checklist_config = ctx.config["checklist"]
|
|
257
|
-
prefix_map = checklist_config.get("item_prefixes", {})
|
|
258
|
-
for section in checklist_config["sections"]:
|
|
259
|
-
prefix = prefix_map.get(section, section.split(".")[0])
|
|
260
|
-
if not any(item_id.startswith(prefix) for item_id in item_ids):
|
|
261
|
-
findings.append(
|
|
262
|
-
_finding(
|
|
263
|
-
ctx,
|
|
264
|
-
"G05",
|
|
265
|
-
"conformance_checklist.md",
|
|
266
|
-
0,
|
|
267
|
-
f"checklist section has no item rows: {section}",
|
|
268
|
-
f"no item ID starts with prefix {prefix!r}",
|
|
269
|
-
)
|
|
270
|
-
)
|
|
271
|
-
expected_items = checklist_config["expected_items"]
|
|
272
|
-
if item_count != expected_items:
|
|
273
|
-
findings.append(
|
|
274
|
-
_finding(
|
|
275
|
-
ctx,
|
|
276
|
-
"G05",
|
|
277
|
-
"conformance_checklist.md",
|
|
278
|
-
0,
|
|
279
|
-
f"checklist item count is {item_count}, expected {expected_items}",
|
|
280
|
-
"item rows matching the A-Z numbering pattern",
|
|
281
|
-
)
|
|
282
|
-
)
|
|
283
|
-
expected_stars = checklist_config["expected_star_items"]
|
|
284
|
-
if star_count != expected_stars:
|
|
285
|
-
findings.append(
|
|
286
|
-
_finding(
|
|
287
|
-
ctx,
|
|
288
|
-
"G05",
|
|
289
|
-
"conformance_checklist.md",
|
|
290
|
-
0,
|
|
291
|
-
f"star item count is {star_count}, expected {expected_stars}",
|
|
292
|
-
"item rows containing the star marker",
|
|
293
|
-
)
|
|
294
|
-
)
|
|
295
|
-
for line_number in bad_star_rows:
|
|
296
|
-
findings.append(
|
|
297
|
-
_finding(
|
|
298
|
-
ctx,
|
|
299
|
-
"G05",
|
|
300
|
-
"conformance_checklist.md",
|
|
301
|
-
line_number,
|
|
302
|
-
"star item result column must not contain 不适用",
|
|
303
|
-
"star item rows may not be exempted in the checklist source",
|
|
304
|
-
)
|
|
305
|
-
)
|
|
306
|
-
return findings
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
def _check_sections(ctx, rule_id, filename, config_key):
|
|
310
|
-
lines = _read_lines(ctx, filename)
|
|
311
|
-
if lines is None:
|
|
312
|
-
return []
|
|
313
|
-
present = {heading for _, heading in _headings(lines)}
|
|
314
|
-
findings = []
|
|
315
|
-
for section in ctx.config[config_key]["required_sections"]:
|
|
316
|
-
if section not in present:
|
|
317
|
-
findings.append(
|
|
318
|
-
_finding(
|
|
319
|
-
ctx,
|
|
320
|
-
rule_id,
|
|
321
|
-
filename,
|
|
322
|
-
0,
|
|
323
|
-
f"missing required section: {section}",
|
|
324
|
-
"heading inventory",
|
|
325
|
-
)
|
|
326
|
-
)
|
|
327
|
-
return findings
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
def check_contract_template_sections(ctx):
|
|
331
|
-
return _check_sections(ctx, "G06", "contract_template.md", "contract_template")
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
def check_decision_template_sections(ctx):
|
|
335
|
-
return _check_sections(ctx, "G07", "decision_request_template.md", "decision_request_template")
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
def check_delivery_template_sections(ctx):
|
|
339
|
-
return _check_sections(ctx, "G08", "delivery_report_template.md", "delivery_report_template")
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
ALL_RULES = (
|
|
343
|
-
check_required_files,
|
|
344
|
-
check_upstream_references,
|
|
345
|
-
check_required_headings,
|
|
346
|
-
check_markdown_tables,
|
|
347
|
-
check_checklist_invariants,
|
|
348
|
-
check_contract_template_sections,
|
|
349
|
-
check_decision_template_sections,
|
|
350
|
-
check_delivery_template_sections,
|
|
351
|
-
)
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
def run_all_checks(ctx):
|
|
355
|
-
findings = []
|
|
356
|
-
for rule in ALL_RULES:
|
|
357
|
-
findings.extend(rule(ctx))
|
|
358
|
-
findings.sort(key=lambda item: (item.file, item.line, item.rule_id))
|
|
359
|
-
return findings
|
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
"""Command line entry point for repo_governance_check."""
|
|
2
|
-
|
|
3
|
-
import argparse
|
|
4
|
-
from datetime import datetime, timezone
|
|
5
|
-
from pathlib import Path
|
|
6
|
-
import time
|
|
7
|
-
|
|
8
|
-
from .checks import CheckContext, _upper_name, run_all_checks
|
|
9
|
-
from .config import load_config
|
|
10
|
-
from .report import render_console, render_json, render_markdown, result_payload
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
def find_config(root):
|
|
14
|
-
"""Locate the check config: explicit root copy first, then the bundled
|
|
15
|
-
default that ships next to this package."""
|
|
16
|
-
candidates = [
|
|
17
|
-
root / "governance_check_config.json",
|
|
18
|
-
Path(__file__).resolve().parent / "governance_check_config.json",
|
|
19
|
-
]
|
|
20
|
-
for candidate in candidates:
|
|
21
|
-
if candidate.is_file():
|
|
22
|
-
return candidate
|
|
23
|
-
return None
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
def detect_docs_dir(root, config):
|
|
27
|
-
"""Pick the directory holding the governance document set.
|
|
28
|
-
|
|
29
|
-
Candidates mirror the repository layouts: the root itself, the package's
|
|
30
|
-
per-language assets directories (ZH_CN holds the frozen Chinese
|
|
31
|
-
originals, EN the English translations), the project docs directory, and
|
|
32
|
-
a consumer workspace's .hap/docs/ directory. Falls back to the root so an
|
|
33
|
-
explicit --config against arbitrary directories keeps the historical
|
|
34
|
-
semantics (missing files surface as G01 findings).
|
|
35
|
-
"""
|
|
36
|
-
candidates = [
|
|
37
|
-
root,
|
|
38
|
-
root / "assets" / "ZH_CN",
|
|
39
|
-
root / "assets" / "EN",
|
|
40
|
-
root / "assets",
|
|
41
|
-
root / "vendor" / "governance-snapshot" / "ZH_CN",
|
|
42
|
-
root / "vendor" / "governance-snapshot" / "EN",
|
|
43
|
-
root / "vendor" / "governance-snapshot",
|
|
44
|
-
root / "docs",
|
|
45
|
-
root / ".hap" / "docs",
|
|
46
|
-
]
|
|
47
|
-
|
|
48
|
-
def _doc_exists(candidate, name):
|
|
49
|
-
return (candidate / name).is_file() or (candidate / _upper_name(name)).is_file()
|
|
50
|
-
|
|
51
|
-
for candidate in candidates:
|
|
52
|
-
if all(_doc_exists(candidate, name) for name in config["required_files"]):
|
|
53
|
-
return candidate
|
|
54
|
-
return root
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
def parse_args(argv=None):
|
|
58
|
-
parser = argparse.ArgumentParser(
|
|
59
|
-
prog="repo_governance_check",
|
|
60
|
-
description="Check governance documents for structural consistency.",
|
|
61
|
-
)
|
|
62
|
-
parser.add_argument("--root", default=".", help="repository root to check")
|
|
63
|
-
parser.add_argument(
|
|
64
|
-
"--config",
|
|
65
|
-
default=None,
|
|
66
|
-
help="path to governance_check_config.json (default: ROOT or the bundled package copy)",
|
|
67
|
-
)
|
|
68
|
-
parser.add_argument(
|
|
69
|
-
"--out-dir",
|
|
70
|
-
default="reports",
|
|
71
|
-
help="directory for generated reports (default: ROOT/reports)",
|
|
72
|
-
)
|
|
73
|
-
parser.add_argument(
|
|
74
|
-
"--format",
|
|
75
|
-
choices=("json", "markdown", "both"),
|
|
76
|
-
default="both",
|
|
77
|
-
help="report format to write",
|
|
78
|
-
)
|
|
79
|
-
return parser.parse_args(argv)
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
def main(argv=None):
|
|
83
|
-
args = parse_args(argv)
|
|
84
|
-
root = Path(args.root).resolve()
|
|
85
|
-
if args.config:
|
|
86
|
-
config_path = Path(args.config).resolve()
|
|
87
|
-
else:
|
|
88
|
-
config_path = find_config(root)
|
|
89
|
-
if config_path is None:
|
|
90
|
-
raise SystemExit(
|
|
91
|
-
"no governance_check_config.json found under --root "
|
|
92
|
-
"or next to the package; pass --config explicitly"
|
|
93
|
-
)
|
|
94
|
-
out_dir = Path(args.out_dir)
|
|
95
|
-
if not out_dir.is_absolute():
|
|
96
|
-
out_dir = root / out_dir
|
|
97
|
-
config = load_config(config_path)
|
|
98
|
-
docs_dir = detect_docs_dir(root, config)
|
|
99
|
-
context = CheckContext(root, config, docs_dir)
|
|
100
|
-
started = time.perf_counter()
|
|
101
|
-
findings = run_all_checks(context)
|
|
102
|
-
elapsed = time.perf_counter() - started
|
|
103
|
-
run_id = datetime.now(timezone.utc).strftime("RGC-%Y%m%d-%H%M%S")
|
|
104
|
-
payload = result_payload(run_id, root, config_path, docs_dir, elapsed, findings)
|
|
105
|
-
out_dir.mkdir(parents=True, exist_ok=True)
|
|
106
|
-
if args.format in ("json", "both"):
|
|
107
|
-
(out_dir / "repo_governance_check_report.json").write_text(
|
|
108
|
-
render_json(payload), encoding="utf-8"
|
|
109
|
-
)
|
|
110
|
-
if args.format in ("markdown", "both"):
|
|
111
|
-
(out_dir / "repo_governance_check_report.md").write_text(
|
|
112
|
-
render_markdown(payload), encoding="utf-8"
|
|
113
|
-
)
|
|
114
|
-
print(render_console(payload))
|
|
115
|
-
return 1 if payload["summary"]["total_findings"] else 0
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
"""Load and validate governance check configuration."""
|
|
2
|
-
|
|
3
|
-
import json
|
|
4
|
-
from pathlib import Path
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
REQUIRED_KEYS = (
|
|
8
|
-
"required_files",
|
|
9
|
-
"upstream_references",
|
|
10
|
-
"required_headings",
|
|
11
|
-
"checklist",
|
|
12
|
-
"contract_template",
|
|
13
|
-
"decision_request_template",
|
|
14
|
-
"delivery_report_template",
|
|
15
|
-
)
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def load_config(path):
|
|
19
|
-
"""Read a JSON config file and return the decoded object.
|
|
20
|
-
|
|
21
|
-
The function only performs structural validation. Rule-specific values
|
|
22
|
-
are consumed by the checks module.
|
|
23
|
-
"""
|
|
24
|
-
config_path = Path(path)
|
|
25
|
-
config = json.loads(config_path.read_text(encoding="utf-8"))
|
|
26
|
-
missing = [key for key in REQUIRED_KEYS if key not in config]
|
|
27
|
-
if missing:
|
|
28
|
-
raise ValueError(f"config missing keys: {', '.join(missing)}")
|
|
29
|
-
if not isinstance(config["required_files"], list):
|
|
30
|
-
raise ValueError("config 'required_files' must be a list")
|
|
31
|
-
if not isinstance(config["required_headings"], dict):
|
|
32
|
-
raise ValueError("config 'required_headings' must be a dict")
|
|
33
|
-
return config
|