repopact 2.0.2__tar.gz → 2.1.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.0.2 → repopact-2.1.0}/PKG-INFO +17 -8
  2. {repopact-2.0.2 → repopact-2.1.0}/README.md +16 -7
  3. repopact-2.1.0/VERSION +1 -0
  4. {repopact-2.0.2 → repopact-2.1.0}/schemas/work-item.schema.json +1 -2
  5. {repopact-2.0.2 → repopact-2.1.0}/scripts/init_repo.py +1 -1
  6. {repopact-2.0.2 → repopact-2.1.0}/scripts/new.py +10 -4
  7. {repopact-2.0.2 → repopact-2.1.0}/scripts/plan_import.py +11 -4
  8. {repopact-2.0.2 → repopact-2.1.0}/scripts/repo_model.py +1 -2
  9. {repopact-2.0.2 → repopact-2.1.0}/scripts/repopact.egg-info/PKG-INFO +17 -8
  10. {repopact-2.0.2 → repopact-2.1.0}/scripts/repopact_cli.py +14 -10
  11. {repopact-2.0.2 → repopact-2.1.0}/scripts/takeover.py +1 -1
  12. {repopact-2.0.2 → repopact-2.1.0}/scripts/validate_repo.py +12 -5
  13. {repopact-2.0.2 → repopact-2.1.0}/tests/test_validate_repo.py +64 -28
  14. repopact-2.0.2/VERSION +0 -1
  15. {repopact-2.0.2 → repopact-2.1.0}/LICENSE +0 -0
  16. {repopact-2.0.2 → repopact-2.1.0}/pyproject.toml +0 -0
  17. {repopact-2.0.2 → repopact-2.1.0}/schemas/audit-finding.schema.json +0 -0
  18. {repopact-2.0.2 → repopact-2.1.0}/schemas/conformance-manifest.schema.json +0 -0
  19. {repopact-2.0.2 → repopact-2.1.0}/schemas/evidence-run.schema.json +0 -0
  20. {repopact-2.0.2 → repopact-2.1.0}/schemas/frozen-surface.schema.json +0 -0
  21. {repopact-2.0.2 → repopact-2.1.0}/schemas/invariants.schema.json +0 -0
  22. {repopact-2.0.2 → repopact-2.1.0}/schemas/record-frontmatter.schema.json +0 -0
  23. {repopact-2.0.2 → repopact-2.1.0}/scripts/adopt_repo.py +0 -0
  24. {repopact-2.0.2 → repopact-2.1.0}/scripts/check_frozen_surface.py +0 -0
  25. {repopact-2.0.2 → repopact-2.1.0}/scripts/doctor.py +0 -0
  26. {repopact-2.0.2 → repopact-2.1.0}/scripts/frontmatter.py +0 -0
  27. {repopact-2.0.2 → repopact-2.1.0}/scripts/generate_dashboard.py +0 -0
  28. {repopact-2.0.2 → repopact-2.1.0}/scripts/generate_spec.py +0 -0
  29. {repopact-2.0.2 → repopact-2.1.0}/scripts/repopact.egg-info/SOURCES.txt +0 -0
  30. {repopact-2.0.2 → repopact-2.1.0}/scripts/repopact.egg-info/dependency_links.txt +0 -0
  31. {repopact-2.0.2 → repopact-2.1.0}/scripts/repopact.egg-info/entry_points.txt +0 -0
  32. {repopact-2.0.2 → repopact-2.1.0}/scripts/repopact.egg-info/requires.txt +0 -0
  33. {repopact-2.0.2 → repopact-2.1.0}/scripts/repopact.egg-info/top_level.txt +0 -0
  34. {repopact-2.0.2 → repopact-2.1.0}/scripts/run_conformance.py +0 -0
  35. {repopact-2.0.2 → repopact-2.1.0}/scripts/track_import.py +0 -0
  36. {repopact-2.0.2 → repopact-2.1.0}/setup.cfg +0 -0
  37. {repopact-2.0.2 → repopact-2.1.0}/templates/README.md +0 -0
  38. {repopact-2.0.2 → repopact-2.1.0}/templates/decision.md +0 -0
  39. {repopact-2.0.2 → repopact-2.1.0}/templates/evidence-run.json +0 -0
  40. {repopact-2.0.2 → repopact-2.1.0}/templates/policy.md +0 -0
  41. {repopact-2.0.2 → repopact-2.1.0}/templates/work-item.README.md +0 -0
  42. {repopact-2.0.2 → repopact-2.1.0}/templates/work-item.json +0 -0
  43. {repopact-2.0.2 → repopact-2.1.0}/tests/test_conformance.py +0 -0
  44. {repopact-2.0.2 → repopact-2.1.0}/tests/test_plan_import_links.py +0 -0
  45. {repopact-2.0.2 → repopact-2.1.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.2
3
+ Version: 2.1.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.2**
26
- ([changelog](decisions/0022-release-2.0.2-installed-seed-lookup.md)).
25
+ `pip install repopact` · Apache-2.0 · current release **2.1.0**
26
+ ([changelog](decisions/0024-release-2.1.0-proposed-lifecycle.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 records (incl. the preflight marker)
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/0022-release-2.0.2-installed-seed-lookup.md) (supersedes 0018):
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 `work/active`, `work/blocked`, `work/deferred`, and `work/completed`; its
127
- `work-item.json` status must match its directory. Moving a work item never deletes its
128
- reasoning, decisions, or evidence links.
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.2**
13
- ([changelog](decisions/0022-release-2.0.2-installed-seed-lookup.md)).
12
+ `pip install repopact` · Apache-2.0 · current release **2.1.0**
13
+ ([changelog](decisions/0024-release-2.1.0-proposed-lifecycle.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 records (incl. the preflight marker)
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/0022-release-2.0.2-installed-seed-lookup.md) (supersedes 0018):
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 `work/active`, `work/blocked`, `work/deferred`, and `work/completed`; its
114
- `work-item.json` status must match its directory. Moving a work item never deletes its
115
- reasoning, decisions, or evidence links.
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.1.0/VERSION ADDED
@@ -0,0 +1 @@
1
+ 2.1.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
-
@@ -18,7 +18,7 @@ from pathlib import Path
18
18
 
19
19
  HERE = Path(__file__).resolve().parent # directory holding the tooling modules
20
20
  CHECKOUT = HERE.parent # repo root when running from a checkout
21
- LIFECYCLE = ("active", "blocked", "deferred", "completed")
21
+ LIFECYCLE = ("proposed", "active", "blocked", "deferred", "completed")
22
22
  MODULES = (
23
23
  "repo_model.py", "validate_repo.py", "generate_dashboard.py", "generate_spec.py",
24
24
  "init_repo.py", "new.py", "check_frozen_surface.py", "frontmatter.py", "repopact_cli.py",
@@ -16,6 +16,8 @@ import sys
16
16
  from datetime import date, datetime
17
17
  from pathlib import Path
18
18
 
19
+ from repo_model import STATUSES
20
+
19
21
  ROOT = Path(__file__).resolve().parents[1]
20
22
 
21
23
 
@@ -32,11 +34,13 @@ def _next_numeric(paths: list[Path], width: int, start: int) -> str:
32
34
  return str(max(used + [start - 1]) + 1).zfill(width)
33
35
 
34
36
 
35
- def new_work_item(title: str, today: date, root: Path = ROOT) -> Path:
37
+ def new_work_item(title: str, today: date, root: Path = ROOT, status: str = "active") -> Path:
38
+ if status not in STATUSES:
39
+ raise ValueError(f"unknown work-item status '{status}'")
36
40
  slug = slugify(title)
37
41
  existing = list((root / "work").glob("*/*/work-item.json"))
38
42
  item_id = _next_numeric([p.parent for p in existing], 3, 1)
39
- directory = root / "work" / "active" / f"{item_id}-{slug}"
43
+ directory = root / "work" / status / f"{item_id}-{slug}"
40
44
  directory.mkdir(parents=True)
41
45
  # Mandatory preflight (decision 0021): creating the work item with `new` IS the
42
46
  # preflight act, so stamp the marker and concrete provenance up front. No work
@@ -44,7 +48,7 @@ def new_work_item(title: str, today: date, root: Path = ROOT) -> Path:
44
48
  created_at = datetime.combine(today, datetime.min.time()).isoformat() + "Z"
45
49
  manifest = {
46
50
  "$schema": "../../../schemas/work-item.schema.json",
47
- "id": item_id, "title": title, "status": "active",
51
+ "id": item_id, "title": title, "status": status,
48
52
  "owner_scope": "governance", "affected_scopes": [], "depends_on": [],
49
53
  "provenance": "concrete",
50
54
  "preflight": {
@@ -83,10 +87,12 @@ def main() -> int:
83
87
  parser = argparse.ArgumentParser(description="Stamp a new record from a template")
84
88
  parser.add_argument("kind", choices=["work-item", "decision", "policy"])
85
89
  parser.add_argument("title")
90
+ parser.add_argument("--status", choices=STATUSES, default="active",
91
+ help="Lifecycle status for a new work item")
86
92
  args = parser.parse_args()
87
93
  today = date.today()
88
94
  if args.kind == "work-item":
89
- path = new_work_item(args.title, today)
95
+ path = new_work_item(args.title, today, status=args.status)
90
96
  else:
91
97
  path = new_markdown(args.kind, args.title, today)
92
98
  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
 
@@ -33,16 +33,23 @@ import adopt_repo # reuse Report and _slug
33
33
 
34
34
  PLAN_DIR_NAMES = ("todos", "todo", "tasks", "plan", "planning", "backlog")
35
35
  CHECKLIST_FILES = ("TODO.md", "TODOS.md", "ROADMAP.md", "BACKLOG.md", "PLAN.md", "TASKS.md")
36
- LIFECYCLE_DIRS = {"completed", "deferred", "blocked", "active"}
36
+ LIFECYCLE_DIRS = {"proposed", "completed", "deferred", "blocked", "active"}
37
37
 
38
38
  # source lifecycle -> (work status, acceptance-criterion state)
39
39
  _STATUS = {
40
+ "proposed": ("proposed", "pending"),
40
41
  "completed": ("completed", "waived"),
41
42
  "deferred": ("deferred", "pending"),
42
43
  "blocked": ("blocked", "pending"),
43
44
  "active": ("active", "pending"),
44
45
  }
45
- _EMOJI = {"completed": "✅ Complete", "deferred": "⏸️ Deferred", "blocked": "⛔ Blocked", "active": "📋 Active"}
46
+ _EMOJI = {
47
+ "proposed": "Proposed",
48
+ "completed": "✅ Complete",
49
+ "deferred": "⏸️ Deferred",
50
+ "blocked": "⛔ Blocked",
51
+ "active": "📋 Active",
52
+ }
46
53
 
47
54
  _SKIP_DIRS = {".git", ".venv", "node_modules", "__pycache__"}
48
55
  # Index/template/readme files in a plan directory are scaffolding, not plan items.
@@ -55,7 +62,7 @@ def _is_scaffold(name: str) -> bool:
55
62
 
56
63
  @dataclass
57
64
  class PlanItem:
58
- lifecycle: str # completed | deferred | blocked | active
65
+ lifecycle: str # proposed | completed | deferred | blocked | active
59
66
  slug: str # RepoPact slug (no leading number)
60
67
  title: str
61
68
  narrative: str # markdown body to preserve
@@ -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.2
3
+ Version: 2.1.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.2**
26
- ([changelog](decisions/0022-release-2.0.2-installed-seed-lookup.md)).
25
+ `pip install repopact` · Apache-2.0 · current release **2.1.0**
26
+ ([changelog](decisions/0024-release-2.1.0-proposed-lifecycle.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 records (incl. the preflight marker)
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/0022-release-2.0.2-installed-seed-lookup.md) (supersedes 0018):
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 `work/active`, `work/blocked`, `work/deferred`, and `work/completed`; its
127
- `work-item.json` status must match its directory. Moving a work item never deletes its
128
- reasoning, decisions, or evidence links.
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())
@@ -169,10 +173,10 @@ def main(argv: list[str] | None = None) -> int:
169
173
  print("Generated SPEC.md derived blocks")
170
174
  return 0
171
175
 
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)
176
+ if args.command == "new":
177
+ import new
178
+ if args.kind == "work-item":
179
+ path = new.new_work_item(args.title, date.today(), root, status=args.status)
176
180
  else:
177
181
  path = new.new_markdown(args.kind, args.title, date.today(), root)
178
182
  print(f"Created {path.relative_to(root)}")
@@ -197,7 +197,7 @@ def registry_scopes_inside(root: Path, d: Path) -> list[str]:
197
197
  # File types we scan for inbound references; everything else (binaries, build output) is skipped.
198
198
  _TEXT_EXTS = {".md", ".markdown", ".txt", ".rst", ".py", ".toml", ".yaml", ".yml",
199
199
  ".json", ".cfg", ".ini", ".sh", ".ps1"}
200
- _LIFECYCLE = {"completed", "deferred", "active", "blocked"}
200
+ _LIFECYCLE = {"completed", "deferred", "active", "blocked", "proposed"}
201
201
  _MD_LINK = re.compile(r"(\]\()([^)\s]+)(\))")
202
202
 
203
203
 
@@ -381,11 +381,18 @@ def validate_work(root: Path, owner_scopes: set[str], enforce_disjoint: bool, pr
381
381
  validate_work_preflight(item, manifest, preflight_cfg, problems)
382
382
  validate_provenance(item, ev_prov, manifest, problems)
383
383
 
384
- all_ids = set(seen)
385
- for item in items:
386
- for dependency in item.data.get("depends_on", []):
387
- if dependency not in all_ids:
388
- problems.append(Problem(item.directory / "work-item.json", f"unknown dependency '{dependency}'"))
384
+ all_ids = set(seen)
385
+ status_by_id = {item.item_id: item.status for item in items}
386
+ for item in items:
387
+ for dependency in item.data.get("depends_on", []):
388
+ if dependency not in all_ids:
389
+ problems.append(Problem(item.directory / "work-item.json", f"unknown dependency '{dependency}'"))
390
+ elif item.status in ("active", "completed") and status_by_id.get(dependency) == "proposed":
391
+ problems.append(Problem(
392
+ item.directory / "work-item.json",
393
+ f"{item.status} work item depends on proposed work item '{dependency}'; "
394
+ "proposed work is not accepted implementation authority",
395
+ ))
389
396
 
390
397
  detect_dependency_cycles(items, all_ids, problems)
391
398
  if enforce_disjoint:
@@ -48,17 +48,23 @@ class RepositoryValidationTests(unittest.TestCase):
48
48
  mutate(data)
49
49
  path.write_text(json.dumps(data), encoding="utf-8")
50
50
 
51
- def add_active_item(self, item_id: str, owner_scope: str = "work", preflight: bool = True) -> None:
52
- directory = self.root / "work" / "active" / f"{item_id}-probe"
53
- directory.mkdir(parents=True)
54
- (directory / "README.md").write_text("# probe\n", encoding="utf-8")
55
- data = {
56
- "id": item_id,
57
- "title": "probe",
58
- "status": "active",
59
- "owner_scope": owner_scope,
60
- "affected_scopes": [],
61
- "depends_on": [],
51
+ def add_work_item(
52
+ self,
53
+ item_id: str,
54
+ status: str = "active",
55
+ owner_scope: str = "work",
56
+ preflight: bool = True,
57
+ ) -> None:
58
+ directory = self.root / "work" / status / f"{item_id}-probe"
59
+ directory.mkdir(parents=True)
60
+ (directory / "README.md").write_text("# probe\n", encoding="utf-8")
61
+ data = {
62
+ "id": item_id,
63
+ "title": "probe",
64
+ "status": status,
65
+ "owner_scope": owner_scope,
66
+ "affected_scopes": [],
67
+ "depends_on": [],
62
68
  "provenance": "concrete",
63
69
  "acceptance_criteria": [{"id": "AC-1", "text": "x", "state": "pending", "evidence": []}],
64
70
  "created": "2026-06-15",
@@ -69,9 +75,12 @@ class RepositoryValidationTests(unittest.TestCase):
69
75
  "created_before_work_started": True,
70
76
  "created_at": "2026-06-15T00:00:00Z",
71
77
  "note": "probe",
72
- }
73
- (directory / "work-item.json").write_text(json.dumps(data), encoding="utf-8")
74
-
78
+ }
79
+ (directory / "work-item.json").write_text(json.dumps(data), encoding="utf-8")
80
+
81
+ def add_active_item(self, item_id: str, owner_scope: str = "work", preflight: bool = True) -> None:
82
+ self.add_work_item(item_id, "active", owner_scope, preflight)
83
+
75
84
  # --- baseline -----------------------------------------------------------
76
85
 
77
86
  def test_repository_is_valid(self) -> None:
@@ -90,10 +99,27 @@ class RepositoryValidationTests(unittest.TestCase):
90
99
  self.write_json(path, lambda d: (d["acceptance_criteria"][0].update({"state": "satisfied", "evidence": []})))
91
100
  self.assertTrue(any("satisfied without evidence" in v for v in self.problems()))
92
101
 
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()))
102
+ def test_completed_item_cannot_have_pending_criteria(self) -> None:
103
+ path = self.manifest()
104
+ self.write_json(path, lambda d: (d["acceptance_criteria"][0].update({"state": "pending", "evidence": []})))
105
+ self.assertTrue(any("completed item has pending criterion" in v for v in self.problems()))
106
+
107
+ def test_valid_proposed_work_item_validates(self) -> None:
108
+ self.add_work_item("900", status="proposed")
109
+ self.assertEqual([], self.problems())
110
+
111
+ def test_invalid_proposed_work_item_fails_structure_validation(self) -> None:
112
+ self.add_work_item("900", status="proposed")
113
+ path = self.root / "work" / "proposed" / "900-probe" / "work-item.json"
114
+ self.write_json(path, lambda d: d.pop("title"))
115
+ self.assertTrue(any("missing fields: title" in v for v in self.problems()))
116
+
117
+ def test_active_work_cannot_depend_on_proposed_work(self) -> None:
118
+ self.add_work_item("900", status="proposed")
119
+ self.add_active_item("901")
120
+ active = self.root / "work" / "active" / "901-probe" / "work-item.json"
121
+ self.write_json(active, lambda d: d.__setitem__("depends_on", ["900"]))
122
+ self.assertTrue(any("depends on proposed work item '900'" in v for v in self.problems()))
97
123
 
98
124
  def test_readme_checkbox_parity_flags_contradiction(self) -> None:
99
125
  path = self.manifest()
@@ -398,10 +424,11 @@ class RepositoryValidationTests(unittest.TestCase):
398
424
 
399
425
  # --- bootstrap (003 B1) -------------------------------------------------
400
426
 
401
- def test_bootstrap_produces_valid_repo(self) -> None:
402
- target = Path(self.temp.name) / "seeded"
403
- init_repo.bootstrap(target)
404
- self.assertEqual([], [p.message for p in validate(target)])
427
+ def test_bootstrap_produces_valid_repo(self) -> None:
428
+ target = Path(self.temp.name) / "seeded"
429
+ init_repo.bootstrap(target)
430
+ self.assertTrue((target / "work" / "proposed").is_dir())
431
+ self.assertEqual([], [p.message for p in validate(target)])
405
432
 
406
433
  # --- SPEC generator determinism (004) ----------------------------------
407
434
 
@@ -418,12 +445,21 @@ class RepositoryValidationTests(unittest.TestCase):
418
445
  def test_cli_validate_returns_zero_on_valid_repo(self) -> None:
419
446
  self.assertEqual(0, repopact_cli.main(["validate", "--root", str(self.root)]))
420
447
 
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)])
448
+ def test_cli_new_stamps_a_valid_record(self) -> None:
449
+ rc = repopact_cli.main(["new", "work-item", "Cli Probe", "--root", str(self.root)])
450
+ self.assertEqual(0, rc)
451
+ stamped = list((self.root / "work" / "active").glob("*-cli-probe/work-item.json"))
452
+ self.assertEqual(1, len(stamped))
453
+ self.assertEqual([], [p.message for p in validate(self.root)])
454
+
455
+ def test_cli_new_can_stamp_proposed_work_item(self) -> None:
456
+ rc = repopact_cli.main(["new", "work-item", "Cli Proposal", "--status", "proposed", "--root", str(self.root)])
457
+ self.assertEqual(0, rc)
458
+ stamped = list((self.root / "work" / "proposed").glob("*-cli-proposal/work-item.json"))
459
+ self.assertEqual(1, len(stamped))
460
+ data = json.loads(stamped[0].read_text(encoding="utf-8"))
461
+ self.assertEqual("proposed", data["status"])
462
+ self.assertEqual([], [p.message for p in validate(self.root)])
427
463
 
428
464
  # --- proving-ground hardening (007) -------------------------------------
429
465
 
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