@hap-labs/human-agent-paradigm 0.1.0 → 0.2.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/README.md +19 -45
- 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/docs/ZH_CN/README.md +30 -0
- package/package.json +7 -16
- package/CHANGELOG.md +0 -21
- package/README.zh-CN.md +0 -52
- 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
package/README.zh-CN.md
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
# hap-governance
|
|
2
|
-
|
|
3
|
-
人机协作范式(HAP)的 **治理内容包**:六份冻结治理文档与其结构自检工具, 作为独立 npm 包发布,使内容(按人批准节奏演进)与工具(按工程节奏演进)解耦发布。
|
|
4
|
-
|
|
5
|
-
[English](README.md) | **简体中文**
|
|
6
|
-
|
|
7
|
-
## 内容
|
|
8
|
-
|
|
9
|
-
| 路径 | 用途 |
|
|
10
|
-
|:----------------------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|
11
|
-
| `assets/ZH_CN/` | 六份治理文档(简体中文原文):`HUMAN_AGENT_PARADIGM.md`(宪法 v1.0)、`DERIVED_SPECIFICATION.md`(派生规范 v1.0)、`CONFORMANCE_CHECKLIST.md`(符合性清单 v1.0)、`CONTRACT_TEMPLATE.md`、`DECISION_REQUEST_TEMPLATE.md`、`DELIVERY_REPORT_TEMPLATE.md` |
|
|
12
|
-
| `assets/EN/` | 同一六份的官方英文译本 |
|
|
13
|
-
| `scripts/python/repo_governance_check/` | 文档组结构自检(仅 Python 标准库;自动探测 `assets/ZH_CN` 等) |
|
|
14
|
-
|
|
15
|
-
## 用法
|
|
16
|
-
|
|
17
|
-
消费方把 `assets/` 语言目录复制进各自工作区的治理根。独立使用:
|
|
18
|
-
|
|
19
|
-
```bash
|
|
20
|
-
python3 -m scripts.python.repo_governance_check --root . --out-dir reports
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
## 版本策略
|
|
24
|
-
|
|
25
|
-
| 内容版本 | 包版本 | 触发 |
|
|
26
|
-
|:---------------------------|:-------------------|:-------------|
|
|
27
|
-
| 宪法 v1.0 | 1.0.0(基线) | 首发 |
|
|
28
|
-
| 宪法修订(主版本) | major 递增 | 宪法新版本 |
|
|
29
|
-
| 派生规范 / 清单 / 模板修订 | minor / patch 递增 | 派生家族编辑 |
|
|
30
|
-
|
|
31
|
-
宪法版本为准,包版本随动。任何内容修订都须在 HAP 流程中经所有者明确批准
|
|
32
|
-
(宪法 §0.4、派生规范 §0.4)。自检工具的工具性改动不改变治理内容。
|
|
33
|
-
|
|
34
|
-
## 发布
|
|
35
|
-
|
|
36
|
-
版本经 GitHub Actions([.github/workflows/publish.yml](.github/workflows/publish.yml))发布到 npm:推送与 `package.json` 版本一致的 `v*` tag 会触发构建 + `npm publish`;也可用 `workflow_dispatch` 手动触发。
|
|
37
|
-
|
|
38
|
-
工作流使用 npm **Trusted Publishing(OIDC)**——发布时以 GitHub Actions 的 OIDC token 认证,**本仓库无需存储任何 npm token/密钥**;公开仓库发布时 provenance 自动生成。
|
|
39
|
-
|
|
40
|
-
首次发布成功前,需在 npmjs.com 完成一次性 Trusted Publisher 配置(包 Settings → Trusted publishing → GitHub Actions):
|
|
41
|
-
|
|
42
|
-
- Organization or user:`hap-labs`
|
|
43
|
-
- Repository:`human-agent-paradigm-governance`
|
|
44
|
-
- Workflow filename:`publish.yml`
|
|
45
|
-
- Allowed actions:`npm publish`
|
|
46
|
-
|
|
47
|
-
首次发布成功后,可进一步加固(包 Settings → Publishing access → 「Require two-factor authentication and disallow tokens」),并吊销不再使用的自动化 token。
|
|
48
|
-
|
|
49
|
-
## 治理
|
|
50
|
-
|
|
51
|
-
本仓库受自身内容(HAP 宪法)治理。修订通过 HAP run + 所有者签署完成;
|
|
52
|
-
发布是所有者授权动作。
|
|
@@ -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
|