diff options
Diffstat (limited to 'docs')
| -rw-r--r-- | docs/design.md | 64 |
1 files changed, 55 insertions, 9 deletions
diff --git a/docs/design.md b/docs/design.md index 9a15425..150e135 100644 --- a/docs/design.md +++ b/docs/design.md @@ -1,6 +1,7 @@ # krino design -Status: describes krino 0.0.1, 2026-09-13. +Status: describes krino 0.0.1, 2026-09-13; amended 2026-09-14 for 0.0.2: +duplicates are never deleted (§5.5). krino (from Greek κρίνω, "to separate, to judge, to decide") sorts files in chosen directories by rules. A rule tests a file's type, name, path, size, @@ -160,6 +161,10 @@ Items, in any order except that actions run in the order written: A rule with only `(stop)` is an exclusion: files it matches receive no actions from later rules. +A rule whose condition contains `(duplicate)` anywhere, including inside +`and`, `or` or `not`, cannot contain `(delete)` or `(delete permanent)`: +`krino check` and every run refuse it (§5.5). + `DEST` is a directory: relative paths are relative to the root, `~` expands, absolute paths are allowed. It is created if missing. `DEST` and `NAME` take placeholders (§7.3). @@ -225,12 +230,45 @@ of the given DIRs, then the oldest by mtime, then the shortest name, then the name that sorts first. `(duplicate)` is true for every other scanned file in the group. -`(duplicate)` conditions with different scopes do not share an original: -two conditions electing from different candidate sets can each treat a -different file as the original of the same content, and within one -directory's rules this can select every copy in a group for deletion. A -run-wide election that keeps at least one copy of every group is not in -0.0.1. +**Duplicates are found, never deleted.** Deciding which copy of identical +content to remove is left to the user, or to a tool built for it such as +jdupes. Two rules enforce this: + +1. A rule whose condition contains `(duplicate)` cannot contain a `delete` + action (§4.5). +2. In a directory whose rules use `(duplicate)`, a file that is a duplicate + under any of the duplicate scopes those rules use — each distinct set of + DIRs, and the plain `(duplicate)`, looked up for the file whether or not + evaluation reached that test — gets no delete step from any rule. The + plan shows the step as "skipped: a duplicate is never deleted" and the + rest of the chain continues from the file's current path. This covers what + rule 1 cannot see: a later rule deleting through `(matched)` or through a + condition of its own. + +Together they mean no rule can delete every copy of content that a duplicate +test in that directory can see. Under each scope every scanned file in a +group except that scope's original is a duplicate, so a scanned file that is +a duplicate under no scope must be the original of every scope, and there is +at most one such file (its hard-linked names count as that one file). Every +other copy is a duplicate somewhere, and a duplicate is never deleted. A file +displaced by `(on-conflict overwrite)` is not covered; it goes to the Trash, +and `krino undo` restores it (§7.4). + +The way to deal with duplicates is to move them aside and decide later: + +```lisp +(rule "dupes" + (when (duplicate "~/docs/Archive")) + (move "~/.dupes/") + (stop)) +``` + +Every move is logged (§9), so `krino undo` puts them back. + +Duplicate conditions with different scopes do not share an original: each +elects from its own candidates, so two such rules can each select a +different copy of the same content, and between them move every copy aside. +Nothing is deleted, and undo restores them. ## 6. Content extraction @@ -266,7 +304,8 @@ path: - `copy` leaves the file where it is; - `rename` changes its name, `move` its directory; later steps use the new path; - `delete` ends the chain. Steps after it appear in the plan as - "skipped: deleted by rule X". + "skipped: deleted by rule X". A `delete` of a duplicate is itself skipped + (§5.5) and does not end the chain. The plan warns when a chain moves a file more than once; that is usually a missing `(stop)`. @@ -373,7 +412,7 @@ krino: downloads ~/downloads 3 IMG_2031.JPG rename → 2026-09-01_IMG_2031.JPG photos type image move → Photos/2026/ 4 setup-1.2.deb DELETE permanently old-pkgs age 94d - 5 report (1).pdf trash (duplicate of Work/report.pdf) refiled + 5 report (1).pdf move → ~/.dupes/ dupes duplicate of Work/report.pdf warnings brochure.doc content unreadable: antiword/catdoc not installed (rule acme) @@ -604,6 +643,12 @@ placeholder numbers, screenshots by date, cleaning up old installers). contents, modes, mtimes). A cross-filesystem move test runs when a tmpfs is available on a different device, and skips otherwise. - **Trash:** with `XDG_DATA_HOME` pointed at a temporary directory. +- **Duplicates never deleted:** `krino check` refuses `(duplicate)` with a + `delete` action, including inside `or` and `not`; and no duplicate is + deleted through `(matched)`, through another rule's own condition, through + a duplicate test that evaluation skipped, or with two scopes whose + originals differ. Each case plans and applies in a temporary directory and + checks that every copy is still on disk. - **CLI:** golden files for plan rendering without colour; exit codes. - **Parity check before release:** run `krino -n` beside the prototype's dry run over the same real directory, and explain every difference. The @@ -634,6 +679,7 @@ keyword. Its defects shaped these decisions: | s-expressions, not TOML, YAML or INI | conditions are the core of the config; in sexp they are structure rather than a string in a second language. No nested quoting (TOML's `'''` around single-quoted regexes), no precedence, actions in written order. YAML turns `*` and `!` in ignore patterns into syntax; INI has no standard escaping | | `pdftotext` rather than a Go PDF library | best text quality of the open tools, fast, packaged on all three systems | | Delete to Trash by default | recoverable, and undo can restore it | +| Duplicates are found, never deleted | duplicate conditions with different scopes elect different originals, so deleting duplicates could remove every copy; moving them aside is always recoverable, and choosing which copy to delete belongs to the user or a tool built for it (jdupes). Decided 2026-09-14 after 0.0.1 shipped with the hazard documented | | A new repository rather than the prototype's | the prototype's history holds personal data; the prototype keeps working until krino reaches parity | | Personal configuration never enters the repository | rules hold private data; the leak check enforces it on every commit and in `make ci` | |
