@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.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,3 +0,0 @@
1
- """repo_governance_check package."""
2
-
3
- __version__ = "1.0.0"
@@ -1,6 +0,0 @@
1
- """Allow ``python -m repo_governance_check``."""
2
-
3
- from .cli import main
4
-
5
- if __name__ == "__main__":
6
- raise SystemExit(main())
@@ -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