Skip to content

Release v0.10.0 - #115

Merged
otsobide merged 14 commits into
mainfrom
develop
Aug 28, 2026
Merged

Release v0.10.0#115
otsobide merged 14 commits into
mainfrom
develop

Conversation

@otsobide

Copy link
Copy Markdown
Owner

Release of the work sitting on develop. No version bump is needed, and
that is worth explaining rather than assuming.

v0.10.0 was bumped but never shipped

The newest tag in the repository is v0.9.0. All twelve version locations — the
five Cargo.tomls, tauri.conf.json, both package.jsons and all four
lockfiles — already read 0.10.0, set by c8d2b66 and merged to main in
#111, but no v0.10.0 tag was ever pushed and no release was ever published.

So 0.10.0 is a version number nobody has. Shipping this state as v0.10.0 is
correct: it needs no bump, it burns no number, and it leaves no gap in the tag
sequence. Bumping to 0.11.0 would create a phantom 0.10.0 that existed only
in the manifests.

The audit is in the checklist below; every location was verified before opening
this.

What ships

Extraction refuses a name this system cannot write, instead of renaming it.
The behaviour change users will notice. A refused character used to become a
question and be replaced with the user's answer; a trailing dot or space was
truncated in silence; a reserved device name gained a _. What that produced
was a tree of names the program had invented, indistinguishable afterwards from
the archive's own. An extraction now reproduces what the archive says or it does
not happen, and the refusal names the entry, the component and the reason.

On Linux almost nothing changes: NameRules::unix() refuses only the NUL byte.
This is a Windows-facing change, and the Windows CI leg exercises it.

Closes #95 (the names the user approved are not provably the names extracted —
there is nothing to approve any more) and #91 (the collision guard, which cannot
be reached now that nothing is renamed). Supersedes #90, whose reported symptom
is now the intended policy for every unwritable name.

Repository furniture, no user-facing effect: the development branch moved
from dev to develop across the workflows and documentation, and the
assistant tooling entries came out of .gitignore and .dockerignore.

Pre-release checklist

  • All twelve version locations agree on 0.10.0
  • develop is green, including test (rust, windows) and test (rust, macos)
  • docs/threat_model.md measure 4b describes the shipped behaviour
  • Tag v0.10.0 on the merge commit — release.yml refuses a tag whose
    commit is not on main, and checks the tag against apps/cli/Cargo.toml
    and tauri.conf.json

Extraction used to negotiate. A character Windows refuses became a question
for the user, a trailing dot was dropped without asking, and a reserved device
name gained a `_`; the entries were then written under whatever name that
arrived at. The tree on disk was this crate's invention rather than the
archive's, and nothing downstream could tell which files those were.

The policy is now the narrow one: all of the archive under its own names, or
none of it. `refuse_unwritable_names` judges the whole listing against
`NameRules::can_write` before the first byte, and the first component the host
cannot hold exactly as spelled fails the extraction with `NameError::Unwritable`,
naming the entry, the component and why. All four faults answer the same way,
because from the caller's side they are one failure: the name they asked for is
not the name they would get.

Consequences worth stating rather than discovering:

- `extract` and `extract_with` now refuse archives they used to extract. On
  Windows that includes ordinary Linux tarballs holding `aux.log` or
  `notes.txt.`, which is the whole point and also the cost.
- `ExtractOptions::with_replacements` is inert. Kept, with `Substitutions`, so
  the two front-end dialogs still compile while they are taken out.
- `check_replacements` no longer runs from `extract_with`: validating an input
  that is ignored is worse than not taking one.
- `refuse_overwriting_the_archive` drops its plan argument. It followed the
  rewritten name because a substitution could land an entry on the archive that
  the archive's own name did not match; nothing rewrites a name any more.
- `NamePlan` is now always the identity and `extract_with` calls the plain
  backends. The type, the `extract_*_planned` variants and tar's second write
  path are dead and deliberately left in place: that path carries the
  containment guard and should not move in a commit about naming.

The tests are untouched on purpose, so the ones that pinned the old behaviour
fail. They are the record of what was decided before and rewriting them in the
same commit would hide the size of the change.
`run_extract` refused an archive whose listing needed a character replaced and
waved through a trailing dot or a reserved device, because those two had one
correct answer that needed nobody. Core applied them silently and extraction
continued.

Core refuses all three now, so the split has nothing behind it: an archive with
`notes.txt.` in it will not extract on Windows whatever this check says. Widened
to the whole report.

Refusing here as well is still not redundant. Core stops at the first offending
component; this has the same listing in hand and names every entry at fault at
once, which is the message someone can act on.

`adjustments` is left in place and is now provably empty by the time it is
reached, since the report above it was empty. It goes when
`Outcome::Extracted` loses the field.

