aboutsummaryrefslogtreecommitdiff
path: root/docs/design.md
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-09-14 10:30:12 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-09-14 10:30:12 +0200
commitebdd7bb254a0f19f815a644d26ce232de7be0adb (patch)
tree5174731c8e446666c84fa0c0273fec396da22132 /docs/design.md
parent26c94eb3db62ec6eebbf8d22c11afe691d9520c4 (diff)
downloadkrino-ebdd7bb254a0f19f815a644d26ce232de7be0adb.tar.gz
krino-ebdd7bb254a0f19f815a644d26ce232de7be0adb.zip
krino: duplicates are found, never deleted
Lukasz, 2026-09-14: leave deleting duplicates to the user or jdupes. Section 5.5 now forbids a delete action in any rule using (duplicate), and skips any delete step for a file that is a duplicate under a scope its directory uses, which closes the (matched) and other-rule routes. Two scopes that elect different originals can then only move copies aside, never delete them. The consequences land in 4.5, 7.1, the 8.2 example, 15 and 17.
Diffstat (limited to 'docs/design.md')
-rw-r--r--docs/design.md64
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` |