summaryrefslogtreecommitdiff
path: root/docs
diff options
context:
space:
mode:
Diffstat (limited to 'docs')
-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` |