Tests untouched, so the cases that pinned a trailing dot extracting fail.
`NameInspection`'s doc claimed the extraction validates the answers again
before opening the archive. It did, and no longer does: core neither validates
nor applies a substitution, so an answer cannot reach a file name at all.

The dialog still runs and still collects answers that go nowhere. Removing it,
along with the `unwritable_names` command it is built from, is the follow-up.
The five that failed asserted the answers being applied. Rewritten to assert
what happens instead, keeping each one pointed at the guarantee it was written
for rather than deleting it and losing the record.

- `every_fault_stops_the_whole_archive_the_same_way` replaces
  `the_answers_are_written_and_the_listing_names_what_is_on_disk`, and is now a
  table over all four faults, since the point of the policy is that they have
  one ending. `summary.txt` rides along in each archive: it is writable
  everywhere and must still not reach disk.
- `a_colon_entry_is_refused_and_its_neighbour_is_never_written` keeps issue #63
  and moves its sharpest assertion. `notes.txt` used to have to survive intact;
  now it must not exist at all.
- `two_entries_that_used_to_collide_are_refused_for_their_own_names` records
  that a collision cannot be manufactured any more, and asserts no planned name
  appears in the message, which is what would show something still working one
  out.
- `a_trailing_dot_is_refused_rather_than_folded_onto_the_name_beside_it` is the
  renamed collision-with-an-untouched-entry case. It was passing under a name
  that had stopped being true.
- `an_answer_no_longer_rescues_an_entry_the_host_cannot_write` is the old
  answer-validation test turned around. Its subject is gone, so it now guards
  the policy most likely to be quietly undone: the most reasonable answer to
  the most ordinary question must still extract nothing.

`an_entry_with_no_answer_stops_before_anything_is_written` is dropped, absorbed
whole by the first of these.

In security.rs, two tests depended on the renamed write path that no longer
exists. Both keep their real assertion (the archive intact, the file outside
the output directory absent) and say in the body that the reason changed:
refusal now comes before the write rather than the guard catching it during.
`a_replacement_cannot_carry_an_entry_out_of_the_output_directory` still offers
the hostile answers and still must see nothing move.
`a_name_that_needs_no_answer_is_adjusted_rather_than_refused` was passing while
asserting the opposite of what the CLI does, which is the worst state a test
can be in: it exercised `adjustments`, a pure function that still works, and
its name and doc comment described a product behaviour that is gone.

Both adjustment tests now pin the criterion `run_extract` actually uses. The
report's `characters` half is empty for a trailing dot or a device name — that
is why they used to extract — and `is_empty()` is not, which is the whole of
the one-line change. Judged through `NameReport` rather than by running the
command, since these faults are Windows-only and this suite runs on Linux.

`adjustments` and `Adjustment` lose their only tests and their import here.
They are dead code, reached only where the report is provably empty, and go
when `Outcome::Extracted` loses the field.
The host-rules half of this suite drove the NUL byte through the whole
exchange: ask, answer, extract, check the name on disk. The answer reaches
nothing now, so those assertions had no subject.

- `no_answer_rescues_a_name_this_computer_cannot_write` replaces both the
  substantive answer and the empty one with a table over the two, since they
  were the same test with a different string. It is the one to look at if the
  dialog is ever wired back up by accident.
- `a_hostile_answer_changes_nothing_because_no_answer_is_applied` keeps the
  `../escaped` fixture from the separator test. That answer used to be refused
  by the ruleset; now it is simply inert, and the archive extracts while
  nothing called `escaped` appears anywhere.
- `the_refusal_says_which_character_is_the_problem` keeps the reason travelling
  to the dialog, which matters more here than anywhere: a NUL prints as
  nothing, so an entry named without its character looks fine.
- `an_entry_beside_the_name_it_would_have_taken_is_still_just_refused` records
  that the collision it was built for cannot happen, and holds on to the part
  that still matters — the writable neighbour is not written either.
Left behind by rewriting the last test in two files. `fmt/check` is CI's first
gate and everything waits on it, so this cost a whole run.
rustfmt breaks a call whose arguments exceed 60 columns, not 100, which is the
width the eye checks against. Both of these fit a line and still get split.
The three documents describing the naming layer all still described the scheme
that was replaced, which is worse than describing nothing: threat_model.md is
the stated source of truth for why each guard exists, and it was explaining a
guard that had stopped existing.

**threat_model.md**, measure 4b, is rewritten around the narrowing and keeps the
reason for it, since the scheme it replaced looked like the kinder answer and
somebody will propose it again. What it produced was a tree of names this
program had invented, indistinguishable afterwards from the archive's own. The
`unwritable_names` report is now described as what it became: a prediction of a
refusal rather than a questionnaire.

Measure 4a loses the paragraph about following the *planned* name. That guard
had to, while a substitution could land an entry on the archive under a name the
archive did not spell; with nothing renamed the two names are one, and the guard
got simpler rather than weaker.

