Commit Graph
1396 Commits
Author SHA1 Message Date
skylar taylor-barrick d873514e8a Document driver option refusal and equivalent grayscale capability 2026-10-13 04:50:19 +02:00
skylar taylor-barrick 3929c3d14c Refuse explicit print options dropped by Windows drivers 2026-10-13 04:48:42 +02:00
skylartaylor b55b21cdeb Apply permission policy review before first release 2026-10-13 04:33:01 +02:00
aiongg 7f05840bc0 craft-cms: portable moxcms CMYK oracle with the built-in Coated FOGRA39
The Generic CMYK oracle only runs where macOS ships that profile. The
same comparison (CMYK -> sRGB, 8-bit, relative colorimetric without
BPC, same bytes for both engines) with the built-in Coated FOGRA39
runs everywhere. Measured: max 10, p99 5, mean 0.89 codes; the worst
pixel matches an independent float computation, not moxcms.
2026-10-13 01:50:14 +00:00
aiongg 1f0d815477 craft-cms: map device white to device white on every path
With a LUT destination, sRGB white came out as 0.3-0.5 % C/M/Y through
Transform::eval and precise float conversions: the legacy Lab encoding
puts L* 100 at 0.996 and a* = 0 at 0.50001, between the BToA grid
nodes, and fix_white only patched the device-link node, so only the
table paths were right.

Transform: for every intent but absolute colorimetric (and proofs
without paper simulation), a pixel equal to the source's device white
gives exactly the destination's device white. fix_white still sets the
device-link node; the paths that do not read that node exactly (the
exact pipeline, float interpolation, and 8-bit lookups when white is no
node) compare each pixel with white once. No other colour changes.
device_white(Lab) had a* = b* = 0.5; Lab white is 128/255 in the v4
encoding, which is no node of the 33-point grid, so Lab sources take
the per-pixel check there too.

Profile: synth::CmykParams::white_on_node maps no ink to exactly
L* 100, a* = b* = 0 in the AToB and gives the BToA tables input curves
that put that white on a node holding no ink. The Coated FOGRA39
profile is regenerated with it and separates PCS white to 2e-5 ink
(was 0.5 %), so the step into white is as smooth as its neighbours.
The default parameters and the Photocraft profile are unchanged; its
white still falls between nodes, so its last grey step into white is
up to about 0.4 % C larger than the one before, as with lcms's
white-node fix, and the test allows that for this profile.

FOGRA39 accuracy against 16a66e1: AToB against the patches unchanged
(mean 0.07, p95 0.18, max 0.53 ΔE00), reference sRGB values unchanged
(max 0.76), Lab -> CMYK -> Lab p95 1.04 -> 1.07, max 2.54 -> 2.55
(mean 0.24).
2026-10-13 01:50:14 +00:00
aiongg d0107d1130 craft-cms: return errors instead of panicking on caller input
The crate denies panics, but asserts and slice indexing were reachable
from arguments: pixel strides, buffers that are not whole pixels or a
destination shorter than the source, single-colour slices shorter than
the channel count, CLUT and pipeline dimensions, and LUT sizes.

Every such check now returns CmsError::InvalidArgument. Buffer
conversions validate once per call, before any pixel is written, so the
per-pixel loops carry no extra checks; internal callers that already
guarantee lengths use crate-private unchecked evaluation.

Pipeline, Clut and Lut3d keep their fields private (read through
accessors) so a validated value cannot be changed into one that panics.
Pipeline::new checks matrix shapes and channel counts; Clut::new and
Clut::sample check grid, outputs and the table size limit.

API changes (unreleased crate):
- Transform::eval, eval_fast, convert_u8/u16/f32, apply,
  convert_bytes_many and from_pipeline return Result.
- Pipeline::new, extend and eval, Clut::new, sample, eval,
  eval_trilinear and eval_multilinear return Result.
- GamutCheck::delta_e and out_of_gamut, Lut3d::from_transform,
  LutFile::identity and from_fn, synth::cmyk_profile and coated_cmyk
  return Result.
