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.
Files changed (45) hide show
  1. {repopact-2.1.0 → repopact-2.2.0}/PKG-INFO +3 -3
  2. {repopact-2.1.0 → repopact-2.2.0}/README.md +2 -2
  3. repopact-2.2.0/VERSION +1 -0
  4. {repopact-2.1.0 → repopact-2.2.0}/scripts/adopt_repo.py +17 -10
  5. {repopact-2.1.0 → repopact-2.2.0}/scripts/doctor.py +17 -8
  6. {repopact-2.1.0 → repopact-2.2.0}/scripts/generate_dashboard.py +30 -16
  7. {repopact-2.1.0 → repopact-2.2.0}/scripts/init_repo.py +3 -0
  8. {repopact-2.1.0 → repopact-2.2.0}/scripts/new.py +3 -0
  9. {repopact-2.1.0 → repopact-2.2.0}/scripts/plan_import.py +3 -0
  10. {repopact-2.1.0 → repopact-2.2.0}/scripts/repopact.egg-info/PKG-INFO +3 -3
  11. {repopact-2.1.0 → repopact-2.2.0}/scripts/repopact_cli.py +5 -6
  12. {repopact-2.1.0 → repopact-2.2.0}/scripts/run_conformance.py +6 -0
  13. {repopact-2.1.0 → repopact-2.2.0}/scripts/takeover.py +3 -0
  14. {repopact-2.1.0 → repopact-2.2.0}/scripts/validate_repo.py +38 -10
  15. {repopact-2.1.0 → repopact-2.2.0}/tests/test_conformance.py +2 -0
  16. {repopact-2.1.0 → repopact-2.2.0}/tests/test_validate_repo.py +33 -10
  17. repopact-2.1.0/VERSION +0 -1
  18. {repopact-2.1.0 → repopact-2.2.0}/LICENSE +0 -0
  19. {repopact-2.1.0 → repopact-2.2.0}/pyproject.toml +0 -0
  20. {repopact-2.1.0 → repopact-2.2.0}/schemas/audit-finding.schema.json +0 -0
  21. {repopact-2.1.0 → repopact-2.2.0}/schemas/conformance-manifest.schema.json +0 -0
  22. {repopact-2.1.0 → repopact-2.2.0}/schemas/evidence-run.schema.json +0 -0
  23. {repopact-2.1.0 → repopact-2.2.0}/schemas/frozen-surface.schema.json +0 -0
  24. {repopact-2.1.0 → repopact-2.2.0}/schemas/invariants.schema.json +0 -0
  25. {repopact-2.1.0 → repopact-2.2.0}/schemas/record-frontmatter.schema.json +0 -0
  26. {repopact-2.1.0 → repopact-2.2.0}/schemas/work-item.schema.json +0 -0
  27. {repopact-2.1.0 → repopact-2.2.0}/scripts/check_frozen_surface.py +0 -0
  28. {repopact-2.1.0 → repopact-2.2.0}/scripts/frontmatter.py +0 -0
  29. {repopact-2.1.0 → repopact-2.2.0}/scripts/generate_spec.py +0 -0
  30. {repopact-2.1.0 → repopact-2.2.0}/scripts/repo_model.py +0 -0
  31. {repopact-2.1.0 → repopact-2.2.0}/scripts/repopact.egg-info/SOURCES.txt +0 -0
  32. {repopact-2.1.0 → repopact-2.2.0}/scripts/repopact.egg-info/dependency_links.txt +0 -0
  33. {repopact-2.1.0 → repopact-2.2.0}/scripts/repopact.egg-info/entry_points.txt +0 -0
  34. {repopact-2.1.0 → repopact-2.2.0}/scripts/repopact.egg-info/requires.txt +0 -0
  35. {repopact-2.1.0 → repopact-2.2.0}/scripts/repopact.egg-info/top_level.txt +0 -0
  36. {repopact-2.1.0 → repopact-2.2.0}/scripts/track_import.py +0 -0
  37. {repopact-2.1.0 → repopact-2.2.0}/setup.cfg +0 -0
  38. {repopact-2.1.0 → repopact-2.2.0}/templates/README.md +0 -0
  39. {repopact-2.1.0 → repopact-2.2.0}/templates/decision.md +0 -0
  40. {repopact-2.1.0 → repopact-2.2.0}/templates/evidence-run.json +0 -0
  41. {repopact-2.1.0 → repopact-2.2.0}/templates/policy.md +0 -0
  42. {repopact-2.1.0 → repopact-2.2.0}/templates/work-item.README.md +0 -0
  43. {repopact-2.1.0 → repopact-2.2.0}/templates/work-item.json +0 -0
  44. {repopact-2.1.0 → repopact-2.2.0}/tests/test_plan_import_links.py +0 -0
  45. {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.1.0
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.1.0**
26
- ([changelog](decisions/0024-release-2.1.0-proposed-lifecycle.md)).
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.1.0**
13
- ([changelog](decisions/0024-release-2.1.0-proposed-lifecycle.md)).
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 init_repo # reuse _seed_dir, _json/_write semantics, LIFECYCLE
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
- rep.gitignored = gitignored_records(target, rep.created)
342
- return rep
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 validate_repo
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
- return actions
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
- today = date.today()
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
- "> Generated from source records. Do not edit manually.",
67
- f"> Generated: {today.isoformat()}",
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 main() -> None:
108
- root = Path(__file__).resolve().parents[1]
109
- output = root / "audits" / "reports" / "dashboard.md"
110
- output.write_text(generate(root), encoding="utf-8")
111
- print(f"Generated {output.relative_to(root)}")
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.1.0
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.1.0**
26
- ([changelog](decisions/0024-release-2.1.0-proposed-lifecycle.md)).
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 / "audits" / "reports" / "dashboard.md"
156
- out.write_text(generate_dashboard.generate(root), encoding="utf-8")
157
- print(f"Generated {out.relative_to(root)}")
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
- from repo_model import STATUSES, discover_evidence_ids, discover_work_items, iter_contracts, load_json
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 pathlib import Path
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 generate_spec # noqa: E402
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
- report = takeover.takeover(repo, delete=True)
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