# Changelog ## Unreleased ## 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. - Extraction tools are only found through absolute PATH entries. Apply and undo - Every step is logged as soon as it has run; SIGHUP is handled like SIGTERM. 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. - 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. - 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, with that reason. - 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.