summaryrefslogtreecommitdiff
path: root/man
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-09-13 02:31:32 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-09-13 02:31:32 +0200
commit26c94eb3db62ec6eebbf8d22c11afe691d9520c4 (patch)
tree165e5bf69234b4f96c9b74deb4898d7143ddf120 /man
parenta6e442a645902011b2081c216daaec052cdc6ce6 (diff)
downloadkrino-0.0.1.tar.gz
krino-0.0.1.zip
krino: release 0.0.1 — man pages, install, examples, cross and release, README, changelogv0.0.1
Also: undo removes the directories its run created; a hardlink is never a duplicate of its own other name; a flag written before "undo" is honoured; --version prints no leading v. Duplicate conditions with different scopes not sharing an original is documented as a known limitation.
Diffstat (limited to 'man')
-rw-r--r--man/krino.1348
-rw-r--r--man/krino.conf.5696
2 files changed, 1044 insertions, 0 deletions
diff --git a/man/krino.1 b/man/krino.1
new file mode 100644
index 0000000..627a2ee
--- /dev/null
+++ b/man/krino.1
@@ -0,0 +1,348 @@
+.\" SPDX-License-Identifier: GPL-3.0-or-later
+.Dd September 13, 2026
+.Dt KRINO 1
+.Os
+.Sh NAME
+.Nm krino
+.Nd sort files in a directory by rules
+.Sh SYNOPSIS
+.Nm
+.Op Fl y | Fl n
+.Op Fl v
+.Op Fl -json
+.Op Fl c Ar file
+.Op Ar name ...
+.Pp
+.Nm
+.Cm init
+.Pp
+.Nm
+.Cm new
+.Ar name path
+.Pp
+.Nm
+.Cm check
+.Op Ar name ...
+.Pp
+.Nm
+.Cm explain
+.Ar file
+.Pp
+.Nm
+.Cm log
+.Op Fl n Ar count
+.Pp
+.Nm
+.Cm undo
+.Op Ar run
+.Sh DESCRIPTION
+.Nm
+sorts the files in one or more configured directories according to rules
+kept in
+.Xr krino.conf 5 .
+For each directory in turn it scans the files, evaluates every rule against
+every file, and builds a plan: the chain of actions
+.Pq Ic copy , Ic move , Ic rename , Ic delete
+that the file's matching rules add up to.
+.Pp
+Every rule that matches a file adds its actions to that file's chain, in
+the order the rules are written;
+.Sy (stop)
+on a matching rule ends the chain for that file, so a later rule is never
+even evaluated against it.
+A rule with no actions besides
+.Sy (stop)
+is an exclusion.
+.Pp
+Every test sees the file as it was when the directory was scanned: name,
+path, size, modification time and content.
+Actions already added to a file's chain by an earlier rule never change
+what a later rule's tests see, so the whole plan for a directory can be
+computed, shown, and approved before anything is touched.
+.Pp
+With no
+.Fl y
+or
+.Fl n ,
+.Nm
+prints the plan and asks how to proceed
+.Pq Sx REVIEW .
+.Fl y
+applies the plan without asking;
+.Fl n
+prints it and changes nothing.
+If standard input is not a terminal and neither is given,
+.Nm
+refuses rather than guess.
+.Sh OPTIONS
+.Bl -tag -width Ds
+.It Fl y
+Apply the plan without asking.
+Cannot be combined with
+.Fl n .
+.It Fl n
+Dry run: print the plan and exit without changing anything.
+.It Fl v
+Also list files that were skipped as unmatched, ignored or busy, and show
+the full reason a test matched or not.
+.It Fl -json
+With
+.Fl n ,
+print the plan as JSON instead of the table.
+Refused unless
+.Fl n
+is also given.
+The document's own
+.Ic note
+field says its shape is unstable before krino 1.0; a script that reads it
+should expect it to change before then.
+.It Fl c Ar file
+Use
+.Ar file
+in place of
+.Pa $XDG_CONFIG_HOME/krino/krino.conf .
+.It Fl h , Fl -help
+Print usage and exit.
+.It Fl -version
+Print
+.Dq krino Ar version
+and exit.
+.El
+.Sh SUBCOMMANDS
+.Bl -tag -width Ds
+.It Ic init
+Create the config directory with a commented
+.Pa krino.conf
+and
+.Pa template.conf .
+Refuses if
+.Pa krino.conf
+already exists, and leaves an existing
+.Pa template.conf
+alone.
+.It Ic new Ar name path
+Copy
+.Pa template.conf
+to
+.Pa dirs/ Ns Ar name Ns Pa .conf ,
+fill in
+.Ar path ,
+and append
+.Ar name
+to
+.Ic include
+in
+.Pa krino.conf ,
+keeping its comments.
+.It Ic check Op Ar name ...
+Validate the configuration, list each included directory's rules, and list
+which content-extraction tools
+.Pq Xr krino.conf 5 , Sx CONTENT EXTRACTION
+are available.
+With no
+.Ar name ,
+checks every included directory.
+.It Ic explain Ar file
+Evaluate every rule of
+.Ar file Ns 's
+directory against it and show each test's result, so a rule that should
+match but does not
+.Pq or the reverse
+can be diagnosed test by test.
+See
+.Sx KNOWN LIMITATIONS .
+.It Ic log Op Fl n Ar count
+List the most recent runs, newest first
+.Pq Ar count No defaults to 10 :
+the run id, its start time, the directories it touched, and what it did.
+A run that a later
+.Ic undo
+has reversed at least one file of is marked
+.Pq undone .
+.It Ic undo Op Ar run
+Reverse
+.Ar run .
+With no
+.Ar run ,
+reverse the most recent run; if that run is itself an undo, it is refused.
+An older run is undone by naming it.
+See
+.Sx UNDO .
+.El
+.Sh REVIEW
+With neither
+.Fl y
+nor
+.Fl n ,
+after showing one directory's plan
+.Nm
+asks:
+.Bd -literal -offset indent
+[a] apply all [c] choose per file [s] skip this directory [q] quit
+.Ed
+.Pp
+.Ic a
+applies every step of every chain shown.
+.Ic s
+applies nothing in this directory and moves on to the next one.
+.Ic q
+stops
+.Nm
+entirely; directories already applied earlier in this run stay applied and
+can be reversed with
+.Ic undo .
+.Pp
+.Ic c
+asks about each file in turn:
+.Bd -literal -offset indent
+ [y] yes [n] no [a] yes to this and all remaining [d] done, apply chosen so far [q] quit, apply nothing
+.Ed
+.Pp
+Approval is per file: a file's whole chain runs, or none of it.
+.Ic d
+stops asking and applies whatever was already chosen, declining the rest.
+.Ic q
+here aborts the review for this directory entirely, discarding even a file
+already marked
+.Ic yes ,
+and folds into the top-level
+.Ic q
+above.
+.Sh UNDO
+.Ic krino undo
+reverses a run's steps, last step first within each file, after building
+and showing a plan of its own
+.Pq shown and approved the same way as a sort plan; Fl y No and Fl n No apply .
+One undo plan covers every directory the run touched, so its menu has no
+per-directory skip;
+.Ic s
+and
+.Ic q
+both apply nothing:
+.Bd -literal -offset indent
+[a] apply all [c] choose per file [s] skip [q] quit
+.Ed
+.Pp
+An undo run cannot itself be undone: naming it to
+.Ic undo
+is refused.
+.Pp
+A step's reversal is refused when the world has moved on since the step ran
+.Pq its target is gone or has changed, or the original path is occupied again ;
+a refused file has nothing of its chain reversed, so no file is left half
+undone.
+An exception is a created directory found non-empty at its own turn: that
+alone does not refuse the rest of the file's reversal, since it means
+another file still lives there, not that something unexpected changed
+underfoot.
+Every created directory still empty once every file in the run has had its
+turn is then removed, deepest path first, so an undo does not leave the
+empty directories it made behind.
+.Pp
+A permanent delete,
+.Sy (delete permanent) ,
+is never undoable; it is shown in the plan with that reason and nothing is
+attempted for it.
+.Pp
+.Ic krino log
+marks a run
+.Pq undone
+once its undo run has reversed at least one file's chain \(em not that
+every file in it was restored: a run left partly reversed, because some
+files were declined during the undo's own review, is still shown as
+.Pq undone
+in full.
+.Sh ENVIRONMENT
+.Bl -tag -width Ds
+.It Ev XDG_CONFIG_HOME
+Base of the configuration directory
+.Pq Pa $XDG_CONFIG_HOME/krino ;
+default
+.Pa ~/.config .
+.It Ev XDG_STATE_HOME
+Base of the log and the per-directory lock files
+.Pq Pa $XDG_STATE_HOME/krino ;
+default
+.Pa ~/.local/state .
+.It Ev XDG_DATA_HOME
+Base of the trash a
+.Sy (delete)
+step uses
+.Pq Pa $XDG_DATA_HOME/Trash ;
+default
+.Pa ~/.local/share .
+.It Ev PAGER
+Used to show a plan taller than the terminal; default
+.Dq less -FRX .
+Never used for
+.Fl -json
+output.
+.It Ev NO_COLOR
+When set, disables the ANSI colour
+.Nm
+otherwise uses on a terminal for a permanent delete and a refused undo
+step.
+.El
+.Sh FILES
+.Bl -tag -width Ds
+.It Pa $XDG_CONFIG_HOME/krino/krino.conf
+The main configuration file: which directories run, defaults, the log
+path.
+.It Pa $XDG_CONFIG_HOME/krino/template.conf
+Copied by
+.Ic krino new
+for each new directory.
+.It Pa $XDG_CONFIG_HOME/krino/dirs/ Ns Ar name Ns Pa .conf
+One file per configured directory.
+.It Pa $XDG_STATE_HOME/krino/krino.log
+The append-only log every run writes to.
+.It Pa $XDG_STATE_HOME/krino/ Ns Ar name Ns Pa .lock
+Held for the duration of a run against directory
+.Ar name ,
+so a second
+.Nm
+against the same directory waits, or fails immediately with
+.Fl y .
+.It Pa $XDG_DATA_HOME/Trash
+The freedesktop.org trash a
+.Sy (delete)
+step moves files into.
+.El
+.Sh EXIT STATUS
+.Bl -tag -width Ds
+.It 0
+Success, including a run that found nothing to do, or one whose plan was
+entirely declined at review.
+.It 1
+A step failed, a directory's lock was already held by another
+.Nm ,
+or a configured directory could not be read.
+.It 2
+A usage mistake or a configuration error.
+Flags must come before any directory name on the command line: a flag
+found after a directory name is a usage error rather than a guess.
+.It 130
+Interrupted
+.Pq Ic Ctrl-C
+or terminated; the current step, if any, is finished and logged first.
+.El
+.Sh KNOWN LIMITATIONS
+.Ic krino explain
+evaluates every test of every rule's condition against the file, without
+the cost-ordered short-circuiting a real run uses within an
+.Sy and/or .
+It can therefore run a content-extraction tool, and report a
+.Dq content unreadable
+warning, for a test that a real run would never reach because an earlier,
+cheaper test in the same condition already decided the rule did not match.
+.Ic explain
+is a full trace of every test, not a cost-faithful preview of what
+.Fl n
+would actually spend time on.
+.Sh SEE ALSO
+.Xr krino.conf 5
+.Pp
+.Pa docs/sexp-primer.md
+in the source repository
+.Pq installed at Pa $PREFIX/share/doc/krino/sexp-primer.md
+is a short tutorial on the configuration syntax.
diff --git a/man/krino.conf.5 b/man/krino.conf.5
new file mode 100644
index 0000000..fbd1c06
--- /dev/null
+++ b/man/krino.conf.5
@@ -0,0 +1,696 @@
+.\" SPDX-License-Identifier: GPL-3.0-or-later
+.Dd September 13, 2026
+.Dt KRINO.CONF 5
+.Os
+.Sh NAME
+.Nm krino.conf
+.Nd krino's configuration language and files
+.Sh DESCRIPTION
+.Xr krino 1 Ns 's
+configuration is written in s-expressions: each form is a list inside
+parentheses, holding an operation optionally followed by its arguments.
+A short tutorial is
+.Pa docs/sexp-primer.md
+in the source repository, installed at
+.Pa $PREFIX/share/doc/krino/sexp-primer.md .
+This page is the reference for every form.
+.Pp
+.Bl -bullet -compact
+.It
+.Sy List :
+.Ql \&(
+items separated by whitespace
+.Ql \&) .
+.It
+.Sy String :
+.Ql \&"...\&" .
+A backslash escapes only
+.Ql \&"
+and
+.Ql \e ;
+any other backslash is kept literally, so
+.Ql \&"\ebacme\eb\&"
+is the regex
+.Ql \ebacme\eb .
+Strings may span lines.
+.It
+.Sy Symbol :
+any other run of characters except whitespace,
+.Ql \&( ,
+.Ql \&) ,
+.Ql \&" ,
+.Ql \&; .
+.It
+.Sy Comment :
+.Ql \&;
+to end of line.
+.El
+.Pp
+Encoding is UTF-8 only.
+There is no other syntax: no quote characters, no dotted pairs, no block
+comments.
+Paths, keywords, regexes and rule names must be strings; settings values,
+type names, operators, sizes and durations are symbols.
+.Pp
+Every list and atom's byte offset, line and column is recorded, so
+.Ic krino new
+can splice a name into
+.Ic include
+without disturbing anything else in the file, and a syntax error is
+reported as
+.Ar file : Ns Ar line : Ns Ar col .
+.Sh FILES
+.Ss krino.conf
+The main file, read first:
+.Bd -literal -offset indent
+(include "downloads" "invoices") ; dirs/\*(Ltname\*(Gt.conf, run in this order
+(log "~/.local/state/krino/krino.log") ; optional
+(defaults ; optional; any setting below
+ (min-age 5m))
+.Ed
+.Bl -tag -width Ds
+.It Ic (include Ar name No ...)
+The directories to sort, in the order given.
+Each
+.Ar name
+has its rules in
+.Pa dirs/ Ns Ar name Ns Pa .conf .
+.It Ic (log Ar path )
+Where the log goes.
+Optional; defaults to
+.Pa $XDG_STATE_HOME/krino/krino.log .
+.It Ic (defaults Ar setting No ...)
+Defaults for every directory; a directory's own file, and a rule inside
+it, can override them.
+See
+.Sx SETTINGS .
+.El
+.Ss dirs/name.conf
+One file per configured directory:
+.Bd -literal -offset indent
+(path "~/downloads") ; required
+(recursive no) ; any setting from SETTINGS
+(ignore "*.part" "*.aria2" ".*") ; may repeat; patterns accumulate in order
+
+(rule "acme"
+ (when (type document)
+ (or (content "acme ltd" "0000000000")
+ (name "(^|[^a-z0-9])acme([^a-z0-9]|$)"))
+ (not (name "^draft")))
+ (move "Work/Acme/{mtime:%Y}")
+ (stop))
+.Ed
+.Pp
+Top-level forms may appear in any order, except that rules run in the
+order written.
+.Bl -tag -width Ds
+.It Ic (path Ar dir )
+Required.
+The directory this file sorts.
+.Ar ~
+expands.
+.It Ic (ignore Ar pattern No ...)
+May repeat; patterns accumulate in order.
+Gitignore syntax:
+.Ql * ,
+.Ql ** ,
+.Ql \&? ,
+.Ql [...] ;
+a leading
+.Ql /
+anchors to the root; a trailing
+.Ql /
+matches directories only;
+.Ql \&!
+re-includes; a pattern with no
+.Ql /
+matches at any depth; the last matching pattern wins.
+An ignored directory is not descended into.
+.It Ic (rule Ar name item No ...)
+See
+.Sx RULES .
+.El
+.Sh SETTINGS
+Each setting may appear in
+.Ic (defaults ...) ,
+at the top of a directory file, or, where marked
+.Pq rule ,
+inside a
+.Ic rule
+form.
+The most specific wins: rule, then directory, then defaults, then the
+built-in default below.
+Durations are an integer plus
+.Ql s m h d w ;
+sizes an integer plus an optional
+.Ql K M G T
+.Pq powers of 1024 .
+.Bl -tag -width Ds
+.It Ic case
+.Pq rule .
+.Sy ignore
+or
+.Sy strict :
+case sensitivity for
+.Ic name , path
+and
+.Ic content .
+Built-in:
+.Sy ignore .
+.It Ic fold
+.Pq rule .
+.Sy yes
+or
+.Sy no :
+strip diacritics before comparing
+.Po
+a with an ogonek becomes plain a, an l with a stroke becomes plain l,
+e acute becomes plain e, u with diaeresis becomes plain u,
+and so on for every Latin letter
+.Pc .
+Built-in:
+.Sy yes .
+.It Ic recursive
+.Sy yes
+or
+.Sy no .
+Built-in:
+.Sy no .
+.It Ic max-depth
+An integer;
+.Sy 1
+means the root only.
+Built-in: unlimited.
+.It Ic min-age
+A duration.
+Files modified more recently are skipped as busy.
+Built-in:
+.Sy 2m .
+.It Ic max-read
+A size.
+No content is extracted from a file above this size.
+Built-in:
+.Sy 50M .
+.It Ic busy
+One or more suffixes.
+A file is skipped when its own name with one of these suffixes appended
+also exists beside it, e.g.
+.Pa report.pdf.part
+beside
+.Pa report.pdf .
+Built-in:
+.Sy .part .aria2 .crdownload .
+.It Ic on-conflict
+.Pq rule .
+.Sy suffix , skip
+or
+.Sy overwrite :
+see
+.Sx Conflicts .
+Built-in:
+.Sy suffix .
+.El
+.Sh RULES
+.Bd -literal -offset indent
+(rule NAME ITEM...)
+.Ed
+.Pp
+Items, in any order except that actions run in the order written:
+.Bl -tag -width Ds
+.It Ic (when Ar cond No ...)
+The condition; see
+.Sx CONDITIONS .
+Several conditions means all must hold.
+A rule with no
+.Ic when
+matches every file.
+An empty
+.Ic (when)
+is an error.
+.It Ic case , Ic fold , Ic on-conflict
+Rule-level settings; see
+.Sx SETTINGS .
+.It Ic (copy Ar dest )
+Copy the file into directory
+.Ar dest .
+.It Ic (move Ar dest )
+Move the file into directory
+.Ar dest .
+.It Ic (rename Ar name )
+Rename in place.
+.Ar name
+must not contain
+.Ql / .
+.It Ic (delete)
+Move to the trash.
+.It Ic (delete permanent)
+Unlink.
+Cannot be undone.
+.It Ic (stop)
+Once this rule matches, evaluate no further rules for this file.
+.El
+.Pp
+A rule with only
+.Ic (stop)
+is an exclusion: files it matches receive no actions from later rules.
+.Pp
+.Ar dest
+is a directory: a relative path is relative to the root,
+.Ql ~
+expands, an absolute path is allowed, and it is created if missing.
+.Ar dest
+and
+.Ar name
+take placeholders; see
+.Sx Placeholders .
+.Sh CONDITIONS
+.Ss Operators
+.Ic (and Ar cond No ...) ,
+.Ic (or Ar cond No ...) ,
+.Ic (not Ar cond ) .
+.Ic and
+and
+.Ic or
+take one or more arguments;
+.Ic not
+exactly one.
+.Ss Tests
+.Bl -tag -width Ds
+.It Ic (type Ar t No ...)
+True when the name ends in
+.Ql \&. Ns Ar t
+for any
+.Ar t ,
+case-insensitively.
+.Ar t
+may be a type group
+.Po
+e.g.\&
+.Ic document ;
+see
+.Sx TYPE GROUPS
+.Pc
+or a multi-part suffix like
+.Ic tar.gz .
+.It Ic (name Ar re No ...)
+True when the file name matches any of the regexes.
+.It Ic (path Ar re No ...)
+True when the path relative to the root matches any of the regexes.
+.It Ic (content Ar keyword No ...)
+True when the extracted text contains any of the keywords; see
+.Sx CONTENT EXTRACTION .
+.It Ic (size Ar op size )
+.Ar op
+is one of
+.Ql > >= < <= = .
+.It Ic (age Ar op duration )
+Age by modification time.
+.It Ic (duplicate)
+True when another scanned file has identical content and this one is not
+the chosen original; see
+.Sx DUPLICATES .
+.It Ic (duplicate Ar dir No ...)
+The same, also comparing against every regular file under
+.Ar dir .
+.It Ic (matched)
+True when an earlier rule already matched this file.
+.El
+.Pp
+Regexes use Go's RE2 syntax: POSIX extended regular expressions plus
+.Ql \ed \ew \es \eb ,
+non-greedy quantifiers, and the inline flags
+.Ql (?i)
+and
+.Ql (?-i) ;
+there are no backreferences and no lookaround.
+.Ss Case, folding and word boundaries
+.Ic case
+and
+.Ic fold
+apply to
+.Ic name , path
+and
+.Ic content ,
+in both the file's data and the pattern;
+.Ic type
+is always case-insensitive.
+A regex can override
+.Ic case
+locally with
+.Ql (?i)
+or
+.Ql (?-i) .
+.Pp
+.Sy RE2 treats
+.Ql _
+as a word character.
+.Ql \eb
+is the boundary between a word character and a non-word character, so
+.Ql \ebacme\eb
+does
+.Em not
+match
+.Ar ACME_REPORT_2026.pdf :
+the
+.Ql E
+before the underscore and the underscore itself are both word characters,
+so there is no boundary there for
+.Ql \eb
+to match.
+Use a character class instead of
+.Ql \eb
+when the name may be glued to the rest with an underscore or a digit:
+.Bd -literal -offset indent
+(name "(^|[^a-z0-9])acme([^a-z0-9]|$)")
+.Ed
+matches
+.Ar ACME_REPORT_2026.pdf
+.Pq case-insensitively, by default
+because it treats anything that is not a lowercase letter or digit,
+including
+.Ql _ ,
+as a separator.
+.Sh TYPE GROUPS
+A group name in
+.Ic (type ...)
+stands for every extension listed for it:
+.Bl -tag -width "presentation"
+.It Ic image
+jpg jpeg png gif webp bmp tif tiff heic heif avif svg ico raw cr2 nef arw dng
+.It Ic video
+mp4 mkv webm mov avi m4v mpg mpeg wmv flv 3gp
+.It Ic audio
+mp3 flac ogg opus m4a aac wav wma aiff
+.It Ic archive
+zip tar gz tgz bz2 tbz2 xz txz zst 7z rar lz lzma cpio
+.It Ic document
+pdf doc docx odt rtf txt md tex
+.It Ic spreadsheet
+xls xlsx ods csv tsv
+.It Ic presentation
+ppt pptx odp
+.It Ic ebook
+epub mobi azw azw3 fb2 djvu
+.It Ic code
+go c h cpp hpp py sh js ts rs java rb pl lua html css json yaml yml toml xml sql
+.It Ic text
+txt md log csv tsv json yaml yml toml xml ini conf
+.It Ic package
+deb rpm apk appimage exe msi flatpak snap
+.It Ic font
+ttf otf woff woff2
+.El
+.Pp
+Groups overlap; a file can belong to several.
+.Sh CONTENT EXTRACTION
+.Bl -tag -width "presentation"
+.It Sy text
+Known text extensions, or detected from the first 8 KiB: valid UTF-8, or
+UTF-16 with a BOM, and no NUL bytes.
+.It Sy pdf
+.Ic pdftotext
+.Pq 30 second timeout .
+.It Sy docx, xlsx, pptx, odt, ods, odp, epub
+Zip plus streaming XML, Go standard library only.
+.It Sy html, xml
+Tags removed, entities decoded.
+.It Sy doc
+.Ic antiword ,
+else
+.Ic catdoc .
+.It Sy xls / ppt
+.Ic xls2csv
+or
+.Ic catppt
+.Pq both from catdoc .
+.It Sy anything else
+No content.
+.El
+.Pp
+External tools are optional and looked up once per run;
+.Ic krino check
+lists which were found.
+A file above
+.Ic max-read
+is not extracted.
+Tools run without a shell and are given absolute paths only, so a file
+name starting with
+.Ql -
+is never read as an option, and each is killed at its timeout.
+.Pp
+Before matching, text and keywords are normalised the same way: case
+.Pq if Sy ignore ,
+folding
+.Pq if Sy yes ,
+and runs of whitespace collapsed to one space, so a keyword split across
+lines in a PDF still matches.
+Matching is substring:
+.Ql \&"acme\&"
+matches
+.Ql \&"acmeco\&" .
+Words hyphenated across lines in a PDF are not rejoined.
+.Sh ACTIONS
+.Ss Chains
+A file's chain is the actions of every matching rule, in rule order, and
+within a rule in the order written.
+.Ic copy
+leaves the file where it is;
+.Ic rename
+changes its name and
+.Ic move
+its directory, and later steps use the new path;
+.Ic delete
+ends the chain, and steps after it are shown in the plan as
+.Dq skipped: deleted by rule Ar x .
+.Pp
+The plan warns when a chain moves a file more than once; that is usually
+a missing
+.Ic (stop) .
+.Ss How each action runs
+.Bl -tag -width Ds
+.It Ic move
+.Xr rename 2
+when source and destination share a filesystem; otherwise a copy to a
+temporary file in the destination, fsync, rename into place, then the
+source is removed.
+If any step fails the source is untouched and the temporary file is
+removed.
+.It Ic copy
+To a temporary file in the destination, then renamed into place.
+Mode and modification time are preserved.
+.It Ic rename
+.Xr rename 2
+within the same directory.
+.It Ic delete
+Into the freedesktop.org trash at
+.Pa $XDG_DATA_HOME/Trash .
+A file on a different filesystem from the trash is not trashed: the step
+fails with a message suggesting
+.Ic (delete permanent)
+or a move.
+.It Ic (delete permanent)
+.Xr unlink 2 .
+.El
+.Pp
+Before each step, the executor checks that the source still exists with
+the size and modification time recorded in the plan; if not, the step
+fails as
+.Dq changed since plan
+and the rest of that file's chain is skipped.
+.Ss Placeholders
+.Bl -tag -width "{mtime:FMT}"
+.It Ic {name}
+The current file name.
+.It Ic {stem}
+The name without its last extension.
+.It Ic {ext}
+The last extension with its dot, e.g.
+.Ql .pdf ;
+empty if none.
+.It Ic {1} No ... Ic {9}
+Capture groups of the first
+.Ic name
+test, not inside a
+.Ic not ,
+that matched while evaluating this rule.
+.Ic krino check
+refuses a rule that uses
+.Ic { Ns Ar n Ns Ic }
+unless every such
+.Ic name
+test in it has at least
+.Ar n
+groups, and refuses one that uses
+.Ic { Ns Ar n Ns Ic }
+with no
+.Ic name
+test at all.
+.It Ic {mtime:FMT}
+The file's modification time.
+.It Ic {now:FMT}
+The start of the run.
+.It Ic {{ No and Ic }}
+A literal
+.Ql {
+or
+.Ql } .
+.El
+.Pp
+.Ar FMT
+is a strftime subset:
+.Ql %Y %m %d %H %M %S %j %% .
+.Ss Conflicts
+When the computed target already exists:
+.Bl -tag -width Ds
+.It Ic suffix
+.Pq default .
+Use
+.Pa stem_1.ext , stem_2.ext ,
+and so on.
+.It Ic skip
+Skip this step; the chain continues from the file's current path.
+.It Ic overwrite
+Move the existing target to the trash first
+.Pq logged, so undo restores it ,
+then proceed.
+.El
+.Pp
+A
+.Ic copy
+whose target already has identical content is skipped as
+.Dq already there ,
+whatever the policy, so a backup rule can run every time.
+A step whose target is the file itself
+.Pq a Ar dest No that resolves to the file's own directory, or a Ic rename No to its current name
+is skipped as
+.Dq already there
+as well; a directory whose destination's first path component is a
+placeholder relies on this to avoid re-filing its own output \(em see
+.Sx KNOWN LIMITATIONS .
+.Pp
+Conflicts between files in the same plan are resolved when planning, so
+the plan shows final names; an in-plan claim is never displaced.
+The claim set spans the whole run, so two configured directories cannot
+plan the same final name.
+The executor re-checks at execution time; if the name has to change, the
+log records the actual name used.
+.Sh DUPLICATES
+Candidates are the scanned files plus, for
+.Ic (duplicate Ar dir ) ,
+every regular file under
+.Ar dir
+.Pq symlinks skipped .
+Files are grouped by size; only groups of two or more are hashed, first
+the first and last 64 KiB, then SHA-256 of the whole file.
+Empty files are never duplicates.
+.Pp
+Two names for the same file
+.Pq the same device and inode, as a hard link creates
+are never duplicates of
+.Em each other ;
+they may still both independently be duplicates of a separate file with
+identical content.
+.Pp
+The
+.Em original
+in each group is, in order of preference: a file under one of the given
+.Ar dir
+arguments, then the oldest by modification time, then the shortest name,
+then the name that sorts first.
+.Ic (duplicate)
+is true for every other scanned file in the group.
+.Pp
+.Sy Warning:
+.Ic (duplicate)
+cannot tell a deliberate second copy from an accidental one.
+Byte-identical content is byte-identical either way; the distinction
+between
+.Dq I meant to keep two
+and
+.Dq this should not exist twice
+exists only in the user's intent, not in the files.
+Pairing
+.Ic (duplicate)
+with
+.Ic (delete)
+over a directory that may hold intentional copies
+.Pq a mirror, a staging queue, anything another tool manages
+will delete things the user meant to keep.
+Keep such a rule on a directory scoped narrowly enough that every
+byte-identical pair in it really is an accident.
+.Pp
+.Sy Warning:
+each
+.Ic (duplicate)
+or
+.Ic (duplicate Ar dir )
+condition elects its own original from its own candidates; two conditions
+with different scopes can therefore elect
+.Em different
+originals for the same content.
+Within one directory's rules
+.Pq one rule combining conditions with Ic or , or two separate rules ,
+this can make every copy in a group selected by some condition, and with
+.Ic (delete permanent)
+every copy selected is gone for good.
+.Ic krino
+plans and applies one directory at a time, so a directory already applied
+in the run stays applied while a later directory is planned; a
+.Fl n
+run only ever plans, so it shows every directory's plan as if none of the
+others had been applied.
+Two files in different directories that a
+.Fl n
+plan each lists as a duplicate of the other are therefore not necessarily
+both deleted when the same run is applied with
+.Fl y :
+deleting the first can remove the very original the second file was a
+duplicate of, leaving the second no longer a duplicate by the time its own
+directory is planned.
+Use one duplicate scope for rules that delete within a directory; prefer
+.Ic (delete)
+to
+.Ic (delete permanent)
+with
+.Ic (duplicate) ;
+and within one directory's rules, two files each listed as a duplicate of
+the other in the
+.Fl n
+plan means both would be deleted.
+.Sh KNOWN LIMITATIONS
+A
+.Ar dest
+whose
+.Em first
+path component is itself a placeholder
+.Pq e.g. Ic {ext} No or Ic {mtime:%Y}
+has no static prefix, so nothing under it can be excluded from the walk
+before scanning starts, and a recursive directory walks into
+.Ic krino Ns 's
+own output.
+A file already sitting at its own computed destination is a no-op,
+.Dq already there
+.Pq Sx Conflicts ,
+rather than being re-filed on every run, but the walk still visits it,
+which a
+.Ic (duplicate)
+or
+.Ic content
+test in another rule can also see.
+Give such a rule its own narrow
+.Ic ignore
+pattern, or a
+.Ar dest
+whose first path component is a literal string, when that matters.
+.Pp
+.Ic (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
+.Pq Sx DUPLICATES .
+.Sh SEE ALSO
+.Xr krino 1
+.Pp
+.Pa docs/sexp-primer.md
+in the source repository
+.Pq installed at Pa $PREFIX/share/doc/krino/sexp-primer.md .