# Changelog ## Unreleased ## 0.0.11 — 2026-09-17 The window, worked over from a fortnight's worth of using it: the plan reads like a table, a filter and a sort over it, choices that act on everything checked, the file itself shown beside its explanation, and rules edited as a tree. The command line is unchanged. - The plan is a table with headings: file, size, age, action, where it would go, rule, and an outcome column once it has been applied. The action is in capitals and carries the running GTK theme's own colours - accent for a move, the selection colour for a rename, success, warning and error for the rest - and stays readable on a selected row. Size, age and rule can each be turned off. - A filter over the plan: the letters of a name or a rule, in order, capitals and accents ignored as `(fold yes)` does. `Select all` then takes what is shown, and the window says how many checked files the filter is hiding. - A sort picker: as scanned, by name, size, age, action or rule. - `With checked` sets every checked file to the Trash, or - after a confirmation naming the count - to a permanent delete. A row's own menu still does one file at a time. - A file krino found another copy of names that copy in full, and offers to keep this one instead: it takes the other's place and the other goes to the Trash, which `krino undo` reverses. - The file behind the selected row is shown: a picture for an image, the first page for a PDF, the first lines of anything that is text. The divider above it sets how big it is, and the page is rendered to suit. - The explanation is laid out rather than printed: the name, the size and age, the copy it duplicates, then a line per step with its action coloured. - Rules: the conditions of a rule are a tree - `and`, `or` and `not` holding the conditions under them - the text has line numbers and colours, a `Check` button, and `Test rule` lists the files a rule would take. `Add directory` writes `dirs/NAME.conf` and adds it to the include, as `krino new` does. - `Settings`, at the top right: krino's own defaults, written to `krino.conf` with the same care a rules file is saved with, and how the window behaves - the layout (the file list beside the explanation or above it), which columns are shown, the sort, the preview, and whether a scan starts with every file checked. Every setting says what it does. The window keeps where each divider was left. - Nothing on disk changes until Apply, Undo or Save: a review choice says so, and closing with an unapplied plan asks first. - `FileMatch.DuplicateOf`: the file a `(duplicate)` test matched, absolute, so a front end can say where the other copy is and act on it. The reason text is unchanged. - Fixed: selecting the exclude row in Forms took the window down with it (0.0.10). An exclude has conditions but no name, action or `(stop)`, and the form now opens with only what an exclude has. ## 0.0.10 — 2026-09-16 A window, `krino-gui`, over the same engine: review and apply a plan, look back over runs and undo one, and edit a directory's rules with the configuration checked as you type. The command line is unchanged. - `krino-gui`, an optional GTK 4 program built and installed separately (`make gui`, `make install-gui`; it needs GTK 4 development files and cgo, which `krino` itself does not). Three tabs: **Plan** (scan a directory, a checkbox and an explanation per file, Trash or delete-permanently instead from a row's menu, Apply), **History and undo** (runs newest first, a run's reversal file by file, refused files with the reason, Undo), and **Rules** (the directory's excludes and rules as forms, or the whole file as text; a check after every edit; Test on file; Save that refuses a file which would not load, keeps the previous text as `NAME.conf.bak`, and asks before overwriting an edit made elsewhere). Described in krino-gui(1). - A plan the window holds open keeps its directory's lock, so nothing moves underneath it, and applying or closing frees it. Each plan and each undo is a run of its own in the log, exactly as a command-line invocation is, and `krino log` and `krino undo` treat them no differently. - Engine, for both front ends: a `Session` that owns a run's lock, log, run id and claims (the command line now goes through it, with no change to what it does); loading a configuration with some files' text supplied in memory, for the editor's live check; `Explain` that also reports a rule's captures and the chain a file would get; a printer and splice for one configuration form; and one atomic way to write a configuration file. - An undo result now says which file each outcome belongs to, so a front end can show it per file. Nothing on the command line changes. - A directory that applies nothing no longer frees what an earlier directory of the same run put somewhere: every applied destination stays protected from a later `(on-conflict overwrite)` for the whole run. ## 0.0.9 — 2026-09-15 Rules that cannot be decided fail closed in two more places, `krino check` catches bad placeholders, and krino.conf(5) explains how rules are written and used, checked against the code. - A `(duplicate)` test whose lookup fails (an unreadable file) is unknown, like unreadable content: an exclude holds, marked "(duplicate check failed)", and no rule acts on it, `(not (duplicate))` included. - A `(stop)` rule that cannot be decided ends the search for that file: a "keep" rule like `(rule "keep" (when (content "confidential")) (stop))` protects an unreadable file from the rules below it. `explain` shows the rule as "undecided" and the later rules as stopped by it. - `krino check`, and every run, refuses a placeholder that could never expand (`{foo}`, `{mtime}` or `{mtime:}` without a format, `%B`, `{0}`, `{10}`, an unclosed `{`) at the action's file:line:col, instead of skipping the step at plan time. - krino.conf(5) gains WRITING RULES: what happens to a file from scan to execution, how to make and test a configuration, a worked example with real output, and the mistakes `krino check` reports. Its reference sections were checked statement by statement against the code and corrected where they were wrong or left things to guess (claims across directories, `busy` values, `ignore` anchoring and case, capture order, `{now}`, max-read, text detection, overwrite and execution-time conflicts, unscanned destinations, and more). - `make ci` passes on OpenBSD 7.9 (OpenBSD make) and FreeBSD 15.0 (bmake) with Go 1.26.8; the design notes that OpenBSD's `go` package needs `GOTOOLCHAIN=auto` to use it. ## 0.0.8 — 2026-09-15 The rest of the open work after 0.0.7, and the checks on real FreeBSD and OpenBSD hosts: every test passes there, and the lock's stale-holder check works with real processes. - A file of no known extension that starts as text and turns binary further on (a self-extracting installer) has no content, like an image: it no longer matches a content test, is not set aside by a content exclude, and raises no warning. - A content test that cannot read its file is unknown rather than false, and `and`/`or`/`not` combine unknowns by three-valued logic. A rule never matches on an unknown, so `(not (content "x"))` no longer acts on a file krino could not read; an exclude holds on an unknown only when the text could change its answer; `explain` shows `?`. `(matched)` is unknown while no earlier rule matched but one could not be decided, so a `(not (matched))` catch-all leaves such a file alone. A document read only in part answers the keywords found in what was read, and leaves the others unknown; its answers are never cached, and keyword caches written by 0.0.7 are discarded. - `krino log` marks a run "(partly undone)" while some of its reversible steps are not reversed, and "(undone)" only once every one is. - Tests read real documents made by LibreOffice and pandoc (docx, odt, ods, xlsx, pptx, epub, pdf, doc, xls), the tool formats when a tool is installed; antiword's refusal of a very short `.doc` is documented. - Tests: a genuine apply error exits 1, and `w` in review stops krino even when applying fails, both through the real command; the undo projection's "about to be occupied" half. - Build: `make build` and `make install` refuse a `VERSION` that is not a plain version string, as `cross` and `release` did; `make cross` names its directories without the tag's `v`; the dependency gate covers test imports and FreeBSD and OpenBSD builds; `scripts/deps --print` prints only the command on stdout; the README says how to install from a release tarball; tests check that the examples load and that the Makefile's extractor list matches the code. - krino.conf's excludes are checked with the defaults' `case` and `fold` even while no directory is included; Latin-1 text is decoded in a third of the memory. - `-v` lists the rule destinations inside a directory that were not scanned, so files there are no longer invisible. - Plan and table widths count terminal columns: a name in a wide script (CJK) no longer runs past the terminal or misaligns its column. A long name's continuation lines - in the plan, and in the per-file headings of review and undo - start past the label column, so a name cannot pass for a step line. - Duplicate warnings, for the directory and on a rule, name the path shortened to `~` and do not repeat it inside the cause (`duplicate: ~/dl/a.txt: open: permission denied`). - `--json` says which exclude set a file aside, carries the warnings raised while matching, and lists unmatched files that raised one. - A literal `{{` or `}}` in a destination is text, not a placeholder: the directory a placeholder may not leave, and the directory left out of the walk, now include it. - In a real run, a later directory is no longer blocked by paths an earlier one moved files away from, or planned and did not apply ("target exists"); where an earlier directory's files ended up stays protected from a later `overwrite`. - `krino explain` says when a duplicate's delete would be skipped, no longer removes a directory's unused keyword cache (it holds no lock), and walks the directory only when a duplicate test needs it. - `{1}` … `{9}` keep the name's diacritics: a folded `name` test still matches `Lodz`, but `Łódź-faktura.pdf` now files under `Łódź`, not `Lodz`, including when the name is stored decomposed. - A tool's error message is shown even when it writes more than the capture limit at once. - A damaged lock file naming a pid beyond 32 bits no longer counts as held by whatever process the kernel reads it as. - Tests pass on OpenBSD 7.9 and FreeBSD 15.0 (test binaries run on the hosts; the fake extraction tools no longer rely on GNU `head -c` or a `printf` builtin). ## 0.0.7 — 2026-09-14 Security and correctness hardening, from a threat model (spec §15.1), an independent review, and the tests that hold both. Hostile names and contents - File names, paths, tool messages, errors, explain traces, JSON plans and log fields can no longer control or reorder the terminal: control characters, every bidirectional control, line and paragraph separators and invalid UTF-8 are printed as escapes (`\x1b`, `\u202e`). - A placeholder can no longer take a destination outside the directory the rule names: a capture of "..", "~" or nothing is refused with a reason, and so is a rename to "", "." or "..". - A file swapped between planning and applying - for a symlink, or for another file with the same size and modification time - is not acted on; a step that had to take a free name stops the rest of its chain. - `on-conflict overwrite` never trashes a directory, or another file of the same plan; it skips or takes a free name instead. - A content exclude holds when its content test cannot read the file (fails closed), so a protected file krino could not read is left alone. A file whose format has no text (an image, an archive) simply does not match a content test, so it is not set aside; one that starts as text and turns binary further on is unreadable, and is. - Extraction tools are only found through absolute PATH entries. Apply and undo - Every step is logged as soon as it has run; a step that ran but could not be logged is reported with where its file is now. SIGHUP is handled like SIGTERM, unless krino runs under `nohup`. An interrupt finishes the current step and skips the rest of that file's chain (an undo finishes the file it is reversing). A run killed mid-chain can be undone as far as it got. - Undo reverses a file its run renamed and then moved, moved twice, or moved and then trashed (it refused them before). - Undo restores a trash entry only while it is still the file the run put there, re-checks every file just before acting, keeps same-named files of two directories apart, and leaves no directory behind once it completes. Finishing an undo that stopped part way refuses a file changed since. - An undo that stopped part way can be finished by undoing the run again; plain `krino undo` after an undo does that. - The log restores a missing final newline, so a crash mid-line no longer breaks the next run's undo, and a damaged line refuses only the file it names. - Trash entries skip names left orphaned in the Trash, and long names are shortened to fit. Matching and the keyword cache - Folding maps the capital sharp s (ẞ) to "SS", and replaces invalid UTF-8 first so a stray byte no longer stops the next letter losing its accent. - The cache keys on the extension too (it picks the extractor), and is discarded when max-read, the Go release or the Unicode tables change; removed keywords leave it, and an unused cache is removed. Command line - Undo's per-file review behaves like review's (`w` leaves the rest unreviewed, choices echoed). - `w` stops krino even when applying fails. - `--min-age` is refused by commands it does not apply to, and an empty value is an error; a file dated in the future counts as brand new. - `krino new` refuses a path that is not valid UTF-8 or holds control or bidirectional characters, with that reason. - `-n`, `-y`, `--json` and `-v` are refused by commands that do not use them, instead of ignored: `krino -n new` no longer writes config. - A rule name cannot start with "(". - Builds need Go 1.25 or newer and use the Go 1.26.8 toolchain; `golang.org/x/text` is v0.41.0 (GO-2026-5970, and through the toolchain GO-2026-6088 and the `os/exec` and `os` fixes). Tests - Fuzz targets for every decoder of outside data (`make fuzz`), a generated apply-and-undo round trip comparing files and directories (`KRINO_PROPERTY_RUNS`), enum-completeness tests, `make race`, `make vulncheck`, and a dependency gate that checks modules. ## 0.0.6 — 2026-09-14 - Choosing per file: `w` now applies what was decided and quits krino, instead of going on to the next directory, so a long review can be done a session at a time. Files never reached are not logged as declined; the outcome line counts them as "not reviewed". - Each choice is echoed on its own red line under the file (`→ yes`, `→ trash`, ...). - Enter is ignored at the review prompts instead of being reported as a wrong key. ## 0.0.5 — 2026-09-14 - Keyword cache: for each file whose text it extracts, krino records which content keywords the text contains, in `~/.cache/krino/NAME.cache`, and answers content tests from it while the file is unchanged. A rerun over the same files extracts nothing: on a real downloads folder a dry run went from 12.8 s to 0.13 s. No text and no file names are stored. A changed file, a new keyword, or a changed extraction tool means reading again; failures are never cached. `krino check` shows where the cache is. - Choosing per file: `t` sends the file to the Trash and `d` deletes it permanently, after a `y` to confirm, instead of what its rules planned; both are logged under the rule `(review)`. "Done, apply chosen so far" moves from `d` to `w`, in undo's review too. ## 0.0.4 — 2026-09-14 - `max-size` setting: files larger than it are skipped as "too big", in `(defaults ...)` or a directory file. - `(exclude COND...)` sets files aside before any rule sees them, by type or extension, name regex, content, or any other condition; all conditions in one form must hold. In `krino.conf` it applies to every directory, in a directory file to that directory. Excluded files are counted in the plan and listed with the form that matched under `-v`; `explain` traces every exclusion and `check` lists them. - `--min-age DURATION` overrides every directory's `min-age` for one run, e.g. `krino -n --min-age 0` to include files written a moment ago. - `krino init` and `krino new` write commented examples of all three. ## 0.0.3 — 2026-09-14 - The plan is shown as one block per file instead of a table: the file's name, each step, then the rule and the reason it matched. On a terminal every line wraps to its width, with continuation lines indented under their own column; piped output is never wrapped. - Choosing per file (`c`) shows that same full block for each file, not only its steps. - `-P` / `--no-pager` prints the plan straight out instead of through the pager. - A duplicate's original is shown with `~` for the home directory. ## 0.0.2 — 2026-09-14 - Duplicates are found, never deleted. A rule combining `(duplicate)` with `(delete)` or `(delete permanent)` is refused by `krino check` and every run, and a file that is a duplicate under any duplicate scope its directory's rules use gets no delete step from any rule. This replaces 0.0.1's known limitation: duplicate conditions with different scopes can no longer delete every copy of a group, only move copies aside. - Coloured output on a terminal, in the terminal's own 16-colour palette: actions by kind, rule names, skipped steps and reasons, warnings, the outcome counts and the prompt keys. `--no-color`, before or after any subcommand, turns it off, as `NO_COLOR` does. ## 0.0.1 — 2026-09-13 - Per-directory rule files, a main file listing which directories run, and a template for new ones. - Conditions with `and`, `or` and `not` over type, name, path, content, size, age and duplicates. - Actions copy, move, rename and delete (to Trash by default), chained. - Plan, review and approve — all files or per file — with `-y` to skip review. - A log of every step, and `krino undo`. - Placeholders in destinations and new names. - Builds for Linux, FreeBSD and OpenBSD. Performance: no threshold or target for 0.0.1. These were measured during development and are reported rather than promised — throughput is dominated by external content extractors, not by krino itself: - Full plans over a real folder of about 265 files, 164 of them needing text extraction through `pdftotext` at roughly 80 ms each, measured between about 13 and 20 s across runs. - A synthetic benchmark during development planned a generated tree of 4,405 files needing no extraction in 1.09 s. That benchmark is not `make bench`, whose generated tree is smaller. Files that share a byte size but differ in content — six templated invoices, for instance — are still told apart, because the duplicate check hashes the first and last 64 KiB and then the whole file rather than trusting size. Known limitation: `(duplicate)` conditions with different scopes can elect different originals for the same content, and within one directory's rules this can select every copy for deletion — see `krino.conf(5)`, DUPLICATES.