- Pipeline, Clut and Lut3d fields are read through methods.
2026-10-13 01:33:07 +00:00
skylartaylor 6ba4d0b97a Refuse annotation visibility the native renderer cannot print correctly 2026-10-13 03:29:55 +02:00
aiongg f97ed953f4 craft-cms: built-in Coated FOGRA39 profile fitted to Fogra's data
Add Builtin::CoatedFogra39 ("Coated FOGRA39 (fitted, craft-cms)",
profiles/craft-coated-fogra39.icc: AToB 11^4, BToA 33^3, 330% TAC,
medium GCR). It is fitted to Fogra's FOGRA39 characterisation data
(tests/data/FOGRA39.txt, unmodified, 1617 patches): a Yule-Nielsen
Neugebauer model with free overprints, per-ink tone curves and n, plus
a Gaussian-kernel ridge correction of the residuals. Against the
patches, relative colorimetric through the AToB: mean dE00 0.07,
p95 0.18, max 0.53. The BToA tables invert the AToB table
(synth::cmyk_profile_from_model); the Photocraft coated profile and its
synth defaults are unchanged.

Builtin::from_profile_name maps the names documents carry for
FOGRA39-based profiles (Coated FOGRA39, ISO Coated v2 (ECI)) to the new
profile and every built-in by its description. Adds math::delta_e2000.

Refs #36
2026-10-13 01:23:45 +00:00
+2 dd9e3f1bc3 Add craft-cms: ICC colour management extracted from PhotoCraft
Move PhotoCraft's crates/cms (storytold/photocraft@b3222ee8e7)
into crates/cms as craft-cms: ICC v2/v4 profile parsing and writing, transforms, rendering
intents, black point compensation, soft proofing, gamut checks, synthetic CMYK, 3D LUTs, LUT
files and built-in profiles, with all tests.

The Bradford inverse and the built-in RGB colorant matrices are computed in constants, so the
build rejects a singular matrix and the two expect calls are gone; the values are bit-identical
to the runtime results. Built-in profile descriptions and copyright text keep their PhotoCraft
wording so the profile bytes do not change. rayon joins the workspace dependencies.

Refs #36

Co-authored-by: Alex Gorevski <agorevski@users.noreply.github.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Elvis33LE <67311580+Elvis33LE@users.noreply.github.com>
Co-authored-by: F4LCON <107556180+itsF4LCON@users.noreply.github.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
Co-authored-by: HEOJUNFO <100739943+HEOJUNFO@users.noreply.github.com>
Co-authored-by: Shehan Kavishka <kavishkashehan726@gmail.com>
Co-authored-by: Valentin Klein <vangel1976@googlemail.com>
Co-authored-by: ljkx <177671075+ljkx@users.noreply.github.com>
Co-authored-by: storybored379 <storybored379@gmail.com>
Co-authored-by: wstock <w.stock@yahoo.com>
2026-10-13 01:23:45 +00:00
skylartaylor ee70afc29a Pin driver copy-validity qualification fix 2026-10-13 02:49:34 +02:00
skylartaylor b9f475471a Support normal new-file permissions with private atomic staging 2026-10-13 02:26:24 +02:00
skylartaylor 8fb89562d1 Expose shared safe driver settings and RGB print transport 2026-10-13 02:15:46 +02:00
skylartaylor ccb1eae613 fix(print): refuse fitting that would discard annotations 2026-10-13 01:47:19 +02:00
skylartaylor 4af92ad6aa feat(print): bridge native page processing to submitter callbacks 2026-10-13 01:20:21 +02:00
skylartaylorandTKIPisalegacycipher ebc4d8cfbd Add caller-thread progress for native sheet processing
Co-authored-by: TKIPisalegacycipher <git@chir.us>
2026-10-13 01:20:21 +02:00
skylartaylor eb49a247a3 Gate Unix-only atomic test imports by platform 2026-10-13 00:29:22 +02:00
skylar taylor-barrick 79e89dc129 Merge remote-tracking branch 'origin/main' into codex/craft-print-fix-merge-20261012T210558Z
# Conflicts:
#	Cargo.lock
2026-10-12 23:49:49 +02:00
Alejandro Londono 6006abb757 feat(settings): add shared config storage for Craft apps
Add shared config.toml storage while keeping settings types, defaults and UI policy in each app.

