Commit Graph
499 Commits
Author SHA1 Message Date
skylar fc578a0b02 chore(deps): bump 11 safe dependency updates 2026-10-25 16:36:22 -06:00
58d368f1f0 M10: Word export keeps an italic run inside its sentence's paragraph (#4158) (#4411)
The reading blocks keep style boundaries, so "Before Chevron after." with an
italic case name reached the Office exporters as three blocks, and each writer
emitted one paragraph per block. A block that continues another on the same
line (the test `continuations` already applies to keep such blocks out of
table cells) is now a run in its own size, bold/italic and colour inside the
paragraph of the block it continues, in Word, HTML, RTF and PowerPoint.

- `continued_from` names the block each one continues (nearest end);
  `continuations` is its `is_some`, so table detection is unchanged.
- `paragraphs` groups blocks left to right, joining only a block that starts
  strictly further left, with the same rotation, and never a table cell or the
  slide title.
- Block text is trimmed, so the engine records the source's spaces at its ends
  (`Block::space_around`, from the reading block's editor text); runs keep that
  space, or one where the page shows a gap.
- A paragraph is a heading only if its runs are (a bold lead-in no longer makes
  its line a heading); a smaller footnote marker does not demote a heading.

Style changes that wrap onto the next line still split paragraphs; that needs
the reading-block grouping change and is left for later.


Claude-Session: https://claude.ai/code/session_0175FWj4xFhMhSH3UpMU8DSC

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-25 10:11:00 -04:00
mtlcl410andClaude Opus 5.5 e391053658 M4: Document Properties reads a named opening destination's page, so choosing page 1 replaces it (ISO 32000-2 12.3.2.4) (#4574)
* M4: Document Properties reads a named opening destination's page, so choosing page 1 replaces it (#4572)

initial_view resolved only explicit opening destinations, so a named one
(ISO 32000-2 §12.3.2.4) read as page 1 at the default magnification, and
set_initial_view kept it as "unchanged" when page 1 was chosen. Names are
now resolved through the /Names /Dests tree and the catalog /Dests with the
existing bounded resolver, and a bare name or string counts as a destination
when page 1 at the default magnification removes /OpenAction.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0175FWj4xFhMhSH3UpMU8DSC

* M4: parity notes say page 1 at the default magnification removes /OpenAction

Document Properties replaces a named opening destination (ISO 32000-2
§12.3.2.4) with an explicit one when another page or magnification is
chosen; page 1 at the default magnification removes the catalog's
/OpenAction instead (§7.7.2), so the first page opens.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0175FWj4xFhMhSH3UpMU8DSC

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 10:02:40 -04:00
.CooCoo 0db7d57619 Tests: XFA calculation tests no longer fail when the machine is busy (#4535)
Five XFA tests failed at random on a busy machine. The scripts of an
opening or a change share a 3 s wall-clock budget (a click's share 5 s),
each script has 1 s or 3 s, and the engine has 10 s to start; under load
the tests went past one of them, and the scripts they check were skipped
or did not start.

Session::with_xfa_time_scale (and Automation::with_xfa_time_scale)
multiplies each of these limits for a test; the default stays 1. The
script session counts the engine threads it starts on the calling thread
(xfa_session::engines_started).

The hundreds-of-calculations tests run with the limits raised, and the
engine's test now checks that opening starts one engine and a change one
more. A new test sets the factor to 0 and checks that the budget skips
all 210 calculations and says so. Three more tests that failed the same
way under load run with the limits raised.
2026-10-25 10:02:00 -04:00
Eric Kaden 3f5d8d06fe M9: name a laid-out XFA form's signature fields by their SOM path, so other viewers find the signature (#4507)
* M9: name a laid-out XFA form's signature fields by their SOM path, so other viewers find the signature

* M9: doc comments on the functions the signature naming touches

* M9: say other viewers in the signature naming's comments

* M9: a new layout keeps a signature a generated signature box holds

* M9: the re-layout test checks the signature's picture is kept too

* M9: a signature field named by its SOM path doesn't repeat a name a field already has

* M9: every name a field of the pass takes is known before a signature field is named, in either order

* M9: the signed-box test's doc comment is back on its test

A merge had left it above the next test, which has its own.
2026-10-25 10:01:53 -04:00
b6fedc09f0 M7.5: crop an added image by more than half from one side (#4521)
* M7.5: crop an added image by more than half from one side

Each side of an added image's crop was capped at 45 %, so keeping its right
third (70 % off the left) wasn't possible, and the drawing silently cut a
larger crop down. The limit is now per axis: a side may trim up to 95 % as
long as left and right, and bottom and top, keep at least 5 % together.
AddedImage::crop_valid checks input (content_update refuses the rest with
the rule), clamp_crop brings a crop read from a file into range keeping the
sides' proportion, and the Edit panel lets a side go up to what the opposite
leaves.

Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Marco Hagspiel <m.hagspiel@lcs-group.com>

* M7.5: accept crops that sum to the limit; keep untouched crop sides exact

`0.05 + 0.9` is just above `0.95` in floating point, so `crop_valid` rejected a crop that
keeps 5 %. Sums now pass within a rounding tolerance, and `clamp_crop` leaves such a crop as
it is. The image panel no longer rounds the stored crop to whole percent or clamps sides the
user doesn't touch: only an edited side is written back.

Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Marco Hagspiel <m.hagspiel@lcs-group.com>

* M7.5: an excessive crop from a file keeps its proportion

`clamp_crop` capped each side before fitting the pair, so `[1.5, 0, 0.5, 0]` lost its 3:1
proportion. The pair is now scaled first, from its halves so two huge sides can't overflow;
a pair within the limit keeps each side within it.

Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Marco Hagspiel <m.hagspiel@lcs-group.com>

---------

Signed-off-by: Marco Hagspiel <m.hagspiel@lcs-group.com>
Co-authored-by: Marco Hagspiel <m.hagspiel@lcs-group.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-10-25 10:01:48 -04:00
af4a307e99 M7: dotted day-first dates in header and footer (#4503)
Header and footer dates had US, ISO and day-first slash forms only. The
dotted forms written in German-speaking and much of Central and Eastern
Europe join them: <<dd.mm.yyyy>>, <<d.m.yyyy>>, <<dd.mm.yy>>, <<d.m.yy>>.
The dialog's date list offers the first three after the existing ones.

Generated with Claude Code

Signed-off-by: Marco Hagspiel <m.hagspiel@lcs-group.com>
Co-authored-by: Marco Hagspiel <m.hagspiel@lcs-group.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-10-25 10:00:06 -04:00
alehel ad8c2f46dd Merge origin/main into fix/comment-threads-by-irt
Conflicts:
- crates/render/src/inspect.rs: this branch's reply-linking methods and main's NoRotate
  rectangles (#3016) both go in the Annotation impl; the annotation reader takes this
  branch's loop with main's no_rotate field.
- crates/automation/src/comments.rs: this branch's HashMap import, main's import list.
- ATTRIBUTION.toml: each catalogue's source note keeps main's secured-document clause and
  this branch's reply-count clause; ATTRIBUTION.md regenerated (cargo xtask assets --write).
- The 33 catalogues: main's new rows, then this branch's reply-count row.

main's #3016 tests build Annotation field by field; they now set parent and grouped.
2026-10-24 20:42:11 +02:00
cc5fd54868 M6: an editable combo box takes a typed value (ISO 32000-2 Table 233 Edit) (#4451)
A combo box with the Edit flag (bit 19) opened only its list, so a value
that isn't one of its options could not be typed on the page, although
form_fill already took one. ISO 32000-2 12.7.5.4 and Table 233: with Edit
set, "the combo box shall include an editable text box as well as a
drop-down list".

- An editable combo box now opens a single-line text box over the field
  as well as its list. Enter, Tab or a click away commits the text as
  Edit::SetFieldValue with a Choice, the path form_fill and the list use,
  so undo, field scripts and the appearance follow; a pick from the list
  still sets an option.
- The editor starts on the shown text of the picked option, and typing a
  shown text picks that option (the engine maps it to its export value).
- A combo box without Edit is unchanged: a drop-down list only.
- Tests: kittest typing into an editable combo (Enter, list pick, Tab,
  save, undo) and a non-editable one refusing typing; automation
  form_fill takes free text only with Edit, drawn before and after save.
- Parity: form.fill-choice notes and evidence.


Claude-Session: https://claude.ai/code/session_0175FWj4xFhMhSH3UpMU8DSC

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-24 10:44:38 -04:00
mtlcl410andClaude Opus 5.5 fc91ab9250 M3: a document opens at its /OpenAction destination, position and named destinations included (ISO 32000-2 Table 149, 12.3.2.4) (#4376)
- render: DocInfo::open_dest resolves /OpenAction (an explicit destination,
  a /GoTo action's /D, or a name or string through /Names /Dests or the
  catalog /Dests) with the existing bounded dest() resolver.
- ui-egui: apply_initial_view goes there with DocView::go_to_dest, so the
  /XYZ point lands at the window's top-left and /FitH top at its top edge.
  With "Always use my default zoom and page layout", only the page.
- organize, ui-egui: /ViewerPreferences /NonFullScreenPageMode (Table 147)
  is read for /PageMode /FullScreen documents and names the panel shown
  behind the full-screen prompt; such a document no longer gets the outline
  panel only because it has an outline.
- organize: set_initial_view keeps an opening action whose page and
  magnification are unchanged, instead of dropping its position or deleting
  a named go-to.
- automation: doc_info reports open_destination.


Claude-Session: https://claude.ai/code/session_0175FWj4xFhMhSH3UpMU8DSC

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 10:30:21 -04:00
Gabriel Luís da Silva be0ec70756 fix(automation): roots-only mode refuses other shares and devices by name (#4289)
With only --automation-read-root / --automation-write-root set, a path
went straight to std::fs::canonicalize, so a UNC, WebDAV or device path
(`\host\share\x.pdf`, `\.\pipe\x`) was resolved, contacting the host,
before it was refused. `--root` mode already refuses those by name
(`foreign_share`).

`foreign_share` moves into file_access, and `AutomationRoots::check_path`
applies it to the path made absolute (by name, nothing read) before
following links, with the same refusal as any other path outside the
root. A root on a share still accepts that share. `resolve` is split
into `absolute` (empty path, device names, made absolute) and `resolve`
(links followed), so `out\NUL` keeps its own refusal.

Part of #4003 (the first of its three points).
2026-10-24 10:30:06 -04:00
alehel c16956f539 Merge origin/main into fix/comment-threads-by-irt
main's #4284 stopped hashing translation catalogues in ATTRIBUTION.toml; the
conflict took main's side (no sha256 lines) and kept this branch's source notes.
2026-10-24 11:31:20 +02:00
aiongg f9a179fee0 Merge integration batch B's review fixes 2026-10-24 08:26:10 +00:00
aiongg afa14c3b98 Merge integration batch A's review fixes 2026-10-24 08:26:10 +00:00
aiongg 82eedb6607 Integration: a retained-version optimize reports the later of header and catalog version (#3227)
Object streams raise the header to 1.5 over a lower catalog /Version; pdf_version now reports the version viewers read.
2026-10-24 08:21:14 +00:00
aiongg 1fb1877923 Integration: attachment_add reads its file through the size-capped reader (#1960)
attachment_add used an unbounded std::fs::read; it now uses read_input like the other file-reading tools, and the oversized-input test covers it.
2026-10-24 08:20:39 +00:00
aiongg f053da3d93 Merge PR #1960: Attachments: attach a file and delete one (panel, edit, tools); duplicate names numbered as text
i18n ca, fi, hi, id, ko, ro, sq .tsv: kept both sides' rows (no duplicate keys).
2026-10-24 07:00:24 +00:00
aiongg 464dbd49e0 Integration: #3765 name the case-only comparison in the doc comments (NFC/NFD not compared) 2026-10-24 07:00:10 +00:00
aiongg 23d2e1c823 Merge PR #3765: Refuse an OCR batch whose inputs would write one output file
Cargo.lock: took main's.
crates/automation/src/a11y.rs: kept both sides (PR's helpers, then main's request_json doc comment).
2026-10-24 06:59:56 +00:00
aiongg ce684cd1d3 Merge track r3-t06 2026-10-24 06:50:31 +00:00
aiongg 6a206a0a2e Merge track r3-t05 2026-10-24 06:48:48 +00:00
aiongg 692f34fce2 Merge track r3-t04 2026-10-24 06:40:21 +00:00
aiongg 9705e7a6c1 Merge track r3-t03 2026-10-24 06:39:33 +00:00
aiongg a05dbdcb1e Merge track r3-t02 2026-10-24 06:38:46 +00:00
aiongg aedf339fb8 Merge PR #4181: M11: PDF Optimizer Clean Up stores LZW streams with Flate
Conflicts: ATTRIBUTION.toml: took main's side (hashes recomputed after the merges). crates/optimize/src/lib.rs, crates/automation/src/lib.rs, tools.rs: Settings and tool schema keep object_streams (#3227) and lzw_to_flate (#4181); doc_optimize description names both options. crates/optimize/tests/optimize.rs: both sides' imports and tests kept.
2026-10-24 06:26:33 +00:00
aiongg 1d270206be Merge PR #3227: M11: target PDF version and object streams option in PDF Optimizer
Conflicts: docs/roadmap.md: all status lines kept, M10 row from main, M11 row with #3227's two sentences. parity/acrobat-features.toml: save-with-object-streams entry from #3227 with its test paths under tests/it/.
2026-10-24 06:25:55 +00:00
aiongg 880723e3af Merge PR #4266: M7: added text takes character spacing from Format text
crates/engine/src/lib.rs: union of the added:: re-exports (paste_place/paste_rect from #4230, CHAR_SPACING_LIMIT).
crates/ui-egui/src/content_ui.rs: editor keeps #4178's anchored width and horizontal_align and #4266's spacing layouter; spaced_job takes the alignment so the custom layouter keeps it.
crates/ui-egui/tests/it/content.rs: kept both tests.
2026-10-24 06:24:10 +00:00
aiongg 541c82d59f Merge PR #4318: M12: links in XFA rich text become clickable links
crates/xfa/src/tests.rs: both sides' appended tests kept.
parity/acrobat-features.toml: form.xfa-dynamic evidence and notes: both sides' insertions (word-level merge).
2026-10-24 06:24:04 +00:00
aiongg f017ca3b78 Merge PR #3580: M6: form buttons show the side panels (app.execMenuItem)
crates/js/src/lib.rs: module doc app/util list has execMenuItem and scand; install() doc comment from main.
crates/js/src/tests.rs, crates/engine/src/tests.rs: both sides' appended tests kept.
parity/acrobat-features.toml: js notes: execMenuItem wording plus scand wording.
2026-10-24 06:23:47 +00:00
aiongg f71a305915 Merge PR #4230: M7: paste added text boxes and images in the engine and through content_copy (#2636)
ROADMAP.md, docs/roadmap.md: kept both sides' status and progress-log lines; did not restore the older M14 status line that the branch had already trimmed.
2026-10-24 06:23:24 +00:00
aiongg 581faa876a Merge PR #4349: M5: stamps without an appearance draw the stamp they name (ISO 32000-2 Table 184) 2026-10-24 06:23:15 +00:00
aiongg 8a64b65e4f Merge PR #4330: M8: redact labelled Italian international slash numbers
ROADMAP.md: kept both progress-log lines, newest first by position (HEAD's #4280 line, then this one).
2026-10-24 06:23:03 +00:00
aiongg 8ff6c8af31 Merge PR #2748: M10: a booklet decides two-sided itself, as Acrobat does (#2707)
Conflicts: crates/automation/src/printing.rs: kept job_duplex (#2748) next to bad; #4161's paper_named/spool_job unchanged.
2026-10-24 06:22:34 +00:00
aiongg ee989ab5ca Merge PR #3016: M5: sticky notes stay upright where they are placed on rotated pages (NoRotate, #2583)
Conflicts:
- parity/acrobat-features.toml: comment.note: kept main's icon notes text, added the NoRotate sentence and the PR's three evidence entries.
- vendor/README.md: kept main's rows; added the PR's hayro-interpret patch as (53) with its test (53).
2026-10-24 06:22:25 +00:00
aiongg c24e90b59c Merge PR #3985: M6: a document script that throws no longer stops the form's other scripts (#3940)
crates/automation/src/tools.rs: js_run description: both sides' wording merged.
2026-10-24 06:22:15 +00:00
aiongg 0041a3b10e Merge PR #4161: M10: every paper size reaches the printer, from the dialog and doc_print (#3752)
Conflicts: ROADMAP.md: kept both progress-log lines.
2026-10-24 06:22:15 +00:00
aiongg 19b36de16f Merge PR #3997: fix(automation): guard MCP dispatch and bound line and file reads (#3924) 2026-10-24 06:22:01 +00:00
aiongg 01dc49c13c Merge PR #4053: fix: Issue #4018, form_fill warns when a value does not fit its field (#4018)
crates/forms/src/tests.rs: append conflict, kept both tests.
2026-10-24 06:21:41 +00:00
aiongg 4fedfe7e8c Merge PR #2648: fix(xfa): skip PDF fallback scripts while XFA layout is active
crates/engine/src/js.rs: kept main's DocInfo.xfa field and multi-line layout, added the PR's xfa_active gate on doc_scripts.
2026-10-24 06:21:20 +00:00
aiongg fa3a0221d9 Merge PR #3000: fix(edit): batch styled paragraph fallback glyphs 2026-10-24 06:21:12 +00:00
aiongg ba85d2af66 Merge PR #2957: M6: dotted field names make parent fields (#2696) 2026-10-24 06:21:05 +00:00
a047c6ba09 chore: resolve SonarCloud maintainability issues (#4356)
* chore: resolve SonarCloud maintainability issues

* fix(packaging): use verify_sha256 helper function

* style: cargo fmt

* fix: restore wildcard imports and fix method-reference type errors

Revert the glob-import expansions (they missed SomIndices and left unused
imports), revert char::is_ascii_digit where the iterator yields char by
value, and pass the content slice to prune_doomed_xobjects as &[Vec<u8>].

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Keep XObject decodability check without binding content in interp

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Dylan Taylor <dylan@craftmaker.dev>
2026-10-23 23:28:18 -04:00
mtlcl410andClaude Opus 5.5 184e9f30ff M10: a booklet decides two-sided itself, as Acrobat does (#2707)
dialog hides Two-sided while Booklet is chosen and prints the booklet two-sided itself
(Using Acrobat X Pro, Print dialog box options: "Acrobat automatically enables duplex
printing, if available, for booklet printing"; later manuals and the current help no
longer say so, and on a current Acrobat the Two-sided control is shown for Size and
hidden for Booklet with the same duplex-capable printer). print::booklet_duplex gives a
booklet's setting: both sides flip on the short edge, the fold of a 2-up sheet turned
sideways; front or back only is one pass of a manual duplex and prints one-sided. The
Print dialog hides Two-sided while Booklet is chosen and sends that setting, leaving the
Two-sided choice as it was for the other layouts (and as #2001 remembers it); doc_print
applies the same rule. This replaces the first version, which rewrote Two-sided to the
short edge when Booklet was chosen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 09:27:26 +08:00
mtlcl410andClaude Opus 5.5 3db28cc4e3 M5: stamps without an appearance draw the stamp they name (ISO 32000-2 Table 184)
A rubber stamp from another program with no normal appearance was not
drawn at all. The display copy now gives it one:

- the fourteen standard names of Table 184 draw that stamp with
  PdfCraft's stamp drawing (eight from the palette, six with labels of
  their own), fitted to /Rect, in /C or the stamp's colour;
- no /Name draws Draft, the table's default, unless /IT says the stamp
  is an image or a snapshot;
- any other name (names match byte for byte, §7.3.5) draws a thin grey
  frame with both diagonals, so the stamp stays visible and selectable.

The drawing is display-only, like the other comments without an
appearance: Save writes the file as it was, and build() still declines
these stamps, so restyling and flattening never write it into the file.
A stamp with its own /AP /N is never touched. stamp() now takes the
label and the sign-here flag instead of a StampKind.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0175FWj4xFhMhSH3UpMU8DSC
2026-10-24 09:15:12 +08:00
nlevaandOpus 5.5 medium ultracode 5515f321c8 M7: copy added JPEG 2000 images as their file (#2636)
Review: copying an added JPEG 2000 image failed even within its own
document, because the exporter can't write JPEG 2000 yet. Add Image stores
such a picture as the file it was given, so that file is the copy: it
pastes into the same document as the same object and into another as one
embedded image. The notes now also say that a CMYK picture stored
losslessly turns to RGB in another document.

Co-Authored-By: Opus 5.5 medium ultracode <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017gEgu9NcTcPqKovsLyfPxK
2026-10-24 01:17:34 +02:00
nlevaandOpus 5.5 medium ultracode 302ad68502 M7: paste added text boxes and images in the engine and through content_copy (#2636)
Edit::PasteContent pastes an added text box or image copied in the same
document or another, as it was (a text box's style; an image's picture,
turns, flips and crop), as one undo step. A pasted image draws its picture
from the copied object while that is still the picture in the target, else
from an added image there that already holds it (an earlier paste from
another document), else the file is embedded. Document::added_image_file
gives a copied image's picture as a file and honours the copy permission.

pdfcraft_edit::added::paste_rect / paste_place place a paste where the item
was, 18 pt down and to the right past each copy of it on the page (an image
with the same box and transforms counts whichever object holds its
picture), or at a point, kept on the page.

The content_copy tool drives it headlessly, across pages and documents.

The desktop keys and menus come in follow-up PRs.

Co-Authored-By: Opus 5.5 medium ultracode <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017gEgu9NcTcPqKovsLyfPxK
2026-10-24 01:17:34 +02:00
Osvaldo Duran MelendezandClaude Opus 5.5 8b2cefc04c Merge upstream main a2ec27b4 into pr/attachments: translation catalogs and notes kept from both sides, ATTRIBUTION hashes recomputed (conflicted: nothing)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 18:16:24 -04:00
Kyle Kassen e373dffa20 M8: redact labelled Italian international slash numbers 2026-10-23 16:37:10 -05:00
Eric Kaden 0b8758eda3 M12: links in XFA rich text become clickable links 2026-10-23 16:16:40 -04:00
aiongg c9ac28a048 test(automation): give sign_document a real digital ID and document in the hostile-arguments sweep (#4277)
* test(automation): give sign_document a real digital ID and document in the hostile-arguments sweep

sign_id_create, earlier in the sweep, wrote a digital ID to the in.pdf
placeholder that sign_document then took as its id. Almost every
sign_document call failed to decrypt it and never reached the hostile
value under test, and the decrypts took most of the test's time.

Each tool now starts from an empty root, and sign_document gets a test
ID with its password and a fresh blank document per call. 370 of its
440 calls get past the ID (1 before) and 340 past the document (0
before); the sweep takes 7 s instead of 89 s.

* test(automation): empty the sweep's root through its canonical path

form_merge_data, given the hostile name " ", writes a file of that name
into the root through the canonical \\?\ path, where Windows takes it
literally. Emptying the root through the plain path dropped the trailing
space, so the entry named the root itself and removing it deleted the
root. Entries are now reached through the canonical path, their type is
read from the directory entry, and a failed removal is reported.

* test(automation): fail the sweep when the root can't be emptied

Listing, typing or removing an entry left in the root now fails the test
with the entry's path, so no tool runs against an earlier tool's files.
2026-10-23 14:22:19 -04:00