repopact 2.1.0__tar.gz → 2.2.0__tar.gz
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.
- {repopact-2.1.0 → repopact-2.2.0}/PKG-INFO +3 -3
- {repopact-2.1.0 → repopact-2.2.0}/README.md +2 -2
- repopact-2.2.0/VERSION +1 -0
- {repopact-2.1.0 → repopact-2.2.0}/scripts/adopt_repo.py +17 -10
- {repopact-2.1.0 → repopact-2.2.0}/scripts/doctor.py +17 -8
- {repopact-2.1.0 → repopact-2.2.0}/scripts/generate_dashboard.py +30 -16
- {repopact-2.1.0 → repopact-2.2.0}/scripts/init_repo.py +3 -0
- {repopact-2.1.0 → repopact-2.2.0}/scripts/new.py +3 -0
- {repopact-2.1.0 → repopact-2.2.0}/scripts/plan_import.py +3 -0
- {repopact-2.1.0 → repopact-2.2.0}/scripts/repopact.egg-info/PKG-INFO +3 -3
- {repopact-2.1.0 → repopact-2.2.0}/scripts/repopact_cli.py +5 -6
- {repopact-2.1.0 → repopact-2.2.0}/scripts/run_conformance.py +6 -0
- {repopact-2.1.0 → repopact-2.2.0}/scripts/takeover.py +3 -0
- {repopact-2.1.0 → repopact-2.2.0}/scripts/validate_repo.py +38 -10
- {repopact-2.1.0 → repopact-2.2.0}/tests/test_conformance.py +2 -0
- {repopact-2.1.0 → repopact-2.2.0}/tests/test_validate_repo.py +33 -10
- repopact-2.1.0/VERSION +0 -1
- {repopact-2.1.0 → repopact-2.2.0}/LICENSE +0 -0
- {repopact-2.1.0 → repopact-2.2.0}/pyproject.toml +0 -0
- {repopact-2.1.0 → repopact-2.2.0}/schemas/audit-finding.schema.json +0 -0
- {repopact-2.1.0 → repopact-2.2.0}/schemas/conformance-manifest.schema.json +0 -0
- {repopact-2.1.0 → repopact-2.2.0}/schemas/evidence-run.schema.json +0 -0
- {repopact-2.1.0 → repopact-2.2.0}/schemas/frozen-surface.schema.json +0 -0
- {repopact-2.1.0 → repopact-2.2.0}/schemas/invariants.schema.json +0 -0
- {repopact-2.1.0 → repopact-2.2.0}/schemas/record-frontmatter.schema.json +0 -0
- {repopact-2.1.0 → repopact-2.2.0}/schemas/work-item.schema.json +0 -0
- {repopact-2.1.0 → repopact-2.2.0}/scripts/check_frozen_surface.py +0 -0
- {repopact-2.1.0 → repopact-2.2.0}/scripts/frontmatter.py +0 -0
- {repopact-2.1.0 → repopact-2.2.0}/scripts/generate_spec.py +0 -0
- {repopact-2.1.0 → repopact-2.2.0}/scripts/repo_model.py +0 -0
- {repopact-2.1.0 → repopact-2.2.0}/scripts/repopact.egg-info/SOURCES.txt +0 -0
- {repopact-2.1.0 → repopact-2.2.0}/scripts/repopact.egg-info/dependency_links.txt +0 -0
- {repopact-2.1.0 → repopact-2.2.0}/scripts/repopact.egg-info/entry_points.txt +0 -0
- {repopact-2.1.0 → repopact-2.2.0}/scripts/repopact.egg-info/requires.txt +0 -0
- {repopact-2.1.0 → repopact-2.2.0}/scripts/repopact.egg-info/top_level.txt +0 -0
- {repopact-2.1.0 → repopact-2.2.0}/scripts/track_import.py +0 -0
- {repopact-2.1.0 → repopact-2.2.0}/setup.cfg +0 -0
- {repopact-2.1.0 → repopact-2.2.0}/templates/README.md +0 -0
- {repopact-2.1.0 → repopact-2.2.0}/templates/decision.md +0 -0
- {repopact-2.1.0 → repopact-2.2.0}/templates/evidence-run.json +0 -0
- {repopact-2.1.0 → repopact-2.2.0}/templates/policy.md +0 -0
- {repopact-2.1.0 → repopact-2.2.0}/templates/work-item.README.md +0 -0
- {repopact-2.1.0 → repopact-2.2.0}/templates/work-item.json +0 -0
- {repopact-2.1.0 → repopact-2.2.0}/tests/test_plan_import_links.py +0 -0
- {repopact-2.1.0 → repopact-2.2.0}/tests/test_takeover_refs.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: repopact
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.2.0
|
|
4
4
|
Summary: A repository-native standard for durable agent work: binding invariants, evidence-gated work items, and drift audits.
|
|
5
5
|
License: Apache-2.0
|
|
6
6
|
Project-URL: Homepage, https://github.com/ForgeWireLabs/repopact
|
|
@@ -22,8 +22,8 @@ cannot be silently weakened.
|
|
|
22
22
|
> The repository is the pact: authority, intent, evidence, and history that survive every
|
|
23
23
|
> session.
|
|
24
24
|
|
|
25
|
-
`pip install repopact` · Apache-2.0 · current release **2.
|
|
26
|
-
([changelog](decisions/
|
|
25
|
+
`pip install repopact` · Apache-2.0 · current release **2.2.0**
|
|
26
|
+
([changelog](decisions/0025-release-2.2.0-dashboard-integrity.md)).
|
|
27
27
|
|
|
28
28
|
## How it relates to `AGENTS.md`
|
|
29
29
|
|
|
@@ -9,8 +9,8 @@ cannot be silently weakened.
|
|
|
9
9
|
> The repository is the pact: authority, intent, evidence, and history that survive every
|
|
10
10
|
> session.
|
|
11
11
|
|
|
12
|
-
`pip install repopact` · Apache-2.0 · current release **2.
|
|
13
|
-
([changelog](decisions/
|
|
12
|
+
`pip install repopact` · Apache-2.0 · current release **2.2.0**
|
|
13
|
+
([changelog](decisions/0025-release-2.2.0-dashboard-integrity.md)).
|
|
14
14
|
|
|
15
15
|
## How it relates to `AGENTS.md`
|
|
16
16
|
|
repopact-2.2.0/VERSION
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
2.2.0
|
|
@@ -21,9 +21,10 @@ import re
|
|
|
21
21
|
import subprocess
|
|
22
22
|
import sys
|
|
23
23
|
from datetime import date, datetime, timedelta
|
|
24
|
-
from pathlib import Path
|
|
25
|
-
|
|
26
|
-
import
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
import generate_dashboard
|
|
27
|
+
import init_repo # reuse _seed_dir, _json/_write semantics, LIFECYCLE
|
|
27
28
|
|
|
28
29
|
|
|
29
30
|
# --- non-destructive primitives --------------------------------------------
|
|
@@ -330,16 +331,22 @@ def adopt(target: Path, today: date | None = None, dry_run: bool = False) -> Rep
|
|
|
330
331
|
"from the repository scan rather than proven by a run. Attach real verification evidence and\n"
|
|
331
332
|
"run `repopact doctor` to ratchet it to concrete, then complete it. " + summary + "\n", target)
|
|
332
333
|
|
|
333
|
-
rep.write(target / "decisions" / "0001-adopt-repopact.md",
|
|
334
|
+
rep.write(target / "decisions" / "0001-adopt-repopact.md",
|
|
334
335
|
"---\nid: 0001\ntitle: Adopt RepoPact\nstatus: accepted\n"
|
|
335
336
|
f"date: {today.isoformat()}\nsupersedes: []\n---\n\n# 0001: Adopt RepoPact\n\n## Context\n\n"
|
|
336
337
|
"The project already had ad-hoc governance (CODEOWNERS, CI gates, AGENTS.md). RepoPact\n"
|
|
337
|
-
"makes those bindings explicit and machine-checkable.\n\n## Decision\n\n"
|
|
338
|
-
"Adopt RepoPact; existing workflows become binding gates (INV-2) and ownership becomes\n"
|
|
339
|
-
"scopes/roles. Existing files were preserved; RepoPact records were added around them.\n", target)
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
338
|
+
"makes those bindings explicit and machine-checkable.\n\n## Decision\n\n"
|
|
339
|
+
"Adopt RepoPact; existing workflows become binding gates (INV-2) and ownership becomes\n"
|
|
340
|
+
"scopes/roles. Existing files were preserved; RepoPact records were added around them.\n", target)
|
|
341
|
+
|
|
342
|
+
# The dashboard is derived, but adoption remains non-destructive: create the
|
|
343
|
+
# canonical projection only when the reserved path is absent. An existing file
|
|
344
|
+
# is preserved and validation will identify it if it is not canonical.
|
|
345
|
+
dashboard_text = "" if dry_run else generate_dashboard.generate(target, today=today)
|
|
346
|
+
rep.write(target / "audits" / "reports" / "dashboard.md", dashboard_text, target)
|
|
347
|
+
|
|
348
|
+
rep.gitignored = gitignored_records(target, rep.created)
|
|
349
|
+
return rep
|
|
343
350
|
|
|
344
351
|
|
|
345
352
|
def _print_report(rep: Report) -> None:
|
|
@@ -20,10 +20,11 @@ import shutil
|
|
|
20
20
|
import sys
|
|
21
21
|
from dataclasses import dataclass
|
|
22
22
|
from datetime import date, timedelta
|
|
23
|
-
from pathlib import Path
|
|
24
|
-
|
|
25
|
-
import adopt_repo
|
|
26
|
-
import
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
|
|
25
|
+
import adopt_repo
|
|
26
|
+
import generate_dashboard
|
|
27
|
+
import validate_repo
|
|
27
28
|
|
|
28
29
|
|
|
29
30
|
@dataclass
|
|
@@ -296,10 +297,18 @@ def fix(root: Path, today: date | None = None) -> list[str]:
|
|
|
296
297
|
# 6. ratchet provisional records whose evidence is now concrete (decision 0021)
|
|
297
298
|
actions.extend(_ratchet_provisional(root))
|
|
298
299
|
|
|
299
|
-
# 7. migrate preflight on upgrade: grandfather pre-2.0 work items (decision 0021)
|
|
300
|
-
actions.extend(_migrate_preflight(root, today))
|
|
301
|
-
|
|
302
|
-
|
|
300
|
+
# 7. migrate preflight on upgrade: grandfather pre-2.0 work items (decision 0021)
|
|
301
|
+
actions.extend(_migrate_preflight(root, today))
|
|
302
|
+
|
|
303
|
+
# 8. derived output is safe to repair because it is fully reproducible.
|
|
304
|
+
dashboard = root / "audits" / "reports" / "dashboard.md"
|
|
305
|
+
expected = generate_dashboard.generate(root, today=today)
|
|
306
|
+
actual = dashboard.read_text(encoding="utf-8") if dashboard.is_file() else None
|
|
307
|
+
if actual != expected:
|
|
308
|
+
generate_dashboard.write_dashboard(root, today=today)
|
|
309
|
+
actions.append("regenerated audits/reports/dashboard.md")
|
|
310
|
+
|
|
311
|
+
return actions
|
|
303
312
|
|
|
304
313
|
|
|
305
314
|
def main() -> int:
|
|
@@ -46,8 +46,15 @@ def _overdue_scopes(root: Path, today: date) -> list[tuple[str, str]]:
|
|
|
46
46
|
return overdue
|
|
47
47
|
|
|
48
48
|
|
|
49
|
-
def generate(root: Path) -> str:
|
|
50
|
-
|
|
49
|
+
def generate(root: Path, today: date | None = None) -> str:
|
|
50
|
+
"""Render the canonical dashboard projection for ``root``.
|
|
51
|
+
|
|
52
|
+
``today`` is injectable so audit-cadence transitions can be tested. The
|
|
53
|
+
rendering deliberately does not include the wall-clock date: a generated
|
|
54
|
+
artifact should change only when a displayed source-derived value changes,
|
|
55
|
+
not merely because the command ran on another day.
|
|
56
|
+
"""
|
|
57
|
+
today = today or date.today()
|
|
51
58
|
items = discover_work_items(root)
|
|
52
59
|
counts = Counter(item.status for item in items)
|
|
53
60
|
contracts = iter_contracts(root)
|
|
@@ -60,12 +67,12 @@ def generate(root: Path) -> str:
|
|
|
60
67
|
finding_count = _count_json_records(root / "audits" / "findings")
|
|
61
68
|
overdue = _overdue_scopes(root, today)
|
|
62
69
|
|
|
63
|
-
lines = [
|
|
64
|
-
"# Repository Dashboard",
|
|
65
|
-
"",
|
|
66
|
-
">
|
|
67
|
-
|
|
68
|
-
f"> RepoPact spec version: {_spec_version(root)}",
|
|
70
|
+
lines = [
|
|
71
|
+
"# Repository Dashboard",
|
|
72
|
+
"",
|
|
73
|
+
"> Canonically generated from source records. Do not edit manually.",
|
|
74
|
+
"> Validation fails when this file differs from `repopact dashboard` output.",
|
|
75
|
+
f"> RepoPact spec version: {_spec_version(root)}",
|
|
69
76
|
"",
|
|
70
77
|
"## Health",
|
|
71
78
|
"",
|
|
@@ -101,14 +108,21 @@ def generate(root: Path) -> str:
|
|
|
101
108
|
lines.append("No active or blocked work.")
|
|
102
109
|
else:
|
|
103
110
|
lines.extend(f"- {item.item_id}: {item.data['title']} ({item.status})" for item in active)
|
|
104
|
-
return "\n".join(lines) + "\n"
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
def
|
|
108
|
-
|
|
109
|
-
output = root / "audits" / "reports" / "dashboard.md"
|
|
110
|
-
output.
|
|
111
|
-
|
|
111
|
+
return "\n".join(lines) + "\n"
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def write_dashboard(root: Path, today: date | None = None) -> Path:
|
|
115
|
+
"""Write the one dashboard derived artifact and return its path."""
|
|
116
|
+
output = root / "audits" / "reports" / "dashboard.md"
|
|
117
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
118
|
+
output.write_text(generate(root, today=today), encoding="utf-8")
|
|
119
|
+
return output
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def main() -> None:
|
|
123
|
+
root = Path(__file__).resolve().parents[1]
|
|
124
|
+
output = write_dashboard(root)
|
|
125
|
+
print(f"Generated {output.relative_to(root)}")
|
|
112
126
|
|
|
113
127
|
|
|
114
128
|
if __name__ == "__main__":
|
|
@@ -16,6 +16,8 @@ import sys
|
|
|
16
16
|
from datetime import date, timedelta
|
|
17
17
|
from pathlib import Path
|
|
18
18
|
|
|
19
|
+
import generate_dashboard
|
|
20
|
+
|
|
19
21
|
HERE = Path(__file__).resolve().parent # directory holding the tooling modules
|
|
20
22
|
CHECKOUT = HERE.parent # repo root when running from a checkout
|
|
21
23
|
LIFECYCLE = ("proposed", "active", "blocked", "deferred", "completed")
|
|
@@ -126,6 +128,7 @@ def bootstrap(target: Path, today: date | None = None) -> Path:
|
|
|
126
128
|
_write(target / "README.md",
|
|
127
129
|
"# Repository\n\nBootstrapped with RepoPact. Run `repopact validate` "
|
|
128
130
|
"(or `python scripts/validate_repo.py`).\n")
|
|
131
|
+
generate_dashboard.write_dashboard(target, today=today)
|
|
129
132
|
return target
|
|
130
133
|
|
|
131
134
|
|
|
@@ -16,6 +16,7 @@ import sys
|
|
|
16
16
|
from datetime import date, datetime
|
|
17
17
|
from pathlib import Path
|
|
18
18
|
|
|
19
|
+
import generate_dashboard
|
|
19
20
|
from repo_model import STATUSES
|
|
20
21
|
|
|
21
22
|
ROOT = Path(__file__).resolve().parents[1]
|
|
@@ -64,6 +65,7 @@ def new_work_item(title: str, today: date, root: Path = ROOT, status: str = "act
|
|
|
64
65
|
(directory / "README.md").write_text(
|
|
65
66
|
(root / "templates" / "work-item.README.md").read_text(encoding="utf-8")
|
|
66
67
|
.replace("NNN", item_id).replace("Title Of The Work", title), encoding="utf-8")
|
|
68
|
+
generate_dashboard.write_dashboard(root, today=today)
|
|
67
69
|
return manifest_path
|
|
68
70
|
|
|
69
71
|
|
|
@@ -80,6 +82,7 @@ def new_markdown(kind: str, title: str, today: date, root: Path = ROOT) -> Path:
|
|
|
80
82
|
text = text.replace("Decision Title", title).replace("Policy Title", title)
|
|
81
83
|
path = directory / f"{record_id}-{slug}.md"
|
|
82
84
|
path.write_text(text, encoding="utf-8")
|
|
85
|
+
generate_dashboard.write_dashboard(root, today=today)
|
|
83
86
|
return path
|
|
84
87
|
|
|
85
88
|
|
|
@@ -30,6 +30,7 @@ from datetime import date
|
|
|
30
30
|
from pathlib import Path
|
|
31
31
|
|
|
32
32
|
import adopt_repo # reuse Report and _slug
|
|
33
|
+
import generate_dashboard
|
|
33
34
|
|
|
34
35
|
PLAN_DIR_NAMES = ("todos", "todo", "tasks", "plan", "planning", "backlog")
|
|
35
36
|
CHECKLIST_FILES = ("TODO.md", "TODOS.md", "ROADMAP.md", "BACKLOG.md", "PLAN.md", "TASKS.md")
|
|
@@ -359,6 +360,8 @@ def import_plan(root: Path, today: date | None = None, dry_run: bool = False,
|
|
|
359
360
|
# A `tracking/` governance system maps to decisions/, findings, and work items.
|
|
360
361
|
import track_import
|
|
361
362
|
track_import.import_tracking(root, rep, today)
|
|
363
|
+
if not dry_run:
|
|
364
|
+
generate_dashboard.write_dashboard(root, today=today)
|
|
362
365
|
return rep
|
|
363
366
|
|
|
364
367
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: repopact
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.2.0
|
|
4
4
|
Summary: A repository-native standard for durable agent work: binding invariants, evidence-gated work items, and drift audits.
|
|
5
5
|
License: Apache-2.0
|
|
6
6
|
Project-URL: Homepage, https://github.com/ForgeWireLabs/repopact
|
|
@@ -22,8 +22,8 @@ cannot be silently weakened.
|
|
|
22
22
|
> The repository is the pact: authority, intent, evidence, and history that survive every
|
|
23
23
|
> session.
|
|
24
24
|
|
|
25
|
-
`pip install repopact` · Apache-2.0 · current release **2.
|
|
26
|
-
([changelog](decisions/
|
|
25
|
+
`pip install repopact` · Apache-2.0 · current release **2.2.0**
|
|
26
|
+
([changelog](decisions/0025-release-2.2.0-dashboard-integrity.md)).
|
|
27
27
|
|
|
28
28
|
## How it relates to `AGENTS.md`
|
|
29
29
|
|
|
@@ -150,12 +150,11 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
150
150
|
else f"\nValidation failed with {len(problems)} error(s).")
|
|
151
151
|
return 1 if problems else 0
|
|
152
152
|
|
|
153
|
-
if args.command == "dashboard":
|
|
154
|
-
import generate_dashboard
|
|
155
|
-
out = root
|
|
156
|
-
out.
|
|
157
|
-
|
|
158
|
-
return 0
|
|
153
|
+
if args.command == "dashboard":
|
|
154
|
+
import generate_dashboard
|
|
155
|
+
out = generate_dashboard.write_dashboard(root)
|
|
156
|
+
print(f"Generated {out.relative_to(root)}")
|
|
157
|
+
return 0
|
|
159
158
|
|
|
160
159
|
if args.command == "spec":
|
|
161
160
|
import generate_spec
|
|
@@ -16,6 +16,8 @@ import tempfile
|
|
|
16
16
|
from dataclasses import dataclass
|
|
17
17
|
from pathlib import Path
|
|
18
18
|
|
|
19
|
+
import generate_dashboard
|
|
20
|
+
|
|
19
21
|
|
|
20
22
|
ROOT = Path(__file__).resolve().parents[1]
|
|
21
23
|
MANIFEST = ROOT / "conformance" / "manifest.json"
|
|
@@ -54,6 +56,10 @@ def materialize_case(root: Path, fixtures_root: Path, case: dict) -> Path:
|
|
|
54
56
|
else:
|
|
55
57
|
shutil.copytree(fixtures_root / str(case["path"]), repo)
|
|
56
58
|
shutil.copytree(ROOT / "schemas", repo / "schemas")
|
|
59
|
+
# Overlays intentionally mutate source records. Materialize their canonical
|
|
60
|
+
# derived projection so each case tests its declared rule rather than failing
|
|
61
|
+
# secondarily on dashboard drift.
|
|
62
|
+
generate_dashboard.write_dashboard(repo)
|
|
57
63
|
return repo
|
|
58
64
|
|
|
59
65
|
|
|
@@ -35,6 +35,8 @@ import sys
|
|
|
35
35
|
from datetime import date
|
|
36
36
|
from pathlib import Path
|
|
37
37
|
|
|
38
|
+
import generate_dashboard
|
|
39
|
+
|
|
38
40
|
import adopt_repo
|
|
39
41
|
import plan_import
|
|
40
42
|
import validate_repo
|
|
@@ -362,6 +364,7 @@ def takeover(root: Path, delete: bool = False, dry_run: bool = False) -> dict:
|
|
|
362
364
|
report["review"].append(hint)
|
|
363
365
|
|
|
364
366
|
if not dry_run and report["retired"]:
|
|
367
|
+
generate_dashboard.write_dashboard(root)
|
|
365
368
|
report["post_validate_ok"] = not validate_repo.validate(root)
|
|
366
369
|
return report
|
|
367
370
|
|
|
@@ -8,10 +8,11 @@ from dataclasses import dataclass
|
|
|
8
8
|
from datetime import date, datetime
|
|
9
9
|
from pathlib import Path
|
|
10
10
|
|
|
11
|
-
import jsonschema
|
|
12
|
-
|
|
13
|
-
from frontmatter import FrontMatterError, parse_file
|
|
14
|
-
|
|
11
|
+
import jsonschema
|
|
12
|
+
|
|
13
|
+
from frontmatter import FrontMatterError, parse_file
|
|
14
|
+
import generate_dashboard
|
|
15
|
+
from repo_model import STATUSES, discover_evidence_ids, discover_work_items, iter_contracts, load_json
|
|
15
16
|
|
|
16
17
|
|
|
17
18
|
REQUIRED_WORK_FIELDS = {
|
|
@@ -473,7 +474,7 @@ def validate_evidence(root: Path, work_ids: set[str], problems: list[Problem]) -
|
|
|
473
474
|
problems.append(Problem(path, f"unknown work_item '{data['work_item']}'"))
|
|
474
475
|
|
|
475
476
|
|
|
476
|
-
def validate_audit_registry(root: Path, problems: list[Problem]) -> None:
|
|
477
|
+
def validate_audit_registry(root: Path, problems: list[Problem]) -> None:
|
|
477
478
|
path = root / "audits" / "registry.json"
|
|
478
479
|
try:
|
|
479
480
|
data = load_json(path)
|
|
@@ -485,7 +486,33 @@ def validate_audit_registry(root: Path, problems: list[Problem]) -> None:
|
|
|
485
486
|
if not scope_path.exists():
|
|
486
487
|
problems.append(Problem(path, f"audit scope does not exist: {entry.get('path')}"))
|
|
487
488
|
validate_dates(entry.get("last_reviewed"), "last_reviewed", path, problems)
|
|
488
|
-
validate_dates(entry.get("next_review"), "next_review", path, problems)
|
|
489
|
+
validate_dates(entry.get("next_review"), "next_review", path, problems)
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
def validate_dashboard(root: Path, problems: list[Problem]) -> None:
|
|
493
|
+
"""Reject a missing or stale committed dashboard projection.
|
|
494
|
+
|
|
495
|
+
Source-record validation remains authoritative. If malformed sources prevent
|
|
496
|
+
generation, their existing validators report the primary error and this
|
|
497
|
+
secondary comparison stays silent instead of crashing validation.
|
|
498
|
+
"""
|
|
499
|
+
path = root / "audits" / "reports" / "dashboard.md"
|
|
500
|
+
if not path.is_file():
|
|
501
|
+
problems.append(Problem(
|
|
502
|
+
path,
|
|
503
|
+
"missing generated dashboard; run `repopact dashboard --root .`",
|
|
504
|
+
))
|
|
505
|
+
return
|
|
506
|
+
try:
|
|
507
|
+
expected = generate_dashboard.generate(root)
|
|
508
|
+
actual = path.read_text(encoding="utf-8")
|
|
509
|
+
except (OSError, ValueError, KeyError, TypeError, json.JSONDecodeError):
|
|
510
|
+
return
|
|
511
|
+
if actual != expected:
|
|
512
|
+
problems.append(Problem(
|
|
513
|
+
path,
|
|
514
|
+
"generated dashboard is stale; run `repopact dashboard --root .` and commit the result",
|
|
515
|
+
))
|
|
489
516
|
|
|
490
517
|
|
|
491
518
|
def _validate_records(root: Path, directory: Path, pattern: str, statuses: tuple[str, ...],
|
|
@@ -594,10 +621,11 @@ def validate(root: Path) -> list[Problem]:
|
|
|
594
621
|
validate_findings(root, owner_scopes, problems)
|
|
595
622
|
work_ids = validate_work(root, owner_scopes, enforce_disjoint, problems)
|
|
596
623
|
validate_orphan_work_dirs(root, problems)
|
|
597
|
-
validate_evidence(root, work_ids, problems)
|
|
598
|
-
validate_audit_registry(root, problems)
|
|
599
|
-
validate_decisions(root, problems)
|
|
600
|
-
validate_policies(root, problems)
|
|
624
|
+
validate_evidence(root, work_ids, problems)
|
|
625
|
+
validate_audit_registry(root, problems)
|
|
626
|
+
validate_decisions(root, problems)
|
|
627
|
+
validate_policies(root, problems)
|
|
628
|
+
validate_dashboard(root, problems)
|
|
601
629
|
return sorted(problems, key=lambda problem: (str(problem.path), problem.message))
|
|
602
630
|
|
|
603
631
|
|
|
@@ -21,6 +21,7 @@ ROOT = Path(__file__).resolve().parents[1]
|
|
|
21
21
|
sys.path.insert(0, str(ROOT / "scripts"))
|
|
22
22
|
|
|
23
23
|
from validate_repo import validate # noqa: E402
|
|
24
|
+
import generate_dashboard # noqa: E402
|
|
24
25
|
|
|
25
26
|
import run_conformance # noqa: E402
|
|
26
27
|
|
|
@@ -33,6 +34,7 @@ INVALID = FIXTURES / "invalid"
|
|
|
33
34
|
def build_repo(dst: Path, overlay: Path | None = None) -> Path:
|
|
34
35
|
shutil.copytree(VALID, dst)
|
|
35
36
|
shutil.copytree(ROOT / "schemas", dst / "schemas")
|
|
37
|
+
generate_dashboard.write_dashboard(dst)
|
|
36
38
|
if overlay is not None:
|
|
37
39
|
for src in overlay.rglob("*"):
|
|
38
40
|
if src.is_dir() or src.name == "meta.json":
|
|
@@ -3,9 +3,10 @@ from __future__ import annotations
|
|
|
3
3
|
import json
|
|
4
4
|
import shutil
|
|
5
5
|
import sys
|
|
6
|
-
import tempfile
|
|
7
|
-
import unittest
|
|
8
|
-
from
|
|
6
|
+
import tempfile
|
|
7
|
+
import unittest
|
|
8
|
+
from datetime import date
|
|
9
|
+
from pathlib import Path
|
|
9
10
|
|
|
10
11
|
|
|
11
12
|
ROOT = Path(__file__).resolve().parents[1]
|
|
@@ -15,8 +16,9 @@ import adopt_repo # noqa: E402
|
|
|
15
16
|
import plan_import # noqa: E402
|
|
16
17
|
import takeover # noqa: E402
|
|
17
18
|
import doctor # noqa: E402
|
|
18
|
-
import check_frozen_surface # noqa: E402
|
|
19
|
-
import
|
|
19
|
+
import check_frozen_surface # noqa: E402
|
|
20
|
+
import generate_dashboard # noqa: E402
|
|
21
|
+
import generate_spec # noqa: E402
|
|
20
22
|
import init_repo # noqa: E402
|
|
21
23
|
import repopact_cli # noqa: E402
|
|
22
24
|
from validate_repo import validate # noqa: E402
|
|
@@ -70,21 +72,41 @@ class RepositoryValidationTests(unittest.TestCase):
|
|
|
70
72
|
"created": "2026-06-15",
|
|
71
73
|
"updated": "2026-06-15",
|
|
72
74
|
}
|
|
73
|
-
if preflight:
|
|
75
|
+
if preflight:
|
|
74
76
|
data["preflight"] = {
|
|
75
77
|
"created_before_work_started": True,
|
|
76
78
|
"created_at": "2026-06-15T00:00:00Z",
|
|
77
79
|
"note": "probe",
|
|
78
80
|
}
|
|
79
81
|
(directory / "work-item.json").write_text(json.dumps(data), encoding="utf-8")
|
|
82
|
+
generate_dashboard.write_dashboard(self.root)
|
|
80
83
|
|
|
81
84
|
def add_active_item(self, item_id: str, owner_scope: str = "work", preflight: bool = True) -> None:
|
|
82
85
|
self.add_work_item(item_id, "active", owner_scope, preflight)
|
|
83
86
|
|
|
84
87
|
# --- baseline -----------------------------------------------------------
|
|
85
88
|
|
|
86
|
-
def test_repository_is_valid(self) -> None:
|
|
87
|
-
self.assertEqual([], self.problems())
|
|
89
|
+
def test_repository_is_valid(self) -> None:
|
|
90
|
+
self.assertEqual([], self.problems())
|
|
91
|
+
|
|
92
|
+
def test_missing_dashboard_is_rejected(self) -> None:
|
|
93
|
+
(self.root / "audits" / "reports" / "dashboard.md").unlink()
|
|
94
|
+
self.assertTrue(any("missing generated dashboard" in value for value in self.problems()))
|
|
95
|
+
|
|
96
|
+
def test_stale_dashboard_is_rejected_and_doctor_repairs_it(self) -> None:
|
|
97
|
+
dashboard = self.root / "audits" / "reports" / "dashboard.md"
|
|
98
|
+
dashboard.write_text("# obsolete dashboard\n", encoding="utf-8")
|
|
99
|
+
self.assertTrue(any("generated dashboard is stale" in value for value in self.problems()))
|
|
100
|
+
|
|
101
|
+
actions = doctor.fix(self.root, today=date(2026, 7, 18))
|
|
102
|
+
self.assertIn("regenerated audits/reports/dashboard.md", actions)
|
|
103
|
+
self.assertEqual([], self.problems())
|
|
104
|
+
|
|
105
|
+
def test_dashboard_generation_is_stable_between_audit_transitions(self) -> None:
|
|
106
|
+
first = generate_dashboard.generate(self.root, today=date(2026, 7, 18))
|
|
107
|
+
later = generate_dashboard.generate(self.root, today=date(2026, 7, 19))
|
|
108
|
+
self.assertEqual(first, later)
|
|
109
|
+
self.assertNotIn("> Generated:", first)
|
|
88
110
|
|
|
89
111
|
# --- work lifecycle -----------------------------------------------------
|
|
90
112
|
|
|
@@ -704,8 +726,9 @@ class RepositoryValidationTests(unittest.TestCase):
|
|
|
704
726
|
"contract": "todos/12-search/AGENTS.md", "last_reviewed": "2026-06-15",
|
|
705
727
|
"next_review": "2026-09-13", "alignment": "current", "notes": "nested",
|
|
706
728
|
})
|
|
707
|
-
(repo / "audits" / "registry.json").write_text(json.dumps(reg, indent=2) + "\n", encoding="utf-8")
|
|
708
|
-
|
|
729
|
+
(repo / "audits" / "registry.json").write_text(json.dumps(reg, indent=2) + "\n", encoding="utf-8")
|
|
730
|
+
generate_dashboard.write_dashboard(repo)
|
|
731
|
+
report = takeover.takeover(repo, delete=True)
|
|
709
732
|
self.assertEqual([], report["retired"]) # not retired
|
|
710
733
|
self.assertTrue((repo / "todos").exists())
|
|
711
734
|
self.assertTrue(any(b["dir"] == "todos" for b in report["blocked"]))
|
repopact-2.1.0/VERSION
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
2.1.0
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|