"Cotejo" means nothing outside Spanish and is hard to pronounce elsewhere, so the
project is now Kotej: same root, but it reads and sounds the same in English and
Spanish. Renamed throughout — engine, targets, bundle ids and the URL scheme,
which is now kotej://compare.
Icon generated with Codex: two facing panels split by a seam, content lines on
each side and one amber line marking a difference — the tool's job at a glance,
still legible at 16pt.
34 tests still green; kotej:// verified end to end against the demo JARs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UfhDYRLTpGJSKGP1m3LVJN
- Tabs: TabsModel owns one ComparisonModel per tab, so each keeps its own scan,
selection and options. A comparison arriving from Finder reuses the current tab
only while it's still empty, otherwise it opens another — an in-progress
comparison is never overwritten. Tab strip with close buttons, plus cmd-T and
cmd-W.
- Localization: UI strings are now English keys with en/es .strings for both the
app and the Finder extension, so it follows the OS language. Care was needed
where SwiftUI wouldn't localise: Text(someString) renders verbatim, so titles
became LocalizedStringKey and file names are explicitly Text(verbatim:) — data
must not be translated. Comparison mode labels are localised in the app because
the engine stays UI-free and English.
Verified: both .lproj ship inside the app and the appex (40 keys each, "Empty
comparison" -> "Comparación vacía"), and the app launches under es and en.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UfhDYRLTpGJSKGP1m3LVJN
- TextDiff: line alignment for the file view. Common prefix/suffix are matched
first, so one change in a 20k-line file costs almost nothing; one-sided runs
are paired into "changed" rows instead of a delete plus an insert, and very
large differing sections fall back to positional pairing rather than hanging.
- App: empty comparison with drop wells, drag & drop or pick per side, mode
picker (automatic per file type, or force binary/text/semantic), side-by-side
tree with archives as folders, "only differences" filter, per-file diff and a
totals bar. Scanning runs off the main actor.
- Finder extension: "Comparar los 2 elementos", or the two-step "Seleccionar
para comparar" then "Comparar con …". It holds no logic — it just opens
cotejo://compare, which the app handles.
- Engine target raised to macOS 14 for @Observable; Totals/status made public.
Verified on a real pair of JARs: reordered JSON keys and reformatted XML come out
"equivalent", genuine edits "different", and added/removed files as only-left or
only-right.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UfhDYRLTpGJSKGP1m3LVJN
First slice of Cotejo, a Beyond Compare-style tool for macOS. The engine has no
UI dependencies so the same code can back a CLI later.
- ZipReader: own central-directory parser (no third-party zip), inflating via the
system Compression framework and handling ZIP64. Because the directory already
carries每 entry's CRC32 and size, two archives can be compared entry by entry
without decompressing anything; bytes are only inflated when an entry is opened.
- TreeScanner: folders and archives scan into one tree type, so JAR/EAR/WAR are
browsed as directories — nested archives included (EAR -> JAR -> classes).
- DirectoryComparer: layered so large trees stay fast. Cheap checks (size, CRC)
can prove files identical but never that they differ, since JSON/XML with other
bytes may still be equivalent; whatever they can't settle is read, or left
pending under a size limit and resolved on demand.
- ContentComparer: byte-for-byte, text with configurable tolerances, and semantic
equality for JSON (key order irrelevant, array order significant, true != 1)
and XML (attribute order and whitespace irrelevant, element order significant).
25 tests, including an EAR containing a JAR.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UfhDYRLTpGJSKGP1m3LVJN