repopact 2.0.1__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.
- {repopact-2.0.1 → repopact-2.1.0}/PKG-INFO +26 -9
- {repopact-2.0.1 → repopact-2.1.0}/README.md +161 -144
- repopact-2.1.0/VERSION +1 -0
- {repopact-2.0.1 → repopact-2.1.0}/pyproject.toml +7 -6
- repopact-2.1.0/schemas/conformance-manifest.schema.json +34 -0
- {repopact-2.0.1 → repopact-2.1.0}/schemas/work-item.schema.json +1 -2
- {repopact-2.0.1 → repopact-2.1.0}/scripts/init_repo.py +12 -8
- {repopact-2.0.1 → repopact-2.1.0}/scripts/new.py +10 -4
- {repopact-2.0.1 → repopact-2.1.0}/scripts/plan_import.py +11 -4
- {repopact-2.0.1 → repopact-2.1.0}/scripts/repo_model.py +1 -2
- {repopact-2.0.1 → repopact-2.1.0}/scripts/repopact.egg-info/PKG-INFO +26 -9
- {repopact-2.0.1 → repopact-2.1.0}/scripts/repopact.egg-info/SOURCES.txt +2 -0
- {repopact-2.0.1 → repopact-2.1.0}/scripts/repopact.egg-info/top_level.txt +1 -0
- {repopact-2.0.1 → repopact-2.1.0}/scripts/repopact_cli.py +14 -10
- repopact-2.1.0/scripts/run_conformance.py +112 -0
- {repopact-2.0.1 → repopact-2.1.0}/scripts/takeover.py +1 -1
- {repopact-2.0.1 → repopact-2.1.0}/scripts/validate_repo.py +12 -5
- {repopact-2.0.1 → repopact-2.1.0}/tests/test_conformance.py +27 -6
- {repopact-2.0.1 → repopact-2.1.0}/tests/test_validate_repo.py +64 -28
- repopact-2.0.1/VERSION +0 -1
- {repopact-2.0.1 → repopact-2.1.0}/LICENSE +0 -0
- {repopact-2.0.1 → repopact-2.1.0}/schemas/audit-finding.schema.json +0 -0
- {repopact-2.0.1 → repopact-2.1.0}/schemas/evidence-run.schema.json +0 -0
- {repopact-2.0.1 → repopact-2.1.0}/schemas/frozen-surface.schema.json +0 -0
- {repopact-2.0.1 → repopact-2.1.0}/schemas/invariants.schema.json +0 -0
- {repopact-2.0.1 → repopact-2.1.0}/schemas/record-frontmatter.schema.json +0 -0
- {repopact-2.0.1 → repopact-2.1.0}/scripts/adopt_repo.py +0 -0
- {repopact-2.0.1 → repopact-2.1.0}/scripts/check_frozen_surface.py +0 -0
- {repopact-2.0.1 → repopact-2.1.0}/scripts/doctor.py +0 -0
- {repopact-2.0.1 → repopact-2.1.0}/scripts/frontmatter.py +0 -0
- {repopact-2.0.1 → repopact-2.1.0}/scripts/generate_dashboard.py +0 -0
- {repopact-2.0.1 → repopact-2.1.0}/scripts/generate_spec.py +0 -0
- {repopact-2.0.1 → repopact-2.1.0}/scripts/repopact.egg-info/dependency_links.txt +0 -0
- {repopact-2.0.1 → repopact-2.1.0}/scripts/repopact.egg-info/entry_points.txt +0 -0
- {repopact-2.0.1 → repopact-2.1.0}/scripts/repopact.egg-info/requires.txt +0 -0
- {repopact-2.0.1 → repopact-2.1.0}/scripts/track_import.py +0 -0
- {repopact-2.0.1 → repopact-2.1.0}/setup.cfg +0 -0
- {repopact-2.0.1 → repopact-2.1.0}/templates/README.md +0 -0
- {repopact-2.0.1 → repopact-2.1.0}/templates/decision.md +0 -0
- {repopact-2.0.1 → repopact-2.1.0}/templates/evidence-run.json +0 -0
- {repopact-2.0.1 → repopact-2.1.0}/templates/policy.md +0 -0
- {repopact-2.0.1 → repopact-2.1.0}/templates/work-item.README.md +0 -0
- {repopact-2.0.1 → repopact-2.1.0}/templates/work-item.json +0 -0
- {repopact-2.0.1 → repopact-2.1.0}/tests/test_plan_import_links.py +0 -0
- {repopact-2.0.1 → 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
|
|
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,15 +22,15 @@ 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.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
|
|
|
30
30
|
`AGENTS.md` (and `CLAUDE.md`, editor rules) tell an agent *what to do* — they're
|
|
31
31
|
instructions, plain Markdown, with no enforcement. RepoPact is the layer **above** them:
|
|
32
32
|
|
|
33
|
-
> `AGENTS.md` tells
|
|
33
|
+
> `AGENTS.md` tells an agent how to behave. RepoPact enforces and records whether the work respected the contract.
|
|
34
34
|
|
|
35
35
|
RepoPact's distinguishing primitive is the **binding invariant** — a declared guarantee with
|
|
36
36
|
a rationale, an escalation path, and (where its logical type permits) a machine enforcer.
|
|
@@ -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
|
```
|
|
@@ -83,6 +84,14 @@ repopact dashboard
|
|
|
83
84
|
[`AGENTS.md`](AGENTS.md), then [`governance/charter.md`](governance/charter.md) and
|
|
84
85
|
[`governance/workflow.md`](governance/workflow.md).
|
|
85
86
|
|
|
87
|
+
Alternative implementations can run the published conformance suite:
|
|
88
|
+
|
|
89
|
+
```powershell
|
|
90
|
+
python scripts/run_conformance.py --command "your-validator --root {repo}"
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
See [`CONFORMANCE.md`](CONFORMANCE.md) and [`conformance/`](conformance/).
|
|
94
|
+
|
|
86
95
|
## Adopt an *existing* repository
|
|
87
96
|
|
|
88
97
|
For a project that already has CODEOWNERS, CI workflows, and nested `AGENTS.md` contracts,
|
|
@@ -115,9 +124,17 @@ Decision [`0021`](decisions/0021-preflight-mandatory-and-provenance.md) (superse
|
|
|
115
124
|
|
|
116
125
|
## Status is a filesystem transition
|
|
117
126
|
|
|
118
|
-
Work moves between
|
|
119
|
-
|
|
120
|
-
|
|
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.
|
|
121
138
|
|
|
122
139
|
## Derive over declare
|
|
123
140
|
|
|
@@ -132,7 +149,7 @@ holds a [formal model](research/formal-model.md) (the L0–L5 kernel, the typed
|
|
|
132
149
|
lattice, the adoption trilemma), the pre-registered [experiment protocol](research/protocol.md)
|
|
133
150
|
and [benchmark protocol](research/benchmark-protocol.md) (hypotheses H1–H13, falsification
|
|
134
151
|
criteria, [threats to validity](research/threats-to-validity.md)), a [findings
|
|
135
|
-
register](research/findings.md), and
|
|
152
|
+
register](research/findings.md), and the current [paper](research/paper.md).
|
|
136
153
|
|
|
137
154
|
**PactBench** — the runnable benchmark suite (pre-registered tasks measuring whether RepoPact
|
|
138
155
|
enforcement reduces silent guarantee drift, with a model-agnostic harness and an S5 drift
|
|
@@ -1,144 +1,161 @@
|
|
|
1
|
-
# RepoPact
|
|
2
|
-
|
|
3
|
-
RepoPact is a **repository-native governance kernel for durable agent work**. It keeps the
|
|
4
|
-
load-bearing state of a project — intent, authority, evidence, decisions, and drift — as
|
|
5
|
-
typed, version-controlled records in the filesystem, so a new contributor or agent can
|
|
6
|
-
recover where things stand *without* a prior conversation, and so the guarantees that matter
|
|
7
|
-
cannot be silently weakened.
|
|
8
|
-
|
|
9
|
-
> The repository is the pact: authority, intent, evidence, and history that survive every
|
|
10
|
-
> session.
|
|
11
|
-
|
|
12
|
-
`pip install repopact` · Apache-2.0 · current release **2.0
|
|
13
|
-
([changelog](decisions/
|
|
14
|
-
|
|
15
|
-
## How it relates to `AGENTS.md`
|
|
16
|
-
|
|
17
|
-
`AGENTS.md` (and `CLAUDE.md`, editor rules) tell an agent *what to do* — they're
|
|
18
|
-
instructions, plain Markdown, with no enforcement. RepoPact is the layer **above** them:
|
|
19
|
-
|
|
20
|
-
> `AGENTS.md` tells
|
|
21
|
-
|
|
22
|
-
RepoPact's distinguishing primitive is the **binding invariant** — a declared guarantee with
|
|
23
|
-
a rationale, an escalation path, and (where its logical type permits) a machine enforcer.
|
|
24
|
-
That, plus evidence-gated completion and a filesystem state machine, is what turns a folder
|
|
25
|
-
convention into a contract. `repopact adopt` ingests an existing `AGENTS.md` rather than
|
|
26
|
-
replacing it (decision [`0020`](decisions/0020-launch-positioning-layer-above-agents-md.md)).
|
|
27
|
-
|
|
28
|
-
## Core loop
|
|
29
|
-
|
|
30
|
-
```text
|
|
31
|
-
intent -> scoped authority -> work item -> implementation -> evidence -> audit -> history
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-

|
|
35
|
-
|
|
36
|
-
## Primitives
|
|
37
|
-
|
|
38
|
-
1. **Charter & invariants** — principles (judgment) and binding invariants
|
|
39
|
-
(escalation-gated) in `governance/`.
|
|
40
|
-
2. **Frozen surface** — paths and symbols that require operator approval (`--ack`) to change.
|
|
41
|
-
3. **Scopes & roles** — layered `AGENTS.md` contracts plus a role/scope map in
|
|
42
|
-
`governance/owners.json`.
|
|
43
|
-
4. **Work items** — narrative `README.md` + machine-readable `work-item.json` with
|
|
44
|
-
evidence-linked acceptance criteria. **Mandatory preflight** (2.0): a work item must be
|
|
45
|
-
recorded *before* implementation begins (`repopact new` stamps the marker).
|
|
46
|
-
5. **Evidence** — immutable run manifests under `evidence/runs/`.
|
|
47
|
-
6. **Decisions & policies** — durable choices (`decisions/`) and operating rules
|
|
48
|
-
(`governance/policies/`) whose rationale outlives any single work item.
|
|
49
|
-
7. **Provenance** (2.0) — every record is `concrete`, `provisional`, or `inferred`.
|
|
50
|
-
`adopt` emits provisional/inferred records (honest, not fabricated); `doctor` ratchets
|
|
51
|
-
them to `concrete` as real evidence arrives. See *2.0 changes* below.
|
|
52
|
-
8. **Reconciliation** — audits and a *generated* dashboard surface drift and review
|
|
53
|
-
staleness rather than hand-maintaining it.
|
|
54
|
-
|
|
55
|
-
## Install & quick start
|
|
56
|
-
|
|
57
|
-
```powershell
|
|
58
|
-
pip install repopact # the CLI + reference validator, from PyPI
|
|
59
|
-
repopact init --target ../your-repo # seed a valid RepoPact in a new repo
|
|
60
|
-
cd ../your-repo
|
|
61
|
-
repopact new work-item "Title of the work" # stamps
|
|
62
|
-
repopact
|
|
63
|
-
repopact
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
`
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
[`
|
|
71
|
-
[`governance/
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
```
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
1
|
+
# RepoPact
|
|
2
|
+
|
|
3
|
+
RepoPact is a **repository-native governance kernel for durable agent work**. It keeps the
|
|
4
|
+
load-bearing state of a project — intent, authority, evidence, decisions, and drift — as
|
|
5
|
+
typed, version-controlled records in the filesystem, so a new contributor or agent can
|
|
6
|
+
recover where things stand *without* a prior conversation, and so the guarantees that matter
|
|
7
|
+
cannot be silently weakened.
|
|
8
|
+
|
|
9
|
+
> The repository is the pact: authority, intent, evidence, and history that survive every
|
|
10
|
+
> session.
|
|
11
|
+
|
|
12
|
+
`pip install repopact` · Apache-2.0 · current release **2.1.0**
|
|
13
|
+
([changelog](decisions/0024-release-2.1.0-proposed-lifecycle.md)).
|
|
14
|
+
|
|
15
|
+
## How it relates to `AGENTS.md`
|
|
16
|
+
|
|
17
|
+
`AGENTS.md` (and `CLAUDE.md`, editor rules) tell an agent *what to do* — they're
|
|
18
|
+
instructions, plain Markdown, with no enforcement. RepoPact is the layer **above** them:
|
|
19
|
+
|
|
20
|
+
> `AGENTS.md` tells an agent how to behave. RepoPact enforces and records whether the work respected the contract.
|
|
21
|
+
|
|
22
|
+
RepoPact's distinguishing primitive is the **binding invariant** — a declared guarantee with
|
|
23
|
+
a rationale, an escalation path, and (where its logical type permits) a machine enforcer.
|
|
24
|
+
That, plus evidence-gated completion and a filesystem state machine, is what turns a folder
|
|
25
|
+
convention into a contract. `repopact adopt` ingests an existing `AGENTS.md` rather than
|
|
26
|
+
replacing it (decision [`0020`](decisions/0020-launch-positioning-layer-above-agents-md.md)).
|
|
27
|
+
|
|
28
|
+
## Core loop
|
|
29
|
+
|
|
30
|
+
```text
|
|
31
|
+
intent -> scoped authority -> work item -> implementation -> evidence -> audit -> history
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+

|
|
35
|
+
|
|
36
|
+
## Primitives
|
|
37
|
+
|
|
38
|
+
1. **Charter & invariants** — principles (judgment) and binding invariants
|
|
39
|
+
(escalation-gated) in `governance/`.
|
|
40
|
+
2. **Frozen surface** — paths and symbols that require operator approval (`--ack`) to change.
|
|
41
|
+
3. **Scopes & roles** — layered `AGENTS.md` contracts plus a role/scope map in
|
|
42
|
+
`governance/owners.json`.
|
|
43
|
+
4. **Work items** — narrative `README.md` + machine-readable `work-item.json` with
|
|
44
|
+
evidence-linked acceptance criteria. **Mandatory preflight** (2.0): a work item must be
|
|
45
|
+
recorded *before* implementation begins (`repopact new` stamps the marker).
|
|
46
|
+
5. **Evidence** — immutable run manifests under `evidence/runs/`.
|
|
47
|
+
6. **Decisions & policies** — durable choices (`decisions/`) and operating rules
|
|
48
|
+
(`governance/policies/`) whose rationale outlives any single work item.
|
|
49
|
+
7. **Provenance** (2.0) — every record is `concrete`, `provisional`, or `inferred`.
|
|
50
|
+
`adopt` emits provisional/inferred records (honest, not fabricated); `doctor` ratchets
|
|
51
|
+
them to `concrete` as real evidence arrives. See *2.0 changes* below.
|
|
52
|
+
8. **Reconciliation** — audits and a *generated* dashboard surface drift and review
|
|
53
|
+
staleness rather than hand-maintaining it.
|
|
54
|
+
|
|
55
|
+
## Install & quick start
|
|
56
|
+
|
|
57
|
+
```powershell
|
|
58
|
+
pip install repopact # the CLI + reference validator, from PyPI
|
|
59
|
+
repopact init --target ../your-repo # seed a valid RepoPact in a new repo
|
|
60
|
+
cd ../your-repo
|
|
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
|
|
63
|
+
repopact validate
|
|
64
|
+
repopact dashboard
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
`repopact` dispatches `init`, `adopt`, `validate`, `new`, `dashboard`, `spec`,
|
|
68
|
+
`check-frozen`, `import-plan`, and `doctor`. Records are validated against `schemas/*.json`
|
|
69
|
+
(structure) and by the validator (cross-record semantics; decision
|
|
70
|
+
[`0003`](decisions/0003-validate-records-against-json-schemas.md)). Begin with
|
|
71
|
+
[`AGENTS.md`](AGENTS.md), then [`governance/charter.md`](governance/charter.md) and
|
|
72
|
+
[`governance/workflow.md`](governance/workflow.md).
|
|
73
|
+
|
|
74
|
+
Alternative implementations can run the published conformance suite:
|
|
75
|
+
|
|
76
|
+
```powershell
|
|
77
|
+
python scripts/run_conformance.py --command "your-validator --root {repo}"
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
See [`CONFORMANCE.md`](CONFORMANCE.md) and [`conformance/`](conformance/).
|
|
81
|
+
|
|
82
|
+
## Adopt an *existing* repository
|
|
83
|
+
|
|
84
|
+
For a project that already has CODEOWNERS, CI workflows, and nested `AGENTS.md` contracts,
|
|
85
|
+
`adopt` maps those existing signals into RepoPact records without overwriting anything:
|
|
86
|
+
|
|
87
|
+
```powershell
|
|
88
|
+
repopact adopt --target ../existing-repo --dry-run # preview the plan, write nothing
|
|
89
|
+
repopact adopt --target ../existing-repo # create records, then validate
|
|
90
|
+
repopact doctor # diagnose + repair drift; migrate on upgrade
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
CODEOWNERS becomes scopes and roles; each `.github/workflows/*` becomes a binding-gate policy
|
|
94
|
+
(plus invariant `INV-2` and a frozen-surface entry); every nested `AGENTS.md` is registered as
|
|
95
|
+
a contract; git history seeds a first **inferred** evidence run, and the adoption record is
|
|
96
|
+
recorded as **provisional** — honestly typed, not a fabricated "completed" claim. Adoption is
|
|
97
|
+
idempotent.
|
|
98
|
+
|
|
99
|
+
## 2.0: mandatory preflight + provenance-typed records
|
|
100
|
+
|
|
101
|
+
Decision [`0021`](decisions/0021-preflight-mandatory-and-provenance.md) (supersedes 0018):
|
|
102
|
+
|
|
103
|
+
- **Mandatory preflight (default on).** No work begins until a work item exists and
|
|
104
|
+
propagates through the pact; `repopact new` stamps the marker. Existing repos grandfather
|
|
105
|
+
their pre-2.0 items automatically — `adopt`/`init` set a preflight epoch and `doctor`
|
|
106
|
+
migrates on upgrade. *This is a breaking change*: run `repopact doctor` after upgrading.
|
|
107
|
+
- **Provenance typing** (`concrete` / `provisional` / `inferred`, default `concrete`). This
|
|
108
|
+
is the principled escape from the *adoption trilemma*: `adopt` emits provisional/inferred
|
|
109
|
+
records so the result is both **valid** and **faithful** (reconstruction is labelled, not
|
|
110
|
+
faked). Completion still requires `concrete` evidence; `doctor` ratchets when it arrives.
|
|
111
|
+
|
|
112
|
+
## Status is a filesystem transition
|
|
113
|
+
|
|
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.
|
|
125
|
+
|
|
126
|
+
## Derive over declare
|
|
127
|
+
|
|
128
|
+
Anything computable from source records is generated, not authored by hand (the dashboard,
|
|
129
|
+
audit-freshness views, and the derived blocks of `SPEC.md`). Only genuine sources are
|
|
130
|
+
hand-maintained. See policy `001`.
|
|
131
|
+
|
|
132
|
+
## Evaluation, formal model & the Proving Ground
|
|
133
|
+
|
|
134
|
+
RepoPact is developed against its own evidence, not assertion. The `research/` lab notebook
|
|
135
|
+
holds a [formal model](research/formal-model.md) (the L0–L5 kernel, the typed invariant
|
|
136
|
+
lattice, the adoption trilemma), the pre-registered [experiment protocol](research/protocol.md)
|
|
137
|
+
and [benchmark protocol](research/benchmark-protocol.md) (hypotheses H1–H13, falsification
|
|
138
|
+
criteria, [threats to validity](research/threats-to-validity.md)), a [findings
|
|
139
|
+
register](research/findings.md), and the current [paper](research/paper.md).
|
|
140
|
+
|
|
141
|
+
**PactBench** — the runnable benchmark suite (pre-registered tasks measuring whether RepoPact
|
|
142
|
+
enforcement reduces silent guarantee drift, with a model-agnostic harness and an S5 drift
|
|
143
|
+
harness) — lives in the **[RepoPact Proving
|
|
144
|
+
Ground](https://github.com/ForgeWireLabs/repopact-proving-ground)**, a throwaway-but-real
|
|
145
|
+
project that consumes RepoPact from PyPI and is driven across every primitive, including cases
|
|
146
|
+
designed to break it. RepoPact defines the *protocol* (`research/`); the Proving Ground hosts
|
|
147
|
+
the runnable suite. *RepoPact defines the pact; the Proving Ground tests whether the pact
|
|
148
|
+
holds under agent pressure.*
|
|
149
|
+
|
|
150
|
+
## Ecosystem
|
|
151
|
+
|
|
152
|
+
RepoPact is the work-governance layer of [ForgeWire Labs](https://github.com/ForgeWireLabs) —
|
|
153
|
+
inspectable agentic infrastructure (*inspect the work, bound the authority, preserve the
|
|
154
|
+
evidence*). It composes with Fabric (execution governance) and ForgeLink (human-agent
|
|
155
|
+
communication governance), but is independently useful on its own.
|
|
156
|
+
|
|
157
|
+
## License & version
|
|
158
|
+
|
|
159
|
+
Apache-2.0 ([`LICENSE`](LICENSE), decision [`0002`](decisions/0002-license-apache-2.0.md)).
|
|
160
|
+
The spec version is in [`VERSION`](VERSION); templates for every record type live in
|
|
161
|
+
[`templates/`](templates/).
|
repopact-2.1.0/VERSION
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
2.1.0
|
|
@@ -31,10 +31,11 @@ py-modules = [
|
|
|
31
31
|
"track_import",
|
|
32
32
|
"takeover",
|
|
33
33
|
"doctor",
|
|
34
|
-
"new",
|
|
35
|
-
"check_frozen_surface",
|
|
36
|
-
"frontmatter",
|
|
37
|
-
|
|
34
|
+
"new",
|
|
35
|
+
"check_frozen_surface",
|
|
36
|
+
"frontmatter",
|
|
37
|
+
"run_conformance",
|
|
38
|
+
]
|
|
38
39
|
|
|
39
40
|
[tool.setuptools.package-dir]
|
|
40
41
|
"" = "scripts"
|
|
@@ -45,5 +46,5 @@ version = { file = "VERSION" }
|
|
|
45
46
|
# Seed content shipped so `repopact init` works from an installed wheel. These
|
|
46
47
|
# reference the canonical top-level files (one source of truth — no duplication).
|
|
47
48
|
[tool.setuptools.data-files]
|
|
48
|
-
"share/repopact/schemas" = ["schemas/*.json"]
|
|
49
|
-
"share/repopact/templates" = ["templates/*"]
|
|
49
|
+
"share/repopact/schemas" = ["schemas/*.json"]
|
|
50
|
+
"share/repopact/templates" = ["templates/*"]
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "conformance-manifest.schema.json",
|
|
4
|
+
"type": "object",
|
|
5
|
+
"required": ["suite_version", "fixtures_root", "cases"],
|
|
6
|
+
"properties": {
|
|
7
|
+
"suite_version": {"type": "string", "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"},
|
|
8
|
+
"spec_version_file": {"type": "string"},
|
|
9
|
+
"fixtures_root": {"type": "string", "minLength": 1},
|
|
10
|
+
"cases": {
|
|
11
|
+
"type": "array",
|
|
12
|
+
"minItems": 1,
|
|
13
|
+
"items": {
|
|
14
|
+
"type": "object",
|
|
15
|
+
"required": ["id", "path", "expect", "rules", "description"],
|
|
16
|
+
"properties": {
|
|
17
|
+
"id": {"type": "string", "minLength": 1},
|
|
18
|
+
"path": {"type": "string", "minLength": 1},
|
|
19
|
+
"overlay_on": {"type": "string", "minLength": 1},
|
|
20
|
+
"expect": {"enum": ["accept", "reject"]},
|
|
21
|
+
"expected_message": {"type": "string"},
|
|
22
|
+
"rules": {"type": "array", "items": {"type": "string"}, "minItems": 1},
|
|
23
|
+
"description": {"type": "string", "minLength": 1}
|
|
24
|
+
},
|
|
25
|
+
"allOf": [
|
|
26
|
+
{
|
|
27
|
+
"if": {"properties": {"expect": {"const": "reject"}}},
|
|
28
|
+
"then": {"required": ["expected_message", "overlay_on"]}
|
|
29
|
+
}
|
|
30
|
+
]
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -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
|
-
|
|
@@ -11,13 +11,14 @@ from __future__ import annotations
|
|
|
11
11
|
import argparse
|
|
12
12
|
import json
|
|
13
13
|
import shutil
|
|
14
|
+
import site
|
|
14
15
|
import sys
|
|
15
16
|
from datetime import date, timedelta
|
|
16
17
|
from pathlib import Path
|
|
17
18
|
|
|
18
19
|
HERE = Path(__file__).resolve().parent # directory holding the tooling modules
|
|
19
20
|
CHECKOUT = HERE.parent # repo root when running from a checkout
|
|
20
|
-
LIFECYCLE = ("active", "blocked", "deferred", "completed")
|
|
21
|
+
LIFECYCLE = ("proposed", "active", "blocked", "deferred", "completed")
|
|
21
22
|
MODULES = (
|
|
22
23
|
"repo_model.py", "validate_repo.py", "generate_dashboard.py", "generate_spec.py",
|
|
23
24
|
"init_repo.py", "new.py", "check_frozen_surface.py", "frontmatter.py", "repopact_cli.py",
|
|
@@ -26,13 +27,16 @@ MODULES = (
|
|
|
26
27
|
|
|
27
28
|
def _seed_dir(name: str) -> Path:
|
|
28
29
|
"""Locate seed content (schemas/templates) in a checkout or an installed wheel."""
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
30
|
+
candidates = [
|
|
31
|
+
CHECKOUT / name,
|
|
32
|
+
Path(sys.prefix) / "share" / "repopact" / name,
|
|
33
|
+
Path(site.USER_BASE) / "share" / "repopact" / name,
|
|
34
|
+
]
|
|
35
|
+
for candidate in candidates:
|
|
36
|
+
if candidate.is_dir():
|
|
37
|
+
return candidate
|
|
38
|
+
looked = ", ".join(str(candidate) for candidate in candidates)
|
|
39
|
+
raise FileNotFoundError(f"cannot locate seed '{name}' (looked in {looked})")
|
|
36
40
|
|
|
37
41
|
|
|
38
42
|
def _write(path: Path, text: str) -> None:
|
|
@@ -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" /
|
|
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":
|
|
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 = {
|
|
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
|
|
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,15 +22,15 @@ 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.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
|
|
|
30
30
|
`AGENTS.md` (and `CLAUDE.md`, editor rules) tell an agent *what to do* — they're
|
|
31
31
|
instructions, plain Markdown, with no enforcement. RepoPact is the layer **above** them:
|
|
32
32
|
|
|
33
|
-
> `AGENTS.md` tells
|
|
33
|
+
> `AGENTS.md` tells an agent how to behave. RepoPact enforces and records whether the work respected the contract.
|
|
34
34
|
|
|
35
35
|
RepoPact's distinguishing primitive is the **binding invariant** — a declared guarantee with
|
|
36
36
|
a rationale, an escalation path, and (where its logical type permits) a machine enforcer.
|
|
@@ -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
|
```
|
|
@@ -83,6 +84,14 @@ repopact dashboard
|
|
|
83
84
|
[`AGENTS.md`](AGENTS.md), then [`governance/charter.md`](governance/charter.md) and
|
|
84
85
|
[`governance/workflow.md`](governance/workflow.md).
|
|
85
86
|
|
|
87
|
+
Alternative implementations can run the published conformance suite:
|
|
88
|
+
|
|
89
|
+
```powershell
|
|
90
|
+
python scripts/run_conformance.py --command "your-validator --root {repo}"
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
See [`CONFORMANCE.md`](CONFORMANCE.md) and [`conformance/`](conformance/).
|
|
94
|
+
|
|
86
95
|
## Adopt an *existing* repository
|
|
87
96
|
|
|
88
97
|
For a project that already has CODEOWNERS, CI workflows, and nested `AGENTS.md` contracts,
|
|
@@ -115,9 +124,17 @@ Decision [`0021`](decisions/0021-preflight-mandatory-and-provenance.md) (superse
|
|
|
115
124
|
|
|
116
125
|
## Status is a filesystem transition
|
|
117
126
|
|
|
118
|
-
Work moves between
|
|
119
|
-
|
|
120
|
-
|
|
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.
|
|
121
138
|
|
|
122
139
|
## Derive over declare
|
|
123
140
|
|
|
@@ -132,7 +149,7 @@ holds a [formal model](research/formal-model.md) (the L0–L5 kernel, the typed
|
|
|
132
149
|
lattice, the adoption trilemma), the pre-registered [experiment protocol](research/protocol.md)
|
|
133
150
|
and [benchmark protocol](research/benchmark-protocol.md) (hypotheses H1–H13, falsification
|
|
134
151
|
criteria, [threats to validity](research/threats-to-validity.md)), a [findings
|
|
135
|
-
register](research/findings.md), and
|
|
152
|
+
register](research/findings.md), and the current [paper](research/paper.md).
|
|
136
153
|
|
|
137
154
|
**PactBench** — the runnable benchmark suite (pre-registered tasks measuring whether RepoPact
|
|
138
155
|
enforcement reduces silent guarantee drift, with a model-agnostic harness and an S5 drift
|
|
@@ -3,6 +3,7 @@ README.md
|
|
|
3
3
|
VERSION
|
|
4
4
|
pyproject.toml
|
|
5
5
|
schemas/audit-finding.schema.json
|
|
6
|
+
schemas/conformance-manifest.schema.json
|
|
6
7
|
schemas/evidence-run.schema.json
|
|
7
8
|
schemas/frozen-surface.schema.json
|
|
8
9
|
schemas/invariants.schema.json
|
|
@@ -19,6 +20,7 @@ scripts/new.py
|
|
|
19
20
|
scripts/plan_import.py
|
|
20
21
|
scripts/repo_model.py
|
|
21
22
|
scripts/repopact_cli.py
|
|
23
|
+
scripts/run_conformance.py
|
|
22
24
|
scripts/takeover.py
|
|
23
25
|
scripts/track_import.py
|
|
24
26
|
scripts/validate_repo.py
|
|
@@ -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)}")
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""Run the published RepoPact conformance suite.
|
|
2
|
+
|
|
3
|
+
The runner materializes each fixture as an isolated temporary repository, injects
|
|
4
|
+
the canonical schemas from this checkout, runs a RepoPact implementation, and
|
|
5
|
+
compares the result with conformance/manifest.json.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
import json
|
|
12
|
+
import shutil
|
|
13
|
+
import subprocess
|
|
14
|
+
import sys
|
|
15
|
+
import tempfile
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
ROOT = Path(__file__).resolve().parents[1]
|
|
21
|
+
MANIFEST = ROOT / "conformance" / "manifest.json"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True)
|
|
25
|
+
class CaseResult:
|
|
26
|
+
case_id: str
|
|
27
|
+
passed: bool
|
|
28
|
+
detail: str
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def load_manifest(path: Path = MANIFEST) -> dict:
|
|
32
|
+
with path.open(encoding="utf-8") as handle:
|
|
33
|
+
data = json.load(handle)
|
|
34
|
+
if not isinstance(data, dict):
|
|
35
|
+
raise ValueError(f"manifest must be a JSON object: {path}")
|
|
36
|
+
return data
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _copy_overlay(src: Path, dst: Path) -> None:
|
|
40
|
+
for path in src.rglob("*"):
|
|
41
|
+
if path.is_dir() or path.name == "meta.json":
|
|
42
|
+
continue
|
|
43
|
+
target = dst / path.relative_to(src)
|
|
44
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
45
|
+
shutil.copy2(path, target)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def materialize_case(root: Path, fixtures_root: Path, case: dict) -> Path:
|
|
49
|
+
repo = root / "repo"
|
|
50
|
+
overlay_on = case.get("overlay_on")
|
|
51
|
+
if overlay_on:
|
|
52
|
+
shutil.copytree(fixtures_root / str(overlay_on), repo)
|
|
53
|
+
_copy_overlay(fixtures_root / str(case["path"]), repo)
|
|
54
|
+
else:
|
|
55
|
+
shutil.copytree(fixtures_root / str(case["path"]), repo)
|
|
56
|
+
shutil.copytree(ROOT / "schemas", repo / "schemas")
|
|
57
|
+
return repo
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def run_command(command: str, repo: Path) -> subprocess.CompletedProcess[str]:
|
|
61
|
+
rendered = command.format(repo=str(repo))
|
|
62
|
+
return subprocess.run(rendered, shell=True, text=True, capture_output=True)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def evaluate_case(case: dict, command: str, fixtures_root: Path) -> CaseResult:
|
|
66
|
+
case_id = str(case["id"])
|
|
67
|
+
with tempfile.TemporaryDirectory(prefix=f"repopact-conformance-{case_id}-") as tmp:
|
|
68
|
+
repo = materialize_case(Path(tmp), fixtures_root, case)
|
|
69
|
+
proc = run_command(command, repo)
|
|
70
|
+
output = "\n".join(part for part in (proc.stdout, proc.stderr) if part)
|
|
71
|
+
expect = case.get("expect")
|
|
72
|
+
if expect == "accept":
|
|
73
|
+
passed = proc.returncode == 0
|
|
74
|
+
detail = "accepted" if passed else f"expected accept, exit={proc.returncode}: {output.strip()}"
|
|
75
|
+
return CaseResult(case_id, passed, detail)
|
|
76
|
+
expected = str(case.get("expected_message", ""))
|
|
77
|
+
passed = proc.returncode != 0 and expected in output
|
|
78
|
+
detail = (
|
|
79
|
+
f"rejected with '{expected}'"
|
|
80
|
+
if passed
|
|
81
|
+
else f"expected reject containing '{expected}', exit={proc.returncode}: {output.strip()}"
|
|
82
|
+
)
|
|
83
|
+
return CaseResult(case_id, passed, detail)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def run_suite(command: str, manifest_path: Path = MANIFEST) -> list[CaseResult]:
|
|
87
|
+
manifest = load_manifest(manifest_path)
|
|
88
|
+
fixtures_root = manifest_path.parent / str(manifest.get("fixtures_root", "fixtures"))
|
|
89
|
+
return [evaluate_case(case, command, fixtures_root) for case in manifest.get("cases", [])]
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def main() -> int:
|
|
93
|
+
parser = argparse.ArgumentParser(description="Run the RepoPact conformance suite")
|
|
94
|
+
parser.add_argument("--manifest", type=Path, default=MANIFEST)
|
|
95
|
+
parser.add_argument(
|
|
96
|
+
"--command",
|
|
97
|
+
default=f'"{sys.executable}" "{ROOT / "scripts" / "validate_repo.py"}" --root "{{repo}}"',
|
|
98
|
+
help="Implementation command template; {repo} is replaced with the fixture repo path.",
|
|
99
|
+
)
|
|
100
|
+
args = parser.parse_args()
|
|
101
|
+
|
|
102
|
+
results = run_suite(args.command, args.manifest.resolve())
|
|
103
|
+
failed = [result for result in results if not result.passed]
|
|
104
|
+
for result in results:
|
|
105
|
+
status = "PASS" if result.passed else "FAIL"
|
|
106
|
+
print(f"{status} {result.case_id}: {result.detail}")
|
|
107
|
+
print(f"\n{len(results) - len(failed)}/{len(results)} conformance cases passed.")
|
|
108
|
+
return 1 if failed else 0
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
if __name__ == "__main__":
|
|
112
|
+
sys.exit(main())
|
|
@@ -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
|
-
|
|
387
|
-
|
|
388
|
-
|
|
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:
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
"""Conformance corpus test (work
|
|
1
|
+
"""Conformance corpus test (work items 006 and 019).
|
|
2
2
|
|
|
3
|
-
Validates the
|
|
4
|
-
and each invalid overlay must be rejected with its declared message.
|
|
5
|
-
not vendor schemas; the canonical schemas/ are injected here so a
|
|
6
|
-
pass against a stale schema copy (no drift).
|
|
3
|
+
Validates the published suite under conformance/: the valid baseline must be
|
|
4
|
+
accepted, and each invalid overlay must be rejected with its declared message.
|
|
5
|
+
Fixtures do not vendor schemas; the canonical schemas/ are injected here so a
|
|
6
|
+
fixture can never pass against a stale schema copy (no drift).
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
9
|
from __future__ import annotations
|
|
@@ -15,12 +15,17 @@ import tempfile
|
|
|
15
15
|
import unittest
|
|
16
16
|
from pathlib import Path
|
|
17
17
|
|
|
18
|
+
import jsonschema
|
|
19
|
+
|
|
18
20
|
ROOT = Path(__file__).resolve().parents[1]
|
|
19
21
|
sys.path.insert(0, str(ROOT / "scripts"))
|
|
20
22
|
|
|
21
23
|
from validate_repo import validate # noqa: E402
|
|
22
24
|
|
|
23
|
-
|
|
25
|
+
import run_conformance # noqa: E402
|
|
26
|
+
|
|
27
|
+
MANIFEST = ROOT / "conformance" / "manifest.json"
|
|
28
|
+
FIXTURES = ROOT / "conformance" / "fixtures"
|
|
24
29
|
VALID = FIXTURES / "valid"
|
|
25
30
|
INVALID = FIXTURES / "invalid"
|
|
26
31
|
|
|
@@ -39,6 +44,22 @@ def build_repo(dst: Path, overlay: Path | None = None) -> Path:
|
|
|
39
44
|
|
|
40
45
|
|
|
41
46
|
class ConformanceTests(unittest.TestCase):
|
|
47
|
+
def test_manifest_is_structurally_valid(self) -> None:
|
|
48
|
+
manifest = json.loads(MANIFEST.read_text(encoding="utf-8"))
|
|
49
|
+
schema = json.loads((ROOT / "schemas" / "conformance-manifest.schema.json").read_text(encoding="utf-8"))
|
|
50
|
+
jsonschema.Draft202012Validator(schema).validate(manifest)
|
|
51
|
+
self.assertEqual((ROOT / "VERSION").read_text(encoding="utf-8").strip(), manifest["suite_version"])
|
|
52
|
+
for case in manifest["cases"]:
|
|
53
|
+
self.assertTrue((FIXTURES / case["path"]).exists(), case["path"])
|
|
54
|
+
|
|
55
|
+
def test_manifest_matches_reference_suite(self) -> None:
|
|
56
|
+
results = run_conformance.run_suite(
|
|
57
|
+
f'"{sys.executable}" "{ROOT / "scripts" / "validate_repo.py"}" --root "{{repo}}"',
|
|
58
|
+
MANIFEST,
|
|
59
|
+
)
|
|
60
|
+
self.assertTrue(results, "no conformance cases found")
|
|
61
|
+
self.assertEqual([], [result.detail for result in results if not result.passed])
|
|
62
|
+
|
|
42
63
|
def test_valid_fixture_is_accepted(self) -> None:
|
|
43
64
|
with tempfile.TemporaryDirectory() as tmp:
|
|
44
65
|
repo = build_repo(Path(tmp) / "repo")
|
|
@@ -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
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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.
|
|
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.1/VERSION
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
2.0.1
|
|
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
|