aboutsummaryrefslogtreecommitdiff
path: root/CHANGELOG.md
blob: ff9f900aa4653688368d27639148172832246401 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
# Changelog

## Unreleased

## 0.0.8 — unreleased

- 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 `?`. A document read only in part
  answers the keywords found in what was read, and leaves the others
  unknown.
- 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").
- `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`.
- 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.