* Normalize tag needles for search and suggestions
Tags are stored NFC-composed, but the read paths compared raw input against
them, so a needle typed decomposed found nothing. Search queries and both
suggestion services now normalize the needle first.
Suggestion prefixes were also split on the ASCII space alone, so a
part-typed tag after a comma or an ideographic separator offered nothing
even though those separators split tags on save.
Adds normalizeTagNeedle and tagPrefixOf, and folds the two ad-hoc
hash-prefix checks into Char.isTagPrefix so the fullwidth form is
recognized in queries as well.
* Agree tag search with tag storage on where a tag starts and ends
Review of the previous commit found that widening tag recognition on the
needle side alone left three regressions.
parseQuery normalized needles, which let two typed spellings collapse onto
one string; the tag list was still an undeduped List feeding a keyed LazyRow
in Global Search, so `##epic #epic` crashed composition. It also treated a
hash anywhere as a tag opener, swallowing the fullwidth one out of ordinary
free text, and still ended a needle at whitespace while storage ends tags at
commas too. A hash now opens a tag only at a word boundary, needles split on
the storage separators, and the tags come back deduped.
The suggestion strip fired on the last run of the draft but its callers threw
the whole draft away on select, losing tags already typed. replaceTagPrefix
swaps just the run being completed.
Also normalizes tag keys as the index is built, so a tag that reached disk
unnormalized is still reachable, and folds the triplicated suggestion
derivation into rememberTagSuggestions.
* Match global search against projected Markdown prose
Search compared queries against raw Markdown, so backslash escapes and
emphasis markers sitting between words caused misses, and snippets
rendered storage syntax.
Flatten stored Markdown to the prose a reader sees before matching:
escapes resolve to their literal character, and paired emphasis or code
delimiters are dropped. Pairing follows CommonMark flanking rules, so
literal markers in imported or hand-edited content survive; user_name
and a bare *** divider are left alone.
Timeline dates are a plain-text field and are not projected. Derived
titles go through the same projection as the note list so the two views
agree, and blank projections fall back to the raw source so marker-only
content still gets a title and still appears in tag searches.
Fixes#811
* Scope Markdown pairing to paragraphs and narrow the title change
Delimiter pairing used a single document-wide stack, so an unpaired
asterisk or a backtick used as an apostrophe paired with an unrelated
one paragraphs away. Both characters were deleted, and runs between a
bogus code span were marked inert, which left real emphasis in place and
defeated the cross-markup matching this is for.
Pair within a paragraph only. Emphasis still spans a soft line break.
Strip leading blockquote, heading and bullet markers from every line
rather than only from derived titles, so a title and its snippet agree.
Ordered-list markers are left alone: one line cannot tell "1. Draft" from
"1984. The year everything changed".
Titles now take the first non-blank line of the whole projection instead
of projecting a line in isolation, so emphasis closing on the next line
still pairs.
Revert firstNonBlankLine to its verbatim behavior. Routing it through the
projection reached into Browse Notes, Story Ideas and sync conflict
labels and unescaped backslashes there, corrupting stored paths.
Skip the raw fallback scan when the query holds no character the
projection can remove, which makes it provably redundant.
* Project Markdown in a reusable scan workspace
Global search re-projected every document on every keystroke, allocating
about 36x the source in garbage each pass: one object per delimiter run,
two lists, a StringBuilder and an output String, per scene, per search.
MarkdownProjector holds that work in buffers it keeps. Delimiter runs
move into parallel primitive arrays, the projected prose lands in a char
buffer that is matched in place rather than turned into a String, and
every buffer grows to the widest document seen and is then reused. A full
scan of a 1.25M word project drops from 31.7MB of garbage to none, at the
same wall time.
Two smaller wins came with it: the source is copied into a flat array so
the scan and render index an array instead of paying a CharSequence call
per character on both passes, and the ASCII punctuation test became four
range checks instead of a scan over a 32 character string.
The projector cannot be a field on the use case. Cancelling a search is
cooperative, so the outgoing scan can still be running when the next one
starts, and the two would share a buffer. MarkdownProjectorPool lends one
per scan and keeps it afterwards, so the buffers survive to the next
keystroke without forcing the four scans to run one at a time.
projectMarkdownToPlainText stays as the one-shot convenience over the
same code, so the behaviour is defined in one place. MarkdownProjectionTest
passes unchanged, which is the point: the rewrite is internal.
* Read scenes straight into the scan buffer
Search re-read every scene from disk on each keystroke and took a String
back for each one, about 11.5MB of garbage per pass over a 300k word
project before any matching happened.
SceneDatasource can now decode a scene into buffers the caller owns and
return the char count, so a scan reuses one pair of buffers instead of
taking a string per file. ScanBuffers is the contract; MarkdownProjector
implements it, so the bytes land in the same workspace that projects
them. The same scan now costs 0.42MB at the same wall time.
The bytes are pulled in bulk and decoded from an array. Reading a byte at
a time off a BufferedSource was measurably slower than Okio's own
readUtf8, and the decode was never the expensive part.
Matching follows the text into the buffer: findProjectedMatch works over
whatever the projector holds, and the raw-markup fallback searches the
source buffer, so neither path needs the document as a string. Only the
snippet window is copied out, and only on a hit.
SearchProjectUseCaseTest stubbed loadSceneMarkdownRaw, which the scene
path no longer calls. The stub now fills the buffer it is handed, which
is what the collaborator actually does.
* Carry the projection across every search surface
Rebasing onto develop put #831's unification and #821's projection in the
same tree, and they disagreed.
#831 made four surfaces share one rule, markdownContains, which resolves
escapes only. Global search now projects, so leaving markdownContains
alone would have re-split the surfaces it had just joined: Notes, Timeline
and Story Ideas would still miss a phrase spanning "**emphasis**". It
projects now too, and mirrors the same fallback.
The two PRs also pulled opposite ways on the raw-source fallback. #831
pinned that searching the storage form of prose must not work, and in the
same breath that literal "**Chapter**" must still be found. Under the
projection those need different answers, and #821's gate, "the query holds
any character the projection could remove", cannot give them: a backslash
and an emphasis marker are both removable, so honouring one broke the
other.
containsInlineMarkup replaces it. Only emphasis and code markers open the
fallback, because spelling those out is someone hunting for markup.
Escapes and block markers do not, because "well\-known" is the storage
form of prose and nobody types it. Both of #831's assertions hold.
matchOrPreview keeps the fallback argument and the empty-snippet chain
from #833, which the projection commits had dropped, so a tag-only search
still returns bodiless items.
* Match the query as literal text and nothing else
Global search resolved the query against the prose on screen, then, for a
query containing an emphasis or code marker, searched the raw storage form
as well. That second pass was the query being read as markup: typing
"**Chapter**" found a document whose prose reads "Chapter", because the
asterisks were matched against the source rather than against what the
document displays.
The rule is now one line. A query is literal text, matched against the
prose the document renders as, and there is no second interpretation. The
asterisks are not on screen, so typing them finds nothing.
Nothing about escapes changes, and this is easiest to see in the case that
motivated it. Text stored as "well\-known" renders as "well-known" and is
found by typing that; the storage form is not. Text stored as "well\-known"
renders as "well\-known", backslash and all, and typing that backslash
finds it. Markers the projection leaves alone, "5*4" and "user_name", are
matched where they sit, because there they are prose.
Three assertions pinned the behaviour that has gone, one per surface, and
each now states the opposite. containsInlineMarkup and the projector's
source-side accessors existed only to serve the fallback and go with it.
* Drop a comma from a test name so iOS can compile it
Kotlin/Native rejects a comma in a backticked identifier, so the whole
iosTest compilation failed on one test name. Desktop accepts it, and
compileIosMainKotlinMetadata only covers main, so nothing local caught it.
:common:compileTestKotlinIosSimulatorArm64 does, and runs on a non-Mac
host.
* Add per-project language setting (#754)
An optional BCP-47 language on ProjectData, picked from a searchable
list of all platform locales in project settings. New projects default
to the device locale; the Alice example project is en-US.
Spell check is gated per project: when the project language does not
leniently match the dictionary locale, the dictionary is withheld
(ProjectSpellCheckRepository) and project settings explain why.
The public story page emits <html lang> and JSON-LD inLanguage from the
declared language, and EPUB export prefers it over the device locale.
The hasher contributes zero bytes when unset so existing sync hashes
stay stable.
* Fix review findings in the project-language feature
createProject now only seeds the default language for genuinely new
projects (seedDefaultLanguage), so account sync materializes server
projects with the never-synced baseline intact, and the seed is
language-only so it cannot gate spell check against a same-language
dictionary. The hasher's language block gets a -1 marker plus length
prefix so it can never collide with a tags block, and the initial
write goes through the shared saveStoredProjectData path.
The Locale type now retains the script subtag, keeping zh-Hans/zh-Hant
style locales distinct in the picker. The picker's clear row is pinned
above the list so it survives an empty search, watchSpellCheckAllowed
delivers on the main dispatcher, and the public story page hashes the
stored project-data hash into its validator instead of parsing the
blob per request, applying the language override after withDefaults so
chrome links keep the viewer's locale.
* Enforce single-owner persisted formats
The tags write in PromoteIdeaUseCase rewrote project_data.toml from
scratch, erasing the language seed createProject had just written: the
exact hazard of a second inline writer. It now read-modify-writes
through the datasource's scope-less helpers, and ProjectsListComponent's
hand-rolled reader delegates to a new blocking readStoredProjectData.
The rule is written down (ARCHITECTURE.md hard constraint 7, CLAUDE.md)
and enforced by PersistedFormatOwnershipTest, which fails the build when
raw TOML I/O appears outside a Datasource file. Migrators are exempt by
role; the two remaining legacy offenders are allowlisted as a burn-down
that can only shrink.
* Burn down the last raw TOML I/O outside datasources
ProjectStatisticsCacheReader now delegates to a scope-less
readProjectStatistics helper in StatisticsDatasource, and the example
project's fabricated activity log goes through writeDeviceLog in
WritingActivityDatasource, which also becomes the single owner of the
.activity path convention.
With no offenders left, PersistedFormatOwnershipTest drops its
burn-down allowlist entirely: only Datasource files and migrators may
touch persisted TOML formats from here on.
* Pass seedDefaultLanguage in the Android instrumented-test harness
* Pass seedDefaultLanguage in the round-trip sync HeadlessClient
* Match every search surface through one rule
Global search resolved escapes and then retried the raw source for a
query containing a backslash, while the Notes and Timeline filters only
did the first half and Story Ideas matched raw storage. The same query
gave different answers depending on which screen it was typed into.
Drop the retry rather than spread it. Searching the storage form of your
own text is not a thing readers do, and supporting it cost real
precision: a query of \* fell back to * and matched every emphasis
marker in the project, and a query ending in a backslash matched
nothing at all. It also forced the snippet and the title to be built
from different strings, so one result row could spell the same sentence
two ways.
What is left is one rule: resolve the stored escapes, match the query
literally. markdownContains carries it, next to matchesAllTags, and all
four surfaces call it. Names, tags, dates and idea titles are not
Markdown, so matching them as stored is now consistent rather than a
bypass.
Each screen's own composition of the rule is extracted so it can be
tested; composeUi/src/desktopTest covers all three.
* Say what the search rule actually does
The KDoc claimed markdownContains compares the prose a reader sees. It
only resolves backslash escapes, so a note stored as "the **big** dog"
is still unfindable by "big dog" on every surface, which is the #811
emphasis gap. State the limit instead of denying it.
SearchFilterTest claimed to pin each screen's composition of the rule
while composing no screen, so reverting a filter to an inline raw
contains would have kept it green. TimeLineOverviewUiTest now drives a
screen's search field end to end, and the predicate tests say that is
what they are.
Global search resolves and matches separately because it needs offsets
rather than a boolean, so add a test asserting it answers the same as
markdownContains across a table of content and query shapes.
* Drop the comma from a test name Kotlin/Native rejects
Native forbids commas in backtick-quoted names, so common/commonTest
failed to compile for iOS while desktop was fine.
A tag match stands on its own, so an item with an empty body no longer
loses its result to a null preview snippet. Covers notes, encyclopedia
entries, timeline events and scenes.
The tags-present branch of the encyclopedia match now tries the entry
name before the body, the way the scene path already does, so
"#fantasy alice" returns the entry named Alice.
Fixes#828
* Allow accented and non-latin characters in tags
The tag pattern used \w, which is ASCII-only on both JVM and Native, so
cleanTags() silently dropped anything like "thème" on the way to disk.
Validate with Unicode-aware character predicates instead, including
combining marks so decomposed input from iOS/macOS survives too.
Fixes#778
* Address review of the Unicode tag fix
Splitting tag input, stripping the `#` and validating the result were all
still assuming ASCII in one way or another:
- Split on any Unicode whitespace and on the fullwidth/ideographic commas,
so an IME's fullwidth space separates tags instead of fusing them into one
token that then fails validation.
- Normalize to NFC, so the precomposed and decomposed spellings of a tag are
one index key rather than two identical-looking ones.
- Validate by code point, so an astral letter (rare kanji, Adlam) is not read
as two non-letter surrogates and rejected.
- Accept ZWJ/ZWNJ, which Persian and Indic scripts need word-internally, and
require at least one letter or digit so a mark-only tag has a base glyph.
- Strip a fullwidth `#` prefix as well as the ASCII one.
The tag field now commits chips on the same separators and parses them with
parseTagInput, so what it shows is what survives the save.
* Resolve Markdown escapes when searching project text
Global search compared queries against raw stored Markdown, so a phrase
that crosses a backslash escape never matched: "well-known" missed a
document holding "well\-known", and the snippet showed the backslash.
Resolve escapes before matching, and build the snippet from the resolved
text. This inverts exactly what the editor does on save, so it is safe
for imported and hand-edited content too, where a backslash escape means
the same thing.
Nothing else is rewritten. Emphasis, code, link and block markers are
left as stored, because telling syntax apart from a literal character
needs a parser, and guessing wrong silently alters the author's words.
Timeline dates are a plain-text field, so only the event body is
resolved. A query holding a backslash also tries the raw source, so
searching for a literal escape keeps working.
* Resolve escapes in search titles and in-screen search
The title above a result still came from raw storage while its snippet
was unescaped, so one card rendered the same sentence two ways. Resolve
escapes for the title as well.
The Notes and Timeline screens filter the same text with a raw substring
test, so a query that found a note in global search returned the empty
state on the screen that owns it. Resolve escapes there too.
Move unescapeMarkdown out of StoryExportCommon into data/search so the
exporters and search share one implementation rather than two copies,
and take its tests with it. Guard the timeline raw fallback the way
findMarkdownMatch already is, so a query without a backslash stops
rescanning an identical string.
The acceptance criteria call for leading-hyphen labels, empty labels and
underscores to be rejected, but only the trailing-hyphen case had a test.
Hyphens in subdomain and middle labels were untested too.
All five already behave correctly; this just pins the behavior down.
Hostnames are case-insensitive, but the cleaned value is what gets
stored, so it should be normalized. Lowercasing before the removePrefix
calls also fixes uppercase schemes: removePrefix is case-sensitive, so
HTTPS://example.com kept its scheme and then failed validation.
Refs #797
Markdown chapter detection that finds nothing collapses a whole manuscript into
one scene. The first sign of that today is the editor locking up, after the
import has already been committed.
The preview now carries a word count per scene and flags any at or over 10,000
words: an amber notice above the list, and the offending rows tinted and stamped
with their real count. The import stays enabled; one huge scene is a legitimate
thing to want.
10,000 clears almost every real chapter (3,000 to 5,000 typical, ~20,000 for the
longest) and sits far below any whole book, so it fires on the collapse case
without nagging.
Also cuts three main-thread wastes profiling turned up on very large scenes.
SceneEditorUi and FocusModeUi passed getInitialEditorContent(...) to
rememberSpellCheckState unremembered; the value is only read on first
composition, but the argument was still evaluated every recomposition and
rebuilt the whole document's AnnotatedString on the UI thread, while the scene
buffer republishes every 500ms during typing. MarkdownEditField built a fresh
spell checker each recomposition, re-keying the library's full-rescan effect.
countWords no longer materialises a list of every word to count them.
* Fix download link
* Detect Setext and bold chapter titles on Markdown import
Markdown import only recognized ATX headings, so a manuscript whose
chapter titles were Setext-underlined or merely bolded collapsed into a
single scene.
- Setext headings (`===` level 1, `---` level 2). A `---` only underlines
a line that opens a paragraph, so scene-break rules and front-matter
fences stay body text.
- In Auto, bold-only lines become chapters when heading markup produced
at most a story title and several bold lines agree.
- A Pattern strategy for Markdown mirroring the RTF one, with the chapter
regex field wired through ImportOptions and the import dialog.
ChapterHeadingLevel becomes MarkdownSplitStrategy now that it carries a
non-level member.
* Keep the word-count guard local to the Markdown importer
Sharing it via ImportStructure meant touching RtfStoryImporter, which #806 is already rewriting.
RTF writers routinely include the surrounding spaces inside a formatting
run, so the importer emitted "** Chapter One **", which CommonMark does
not parse as emphasis. Emphasis delimiters now hug the text: leading and
trailing whitespace of a run moves outside the markers and a run made
only of whitespace opens nothing.
Italics emit "*" instead of "_" so intra-word emphasis renders, and the
escape set now mirrors ComposeTextEditor's MARKDOWN_SPECIAL_CHARS plus
its ordered-list marker escaping, so a paragraph starting with "- " or
"1. " no longer imports as a list.
Fixes#803
Clients are HTTPS-only, but the Docker image serves plain HTTP, so a client
pointed at it fails its TLS handshake and reported only "Network error
connecting to /api/account/create". The handshake is rejected by Jetty's HTTP
parser before any route runs, so nothing about it reaches the server log
either, leaving unrelated UnsupportedProtocolVersionException entries as the
only visible clue.
Split the IOException arm of Api.makeRequest so a TLS failure names HTTPS and
the certificate or reverse-proxy requirement. Detection is expect/actual:
SSLException on JVM, message markers on iOS where NSURLSession carries nothing
else through.
Also fix the server URL field on the way in: cleanUpUrl stripped the scheme
with removeSuffix instead of removePrefix, so a pasted http:// URL failed
validation, and validateUrl required a dotted TLD, rejected capitals, and
admitted ports above 65535 that would crash the unguarded toInt() in url().
Fixes#790
Importing a 100k+ word Markdown story froze the editor and consumed all its
RAM. Parsing was never slow (~60ms for 120k words); creating the scenes was,
and quadratically in their number. Resolving a path by id regex-parsed every
filename in the project, sibling counts re-listed the parent directory several
times per scene, and the order re-padding loop re-scanned once per renamed
sibling.
The cached directory scan now carries a scene-id index and a child-count
index, in ScenePathIndex. A created file is absorbed in place; anything else
drops the cache. The re-padding loop reads what it needs before it rather than
inside it, so a run of renames costs one re-scan instead of one each.
Measured on an in-memory filesystem, importing 400 scenes drops from 2.1s to
~0.3s and 1600 from 37s to ~3s. A real disk gains more, since the discarded
scans are syscalls.
Also builds the import preview off the UI thread, debounced and single
flighted, shows the dialog while it reads instead of leaving the projects list
looking idle, makes the preview pane lazy, and ignores the local scratch
directory.
* Fix the crowdin.yml glob that was skipping strings.xml
Crowdin's * requires at least one character, so strings*.xml matched the
16 strings_*.xml files but never strings.xml itself. That file had been
silently absent from every sync. The composeResources values directory
only holds value resources, so *.xml is the safe form.
The android pattern is left alone: that directory also contains theme.xml
and ic_launcher_background.xml, which must not be uploaded.
* Sync translations from Crowdin
First download since the string cleanup and the export-processor fix.
- Removes the 98 retired keys from all six locales, so every values-* now
carries the same 984 keys as values/.
- Drops Crowdin's spurious \' and \" from the Compose resources. Compose
never unescaped those, so they were rendering on screen.
- Picks up 38 newly added server strings, which arrive as English until
they are translated.
Remove 98 string resources that have no reference anywhere in the source.
Fix five strings that were wrong or misleading:
- notes_delete_toast_success printed the note's numeric id ("Note 4 Deleted").
Notes have no title, so the toast no longer names the note at all.
- encyclopedia_create_entry_toast_tag_too_short is shown for an empty entry
name, not a tag. There is no TAG_TOO_SHORT error. Renamed and reworded.
- backup_manager_delete_content_description is a visible button label, not a
content description. Renamed.
- splash_subtitle duplicated about_description. All six locales already
translated them identically, so the splash screen now uses about_description.
- "Time Line" is now "Timeline", matching the glossary and every other screen.
Remove the HTTP/HTTPS protocol picker and all ssl plumbing from the
client. Persisted server settings now always resolve to HTTPS, so a
legacy ssl=false server.json is upgraded on load. ServerSettings.ssl is
kept only as an internal seam for the plain-HTTP integration-test server.
Android: drop the permissive network_security_config so cleartext
traffic uses the secure platform default (blocked).
Server keeps its plain HTTP connector for reverse-proxy deployments. In
--dev with no sslCert configured, it now generates and persists a
self-signed keystore (hammer_data/dev-selfsigned.jks) and serves TLS on
a non-privileged port (8443 by default). The desktop --dev client trusts
that cert for loopback hosts only; remote hosts still get full cert and
hostname validation.
* Add optional Terms of Service gate for account creation
Self-hosters can set an undocumented `termsOfService` path in ServerConfig
pointing at a plaintext file. When set, account creation is gated: the server
answers POST /api/account/create with 451 + the TOS text and a content-hash
version. The client shows a scrollable dialog; accepting resubmits with the
accepted version, declining discards the provisional server settings.
Disabled by default (null path); existing servers are unaffected.
* Cover TOS repository and 451 client handling; harden error-body parsing
Adds direct tests for TermsOfServiceRepository (FakeFileSystem: absent/missing/
blank/populated file, stable and content-derived version) and ServerAccountApi
(201 success, 451 -> TermsOfServiceRequiredException, malformed 451 -> default
failure).
Also broadens error-body parse handling: ktor raises ContentConvertException
(not kotlinx SerializationException) on malformed JSON, so both the create-account
451 path and the shared defaultFailureHandler now catch it and fall back to a
graceful failure instead of letting it escape as an unhandled coroutine exception.
* Fail fast when termsOfService points at a missing or blank file
A configured but unreadable/empty TOS path previously made challenge() return
null, silently disabling the terms gate and letting accounts be created with no
terms at all. resolveServerConfig now validates the path at startup and aborts
(as it already does for unparseable config), so a misconfiguration can't quietly
drop the legal gate.
* Resolve a relative termsOfService path against the config file's directory
A bare `termsOfService = "tos.txt"` previously resolved against the server's
working directory, so a terms file sitting next to config.toml wasn't found.
Relative paths now resolve against the config file's own directory; absolute
paths are unchanged.
The Encyclopedia search box previously stripped `#` tokens on every
keystroke, making a literal `#` impossible to type and tag search
unusable. Standardize on the same combined-query idiom the project
list and Global Search use: the field holds raw text, and `parseQuery`
pulls `#tag` needles out at filter time.
- Search field binds to the raw query; `#tag` stays as literal text and
filters by tag (substring, case-insensitive, AND-combined) with the
parsed tags shown as reflected chips.
- Tag membership resolves through the shared TagIndexService; the UI
observes the TagIndex so results recompute when the index rebuilds.
- Tapping a tag on an entry card appends `#tag` to the query.
- Name matching stays whitespace-insensitive ("darkforest" finds
"Dark Forest").
Under compileSafety=true the Koin compiler plugin emits a synthetic
dsl_single hint for every definition in a plugin-processed module{},
encoding the named() qualifier as a value-parameter NAME. Kotlin/Native's
IdSignature ignores parameter names, so the three same-type qualified
CoroutineContext dispatchers (main/default/io) collapse to one signature
and fail klib serialization with a SignatureClashDetector AssertionError.
Move the three dispatcher definitions into a plugin-less module in :base
(dispatcherModule) and include it from mainModule. No hints are generated
for them, so the clash is gone and compileSafety stays on for every target
including iOS. All dispatcher consumers use runtime inject(named()), never
auto-wiring, so nothing the plugin validates depends on these definitions;
the DISPATCHER_* constants stay in :common so no consumer changes.
Adopt the Koin Kotlin compiler plugin (io.insert-koin.compiler.plugin 1.0.2)
and convert :common's DI from classic DSL to the plugin DSL
(org.koin.plugin.module.dsl), enabling compile-time DI graph validation
(compileSafety = true) while keeping the centralized module{} layout.
- Convert singleOf/factoryOf/scopedOf(::X) -> single<X>()/factory<X>()/scoped<X>()
across mainModule (root + ProjectDefScope block), migratorModule, and the
android/desktop/ios platformModule + exampleProjectModule actuals.
- Provider functions and when/config bodies use create(::fn) to keep auto-wiring
and validation; explicit-lambda and named() bindings stay classic.
- Add koin-annotations (BOM-managed) for @Provided; mark SandboxFileAccess
@Provided (its definition lives in the desktop app module, external to :common).
- Relocate the iOS startKoin into a plugin-less bootstrapKoin() helper in :base so
common's iOS compilation is no longer a full-graph (A3) aggregator.
Green with compileSafety=true on desktop, android, common metadata, and
desktopTest (1404 tests). iOS/Native is blocked by an upstream plugin bug: the
generated dsl_single hint functions encode named() qualifiers as parameter names,
which K/N's IdSignature ignores, so same-type qualified definitions (the three
CoroutineContext dispatchers) clash during klib serialization. Documented inline.
Repo-wide audit of the common desktopTest and server test suites:
- Replace vacuous assertions and any()-stub echoes with argument-exact
stubs, captured-argument assertions, and persistence round-trips
- Fix genuine test bugs: cross-test fixture leak, stubs that never
matched, tests exercising the wrong branch or fixture
- Drop brittle pins: private-field reflection, incidental call counts,
locale-dependent formatting
- Delete zero-assertion and duplicate tests superseded by stronger ones
- Extract SYNC_DATE_PATTERN constant shared by prod and test
- Document testing philosophy in DEVELOPMENT.md
Installs a Kotlin/Native setUnhandledExceptionHook alongside the iOS file
logger, mirroring the desktop and Android handlers. Logs the throwable and
writes a synchronous crash-<ts>.txt to the logs dir before handing back to
terminateWithUnhandledException so the OS still produces a crash report.
Only Kotlin exceptions (including those crossing the Kotlin/Obj-C interop
boundary) reach the hook; pure Obj-C NSExceptions and native signals are not
caught. Not yet build-verified: Apple targets require a macOS toolchain.
138 new tests: ProjectRoot (+both routers), ProjectSynchronization,
StoryEditor (+Details/List routers), SceneList, FocusMode, drafts,
Notes, Encyclopedia, CreateEntry, OutlineOverview, BackupManager,
ProjectHome (replacing the empty stub), and ProjectSettings.
Extracts two menu-id string literals into component constants so
tests reference them instead of duplicating the literals.
The repository moved from github.com/Wavesonics/hammer-editor to
github.com/Darkrock-Studios/hammer-editor. Update all URLs across
source, build scripts, web templates, docs, store metadata, and
test fixtures.
The suspend FileKit dialog API resolves the ActivityResultRegistry from a
global set only by FileKit.init(). It was never called on any activity, so
the picker in ProjectRootActivity (encyclopedia) either targeted a stale
registry (result never delivered) or threw and was silently swallowed.
Call FileKit.init(this) in both activities' onCreate.
Once the picker returned a file, readExternalFile crashed: stageIntoCache
hands it a plain cache file path, but AndroidExternalFileIo forced every
path through ContentResolver (content:// only). Branch on URI scheme so
file paths are read directly, matching desktop/iOS.
Concise alternative if you prefer a shorter body:
Fix Encyclopedia image selection on Android
- Call FileKit.init(this) in both activities so the suspend picker API has
a live ActivityResultRegistry; without it the encyclopedia picker failed
silently.
- AndroidExternalFileIo.readExternalFile now handles plain file paths (not
just content:// URIs), fixing the crash on the staged cache path.