Two test names it cited no longer existed, both renamed in this branch, and a
sweep of every identifier the docs cite says the rest resolve. The new pair that
pins the policy itself is cited too.

**architecture.md**, the names.rs section, is rewritten the same way, and now
says which surface is inert (`Substitutions`, `with_replacements`, `NamePlan`,
the `extract_*_planned` variants) so the next reader does not take dead
machinery for live design. The CLI section gains the pre-flight refusal and why
it duplicates core's: core stops at the first offending component, the CLI has
the whole listing and names every entry at once.

Also corrected along the way, drift that predates this branch: both
architecture.md and desktop.md said the desktop exposes four Tauri commands and
listed four. There are five — `unwritable_names` arrived later — and
desktop.md's layout block never gained `src/names.rs` either.
None of these come from this branch; they are drift the review found while
checking the naming rewrite, and they are worth the same commit because they
are all "the documentation describes a smaller product than the one that
ships".

**`--verify` was undocumented for the CLI.** The flag exists, is shipped, and
`docs/server.md` documents its server-side twin in full, while README.md and
architecture.md's command surface never mentioned it. Worse, neither said the
thing a user would actually want to know: *every* local compression is already
read back, and `--verify` only chooses how deeply. A compressor here finalises
on drop, so a run that dies partway through leaves an archive that opens
cleanly and is silently short — the default check is what catches that, and it
was invisible in the docs. Also recorded: the flag is refused with `--server`,
because the archive is built on the far side.

**`test (desktop, rust)` was missing from the CI section**, which exists to
describe the job graph and listed every other job. It is the only Linux job
that compiles the Tauri crate, and it needs Node for the bundle
`generate_context!()` embeds.

**The README's test counts were stale**, and were stale before this branch:
620/505 written against 623/507 on `main`. Now 621/506, which this branch's own
net of two removed tests produced. A number nothing verifies will drift again;
worth either dropping or adding a check, and it is not obvious which.

A sweep of every internal link, anchor, cited file path and cited test name
across all eight documents came back clean otherwise.
One logical change across CI and the documentation, kept in one commit because
splitting it would leave a state where the workflows enforce one branch name and
the documents describe another.

**Workflows.** `gitflow.yml` is the one that matters: it only triggers on PRs
whose base is in `branches:`, and its two jobs are the required status checks on
the rulesets. Left pointing at `dev`, a PR into `develop` would never run it, so
the required check would never report and the PR would be unmergeable — blocked
waiting on something that cannot arrive. Its trigger, both `base_ref` guards,
the release head-branch check and every message now say `develop`.

`test-and-build.yml` gets the push trigger and the `head_ref` gate that lets the
macOS and Windows suites run on the release path. `deploy-landing.yml` gets its
push trigger. `release.yml` only mentioned the old name in a comment.

**The staging branch keeps its name.** `pages/landing-dev` is what serves the
staging site, so renaming it would change that URL and buy nothing. The name
describes the environment, not the branch the build came from, and both
`deployment.md` and the workflow now say so rather than leaving it looking like
something that was missed.

**Documentation.** 41 references across `git_flow.md`, `deployment.md`,
`architecture.md` and `README.md`. The rename was applied only to the backticked
branch reference, so `pages/landing-dev`, the `collapse-server-aio:dev` image
tag, `npm run tauri dev` and the word "development" were left alone; the ASCII
flow diagram and a table's column alignment were fixed by hand afterwards.

**Not in this commit, and required before `dev` is deleted:** the rulesets.
`protect-dev` still targets `refs/heads/dev`. The documentation now calls it
`protect-develop`, which is a promise this commit cannot keep on its own.
chore: move the development branch from dev to develop
Two lines, and they were the only mentions left in tracked files.

`.gitignore` listed `CLAUDE.md`, a file that was never committed — the entry
existed so it could not be. With the file gone from the working copy too, the
entry has nothing to ignore, and leaving it would keep a name in the repository
for a file the repository does not have.

`.dockerignore` excluded `.claude/` under "never ship history or local tooling
state into the context", beside `.git/`, `.github/` and `node_modules/`.

**That second one has a small cost, stated rather than buried:** if a `.claude/`
directory ever appears at the root it now enters the Docker build context. It
does not exist today, and `*.md` further down already keeps stray markdown out,
so nothing changes for the images as they stand. The exclusion was hygiene, not
a dependency — no build reads that path — but it was doing something, and this
gives it up on purpose. Re-adding the line is the fix if a context ever grows
for that reason.

Nothing here touches the build: `cargo`, the Dockerfiles and the workflows never
read either path.
…repo

chore: take the assistant tooling out of the repository
@otsobide
otsobide merged commit 6f23f71 into main Aug 28, 2026
38 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

core: the names the user approved are not provably the names extracted

1 participant