Commit Graph
1565 Commits
Author SHA1 Message Date
.CooCoo fa1a657476 Add craft-doc: read PhotoCraft, VectorCraft and EffectCraft documents without the apps (#404)
* Add craft-doc: read PhotoCraft, VectorCraft and EffectCraft documents without the apps

A new crate, craft-doc, reads the native files of three apps from bytes:
PhotoCraft .pcraft (a ZIP bundle), VectorCraft .vectorcraft (JSON, plain
or gzip, with the version 4 data after the JSON) and EffectCraft .ecproj
(JSON).

- sniff tells which app wrote a file from its first bytes.
- info gives the format version, the writer the file names, and whether
  the version is one the crate reads.
- renditions returns VectorCraft's preview and PDF and PhotoCraft's
  thumbnail and composite, when the file has them.
- outline returns PhotoCraft's canvas and layer tree, VectorCraft's
  artboards and top-level layers, and EffectCraft's compositions with
  their layers.

A file whose version is newer than the crate reads is refused with
Error::TooNew. Limits bound the JSON, each entry, the total bytes
decompressed and the JSON nesting, and each is checked before the bytes
it covers are allocated. ZIP entries are read by a small STORE and
DEFLATE reader in the crate. The crate depends on serde, serde_json,
thiserror and flate2 and builds for wasm32-unknown-unknown.

The tests build every document they read: JSON written by hand, PNG,
ZIP and gzip bytes assembled in the test. doc.yml runs fmt, clippy, the
tests and a wasm32 check.

* ci: do not keep the checkout token in the doc workflow

The checkout step sets persist-credentials: false, as jpeg.yml and media.yml do. No later step in the job uses the token.

* craft-doc: read each format's envelope through its format crate

craft-doc no longer keeps its own copies of the envelope types and
constants. It reads them with craft-pcraft-format (`MANIFEST`,
`FORMAT_VERSION`, `THUMB`, `COMPOSITE`, `check_manifest`),
craft-vectorcraft-format (`Head`, `Image`, `sniff`, `check`, `VERSION`,
`BLOB_MAGIC`, base64) and craft-ecproj-format (`Head::read`,
`SCHEMA_VERSION`). Its own base64 module is removed. A `.pcraft`
manifest is still read in one pass without building a
`serde_json::Value`, with the checks `read_manifest` applies. The
outlines are still read with craft-doc's read-only views of each
document, and the public API (`sniff`, `info`, `renditions`, `outline`,
`Limits`) does not change.

These inputs are now read as the format crates read them:

- A `.pcraft` manifest without `generator`, or with a `thumbnail` other
  than `thumb.png` or a `composite` other than `composite/preview.png`,
  is malformed for `renditions` and `outline`; `info` still reports its
  version. A manifest with more than 4,194,304 JSON items is refused
  with `LimitExceeded`.
- A `.vectorcraft` preview or PDF member without `mime` gives no
  rendition.
- `sniff` takes `format` only from a top-level member, so
  `{"meta":{"format":"vectorcraft"}}` is not a VectorCraft file.

tests/envelopes.rs writes a file of each format with the crates' types
and reads it through every call, and checks the inputs above.
doc.yml also runs when one of the three crates changes.

* craft-doc: sniff reads only top-level members of an EffectCraft project

`sniff` took any JSON object whose first member is `savedBy` for an
EffectCraft project when `"schema"` appeared anywhere in its first
bytes, so `{"savedBy":"1.0","metadata":{"schema":1}}` was claimed. It
now checks the top-level member names only, one at a time, without
keeping them.

* craft-doc: bound list elements with max_items; no rendition without a MIME type

Limits gains max_items (default 1,048,576): EffectCraft project items and
composition layers, VectorCraft artboards and layers, and PhotoCraft layers
at every level are counted before each one is read, and a file that lists
more gives Error::LimitExceeded. A list of empty objects within max_json no
longer expands into gigabytes of layers.

A VectorCraft preview or pdf member with an empty mime gives no rendition, as
one without a mime does; a preview may be PNG or JPEG. An EffectCraft
composition without a duration or a frame rate gets no timing instead of a
duration of 0.
2026-11-02 16:29:51 -05:00
Dylan M. Taylor 431b34ad13 Merge pull request #486 from benandez/feat/font-store-language-packs
font-store: item kind language-pack (CP4.1)
2026-11-02 15:50:29 -05:00
Dylan M. Taylor e654924da4 Merge pull request #495 from benandez/p6-coderabbit
craft-proof: fixes from CodeRabbit on the ported code
2026-11-02 15:50:14 -05:00
Dylan M. Taylor 7cd6010068 Merge pull request #485 from benandez/feat/craft-proof-packs
craft-proof: installed Language packs (CP4.2) and the CRAFT_PROOF_EMBED switch (CP4.4 prep)
2026-11-02 15:49:59 -05:00
Dylan M. Taylor 9fe3ac302f Merge pull request #484 from benandez/p6-encodings
craft-proof: decode ISO 8859-7 and ISO 8859-15 dictionaries (Greek, Estonian)
2026-11-02 15:49:45 -05:00
Dylan M. Taylor a8522229f7 Merge pull request #490 from benandez/p6-sharps
craft-proof: CHECKSHARPS (German SS for ß in capitals)
2026-11-02 15:49:30 -05:00
Dylan M. Taylor 99a8686261 Merge pull request #489 from benandez/p6-oconv
craft-proof: OCONV, and capped, indexed ICONV/OCONV tables
2026-11-02 15:49:15 -05:00
Dylan M. Taylor ba26551de2 Merge pull request #488 from benandez/p6-affix
craft-proof: NEEDAFFIX, CIRCUMFIX and FULLSTRIP (port of wordcraft#5012's engine part)
2026-11-02 15:48:59 -05:00
Dylan M. Taylor 424461499b Merge pull request #487 from benandez/p6-flags
craft-proof: FLAG num and FLAG long affix dictionaries (port of wordcraft#5142)
2026-11-02 15:48:43 -05:00
Dylan M. Taylor 9e1ea140f3 Merge pull request #483 from benandez/feat/cp2-scowl
CP2.2: craft-proof reads SCOWL 2026 for English spelling
2026-11-02 15:48:22 -05:00
Dylan M. Taylor fcf3be172e Merge pull request #482 from benandez/feat/craft-proof
craft-proof: WordCraft's proofing library, shared (spelling, grammar, hyphenation, Hunspell)
2026-11-02 15:48:03 -05:00
vangel76andClaude Sonnet 5.5 b25d6e4797 bitstream: one 8-byte load in BitReader::peek, direct read_bit (#516)
peek built its window from five bounds-checked byte loads on every call.
While at least 8 bytes remain it now does one big-endian 8-byte load; the
last 7 bytes and positions past the end keep the old zero-padded byte
loop in a cold function. read_bit reads its byte directly instead of
going through read_bits(1).

Behaviour is unchanged: values, errors and positions are identical,
including peeks near and past the end of the buffer. Tests compare the
reader against a verbatim copy of the previous logic over random
operation sequences, plus the exact edge positions and a bitwise
reference for peek and read_bit.

Closes #516

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-02 21:36:05 +01:00
ljkx 867c9479b4 update: keep the remote HTTP agents and their redirects on HTTPS (#477)
The ureq agents for remote hosts now set https_only, so a redirect to http:// is refused before connecting instead of being followed in plain text. A download refused this way fails at once rather than retrying until it pauses. Tests and the local-mirror feature keep plain HTTP, and the direct agents still serve loopback links.
2026-11-02 19:38:56 +01:00
ljkx e412b14fa3 update: one asset per name in SHA256SUMS.txt (#478)
A line repeated as is counts once. A name listed with two different
hashes is dropped, with a warning, so the result no longer depends on
line order: the file has no published checksum and its install is
refused, as for any file the release doesn't vouch for.
2026-11-02 18:43:43 +01:00
FredApps ea1fbeb05b Merge pull request #470 from KotovGGG/codex/docking-host-geometry
Add opt-in docking host surfaces and fitted resize chrome
2026-11-02 18:26:01 +01:00
Aleksander HelgakerandClaude Opus 5.5 006b50d2ae craft-media: shared codec identifiers, one ByteSource, the testkit (1/10) (#439)
* craft-media: shared codec identifiers, one ByteSource, the testkit

The types a codec crate needs to describe a file and configure a
decoder, ahead of craft-audio:

- `Container`, `CodecId`, `PcmFormat` and `CodecParams` (`id.rs`),
  always built, with optional serde.
- `codec_params()` on `isobmff::Track` and `matroska::Track`, both
  returning `CodecParams` in the same form; `extradata` follows
  Matroska's `CodecPrivate`.
- One `ByteSource` trait (`source.rs`), with `ReadSeekSource`;
  `isobmff::ByteSource` and `matroska::ByteSource` are that trait.
- craft-media-testkit (`publish = false`): the ffmpeg oracle helpers
  from craft-media's tests, so the codec crates share one copy.

The existing tests prove the refactor; the new types have their own.

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

* craft-media: APV extradata is the bare record; reseek after a poisoned lock

An MP4 apvC track gave CodecParams.extradata with the 4-byte FullBox header
in front, where Matroska's V_APV CodecPrivate is the bare
APVDecoderConfigurationRecord. ApvConfig::record_bytes serializes the record
alone and extradata uses it.

ReadSeekSource kept its tracked position after recovering a poisoned lock or
a failed seek, so a reader moved by a panic was read from the wrong offset
with no error. Both now force a seek on the next read.

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

* craft-media: MP4 codec params per sample description

Track::codec_params always described the first stsd entry, while each sample
selects its own through description_index. It now describes the first
sample's entry; codec_params_for gives the parameters of any entry, and
has_mixed_descriptions says whether one set covers every sample.

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

* craft-media: test file-backed ByteSources; a File read past i64::MAX is an EOF

A File and a ReadSeekSource over a file now have the buffer's test: the same
bytes at the same offsets, and UnexpectedEof past the end. The test found
that a File read at an offset past i64::MAX failed with the OS's
InvalidInput instead of the UnexpectedEof the trait promises; it now
reports the end of the source like the other implementations.

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

* craft-media: Matroska codec params match MP4 for ALAC, DNx and Opus

Matroska's codec_params gave Unknown for A_ALAC and for DNxHD/DNxHR stored
as V_MS/VFW/FOURCC (AVdn, AVdh), where MP4 gives Alac and Dnxhd; ALAC's
extradata is the CodecPrivate, the ALACSpecificConfig MP4's alac box holds.
Opus now reports 48 kHz from Matroska as from MP4, whatever rate the muxer
declared.

ReadSeekSource clears the poison when it recovers the lock, so one
panicked read costs one recovery instead of a seek on every later read.

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

* craft-media: MP4 reports the MPEG audio layer the first frame names

An MP4 track with MPEG audio reported CodecId::Mp3 whatever the layer, since
mp4a + 0x69/0x6B names none (ffmpeg stores MP2 that way), while Matroska
reports Mp1/Mp2/Mp3 from its CodecID. open() now reads the frame header of
each such entry's first sample and codec_params() reports the layer it
names; without a valid header, QuickTime's .mp2 gives Mp2 and anything else
Mp3 as before. CodecConfig::Mp3 is unchanged.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 14:18:36 +01:00
cdhdt 939ded3e3d craft-update: a translator given after the prompt opened reaches it (review)
Updater::with_translator stored the function for later prompts only; a prompt already open
kept the old one. It now also sets the open prompt's.
2026-11-02 13:13:12 +01:00
cdhdt 76e2bb0eb9 craft-update: the updater's own install failures are translated; an error written to status_message wins over a note (review)
A panicking or dropped install worker produced English text shown as a literal error. The
worker's answer is now a Failure: the installer's words, shown as they came, or the updater's
own, a Note translated when the prompt draws. StartupPrompt::message() prefers status_message,
so an error written straight to the public field isn't hidden by an earlier note.
2026-11-02 12:08:03 +01:00
cdhdt 493bdb9b1c craft-update: the prompt's own messages are translated when it draws; fill keeps unknown placeholders whole (review)
The installed, save-your-work-first, can't-auto-update and couldn't-start messages were
translated when set, so a language change while the prompt showed one left it in the old
language. They are now a Note (English text and values) in StartupPrompt::status_note,
translated each time the prompt draws; errors from elsewhere stay in status_message as they
came. set_note/set_message/clear_message keep only one; message() is what the prompt shows.

fill keeps an unknown placeholder through its closing brace (`{bad {app}` stays), the rule
craft_i18n::fmt follows.
2026-11-02 11:17:00 +01:00
cdhdt a9e5afebf8 craft-update: the update dialogs take the app's translator
The startup prompt, the Check for Updates result card and the messages the Updater puts in the
prompt were English whatever the app's interface language (storytold/wordcraft#5257).
`StartupPrompt`, `CheckResultDialog` and `Updater` get `with_translator(Translate)`: a
`fn(&str, &[(&str, &str)]) -> String` that gets the English text (the key, as in the craft-i18n
catalogs) and its `{name}` values, so translations may reorder them and an app on craft-i18n
passes `craft_i18n::fmt(i18n.t(lang, text), args)`. It is called when the card draws, so a
language change shows on the next frame. Buttons are looked up in title case and drawn in
capitals, so the apps' existing button rows serve. Without a translator nothing changes.
2026-11-02 10:55:49 +01:00
Martin BallandClaude Opus 5.5 9b6ef1199a CP7.8: suggestion steps charge the work done; big homonym aliases load once
A step now counts a rule, a condition class, a split's suffix lookup,
16 flags of a set looked through and a group visit, so hostile rules
whose steps were cheap to count but costly to run hit the budget in
time. Retuned: 2^13 steps per candidate, 2^25 per suggest call; the
81 packs' typo sets suggest exactly as at d0c341c8.

Condition character sets are sorted and looked up by binary search.
A stem's sets are worked out once per distinct entry list when its
entries hold 256+ flags (16 kept-apart homonyms of 8,190 flags on
17,576 stems: 55 s load, now 57 ms). A near-word candidate whose
steps ran out counts as a word, so suggest never offers a compound
check() would refuse.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FypgCX8LsYYE8fsj4byBm2
2026-11-02 05:55:02 +00:00
Martin BallandClaude Opus 5.5 c6c5386c6b CP7.7: one suggest call shares one simple-word budget
Every TRY/KEY/typo candidate got a fresh 65,536-rule check, so a hostile
.aff with a 128-letter TRY line took ~20 s per suggest call. Now a
suggest call shares 2^23 affix rules across its simple-word checks, each
capped at 2^11; prefix-rule collection counts against the budget and
mixed-case lookups try at most MAX_HOMONYMS stems. Suggestion lists are
unchanged on the 77 packs and the recall sets.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FypgCX8LsYYE8fsj4byBm2
2026-11-02 04:56:18 +00:00
.CooCoo b8f7d93b00 Add craft-pcraft-format: the envelope of PhotoCraft's .pcraft bundle (#407)
* Add craft-pcraft-format: the envelope of PhotoCraft's .pcraft bundle

The manifest around the document, with the document as a type parameter
(Manifest<D>), TileRef, the bundle's entry paths and hash check,
FORMAT_VERSION, the manifest decode limits with the nesting and item
scan, and the migration runner as a generic step list. Messages and
serialized field order are PhotoCraft's, so its saved manifests stay
byte-identical when it switches to these types.

Part of #59.

* ci: do not keep the checkout token in the pcraft-format workflow

The checkout step sets persist-credentials: false, as jpeg.yml and media.yml do. No later step in the job uses the token.

* pcraft-format: build no entry name from an invalid hash

tile_path and blob_path return None unless the hash is 64 lowercase hex
digits, so a hash read from a file such as "../x" never becomes an entry
name. TileRef::path gives a tile's entry the same way.

The docs note that MAX_GROUP_DEPTH is a copy of PhotoCraft's constant,
and that check_manifest also refuses a manifest larger than
max_manifest_bytes, which PhotoCraft's check before a save does not.

* ci: pin actions/checkout by commit and Rust to 1.95 in the pcraft-format workflow

* pcraft-format: read_manifest is the one reader of a manifest

read_manifest(json, limits, steps, target) checks the JSON against the
limits, refuses a format_version newer than target, runs the migration
steps, and reads the document as D and the other envelope members.
read_manifest_with reads the document with the caller's function
instead. Manifest no longer implements Deserialize.

Error::TooNew holds the manifest's generator, so a message can name the
writer; its message is unchanged. TooNew, Manifest and TileRef are
#[non_exhaustive], and Manifest::new and TileRef::new build them.

TILES_DIR, BLOBS_DIR, COMPOSITE_DIR and OBJECT_SUFFIX name the bundle's
directories and the object suffix. The docs of max_blob_bytes and
max_total_bytes say the caller enforces them.

* pcraft-format: read_manifest reports errors in serde's struct order

read_manifest reads the envelope members and the document in the order
the parsed object holds them, and reports a missing member after the
members that are present. A manifest with several faults gives the same
error as a derived Deserialize of the whole manifest, as PhotoCraft
reads it today.

* pcraft-format: document that a missing or invalid format_version is Corrupt

read_manifest's docs and the README say that a missing or invalid
format_version gives Error::Corrupt from migrate, and that a missing
generator or document, or any other mistyped member, gives Error::Json.
A test covers a string and a fractional format_version.

* pcraft-format: read_manifest refuses preview names other than its own

A manifest's `thumbnail` and `composite` named any entry the reader then
opened, while tile entries go through `is_valid_hash`. `read_manifest`
now gives `Error::Corrupt` for a `thumbnail` other than `thumb.png` or a
`composite` other than `composite/preview.png`, the only names PhotoCraft
writes. The check runs after the members are read, so JSON errors keep
their order.

The reader's doc now says a missing or invalid `format_version` is
`Error::Corrupt`, from `migrate`, not `Error::Json`.
2026-11-01 23:31:04 -05:00
Martin BallandClaude Opus 5.5 d0c341c88f CP7.6: craft-libs#497 re-review — prefix flags sorted before lookup; one step budget per simple-word check
- simple_word sorts the grouped prefix flags (non-cross then cross flags were unsorted, so
  `redo`/`rework` from a no-cross prefix beside cross ones were missed).
- One budget of 65,536 affix rules per simple-word check, shared by every prefix group and
  suffix walk; past it the word is taken for no word. Prefixes that each strip their own
  text (h2.aff: 1,000 roots) no longer cost a full walk each: 1.7 s → 1.1 ms per two
  words, suggest 14.9 s → 65 ms. The 77 fetched spelling packs answer as without a budget
  on 6,000 generated words each.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FypgCX8LsYYE8fsj4byBm2
2026-11-02 04:22:14 +00:00
Martin BallandClaude Opus 5.5 188c3154ae CP7.5: craft-libs#497 re-review — prefixes leaving one root share one suffix walk; all-caps mixed-case stems are no compounds; homonym flags written; compact format WCAFF2
- simple_word groups the prefix rules that leave the same root (circumfix and NEEDAFFIX
  alike) and answers them in one stem lookup, one chain walk and one suffix walk with all
  their flags (FlagProbe): 1,000 or 16,000 prefixes adding nothing no longer cost one walk
  each (h1.aff: 1.6 s → 17 ms per two words; 16k prefixes 22 s → 19 ms).
- Deferred suggestion candidates are not looked up again before their compound check.
- A word in capitals matches a mixed-case stem as a simple word only (`JQUERYKABEL`,
  `MSCFOO` wrong, as in the CLI).
- to_bytes writes the flags only the kept-apart homonyms use (NEEDAFFIX beside a compound
  flag survives a round trip).
- MAGIC WCAFF2: compact data from before homonyms were kept apart is refused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FypgCX8LsYYE8fsj4byBm2
2026-11-02 03:59:36 +00:00
Martin BallandClaude Opus 5.5 b8efc5add9 CP7.4: craft-libs#497 re-review — hu prefixed suffix chains, homonyms kept apart, all-caps mixed-case stems; compound suggestions cheapest-first in one budget
- A prefix with two chained suffixes (hu `meg`+`épít`+`és`+`e`, eo `enjugo`): every rule
  cross, the prefix flag from the stem or a continuation class (CLI answers).
- Homonyms with different flags are kept apart (and written apart in the compact form):
  a prefix and a suffix, or a compound flag and a suffix, must come from one homonym
  (nl `afstopje`, `schuurpoederden` wrong in the CLI). nl now 100% in both comparisons.
- Hungarian syllable rule leaves out a last suffix with no continuation classes;
  COMPOUNDROOT counts on the stem only.
- Near-word check: a word listed as an ONLYINCOMPOUND/NEEDAFFIX stem counts only with a
  suffix, not a prefix (de `Fahrradverkehr` right).
- A word in capitals may be a mixed-case stem (`KB`/`KBS` from `kB/S`, hu `DNY`, nl `BSC`).
- Mixed-case retry keeps KEEPCASE for the first part; KEEPCASE kept apart from the other
  exclusions (NOSUGGEST sharing its flag is no longer cleared); overflow of the homonym
  groups counts as dropped.
- Suggestions: candidates are checked as simple words first; the rest get the compound
  check cheapest first, those changed where the word stops being parts before others, in
  one step budget per call, with piece analyses shared between candidates. The suggestions'
  stem scan makes forms in one shared budget (mn).
- affix_check example: `suggest` command; AffixDict::suggest.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FypgCX8LsYYE8fsj4byBm2
2026-11-02 03:18:31 +00:00
Martin BallandClaude Opus 5.5 3eaa276786 CP7.3: craft-libs#497 review — compounds as the hunspell CLI answers: ONLYINCOMPOUND homonyms keep their flags, triples after SIMPLIFIEDTRIPLE, KEEPCASE and capitals in parts, CHECKCOMPOUNDPATTERN 0, a forbidden prefix, FORCEUCASE; suggestions and many-way pieces stay cheap
- ONLYINCOMPOUND homonyms beside others are kept apart (compound parts only), so their suffixes
  make no standalone words (de verfilmungs, sv norges); round-trips as a second stem line.
- CHECKCOMPOUNDTRIPLE also checks a SIMPLIFIEDTRIPLE join (busssen, booob wrong).
- KEEPCASE holds for the first part only; a capitalized stem is a part (FooQux).
- CHECKCOMPOUNDPATTERN `0`: no suffix (a prefix doesn't count): unfoobar wrong.
- COMPOUNDFORBIDFLAG on a prefix: no part anywhere (bardefoo wrong).
- FORCEUCASE: from the stem only; wrong only without any capital (fooQuxbar right).
- Suggestion candidates: 2,048 compound steps each, compounds for the first 128 only.
- A piece's ways are hashed, capped at 256 and counted (with the patterns) against the budget.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FypgCX8LsYYE8fsj4byBm2
2026-11-02 02:08:35 +00:00
Martin BallandClaude Opus 5.5 9a442e7048 CP7.2: affix_check example — compare the affix engine with another checker's answers (black-box)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FypgCX8LsYYE8fsj4byBm2
2026-11-02 01:43:54 +00:00
Martin BallandClaude Opus 5.5 87c7d1874a CP7.1: craft-proof reads flag-based compounding (COMPOUNDFLAG/BEGIN/MIDDLE/END, PERMITFLAG, FORBIDFLAG, ROOT, WORDMAX, SYLLABLE, CHECKCOMPOUND*, SIMPLIFIEDTRIPLE, FORCEUCASE), as the hunspell CLI answers
Black-box: every rule pinned to hunspell 1.7.5's answers on synthetic dictionaries and on
the Language packs' own files. Bounded work per word (16 parts, 65,536 steps). Also:
ONLYINCOMPOUND belongs to its homonym like NEEDAFFIX; COMPOUNDRULE reads the Swedish `)k`
and the Mongolian `[..]`, `%?`, `.` as the CLI does; COMPOUNDMORESUFFIXES is refused only
with flag-based compounding (Korean keeps loading).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FypgCX8LsYYE8fsj4byBm2
2026-11-02 01:43:54 +00:00
Martin BallandClaude Opus 5.5 65ce4f882a FU.7: craft-proof pack languages — another script is told by the letters' Unicode scripts, not the dictionary's letter inventory, so a misspelling in the dictionary's own script (zzzz in Dutch) is always checked
Found by a local CodeRabbit review of #496 (major).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FypgCX8LsYYE8fsj4byBm2
2026-11-02 01:18:42 +00:00
Martin BallandClaude Opus 5.5 8712aa105a FU.6: craft-proof affix — the stems' flags are budgeted while a dictionary is read (running counts of flag text and keys), so a huge .dic fails early instead of after holding every entry's flags (#495 P3)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FypgCX8LsYYE8fsj4byBm2
2026-11-02 01:18:42 +00:00
Martin BallandClaude Opus 5.5 1f345ea809 FU.5: craft-proof id, ru — the cased-letter rule for words in capitals (craft-libs#485): a caseless part is no abbreviation; shared in_capitals / capitals_part
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FypgCX8LsYYE8fsj4byBm2
2026-11-02 01:18:42 +00:00
Martin BallandClaude Opus 5.5 86345ebcf5 FU.4: craft-proof packs — remembered hyphenation patterns keep to a 256 MB budget of their own; a pack's spelling files are never candidates through a symbolic link
Follow-ups from craft-libs#485: up to 256 languages' pack patterns (64 MB files each) were kept
by count only; `hunspell::ordered` checked pack files with `is_file`, which follows links,
while the pack scan refuses them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FypgCX8LsYYE8fsj4byBm2
2026-11-02 01:18:42 +00:00
Martin BallandClaude Opus 5.5 87defe169f FU.3: craft-proof pack languages — missing-dictionary list forgets its oldest, not all; a panic inside a check unpins its dictionaries; a word in another script than the dictionary's isn't marked
Follow-ups from craft-libs#485: `CHECK_FROM` is reset by a drop guard; `remember_missing` drops
the oldest of 256; a word none of whose letters the dictionary's words use (a Cyrillic word in
a Dutch run) is left alone, as Word does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FypgCX8LsYYE8fsj4byBm2
2026-11-02 01:18:42 +00:00
Martin BallandClaude Opus 5.5 820695e951 FU.2: craft-proof — Australian English reads SCOWL's own en-AU dictionary (ProofLang::EnAu), with en-GB's spelling policy; en-NZ stays on en-GB
SCOWL en-AU has 238 words en-GB lacks (billabong, brumby, larrikinism, Australian place
names) and spells program, inquiry, livable, licorice, verandah the Australian way (no
programme, enquiry, liveable, dialog or veranda, as Hunspell en_AU says); -ise/-ize policy and
affix file are the same as en-GB's. craft-i18n already ships language-data/en-au.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FypgCX8LsYYE8fsj4byBm2
2026-11-02 01:18:42 +00:00
Martin BallandClaude Opus 5.5 e36fb5d069 FU.1: craft-proof — British English never swaps -yze for -yse (reanalyze, breathalyzer, paralyzingly stay American, as Hunspell en_GB says)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FypgCX8LsYYE8fsj4byBm2
2026-11-02 01:18:42 +00:00
Martin BallandClaude Opus 5.5 89fa1559e9 CP6.13: craft-libs#495 review — Windows drives are probed for LibreOffice once per process; sr@latin is sr-latn
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FypgCX8LsYYE8fsj4byBm2
2026-11-02 01:06:58 +00:00
Martin BallandClaude Opus 5.5 c5cc9c4b7a CP6.13: craft-libs#495 review — cs: ne ne, ano ano, už už and to to are no repeated words; ru: no comma asked for inside тем более что, при условии что, в то время как; -ой adjectives take и after г, к, х, ж, ш, ч, щ
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FypgCX8LsYYE8fsj4byBm2
2026-11-02 01:06:58 +00:00
Martin BallandClaude Opus 5.5 05acd513cc CP6.13: craft-libs#495 review — tr suggest: the diacritic and typo steps find names the dictionary capitalizes (istanbu → İstanbul)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FypgCX8LsYYE8fsj4byBm2
2026-11-02 01:06:58 +00:00
Martin BallandClaude Opus 5.5 724d541856 CP6.13: craft-libs#495 review — tr grammar: Turkish case folding (İst., Işık ışık), initials (A.Ş., T.C.) end no sentence, and a repeated word with an apostrophe (Ankara'da Ankara'da) is marked again
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FypgCX8LsYYE8fsj4byBm2
2026-11-02 01:06:58 +00:00
.CooCoo 14d5dbb50c Add craft-vectorcraft-format: the .vectorcraft envelope, with the document as a type parameter (#408)
* Add craft-vectorcraft-format: the .vectorcraft envelope, with the document as a type parameter

File<D>, Head and Image hold the members VectorCraft writes, in its
order, with its skip rules: serde_json output of a File<D> is the JSON
VectorCraft writes, compact or pretty. Tail places entries after the
JSON from version 4 (INLINE_MAX bytes or more, after BLOB_MAGIC), and
split_tail and Image::bytes read them back. The crate also has the
version and name constants, check() for the format name and newer
versions, sniff() for plain and gzip files (the top-level "format"
member must be vectorcraft or drawcraft), and standard base64.

No dependency on any app; builds for wasm32-unknown-unknown.

* ci: do not keep the checkout token in the vectorcraft-format workflow

The checkout step sets persist-credentials: false, as jpeg.yml and media.yml do. No later step in the job uses the token.

* vectorcraft-format: limit the bytes copied from the data after the JSON

Entries::bytes reads a file's entries and returns Error::TooBig once the
blob ranges it has copied add up to more than the data after the JSON
holds, so entries that name the same bytes again and again are refused,
as VectorCraft's reader refuses them.

A generator, preview or pdf of the wrong shape now reads as None in File
and Head, as VectorCraft opens such a file. File::tail gives a Tail for
the file's own version.

* ci: pin actions/checkout by commit and Rust to 1.95 in the vectorcraft-format workflow

* vectorcraft-format: document that base64 decode ignores the bits before the padding

decode reads a group such as Zh== like Zg==, and Zm9= like Zm8=, as
VectorCraft's own base64_decode does. The doc comment says so, and a
test pins both cases to the canonical bytes.

* vectorcraft-format: entries naming the same range share one buffer

VectorCraft's reader keeps the bytes of each [offset, length] it has
read and gives a repeated entry the same buffer without counting it
again (crates/format/src/unpack.rs). Entries::bytes refused such a
file once the repeats passed the size of the data after the JSON. It
now returns Arc<[u8]>, caches each range, and counts it once.
Overlapping ranges that differ are still copied and counted each time.

* vectorcraft-format: a refused entry does not count against the copy limit

Entries::bytes added a refused entry's length to the bytes copied, so a later entry that fit was refused too. It now counts an entry only once it is read.

* vectorcraft-format: Contents reads the document without keeping the preview and PDF

File<D> checks the shape of generator, preview and pdf by holding each
value in memory, and keeps the preview and PDF base64 it reads. Contents<D>
reads format, version, profiles, document and images, and skips the other
three as the JSON is read. It accepts and refuses the same files as File<D>.

* vectorcraft-format: Contents reads a repeated generator, preview or pdf

VectorCraft opens a file whose generator, preview or pdf member is repeated, since its reader does not declare those members, and refuses a repeated format, version, profiles, document or images. Contents now leaves the three members undeclared, so serde skips every copy of them, and still refuses the other repeated members. File and Head are unchanged.

* vectorcraft-format: sniff decodes JSON escapes in the format member

serde_json reads a file whose format key or value is written with escapes, such as \u0066ormat, as a VectorCraft file, and sniff compared the raw bytes and refused it. sniff now decodes the key and the value before comparing them.

* vectorcraft-format: File::read and Contents::read check the version

File::read and Contents::read are the readers of a file's JSON. Both
refuse another format or a version newer than VERSION after the JSON
reads, with ReadError::Envelope holding the Error (Format or TooNew), and
return ReadError::Json otherwise. File and Contents no longer implement
Deserialize, so a reader cannot skip the check.

Both read one implementation and treat repeated members the same way:
a repeated generator, preview or pdf is accepted, and File keeps the last
value, as serde_json reads a repeated key into a Value. A repeated
format, version, document, images or profiles is refused. A top-level
array holds format, version, document, images and profiles in that
order, as VectorCraft reads it; Contents' fields follow that order.

- Image::bytes returns None for an entry of more than MAX_ENTRY (1 GiB,
  VectorCraft's limit on one entry of a compressed file), and
  Image::bytes_at_most takes another limit. Entries keeps no per-entry
  limit, as VectorCraft opens larger entries in uncompressed files.
- Entries::blob reads a range; Entries returns Arc<Vec<u8>>.
- marker_at finds BLOB_MAGIC where the writer puts it in a file's first
  bytes; split_tail uses it.
- File, Contents, Head, Image, Part and ReadError are #[non_exhaustive].
2026-11-01 19:57:07 -05:00
.CooCoo ca1b5e2edb Add craft-ecproj-format: the envelope of EffectCraft project files (#409)
* Add craft-ecproj-format: the envelope of EffectCraft project files

An .ecproj file is one JSON object: savedBy, then the saved view's
members, then the project's members, written with two-space indentation.
The crate defines that envelope with the view and the project as type
parameters, so the project model stays in EffectCraft:

- SCHEMA_VERSION (2) and check_schema, which refuses a newer schema
  with EffectCraft's own message.
- File { saved_by, view, project }, serialized with both parts
  flattened, and File::to_json.
- Head, which reads savedBy and schema from a JSON object, and
  saved_by, which reads savedBy alone.

The tests write synthetic projects and compare the text with JSON
written by hand in the shape EffectCraft's writer gives. The workflow
runs fmt, clippy, the tests and a wasm32-unknown-unknown check.

* ci: do not keep the checkout token in the ecproj-format workflow

The checkout step sets persist-credentials: false, as jpeg.yml and media.yml do. No later step in the job uses the token.

* ecproj-format: write compact files, read savedBy as EffectCraft opens it

EffectCraft's Save Compact Project Files setting (effectcraft 07639b5f)
writes .ecproj files on one line with serde_json::to_string.
File::to_json_compact writes the same text, and the docs say the crate
writes and reads both forms.

Head now reads every file EffectCraft's Project::from_json opens: a
savedBy that is not a string, or appears more than once, reads as None,
and a project written as a JSON array reads with its first element as
the schema. saved_by also gives None for a repeated savedBy, as
EffectCraft's saved_by does. File's docs say the crate does not check
values and that serde_json writes NaN and infinity as null.

* ecproj-format: skip a non-string savedBy without keeping it

Head::read and saved_by read savedBy into a serde_json::Value before
dropping any value that is not a string, so a wide or deeply nested
savedBy was held in memory, and one nested past serde_json's recursion
limit made Head::read fail. A non-string savedBy, and every repeated
savedBy, is now skipped element by element with IgnoredAny, as other
members are. It still reads as None.

* ecproj-format: Head::read refuses a newer schema and reads objects only

Head::read is the one reader of a project file's envelope. It returns
ReadError: Json when the text is not one JSON object with a u32 schema
(the array form included), or TooNew when the schema is newer than
SCHEMA_VERSION, holding the file's savedBy so a message can name the
version that wrote it. TooNew reads as before. saved_by() is removed,
and Head no longer implements Deserialize.

TooNew, ReadError and ReadError::TooNew are #[non_exhaustive].
2026-11-01 19:56:26 -05:00
Martin BallandClaude Opus 5.5 762a06a4c6 CP4.1: craft-libs#486 review — listing Language packs never writes the record without the store lock (an install or removal repairs it); the font-store workflow pins actions/checkout to v7.0.1's commit
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FypgCX8LsYYE8fsj4byBm2
2026-11-02 00:30:58 +00:00
Martin BallandClaude Opus 5.5 d2d5c737a6 CP6.13: craft-libs#482/#485/#488 review — NOTICE carries the embedded Portuguese patterns' BSD notice; README: the usage example defines text, the eight-candidate limit, what the default build embeds; en_words writes under crates/proof
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FypgCX8LsYYE8fsj4byBm2
2026-11-02 00:27:37 +00:00
Martin BallandClaude Opus 5.5 5bd21c0509 CP6.13: craft-libs#482/#485/#487 review — en.UTF-8 and pl@euro resolve to their language; the German, Bulgarian and no-data English tests hold on a machine with Hunspell dictionaries installed
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FypgCX8LsYYE8fsj4byBm2
2026-11-02 00:27:37 +00:00
Martin BallandClaude Opus 5.5 64bd43db1d CP6.13: craft-libs#487 review — a trailing combining mark stays on its word; Italian and Portuguese look a hyphenated word up whole before its parts
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FypgCX8LsYYE8fsj4byBm2
2026-11-02 00:27:37 +00:00
Martin BallandClaude Opus 5.5 be52680c9c CP6.13: craft-libs#495 review — an oversized pack licence is skipped alone; a score (3 : 1) keeps its spaces in hr, it, pl, pt and ru; ' ,,' (a typed quote) is left to the quote rule in pl and ru; pt narration after a dialogue dash is no new sentence
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FypgCX8LsYYE8fsj4byBm2
2026-11-02 00:27:37 +00:00
Martin BallandClaude Opus 5.5 fdb52ec3c6 CP6.13: craft-libs#495 review — MAP variants are made by number of changes, so the 4,096 cap drops the most-changed ones, never a single slip
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FypgCX8LsYYE8fsj4byBm2
2026-11-02 00:27:37 +00:00
Martin BallandClaude Opus 5.5 cb5d7310ec CP6.13: craft-libs#495 review — tr: Ignore words in UPPERCASE reaches Turkish's own check; a comma after de/da, ne or ya is fine; repeated numbers are no repeated words; table suggestions keep the word's case
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FypgCX8LsYYE8fsj4byBm2
2026-11-02 00:27:37 +00:00
Martin BallandClaude Opus 5.5 c5daa8d16c CP6.13: craft-libs#495 review — it grammar: pp., tab. and dr. are abbreviations, so the next word starts no sentence
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FypgCX8LsYYE8fsj4byBm2
2026-11-01 22:57:37 +00:00