repopact 2.0.2__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.0.2 → repopact-2.2.0}/PKG-INFO +17 -8
- {repopact-2.0.2 → repopact-2.2.0}/README.md +16 -7
- repopact-2.2.0/VERSION +1 -0
- {repopact-2.0.2 → repopact-2.2.0}/schemas/work-item.schema.json +1 -2
- {repopact-2.0.2 → repopact-2.2.0}/scripts/adopt_repo.py +17 -10
- {repopact-2.0.2 → repopact-2.2.0}/scripts/doctor.py +17 -8
- {repopact-2.0.2 → repopact-2.2.0}/scripts/generate_dashboard.py +30 -16
- {repopact-2.0.2 → repopact-2.2.0}/scripts/init_repo.py +4 -1
- {repopact-2.0.2 → repopact-2.2.0}/scripts/new.py +13 -4
- {repopact-2.0.2 → repopact-2.2.0}/scripts/plan_import.py +14 -4
- {repopact-2.0.2 → repopact-2.2.0}/scripts/repo_model.py +1 -2
- {repopact-2.0.2 → repopact-2.2.0}/scripts/repopact.egg-info/PKG-INFO +17 -8
- {repopact-2.0.2 → repopact-2.2.0}/scripts/repopact_cli.py +19 -16
- {repopact-2.0.2 → repopact-2.2.0}/scripts/run_conformance.py +6 -0
- {repopact-2.0.2 → repopact-2.2.0}/scripts/takeover.py +4 -1
- {repopact-2.0.2 → repopact-2.2.0}/scripts/validate_repo.py +50 -15
- {repopact-2.0.2 → repopact-2.2.0}/tests/test_conformance.py +2 -0
- {repopact-2.0.2 → repopact-2.2.0}/tests/test_validate_repo.py +97 -38
- repopact-2.0.2/VERSION +0 -1
- {repopact-2.0.2 → repopact-2.2.0}/LICENSE +0 -0
- {repopact-2.0.2 → repopact-2.2.0}/pyproject.toml +0 -0
- {repopact-2.0.2 → repopact-2.2.0}/schemas/audit-finding.schema.json +0 -0
- {repopact-2.0.2 → repopact-2.2.0}/schemas/conformance-manifest.schema.json +0 -0
- {repopact-2.0.2 → repopact-2.2.0}/schemas/evidence-run.schema.json +0 -0
- {repopact-2.0.2 → repopact-2.2.0}/schemas/frozen-surface.schema.json +0 -0
- {repopact-2.0.2 → repopact-2.2.0}/schemas/invariants.schema.json +0 -0
- {repopact-2.0.2 → repopact-2.2.0}/schemas/record-frontmatter.schema.json +0 -0
- {repopact-2.0.2 → repopact-2.2.0}/scripts/check_frozen_surface.py +0 -0
- {repopact-2.0.2 → repopact-2.2.0}/scripts/frontmatter.py +0 -0
- {repopact-2.0.2 → repopact-2.2.0}/scripts/generate_spec.py +0 -0
- {repopact-2.0.2 → repopact-2.2.0}/scripts/repopact.egg-info/SOURCES.txt +0 -0
- {repopact-2.0.2 → repopact-2.2.0}/scripts/repopact.egg-info/dependency_links.txt +0 -0
- {repopact-2.0.2 → repopact-2.2.0}/scripts/repopact.egg-info/entry_points.txt +0 -0
- {repopact-2.0.2 → repopact-2.2.0}/scripts/repopact.egg-info/requires.txt +0 -0
- {repopact-2.0.2 → repopact-2.2.0}/scripts/repopact.egg-info/top_level.txt +0 -0
- {repopact-2.0.2 → repopact-2.2.0}/scripts/track_import.py +0 -0
- {repopact-2.0.2 → repopact-2.2.0}/setup.cfg +0 -0
- {repopact-2.0.2 → repopact-2.2.0}/templates/README.md +0 -0
- {repopact-2.0.2 → repopact-2.2.0}/templates/decision.md +0 -0
- {repopact-2.0.2 → repopact-2.2.0}/templates/evidence-run.json +0 -0
- {repopact-2.0.2 → repopact-2.2.0}/templates/policy.md +0 -0
- {repopact-2.0.2 → repopact-2.2.0}/templates/work-item.README.md +0 -0
- {repopact-2.0.2 → repopact-2.2.0}/templates/work-item.json +0 -0
- {repopact-2.0.2 → repopact-2.2.0}/tests/test_plan_import_links.py +0 -0
- {repopact-2.0.2 → 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.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.0
|
|
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
|
|
|
@@ -71,7 +71,8 @@ intent -> scoped authority -> work item -> implementation -> evidence -> audit -
|
|
|
71
71
|
pip install repopact # the CLI + reference validator, from PyPI
|
|
72
72
|
repopact init --target ../your-repo # seed a valid RepoPact in a new repo
|
|
73
73
|
cd ../your-repo
|
|
74
|
-
repopact new work-item "Title of the work" # stamps
|
|
74
|
+
repopact new work-item "Title of the work" # stamps active work (incl. the preflight marker)
|
|
75
|
+
repopact new work-item "Candidate idea" --status proposed
|
|
75
76
|
repopact validate
|
|
76
77
|
repopact dashboard
|
|
77
78
|
```
|
|
@@ -110,7 +111,7 @@ idempotent.
|
|
|
110
111
|
|
|
111
112
|
## 2.0: mandatory preflight + provenance-typed records
|
|
112
113
|
|
|
113
|
-
Decision [`0021`](decisions/
|
|
114
|
+
Decision [`0021`](decisions/0021-preflight-mandatory-and-provenance.md) (supersedes 0018):
|
|
114
115
|
|
|
115
116
|
- **Mandatory preflight (default on).** No work begins until a work item exists and
|
|
116
117
|
propagates through the pact; `repopact new` stamps the marker. Existing repos grandfather
|
|
@@ -123,9 +124,17 @@ Decision [`0021`](decisions/0022-release-2.0.2-installed-seed-lookup.md) (supers
|
|
|
123
124
|
|
|
124
125
|
## Status is a filesystem transition
|
|
125
126
|
|
|
126
|
-
Work moves between
|
|
127
|
-
|
|
128
|
-
|
|
127
|
+
Work moves between lifecycle directories; its `work-item.json` status must match its
|
|
128
|
+
directory. Moving a work item never deletes its reasoning, decisions, or evidence links.
|
|
129
|
+
|
|
130
|
+
- `proposed`: captured candidate work that is not yet accepted or authorized for
|
|
131
|
+
implementation.
|
|
132
|
+
- `active`: accepted work authorized for design or implementation.
|
|
133
|
+
- `blocked`: accepted/current work that cannot proceed until a named condition changes.
|
|
134
|
+
- `deferred`: accepted work intentionally postponed with rationale.
|
|
135
|
+
- `completed`: delivered work whose acceptance criteria are evidence-closed.
|
|
136
|
+
|
|
137
|
+
Active and completed work cannot depend on proposed work as if it were accepted.
|
|
129
138
|
|
|
130
139
|
## Derive over declare
|
|
131
140
|
|
|
@@ -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.0
|
|
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
|
|
|
@@ -58,7 +58,8 @@ intent -> scoped authority -> work item -> implementation -> evidence -> audit -
|
|
|
58
58
|
pip install repopact # the CLI + reference validator, from PyPI
|
|
59
59
|
repopact init --target ../your-repo # seed a valid RepoPact in a new repo
|
|
60
60
|
cd ../your-repo
|
|
61
|
-
repopact new work-item "Title of the work" # stamps
|
|
61
|
+
repopact new work-item "Title of the work" # stamps active work (incl. the preflight marker)
|
|
62
|
+
repopact new work-item "Candidate idea" --status proposed
|
|
62
63
|
repopact validate
|
|
63
64
|
repopact dashboard
|
|
64
65
|
```
|
|
@@ -97,7 +98,7 @@ idempotent.
|
|
|
97
98
|
|
|
98
99
|
## 2.0: mandatory preflight + provenance-typed records
|
|
99
100
|
|
|
100
|
-
Decision [`0021`](decisions/
|
|
101
|
+
Decision [`0021`](decisions/0021-preflight-mandatory-and-provenance.md) (supersedes 0018):
|
|
101
102
|
|
|
102
103
|
- **Mandatory preflight (default on).** No work begins until a work item exists and
|
|
103
104
|
propagates through the pact; `repopact new` stamps the marker. Existing repos grandfather
|
|
@@ -110,9 +111,17 @@ Decision [`0021`](decisions/0022-release-2.0.2-installed-seed-lookup.md) (supers
|
|
|
110
111
|
|
|
111
112
|
## Status is a filesystem transition
|
|
112
113
|
|
|
113
|
-
Work moves between
|
|
114
|
-
|
|
115
|
-
|
|
114
|
+
Work moves between lifecycle directories; its `work-item.json` status must match its
|
|
115
|
+
directory. Moving a work item never deletes its reasoning, decisions, or evidence links.
|
|
116
|
+
|
|
117
|
+
- `proposed`: captured candidate work that is not yet accepted or authorized for
|
|
118
|
+
implementation.
|
|
119
|
+
- `active`: accepted work authorized for design or implementation.
|
|
120
|
+
- `blocked`: accepted/current work that cannot proceed until a named condition changes.
|
|
121
|
+
- `deferred`: accepted work intentionally postponed with rationale.
|
|
122
|
+
- `completed`: delivered work whose acceptance criteria are evidence-closed.
|
|
123
|
+
|
|
124
|
+
Active and completed work cannot depend on proposed work as if it were accepted.
|
|
116
125
|
|
|
117
126
|
## Derive over declare
|
|
118
127
|
|
repopact-2.2.0/VERSION
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
2.2.0
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"properties": {
|
|
7
7
|
"id": {"type": "string", "pattern": "^[0-9]{3,}$"},
|
|
8
8
|
"title": {"type": "string", "minLength": 1},
|
|
9
|
-
"status": {"enum": ["active", "blocked", "deferred", "completed"]},
|
|
9
|
+
"status": {"enum": ["proposed", "active", "blocked", "deferred", "completed"]},
|
|
10
10
|
"owner_scope": {"type": "string", "minLength": 1},
|
|
11
11
|
"affected_scopes": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
|
|
12
12
|
"depends_on": {"type": "array", "items": {"type": "string", "pattern": "^[0-9]{3,}$"}, "uniqueItems": true},
|
|
@@ -39,4 +39,3 @@
|
|
|
39
39
|
"updated": {"type": "string", "format": "date"}
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
|
-
|
|
@@ -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,9 +16,11 @@ 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
|
-
LIFECYCLE = ("active", "blocked", "deferred", "completed")
|
|
23
|
+
LIFECYCLE = ("proposed", "active", "blocked", "deferred", "completed")
|
|
22
24
|
MODULES = (
|
|
23
25
|
"repo_model.py", "validate_repo.py", "generate_dashboard.py", "generate_spec.py",
|
|
24
26
|
"init_repo.py", "new.py", "check_frozen_surface.py", "frontmatter.py", "repopact_cli.py",
|
|
@@ -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,9 @@ import sys
|
|
|
16
16
|
from datetime import date, datetime
|
|
17
17
|
from pathlib import Path
|
|
18
18
|
|
|
19
|
+
import generate_dashboard
|
|
20
|
+
from repo_model import STATUSES
|
|
21
|
+
|
|
19
22
|
ROOT = Path(__file__).resolve().parents[1]
|
|
20
23
|
|
|
21
24
|
|
|
@@ -32,11 +35,13 @@ def _next_numeric(paths: list[Path], width: int, start: int) -> str:
|
|
|
32
35
|
return str(max(used + [start - 1]) + 1).zfill(width)
|
|
33
36
|
|
|
34
37
|
|
|
35
|
-
def new_work_item(title: str, today: date, root: Path = ROOT) -> Path:
|
|
38
|
+
def new_work_item(title: str, today: date, root: Path = ROOT, status: str = "active") -> Path:
|
|
39
|
+
if status not in STATUSES:
|
|
40
|
+
raise ValueError(f"unknown work-item status '{status}'")
|
|
36
41
|
slug = slugify(title)
|
|
37
42
|
existing = list((root / "work").glob("*/*/work-item.json"))
|
|
38
43
|
item_id = _next_numeric([p.parent for p in existing], 3, 1)
|
|
39
|
-
directory = root / "work" /
|
|
44
|
+
directory = root / "work" / status / f"{item_id}-{slug}"
|
|
40
45
|
directory.mkdir(parents=True)
|
|
41
46
|
# Mandatory preflight (decision 0021): creating the work item with `new` IS the
|
|
42
47
|
# preflight act, so stamp the marker and concrete provenance up front. No work
|
|
@@ -44,7 +49,7 @@ def new_work_item(title: str, today: date, root: Path = ROOT) -> Path:
|
|
|
44
49
|
created_at = datetime.combine(today, datetime.min.time()).isoformat() + "Z"
|
|
45
50
|
manifest = {
|
|
46
51
|
"$schema": "../../../schemas/work-item.schema.json",
|
|
47
|
-
"id": item_id, "title": title, "status":
|
|
52
|
+
"id": item_id, "title": title, "status": status,
|
|
48
53
|
"owner_scope": "governance", "affected_scopes": [], "depends_on": [],
|
|
49
54
|
"provenance": "concrete",
|
|
50
55
|
"preflight": {
|
|
@@ -60,6 +65,7 @@ def new_work_item(title: str, today: date, root: Path = ROOT) -> Path:
|
|
|
60
65
|
(directory / "README.md").write_text(
|
|
61
66
|
(root / "templates" / "work-item.README.md").read_text(encoding="utf-8")
|
|
62
67
|
.replace("NNN", item_id).replace("Title Of The Work", title), encoding="utf-8")
|
|
68
|
+
generate_dashboard.write_dashboard(root, today=today)
|
|
63
69
|
return manifest_path
|
|
64
70
|
|
|
65
71
|
|
|
@@ -76,6 +82,7 @@ def new_markdown(kind: str, title: str, today: date, root: Path = ROOT) -> Path:
|
|
|
76
82
|
text = text.replace("Decision Title", title).replace("Policy Title", title)
|
|
77
83
|
path = directory / f"{record_id}-{slug}.md"
|
|
78
84
|
path.write_text(text, encoding="utf-8")
|
|
85
|
+
generate_dashboard.write_dashboard(root, today=today)
|
|
79
86
|
return path
|
|
80
87
|
|
|
81
88
|
|
|
@@ -83,10 +90,12 @@ def main() -> int:
|
|
|
83
90
|
parser = argparse.ArgumentParser(description="Stamp a new record from a template")
|
|
84
91
|
parser.add_argument("kind", choices=["work-item", "decision", "policy"])
|
|
85
92
|
parser.add_argument("title")
|
|
93
|
+
parser.add_argument("--status", choices=STATUSES, default="active",
|
|
94
|
+
help="Lifecycle status for a new work item")
|
|
86
95
|
args = parser.parse_args()
|
|
87
96
|
today = date.today()
|
|
88
97
|
if args.kind == "work-item":
|
|
89
|
-
path = new_work_item(args.title, today)
|
|
98
|
+
path = new_work_item(args.title, today, status=args.status)
|
|
90
99
|
else:
|
|
91
100
|
path = new_markdown(args.kind, args.title, today)
|
|
92
101
|
print(f"Created {path.relative_to(ROOT)}")
|
|
@@ -11,7 +11,7 @@ Supported sources (extensible):
|
|
|
11
11
|
|
|
12
12
|
* **Plan directories** — `todos/`, `todo/`, `tasks/`, `plan/`, `planning/`, `backlog/`
|
|
13
13
|
with one subdirectory or markdown file per item, optionally grouped under
|
|
14
|
-
`completed/`, `deferred/`, `blocked/`, `active/` lifecycle folders.
|
|
14
|
+
`proposed/`, `completed/`, `deferred/`, `blocked/`, `active/` lifecycle folders.
|
|
15
15
|
* **Checklist files** — `TODO.md`, `TODOS.md`, `ROADMAP.md`, `BACKLOG.md`, `PLAN.md`,
|
|
16
16
|
`TASKS.md` at the root: each `- [ ]` / `- [x]` line becomes a work item.
|
|
17
17
|
|
|
@@ -30,19 +30,27 @@ 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")
|
|
36
|
-
LIFECYCLE_DIRS = {"completed", "deferred", "blocked", "active"}
|
|
37
|
+
LIFECYCLE_DIRS = {"proposed", "completed", "deferred", "blocked", "active"}
|
|
37
38
|
|
|
38
39
|
# source lifecycle -> (work status, acceptance-criterion state)
|
|
39
40
|
_STATUS = {
|
|
41
|
+
"proposed": ("proposed", "pending"),
|
|
40
42
|
"completed": ("completed", "waived"),
|
|
41
43
|
"deferred": ("deferred", "pending"),
|
|
42
44
|
"blocked": ("blocked", "pending"),
|
|
43
45
|
"active": ("active", "pending"),
|
|
44
46
|
}
|
|
45
|
-
_EMOJI = {
|
|
47
|
+
_EMOJI = {
|
|
48
|
+
"proposed": "Proposed",
|
|
49
|
+
"completed": "✅ Complete",
|
|
50
|
+
"deferred": "⏸️ Deferred",
|
|
51
|
+
"blocked": "⛔ Blocked",
|
|
52
|
+
"active": "📋 Active",
|
|
53
|
+
}
|
|
46
54
|
|
|
47
55
|
_SKIP_DIRS = {".git", ".venv", "node_modules", "__pycache__"}
|
|
48
56
|
# Index/template/readme files in a plan directory are scaffolding, not plan items.
|
|
@@ -55,7 +63,7 @@ def _is_scaffold(name: str) -> bool:
|
|
|
55
63
|
|
|
56
64
|
@dataclass
|
|
57
65
|
class PlanItem:
|
|
58
|
-
lifecycle: str # completed | deferred | blocked | active
|
|
66
|
+
lifecycle: str # proposed | completed | deferred | blocked | active
|
|
59
67
|
slug: str # RepoPact slug (no leading number)
|
|
60
68
|
title: str
|
|
61
69
|
narrative: str # markdown body to preserve
|
|
@@ -352,6 +360,8 @@ def import_plan(root: Path, today: date | None = None, dry_run: bool = False,
|
|
|
352
360
|
# A `tracking/` governance system maps to decisions/, findings, and work items.
|
|
353
361
|
import track_import
|
|
354
362
|
track_import.import_tracking(root, rep, today)
|
|
363
|
+
if not dry_run:
|
|
364
|
+
generate_dashboard.write_dashboard(root, today=today)
|
|
355
365
|
return rep
|
|
356
366
|
|
|
357
367
|
|
|
@@ -6,7 +6,7 @@ from pathlib import Path
|
|
|
6
6
|
from typing import Any
|
|
7
7
|
|
|
8
8
|
|
|
9
|
-
STATUSES = ("active", "blocked", "deferred", "completed")
|
|
9
|
+
STATUSES = ("proposed", "active", "blocked", "deferred", "completed")
|
|
10
10
|
|
|
11
11
|
# Directories that are not part of the governed tree: tooling caches and, notably,
|
|
12
12
|
# test fixtures, which are self-contained sub-repositories validated on their own.
|
|
@@ -64,4 +64,3 @@ def discover_evidence_ids(root: Path) -> set[str]:
|
|
|
64
64
|
for path in sorted((root / "evidence" / "runs").glob("*.json")):
|
|
65
65
|
result.add(str(load_json(path).get("id", "")))
|
|
66
66
|
return result
|
|
67
|
-
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: repopact
|
|
3
|
-
Version: 2.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.0
|
|
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
|
|
|
@@ -71,7 +71,8 @@ intent -> scoped authority -> work item -> implementation -> evidence -> audit -
|
|
|
71
71
|
pip install repopact # the CLI + reference validator, from PyPI
|
|
72
72
|
repopact init --target ../your-repo # seed a valid RepoPact in a new repo
|
|
73
73
|
cd ../your-repo
|
|
74
|
-
repopact new work-item "Title of the work" # stamps
|
|
74
|
+
repopact new work-item "Title of the work" # stamps active work (incl. the preflight marker)
|
|
75
|
+
repopact new work-item "Candidate idea" --status proposed
|
|
75
76
|
repopact validate
|
|
76
77
|
repopact dashboard
|
|
77
78
|
```
|
|
@@ -110,7 +111,7 @@ idempotent.
|
|
|
110
111
|
|
|
111
112
|
## 2.0: mandatory preflight + provenance-typed records
|
|
112
113
|
|
|
113
|
-
Decision [`0021`](decisions/
|
|
114
|
+
Decision [`0021`](decisions/0021-preflight-mandatory-and-provenance.md) (supersedes 0018):
|
|
114
115
|
|
|
115
116
|
- **Mandatory preflight (default on).** No work begins until a work item exists and
|
|
116
117
|
propagates through the pact; `repopact new` stamps the marker. Existing repos grandfather
|
|
@@ -123,9 +124,17 @@ Decision [`0021`](decisions/0022-release-2.0.2-installed-seed-lookup.md) (supers
|
|
|
123
124
|
|
|
124
125
|
## Status is a filesystem transition
|
|
125
126
|
|
|
126
|
-
Work moves between
|
|
127
|
-
|
|
128
|
-
|
|
127
|
+
Work moves between lifecycle directories; its `work-item.json` status must match its
|
|
128
|
+
directory. Moving a work item never deletes its reasoning, decisions, or evidence links.
|
|
129
|
+
|
|
130
|
+
- `proposed`: captured candidate work that is not yet accepted or authorized for
|
|
131
|
+
implementation.
|
|
132
|
+
- `active`: accepted work authorized for design or implementation.
|
|
133
|
+
- `blocked`: accepted/current work that cannot proceed until a named condition changes.
|
|
134
|
+
- `deferred`: accepted work intentionally postponed with rationale.
|
|
135
|
+
- `completed`: delivered work whose acceptance criteria are evidence-closed.
|
|
136
|
+
|
|
137
|
+
Active and completed work cannot depend on proposed work as if it were accepted.
|
|
129
138
|
|
|
130
139
|
## Derive over declare
|
|
131
140
|
|
|
@@ -10,8 +10,10 @@ from __future__ import annotations
|
|
|
10
10
|
|
|
11
11
|
import argparse
|
|
12
12
|
import sys
|
|
13
|
-
from datetime import date
|
|
14
|
-
from pathlib import Path
|
|
13
|
+
from datetime import date
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from repo_model import STATUSES
|
|
15
17
|
|
|
16
18
|
|
|
17
19
|
def main(argv: list[str] | None = None) -> int:
|
|
@@ -50,10 +52,12 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
50
52
|
p_spec = sub.add_parser("spec", help="Regenerate SPEC.md derived blocks")
|
|
51
53
|
p_spec.add_argument("--root", type=Path, default=Path.cwd())
|
|
52
54
|
|
|
53
|
-
p_new = sub.add_parser("new", help="Stamp a new record from a template")
|
|
54
|
-
p_new.add_argument("kind", choices=["work-item", "decision", "policy"])
|
|
55
|
-
p_new.add_argument("title")
|
|
56
|
-
p_new.add_argument("--root", type=Path, default=Path.cwd())
|
|
55
|
+
p_new = sub.add_parser("new", help="Stamp a new record from a template")
|
|
56
|
+
p_new.add_argument("kind", choices=["work-item", "decision", "policy"])
|
|
57
|
+
p_new.add_argument("title")
|
|
58
|
+
p_new.add_argument("--root", type=Path, default=Path.cwd())
|
|
59
|
+
p_new.add_argument("--status", choices=STATUSES, default="active",
|
|
60
|
+
help="Lifecycle status for a new work item")
|
|
57
61
|
|
|
58
62
|
p_frz = sub.add_parser("check-frozen", help="Report frozen-surface changes in a diff range")
|
|
59
63
|
p_frz.add_argument("--root", type=Path, default=Path.cwd())
|
|
@@ -146,12 +150,11 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
146
150
|
else f"\nValidation failed with {len(problems)} error(s).")
|
|
147
151
|
return 1 if problems else 0
|
|
148
152
|
|
|
149
|
-
if args.command == "dashboard":
|
|
150
|
-
import generate_dashboard
|
|
151
|
-
out = root
|
|
152
|
-
out.
|
|
153
|
-
|
|
154
|
-
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
|
|
155
158
|
|
|
156
159
|
if args.command == "spec":
|
|
157
160
|
import generate_spec
|
|
@@ -169,10 +172,10 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
169
172
|
print("Generated SPEC.md derived blocks")
|
|
170
173
|
return 0
|
|
171
174
|
|
|
172
|
-
if args.command == "new":
|
|
173
|
-
import new
|
|
174
|
-
if args.kind == "work-item":
|
|
175
|
-
path = new.new_work_item(args.title, date.today(), root)
|
|
175
|
+
if args.command == "new":
|
|
176
|
+
import new
|
|
177
|
+
if args.kind == "work-item":
|
|
178
|
+
path = new.new_work_item(args.title, date.today(), root, status=args.status)
|
|
176
179
|
else:
|
|
177
180
|
path = new.new_markdown(args.kind, args.title, date.today(), root)
|
|
178
181
|
print(f"Created {path.relative_to(root)}")
|
|
@@ -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
|
|
@@ -197,7 +199,7 @@ def registry_scopes_inside(root: Path, d: Path) -> list[str]:
|
|
|
197
199
|
# File types we scan for inbound references; everything else (binaries, build output) is skipped.
|
|
198
200
|
_TEXT_EXTS = {".md", ".markdown", ".txt", ".rst", ".py", ".toml", ".yaml", ".yml",
|
|
199
201
|
".json", ".cfg", ".ini", ".sh", ".ps1"}
|
|
200
|
-
_LIFECYCLE = {"completed", "deferred", "active", "blocked"}
|
|
202
|
+
_LIFECYCLE = {"completed", "deferred", "active", "blocked", "proposed"}
|
|
201
203
|
_MD_LINK = re.compile(r"(\]\()([^)\s]+)(\))")
|
|
202
204
|
|
|
203
205
|
|
|
@@ -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 = {
|
|
@@ -381,11 +382,18 @@ def validate_work(root: Path, owner_scopes: set[str], enforce_disjoint: bool, pr
|
|
|
381
382
|
validate_work_preflight(item, manifest, preflight_cfg, problems)
|
|
382
383
|
validate_provenance(item, ev_prov, manifest, problems)
|
|
383
384
|
|
|
384
|
-
all_ids = set(seen)
|
|
385
|
-
for item in items
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
385
|
+
all_ids = set(seen)
|
|
386
|
+
status_by_id = {item.item_id: item.status for item in items}
|
|
387
|
+
for item in items:
|
|
388
|
+
for dependency in item.data.get("depends_on", []):
|
|
389
|
+
if dependency not in all_ids:
|
|
390
|
+
problems.append(Problem(item.directory / "work-item.json", f"unknown dependency '{dependency}'"))
|
|
391
|
+
elif item.status in ("active", "completed") and status_by_id.get(dependency) == "proposed":
|
|
392
|
+
problems.append(Problem(
|
|
393
|
+
item.directory / "work-item.json",
|
|
394
|
+
f"{item.status} work item depends on proposed work item '{dependency}'; "
|
|
395
|
+
"proposed work is not accepted implementation authority",
|
|
396
|
+
))
|
|
389
397
|
|
|
390
398
|
detect_dependency_cycles(items, all_ids, problems)
|
|
391
399
|
if enforce_disjoint:
|
|
@@ -466,7 +474,7 @@ def validate_evidence(root: Path, work_ids: set[str], problems: list[Problem]) -
|
|
|
466
474
|
problems.append(Problem(path, f"unknown work_item '{data['work_item']}'"))
|
|
467
475
|
|
|
468
476
|
|
|
469
|
-
def validate_audit_registry(root: Path, problems: list[Problem]) -> None:
|
|
477
|
+
def validate_audit_registry(root: Path, problems: list[Problem]) -> None:
|
|
470
478
|
path = root / "audits" / "registry.json"
|
|
471
479
|
try:
|
|
472
480
|
data = load_json(path)
|
|
@@ -478,7 +486,33 @@ def validate_audit_registry(root: Path, problems: list[Problem]) -> None:
|
|
|
478
486
|
if not scope_path.exists():
|
|
479
487
|
problems.append(Problem(path, f"audit scope does not exist: {entry.get('path')}"))
|
|
480
488
|
validate_dates(entry.get("last_reviewed"), "last_reviewed", path, problems)
|
|
481
|
-
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
|
+
))
|
|
482
516
|
|
|
483
517
|
|
|
484
518
|
def _validate_records(root: Path, directory: Path, pattern: str, statuses: tuple[str, ...],
|
|
@@ -587,10 +621,11 @@ def validate(root: Path) -> list[Problem]:
|
|
|
587
621
|
validate_findings(root, owner_scopes, problems)
|
|
588
622
|
work_ids = validate_work(root, owner_scopes, enforce_disjoint, problems)
|
|
589
623
|
validate_orphan_work_dirs(root, problems)
|
|
590
|
-
validate_evidence(root, work_ids, problems)
|
|
591
|
-
validate_audit_registry(root, problems)
|
|
592
|
-
validate_decisions(root, problems)
|
|
593
|
-
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)
|
|
594
629
|
return sorted(problems, key=lambda problem: (str(problem.path), problem.message))
|
|
595
630
|
|
|
596
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
|
|
@@ -48,34 +50,63 @@ class RepositoryValidationTests(unittest.TestCase):
|
|
|
48
50
|
mutate(data)
|
|
49
51
|
path.write_text(json.dumps(data), encoding="utf-8")
|
|
50
52
|
|
|
51
|
-
def
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
53
|
+
def add_work_item(
|
|
54
|
+
self,
|
|
55
|
+
item_id: str,
|
|
56
|
+
status: str = "active",
|
|
57
|
+
owner_scope: str = "work",
|
|
58
|
+
preflight: bool = True,
|
|
59
|
+
) -> None:
|
|
60
|
+
directory = self.root / "work" / status / f"{item_id}-probe"
|
|
61
|
+
directory.mkdir(parents=True)
|
|
62
|
+
(directory / "README.md").write_text("# probe\n", encoding="utf-8")
|
|
63
|
+
data = {
|
|
64
|
+
"id": item_id,
|
|
65
|
+
"title": "probe",
|
|
66
|
+
"status": status,
|
|
67
|
+
"owner_scope": owner_scope,
|
|
68
|
+
"affected_scopes": [],
|
|
69
|
+
"depends_on": [],
|
|
62
70
|
"provenance": "concrete",
|
|
63
71
|
"acceptance_criteria": [{"id": "AC-1", "text": "x", "state": "pending", "evidence": []}],
|
|
64
72
|
"created": "2026-06-15",
|
|
65
73
|
"updated": "2026-06-15",
|
|
66
74
|
}
|
|
67
|
-
if preflight:
|
|
75
|
+
if preflight:
|
|
68
76
|
data["preflight"] = {
|
|
69
77
|
"created_before_work_started": True,
|
|
70
78
|
"created_at": "2026-06-15T00:00:00Z",
|
|
71
79
|
"note": "probe",
|
|
72
|
-
}
|
|
73
|
-
(directory / "work-item.json").write_text(json.dumps(data), encoding="utf-8")
|
|
74
|
-
|
|
80
|
+
}
|
|
81
|
+
(directory / "work-item.json").write_text(json.dumps(data), encoding="utf-8")
|
|
82
|
+
generate_dashboard.write_dashboard(self.root)
|
|
83
|
+
|
|
84
|
+
def add_active_item(self, item_id: str, owner_scope: str = "work", preflight: bool = True) -> None:
|
|
85
|
+
self.add_work_item(item_id, "active", owner_scope, preflight)
|
|
86
|
+
|
|
75
87
|
# --- baseline -----------------------------------------------------------
|
|
76
88
|
|
|
77
|
-
def test_repository_is_valid(self) -> None:
|
|
78
|
-
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)
|
|
79
110
|
|
|
80
111
|
# --- work lifecycle -----------------------------------------------------
|
|
81
112
|
|
|
@@ -90,10 +121,27 @@ class RepositoryValidationTests(unittest.TestCase):
|
|
|
90
121
|
self.write_json(path, lambda d: (d["acceptance_criteria"][0].update({"state": "satisfied", "evidence": []})))
|
|
91
122
|
self.assertTrue(any("satisfied without evidence" in v for v in self.problems()))
|
|
92
123
|
|
|
93
|
-
def test_completed_item_cannot_have_pending_criteria(self) -> None:
|
|
94
|
-
path = self.manifest()
|
|
95
|
-
self.write_json(path, lambda d: (d["acceptance_criteria"][0].update({"state": "pending", "evidence": []})))
|
|
96
|
-
self.assertTrue(any("completed item has pending criterion" in v for v in self.problems()))
|
|
124
|
+
def test_completed_item_cannot_have_pending_criteria(self) -> None:
|
|
125
|
+
path = self.manifest()
|
|
126
|
+
self.write_json(path, lambda d: (d["acceptance_criteria"][0].update({"state": "pending", "evidence": []})))
|
|
127
|
+
self.assertTrue(any("completed item has pending criterion" in v for v in self.problems()))
|
|
128
|
+
|
|
129
|
+
def test_valid_proposed_work_item_validates(self) -> None:
|
|
130
|
+
self.add_work_item("900", status="proposed")
|
|
131
|
+
self.assertEqual([], self.problems())
|
|
132
|
+
|
|
133
|
+
def test_invalid_proposed_work_item_fails_structure_validation(self) -> None:
|
|
134
|
+
self.add_work_item("900", status="proposed")
|
|
135
|
+
path = self.root / "work" / "proposed" / "900-probe" / "work-item.json"
|
|
136
|
+
self.write_json(path, lambda d: d.pop("title"))
|
|
137
|
+
self.assertTrue(any("missing fields: title" in v for v in self.problems()))
|
|
138
|
+
|
|
139
|
+
def test_active_work_cannot_depend_on_proposed_work(self) -> None:
|
|
140
|
+
self.add_work_item("900", status="proposed")
|
|
141
|
+
self.add_active_item("901")
|
|
142
|
+
active = self.root / "work" / "active" / "901-probe" / "work-item.json"
|
|
143
|
+
self.write_json(active, lambda d: d.__setitem__("depends_on", ["900"]))
|
|
144
|
+
self.assertTrue(any("depends on proposed work item '900'" in v for v in self.problems()))
|
|
97
145
|
|
|
98
146
|
def test_readme_checkbox_parity_flags_contradiction(self) -> None:
|
|
99
147
|
path = self.manifest()
|
|
@@ -398,10 +446,11 @@ class RepositoryValidationTests(unittest.TestCase):
|
|
|
398
446
|
|
|
399
447
|
# --- bootstrap (003 B1) -------------------------------------------------
|
|
400
448
|
|
|
401
|
-
def test_bootstrap_produces_valid_repo(self) -> None:
|
|
402
|
-
target = Path(self.temp.name) / "seeded"
|
|
403
|
-
init_repo.bootstrap(target)
|
|
404
|
-
self.
|
|
449
|
+
def test_bootstrap_produces_valid_repo(self) -> None:
|
|
450
|
+
target = Path(self.temp.name) / "seeded"
|
|
451
|
+
init_repo.bootstrap(target)
|
|
452
|
+
self.assertTrue((target / "work" / "proposed").is_dir())
|
|
453
|
+
self.assertEqual([], [p.message for p in validate(target)])
|
|
405
454
|
|
|
406
455
|
# --- SPEC generator determinism (004) ----------------------------------
|
|
407
456
|
|
|
@@ -418,12 +467,21 @@ class RepositoryValidationTests(unittest.TestCase):
|
|
|
418
467
|
def test_cli_validate_returns_zero_on_valid_repo(self) -> None:
|
|
419
468
|
self.assertEqual(0, repopact_cli.main(["validate", "--root", str(self.root)]))
|
|
420
469
|
|
|
421
|
-
def test_cli_new_stamps_a_valid_record(self) -> None:
|
|
422
|
-
rc = repopact_cli.main(["new", "work-item", "Cli Probe", "--root", str(self.root)])
|
|
423
|
-
self.assertEqual(0, rc)
|
|
424
|
-
stamped = list((self.root / "work" / "active").glob("*-cli-probe/work-item.json"))
|
|
425
|
-
self.assertEqual(1, len(stamped))
|
|
426
|
-
self.assertEqual([], [p.message for p in validate(self.root)])
|
|
470
|
+
def test_cli_new_stamps_a_valid_record(self) -> None:
|
|
471
|
+
rc = repopact_cli.main(["new", "work-item", "Cli Probe", "--root", str(self.root)])
|
|
472
|
+
self.assertEqual(0, rc)
|
|
473
|
+
stamped = list((self.root / "work" / "active").glob("*-cli-probe/work-item.json"))
|
|
474
|
+
self.assertEqual(1, len(stamped))
|
|
475
|
+
self.assertEqual([], [p.message for p in validate(self.root)])
|
|
476
|
+
|
|
477
|
+
def test_cli_new_can_stamp_proposed_work_item(self) -> None:
|
|
478
|
+
rc = repopact_cli.main(["new", "work-item", "Cli Proposal", "--status", "proposed", "--root", str(self.root)])
|
|
479
|
+
self.assertEqual(0, rc)
|
|
480
|
+
stamped = list((self.root / "work" / "proposed").glob("*-cli-proposal/work-item.json"))
|
|
481
|
+
self.assertEqual(1, len(stamped))
|
|
482
|
+
data = json.loads(stamped[0].read_text(encoding="utf-8"))
|
|
483
|
+
self.assertEqual("proposed", data["status"])
|
|
484
|
+
self.assertEqual([], [p.message for p in validate(self.root)])
|
|
427
485
|
|
|
428
486
|
# --- proving-ground hardening (007) -------------------------------------
|
|
429
487
|
|
|
@@ -668,8 +726,9 @@ class RepositoryValidationTests(unittest.TestCase):
|
|
|
668
726
|
"contract": "todos/12-search/AGENTS.md", "last_reviewed": "2026-06-15",
|
|
669
727
|
"next_review": "2026-09-13", "alignment": "current", "notes": "nested",
|
|
670
728
|
})
|
|
671
|
-
(repo / "audits" / "registry.json").write_text(json.dumps(reg, indent=2) + "\n", encoding="utf-8")
|
|
672
|
-
|
|
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)
|
|
673
732
|
self.assertEqual([], report["retired"]) # not retired
|
|
674
733
|
self.assertTrue((repo / "todos").exists())
|
|
675
734
|
self.assertTrue(any(b["dir"] == "todos" for b in report["blocked"]))
|
repopact-2.0.2/VERSION
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
2.0.2
|
|
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
|