- protected open val projectDef — the ProjectDef("Test", HPath(...)) that was copy-pasted in all four component tests now lives once in the base (open, so a test can override if needed).
- setupComponentKoin(module) — wraps setupKoin and auto-registers the relaxed TagIndexService that every ProjectComponentBase component injects. This removes the per-test boilerplate and permanently fixes the "easy to
forget" inconsistency the review flagged (SceneEditor had been missing it).
EncyclopediaRepository reached sideways into StatisticsRepository and
ReferenceIndexRepository (markDirty / markEntryDeleted on entry writes).
Hoist that orchestration into a new EncyclopediaService, mirroring how
SceneEditorService owns the same side-effects for scenes. The repository
is now pure data with no sibling-repo deps; the three write call-sites
(CreateEntryComponent, ViewEntryComponent, ClientEncyclopediaSynchronizer)
route writes through the service, reads stay on the repository.
SceneRepository drops its sibling-repo deps and emits a tree-only flow; SceneContentRepository exposes a dirtyBufferIds StateFlow; SceneEditorService composes the scene list (combine) and owns init orchestration. Dirty markers now update reactively.
* Rename IdRepository to IdAllocator
It is a special foundational primative
* Rename SyncDataRepository to SyncJournal
It is a special foundational primative
* Rename GlobalSettingsRepository to GlobalSettingsStore
It is a special foundational primitive
* docs: add Foundation primitives tier to the architecture doc
Document IdAllocator, SyncJournal, and GlobalSettingsStore as a fixed set of
stateful, cross-cutting primitives that the whole data layer may depend on —
acyclic leaves (GlobalSettingsStore <- SyncJournal <- IdAllocator) that named
the dependency reality instead of treating it as a no-sibling violation.
GlobalSearchRepository was a stateful class masquerading as a Repository while
fanning out across six repositories — really cross-repo coordination with UI
state bolted on. Split it to match the layering:
- SearchProjectUseCase: stateless cross-repo search (data layer)
- GlobalSearchState: component-layer holder (state + debounce), retained on
ProjectRootComponent via InstanceKeeper so search survives the modal being
dismissed/reopened and config changes; stateKeeper carries query+filter
across process death
- GlobalSearchComponent: thin presenter delegating to the holder
Search state now lives in the component layer instead of a project-scoped
singleton, and MutableValue updates run on the main thread (the use case
offloads its fan-out to the default dispatcher).
Split the monolithic SceneEditorRepository into single-responsibility pieces:
- SceneRepository — scene tree, structure, ordering, paths, on-disk layout
- SceneContentRepository — in-memory buffers, autosave, dirty tracking
- SceneMetadataRepository — per-scene and project metadata
- SceneEditorService — the component-facing facade that orchestrates the three
and applies the cross-cutting side-effects (statistics, writing activity,
reference index) the repositories deliberately don't reach up to perform
Components and synchronizers now talk to SceneEditorService; the underlying
repositories hold no sibling-repo dependencies. Tests are reorganized to match
the new boundaries, and the scene-editing domain API is documented in
docs/DESIGN_PATTERNS.md.
- Data foundation: SQLDelight tables for API metrics, error logs, and
login attempts with a v1→v2 schema migration
- Metrics pipeline: per-endpoint request counts, latency, and error
rates collected via a Ktor plugin; daily rollup job with configurable
retention windows
- Error tracking: fingerprinted deduplication, occurrence counts,
first/last seen timestamps, and email alerting on persistent errors
- Security page: login-attempt tracking, brute-force/spray detection
with cooldown alerts, optional IP storage
- Admin dashboard UI: performance charts (Frappe), error panel with
route filter and JSON export, live log viewer (10k ring buffer),
security event feed
- Alert deep-links from the dashboard into filtered error views
- Settings UI: all monitoring options converted to toggle switches;
master switch disables and grays out sub-options via JS; email field
conditionally required with htmx:before-request validation
ViewNote/ViewTimeLineEvent come back from process death with isEditing=true
and the restored draft, but the note/event isn't loaded yet (async, or cold
cache). In that window isEditingAndDirty() returned false, so a back-press or
close took the silent discardEdit() path and wiped the restored text.
Treat "editing with no loaded baseline" as dirty so those paths route to the
confirm-discard dialog instead. Behavior is unchanged once loaded.
Add a reusable EditorTestHarness (seed a project via Koin, launch straight
into ProjectRootActivity, navigate, tear down without racing the scope flush)
and one happy-path instrumented test per feature area: navigation smoke,
scene list/editor, notes, encyclopedia, timeline, project home, and global
search.
To make the UI addressable, add optional testTag params to shared design-system
components (FormField, MarkdownEditField, HdHairlineField/TagField/SearchField/
TypePicker, HdBottomBar/HdNavRail) and colocated testTag consts on the relevant
screens. The custom text editor consumes key events rather than Compose SetText
semantics, so the harness types into it via injected keystrokes.
All 9 new tests pass on an api-34 emulator.
ProjectLifecycleTest launches the real ProjectSelectActivity, creates a project,
waits for it to appear in the list, and confirms opening it launches
ProjectRootActivity (via ActivityMonitor). Cleans up in @After.
- Wire jetbrains-compose ui-test-junit4 into the androidTest source set.
- Tag the create-project affordance (CreateProjectButtonTestTag); "Create
Project" otherwise appears as three separate on-screen texts.
- Run the instrumented suite on an emulator in CI (android-emulator-runner) with
AVD snapshot caching.
- Convert HashTest from JUnit Jupiter to JUnit4 so the AndroidJUnit4 runner can
execute it on-device (it had "no runnable methods" otherwise); use assertEquals
since assert() is a no-op when assertions are disabled on a device. It now
verifies EntityHasher's golden vector on Android ART.
- Fix a scope-close crash: getSceneBufferDirectory used a non-recursive
createDirectory, so closing a project whose scenes/ dir is absent threw on the
teardown path and crashed the process. Use createDirectories (matching its
siblings) and order the test teardown so it doesn't delete the project mid-close.
ZipUtils.unzipBytesToDirectory used fileSystem.sink(target).buffer(),
where okio's Sink.buffer() failed to resolve for the iOS/native target
(receiver type mismatch), cascading into a 'cannot infer R' error on the
following .use{} and failing the iOS archive. Compiled fine for JVM, so
it only surfaced in the iOS publish workflow.
Use okio's multiplatform FileSystem.write(target) { } helper, which
yields a BufferedSink directly and handles buffering + closing. Verified
with :common:compileKotlinIosArm64.
The dry run served its purpose (verified the pipeline and surfaced the
two-cert signing bug, now fixed in publish-mac-app-store.yml). Drop the
dry-run workflow and its desktop_build_only lane; keep only the fix.
* ci: add Mac App Store dry-run lane and workflow
Adds a way to exercise the Mac App Store build/sign/package pipeline
without releasing anything to App Store Connect.
- fastlane: new `mac desktop_build_only` lane builds, signs, and verifies
the .pkg (via build-appstore.sh) but skips the App Store Connect build
number lookup and the upload — no API key needed.
- workflow: new "Dry Run — Mac App Store" (workflow_dispatch) mirrors the
real publish workflow's keychain + provisioning-profile setup, runs the
build-only lane, and saves the .pkg as an artifact instead of uploading.
Verified locally: lane builds + passes codesign/pkgutil checks.
* ci: temporarily trigger mac dry-run on push to its branch
workflow_dispatch requires the workflow to exist on the default branch
before it can be dispatched. Add a branch-scoped push trigger so the dry
run can be exercised now; remove before merging.
* ci: import Mac signing certs from two separate p12 files
macOS `security import` only ingests one identity from a combined
multi-key .p12 (which one wins is non-deterministic), so a single secret
holding both the Application and Installer certs left one of them missing
from the CI keychain — caught by the dry run, which failed with "Mac
Installer Distribution certificate not found".
Import the Application and Installer certs from their own single-identity
.p12 files instead. Adds a second secret, MAC_INSTALLER_CERT_P12_BASE64.
Applied to both the dry-run and the real publish-mac-app-store workflows.
* ci: drop temporary push trigger from mac dry-run workflow
The dry run was triggered via a branch-scoped push trigger because
workflow_dispatch only works once the workflow exists on the default
branch. Now that it's merging to develop, revert to workflow_dispatch
only.
The phone project-list row showed created/opened/words as one flowing mono
line that wrapped at arbitrary points. Replace it with equal-weight
label/value columns so the fields align down the whole list, with faint
captions over muted values to keep the title the only bright anchor.
Resolves the "Entity X not found for reId" failure that wedged every
project sync. Three related fixes:
- EntitySynchronizers.reIdEntry now skips a newId with no backing entity
(logged) instead of throwing. The throw aborted sync before finalize,
and finalize is the only place newIds is cleared, so a single phantom
deadlocked every future sync. Skipping lets sync finish and self-heal.
- SyncDataRepository.recordIdDeletion prunes the id from newIds when the
entity was never synced, instead of only appending to deletedIds. This
stops create-then-delete from leaving a permanent phantom newId behind.
- IdConflictResolutionOperation seeds new-id assignment from
max(serverLastId, localMaxId). If the server ever reports a lastId
below the client max (server-side data loss), seeding from serverLastId
alone handed out ids that collided with and clobbered real local
entities. Now logs a WARN and assigns above the local max.
Tests reproduce each failure first, then pass after the fix.
The settings updates collector rebuilt component state without copying
syncAutomaticSync or syncAutoCloseDialog, so toggling either checkbox
persisted the change but never updated the UI. Also removes leftover
debug logging.