rotproof 0.1.0__py3-none-win_amd64.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.
- rotproof-0.1.0.data/scripts/rotproof.exe +0 -0
- rotproof-0.1.0.dist-info/METADATA +452 -0
- rotproof-0.1.0.dist-info/RECORD +8 -0
- rotproof-0.1.0.dist-info/WHEEL +4 -0
- rotproof-0.1.0.dist-info/licenses/LICENSE-APACHE +201 -0
- rotproof-0.1.0.dist-info/licenses/LICENSE-MIT +21 -0
- rotproof-0.1.0.dist-info/licenses/THIRD-PARTY-LICENSES.txt +2870 -0
- rotproof-0.1.0.dist-info/sboms/rotproof.cyclonedx.json +7882 -0
|
Binary file
|
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: rotproof
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
License-File: LICENSE-MIT
|
|
5
|
+
License-File: LICENSE-APACHE
|
|
6
|
+
License-File: THIRD-PARTY-LICENSES.txt
|
|
7
|
+
Summary: Keeps a project's structure from drifting: checks its layers and the records an agent works from (backlog, specs, knowledge, log)
|
|
8
|
+
License-Expression: MIT OR Apache-2.0
|
|
9
|
+
Requires-Python: >=3.9
|
|
10
|
+
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
|
|
11
|
+
Project-URL: Repository, https://github.com/secta113/rotproof
|
|
12
|
+
|
|
13
|
+
# Rotproof
|
|
14
|
+
|
|
15
|
+
Rotproof keeps a project's structure from drifting while LLMs and people change it, so that it does not rot. It keeps
|
|
16
|
+
two structures:
|
|
17
|
+
|
|
18
|
+
- **The layers:** which part of the code may import which (`handler`, `application`, `domain`, `infrastructure`,
|
|
19
|
+
`utils`, and an optional `ui`). Code that cannot be split into these layers mixes responsibilities, so the layers
|
|
20
|
+
are how an LLM, or a person, is made to split it: every piece of code has to land in a layer whose role and allowed
|
|
21
|
+
imports are written down. Rotproof makes them when a project starts and checks them on every run, so the direction
|
|
22
|
+
of dependencies stays what it was meant to be.
|
|
23
|
+
- **The specs and records:** what is open, what is being changed, how things are now, and why. A backlog (open
|
|
24
|
+
defects and postponed work, each with a trigger, a state and a deadline), specs, knowledge documents (an API, a data
|
|
25
|
+
model, a decision, edited in place and named in the log at every edit), and a log, each kept to strict rules. A
|
|
26
|
+
finding does not stay outside them: a `TODO` or `NOTE` in a code comment fails the check, and a stop hook sends an
|
|
27
|
+
agent back when its last message leaves something open that it did not record.
|
|
28
|
+
|
|
29
|
+
## Principles
|
|
30
|
+
|
|
31
|
+
- **The declaration is the truth, and Rotproof does not repair.** The structure a project declares is the structure. A
|
|
32
|
+
tree that differs from it fails, either way, and someone decides whether the tree or the declaration is wrong;
|
|
33
|
+
Rotproof changes the tree only when asked.
|
|
34
|
+
- **Every rule has a check that can fail.** A rule without a check is only a label, and soon drifts.
|
|
35
|
+
- **A check with nothing to check fails.** A check that passes on an empty tree protects nothing.
|
|
36
|
+
- **The rules come with the tool.** A project pins one version of Rotproof, and takes improvements to the rules by
|
|
37
|
+
upgrading it, in a commit of its own.
|
|
38
|
+
- **The same structure in every language.** Rotproof is one binary with no language runtime, so a Rust or TypeScript
|
|
39
|
+
project keeps the same layers and records as a Python one.
|
|
40
|
+
- **Whoever picks up the work next reads one index, not every file.** The index files are generated, never written by
|
|
41
|
+
hand.
|
|
42
|
+
|
|
43
|
+
Rotproof makes the layer directories, checks that they are where the project declares them, and checks their
|
|
44
|
+
direction: from the imports in Python and TypeScript, and from the dependencies each crate declares in Rust.
|
|
45
|
+
|
|
46
|
+
The records live in `docs/`, which is a bundle in [OKF 0.2](https://github.com/GoogleCloudPlatform/open-knowledge-format)
|
|
47
|
+
(its `SPEC.md` as of commit `ad30107`): every document has YAML frontmatter with a `type`, `index.md` and `log.md` are
|
|
48
|
+
reserved names, and the log's headings are dates. An OKF reader can read the records as a bundle. The rules on top of
|
|
49
|
+
that format (a backlog item's trigger, state and deadline, a closed record opening with its resolution, a knowledge
|
|
50
|
+
document's hash in the log, the shape of a log entry) are Rotproof's own, stricter than OKF, and not part of it.
|
|
51
|
+
Rotproof is not an OKF validator.
|
|
52
|
+
|
|
53
|
+
## Install
|
|
54
|
+
|
|
55
|
+
Rotproof is released for Windows x86_64 and Linux x86_64 (glibc 2.17 or newer). Pin the exact version, so that an
|
|
56
|
+
upgrade, which can bring new rules, is a commit of its own:
|
|
57
|
+
|
|
58
|
+
```sh
|
|
59
|
+
pip install rotproof==0.1.0
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
The wheel carries only the binary; no Python code runs. Without Python, take the archive for your platform from
|
|
63
|
+
[GitHub Releases](https://github.com/secta113/rotproof/releases) and check it against `SHA256SUMS` there. Each archive
|
|
64
|
+
holds the binary with `LICENSE-MIT`, `LICENSE-APACHE` and `THIRD-PARTY-LICENSES.txt`.
|
|
65
|
+
|
|
66
|
+
On another platform, pip finds no Rotproof to install. Build it from source with [rustup](https://rustup.rs/) installed
|
|
67
|
+
(the toolchain version comes from `rust-toolchain.toml`):
|
|
68
|
+
|
|
69
|
+
```sh
|
|
70
|
+
cargo build --release # the binary is target/release/rotproof (rotproof.exe on Windows)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Usage
|
|
74
|
+
|
|
75
|
+
```sh
|
|
76
|
+
rotproof --root <repository> init --stack python # write .config/rotproof.toml, once
|
|
77
|
+
rotproof --root <repository> create # make the layers .config/rotproof.toml declares, the records skeleton and the guide
|
|
78
|
+
rotproof --root <repository> check # check the layers and the records; exits 1 when a rule is broken
|
|
79
|
+
rotproof --root <repository> index # write every generated file in docs/ (the index files and the rules)
|
|
80
|
+
rotproof guide --stack python # print the rules Rotproof keeps for a stack, with or without a project
|
|
81
|
+
rotproof stop-hook # run by Claude Code when the agent stops (see "The stop hook")
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
`--root` defaults to the current directory. The declaration is read from `<repository>/.config/rotproof.toml`, and the
|
|
85
|
+
records from `<repository>/docs`.
|
|
86
|
+
|
|
87
|
+
The binary leads the way without this README, for an agent that has only Rotproof: `rotproof --help` says what it is,
|
|
88
|
+
the order to start in and the exit codes, `rotproof <command> --help` what a command reads, writes and never does, and
|
|
89
|
+
each command's output names the next step. A test follows that path from an empty directory to a passing check.
|
|
90
|
+
|
|
91
|
+
Start a project with `rotproof init --stack <stack>` (`python`, `typescript`, `rust`, or `none` for a repository
|
|
92
|
+
that keeps records only). It writes only the declaration, so you declare in `absent` the layers you do not want before
|
|
93
|
+
anything is made, and it never overwrites a declaration that exists. Then run `rotproof create`.
|
|
94
|
+
|
|
95
|
+
A TypeScript project with React starts from Vite: run `npm create vite@latest <name> -- --template react-ts` first,
|
|
96
|
+
then `rotproof init --stack typescript` and `rotproof create` in it, which keep what Vite wrote. Vite's entry point,
|
|
97
|
+
`src/main.tsx`, is `handler`'s where it is (`index.html` loads it); `rotproof check` names the rest of the starter
|
|
98
|
+
(`App.tsx`, the styles, `assets/`) as code outside the layers, with where each goes.
|
|
99
|
+
|
|
100
|
+
Run `rotproof create` when a project starts, and again after you change `.config/rotproof.toml` on purpose. It makes
|
|
101
|
+
only what is missing: a layer that is neither present nor declared absent, the project's files when they do not exist
|
|
102
|
+
(below), and the files Rotproof generates (`.rotproof/AGENTS.md`, and the index files and rules in `docs/`). It never
|
|
103
|
+
overwrites a file it does not generate, and never moves or deletes one. Nothing runs it on its own, so a layer removed
|
|
104
|
+
`xtask/src/layers.rs` in place of the file, and reads the comments of `xtask/` for markers too, which the layout
|
|
105
|
+
leaves out; the records are kept outside, so they are not checked here.
|
|
106
|
+
|
|
107
|
+
The project's files are written once, as a starting point, and are the project's from then on:
|
|
108
|
+
|
|
109
|
+
| File | Content |
|
|
110
|
+
|---|---|
|
|
111
|
+
| `AGENTS.md` | The map (the layers present, with their roles to rewrite), the differences from what Rotproof keeps, and the project's own rules. It points at `.rotproof/AGENTS.md` |
|
|
112
|
+
| `CLAUDE.md` | `@AGENTS.md` and `@.rotproof/AGENTS.md` |
|
|
113
|
+
| `README.md` | The project's name (its root directory's) and how to run Rotproof |
|
|
114
|
+
| `.gitignore`, `.gitattributes` | For the stack; line endings as LF |
|
|
115
|
+
| `docs/log.md` | The log, with its title |
|
|
116
|
+
| `.claude/settings.json` | The stop hook (see "The stop hook") |
|
|
117
|
+
| `requirements-dev.txt` | `python` and `none`: Rotproof pinned with `==` |
|
|
118
|
+
| `.github/workflows/ci.yml` | `python` and `none`: installs `requirements-dev.txt` and runs `rotproof check`, with a time limit |
|
|
119
|
+
| `Cargo.toml` | `rust`: the workspace, whose members are the crates in `crates/`, so `cargo build` builds every layer present |
|
|
120
|
+
|
|
121
|
+
How a `typescript` or `rust` project pins Rotproof is not decided yet, so for those stacks `rotproof create` writes
|
|
122
|
+
neither the pin nor the workflow, and says so.
|
|
123
|
+
|
|
124
|
+
Run it also after upgrading Rotproof. When a newer Rotproof requires a field the declaration lacks, `rotproof check`
|
|
125
|
+
fails and says so, and `rotproof create` adds the field under a comment that says what it is and where its first value
|
|
126
|
+
came from (`areas` gets the tags the records use, sorted by name), keeping every comment and value already there. A
|
|
127
|
+
value that is present is never changed, so an upgrade fails only on what the new rules find.
|
|
128
|
+
|
|
129
|
+
Among the files Rotproof generates is `.rotproof/AGENTS.md`: the rules Rotproof keeps, written for the project's
|
|
130
|
+
stack. It says how to run Rotproof, lists the layers with where each lives and what it may import (from
|
|
131
|
+
`layers/table.toml`), and gives the rules of the records, starting with reading the index files before work. It names
|
|
132
|
+
the Rotproof version that wrote it, so after an upgrade `rotproof check` fails until `rotproof create` has rewritten
|
|
133
|
+
it. Edited by hand, it fails too: a project's own rules go in its own `AGENTS.md`.
|
|
134
|
+
|
|
135
|
+
The guide only helps if your agent reads it. The `AGENTS.md` and `CLAUDE.md` that `rotproof create` writes point at it;
|
|
136
|
+
a project that has its own points at it from its `AGENTS.md` (or whatever file its agent reads first), and imports it
|
|
137
|
+
in `CLAUDE.md` for Claude Code:
|
|
138
|
+
|
|
139
|
+
```markdown
|
|
140
|
+
@AGENTS.md
|
|
141
|
+
@.rotproof/AGENTS.md
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## The stop hook
|
|
145
|
+
|
|
146
|
+
An agent's findings are lost when it reports them and stops: "not checked", "out of scope" in its last message, and
|
|
147
|
+
nothing in the backlog. `rotproof stop-hook` is [Claude Code's `Stop` hook](https://code.claude.com/docs/en/hooks),
|
|
148
|
+
run when the agent stops, and reads that last message. When the message holds a phrase that leaves something open
|
|
149
|
+
and `git status` shows no change in `docs/`, the hook sends the agent back once, asking it to record the finding or to
|
|
150
|
+
say in one line where it already is. While the agent is
|
|
151
|
+
continuing because of a stop hook, the hook lets it stop, so it never loops. A line that points at the records (the
|
|
152
|
+
word `spec`, `backlog` or `knowledge`) is not read: what it leaves open is recorded where it points. The
|
|
153
|
+
phrases are built in (Japanese and English); the agent decides what each one meant.
|
|
154
|
+
|
|
155
|
+
`rotproof create` writes `.claude/settings.json` with the hook when it does not exist. A project that has one adds the
|
|
156
|
+
hook to it:
|
|
157
|
+
|
|
158
|
+
```json
|
|
159
|
+
{
|
|
160
|
+
"hooks": {
|
|
161
|
+
"Stop": [{ "hooks": [{ "type": "command", "command": "rotproof stop-hook" }] }]
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
`rotproof` has to be on the `PATH` the agent runs hooks with (for a venv, start the agent with the venv active). The
|
|
167
|
+
project is the nearest directory upwards that holds `.config/rotproof.toml`; outside one, the hook says nothing. A
|
|
168
|
+
hook that fails (not a git repository, an input from another hook) exits 1, which both agents show without keeping
|
|
169
|
+
the agent from stopping; exit code 2 would keep it from stopping.
|
|
170
|
+
|
|
171
|
+
## The layers
|
|
172
|
+
|
|
173
|
+
A project declares its structure in `.config/rotproof.toml`, the directory tools share for their configuration.
|
|
174
|
+
`rotproof init` writes it, and from then on it is the project's file. pip installs only the Rotproof binary: the layer
|
|
175
|
+
definitions are built into it, and the declaration is never shipped with it.
|
|
176
|
+
|
|
177
|
+
```toml
|
|
178
|
+
stack = "python" # python | typescript | rust | none
|
|
179
|
+
areas = ["billing", "records"] # the areas the records are grouped by, in this order (see The records)
|
|
180
|
+
absent = ["ui"] # layers this project does not have
|
|
181
|
+
unchecked = ["scripts"] # paths outside the layers that Rotproof does not look into
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
What the layers are (`handler`, `ui`, `application`, `infrastructure`, `domain`, `utils`, and the atomic levels of
|
|
185
|
+
`ui`: `pages`, `templates`, `organisms`, `molecules`, `atoms`) is written once, in `layers/table.toml`. Where they live
|
|
186
|
+
is written once per stack:
|
|
187
|
+
|
|
188
|
+
| Stack | A layer is | A `ui` level is | Code Rotproof looks at |
|
|
189
|
+
|---|---|---|---|
|
|
190
|
+
| `python` | `<layer>/__init__.py`, the role as its docstring | `ui/<level>/__init__.py` | `.py` files anywhere (`.PY` too), except `tests/` |
|
|
191
|
+
| `typescript` | `src/<layer>/index.ts`, the role as a doc comment | `src/ui/<level>/index.ts` (React) | every file in `src/`; `src/main.tsx` (Vite's entry point) is `handler`'s |
|
|
192
|
+
| `rust` | a crate, `crates/<layer>/` (`handler` a binary), the role as `//!` | none: Rust has no `ui` yet | every crate in `crates/` |
|
|
193
|
+
|
|
194
|
+
`stack = "none"` declares a repository that keeps records only: `rotproof create` makes only `docs/`, and `Rotproof
|
|
195
|
+
check` checks only the records and prints that it did not check the layers. It is a line in the declaration, not a
|
|
196
|
+
flag, so the structure check is never switched off where the declaration still declares layers.
|
|
197
|
+
|
|
198
|
+
A level of `ui` is declared absent by its dotted name (`absent = ["ui.templates"]`). Files `.gitignore` excludes and
|
|
199
|
+
hidden files are not looked at, so a virtual environment or a build directory is not code. A code file is matched in any
|
|
200
|
+
case (`stray.PY`, `cargo.toml`): Windows runs or reads it all the same.
|
|
201
|
+
|
|
202
|
+
## The records
|
|
203
|
+
|
|
204
|
+
```
|
|
205
|
+
docs/
|
|
206
|
+
index.md generated
|
|
207
|
+
log.md what was done, newest first
|
|
208
|
+
backlog/
|
|
209
|
+
rules.md generated: the backlog rules (type: Guide)
|
|
210
|
+
index.md generated
|
|
211
|
+
<slug>.md one item per file (type: Backlog Item)
|
|
212
|
+
specs/ every spec, open or closed (type: Spec, status: draft, stable or deprecated)
|
|
213
|
+
rules.md generated: the spec rules (type: Guide)
|
|
214
|
+
index.md generated
|
|
215
|
+
knowledge/ how things are now, and why (type: Knowledge, status: stable or deprecated)
|
|
216
|
+
rules.md generated: the knowledge rules (type: Guide)
|
|
217
|
+
index.md generated
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
The three directories answer three questions: what is being changed (`specs/`, closed once implemented or dropped),
|
|
221
|
+
what is open (`backlog/`, closed once dealt with), and how things are now (`knowledge/`, edited in place, deprecated
|
|
222
|
+
only when it no longer holds). A closed spec is history; what it built is described in `knowledge/`, an API or a data
|
|
223
|
+
model, or why something was decided. Every edit of a knowledge document is named in the log by a hash of its
|
|
224
|
+
contents, under the label `**Knowledge**` (`* **Knowledge**: knowledge/api.md@a3f9c1d2`), and `rotproof check` fails
|
|
225
|
+
an edit the log does not name.
|
|
226
|
+
|
|
227
|
+
A record stays where it was written when it closes: its status says it is closed, and the index lists it under
|
|
228
|
+
`# Closed`. Its path, and every link to it, never changes, so closing a record is a change to that record and its
|
|
229
|
+
index line only.
|
|
230
|
+
|
|
231
|
+
Every backlog item and every spec belongs to exactly one area: its only tag, one of the `areas` the declaration
|
|
232
|
+
lists. The index files group by area, in the order of `areas`, so the project puts the largest or most active area
|
|
233
|
+
first. An area says where a record belongs (the layers, the records, billing), and never closes. A declared area that
|
|
234
|
+
no record uses passes, so an area is declared before its first record. Renaming an area is editing `areas` and the tag
|
|
235
|
+
of every record in it, closed ones included: the tag is frontmatter for the index, not history. Guides keep their
|
|
236
|
+
optional tags, which name no area.
|
|
237
|
+
|
|
238
|
+
A large piece of work is split into specs that are parts of an epic. The epic is a spec like any other, with goals and
|
|
239
|
+
an order of work of its own; a part names it by slug (its file name without `.md`) in `epic`:
|
|
240
|
+
|
|
241
|
+
```yaml
|
|
242
|
+
type: Spec
|
|
243
|
+
title: Publish Rotproof for every stack
|
|
244
|
+
status: stable
|
|
245
|
+
tags: [rotproof]
|
|
246
|
+
epic: template-multi-stack
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
The two are independent: an area says where a record belongs and never closes, an epic says which piece of work a
|
|
250
|
+
spec is part of and closes when the work is finished. A part may be in another area than its epic. A spec that cannot
|
|
251
|
+
name one area mixes two, so it is split into one part per area, and the epic ties the parts back into one piece of
|
|
252
|
+
work. In an index, a part in the same area as its epic, and open or closed as its epic is, is listed under it,
|
|
253
|
+
indented; any other part is listed on its own with `Epic: [<title>](...)` after its line, so each spec appears once.
|
|
254
|
+
|
|
255
|
+
A backlog item has this frontmatter and these body headings:
|
|
256
|
+
|
|
257
|
+
```markdown
|
|
258
|
+
---
|
|
259
|
+
type: Backlog Item
|
|
260
|
+
title: Some problem
|
|
261
|
+
description: One sentence: what the problem is.
|
|
262
|
+
tags: [area] # exactly one, declared in areas; the index groups items by it
|
|
263
|
+
status: stable # stable = open, deprecated = closed
|
|
264
|
+
filed: 2026-10-01
|
|
265
|
+
verified: {by: human:someone, at: 2026-10-01T10:00:00+09:00}
|
|
266
|
+
deadline_kind: until # until, or none with the reason in deadline
|
|
267
|
+
deadline: until the next deploy
|
|
268
|
+
stale_after: 2027-01-01T00:00:00+09:00 # optional: when to measure the state again
|
|
269
|
+
---
|
|
270
|
+
|
|
271
|
+
# Resolution (only when closed, and then first)
|
|
272
|
+
# Trigger
|
|
273
|
+
# State
|
|
274
|
+
# Details
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
## What `rotproof check` checks
|
|
278
|
+
|
|
279
|
+
- **The tree matches `.config/rotproof.toml`, either way:** the declaration exists and names a known stack, and only
|
|
280
|
+
layers that stack has in `absent` (a misspelled field fails). Every layer of the stack is present or declared
|
|
281
|
+
absent, and no layer declared absent is present. No code sits outside the layers, the stack's own paths (`tests/`)
|
|
282
|
+
and `unchecked`. A path in `unchecked` exists and neither holds nor sits in a layer, so a layer cannot be switched
|
|
283
|
+
off by listing it. At least one layer is present: with every layer declared absent, nothing would be checked.
|
|
284
|
+
`ui` holds only its levels: code in `ui` beside them fails, apart from the layer's own file (`ui/__init__.py`).
|
|
285
|
+
What the whole UI shares goes in a level: a part that knows no project concept, visible or not (a design value, one
|
|
286
|
+
behaviour, a provider of a theme), is an atom.
|
|
287
|
+
- **The layers import only what the table allows** (Python): every `import` and `from ... import` in the layers,
|
|
288
|
+
relative ones and those inside functions or under `if TYPE_CHECKING:` included. A layer may import itself and the
|
|
289
|
+
layers in its `imports`; a level of `ui` the levels below it and the layers in its `imports`. Only direct imports
|
|
290
|
+
are judged: what the table allows is closed under chaining, so a chain of allowed imports never reaches a forbidden
|
|
291
|
+
layer. A module is placed where its parts, as a path, land as the operating system that runs the check resolves
|
|
292
|
+
them: on Windows, `Infrastructure.db` lands in `infrastructure`, as Python imports it with `PYTHONCASEOK` set.
|
|
293
|
+
Imports of modules in no layer (the standard library, packages) are not judged, and imports built at run time
|
|
294
|
+
(`importlib`) are not seen. A file that is not UTF-8 or has a syntax error fails, since its imports cannot all be
|
|
295
|
+
read.
|
|
296
|
+
- **The layers import only what the table allows** (TypeScript): every `import` (`import type` too), `export ...
|
|
297
|
+
from`, `import x = require(...)`, and `import(...)` and `require(...)` with a literal string, in the `.ts`, `.tsx`,
|
|
298
|
+
`.js` and `.jsx` files (and `.mts`, `.cts`, `.mjs`, `.cjs`) of the layers, read with
|
|
299
|
+
[oxc](https://oxc.rs/). The place of an import is the path it lands on, whether or not a file is there: a relative
|
|
300
|
+
specifier from the file, one starting with `/` from the root (as Vite reads it), and any other through
|
|
301
|
+
`compilerOptions.paths` of the `tsconfig*.json` files at the root (and the local files they extend), then through
|
|
302
|
+
their `baseUrl` when a module is there; otherwise it names a package, which is not judged. The path is placed where
|
|
303
|
+
it lands as the operating system that runs the check resolves it: on Windows, `../Infrastructure/db`,
|
|
304
|
+
`../infrastructure./db` and a short name (`../INFRAS~1/db`) land in `infrastructure`, as Vite builds them, and
|
|
305
|
+
links are followed everywhere. A `tsconfig*.json` that
|
|
306
|
+
cannot be read, or a `paths` entry with more than one `*`, fails rather than leaving its aliases unjudged. Two
|
|
307
|
+
limits: only the first target of a `paths` entry is used, and a config extended from a package is not read, so an
|
|
308
|
+
alias defined only there is taken for a package.
|
|
309
|
+
- **The layers import only what the table allows** (Rust): a crate is compiled only against the crates its
|
|
310
|
+
`Cargo.toml` declares, so the declarations are read instead of `use`: every dependency in `[dependencies]` and
|
|
311
|
+
`[build-dependencies]` (and `build_dependencies`), under `[target.<cfg>]` too, of the `Cargo.toml` files in the
|
|
312
|
+
layers. The place of a dependency is where the path it comes from lands: its `path` from the crate, or with
|
|
313
|
+
`workspace = true` the `path` of its entry in `[workspace.dependencies]` from the workspace, which is the directory
|
|
314
|
+
`[package] workspace` names, or the nearest one up to the root that declares `[workspace]`. A path lands where the
|
|
315
|
+
operating system that runs the check resolves it, as Cargo's does: on Windows, a path in another case, with dots or
|
|
316
|
+
spaces at its end or with a short name (`DOMAIN~1`) lands in `domain`, and links are followed everywhere. A path
|
|
317
|
+
that lands nowhere, where Cargo fails too, or outside the root is not judged. A renamed dependency (`package`) is
|
|
318
|
+
placed by its path all the same. `[dev-dependencies]` serve the tests and are not judged; dependencies from a
|
|
319
|
+
registry or git are not judged. A `Cargo.toml` that is not UTF-8 or
|
|
320
|
+
TOML fails, and so does a dependency taken from a workspace that does not declare it. Not seen: `[patch]` and
|
|
321
|
+
`[replace]`, and source files taken from another crate's directory (`#[path]`, `include!`, `[lib] path`).
|
|
322
|
+
- **No comment holds `TODO`, `FIXME`, `XXX`, `HACK` or `NOTE`**: in upper case, as whole words, in any code file
|
|
323
|
+
outside `unchecked` (a path there that holds or sits in a layer skips nothing), `tests/` included; in TypeScript, the `//` and `/* */` comments of the source files in `src/`,
|
|
324
|
+
JSX text not counted; in Rust, the `//` and `/* */` comments (doc comments too) of every `.rs` file in `crates/`,
|
|
325
|
+
a crate's `tests/` and `build.rs` included, read by Rotproof's own scanner, which skips strings, raw strings and
|
|
326
|
+
character literals as Rust's lexer does. Work left to do belongs in the backlog, where it is listed and closed, and
|
|
327
|
+
a decision with its reason in the spec or the log entry of the change; a comment that explains how to read the code
|
|
328
|
+
stays, without the word. Only comments count: `Status.TODO` and `"XXX-XXXX"` are not markers, and docstrings are
|
|
329
|
+
strings. Comments in other files (a stylesheet, a `Cargo.toml`) are not read.
|
|
330
|
+
|
|
331
|
+
- **The bundle is there:** `docs/`, `docs/index.md`, `docs/backlog/`, `docs/backlog/rules.md`, `docs/specs/`,
|
|
332
|
+
`docs/specs/rules.md`, `docs/knowledge/` and `docs/knowledge/rules.md` exist, and the declaration with its `areas`
|
|
333
|
+
can be read. Without them every other check
|
|
334
|
+
would pass with nothing checked.
|
|
335
|
+
- **Every name is compared exactly,** wherever Rotproof looks for a file or a directory: a link's target, the files
|
|
336
|
+
above, the layers and the declaration. Rotproof reads the names the directories hold instead of asking the
|
|
337
|
+
operating system, which on Windows also finds `README.md` for `readme.md`, `README.md.`, `README.md `, a stream
|
|
338
|
+
(`README.md:secret`) or a short name (`README~1.MD`). Linux and GitHub find none of them, so each fails, and the
|
|
339
|
+
message says what the disk has.
|
|
340
|
+
- **The areas are distinct headings:** none is empty, has a space at either end or a line break, and no two differ only in case.
|
|
341
|
+
- **Every backlog item keeps the format:** the fields above with their types, and non-empty Trigger, State and Details
|
|
342
|
+
(and Resolution when closed, as the first heading). A field Rotproof does not know passes as an extension, as OKF allows, unless it looks
|
|
343
|
+
like a misspelling of a field the document type has (`stale_afer`, `staleAfter`, `Title`), or is a field only
|
|
344
|
+
another type has (a backlog item's `deadline` on a spec, a spec's `epic` on a backlog item): those fail, in every
|
|
345
|
+
document type, as a misspelled optional field would otherwise be silently dropped. A deadline is an event or a reason, never only a date
|
|
346
|
+
(`2026-10-31`, `2026/10/31`, `31.10.2026`, `2026年10月31日` or a month alone); an event may contain a date. Every
|
|
347
|
+
time has a time zone. `stale_after` is later than the last `verified`. A required text is not blank (spaces alone
|
|
348
|
+
are empty), in every document type, and no tag is empty. What the index lists (`title`, `description`, `deadline`
|
|
349
|
+
and the tags) is on one line: a line break would end the entry and start a heading or an entry of its own. A
|
|
350
|
+
title is written into the index with `[`, `]` and `\` escaped, so it stays the text of its own link. The one tag
|
|
351
|
+
is a declared area, and the message lists the declared ones.
|
|
352
|
+
- **Every link in `# Details` resolves,** with heading anchors computed as GitHub computes them. A path with a drive
|
|
353
|
+
letter (`C:/...`) or a `file:` URL fails: it names a file on one machine. Only a URL (`https:`, `mailto:`) is not
|
|
354
|
+
checked, and a file name with a line number (`check.rs:104`) is a path, not a URL. A path is separated with `/`:
|
|
355
|
+
only Windows reads a backslash as a separator, so a path with one fails. A path that climbs above the repository
|
|
356
|
+
fails: GitHub serves only the repository. Links are read as a CommonMark reader reads them: with a title
|
|
357
|
+
(`[a](b.md "title")`), in angle brackets, as reference links defined in `# Details`, as images, and as `href` in
|
|
358
|
+
HTML, and not inside code or comments. A link to a `.py` file names, in its text, a function
|
|
359
|
+
or class defined there, at any depth. The file is parsed as Python, so a `def` line inside a string or a docstring
|
|
360
|
+
does not count. The text is one name exactly as defined (``[`render_index`](../tests/bundle.py)``), not a call
|
|
361
|
+
(`render_index()`) or a dotted path (`Bundle.render`); when it names nothing, the message says which name to write
|
|
362
|
+
or lists the names the file defines.
|
|
363
|
+
- **Every spec keeps the format:** `title`, `description`, `status` and exactly one declared area in `tags`, and a
|
|
364
|
+
non-empty `# Resolution` as the first heading once it is closed. `epic`, when present, is the slug of another spec in `docs/specs/` (not
|
|
365
|
+
a path, not a backlog item or a guide, not the spec itself), and that spec has no `epic` of its own: one level only.
|
|
366
|
+
A spec that breaks one of these is left out of the index files.
|
|
367
|
+
- **An epic closes after its parts:** no closed epic has an open part. A part that is dropped closes as dropped, as
|
|
368
|
+
any spec does.
|
|
369
|
+
- **The log exists, points only at backlog items that exist** (`backlog/<slug>.md`, written with `/` or `\`, the
|
|
370
|
+
slug percent-encoded or not), and its second-level headings are dates, newest first. Below the title, the log is
|
|
371
|
+
a flat list of entries grouped under the dates (OKF 0.2, section 9): every entry is a list item, and its indented
|
|
372
|
+
lines (wrapped text, nested items) belong to it. A task heading (`### ...`), a paragraph, or an entry before the
|
|
373
|
+
first date fails. A new log, with only its title and HTML comments, passes.
|
|
374
|
+
- **Every knowledge document keeps the format,** as a spec does (without `epic`, and `status` `stable` or
|
|
375
|
+
`deprecated`), and **the log names it as it is now:** some `**Knowledge**` field of a log entry, with its wrapped
|
|
376
|
+
lines, names it with the first 8 hex digits of SHA-256 of the whole file (every line ending as `\n`). An edit the
|
|
377
|
+
log does not name fails, and the failure prints the line to write. A `**Knowledge**` field that names a document
|
|
378
|
+
`docs/knowledge/` does not have fails too.
|
|
379
|
+
- **Every document is a known type in its directory.** The fields OKF defines for a document pass as OKF writes them
|
|
380
|
+
(`generated` needs only `by`; every entry of `sources` needs a `resource`; `usage_window` is a `{from, to}` range).
|
|
381
|
+
The names OKF reserves appear only where Rotproof writes and reads them: `index.md` in `docs/` and in each directory
|
|
382
|
+
of documents, `log.md` in `docs/`. A markdown file is named `.md`, in lowercase: GitHub shows a `.MD` file, but
|
|
383
|
+
Rotproof would not read it.
|
|
384
|
+
- **Every file Rotproof generates equals what `rotproof index` writes:** the index files, so nobody maintains a list by
|
|
385
|
+
hand, and `docs/backlog/rules.md` and `docs/specs/rules.md`, so the rules a project reads are the rules its Rotproof
|
|
386
|
+
checks. A project's own rules go in another guide in `docs/backlog/` or `docs/specs/`.
|
|
387
|
+
- **No spec sits at the repository root.**
|
|
388
|
+
|
|
389
|
+
The frontmatter is read as YAML 1.2: quoting a value never changes whether it passes. Its closing `---` may end the
|
|
390
|
+
file. The body is read as GitHub
|
|
391
|
+
renders it: an HTML comment or a fenced code block (``` or ~~~, indented by up to 3 spaces, running to the end when
|
|
392
|
+
it is not closed) is not text, so a heading, a link or the only text of a section inside one counts for nothing. A
|
|
393
|
+
heading may be indented by up to 3 spaces, and a closing run of `#` (`## Notes ##`) is not part of it.
|
|
394
|
+
|
|
395
|
+
## Development
|
|
396
|
+
|
|
397
|
+
`cargo xtask ci` runs format, lint and tests, the same checks as CI. It also fails when the map in `AGENTS.md`
|
|
398
|
+
misses a tracked top-level path or a module of a crate in `crates/` (or names one that is gone), when
|
|
399
|
+
`rust-toolchain.toml`, the `Dockerfile` and the CI workflow name different toolchain versions, and when
|
|
400
|
+
`THIRD-PARTY-LICENSES.txt` does not list the crates the binary links (below), and when Rotproof's own crates break
|
|
401
|
+
the rules of the Rust layout it keeps: a crate outside the layers, a dependency the table does not allow, or a marker
|
|
402
|
+
in a comment of `crates/` or `xtask/`. On Windows the host needs Visual
|
|
403
|
+
Studio's C++ tools and the Windows SDK. Everything also runs in the container (`compose.yaml`), where the host needs
|
|
404
|
+
only Docker: `docker compose run --rm dev cargo xtask ci`.
|
|
405
|
+
|
|
406
|
+
The binary links the crates Rotproof depends on, and their licenses require their notices to go with it.
|
|
407
|
+
`THIRD-PARTY-LICENSES.txt` holds them: every crate the binary links on any platform, with the license text each one
|
|
408
|
+
ships. Rotproof's own crates in `crates/` (`publish = false`) are Rotproof, and are not listed. It is generated by
|
|
409
|
+
[cargo-about](https://github.com/EmbarkStudios/cargo-about) from `about.toml` and the template `about.hbs`, never
|
|
410
|
+
edited by hand:
|
|
411
|
+
|
|
412
|
+
```sh
|
|
413
|
+
cargo xtask licenses # after any change to the dependencies; commit the file with the change
|
|
414
|
+
cargo xtask licenses --check # fails where the file differs from what cargo-about writes now
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
Both need cargo-about at the version the `Dockerfile` installs (`cargo install --locked --features cli
|
|
418
|
+
cargo-about@<version>`); the container has it. The generation fails when a crate's license cannot be met from
|
|
419
|
+
`accepted` in `about.toml`, the licenses Rotproof agrees to ship. cargo-about takes minutes to build, so `cargo xtask
|
|
420
|
+
ci` checks only the list of crates, against `cargo tree`, which needs cargo alone: a new or bumped dependency fails
|
|
421
|
+
there until the file is written again. The text is checked by `.github/workflows/licenses.yml`, on a push that changes
|
|
422
|
+
the dependencies, `about.toml`, `about.hbs` or the file itself. The Dockerfile and that workflow install the same
|
|
423
|
+
version of cargo-about, and `cargo xtask ci` fails when they differ.
|
|
424
|
+
|
|
425
|
+
The pip wheels (Windows x86_64 and Linux x86_64) are built with maturin, in the container:
|
|
426
|
+
|
|
427
|
+
```sh
|
|
428
|
+
docker compose run --rm dev maturin build --release --out dist # Linux
|
|
429
|
+
docker compose run --rm dev maturin build --release --target x86_64-pc-windows-msvc --out dist # Windows
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
Each wheel carries `LICENSE-MIT`, `LICENSE-APACHE` and `THIRD-PARTY-LICENSES.txt` in its `.dist-info/licenses/`
|
|
433
|
+
(`license-files` in `pyproject.toml`). The container's Linux wheel is tagged `manylinux_2_34`, after the container's
|
|
434
|
+
glibc, and its Windows wheel is cross-compiled with cargo-xwin, which downloads the MSVC runtime and the Windows SDK
|
|
435
|
+
under Microsoft's license. Both serve for trying the wheels, not for release.
|
|
436
|
+
|
|
437
|
+
A release is a pushed tag `v<version>` that matches `Cargo.toml`. `.github/workflows/release.yml` checks the licenses,
|
|
438
|
+
builds the Linux wheel in the manylinux2014 image (glibc 2.17) and the Windows wheel on Windows, installs each into a
|
|
439
|
+
fresh venv on its platform and runs it (`.github/smoke.sh`), and installs the Linux wheel in the manylinux2014 image
|
|
440
|
+
too, so the oldest glibc the tag claims is tested. It then publishes the wheels to PyPI and makes the GitHub Release
|
|
441
|
+
with the wheels, an archive of the binary per platform taken out of the tested wheel, and `SHA256SUMS`. Run by hand
|
|
442
|
+
(`gh workflow run release.yml`), it does everything but publish. CI builds and tests the Windows wheel natively on
|
|
443
|
+
Windows too. Other platforms (macOS, Linux aarch64) are welcome as contributions.
|
|
444
|
+
|
|
445
|
+
## License
|
|
446
|
+
|
|
447
|
+
Licensed under either of [MIT](https://github.com/secta113/rotproof/blob/main/LICENSE-MIT) or
|
|
448
|
+
[Apache-2.0](https://github.com/secta113/rotproof/blob/main/LICENSE-APACHE), at your option. The crates the binary
|
|
449
|
+
includes are under their own licenses, listed with their texts in
|
|
450
|
+
[THIRD-PARTY-LICENSES.txt](https://github.com/secta113/rotproof/blob/main/THIRD-PARTY-LICENSES.txt). The links are full
|
|
451
|
+
URLs so that they work on PyPI, where this README is the project's page.
|
|
452
|
+
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
rotproof-0.1.0.data/scripts/rotproof.exe,sha256=bScdqL1LVVTj2VQqwAu-ZkbysZZcJ1YLaCb3Wck5t2Y,7388672
|
|
2
|
+
rotproof-0.1.0.dist-info/METADATA,sha256=ET4WLwWSFquZbRIvYQHBx2FBRP0DjDNIGMLao5YdsVA,32717
|
|
3
|
+
rotproof-0.1.0.dist-info/WHEEL,sha256=8Aej0W0a6Cz6apA3IzJrTnxLRVLAt-w0Oh8SA3Con_c,94
|
|
4
|
+
rotproof-0.1.0.dist-info/licenses/LICENSE-APACHE,sha256=pg7qgXUUUxZo1-AHZXMUSf4U0FnTJJ4LyTs23kX3WfI,10847
|
|
5
|
+
rotproof-0.1.0.dist-info/licenses/LICENSE-MIT,sha256=tNj8Ot7S99mYvS_AvcERnEkZYIPza9fm51KK_fdFGvk,1065
|
|
6
|
+
rotproof-0.1.0.dist-info/licenses/THIRD-PARTY-LICENSES.txt,sha256=xJCFhKYNQ5sur9NmYpahUsRq_DMRDaPwDN_422elgog,136994
|
|
7
|
+
rotproof-0.1.0.dist-info/sboms/rotproof.cyclonedx.json,sha256=Em6WlGab36Aui6iEbYq65TgI5lgiPJduD8nVmf8dD2s,253980
|
|
8
|
+
rotproof-0.1.0.dist-info/RECORD,,
|