Includes isolated in-memory operation, configurable portable paths, atomic saves with private backups, damaged-file preservation, schema migrations, and documented best-effort handling of external edits.

Validation: native macOS formatting, strict workspace clippy, 172 workspace tests and the craft-settings WASM check. Independent backup-permission and failed external-migration repros fail before the fixes and pass afterward.

Hosted CI: all seven UI and printing jobs pass across macOS, Linux, Windows and UI WASM.

Refs #12
2026-10-12 15:40:50 -06:00
skylar taylor-barrick 697b473c0b Stop PDF work immediately after a parser budget is exhausted 2026-10-12 23:27:53 +02:00
skylar taylor-barrick c22935270b Parse duplicate PDF dictionary keys in linear time 2026-10-12 23:22:05 +02:00
skylar taylor-barrick 85ef8b92ac Bound PDF parsing and preserve indirect page content arrays 2026-10-12 23:14:06 +02:00
skylar taylor-barrick 02d08fe9cc fix(print): pin native completion repair and document bounded PDF handling 2026-10-12 23:12:41 +02:00
skylar taylor-barrick a7e3c76c2f Merge remote-tracking branch 'origin/main' into codex/craft-print-fix-merge-20261012T210558Z 2026-10-12 23:05:59 +02:00
James Locke 36ce57c929 Merge remote-tracking branch 'origin/main' into claude/print-native-windows-73c826
# Conflicts:
#	Cargo.lock
2026-10-12 11:33:06 -07:00
skylar taylor-barrick 811558a3ad Create Unix atomic-write staging files with private permissions 2026-10-12 20:23:17 +02:00
Zach Shallbetter 5b273dd82f Clarify application recovery sweep responsibility and hardware durability limits 2026-10-12 20:23:17 +02:00
Zach Shallbetter d4a38f90ee Clarify user-space cleanup semantics and best-effort directory sync in craft-atomic 2026-10-12 20:23:17 +02:00
Zach Shallbetter f75e9c9bc8 Document atomicity vs durability guarantees and add sequence verification test 2026-10-12 20:23:17 +02:00
aiongg 6bcd350d49 README: credit PhotoCraft only, document what a replace does not keep
The crate holds PhotoCraft's engine, and unsafe code is forbidden by the
workspace lint rather than in lib.rs.
2026-10-12 20:23:17 +02:00
aiongg 33b393bd88 Remove the unused proptest dev-dependency 2026-10-12 20:23:17 +02:00
aiongg 9d422a18b1 Retry Windows sharing and lock violations, with a capped backoff
retry_rename also retries ERROR_SHARING_VIOLATION (32) and
ERROR_LOCK_VIOLATION (33), which a rename can report while a scanner
holds the file, and caps each pause at 200 ms.
2026-10-12 20:23:17 +02:00
aiongg 253fd775f0 Set the replaced file's permissions on the open temporary file
The original's permissions are applied to the temporary file before it
is flushed, and a failure fails the save instead of being ignored.
2026-10-12 20:23:17 +02:00
aiongg d78b352004 Refuse to replace a file the user cannot write (Unix)
A rename needs only a writable folder, so a read-only file would be
replaced where writing it in place would fail. Before staging, the
existing file is opened for writing without truncating; when that fails
the save fails with a read-only error.
2026-10-12 20:23:17 +02:00
aiongg 658ca37c8e Follow symbolic links without canonicalize
The target is found by reading links one hop at a time, resolving
relative links against the link's folder. A dangling link writes the
file it names and stays a link, and a loop of links is an error.
2026-10-12 20:23:17 +02:00
aiongg fb26a9f90f Report a missing folder instead of creating it
A save into a folder that does not exist fails with an error naming the
folder, so a save to an unmounted network share does not create a local
folder in its place.
2026-10-12 20:23:17 +02:00
aiongg 9e160690fc Never replace an existing file in write_new
write_new_with gives the staged file its name with a hard link on every
platform. Where hard links fail for any reason other than an existing
target (FAT/exFAT, some SMB and FUSE volumes), it copies the staged
content into a file opened with create_new. It shares the temporary-file
retry, error context and folder flush with the replace path, and its
errors only mention an original file when there is one.
2026-10-12 20:23:17 +02:00
aiongg 818c0c0753 Share one replace path between the byte and streaming writers
atomic_write, atomic_write_with and atomic_write_custom all run through
one internal function that takes the AtomicFs, the RenameRetry and a
writer callback, so fault injection also covers streaming writes.
AtomicFs creates and flushes the temporary file (create_new, sync)
instead of writing a byte slice.
2026-10-12 20:23:17 +02:00
Zach Shallbetter 8df3d87d8b Add craft-atomic: crash-safe atomic write and document replacement (refs #2, fixes #14) 2026-10-12 20:23:17 +02:00
skylar taylor-barrick 8e9e437e78 craft-quit: route menu Quit to the root viewport 2026-10-12 20:18:18 +02:00
Jake MacekandClaude Opus 5.5 f0ae2b3f13 craft-quit: return the app menu install error to the app
install_app_menu printed the error and carried on, so an app could not
tell that ⌘Q had fallen back to winit's Quit, which skips the prompt.
It now returns the error; the app decides whether it is fatal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 20:18:18 +02:00
Jake MacekandClaude Opus 5.5 673c14439b craft-quit: Quit menu id and handler for apps with their own menu
Apps that build their own muda app menu can't use install_app_menu: it
replaces the whole menu and sets muda's process-wide event handler.
QUIT_MENU_ID and on_menu_event(ctx, id) let them give their own Quit
item the id and route its event through the app's close handling.
Neither needs muda, so any muda version works. install_app_menu now
uses both.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 20:18:18 +02:00
Jake MacekandClaude Opus 5.5 ebf3f552cd craft-quit: macOS app menu whose Quit asks the app first
winit's default app menu sends Quit (Cmd+Q) straight to
-[NSApplication terminate:] and winit doesn't implement
applicationShouldTerminate:, so a Craft app quits without its
unsaved-changes prompt. The new opt-in `menu` feature adds
`install_app_menu(ctx, app_name)`, which replaces that menu with a
muda-built copy whose Quit sends ViewportCommand::Close. The app's own
close handling then runs, as for the close button, and
on_close_request quits.

muda is a macOS-only, optional dependency; no unsafe code.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 20:18:18 +02:00
skylar taylor-barrick bafeec6b7d Merge remote-tracking branch 'origin/main' into fix-quit5-20261012 2026-10-12 20:15:22 +02:00
skylar taylor-barrick c6a847b7d0 craft-quit: document logic-based close handling for hidden windows 2026-10-12 20:11:55 +02:00
8caa51af04 Hyphenation: Bulgarian text follows the Bulgarian syllable rules (storytold/wordcraft#826)
Runs in Bulgarian (`w:lang` bg, bg-BG) are hyphenated by the Bulgarian syllable rules
(crates/proof/src/bg.rs, clean-room from the spelling rules; no pattern file or word list):
vowel-vowel splits, one consonant goes with the next syllable, a consonant cluster splits after
its first consonant except that an obstruent before р/л starts the next syllable (до-бре,
та-бли-ца), й stays with its vowel, дж/дз and ьо are never split.

- ProofLang::Bg (bg, bul): hyphenation only. It isn't "proofed", so like Other there are no
  spelling or grammar marks until there is a Bulgarian dictionary.
- Latin-script words in a Bulgarian run are hyphenated as English, as in any other language.
- The layout already takes each word's language from its run (ProofRun), so nothing changes there.

Tests: the rules (срич-ко-пре-на-ся-не, та-бли-ца, шо-фьор, хо-джа, строй-ка), tag resolution,
hyphenate_word_in with the paragraph limits (Бъл-га-рия), and a laid-out bg-BG paragraph whose
hyphenated lines all break at Bulgarian points.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-12 07:51:05 -04:00
3090bcaef0 Portuguese proofing: pt-BR spelling, suggestions, grammar and hyphenation (storytold/wordcraft#100) (storytold/wordcraft#797)
* M15: Polish interface, and Polish spelling, grammar and hyphenation

Interface
- Polish is the tenth interface language ("Polski", pl.tsv, 1,045 entries);
  pl, pl-PL and pl_PL.UTF-8 system locales select it automatically.
- Right-click menu labels now go through translation in every language.
- Review > Language is a menu (English US/UK, Polski, Do not check spelling
  or grammar); the status bar shows the language at the caret.
- With a Polish interface, new blank documents and opened .txt/.md files
  default to pl-PL.

Proofing
- Each run is checked in its own language (w:lang, resolved through
  styles): squiggles, F7, review.issues, suggestions and hyphenation.
  Text in languages without proofing tools is no longer flagged as English.
- Polish spelling from the SJP.PL Hunspell dictionary (Apache-2.0 option),
  read by a new Rust affix engine (crates/proof/src/affix.rs); 875 KB asset,
  ~4.5M word forms. Regenerate with the pl_dictionary example.
- Polish suggestions: case, missing diacritics, rz/ż, ó/u, ch/h confusions,
  missing spaces (napewno -> na pewno), edit distance.
- Conservative Polish grammar rules: repeated words, spacing, doubled
  punctuation, sentence capitals, missing comma before że/który/ponieważ/
  aby/żeby/jeśli…, we/ze forms.
- Polish hyphenation with the hyph-utf8 hyph-pl.tex patterns (MIT option);
  pattern keys widened to 20 letters, TeX exceptions parsed; the heuristic
  no longer treats ł, ż, ś… as vowels.

review.language validates and normalizes tags, can set noProof alone and
the document default; format.state reports lang and noProof.

Tests for every part, docs and ATTRIBUTION.md/NOTICE updated;
cargo xtask ci passes (fmt, clippy, test, assets, layers, wasm).

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

* Layout: rustfmt the test module order left unformatted by storytold/wordcraft#391

cargo fmt --check failed on main after storytold/wordcraft#391 added mod tests_grid.

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

* Review fixes for storytold/wordcraft#346: read Polish spelling from the installed Hunspell dictionary

- Polish spelling uses the Hunspell pl_PL dictionary installed on the
  system (OS packages, Homebrew, LibreOffice, ~/.local/share/hunspell, or
  WORDCRAFT_DICTIONARIES) instead of a bundled 870 KB SJP.PL file;
  without one, Polish text gets no spelling marks, like Word without the
  language's proofing tools. .aff/.dic are size-capped and decoded from
  UTF-8 or ISO 8859-2 (crates/proof/src/hunspell.rs).
- The dictionary can be replaced at runtime (install_dictionary) or
  re-searched (reload_dictionary); an empty one accepts every word.
- Tests use small synthetic dictionaries written for them, so they don't
  depend on what the machine has installed.
- The bundled pl.dic, its ATTRIBUTION/NOTICE entries and the generator
  example are removed; docs describe the installed dictionary.

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

* storytold/wordcraft#100: affix — accept FLAG UTF-8 and DEFAULT flag modes

Per-character flag modes are what the reader already parses; only the
multi-character encodings (long, num) stay errors. VERO pt_BR declares
FLAG UTF-8.

* storytold/wordcraft#100: affix — widen flag masks to u128 (128 flag capacity)

VERO pt_BR needs 106 distinct flags; the u64 bitmask capped at 64.
Serialization is unaffected: WCAFF1 stores flag characters and
re-derives the masks on load.

* storytold/wordcraft#100: affix — support FORBIDDENWORD

VERO pt_BR marks spellings it must reject with a forbidden flag (ý).
Stems carrying it are listed but never correct and never inflect.

* storytold/wordcraft#100: affix — continuation classes in check

SFX A 0 able/B: the form derived by A carries flag B, licensing a
further suffix of class B (workable -> workables) while the bare stem
cannot take one. form_has_flag walks the chain depth-bounded
(MAX_CHAIN = 3); with no continuations declared it degenerates to the
stem lookup, so existing dictionaries are unaffected. VERO pt_BR uses
26 continuation rules.

* storytold/wordcraft#100: affix — continuation classes in forms

Enumerate chained suffix forms (workable -> workables) under the same
MAX_CHAIN bound the checker walks, so every form the checker accepts
is listed and vice versa.

* storytold/wordcraft#100: affix — round-trip continuations and FORBIDDENWORD through WCAFF1

The compact format keeps its WCAFF1 magic: continuation classes ride
on the rule's appended field (as in the .aff source) and the forbidden
flag becomes a FORBIDDENWORD line. Old files read as before.

* storytold/wordcraft#100: affix — strip a UTF-8 BOM from .aff/.dic input

VERO's files open with a BOM; glued to the count line it parsed as a
stem (one garbage entry per load) and to SET it hid the directive.

* storytold/wordcraft#782: affix — the module doc still rejected what the code now reads

Continuation classes and FORBIDDENWORD are supported; the header said
Rejected for both.

* storytold/wordcraft#100: pt — Portuguese spelling, following the Polish module

ProofLang::Pt resolves pt/pt-BR/pt-PT/por tags; the pt module reads the
installed Hunspell pt_BR dictionary (VERO) through the affix engine —
FLAG UTF-8 flags, continuation classes and FORBIDDENWORD spellings all
exercised by the synthetic test dictionary. Hyphenated compounds are
checked part by part (guarda-roupa, ex-marido); without a dictionary
installed, Portuguese text gets no marks. Spelling message in
Portuguese; suggestions and grammar land next.

* storytold/wordcraft#100: pt — Portuguese suggestions

Rounds of rising cost, driven by the dictionary's own tables: missing
accents from MAP (coraçao -> coração, saude -> saúde), the REP
confusions (seje -> seja, excessão -> exceção), a missing space after a
short function word (nãoé -> não é, dagua -> da água), one typo over
TRY, and an accent-folding stem scan — the Polish rounds, retuned for
Portuguese.

* storytold/wordcraft#100: pt — Portuguese grammar flags

Conservative rules with Portuguese messages: repeated words (except
the emphatic doubles Portuguese allows), space before punctuation and
double spaces, sentence capitals knowing the Portuguese abbreviations
and dialogue dashes, and doubled punctuation. No comma rule: que opens
a clause only sometimes, so a wrong flag would cost more than a missed
one.

* storytold/wordcraft#100: pt — Portuguese hyphenation

hyph-utf8's hyph-pt.tex bundled unmodified (BSD 3-clause, ATTRIBUTION
row with source and hash): 426 Liang patterns and 2 exception words;
falls back to the generic heuristic outside their reach.

* storytold/wordcraft#100: pt — Review › Language offers Português (Brasil), and a pt-br interface writes pt-BR

The menu's fourth entry sets the run language; the status bar names
Portuguese; editing_lang is now allowed for pt-br (wordcraft-proof
proofs it), so new documents under a Portuguese interface are
Portuguese by default.

* storytold/wordcraft#100: pt — engine test: Portuguese proofing through the review commands

review.language pt-BR normalizes the tag, review.issues reports
Portuguese spelling and grammar with the lang field, F7 and Change
apply a suggestion, Add to Dictionary works for Portuguese.

* storytold/wordcraft#100: pt — Portuguese in the mixed-language proofing tests and docs

check_text proofs a Portuguese span with the Portuguese message; the
never-panic loops and the module docs cover the third language.

* storytold/wordcraft#100: docs — Portuguese proofing in the gap, parity and roadmap docs

Gap 8 narrowed (English, Polish and Portuguese), localization parity
gains a Portuguese row and a revision entry, target-app-parity's
Proofing row moves 40% -> 42%, and the ROADMAP progress log records
what landed.

* storytold/wordcraft#100: hunspell — LibreOffice extension folders follow the language

The search paths hardcoded LibreOffice's dict-pl; a pt_BR user whose
only dictionary is LibreOffice's dict-pt-BR extension — on Windows the
only realistic on-disk source — got silently no spelling. Both the
primary-language folder and the dashed full name are now tried, so
Polish keeps dict-pl and Portuguese finds dict-pt-BR.

* storytold/wordcraft#100: pt — review fixes

Suggestion ties break toward the change nearest the word's start
(saúde before saudê, proven against a fixture that has both); the
Language menu's label is the English source "Portuguese (Brazil)" with
translations in all twelve catalogs (as Polish is); the attribution
counts pt.pat's 2 exception words; the pt fixture drops a duplicate
stem; docs/roadmap.md catches up with the landing; the throwaway spike
is gone.

* Review fixes for storytold/wordcraft#782: two-suffix chain limit, module doc

- MAX_CHAIN = 2 counts suffixes in a row, matching Hunspell's two-suffix limit
  (the old bound of 3 allowed up to four); test pins that a third is rejected.
- Module doc: say probes allocate small scratch strings instead of claiming none.

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

* Review fixes for storytold/wordcraft#797: NOTICE for pt.pat, no indexing in the LibreOffice folder helper

- NOTICE reproduces hyph-pt.tex's BSD 3-Clause copyright notice, conditions and
  disclaimer, which the licence requires for binary redistributions.
- hunspell::lo_ext_dirs builds its list without indexing (never-crash rule).
- Catalogs: main already translates "Portuguese (Brazil)" in every language, so the
  duplicate rows the branch added are dropped.

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

---------

Co-authored-by: witkowski01 <konradwitkowski01@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-12 07:43:24 -04:00
229f9b32d2 Affix engine: FLAG UTF-8, 128 flag bits, continuation classes, FORBIDDENWORD (storytold/wordcraft#100) (storytold/wordcraft#782)
* M15: Polish interface, and Polish spelling, grammar and hyphenation

Interface
- Polish is the tenth interface language ("Polski", pl.tsv, 1,045 entries);
  pl, pl-PL and pl_PL.UTF-8 system locales select it automatically.
- Right-click menu labels now go through translation in every language.
- Review > Language is a menu (English US/UK, Polski, Do not check spelling
  or grammar); the status bar shows the language at the caret.
- With a Polish interface, new blank documents and opened .txt/.md files
  default to pl-PL.

Proofing
- Each run is checked in its own language (w:lang, resolved through
  styles): squiggles, F7, review.issues, suggestions and hyphenation.
  Text in languages without proofing tools is no longer flagged as English.
- Polish spelling from the SJP.PL Hunspell dictionary (Apache-2.0 option),
  read by a new Rust affix engine (crates/proof/src/affix.rs); 875 KB asset,
  ~4.5M word forms. Regenerate with the pl_dictionary example.
- Polish suggestions: case, missing diacritics, rz/ż, ó/u, ch/h confusions,
  missing spaces (napewno -> na pewno), edit distance.
- Conservative Polish grammar rules: repeated words, spacing, doubled
  punctuation, sentence capitals, missing comma before że/który/ponieważ/
  aby/żeby/jeśli…, we/ze forms.
- Polish hyphenation with the hyph-utf8 hyph-pl.tex patterns (MIT option);
  pattern keys widened to 20 letters, TeX exceptions parsed; the heuristic
  no longer treats ł, ż, ś… as vowels.

review.language validates and normalizes tags, can set noProof alone and
the document default; format.state reports lang and noProof.

Tests for every part, docs and ATTRIBUTION.md/NOTICE updated;
cargo xtask ci passes (fmt, clippy, test, assets, layers, wasm).

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

* Layout: rustfmt the test module order left unformatted by storytold/wordcraft#391

cargo fmt --check failed on main after storytold/wordcraft#391 added mod tests_grid.

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

* Review fixes for storytold/wordcraft#346: read Polish spelling from the installed Hunspell dictionary

- Polish spelling uses the Hunspell pl_PL dictionary installed on the
  system (OS packages, Homebrew, LibreOffice, ~/.local/share/hunspell, or
  WORDCRAFT_DICTIONARIES) instead of a bundled 870 KB SJP.PL file;
  without one, Polish text gets no spelling marks, like Word without the
  language's proofing tools. .aff/.dic are size-capped and decoded from
  UTF-8 or ISO 8859-2 (crates/proof/src/hunspell.rs).
- The dictionary can be replaced at runtime (install_dictionary) or
  re-searched (reload_dictionary); an empty one accepts every word.
- Tests use small synthetic dictionaries written for them, so they don't
  depend on what the machine has installed.
- The bundled pl.dic, its ATTRIBUTION/NOTICE entries and the generator
  example are removed; docs describe the installed dictionary.

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

* storytold/wordcraft#100: affix — accept FLAG UTF-8 and DEFAULT flag modes

Per-character flag modes are what the reader already parses; only the
multi-character encodings (long, num) stay errors. VERO pt_BR declares
FLAG UTF-8.

* storytold/wordcraft#100: affix — widen flag masks to u128 (128 flag capacity)

VERO pt_BR needs 106 distinct flags; the u64 bitmask capped at 64.
Serialization is unaffected: WCAFF1 stores flag characters and
re-derives the masks on load.

* storytold/wordcraft#100: affix — support FORBIDDENWORD

VERO pt_BR marks spellings it must reject with a forbidden flag (ý).
Stems carrying it are listed but never correct and never inflect.

* storytold/wordcraft#100: affix — continuation classes in check

SFX A 0 able/B: the form derived by A carries flag B, licensing a
further suffix of class B (workable -> workables) while the bare stem
cannot take one. form_has_flag walks the chain depth-bounded
(MAX_CHAIN = 3); with no continuations declared it degenerates to the
stem lookup, so existing dictionaries are unaffected. VERO pt_BR uses
26 continuation rules.

* storytold/wordcraft#100: affix — continuation classes in forms

Enumerate chained suffix forms (workable -> workables) under the same
MAX_CHAIN bound the checker walks, so every form the checker accepts
is listed and vice versa.

* storytold/wordcraft#100: affix — round-trip continuations and FORBIDDENWORD through WCAFF1

The compact format keeps its WCAFF1 magic: continuation classes ride
on the rule's appended field (as in the .aff source) and the forbidden
flag becomes a FORBIDDENWORD line. Old files read as before.

* storytold/wordcraft#100: affix — strip a UTF-8 BOM from .aff/.dic input

VERO's files open with a BOM; glued to the count line it parsed as a
stem (one garbage entry per load) and to SET it hid the directive.

* storytold/wordcraft#782: affix — the module doc still rejected what the code now reads

Continuation classes and FORBIDDENWORD are supported; the header said
Rejected for both.

* Review fixes for storytold/wordcraft#782: two-suffix chain limit, module doc

- MAX_CHAIN = 2 counts suffixes in a row, matching Hunspell's two-suffix limit
  (the old bound of 3 allowed up to four); test pins that a third is rejected.
- Module doc: say probes allocate small scratch strings instead of claiming none.

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

---------

Co-authored-by: witkowski01 <konradwitkowski01@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-12 07:38:10 -04:00
a0faa851e3 Persist the custom dictionary across sessions (storytold/wordcraft#40) (storytold/wordcraft#796)
* Persist the custom dictionary across sessions (storytold/wordcraft#40)

* Review fixes for storytold/wordcraft#796: session-only Ignore All, desktop-only persistence, capped atomic I/O

- review.ignoreAll no longer adds to (and saves) the custom dictionary: ignored words go to a
  separate session-only list, as Ignore All does in Word.
- Only the desktop app wires the dictionary file. Headless MCP and `wordcraft-cli run` keep
  words in memory, so an agent never reads or changes the person's dictionary (and nothing
  writes outside an automation write root, storytold/wordcraft#769).
- Loading reads through `take(1 MB)`, so a huge file is never read whole; lines that aren't a
  single word (spaces, control characters, invalid UTF-8, over 64 characters) are skipped.
- review.addToDictionary applies the same word rule (proof::custom_word), so a word added is
  never dropped on the next load.
- Saving merges the words on disk first (another process's additions survive; words just
  removed stay out), creates the folder, and replaces the file atomically (temp + rename).
  The file I/O lives in the proof crate, so the engine doesn't touch std::fs itself.
- Tests for each.

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

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 07:23:51 -04:00
4655ae188c Skip non-English runs in proofing instead of flagging them (storytold/wordcraft#100) (storytold/wordcraft#741)
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 05:55:40 -04:00
dda47d482d M7: ignore ellipses in sentence capitalization proofing (storytold/wordcraft#742)
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 05:29:57 -04:00