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.
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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: maturin (1.15.0)
3
+ Root-Is-Purelib: false
4
+ Tag: py3-none-win_amd64