open-ready 0.1.0__py3-none-any.whl
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.
- open_ready-0.1.0.dist-info/METADATA +223 -0
- open_ready-0.1.0.dist-info/RECORD +40 -0
- open_ready-0.1.0.dist-info/WHEEL +5 -0
- open_ready-0.1.0.dist-info/entry_points.txt +2 -0
- open_ready-0.1.0.dist-info/licenses/LICENSE +21 -0
- open_ready-0.1.0.dist-info/top_level.txt +1 -0
- openready/__init__.py +15 -0
- openready/__main__.py +6 -0
- openready/acknowledge.py +318 -0
- openready/advisory.py +198 -0
- openready/audit.py +578 -0
- openready/backup.py +550 -0
- openready/candidate.py +893 -0
- openready/capabilities.py +360 -0
- openready/cli.py +825 -0
- openready/contracts/project.schema.json +75 -0
- openready/contracts/release-packet.schema.json +135 -0
- openready/contracts/result.schema.json +85 -0
- openready/contracts.py +181 -0
- openready/doctor.py +368 -0
- openready/external.py +140 -0
- openready/feature.py +317 -0
- openready/history.py +349 -0
- openready/intake.py +247 -0
- openready/ledger.py +601 -0
- openready/limits.py +906 -0
- openready/migrate.py +162 -0
- openready/overrides.py +278 -0
- openready/policy.py +761 -0
- openready/private.py +233 -0
- openready/project.py +635 -0
- openready/promote.py +325 -0
- openready/providers.py +282 -0
- openready/publisher.py +685 -0
- openready/registry.py +207 -0
- openready/scan.py +617 -0
- openready/staged.py +233 -0
- openready/status.py +147 -0
- openready/templates.py +635 -0
- openready/verify.py +719 -0
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: open-ready
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Private-first project care and release-candidate preparation: inspect, prepare and verify a private Git repository for public release without shipping secrets or its development history.
|
|
5
|
+
Author: Socraticblock
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://socraticblock.com/work/open-ready/
|
|
8
|
+
Project-URL: Repository, https://github.com/socraticblock/open-source-ready
|
|
9
|
+
Project-URL: Issues, https://github.com/socraticblock/open-source-ready/issues
|
|
10
|
+
Keywords: open source,release engineering,git history,secrets,privacy,ai agents
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Topic :: Software Development :: Version Control :: Git
|
|
18
|
+
Classifier: Topic :: Security
|
|
19
|
+
Requires-Python: >=3.12
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Dynamic: license-file
|
|
24
|
+
|
|
25
|
+
# Open-Ready
|
|
26
|
+
|
|
27
|
+
**Private by default, and it refuses to publish your secrets.** Open-Ready keeps personal
|
|
28
|
+
software projects private, tests and backs them up, and — only when you ask — prepares a
|
|
29
|
+
release candidate that carries its own evidence and stays **unapproved** until you
|
|
30
|
+
authorise one exact artifact.
|
|
31
|
+
|
|
32
|
+
It is a working tool, not a manifesto. The interesting part is not what it does; it is what
|
|
33
|
+
it *won't* do. There is no `publish` command, no code path that changes a repository's
|
|
34
|
+
visibility, and no way for the agent using it to invent your approval. That absence is the
|
|
35
|
+
product.
|
|
36
|
+
|
|
37
|
+
**Who it's for.** People who let AI agents work inside their own private projects and want
|
|
38
|
+
a real boundary around what can leave. You should be comfortable with a terminal, `git` and
|
|
39
|
+
Python. It is opinionated — one owner, local-first, git-native — and it is not a hosted
|
|
40
|
+
service.
|
|
41
|
+
|
|
42
|
+
**Status:** v0.1.0-p0, early. Measured on Linux/WSL2 only, with a GitHub Actions workflow that
|
|
43
|
+
runs the checks on every push and every pull request (observed passing: run `38579119769` in
|
|
44
|
+
this repository — a contributor convenience, not a release gate). Read
|
|
45
|
+
[Verified limitations](#verified-limitations--do-not-treat-these-as-solved) before relying
|
|
46
|
+
on it.
|
|
47
|
+
|
|
48
|
+
## Quickstart
|
|
49
|
+
|
|
50
|
+
Requirements: Python 3.12+ and `git`. No install step, nothing to provision.
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
git clone https://github.com/socraticblock/open-source-ready && cd open-source-ready
|
|
54
|
+
export PYTHONPATH="$PWD/src"
|
|
55
|
+
|
|
56
|
+
# create a private project (scaffold, synthetic fixture, one local commit, no remote)
|
|
57
|
+
python3 -m openready new --path ~/projects/example
|
|
58
|
+
|
|
59
|
+
# what it found, and what it did not check
|
|
60
|
+
python3 -m openready doctor --path ~/projects/example
|
|
61
|
+
|
|
62
|
+
# prepare an UNAPPROVED release candidate, then verify it
|
|
63
|
+
python3 -m openready candidate --path ~/projects/example --out /tmp/candidate
|
|
64
|
+
python3 -m openready verify --candidate /tmp/candidate --packet /tmp/packet.json
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Exit code `2` above is normal and correct: it means **a human decision is required**, not
|
|
68
|
+
that something broke. A release packet is always `REVIEW_REQUIRED`.
|
|
69
|
+
|
|
70
|
+
Two more worth knowing: `openready limits --path .` lists every control this build could
|
|
71
|
+
*not* verify, with what would verify it — one command, and the honest answer to "is this
|
|
72
|
+
thing actually safe?". And `openready precommit --path .` checks what a commit is about to
|
|
73
|
+
add, so the cheap moment catches it rather than the expensive one.
|
|
74
|
+
|
|
75
|
+
## Commands
|
|
76
|
+
|
|
77
|
+
| Command | What it does |
|
|
78
|
+
|---|---|
|
|
79
|
+
| `new --path P --project-id ID [--backup]` | **create a complete private project in one step**: scaffold, synthetic fixture, one local commit, private runtime data root outside the project, and a registration. Also installs an **advisory** commit-time control (`.githooks/pre-commit`) |
|
|
80
|
+
| `init --path P --project-id ID [--git] [--adopt] [--register]` | the bare scaffolding step on its own; **refuses to create a project inside an enclosing git working tree**, and `--adopt` never commits inside an existing tree |
|
|
81
|
+
| `doctor --path P [--json]` | findings, and what was not checked |
|
|
82
|
+
| `fixture --path P` | write the deterministic synthetic corpus |
|
|
83
|
+
| `ledger --path P [--write]` | dependency, asset and provenance inventory |
|
|
84
|
+
| `candidate --path P --out D` | export an **UNAPPROVED** candidate without Git history. The history-carrying modes exist in the code but refuse from the public workflow: an artifact that carries history cannot show that an acknowledged finding was removed, because the same bytes travel either way |
|
|
85
|
+
| `verify --candidate D [--packet FILE] [--no-build] [--expected-digest D]` | re-derive the digest; emit the release packet |
|
|
86
|
+
| `backup --path P \| --project ID [--out F] [--verify]` | private backup, verified restore, migration drill |
|
|
87
|
+
| `test --path P` | run the project's tests with a scrubbed environment |
|
|
88
|
+
| `worth --path P` | private recommendation memo |
|
|
89
|
+
| `update [--path P] --dry-run` | contract and policy drift; changes nothing |
|
|
90
|
+
| `capabilities [--path P]` | host and fleet detection, with no network call |
|
|
91
|
+
| `status [--path P \| --project ID]` | local metadata only; with neither, the **private project registry** |
|
|
92
|
+
| `limits [--path P]` | the one-command honesty view: every unverified control, why, and what would verify it |
|
|
93
|
+
| `precommit [--path P]` | check the **staged** content of a commit (the index blob, not the working file) and refuse a commit that would put a credential, host path, database or `.env` into history. **Advisory, not enforcement**: `git commit --no-verify` skips it |
|
|
94
|
+
| `acknowledge --path P [--write --reason "…" --by "…"] [--include-tree] [--expires DATE]` | adopting a repository that already has history: **propose** one acknowledgement per **blob** that still holds a cleaned-up shape, and write nothing until asked. It proposes nothing for a finding in the working tree, because that file can be edited |
|
|
95
|
+
|
|
96
|
+
### Adopting a repository that already exists
|
|
97
|
+
|
|
98
|
+
Cleaning the working tree is not enough. The object store still holds what was cleaned, and
|
|
99
|
+
the export refuses a candidate while any of it is unacknowledged. That is the first wall a
|
|
100
|
+
real user meets, and it is invisible from this repository, which has always been clean.
|
|
101
|
+
|
|
102
|
+
```
|
|
103
|
+
openready init --path . --adopt --project-id <id> # scaffold into the tree, overwriting nothing
|
|
104
|
+
openready doctor --path . # what it found, and what it did not check
|
|
105
|
+
# fix what is in the working tree -- those files are editable and get no proposal
|
|
106
|
+
openready acknowledge --path . # propose one entry per blob; writes nothing
|
|
107
|
+
openready acknowledge --path . --write --reason "…" --by "…"
|
|
108
|
+
openready candidate --path . --out ../candidate # without history: the default, and the only public path
|
|
109
|
+
openready verify --candidate ../candidate --packet packet.json
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
The export carries the file tree and nothing else. That is the only mode this workflow offers,
|
|
113
|
+
and the acknowledgement above is what lets a first user reach it: the object store still holds
|
|
114
|
+
what was cleaned out of the tree, and a candidate that carries no history does not need those
|
|
115
|
+
bytes cleared -- it needs them not to travel, which is what excluding the history does.
|
|
116
|
+
|
|
117
|
+
Two things this command will not do for you. An entry binds to the **blob** somebody looked
|
|
118
|
+
at, so content committed at the same path later is still a finding — an exception is never
|
|
119
|
+
allowed to grow wider than the reason for it. And the **reason** is yours: a placeholder is
|
|
120
|
+
refused, because a tool cannot tell a fixture from a credential and must not pretend to.
|
|
121
|
+
Measured on a real project (the engineering journal, 106 files, on a copy): 23 findings
|
|
122
|
+
across 12 blobs, 14 entries, then a candidate of 115 files — byte-identical to the same
|
|
123
|
+
operation with a hand-written acknowledgement file.
|
|
124
|
+
|
|
125
|
+
The registry (`registry.py`) is a **metadata-only index** — project id, path, risk tier,
|
|
126
|
+
creation date, whether a baseline commit exists — held in the private runtime data root,
|
|
127
|
+
outside every project and outside any git working tree. It is an index, not an authority:
|
|
128
|
+
the project's own contract wins, a disagreement is reported rather than resolved, and a
|
|
129
|
+
registry that cannot be read is never overwritten.
|
|
130
|
+
|
|
131
|
+
There is **no `publish` command**, and no code path that changes a repository's visibility.
|
|
132
|
+
Publication is an owner decision bound to an exact artifact digest, made through a principal
|
|
133
|
+
the builder cannot write to. To be straight about it: on a host where the agent's
|
|
134
|
+
credentials could also publish, that separation is *reported* as `NOT_ENFORCED` rather than
|
|
135
|
+
enforced — `docs/PUBLISHING-CREDENTIALS.md` is the recipe for making it real.
|
|
136
|
+
|
|
137
|
+
Some source modules and documents here cite the specification this tool was built from —
|
|
138
|
+
`docs/10-CLI-CONTRACT.md`, `02-ARCHITECTURE.md`, `06-ACCEPTANCE-TESTS.md`. Those belong to the
|
|
139
|
+
original brief and are **not** in this repository, so a citation of one is a provenance note,
|
|
140
|
+
not a link that resolves.
|
|
141
|
+
|
|
142
|
+
## Exit codes
|
|
143
|
+
|
|
144
|
+
`0` succeeded — *not* safe to publish. `1` a failure or `STOP`. `2` a required human
|
|
145
|
+
decision. `3` a required check was unavailable, unenforceable or not run. `64` usage error.
|
|
146
|
+
|
|
147
|
+
## Rules this build enforces in code rather than documenting
|
|
148
|
+
|
|
149
|
+
- **Exit 0 never means safe.** A required check that could not run forces exit 3.
|
|
150
|
+
- **A refused export writes nothing.** Every check whose failure severity is `STOP` blocks
|
|
151
|
+
the write; the destination is left untouched — a candidate that is written and then
|
|
152
|
+
criticised is a candidate that ships anyway. Included: an unreadable or only partly
|
|
153
|
+
scanned object store, an empty allowlist, a destination that already holds unrelated data,
|
|
154
|
+
and any unacknowledged history finding.
|
|
155
|
+
- **"Not checked" never renders as "clean".** A scan that could not run, could not be read,
|
|
156
|
+
or hit its size bound is reported `NOT_RUN` and refuses the export, rather than reporting
|
|
157
|
+
that it found nothing.
|
|
158
|
+
- **The digest covers content only.** `MANIFEST.json` and `CANDIDATE.json` describe the
|
|
159
|
+
candidate and are not part of its identity, so a stranger can recompute the digest from the
|
|
160
|
+
files alone. The two records must agree, and `verify` fails if they do not.
|
|
161
|
+
- **Acknowledgements cannot be silent, and they name exact bytes.** Naming a false positive
|
|
162
|
+
requires a check id, an exact path, a real reason, who acknowledged it and when; entries
|
|
163
|
+
expire; and every applied entry is re-reported on every run. An entry binds to the content
|
|
164
|
+
id (git's blob id) of what was looked at, so it covers those bytes and nothing else — a
|
|
165
|
+
fixture's exception cannot hide a credential committed into the same file later.
|
|
166
|
+
- **Rights are never reported as `PASS`.** Ownership cannot be established by a scan, so the
|
|
167
|
+
packet always carries `rights: NOT_RUN` rather than inventing a fact. The same applies to
|
|
168
|
+
dependency ecosystems this build does not parse: an unread `Cargo.toml` is `NOT_RUN`, never
|
|
169
|
+
"no dependencies".
|
|
170
|
+
- **A dependency is a licence obligation.** Unknown licences and copyleft are human review
|
|
171
|
+
items, not conclusions. This tool does not provide legal advice and does not determine
|
|
172
|
+
compatibility.
|
|
173
|
+
|
|
174
|
+
## Tests
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
PYTHONPATH=src python3 -m unittest discover -s tests # 357 tests, stdlib only
|
|
178
|
+
env -i PATH=/usr/bin:/bin HOME=$(mktemp -d) LANG=C.UTF-8 TMPDIR=$TMPDIR \
|
|
179
|
+
PYTHONPATH=src python3 -m unittest discover -s tests # same suite, scrubbed
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Both must pass. The second is not a formality: `verify` runs a candidate's tests in an
|
|
183
|
+
environment with only `PATH`, a throwaway `HOME`, `LANG` and `TMPDIR`, and six tests in this
|
|
184
|
+
repository used to pass only because the developer's `~/.gitconfig` supplied an author
|
|
185
|
+
identity. A suite that needs the author's machine is not evidence about the tool.
|
|
186
|
+
|
|
187
|
+
Also in the repo: `acceptance/` measures 33 end-to-end requirements (A01–A33) by driving the
|
|
188
|
+
CLI as a subprocess, and `acceptance/gates.py` regenerates `docs/P0-GATE.md` …
|
|
189
|
+
`docs/P4-GATE.md` from real runs.
|
|
190
|
+
|
|
191
|
+
## Verified limitations — do not treat these as solved
|
|
192
|
+
|
|
193
|
+
- Every scanner here is a **deny-list**. It can prove it found something, never that nothing
|
|
194
|
+
exists.
|
|
195
|
+
- Arbitrary private **prose** is undetectable by pattern scanning; that check is `NOT_RUN`
|
|
196
|
+
forever.
|
|
197
|
+
- **Network egress is not contained.** A restricted build passing here is not evidence it
|
|
198
|
+
works offline.
|
|
199
|
+
- **Publisher separation is `NOT_ENFORCED`** on a host where the builder's credential can
|
|
200
|
+
also publish, so a release packet is always `REVIEW_REQUIRED`. See
|
|
201
|
+
`docs/PUBLISHING-CREDENTIALS.md`.
|
|
202
|
+
- No secret-scanner binary is installed, so the deny-list is the whole check. It is a set of
|
|
203
|
+
patterns: a credential in a shape none of them describes is not detected.
|
|
204
|
+
- **History scanning is bounded** at 4 MiB per object. Objects above the bound are not read,
|
|
205
|
+
and the tool refuses to export rather than reporting a clean scan over them. It enumerates
|
|
206
|
+
every ref in the object store, so a fetched branch counts.
|
|
207
|
+
- `doctor` makes no network call and therefore cannot tell you whether a remote repository is
|
|
208
|
+
public.
|
|
209
|
+
- One operating system has been measured (linux-x86_64). No cross-platform claim is made.
|
|
210
|
+
|
|
211
|
+
## Design decisions
|
|
212
|
+
|
|
213
|
+
`docs/adr/` records them, each with its reason and its consequences — including the three
|
|
214
|
+
from the last pass: an acknowledgement binds to an object, the acceptance record redacts
|
|
215
|
+
itself and is not written if it cannot pass, and detectable test fixtures are assembled at
|
|
216
|
+
run time so the repository can pass the very gate it ships.
|
|
217
|
+
|
|
218
|
+
## Licence
|
|
219
|
+
|
|
220
|
+
MIT — see `LICENSE`.
|
|
221
|
+
|
|
222
|
+
Maintained by **Jeff Vandervelde** · [socraticblock.com](https://socraticblock.com) · also
|
|
223
|
+
known as `socraticblock`.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
open_ready-0.1.0.dist-info/licenses/LICENSE,sha256=wbtSyemPsX9eThpSg8Z8HQ7A_Ja7bcxnVDJKA-97JwI,1089
|
|
2
|
+
openready/__init__.py,sha256=Wf9S9ezrp0TZ50h7iJKn5bPtWmPmbX3lm3eWqNkyS6Q,692
|
|
3
|
+
openready/__main__.py,sha256=KTrSspqg95pipAD93bTWZhu8l9ni9rJ1USb_1hI-_hQ,124
|
|
4
|
+
openready/acknowledge.py,sha256=C-28i2O404jev_Kj6qTMkgv22z30gR_PjJYVzdfgIdI,13357
|
|
5
|
+
openready/advisory.py,sha256=2lQClhTgXR6ScHD-NooiTfO9AjJA6zEJehukdkZGkQQ,8842
|
|
6
|
+
openready/audit.py,sha256=LpqUldhcEsRtIkP3sW8_G6UG2MPZ_Es-oqrsAmSgz2E,22624
|
|
7
|
+
openready/backup.py,sha256=9mc03aM0cCvztgtwiIp7yu9dNb9DIy357eRMbAtP7j0,24443
|
|
8
|
+
openready/candidate.py,sha256=4hRP75C4ToiiovrbxmeY7Lyd3YCDavl3ZuvjPb-OaCE,43918
|
|
9
|
+
openready/capabilities.py,sha256=AMuNnPKn9yode_yrzT3iLCdqUdmNHQG2yHjkssDDcxA,13459
|
|
10
|
+
openready/cli.py,sha256=HYBAY2-smGg7kmM0rDurx_xzJBq0fQ56qw51y-AHqg0,37030
|
|
11
|
+
openready/contracts.py,sha256=d-z_GI-hAmJ0ZR5VAV57WRHNgMydIk6u8V1jrvPWdNs,7507
|
|
12
|
+
openready/doctor.py,sha256=hRyW3ilZww9inQOVqr3y2-SJD8DpbhL7AEJDYGlICzM,15056
|
|
13
|
+
openready/external.py,sha256=gXL8HHUfUqYe60vL9lv0to_g96yuNShxDtZQsQ-4K9k,5957
|
|
14
|
+
openready/feature.py,sha256=zsV5l6BzfXw7zLh3py1MSciTblXh1GbelxMvGhtv56o,13065
|
|
15
|
+
openready/history.py,sha256=7SFxjKl1mhWIxTsPoP5nBCqcSNp1mlFTKR8h4uE1tJk,14893
|
|
16
|
+
openready/intake.py,sha256=yZaL7JxPvC76yKpZATmc9NlKPlv0nlUSWndua4Eho-I,10910
|
|
17
|
+
openready/ledger.py,sha256=FbGVRHo9vIVkd0nA2MsC9brBcY7xf1QeDmOWNFY7nvM,26210
|
|
18
|
+
openready/limits.py,sha256=FJgHvxlZjTcZPiyqipkEqFqZhQe9R5p2mP_H3slm-eU,38890
|
|
19
|
+
openready/migrate.py,sha256=fenHTDsnwKNoyiK8NikAd5qdQhAmaE7-QTK2ymzzBVs,7598
|
|
20
|
+
openready/overrides.py,sha256=Hc3-epIItA1CF3a9Akws3mNpp2V8ba8fllCSpLRPz4o,12018
|
|
21
|
+
openready/policy.py,sha256=FQB3Wv2dADh8-qJmYQsUFCGAuFDThDdsWkJVN1JZk8U,31152
|
|
22
|
+
openready/private.py,sha256=mfMfuArEIrcUxS10BedUSY_3__B_QAT9XyywSZT4Fe4,8948
|
|
23
|
+
openready/project.py,sha256=QN4ZRud60lWyaFI1qQL1Q7yVkZGRd9KuUImZpFLqAYk,27336
|
|
24
|
+
openready/promote.py,sha256=AEb-mka7lPWIIGMz2JNUu2_6Cm_0jiD1lBXjZfq6928,13543
|
|
25
|
+
openready/providers.py,sha256=lifopqqXJeTtIROqNp1-I4hDi2xw1QFHi5x2fsOGKRc,11319
|
|
26
|
+
openready/publisher.py,sha256=stjvq0W8XytTlddfoa8OjBy23G7MJHGMex8A7biyD2g,29687
|
|
27
|
+
openready/registry.py,sha256=4fPv7XoEz0kwWDNV6GtQv369V0GchSVvPcPoccwtOtU,8847
|
|
28
|
+
openready/scan.py,sha256=LYpjvI3B56gsRBIGqd2GgFUx7alGjP-6RCecg6RrrI8,28548
|
|
29
|
+
openready/staged.py,sha256=jgtFwAM8Dpt35tNjShwjx91IwWIilS_f5ukIXmcDYvs,9966
|
|
30
|
+
openready/status.py,sha256=JTXM7vyTYhN2hEkplqXZF2Awf_xQF_JRwnKuv_-6ZOA,5078
|
|
31
|
+
openready/templates.py,sha256=nmhiaofPY7QTehQCXFhLwF_hDTHMGRnluehxO7Ec8po,25301
|
|
32
|
+
openready/verify.py,sha256=dBVrEKX7SDfaOS9gYAueEd9LINX50JYDSWCC7KiGSvw,32391
|
|
33
|
+
openready/contracts/project.schema.json,sha256=vrpPddsd6cmTb3NNGYx5KcV77g3-pInX_8uYc-B5qo4,1539
|
|
34
|
+
openready/contracts/release-packet.schema.json,sha256=rqFgi5kb9t3noKcA0aLIC8gP2Ry0swo8XXCV6aAj750,3507
|
|
35
|
+
openready/contracts/result.schema.json,sha256=5fUzFnWPIPD28SJ38dM0X8zIw1slDAjsPIBpMFgdWZ4,1440
|
|
36
|
+
open_ready-0.1.0.dist-info/METADATA,sha256=8QDMfgqubEC2D_U1bz-xx-Ya8t9H4xAYnyswsFNvJ_Y,13677
|
|
37
|
+
open_ready-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
38
|
+
open_ready-0.1.0.dist-info/entry_points.txt,sha256=o_5S43XiBhRc2cke1qKzIJthOYwuXNjsRF4Egl7dOPM,49
|
|
39
|
+
open_ready-0.1.0.dist-info/top_level.txt,sha256=Uilap_ULK8-jSipOPRlDE0CJu_UGHp1Y4FHKPWMm_7s,10
|
|
40
|
+
open_ready-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jeff Vandervelde (socraticblock)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
openready
|
openready/__init__.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Open-Ready -- private-first project care and release-candidate preparation.
|
|
2
|
+
|
|
3
|
+
Specification: "Open-Ready definitive agent handoff v1.0". This package implements
|
|
4
|
+
the P0 foundation: versioned contracts, honest capability detection, private
|
|
5
|
+
project initialisation, and a ``doctor`` that reports what it did not check.
|
|
6
|
+
|
|
7
|
+
Governing rule, stated once here and enforced in code: a run that contains a
|
|
8
|
+
required unrun, unenforced or untested check is **never** reported as green.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
__version__ = "0.1.0-p0"
|
|
12
|
+
POLICY_VERSION = "1.0"
|
|
13
|
+
|
|
14
|
+
# The wording every surface must use. There is no safe-to-publish state.
|
|
15
|
+
PUBLICATION_STATUS = "NOT APPROVED -- requires separate owner review of the exact candidate"
|
openready/__main__.py
ADDED
openready/acknowledge.py
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
"""The adoption path for a repository that already exists.
|
|
2
|
+
|
|
3
|
+
A repository that has been carrying code for a while cannot produce a candidate even
|
|
4
|
+
after its working tree is clean, because its **history** still holds what was cleaned.
|
|
5
|
+
Every blob that ever contained the shape is still in the object store, and the export
|
|
6
|
+
refuses to write a candidate while an unacknowledged hit is there. This is the first
|
|
7
|
+
wall a real user meets, and it is invisible from this tool's own repository, which has
|
|
8
|
+
always been clean.
|
|
9
|
+
|
|
10
|
+
Two different problems, and the tool must not answer them the same way:
|
|
11
|
+
|
|
12
|
+
* A finding in the **working tree** is fixable by editing the file. Acknowledging it
|
|
13
|
+
would be an exception *wider than the reason for it* -- one path, every shape at that
|
|
14
|
+
path, until it expires -- and an unnecessary one. So this command names those findings
|
|
15
|
+
and proposes nothing for them. ``--include-tree`` exists for the case where the shape
|
|
16
|
+
genuinely has to stay, and it says in the entry's own limitations that the exception is
|
|
17
|
+
the wide kind.
|
|
18
|
+
* A finding in the **object store** cannot be fixed without rewriting history, which is
|
|
19
|
+
the owner's decision and not this tool's. So one entry per **blob**: the exact bytes
|
|
20
|
+
somebody looked at, not that path forever. Content committed at the same path later is
|
|
21
|
+
still a finding.
|
|
22
|
+
|
|
23
|
+
Writing is a separate, explicit act (``--write --reason ... --by ...``). The tool cannot
|
|
24
|
+
know that a shape is a fixture; only a human can say so, so it will not say it for them.
|
|
25
|
+
Without ``--write`` this command prints what it would record and changes nothing.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
from datetime import date
|
|
31
|
+
from pathlib import Path
|
|
32
|
+
from typing import Any, Dict, List, Optional, Sequence, Tuple
|
|
33
|
+
|
|
34
|
+
from . import __version__
|
|
35
|
+
from .candidate import inspect as inspect_tree, plan as plan_candidate
|
|
36
|
+
from .history import (
|
|
37
|
+
findings as history_findings,
|
|
38
|
+
is_repository,
|
|
39
|
+
scan as history_scan,
|
|
40
|
+
scanned_completely,
|
|
41
|
+
)
|
|
42
|
+
from .overrides import (
|
|
43
|
+
ALLOW_NAME,
|
|
44
|
+
MIN_REASON_CHARS,
|
|
45
|
+
OVERRIDABLE_CHECK_IDS,
|
|
46
|
+
PLACEHOLDER_REASONS,
|
|
47
|
+
apply as apply_overrides,
|
|
48
|
+
load as load_allow,
|
|
49
|
+
)
|
|
50
|
+
from .status import (
|
|
51
|
+
FAIL,
|
|
52
|
+
INFO,
|
|
53
|
+
NEEDS_HUMAN,
|
|
54
|
+
NOT_RUN,
|
|
55
|
+
PASS,
|
|
56
|
+
REVIEW,
|
|
57
|
+
Result,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
def _environment_label() -> str:
|
|
61
|
+
import os
|
|
62
|
+
return f"{os.uname().sysname.lower()}-{os.uname().machine}"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _result(check_id: str, status: str, message: str, *, severity: str = REVIEW,
|
|
66
|
+
required: bool = False, limitations: Optional[List[str]] = None) -> Result:
|
|
67
|
+
return Result(check_id, status, severity, message, __version__, _environment_label(),
|
|
68
|
+
required=required, limitations=limitations or [])
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _default_by() -> str:
|
|
72
|
+
"""Who is acknowledging, for the entry to carry as evidence.
|
|
73
|
+
|
|
74
|
+
Built from ``os`` and ``getpass`` only. The standard-library calls that look this host
|
|
75
|
+
up are tokens A01 scans this tool's own source for, and a label is not worth a hole in
|
|
76
|
+
a check about not reaching the network -- the name comes from the uname structure.
|
|
77
|
+
"""
|
|
78
|
+
import getpass
|
|
79
|
+
import os
|
|
80
|
+
try:
|
|
81
|
+
who = getpass.getuser()
|
|
82
|
+
except Exception: # pragma: no cover - no passwd entry
|
|
83
|
+
who = "unknown"
|
|
84
|
+
host = (os.uname().nodename or "unknown-host").split(".")[0]
|
|
85
|
+
return f"{who}@{host}"
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def reason_errors(reason: str) -> List[str]:
|
|
89
|
+
"""Whether a reason is usable as somebody's statement about their own project."""
|
|
90
|
+
text = str(reason or "").strip()
|
|
91
|
+
if text.lower() in PLACEHOLDER_REASONS:
|
|
92
|
+
return ["a reason is required: it is the operator's statement, not the tool's"]
|
|
93
|
+
if len(text) < MIN_REASON_CHARS:
|
|
94
|
+
return [f"the reason must be at least {MIN_REASON_CHARS} characters"]
|
|
95
|
+
return []
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def residue(root: Path) -> Tuple[List, Optional[str]]:
|
|
99
|
+
"""Unacknowledged object-store findings, or why they could not be judged.
|
|
100
|
+
|
|
101
|
+
The second element is set when the scan did not complete: a partial scan must never
|
|
102
|
+
be turned into "nothing to acknowledge", because that would acknowledge an
|
|
103
|
+
unexamined history by omission.
|
|
104
|
+
"""
|
|
105
|
+
root = Path(root)
|
|
106
|
+
if not is_repository(root):
|
|
107
|
+
return [], ""
|
|
108
|
+
report = history_scan(root)
|
|
109
|
+
complete, why = scanned_completely(report)
|
|
110
|
+
if not complete:
|
|
111
|
+
return [], why
|
|
112
|
+
hits = history_findings(report)
|
|
113
|
+
if not hits:
|
|
114
|
+
return [], ""
|
|
115
|
+
# Already-acknowledged blobs are not residue. Without this the command proposed an
|
|
116
|
+
# entry for every hit it found, including the ones an earlier run had already
|
|
117
|
+
# recorded, and re-running it grew the file with duplicates of its own output.
|
|
118
|
+
remaining, _ = apply_overrides(root, hits)
|
|
119
|
+
return remaining, ""
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def tree_findings(root: Path) -> List:
|
|
123
|
+
"""Unacknowledged findings in the working tree, which the operator can just fix."""
|
|
124
|
+
plan = plan_candidate(Path(root))
|
|
125
|
+
paths = list(plan.get("included", [])) + list(plan.get("untracked", []))
|
|
126
|
+
if not paths:
|
|
127
|
+
return []
|
|
128
|
+
remaining, _ = apply_overrides(Path(root), inspect_tree(Path(root), paths))
|
|
129
|
+
return [item for item in remaining
|
|
130
|
+
if getattr(item, "check_id", "") in OVERRIDABLE_CHECK_IDS]
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def proposal(findings: Sequence, reason: str, by: str, expires: str = "",
|
|
134
|
+
bind_objects: bool = True) -> List[Dict[str, Any]]:
|
|
135
|
+
"""One entry per finding, object-bound when the finding came from the object store."""
|
|
136
|
+
seen: Dict[Tuple[str, str, str], Dict[str, Any]] = {}
|
|
137
|
+
for finding in findings:
|
|
138
|
+
check_id = str(getattr(finding, "check_id", ""))
|
|
139
|
+
path = str(getattr(finding, "path", ""))
|
|
140
|
+
if check_id not in OVERRIDABLE_CHECK_IDS or not path:
|
|
141
|
+
continue
|
|
142
|
+
obj = str(getattr(finding, "object_id", "") or "") if bind_objects else ""
|
|
143
|
+
entry: Dict[str, Any] = {
|
|
144
|
+
"check_id": check_id,
|
|
145
|
+
"path": path,
|
|
146
|
+
"reason": reason,
|
|
147
|
+
"acknowledged_by": by,
|
|
148
|
+
"acknowledged_at": date.today().isoformat(),
|
|
149
|
+
}
|
|
150
|
+
if obj:
|
|
151
|
+
entry["object"] = obj
|
|
152
|
+
if expires:
|
|
153
|
+
entry["expires"] = expires
|
|
154
|
+
seen[(check_id, path, obj)] = entry
|
|
155
|
+
return [seen[key] for key in sorted(seen)]
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def existing_entries(root: Path) -> List[Dict[str, Any]]:
|
|
159
|
+
document, _ = load_allow(Path(root))
|
|
160
|
+
entries = (document or {}).get("entries", []) if document else []
|
|
161
|
+
return [entry for entry in entries or [] if isinstance(entry, dict)]
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def write(root: Path, entries: Sequence[Dict[str, Any]]) -> Path:
|
|
165
|
+
"""Merge entries into the acknowledgement file, keeping the existing ones.
|
|
166
|
+
|
|
167
|
+
Rewriting the file from the proposal alone would silently delete every earlier
|
|
168
|
+
acknowledgement, which is the sort of quiet data loss an evidence tool cannot afford.
|
|
169
|
+
"""
|
|
170
|
+
import json
|
|
171
|
+
root = Path(root)
|
|
172
|
+
path = root / ALLOW_NAME
|
|
173
|
+
document, _ = load_allow(root)
|
|
174
|
+
document = document or {}
|
|
175
|
+
kept = [entry for entry in document.get("entries", []) or [] if isinstance(entry, dict)]
|
|
176
|
+
keyed = {(str(e.get("check_id", "")), str(e.get("path", "")), str(e.get("object", "") or "")): e
|
|
177
|
+
for e in kept}
|
|
178
|
+
for entry in entries:
|
|
179
|
+
keyed[(str(entry["check_id"]), str(entry["path"]), str(entry.get("object", "") or ""))] = entry
|
|
180
|
+
merged = [keyed[key] for key in sorted(keyed)]
|
|
181
|
+
document["entries"] = merged
|
|
182
|
+
path.write_text(json.dumps(document, indent=2, sort_keys=False) + "\n", encoding="utf-8")
|
|
183
|
+
return path
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def run(root: Path, *, reason: str = "", by: str = "", expires: str = "", write_it: bool = False,
|
|
187
|
+
include_tree: bool = False) -> Tuple[List[Result], Dict[str, Any]]:
|
|
188
|
+
"""Propose, or record, the acknowledgements that adoption needs."""
|
|
189
|
+
root = Path(root)
|
|
190
|
+
results: List[Result] = []
|
|
191
|
+
payload: Dict[str, Any] = {}
|
|
192
|
+
|
|
193
|
+
stored, why = residue(root)
|
|
194
|
+
tree = tree_findings(root) if include_tree else []
|
|
195
|
+
fixable = [] if include_tree else tree_findings(root)
|
|
196
|
+
|
|
197
|
+
payload["object_residue"] = len(stored)
|
|
198
|
+
payload["tree_findings"] = len(fixable) + len(tree)
|
|
199
|
+
|
|
200
|
+
if why:
|
|
201
|
+
results.append(_result(
|
|
202
|
+
"acknowledge.scan-incomplete", NOT_RUN,
|
|
203
|
+
f"the object store could not be scanned completely, so no acknowledgement is "
|
|
204
|
+
f"proposed: {why}",
|
|
205
|
+
severity=REVIEW, required=True,
|
|
206
|
+
limitations=[
|
|
207
|
+
"A partial scan is not a clean one. Acknowledging here would exempt the "
|
|
208
|
+
"part that was never read.",
|
|
209
|
+
"Raise the blob limit or run the scan where the object store is fully "
|
|
210
|
+
"readable, then propose again.",
|
|
211
|
+
],
|
|
212
|
+
))
|
|
213
|
+
return results, payload
|
|
214
|
+
|
|
215
|
+
if not stored and not tree:
|
|
216
|
+
results.append(_result(
|
|
217
|
+
"acknowledge.nothing", PASS,
|
|
218
|
+
"nothing to acknowledge: no unacknowledged finding in the object store or the "
|
|
219
|
+
"working tree is of an overridable kind",
|
|
220
|
+
severity=INFO,
|
|
221
|
+
limitations=[
|
|
222
|
+
"This says nothing about findings that are not overridable. Those are "
|
|
223
|
+
"reported by doctor and candidate and need a human decision, not an "
|
|
224
|
+
"acknowledgement.",
|
|
225
|
+
],
|
|
226
|
+
))
|
|
227
|
+
return results, payload
|
|
228
|
+
|
|
229
|
+
proposed = proposal(stored, reason or "<your reason here>", by or _default_by(), expires)
|
|
230
|
+
if tree:
|
|
231
|
+
proposed += proposal(tree, reason or "<your reason here>", by or _default_by(), expires,
|
|
232
|
+
bind_objects=False)
|
|
233
|
+
|
|
234
|
+
objects = {str(getattr(item, "object_id", "") or "") for item in stored}
|
|
235
|
+
objects.discard("")
|
|
236
|
+
payload["proposal"] = proposed
|
|
237
|
+
payload["would_write"] = ALLOW_NAME
|
|
238
|
+
|
|
239
|
+
if stored:
|
|
240
|
+
results.append(_result(
|
|
241
|
+
"acknowledge.object-residue", NEEDS_HUMAN,
|
|
242
|
+
f"{len(stored)} unacknowledged finding(s) in {len(objects)} object(s) in the "
|
|
243
|
+
f"store, from commits that predate this cleanup -- your history, not your "
|
|
244
|
+
f"working tree",
|
|
245
|
+
severity=REVIEW, required=True,
|
|
246
|
+
limitations=[
|
|
247
|
+
"One entry per blob covers the exact bytes somebody looked at. Content "
|
|
248
|
+
"committed at the same path later is still reported.",
|
|
249
|
+
"These are in history. Fixing them means rewriting history, which is the "
|
|
250
|
+
"owner's decision, not this tool's.",
|
|
251
|
+
],
|
|
252
|
+
))
|
|
253
|
+
if fixable:
|
|
254
|
+
results.append(_result(
|
|
255
|
+
"acknowledge.tree-findings", NEEDS_HUMAN,
|
|
256
|
+
f"{len(fixable)} finding(s) are in the working tree, where you can fix them: "
|
|
257
|
+
+ "; ".join(sorted({f"{item.path} ({item.check_id})" for item in fixable})[:6]),
|
|
258
|
+
severity=REVIEW, required=True,
|
|
259
|
+
limitations=[
|
|
260
|
+
"No acknowledgement is proposed for these. An exception at a path is "
|
|
261
|
+
"wider than the reason for it, and editing the file costs less.",
|
|
262
|
+
"Pass --include-tree to acknowledge them anyway, if the shape has to stay.",
|
|
263
|
+
],
|
|
264
|
+
))
|
|
265
|
+
if tree and include_tree:
|
|
266
|
+
results.append(_result(
|
|
267
|
+
"acknowledge.tree-acknowledged", NEEDS_HUMAN,
|
|
268
|
+
f"{len(tree)} working-tree finding(s) are included in the proposal as "
|
|
269
|
+
f"path-bound entries",
|
|
270
|
+
severity=REVIEW, required=True,
|
|
271
|
+
limitations=[
|
|
272
|
+
"A path-bound entry covers every shape at that path until it expires, "
|
|
273
|
+
"including a real credential committed there later.",
|
|
274
|
+
],
|
|
275
|
+
))
|
|
276
|
+
|
|
277
|
+
if not write_it:
|
|
278
|
+
results.append(_result(
|
|
279
|
+
"acknowledge.nothing-written", PASS,
|
|
280
|
+
"nothing was written. This command proposes; recording is a separate act "
|
|
281
|
+
"(add --write --reason \"...\" --by \"...\")",
|
|
282
|
+
severity=INFO, required=False,
|
|
283
|
+
))
|
|
284
|
+
return results, payload
|
|
285
|
+
|
|
286
|
+
problems = reason_errors(reason)
|
|
287
|
+
if not proposed:
|
|
288
|
+
problems.append("no entry was proposed to write")
|
|
289
|
+
if problems:
|
|
290
|
+
results.append(_result(
|
|
291
|
+
"acknowledge.refused", FAIL,
|
|
292
|
+
"nothing was written: " + "; ".join(problems),
|
|
293
|
+
severity="STOP", required=True,
|
|
294
|
+
limitations=[
|
|
295
|
+
"The reason is the operator's own statement about their project. The tool "
|
|
296
|
+
"cannot tell a fixture from a credential, so it will not write one for you.",
|
|
297
|
+
],
|
|
298
|
+
))
|
|
299
|
+
return results, payload
|
|
300
|
+
|
|
301
|
+
path = write(root, proposed)
|
|
302
|
+
# The residue result described the state *before* the write. Leaving it in place made
|
|
303
|
+
# a successful run report the findings it had just acknowledged as unacknowledged.
|
|
304
|
+
results = [item for item in results if item.check_id != "acknowledge.object-residue"]
|
|
305
|
+
results.append(_result(
|
|
306
|
+
"acknowledge.written", PASS,
|
|
307
|
+
f"recorded {len(proposed)} entr(y/ies) in {path.name}, covering {len(stored)} "
|
|
308
|
+
f"finding(s) in {len(objects)} object(s); the findings they name are now "
|
|
309
|
+
f"acknowledged, and nothing else is",
|
|
310
|
+
severity=INFO,
|
|
311
|
+
limitations=[
|
|
312
|
+
"An acknowledgement says 'I know about this one'. It says nothing about "
|
|
313
|
+
"whether this project may be published.",
|
|
314
|
+
f"Entries that name a blob cover that blob. Re-run candidate with "
|
|
315
|
+
f"--history squash to export a tree that carries none of it.",
|
|
316
|
+
],
|
|
317
|
+
))
|
|
318
|
+
return results, payload
|