Spelling: British -> American across in-app copy, docs, and code
Build Debug APK / build (push) Successful in 5m51s
Build Debug APK / build (push) Successful in 5m51s
Rewrites every British spelling found in the codebase to American: neighbour, colour, catalogue, behaviour, honour, flavour, grey, judgement, acknowledgement, licence, defence, organise/optimise/initialise/recognise/ analyse, artefact, cancelled/cancelling, labelled/labelling, modelled/ modelling, and centre/centred/centring all become their American forms — consistently in prose (docs, ADRs, comments) and in identifiers, including the FFI-shared CatalogueSync/Cancelled enum variants and catalogue_* fields that cross the Rust/Kotlin boundary. Left untouched: external API names that only look like a spelling variant (kotlinx.coroutines.CancellationException/NonCancellable, Compose Material3's labelLarge) and the EFF diceware wordlist used for key derivation. Adds a Code Style rule to AGENTS.md/CLAUDE.md requiring American spelling in user-facing copy and documentation going forward, with that same carve-out. Verified: cargo build --workspace, all 1410 Rust tests, Kotlin compile, fmt-rust/fmt-kotlin, lint-kotlin-fast (detekt), and the lint-gui-layout/ lint-sql-params/lint-p2p-admission custom checks all pass clean. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017fHWzCF6oNiCV4ue1bBf1t
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
2104fc9e62
commit
15b0db7a9d
@@ -70,7 +70,7 @@ finished, whatever else it does.
|
||||
- This holds for lists and collections on the form as much as for its plain
|
||||
fields, however the store happens to keep them. "Each row is its own
|
||||
artifact, so there's nothing to batch behind Save" is a fact about storage,
|
||||
not a licence to write one — it makes Save mean two different things on one
|
||||
not a license to write one — it makes Save mean two different things on one
|
||||
form, and it leaves the user unable to back out of a removal. Stage the
|
||||
edit and diff it against what is persisted when Save runs; the Humans
|
||||
screen's `diff_staged`/`diffStaged` is the shape.
|
||||
@@ -78,7 +78,7 @@ finished, whatever else it does.
|
||||
they should be one code path. Watch for state that lives outside the
|
||||
screen's own struct — the desktop's text fields are `TextInput` entities,
|
||||
so a Cancel that only dropped the staged collections would leave a
|
||||
cancelled rename on screen.
|
||||
canceled rename on screen.
|
||||
- Not everything on a screen is part of its form. A thing that is its own
|
||||
document — Humans' interactions, shared with their other attendees — is
|
||||
written when the user says to write it, and lives outside the mode. The
|
||||
@@ -142,6 +142,14 @@ developers build.
|
||||
carries its full reasoning and its escape hatch.
|
||||
|
||||
# Code Style
|
||||
- Use American spelling in user-facing copy and documentation (color, not
|
||||
colour; behavior, not behaviour; canceled, not cancelled; catalog, not
|
||||
catalogue) — consistently, throughout the codebase. Leave alone any spelling
|
||||
that comes from outside the project: a fixed external API/library name (e.g.
|
||||
Kotlin's `kotlinx.coroutines.CancellationException`/`NonCancellable`,
|
||||
Compose Material3's `labelLarge`), a URL, or a third-party wordlist
|
||||
(`rust/lib/src/ensemble/eff_large_wordlist.txt`) — those aren't ours to
|
||||
respell.
|
||||
- Errors: Use typed errors with meaningful messages. In rust, prefer `thiserror`. Avoid using .unwrap() or .expect() in non-test code, and fix uses of these or other panicking functions when you encounter them.
|
||||
- the justfile codifies recipes that run standard Rust and Kotlin formatters (just fmt-kotlin, just fmt-rust). Run these tools to keep code formatted on every commit.
|
||||
- don't generate code that has fully-qualified type names (e.g. `documents::Artifacts::new()`); instead prefer to `use documents::Artifacts`, `Artifacts::new()`
|
||||
|
||||
@@ -51,7 +51,7 @@ most widespread example of an overlay OS today.
|
||||
|
||||
<!-- note: I would eventually like to be able to say Synchronicity is a computing stack, but that is not the case yet -->
|
||||
<!-- the overlay OS terminology comes from https://urbit.org/overview/urbit-explained among others -->
|
||||
<!-- note: "Ra is a single entity distributed across the whole solar system. No part of Ra is slaved to any other part of it. No shell-Earth's core node has a noticeably differing personality from the others, or from the megastructure inside the Sun. Opinions and behaviour and available information are continuously synchronising. Eventually, there is only one Ra." - qntm's Ra, Abstract War -->
|
||||
<!-- note: "Ra is a single entity distributed across the whole solar system. No part of Ra is slaved to any other part of it. No shell-Earth's core node has a noticeably differing personality from the others, or from the megastructure inside the Sun. Opinions and behavior and available information are continuously synchronising. Eventually, there is only one Ra." - qntm's Ra, Abstract War -->
|
||||
|
||||
What can someone do with Synchronicity? Some examples:
|
||||
|
||||
|
||||
@@ -129,7 +129,7 @@ since it isn't worth taking before it shows up in a profile.
|
||||
- **Description**: The Android pass (ADR 0032 §10) landed the foundations, the whole
|
||||
structural half, and all but one of the five gaps against the desktop
|
||||
(`bgMediaCanvas`, the reading-surface story, the live-state slot's two wired
|
||||
publishers, and selection icon/text colours). One remains, deliberately deferred:
|
||||
publishers, and selection icon/text colors). One remains, deliberately deferred:
|
||||
- **A `Presence` tab** (§7). Still the one to hold longest. A nodes × documents grid is a
|
||||
desktop-shaped affordance that `design/visual-design-review/PRESENCE-AFFORDANCE.md`
|
||||
treats as unsettled even on the desktop, and a phone may want a different answer rather
|
||||
@@ -321,8 +321,8 @@ Model and dedup design: `docs/adr/0011-jobs-first-class-model.md`; product desig
|
||||
- **Cancellation is still coarse for a single blocking P2P call.** Every surface is
|
||||
runner-backed now, so a manual pull/push/eviction has a live token — but nothing reads it
|
||||
mid-transfer: `asset_sync`'s functions take no cancellation signal, so the work runs to
|
||||
the end and the row is only *recorded* as `Cancelled`. The imports and scheduler passes,
|
||||
which check `is_cancelled` between units, do stop. Threading a signal into `asset_sync`
|
||||
the end and the row is only *recorded* as `Canceled`. The imports and scheduler passes,
|
||||
which check `is_canceled` between units, do stop. Threading a signal into `asset_sync`
|
||||
(probably a `&dyn ProgressReporter`, since that is what the byte counters would want
|
||||
anyway) is the fix, and it would improve desktop as much as the FFI/daemon.
|
||||
- **The TUI has no import affordance at all.** When it gains one it should start a job from
|
||||
@@ -358,7 +358,7 @@ Model and dedup design: `docs/adr/0011-jobs-first-class-model.md`; product desig
|
||||
- **Description**: `scheduler::ensemble_peers_freshest_first` returns every node gossip has
|
||||
ever heard from for this ensemble — rows are never pruned, so a machine retired months
|
||||
ago is still in the list. Every pass walks it *in series* with a per-peer budget
|
||||
(`PEER_CATALOGUE_TIMEOUT` is 180s), so an ensemble carrying a couple of dead nodes
|
||||
(`PEER_CATALOG_TIMEOUT` is 180s), so an ensemble carrying a couple of dead nodes
|
||||
spends most of a pass dialling them. Symptom: a trivially small sync — one podcast and
|
||||
twenty episode documents — takes many minutes.
|
||||
- **What's already done**: the list is ordered most-recently-seen first, so the live peer
|
||||
@@ -428,12 +428,12 @@ How it works: `rust/lib/src/stores/lock.rs` and
|
||||
button is durable. This is `docs/sync-management.md`'s "push-assisted delivery", moved
|
||||
up; see `docs/content-placement-redesign.md` §5.
|
||||
- **Questions the queue exists to let us answer** (experiment, not argument — the right
|
||||
behaviour against a real sleeping phone isn't knowable from here):
|
||||
behavior against a real sleeping phone isn't knowable from here):
|
||||
- How long does a queued directive stay valid? A `Mirror` pressed a week ago may not be
|
||||
what the user still wants.
|
||||
- What happens if the target's own policy changes before it wakes — does the directive
|
||||
still apply, or has it been superseded by a more recent local decision?
|
||||
- Should a pending directive be visible, and cancellable, or is that clutter for
|
||||
- Should a pending directive be visible, and cancelable, or is that clutter for
|
||||
something that usually lands within seconds?
|
||||
- Delivery: gossip carries small frames (4 KiB, see the presence-announcement item) and
|
||||
reaches nodes as they appear; store-and-forward through a reachable third node is the
|
||||
@@ -459,14 +459,14 @@ How it works: `rust/lib/src/stores/lock.rs` and
|
||||
- **Per-artifact sync controls on the document detail page.** Pulling one artifact works
|
||||
on every surface (`asset_sync::sync_document_asset`).
|
||||
- ~~**Nothing asks peers whether a newer version exists.**~~ Closed: the whole-document
|
||||
pull (`asset_sync::sync_document_assets_recursive`) now runs a scoped catalogue refresh
|
||||
pull (`asset_sync::sync_document_assets_recursive`) now runs a scoped catalog refresh
|
||||
against every ensemble peer before the content walk, so "up to date" is an answer about
|
||||
the ensemble rather than about this node's last scheduler pass. See
|
||||
`docs/sync-management.md`'s "On-demand catalogue refresh for one document".
|
||||
`docs/sync-management.md`'s "On-demand catalog refresh for one document".
|
||||
- **The per-row "Sync" control still doesn't.** `sync_document_asset` — the narrow
|
||||
per-artifact affordance in the Artifacts section — deliberately skips the catalogue
|
||||
per-artifact affordance in the Artifacts section — deliberately skips the catalog
|
||||
phase: it fetches the one `ContentKey` the caller named, against whatever the local
|
||||
catalogue says. That's right for "fetch this blob I can see," and wrong if what the user
|
||||
catalog says. That's right for "fetch this blob I can see," and wrong if what the user
|
||||
means is "get me the current version of this note." Decide whether the row control should
|
||||
refresh that one artifact's row first, or whether the whole-document button is the only
|
||||
place that question belongs.
|
||||
@@ -500,7 +500,7 @@ How it works: `rust/lib/src/stores/lock.rs` and
|
||||
|
||||
### Service credentials: what's left after the store
|
||||
The store is built (ADR 0051): a `ServiceCredential` document per service,
|
||||
sealed with the vault key, synced on the catalogue plane. Podcast Index reads
|
||||
sealed with the vault key, synced on the catalog plane. Podcast Index reads
|
||||
from it; a Settings ▸ Credentials tab on the desktop, the same section on
|
||||
Android, and `syn settings credentials{,-set,-clear}` enter and remove keys;
|
||||
and the tests provision the published test key themselves. Each service's
|
||||
@@ -592,7 +592,7 @@ Open:
|
||||
entirely and drive Media3/ExoPlayer's `PlayerView` directly.
|
||||
- No poster-frame thumbnails — needs a decoded frame, so it's blocked on the
|
||||
above; `MediaLibrary` shows the placeholder glyph for every clip meanwhile.
|
||||
- Only MP4/MOV/M4V are recognised (`re_mp4`); MKV/WebM would need a second
|
||||
- Only MP4/MOV/M4V are recognized (`re_mp4`); MKV/WebM would need a second
|
||||
container parser.
|
||||
- Android has no folder import for video (desktop does, via
|
||||
`import_directory_tree`) — single-file picker only.
|
||||
|
||||
@@ -279,7 +279,7 @@ detekt {
|
||||
config.setFrom("$projectDir/detekt.yml")
|
||||
buildUponDefaultConfig = true
|
||||
allRules = false
|
||||
// Analyse files across threads. Off by default in detekt; the type-resolution
|
||||
// Analyze files across threads. Off by default in detekt; the type-resolution
|
||||
// pass (`detektDebug`, the one that has to pass — ADR 0022) is the slowest
|
||||
// thing in the Kotlin lint path once the Rust build is out of the way.
|
||||
parallel = true
|
||||
|
||||
@@ -23,7 +23,7 @@ class ChangesAgent internal constructor(private val nativeLib: NativeLib) {
|
||||
* the cursor as of waking — pass it back as [since] next time so a change that lands while the
|
||||
* caller is reloading isn't slept through.
|
||||
*
|
||||
* **Blocks the calling thread**, so call it off the main one. A cancelled coroutine can't
|
||||
* **Blocks the calling thread**, so call it off the main one. A canceled coroutine can't
|
||||
* interrupt the call, so [timeoutMs] is also what bounds how long a thread stays parked after
|
||||
* whoever was waiting goes away.
|
||||
*/
|
||||
|
||||
@@ -104,7 +104,7 @@ class DocumentsAgent internal constructor(private val nativeLib: NativeLib) {
|
||||
nativeLib.node().documents().listChildren(parentId)
|
||||
}
|
||||
|
||||
// A blob that isn't present locally yet (NotFound) is normal — e.g. catalogue
|
||||
// A blob that isn't present locally yet (NotFound) is normal — e.g. catalog
|
||||
// metadata synced ahead of the blob — so absence collapses to null.
|
||||
fun getBlobBytes(documentId: String): ByteArray? =
|
||||
ffiCatching { nativeLib.node().documents().getBlobBytes(documentId) }
|
||||
|
||||
@@ -49,7 +49,7 @@ class ImportAgent internal constructor(private val nativeLib: NativeLib) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a batch import: one tracked, cancellable job covering [totalFiles] files holding roughly
|
||||
* Open a batch import: one tracked, cancelable job covering [totalFiles] files holding roughly
|
||||
* [totalBytes] bytes, fed one file at a time through the returned [BatchImport]. Both totals
|
||||
* are counted by the caller before it starts — see `DocumentDirectoryImport.kt`'s tree pass —
|
||||
* and become the Jobs screen's denominators.
|
||||
@@ -87,11 +87,11 @@ class ImportAgent internal constructor(private val nativeLib: NativeLib) {
|
||||
class BatchImport internal constructor(private val batch: uniffi.synchronicity.ImportBatch) {
|
||||
|
||||
/** Whether the Jobs screen's Cancel button has been pressed for this import. */
|
||||
fun isCancelled(): Boolean = batch.isCancelled()
|
||||
fun isCanceled(): Boolean = batch.isCanceled()
|
||||
|
||||
/**
|
||||
* Import one file's bytes. Succeeds with the new document id, or with null when no supported
|
||||
* type recognised the file (a skip, not a failure). [mimeType] is only a fallback for dispatch,
|
||||
* type recognized the file (a skip, not a failure). [mimeType] is only a fallback for dispatch,
|
||||
* used when [fileName] has no extension a supported type claims.
|
||||
*/
|
||||
fun importFile(
|
||||
|
||||
@@ -24,7 +24,7 @@ class JobsAgent internal constructor(private val nativeLib: NativeLib) {
|
||||
/**
|
||||
* Best-effort cancel: `true` only if `jobId` had a live cancellation token to flip. See
|
||||
* `ffi.rs`'s `JobsFfi::cancel_job` doc comment -- only `BackgroundSync`, scheduler-originated
|
||||
* `CatalogueSync`/`ContentFetch`, `ContentStream` (a reader opening something that lives on
|
||||
* `CatalogSync`/`ContentFetch`, `ContentStream` (a reader opening something that lives on
|
||||
* another node -- see `lib::content_fetch`), and a batch (folder) `Import` have one; a
|
||||
* manually-triggered sync/fetch, a single-file `Import`, or any `Push`/`Eviction`/`Purge`,
|
||||
* returns `false` with no error.
|
||||
|
||||
@@ -55,7 +55,7 @@ object NodeReset {
|
||||
* Android settings is the way out.
|
||||
*
|
||||
* Runs under [NonCancellable], and picks its own dispatcher per step. The caller is a click
|
||||
* handler on a screen this navigates away from, so a cancellable reset is one that could stop
|
||||
* handler on a screen this navigates away from, so a cancelable reset is one that could stop
|
||||
* between destroying the store and clearing what was decrypted out of it. The wipe blocks its
|
||||
* thread for as long as it takes, so it goes to [Dispatchers.IO]; the audio transport is a
|
||||
* Media3 `MediaController`, which may only be touched from the main thread.
|
||||
|
||||
@@ -89,7 +89,7 @@ class SettingsAgent internal constructor(private val nativeLib: NativeLib) {
|
||||
nativeLib.node().settings().setSchedulerSettings(settings)
|
||||
}
|
||||
|
||||
// Run one full scheduler tick (catalogue, catalogue-fetch, content passes) against
|
||||
// Run one full scheduler tick (catalog, catalog-fetch, content passes) against
|
||||
// every reachable ensemble peer. Backs scheduler/SchedulerWorker.kt; requires P2P to
|
||||
// be running.
|
||||
fun runSchedulerPasses(): Result<SchedulerRunSummary> = ffiCatching {
|
||||
|
||||
@@ -31,7 +31,7 @@ private const val RETRY_DELAY_MS = 3_000L
|
||||
/**
|
||||
* How long a wake lets the rest of a burst land before it is reported.
|
||||
*
|
||||
* A catalogue sync commits a chunk of documents per transaction and does it repeatedly, so a screen
|
||||
* A catalog sync commits a chunk of documents per transaction and does it repeatedly, so a screen
|
||||
* told about each one would spend the sync rebuilding itself. One report per window bounds that at
|
||||
* a handful a second while a sync runs, and costs nothing while one isn't: this is a settle
|
||||
* interval, not a poll — nothing wakes unless something already happened.
|
||||
|
||||
@@ -14,7 +14,7 @@ import uniffi.synchronicity.recourse
|
||||
* The `when` is exhaustive with no `else` because [SyncException] is a sealed hierarchy — this is
|
||||
* the part of Rust's `Result<T, SyncError>` that survives the trip through `kotlin.Result`, whose
|
||||
* error slot is statically only `Throwable`. When the Rust `SyncError` enum grows a variant, uniffi
|
||||
* regenerates the sealed class and this stops compiling, which is the intended behaviour: see
|
||||
* regenerates the sealed class and this stops compiling, which is the intended behavior: see
|
||||
* docs/adr/0022 and docs/adr/0023.
|
||||
*/
|
||||
val SyncException.detail: String
|
||||
@@ -26,7 +26,7 @@ val SyncException.detail: String
|
||||
is SyncException.Offline -> msg
|
||||
is SyncException.InvalidInput -> msg
|
||||
is SyncException.PreconditionUnmet -> msg
|
||||
is SyncException.Cancelled -> msg
|
||||
is SyncException.Canceled -> msg
|
||||
is SyncException.Internal -> msg
|
||||
}
|
||||
|
||||
|
||||
@@ -256,7 +256,7 @@ private fun ComicPage(
|
||||
produceState<ComicRepository.PageImage?>(initialValue = null, key1 = pageId) {
|
||||
value = repository.loadPage(pageId, PAGE_MAX_EDGE)
|
||||
}
|
||||
// Armed only for the page the reader is actually on. The pager keeps neighbouring slots
|
||||
// Armed only for the page the reader is actually on. The pager keeps neighboring slots
|
||||
// composed, and those are the read-ahead's business — untracked by design, so watching them
|
||||
// would poll for job rows that never exist.
|
||||
val cancelScope = rememberCoroutineScope()
|
||||
|
||||
+3
-3
@@ -214,8 +214,8 @@ fun DocumentDetailScreen(documentId: String, onBack: () -> Unit, modifier: Modif
|
||||
r.artifactsReceived,
|
||||
r.documentsSynced,
|
||||
r.peersTried,
|
||||
r.cataloguePeersAvailable,
|
||||
r.cataloguePeersRefreshed,
|
||||
r.catalogPeersAvailable,
|
||||
r.catalogPeersRefreshed,
|
||||
)
|
||||
},
|
||||
onFailure = { e ->
|
||||
@@ -1348,7 +1348,7 @@ private fun NodeStatusRow(status: DocumentNodeStatusItem) {
|
||||
}
|
||||
}
|
||||
}
|
||||
// The state takes the colour; the timestamp beside it stays dim, so
|
||||
// The state takes the color; the timestamp beside it stays dim, so
|
||||
// the row reads as one fact with a time on it rather than two.
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
Text(
|
||||
|
||||
+4
-4
@@ -23,7 +23,7 @@ private const val TAG = "DocumentDirectoryImport"
|
||||
*
|
||||
* The loop is here because only Kotlin can enumerate and read `content://` URIs; everything else is
|
||||
* Rust's ([com.gregshuflin.synchronicity.BatchImport]). That is what makes the whole folder *one*
|
||||
* `Import` job — showing "12/40 files · 96.4 MB / 1.2 GB" as it goes, cancellable from the Jobs
|
||||
* `Import` job — showing "12/40 files · 96.4 MB / 1.2 GB" as it goes, cancelable from the Jobs
|
||||
* screen, and deduplicated against picking the same folder twice — where before it was one job per
|
||||
* file, none of which could say anything about the folder they belonged to.
|
||||
*
|
||||
@@ -67,7 +67,7 @@ suspend fun importDirectory(
|
||||
*/
|
||||
private fun ImportBatchSummary.describe(): String {
|
||||
val counts = "Imported $imported file(s), skipped ${skipped + failed}"
|
||||
return if (cancelled) "Import cancelled — $counts" else counts
|
||||
return if (canceled) "Import canceled — $counts" else counts
|
||||
}
|
||||
|
||||
/** What a Storage Access Framework subtree holds, for the import job's progress denominators. */
|
||||
@@ -188,9 +188,9 @@ private class DirectoryImporter(
|
||||
|
||||
private fun importTree(parentDocId: String, parentNode: String?) {
|
||||
for (entry in scanner.children(parentDocId)) {
|
||||
// Cancelling stops before the next entry; whatever already landed stays imported,
|
||||
// Canceling stops before the next entry; whatever already landed stays imported,
|
||||
// and re-running the import picks up the rest.
|
||||
if (batch.isCancelled()) return
|
||||
if (batch.isCanceled()) return
|
||||
if (entry.isDirectory) {
|
||||
importSubdirectory(entry, parentNode)
|
||||
} else {
|
||||
|
||||
+2
-2
@@ -185,7 +185,7 @@ fun DocumentsScreen(
|
||||
}
|
||||
|
||||
// Reset and reload the first page whenever the filters change. This effect is
|
||||
// cancelled and restarted on each change, so the debounce coalesces rapid
|
||||
// canceled and restarted on each change, so the debounce coalesces rapid
|
||||
// typing and an interrupted first-page load is simply dropped.
|
||||
LaunchedEffect(
|
||||
searchQuery,
|
||||
@@ -676,7 +676,7 @@ private fun DocumentSettingsRow(label: String, value: String) {
|
||||
* A folder import is the one action on this screen that can run for minutes, and until it finishes
|
||||
* the documents it is creating appear in the list with no explanation of where they are coming
|
||||
* from. Each line carries the job's own progress ("12/40 files · 96.4 MB / 1.2 GB") and a way
|
||||
* through to Jobs, where it can be cancelled.
|
||||
* through to Jobs, where it can be canceled.
|
||||
*/
|
||||
@Composable
|
||||
private fun ImportsInProgress(jobs: List<JobItem>) {
|
||||
|
||||
+11
-11
@@ -125,7 +125,7 @@ fun EnsembleScreen(
|
||||
// Rust so it matches the GUI.
|
||||
//
|
||||
// Nothing is seeded here about a sync that is *running*: each row's
|
||||
// Sync button watches its own peer's `CatalogueSync` job (see
|
||||
// Sync button watches its own peer's `CatalogSync` job (see
|
||||
// [SyncControl]), so one started elsewhere — the background scheduler,
|
||||
// a remote `TriggerSync`, this screen before the app was backgrounded —
|
||||
// shows up without this screen having to ask once on entry and then
|
||||
@@ -154,8 +154,8 @@ fun EnsembleScreen(
|
||||
LaunchedEffect(Unit) { load() }
|
||||
|
||||
// The roster moves without anyone here touching it: a member announces itself over gossip, a
|
||||
// catalogue sync finishes and writes its timestamp, documents arrive and change every row's
|
||||
// coverage. Not the whole picture — gossip neighbour count and live reachability are in-memory
|
||||
// catalog sync finishes and writes its timestamp, documents arrive and change every row's
|
||||
// coverage. Not the whole picture — gossip neighbor count and live reachability are in-memory
|
||||
// P2P state that nothing announces, so pull-to-refresh stays the way to ask for those.
|
||||
OnStoreChange(nativeLib, ChangeChannel.NODES, ChangeChannel.DOCUMENTS) { load(quiet = true) }
|
||||
|
||||
@@ -307,7 +307,7 @@ private fun EnsembleNodesScreen(
|
||||
}
|
||||
FactRow("DHT bootstrap") {
|
||||
// Not truncated: this is a link meant to be read, compared, or handed
|
||||
// to another node, not a bare id to recognise at a glance.
|
||||
// to another node, not a bare id to recognize at a glance.
|
||||
HashChip(
|
||||
full = sd.dhtBootstrapLink,
|
||||
clipLabel = "DHT bootstrap link",
|
||||
@@ -373,17 +373,17 @@ private fun EnsembleNodesScreen(
|
||||
ensembleRosterCopy(
|
||||
total = counts.total,
|
||||
other = counts.other,
|
||||
gossipNeighbours = sd.gossipNeighborCount,
|
||||
gossipNeighbors = sd.gossipNeighborCount,
|
||||
)
|
||||
SectionHeading("Nodes", trailing = rosterCopy.summary)
|
||||
Text(
|
||||
text = rosterCopy.gossipNeighbours,
|
||||
text = rosterCopy.gossipNeighbors,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color =
|
||||
if (rosterCopy.gossipIsQuiet) SynTheme.palette.colorWarning
|
||||
else SynTheme.palette.accent,
|
||||
)
|
||||
// Offered only when there is something behind it, and labelled with
|
||||
// Offered only when there is something behind it, and labeled with
|
||||
// the count: "Show revoked" alone doesn't say whether that uncovers
|
||||
// one row or forty.
|
||||
if (counts.revoked > 0u) {
|
||||
@@ -596,9 +596,9 @@ private enum class Ending {
|
||||
* Retire and Reset, in the one shape they share.
|
||||
*
|
||||
* [com.gregshuflin.synchronicity.ui.FlatButton]'s footprint — a full-width bordered rectangle, 56dp
|
||||
* tall — in the error colour ADR 0032 §4 reserves for destructive acts. Not FlatButton itself,
|
||||
* which is neutral by design. It stays local until something else wants a danger button, at which
|
||||
* point it belongs beside FlatButton.
|
||||
* tall — in the error color ADR 0032 §4 reserves for destructive acts. Not FlatButton itself, which
|
||||
* is neutral by design. It stays local until something else wants a danger button, at which point
|
||||
* it belongs beside FlatButton.
|
||||
*/
|
||||
@Composable
|
||||
private fun EndingButton(
|
||||
@@ -872,7 +872,7 @@ private fun MemberRow(
|
||||
* How much of the library this node holds.
|
||||
*
|
||||
* A meter is one of the two things ADR 0032 §4 lets a gradient do — encode a value — as against
|
||||
* filling a panel. The fill is one colour, matching the desktop's: the meter's *width* already
|
||||
* filling a panel. The fill is one color, matching the desktop's: the meter's *width* already
|
||||
* carries the value, and a red/amber/green judgment on top of it duplicated the encoding without
|
||||
* ever being explained anywhere the reader could find it.
|
||||
*
|
||||
|
||||
@@ -80,8 +80,8 @@ internal fun PingControl(state: PingState, onPing: () -> Unit) {
|
||||
/**
|
||||
* Sync button with its inline status (last-synced / "Not synced"), laid out button-first.
|
||||
*
|
||||
* The button binds to the `CatalogueSync` job for [nodeIdHex] rather than to a local "did I click
|
||||
* it" flag: a sync with this peer can equally have been started by the background scheduler, by a
|
||||
* The button binds to the `CatalogSync` job for [nodeIdHex] rather than to a local "did I click it"
|
||||
* flag: a sync with this peer can equally have been started by the background scheduler, by a
|
||||
* remote node's `TriggerSync`, or from this screen before the app was last backgrounded. While one
|
||||
* is running the button is not a re-trigger — it would only be refused by the store's own
|
||||
* already-in-progress check — and [JobActivity] beside it shows the job's progress and a way to get
|
||||
@@ -91,7 +91,7 @@ internal fun PingControl(state: PingState, onPing: () -> Unit) {
|
||||
internal fun SyncControl(nodeIdHex: String, state: SyncState, onSync: () -> Unit) {
|
||||
val control =
|
||||
rememberJobControl(
|
||||
JobKinds.CATALOGUE_SYNC,
|
||||
JobKinds.CATALOG_SYNC,
|
||||
JobSubjectRef.Peer(nodeIdHex = nodeIdHex),
|
||||
starting = state.starting,
|
||||
)
|
||||
@@ -115,7 +115,7 @@ internal fun SyncControl(nodeIdHex: String, state: SyncState, onSync: () -> Unit
|
||||
modifier = Modifier.height(32.dp),
|
||||
) {
|
||||
// Matches the desktop's label, and for the same reason: this
|
||||
// exchanges catalogues, pulls what *this* node's policy wants,
|
||||
// exchanges catalogs, pulls what *this* node's policy wants,
|
||||
// and tells the peer to do the same for its own.
|
||||
Text("Sync both ways", style = MaterialTheme.typography.labelSmall)
|
||||
}
|
||||
|
||||
@@ -92,8 +92,8 @@ private fun PeerHealthCard(copy: PeerHealthCopy) {
|
||||
}
|
||||
|
||||
/**
|
||||
* How far the severity colour is mixed into the notice's background. Light enough that the text on
|
||||
* top keeps its contrast in both themes — the colour is there to say which of two kinds of problem
|
||||
* How far the severity color is mixed into the notice's background. Light enough that the text on
|
||||
* top keeps its contrast in both themes — the color is there to say which of two kinds of problem
|
||||
* this is, and the words carry the rest.
|
||||
*/
|
||||
private const val SURFACE_TINT_ALPHA = 0.12f
|
||||
|
||||
@@ -206,7 +206,7 @@ fun HumanDetailScreen(
|
||||
|
||||
// The two "add a row" entry fields, hoisted so Save can commit them — see
|
||||
// [FactEntryState]. Keyed on `editing` so half-typed text is discarded
|
||||
// along with the rest of a cancelled edit.
|
||||
// along with the rest of a canceled edit.
|
||||
var factEntry by remember(editing) { mutableStateOf(FactEntryState()) }
|
||||
var contactEntry by remember(editing) { mutableStateOf(ContactEntryState()) }
|
||||
val protocols = remember { nativeLib.humans.contactProtocols().getOrDefault(emptyList()) }
|
||||
|
||||
@@ -39,7 +39,7 @@ private const val PERCENT_SCALE = 100f
|
||||
*/
|
||||
object JobKinds {
|
||||
const val BACKGROUND_SYNC = "background_sync"
|
||||
const val CATALOGUE_SYNC = "catalogue_sync"
|
||||
const val CATALOG_SYNC = "catalog_sync"
|
||||
const val CONTENT_FETCH = "content_fetch"
|
||||
const val EVICTION = "eviction"
|
||||
const val IMPORT = "import"
|
||||
|
||||
@@ -54,7 +54,7 @@ private enum class JobsTab(val label: String) {
|
||||
}
|
||||
|
||||
/**
|
||||
* The Jobs screen: cross-screen visibility into long-running background operations (catalogue syncs
|
||||
* The Jobs screen: cross-screen visibility into long-running background operations (catalog syncs
|
||||
* today; content fetches, imports, pushes, evictions, and purges as they migrate onto a `JobRunner`
|
||||
* on the Android side -- see `docs/sync-management.md`'s "Jobs" section). Mirrors the desktop GUI's
|
||||
* Active/History tabs (`rust/gui-app/src/agents/jobs.rs`), reading from the same `JobItem` FFI
|
||||
@@ -67,12 +67,12 @@ private enum class JobsTab(val label: String) {
|
||||
*
|
||||
* Active rows show a Cancel button like desktop's, but unlike desktop it's best-effort per row:
|
||||
* only jobs started through `SyncNode`'s live `JobRunner` have a cancellation token -- today
|
||||
* `BackgroundSync`, scheduler-originated `CatalogueSync`/`ContentFetch`, and a folder import
|
||||
* started through `ImportFfi::begin_batch`. A manually-triggered sync/fetch, a single-file
|
||||
* `Import`, or a `Push`/`Eviction`/`Purge` job's Cancel click comes back `false` with nothing to
|
||||
* show for it -- see `ffi.rs`'s `cancel_job` doc comment. `list_job_history`/`clear_job_history`
|
||||
* are meaningful regardless of what's currently active, since `JobStore` isn't node-scoped -- this
|
||||
* node can see history from the desktop GUI's `JobRunner` against the same store.
|
||||
* `BackgroundSync`, scheduler-originated `CatalogSync`/`ContentFetch`, and a folder import started
|
||||
* through `ImportFfi::begin_batch`. A manually-triggered sync/fetch, a single-file `Import`, or a
|
||||
* `Push`/`Eviction`/`Purge` job's Cancel click comes back `false` with nothing to show for it --
|
||||
* see `ffi.rs`'s `cancel_job` doc comment. `list_job_history`/`clear_job_history` are meaningful
|
||||
* regardless of what's currently active, since `JobStore` isn't node-scoped -- this node can see
|
||||
* history from the desktop GUI's `JobRunner` against the same store.
|
||||
*/
|
||||
@Composable
|
||||
fun JobsScreen(nativeLib: NativeLib, onBack: () -> Unit, modifier: Modifier = Modifier) {
|
||||
@@ -97,8 +97,8 @@ fun JobsScreen(nativeLib: NativeLib, onBack: () -> Unit, modifier: Modifier = Mo
|
||||
fun cancel(job: JobItem) {
|
||||
scope.launch {
|
||||
withContext(Dispatchers.IO) { nativeLib.jobs.cancelJob(job.id) }
|
||||
.onSuccess { cancelled ->
|
||||
if (cancelled) {
|
||||
.onSuccess { canceled ->
|
||||
if (canceled) {
|
||||
// The Active list follows the store's own change signal; only History
|
||||
// has to be asked again.
|
||||
reloadHistory()
|
||||
@@ -106,7 +106,7 @@ fun JobsScreen(nativeLib: NativeLib, onBack: () -> Unit, modifier: Modifier = Mo
|
||||
// Best-effort: no live cancellation token for this job (see
|
||||
// `JobsScreen`'s doc comment) -- say so rather than pretending the
|
||||
// click did nothing.
|
||||
error = "\"${job.label}\" can't be cancelled."
|
||||
error = "\"${job.label}\" can't be canceled."
|
||||
}
|
||||
}
|
||||
.onFailure { error = it.message }
|
||||
@@ -126,7 +126,7 @@ fun JobsScreen(nativeLib: NativeLib, onBack: () -> Unit, modifier: Modifier = Mo
|
||||
title = { Text("Clear job history?") },
|
||||
text = {
|
||||
Text(
|
||||
"This removes finished, failed, and cancelled jobs from the list. Active " +
|
||||
"This removes finished, failed, and canceled jobs from the list. Active " +
|
||||
"jobs are unaffected."
|
||||
)
|
||||
},
|
||||
@@ -221,7 +221,7 @@ private fun JobRow(job: JobItem, showTimestamps: Boolean, timezone: String?, onC
|
||||
Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
JobLabelRow(job)
|
||||
|
||||
// CatalogueSync's peer chip. Push already renders its peer
|
||||
// CatalogSync's peer chip. Push already renders its peer
|
||||
// inline in the label row above, so it's excluded here to avoid
|
||||
// showing it twice -- matches `rust/gui-app/src/agents/jobs.rs`'s
|
||||
// `render_job_row`.
|
||||
@@ -345,7 +345,7 @@ private fun JobStatusLine(job: JobItem) {
|
||||
MaterialTheme.colorScheme.onSurfaceVariant
|
||||
"done" -> jobStateLabel(job.state) to SynTheme.palette.colorSuccess
|
||||
"failed" -> (job.error ?: jobStateLabel(job.state)) to MaterialTheme.colorScheme.error
|
||||
"cancelled" -> jobStateLabel(job.state) to MaterialTheme.colorScheme.onSurfaceVariant
|
||||
"canceled" -> jobStateLabel(job.state) to MaterialTheme.colorScheme.onSurfaceVariant
|
||||
// An unknown tag comes back from Rust unchanged, so a job row written by a newer
|
||||
// node still lists rather than blanking.
|
||||
else -> jobStateLabel(job.state) to MaterialTheme.colorScheme.onSurfaceVariant
|
||||
|
||||
@@ -148,7 +148,7 @@ fun libraryLevelOf(path: List<Pair<String, String>>): MusicLibraryLevel =
|
||||
* counterpart of the desktop header's own status line.
|
||||
*/
|
||||
fun buildSummaryLine(summary: LibraryBuildSummary): String {
|
||||
if (summary.cancelled) return "Library build cancelled"
|
||||
if (summary.canceled) return "Library build canceled"
|
||||
val tracks = (summary.tracksScanned + summary.tracksReused).toInt()
|
||||
val albums = summary.albums.toInt()
|
||||
val artists = summary.artists.toInt()
|
||||
|
||||
@@ -130,7 +130,7 @@ fun MusicScreen(onBack: () -> Unit, modifier: Modifier = Modifier) {
|
||||
var selectedTrack by remember { mutableStateOf<MusicTrack?>(null) }
|
||||
// The tracks the current selection is surrounded by, in the order they were on screen when the
|
||||
// user picked it — an album's tracks, a folder's contents, whatever level the tap came from.
|
||||
// This is what the player's Previous/Next step through, and its emptiness is what greys them
|
||||
// This is what the player's Previous/Next step through, and its emptiness is what grays them
|
||||
// out: a track opened with nothing either side of it has no next.
|
||||
//
|
||||
// Captured at activation rather than read live, because it belongs to what is *playing*. The
|
||||
@@ -364,7 +364,7 @@ fun MusicScreen(onBack: () -> Unit, modifier: Modifier = Modifier) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether [track] has a song either side of it in [context]. Both false — a greyed Previous and
|
||||
* Whether [track] has a song either side of it in [context]. Both false — a grayed Previous and
|
||||
* Next — for a track with no surrounding list, and at each end of one that has.
|
||||
*/
|
||||
internal fun adjacencyOf(track: MusicTrack?, context: List<MusicTrack>): Adjacency {
|
||||
|
||||
@@ -301,7 +301,7 @@ private fun NoteMetaRow(
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
// Only while a preview is on screen: in Edit mode there is no page to recolour,
|
||||
// Only while a preview is on screen: in Edit mode there is no page to recolor,
|
||||
// and the control would be asking about something the user can't see.
|
||||
if (viewMode != NotesViewMode.EDIT) {
|
||||
ReadingSurfaceDropdown(readingSurface, onReadingSurfaceChange)
|
||||
|
||||
@@ -74,7 +74,7 @@ internal fun NoteTabSidebar(
|
||||
modifier = Modifier.size(20.dp),
|
||||
// onPrimaryContainer, matching the label below and the desktop's own
|
||||
// row_text: the saturated selection block always carries
|
||||
// text_on_accent, never the accent stroke colour itself (ADR 0032 §5).
|
||||
// text_on_accent, never the accent stroke color itself (ADR 0032 §5).
|
||||
tint =
|
||||
if (isActive) MaterialTheme.colorScheme.onPrimaryContainer
|
||||
else MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
|
||||
@@ -334,7 +334,7 @@ private fun TreeItemRow(
|
||||
modifier = Modifier.size(16.dp),
|
||||
// onPrimaryContainer, matching the title below and the desktop's own
|
||||
// row_text: the saturated selection block always carries
|
||||
// text_on_accent, never the accent stroke colour itself (ADR 0032 §5).
|
||||
// text_on_accent, never the accent stroke color itself (ADR 0032 §5).
|
||||
tint =
|
||||
if (isSelected) MaterialTheme.colorScheme.onPrimaryContainer
|
||||
else MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
@@ -434,7 +434,7 @@ private fun TreeItemRow(
|
||||
}
|
||||
|
||||
/**
|
||||
* A row's title, written at the weight and colour its [emphasis] calls for, with the searched-for
|
||||
* A row's title, written at the weight and color its [emphasis] calls for, with the searched-for
|
||||
* run marked inside a match.
|
||||
*
|
||||
* The selected row is left unmarked: an accent run on the selection block is the one place the
|
||||
|
||||
@@ -49,7 +49,7 @@ sealed class NoteTreeItem {
|
||||
|
||||
/**
|
||||
* One row of the notebook tree as drawn: the item, how deep it sits, and how prominently its name
|
||||
* is written relative to its neighbours.
|
||||
* is written relative to its neighbors.
|
||||
*
|
||||
* The tree is flattened into these before rendering rather than drawn recursively, so a filtered
|
||||
* and an unfiltered listing are the same list of rows built two ways — see [visibleNoteRows]. The
|
||||
|
||||
@@ -706,7 +706,7 @@ fun NotesScreen(nativeLib: NativeLib, onBack: () -> Unit, modifier: Modifier = M
|
||||
|
||||
/**
|
||||
* Editor fills the full area; sidebars slide in as overlays on top of it so the editor never
|
||||
* shrinks (Obsidian-style behaviour).
|
||||
* shrinks (Obsidian-style behavior).
|
||||
*
|
||||
* [leftSidebarOverlay] and [rightSidebarOverlay] are plain `@Composable () -> Unit` slots — NOT
|
||||
* scoped extensions — so `AnimatedVisibility` resolves to the correct top-level overload rather
|
||||
|
||||
@@ -31,7 +31,7 @@ class P2PService : Service() {
|
||||
|
||||
private val nativeLib = NativeLib()
|
||||
|
||||
// SupervisorJob alone is NOT a crash barrier: it stops a failing child from cancelling its
|
||||
// SupervisorJob alone is NOT a crash barrier: it stops a failing child from canceling its
|
||||
// siblings, then hands the exception to the thread's default uncaught handler, which kills the
|
||||
// process. Only the CoroutineExceptionHandler actually stops it here. This service has no UI to
|
||||
// report through, so it logs and lets the retry loop's own supervision continue — see
|
||||
|
||||
@@ -96,5 +96,5 @@ private fun HistoryArrow(
|
||||
}
|
||||
|
||||
// Material's standard disabled-content opacity, applied to the history arrows
|
||||
// since the explicit tint overrides IconButton's own disabled colouring.
|
||||
// since the explicit tint overrides IconButton's own disabled coloring.
|
||||
private const val DISABLED_ICON_ALPHA = 0.38f
|
||||
|
||||
@@ -21,7 +21,7 @@ private const val PER_PAGE_BUDGET_BYTES = 2048L * 2048L * 4L
|
||||
// Fraction of the app's max heap the page cache is allowed to occupy.
|
||||
private const val CACHE_HEAP_FRACTION = 0.25
|
||||
|
||||
// The cache always needs the current page plus its two pre-rendered neighbours;
|
||||
// The cache always needs the current page plus its two pre-rendered neighbors;
|
||||
// the upper bound just keeps a very large heap from over-caching.
|
||||
private const val MIN_CACHED_PAGES = 3
|
||||
private const val MAX_CACHED_PAGES_CAP = 8
|
||||
|
||||
@@ -35,7 +35,7 @@ class PdfViewerState {
|
||||
|
||||
// Back/forward page history, backed by the reusable Rust component. The
|
||||
// can* flags mirror its state into Compose so the toolbar arrows recompose
|
||||
// (and grey out) as the user navigates.
|
||||
// (and gray out) as the user navigates.
|
||||
private val history = NavigationHistory.withDefaultCapacity()
|
||||
var canGoBack by mutableStateOf(false)
|
||||
private set
|
||||
|
||||
@@ -529,7 +529,7 @@ private fun ImageLibrarySidebar(
|
||||
modifier = Modifier.size(16.dp),
|
||||
// onPrimaryContainer, matching the title below and the desktop's own
|
||||
// row_text: the saturated selection block always carries
|
||||
// text_on_accent, never the accent stroke colour itself (ADR 0032 §5).
|
||||
// text_on_accent, never the accent stroke color itself (ADR 0032 §5).
|
||||
tint =
|
||||
if (isSelected) MaterialTheme.colorScheme.onPrimaryContainer
|
||||
else MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
|
||||
@@ -14,7 +14,7 @@ import kotlinx.coroutines.flow.stateIn
|
||||
* The device plays one thing at a time, so there is exactly one [AudioPlayerManager] and one
|
||||
* ExoPlayer behind it — but *what is loaded* belongs to whichever agent loaded it, and no screen
|
||||
* may render or drive another agent's playback. A shared handle made that far too easy: pausing a
|
||||
* podcast and opening Music showed the Music bar sitting at the podcast's position, greyed out
|
||||
* podcast and opening Music showed the Music bar sitting at the podcast's position, grayed out
|
||||
* because Music had no track of its own but scrubbed to somebody else's.
|
||||
*
|
||||
* So each agent takes its own handle ([AudioPlayerProvider.forSource]) and sees only its own
|
||||
@@ -30,7 +30,7 @@ class AgentAudioPlayer(
|
||||
private val transport: AudioPlayerManager,
|
||||
private val source: PlaybackSource,
|
||||
) {
|
||||
// Eagerly started and never cancelled: a handle is a per-source singleton with the same
|
||||
// Eagerly started and never canceled: a handle is a per-source singleton with the same
|
||||
// lifetime as the transport, so there is no scope to tie it to and nothing to leak into.
|
||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
|
||||
|
||||
|
||||
+1
-1
@@ -284,7 +284,7 @@ class AudioPlayerManager(private val context: Context) {
|
||||
* Deliberately not queued behind the connection the way every other command here is: [release]
|
||||
* tears that connection down, so a queued stop would never run. If the controller has not
|
||||
* connected yet then nothing it was asked to load has reached the service either, and
|
||||
* [release]'s cancelling of the connection is the stop.
|
||||
* [release]'s canceling of the connection is the stop.
|
||||
*/
|
||||
fun stopAndRelease() {
|
||||
player?.run {
|
||||
|
||||
+1
-1
@@ -88,7 +88,7 @@ fun PlaybackSpeedControl(
|
||||
) {
|
||||
var menuOpen by remember { mutableStateOf(false) }
|
||||
var dialogOpen by remember { mutableStateOf(false) }
|
||||
// The rate to restore if the dialog is cancelled, captured as it opens.
|
||||
// The rate to restore if the dialog is canceled, captured as it opens.
|
||||
var speedBeforeDialog by remember { mutableFloatStateOf(speed) }
|
||||
|
||||
Box(modifier = modifier) {
|
||||
|
||||
+1
-1
@@ -431,7 +431,7 @@ private fun BackgroundSyncSection(
|
||||
SectionHeading("Background sync")
|
||||
Text(
|
||||
text =
|
||||
"Periodically syncs the document catalogue with your ensemble and fetches " +
|
||||
"Periodically syncs the document catalog with your ensemble and fetches " +
|
||||
"content this node is configured to keep, without needing to press Sync " +
|
||||
"manually.",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
|
||||
@@ -64,9 +64,9 @@ private val LABELS: Map<DocumentAvailability, String?> =
|
||||
fun availabilityWord(availability: DocumentAvailability): String? = LABELS[availability]
|
||||
|
||||
/**
|
||||
* The colour a state's word and rule take.
|
||||
* The color a state's word and rule take.
|
||||
*
|
||||
* `REMOTE` spends no colour: it is the ordinary condition in an ensemble, and painting the ordinary
|
||||
* `REMOTE` spends no color: it is the ordinary condition in an ensemble, and painting the ordinary
|
||||
* case would make a mostly-unsynced library shout. Only `UNAVAILABLE` takes the warning hue — and
|
||||
* `colorWarning`, not `error`, because "we have not been told" is not a fault (ADR 0032 §4) and
|
||||
* CLAUDE.md's "absence is not an error" applies to how it is painted too.
|
||||
@@ -118,11 +118,11 @@ fun AvailabilityWord(
|
||||
availability: DocumentAvailability,
|
||||
modifier: Modifier = Modifier,
|
||||
/**
|
||||
* Overrides the colour the state would otherwise take, for the one surface the ladder above
|
||||
* Overrides the color the state would otherwise take, for the one surface the ladder above
|
||||
* doesn't cover: a word sitting *on* the saturated selection block, which carries
|
||||
* `textOnAccent` whatever the thing on it would be coloured elsewhere (ADR 0032 §5) — the notes
|
||||
* `textOnAccent` whatever the thing on it would be colored elsewhere (ADR 0032 §5) — the notes
|
||||
* tree's selected row is the case. Nothing is lost by it: the two states are told apart by the
|
||||
* word, and the colour was never carrying that on its own.
|
||||
* word, and the color was never carrying that on its own.
|
||||
*/
|
||||
color: Color = Color.Unspecified,
|
||||
) {
|
||||
|
||||
@@ -57,7 +57,7 @@ private val CHECK_ICON_SIZE = 12.dp
|
||||
* @param truncateChars show this many characters before the ellipsis instead of the default 8.
|
||||
* Ignored if [short] is given.
|
||||
* @param noTruncate show the full value, never truncated. Ignored if [short] is given.
|
||||
* @param color overrides the label colour (normally `onSurfaceVariant`) for a chip that needs to
|
||||
* @param color overrides the label color (normally `onSurfaceVariant`) for a chip that needs to
|
||||
* read as more than a dim machine value.
|
||||
*/
|
||||
@Composable
|
||||
|
||||
@@ -607,8 +607,8 @@ private fun CoverArt(item: MediaItem, placeholderIcon: ImageVector, glyphSize: D
|
||||
/**
|
||||
* A centered note for the empty / placeholder states.
|
||||
*
|
||||
* `textAlign` is load-bearing, not decoration: the Box centres the text *block*, so a message short
|
||||
* enough to fit one line looks centred without it and a message long enough to wrap does not — the
|
||||
* `textAlign` is load-bearing, not decoration: the Box centers the text *block*, so a message short
|
||||
* enough to fit one line looks centered without it and a message long enough to wrap does not — the
|
||||
* second line hugs the left edge of the block. Comics is where that showed, its message being the
|
||||
* longest of the four.
|
||||
*/
|
||||
|
||||
@@ -11,7 +11,7 @@ import uniffi.synchronicity.placeholderCopy
|
||||
*
|
||||
* Three words, and they are not interchangeable: a nameless *person* does not read as "Untitled",
|
||||
* and a note with no title is not "(unnamed)". Keeping them apart is most of the point — the TUI
|
||||
* had already drifted, labelling untitled notes as though they were nodes, which is exactly the
|
||||
* had already drifted, labeling untitled notes as though they were nodes, which is exactly the
|
||||
* drift a single definition prevents.
|
||||
*
|
||||
* Fetched once and held: these are constants, and the only reason they cross the FFI at all is so
|
||||
|
||||
@@ -50,7 +50,7 @@ import coil.compose.AsyncImage
|
||||
* ```text
|
||||
* Artwork <- optional
|
||||
*
|
||||
* Primary metadata <- centred
|
||||
* Primary metadata <- centered
|
||||
* Secondary metadata
|
||||
*
|
||||
* Optional visualization <- collapses to nothing when absent
|
||||
@@ -72,9 +72,9 @@ import coil.compose.AsyncImage
|
||||
* and trailing times either side of the scrubber and anchors the transport to the left of a wide
|
||||
* row. Neither survives a phone: a ~340dp content width minus two time labels leaves the scrubber a
|
||||
* stub, and a left-anchored transport puts the play button under nobody's thumb. So the times sit
|
||||
* *below* the full-width scrubber and the transport is centred. This is the same width-driven split
|
||||
* the app already makes between two-column and stacked label/value lists — the thing that would
|
||||
* change the answer is width, not tidiness.
|
||||
* *below* the full-width scrubber and the transport is centered. This is the same width-driven
|
||||
* split the app already makes between two-column and stacked label/value lists — the thing that
|
||||
* would change the answer is width, not tidiness.
|
||||
*
|
||||
* This widget is transport-agnostic: it renders the state it is handed and reports gestures back.
|
||||
* It never touches [com.gregshuflin.synchronicity.podcast.AgentAudioPlayer], so the screen stays
|
||||
@@ -103,7 +103,7 @@ fun PlayerControls(
|
||||
secondaryActions: @Composable RowScope.() -> Unit = {},
|
||||
) {
|
||||
// Live scrub position while the user drags the thumb; null when not scrubbing. Owned here so
|
||||
// every host gets the same behaviour — the time label follows the thumb, not the sink.
|
||||
// every host gets the same behavior — the time label follows the thumb, not the sink.
|
||||
var scrubPositionMs by remember { mutableStateOf<Long?>(null) }
|
||||
|
||||
Column(
|
||||
@@ -219,7 +219,7 @@ sealed interface TransportStep {
|
||||
* Whether the screen has a track either side of the one in the player, for a player whose primary
|
||||
* cluster steps between tracks ([TransportStep.Adjacent]).
|
||||
*
|
||||
* Both false — the default — greys the pair out, which is the honest rendering for a track with no
|
||||
* Both false — the default — grays the pair out, which is the honest rendering for a track with no
|
||||
* surrounding ordering: one song opened on its own has no "next".
|
||||
*/
|
||||
data class Adjacency(val previous: Boolean = false, val next: Boolean = false)
|
||||
@@ -297,9 +297,9 @@ private fun Artwork(uri: String?) {
|
||||
}
|
||||
|
||||
/**
|
||||
* **Metadata.** Title over subtitle, centred across the whole player — which is to say centred over
|
||||
* the scrubber below it, not over the transport cluster. Both lines truncate rather than wrap, so a
|
||||
* long episode title can't push the timeline or the controls down the screen.
|
||||
* **Metadata.** Title over subtitle, centered across the whole player — which is to say centered
|
||||
* over the scrubber below it, not over the transport cluster. Both lines truncate rather than wrap,
|
||||
* so a long episode title can't push the timeline or the controls down the screen.
|
||||
*/
|
||||
@Composable
|
||||
private fun Metadata(track: NowPlaying?, isLoading: Boolean) {
|
||||
@@ -394,7 +394,7 @@ private fun Timeline(
|
||||
}
|
||||
|
||||
/**
|
||||
* **Primary transport.** Back, play/pause, forward — centred, because a phone's reachable area is
|
||||
* **Primary transport.** Back, play/pause, forward — centered, because a phone's reachable area is
|
||||
* the middle-bottom of the screen rather than either edge. Play/pause is filled and larger than
|
||||
* what flanks it: "play/pause should be the strongest visual control", which doesn't survive being
|
||||
* one more outlined glyph in a row of them.
|
||||
|
||||
@@ -84,7 +84,7 @@ enum class PresenceLook {
|
||||
}
|
||||
}
|
||||
|
||||
/** The colour this state's dot and label take. */
|
||||
/** The color this state's dot and label take. */
|
||||
@Composable
|
||||
fun PresenceLook.color(): Color =
|
||||
when (this) {
|
||||
@@ -98,7 +98,7 @@ fun PresenceLook.color(): Color =
|
||||
* A filled dot when the copy is here, an empty outlined one when it is not.
|
||||
*
|
||||
* The outline is what carries "nothing is wrong, there is simply nothing here" without spending any
|
||||
* colour on it — and spending no colour on the common case is what leaves the palette free to mark
|
||||
* color on it — and spending no color on the common case is what leaves the palette free to mark
|
||||
* the exceptions.
|
||||
*/
|
||||
@Composable
|
||||
|
||||
@@ -73,10 +73,10 @@ data class RemoteFetch(val state: FetchDisplay, val jobId: String?) {
|
||||
suspend fun cancel(nativeLib: NativeLib) {
|
||||
val jobId = jobId ?: return
|
||||
withContext(Dispatchers.IO) { nativeLib.jobs.cancelJob(jobId) }
|
||||
.onSuccess { cancelled ->
|
||||
if (!cancelled) Log.i(TAG, "Content-stream job $jobId had already settled")
|
||||
.onSuccess { canceled ->
|
||||
if (!canceled) Log.i(TAG, "Content-stream job $jobId had already settled")
|
||||
}
|
||||
.onFailure { Log.w(TAG, "Cancelling content-stream job $jobId failed", it) }
|
||||
.onFailure { Log.w(TAG, "Canceling content-stream job $jobId failed", it) }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ private val TITLE_TRACKING = 0.8.sp
|
||||
*
|
||||
* The parts, left to right: a short accent bar, the title, a hairline rule filling the remaining
|
||||
* width, and an optional trailing value in dim mono. The bar is the only place a heading spends any
|
||||
* colour; the rule is what makes a heading read as a division of the screen rather than as a bold
|
||||
* color; the rule is what makes a heading read as a division of the screen rather than as a bold
|
||||
* line of text.
|
||||
*
|
||||
* This is the one place the two modalities legitimately differ in execution rather than in
|
||||
|
||||
+1
-1
@@ -83,7 +83,7 @@ val LocalSnackbarController =
|
||||
*
|
||||
* [CancellationException] is deliberately not caught (rethrown immediately): coroutine cancellation
|
||||
* depends on it propagating — swallowing it would break structured concurrency (e.g.
|
||||
* cancel-on-navigate-away no longer actually cancelling).
|
||||
* cancel-on-navigate-away no longer actually canceling).
|
||||
*/
|
||||
@Suppress("TooGenericExceptionCaught")
|
||||
fun CoroutineScope.launchReportingFailure(
|
||||
|
||||
@@ -12,8 +12,8 @@ import androidx.compose.ui.graphics.Color
|
||||
import uniffi.synchronicity.ReadingSurface
|
||||
|
||||
/**
|
||||
* The colours of a *reading* surface: the ground a document is rendered on, plus the ink and link
|
||||
* colour that go with it. Kotlin counterpart of `rust/gui-app/src/theme.rs`'s `Page`.
|
||||
* The colors of a *reading* surface: the ground a document is rendered on, plus the ink and link
|
||||
* color that go with it. Kotlin counterpart of `rust/gui-app/src/theme.rs`'s `Page`.
|
||||
*
|
||||
* Separate from [SynPalette] on purpose — ADR 0032 §5 makes the page a different choice from the
|
||||
* chrome (the metaphor is a desk, dark tools and a light page), so a reader who wants white paper
|
||||
@@ -42,7 +42,7 @@ data class Page(
|
||||
*/
|
||||
val panel: Color,
|
||||
) {
|
||||
/** Whether the page is a dark one — decides which base Material scheme it recolours. */
|
||||
/** Whether the page is a dark one — decides which base Material scheme it recolors. */
|
||||
val isDark: Boolean
|
||||
get() = surface == ReadingSurface.DARK
|
||||
|
||||
@@ -93,7 +93,7 @@ private val Dark =
|
||||
)
|
||||
|
||||
/**
|
||||
* This palette recoloured for a reading surface — the Compose counterpart of `Theme::on_page`.
|
||||
* This palette recolored for a reading surface — the Compose counterpart of `Theme::on_page`.
|
||||
*
|
||||
* Only the slots the note-body renderers actually read are overridden (background/surface for the
|
||||
* ground, `surfaceVariant` for a code fence or table header, the `on*` text roles, the outline
|
||||
@@ -115,15 +115,15 @@ private fun pageColorScheme(page: Page): ColorScheme =
|
||||
)
|
||||
|
||||
/**
|
||||
* Wraps [content] so it renders as a reading surface rather than chrome: [MaterialTheme]'s colour
|
||||
* Wraps [content] so it renders as a reading surface rather than chrome: [MaterialTheme]'s color
|
||||
* slots are re-pointed at [page] via [pageColorScheme], and the wrapper itself paints the page's
|
||||
* ground — mirroring `markup_view`'s own `.bg(theme.bg_window)` once its caller has handed it a
|
||||
* page-recoloured `Theme`. Used by the notes preview (`notes/NoteEditorPane.kt`) for Markdown and
|
||||
* page-recolored `Theme`. Used by the notes preview (`notes/NoteEditorPane.kt`) for Markdown and
|
||||
* plain-text bodies; Typst notes render on `bgMediaCanvas` instead (ADR 0032 gap #1), not a page.
|
||||
*
|
||||
* [LocalContentColor] is re-pointed too, and that part is not optional. A `Text` with no explicit
|
||||
* colour does not read the colour scheme at all — it reads `LocalContentColor`, which the app's own
|
||||
* `Surface` set to *chrome* ink several levels up. Recolouring the scheme alone therefore put a
|
||||
* color does not read the color scheme at all — it reads `LocalContentColor`, which the app's own
|
||||
* `Surface` set to *chrome* ink several levels up. Recoloring the scheme alone therefore put a
|
||||
* light page under body text still painted for Charcoal, which is the whole document unreadable.
|
||||
* Providing it here is what a `Surface` would do for its own content, without the elevation overlay
|
||||
* a `Surface` would also bring.
|
||||
|
||||
@@ -7,17 +7,17 @@ import androidx.compose.ui.graphics.Color
|
||||
// modalities read the same names for the same jobs, so a value that moves there
|
||||
// has an obvious counterpart here.
|
||||
//
|
||||
// These are the app's own colours. Material You (dynamic colour) is deliberately
|
||||
// These are the app's own colors. Material You (dynamic color) is deliberately
|
||||
// not used; ADR 0032 §10 has the reasoning, but the short form is that this
|
||||
// palette assigns *meaning* by hue — cobalt is energised, green is present, amber
|
||||
// is attention, red is a fault — and a wallpaper-derived `primary` reassigns the
|
||||
// hue out from under the meaning. A green wallpaper would make "selected" and
|
||||
// "present" the same colour.
|
||||
// "present" the same color.
|
||||
//
|
||||
// Every ratio quoted below is measured against that palette's `bgWindow`.
|
||||
|
||||
/**
|
||||
* One ground: every colour role the app names, for a single theme.
|
||||
* One ground: every color role the app names, for a single theme.
|
||||
*
|
||||
* Adding a role here forces both [Charcoal] and [Slate] to answer for it, which is the point — a
|
||||
* role that only one ground defines is a role that will look wrong on the other.
|
||||
@@ -44,7 +44,7 @@ data class SynPalette(
|
||||
val borderDefault: Color,
|
||||
/** The stronger rule — under a table header, around a card. */
|
||||
val borderCard: Color,
|
||||
/** Body text. Cool light grey on Charcoal, never white. */
|
||||
/** Body text. Cool light gray on Charcoal, never white. */
|
||||
val textPrimary: Color,
|
||||
/** Secondary information, which is the main texture of the app. */
|
||||
val textSecondary: Color,
|
||||
|
||||
@@ -16,7 +16,7 @@ import androidx.compose.ui.graphics.Color
|
||||
* The palette in force, reachable by its ADR 0032 role name.
|
||||
*
|
||||
* Material's slots cover most of what screens need and should be preferred where they fit —
|
||||
* `colorScheme.onSurfaceVariant` says "secondary text on a card" more precisely than a raw colour
|
||||
* `colorScheme.onSurfaceVariant` says "secondary text on a card" more precisely than a raw color
|
||||
* does. This is for the roles Material has no slot for at all: success, warning, the third text
|
||||
* tier, and the media canvas.
|
||||
*/
|
||||
@@ -29,17 +29,17 @@ object SynTheme {
|
||||
}
|
||||
|
||||
/**
|
||||
* Projects one of ADR 0032's grounds onto Material 3's colour slots.
|
||||
* Projects one of ADR 0032's grounds onto Material 3's color slots.
|
||||
*
|
||||
* Two mappings here are deliberate and worth knowing about:
|
||||
*
|
||||
* The **tonal families collapse**. Material offers primary, secondary and tertiary as three
|
||||
* distinguishable accents; ADR 0032 §4 says no colour ever encodes a category, so all three resolve
|
||||
* distinguishable accents; ADR 0032 §4 says no color ever encodes a category, so all three resolve
|
||||
* to the one accent. A screen reaching for `tertiary` to look different gets the accent, which is
|
||||
* the intended answer.
|
||||
*
|
||||
* The **containers are not tinted**. `errorContainer` is a plain card with error-coloured text on
|
||||
* it rather than a red-washed panel, because §5 asks for hairlines and gutters instead of tinted
|
||||
* The **containers are not tinted**. `errorContainer` is a plain card with error-colored text on it
|
||||
* rather than a red-washed panel, because §5 asks for hairlines and gutters instead of tinted
|
||||
* regions. `primaryContainer` is the one exception, and is [SynPalette.accentFill] — that is the
|
||||
* selection block of §5, which is meant to be saturated.
|
||||
*
|
||||
@@ -106,9 +106,9 @@ private val CharcoalScheme = scheme(Charcoal, dark = true)
|
||||
private val SlateScheme = scheme(Slate, dark = false)
|
||||
|
||||
/**
|
||||
* The chrome scheme [Page.pageColorScheme] recolours for a reading surface — chosen by the page's
|
||||
* The chrome scheme [Page.pageColorScheme] recolors for a reading surface — chosen by the page's
|
||||
* own lightness rather than the app's active theme, mirroring `Theme::on_page`'s `source` (a light
|
||||
* page keeps light-mode status/syntax colours even inside a Charcoal-chrome window, or they'd be
|
||||
* page keeps light-mode status/syntax colors even inside a Charcoal-chrome window, or they'd be
|
||||
* unreadable against it).
|
||||
*/
|
||||
internal fun materialBaseScheme(dark: Boolean): ColorScheme =
|
||||
@@ -117,7 +117,7 @@ internal fun materialBaseScheme(dark: Boolean): ColorScheme =
|
||||
/**
|
||||
* The app's theme.
|
||||
*
|
||||
* There is no dynamic-colour path. See the note at the top of Color.kt and ADR 0032 §10 — briefly,
|
||||
* There is no dynamic-color path. See the note at the top of Color.kt and ADR 0032 §10 — briefly,
|
||||
* this palette encodes state in hue, and a wallpaper-derived accent reassigns the hue without
|
||||
* reassigning the meaning.
|
||||
*/
|
||||
|
||||
@@ -5,7 +5,7 @@ import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
/**
|
||||
* What greys out the player's Previous/Next. The rule the buttons depend on is "there is a track
|
||||
* What grays out the player's Previous/Next. The rule the buttons depend on is "there is a track
|
||||
* that way", which has three distinct no-answers: no surrounding list at all, the top of one, and
|
||||
* the bottom of one.
|
||||
*/
|
||||
@@ -15,7 +15,7 @@ class AdjacencyTest {
|
||||
|
||||
/** Playing one song with nothing around it — a folder of one, a track opened on its own. */
|
||||
@Test
|
||||
fun `a track with no surrounding list has no neighbours`() {
|
||||
fun `a track with no surrounding list has no neighbors`() {
|
||||
val alone = tracks("a").first()
|
||||
val adjacency = adjacencyOf(alone, emptyList())
|
||||
assertFalse(adjacency.previous)
|
||||
@@ -44,7 +44,7 @@ class AdjacencyTest {
|
||||
* screen's own selection gone must not offer a step it can't take.
|
||||
*/
|
||||
@Test
|
||||
fun `a track that is not in the list has no neighbours`() {
|
||||
fun `a track that is not in the list has no neighbors`() {
|
||||
val adjacency = adjacencyOf(null, tracks("a", "b"))
|
||||
assertFalse(adjacency.previous)
|
||||
assertFalse(adjacency.next)
|
||||
|
||||
@@ -75,7 +75,7 @@ check:
|
||||
#
|
||||
# The crop is measured, not eyeballed: the white mark occupies 412x451 at
|
||||
# +180+148 of the 775x771 source (`magick ... -threshold 60% -format %@`), so a
|
||||
# 578px square centred on it leaves the mark filling ~78% of the frame. The
|
||||
# 578px square centered on it leaves the mark filling ~78% of the frame. The
|
||||
# uncropped source has enough dead navy around the mark that a 16px toolbar
|
||||
# icon rendered as a smudge with a dot in the middle.
|
||||
[doc: "Re-render browser-extension/src/icons/ from assets/synchronicity-icon.png"]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
* Popup styling.
|
||||
*
|
||||
* Colours are declared once as `light-dark()` pairs and the page opts into
|
||||
* Colors are declared once as `light-dark()` pairs and the page opts into
|
||||
* `color-scheme: light dark`, so the popup follows the browser's theme without
|
||||
* a media query duplicating the palette.
|
||||
*/
|
||||
@@ -221,7 +221,7 @@ button {
|
||||
color: var(--fg-muted);
|
||||
line-height: 1.4;
|
||||
|
||||
/* Two lines of a clipping is enough to recognise it; the node has the rest. */
|
||||
/* Two lines of a clipping is enough to recognize it; the node has the rest. */
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
|
||||
@@ -21,7 +21,7 @@ const PUBKEY_PREFIX_LENGTH = 12;
|
||||
* How much of this browser's own id the collapsed form shows.
|
||||
*
|
||||
* Shorter than a node's, and deliberately: a uuid's first block is already a
|
||||
* distinct-looking chunk, and this is here to be *recognised* against the row
|
||||
* distinct-looking chunk, and this is here to be *recognized* against the row
|
||||
* the node's Settings screen lists it under, not read out.
|
||||
*/
|
||||
const CLIENT_ID_PREFIX_LENGTH = 8;
|
||||
|
||||
@@ -30,7 +30,7 @@ point somewhere else.
|
||||
|
||||
## Why it reads the upstream packages
|
||||
|
||||
Both candidate icon sets are extracted from the exact upstream artefacts the
|
||||
Both candidate icon sets are extracted from the exact upstream artifacts the
|
||||
app builds against — `lucide-static` at the version
|
||||
`rust/gui-app/scripts/vendor-icons.sh` pins, and `@material-design-icons/svg`,
|
||||
the SVG mirror of `androidx.compose.material:material-icons-extended`. A name
|
||||
|
||||
@@ -424,7 +424,7 @@ def grid_panels(theme: dict, lucide: IconSet) -> str:
|
||||
"The 8px <code>CARD_TEXT_GAP</code> between cover and title is already "
|
||||
"empty. A 2px rule there costs no layout, touches no pixel of the artwork, "
|
||||
"and carries the broken-outline idea at 168px instead of 8 — where "
|
||||
"solid-vs-dashed is unmistakable rather than a judgement call.",
|
||||
"solid-vs-dashed is unmistakable rather than a judgment call.",
|
||||
card_row(theme, rule_t),
|
||||
))
|
||||
|
||||
@@ -454,7 +454,7 @@ def grid_panels(theme: dict, lucide: IconSet) -> str:
|
||||
theme, "5 · Rule and word together", "recommended",
|
||||
"The rule is what you see while scanning a grid at arm’s length; the "
|
||||
"word is what you read when one card has caught your eye. Neither is "
|
||||
"load-bearing alone, so the colour-only worry goes away — and the same "
|
||||
"load-bearing alone, so the color-only worry goes away — and the same "
|
||||
"pairing degrades cleanly to just the word in a list row, and to just the "
|
||||
"dot in the sidebar, where there is no room for either of the other two.",
|
||||
card_row(theme, both_t),
|
||||
|
||||
@@ -63,7 +63,7 @@ identity the rest of the app doesn't yet know it has.
|
||||
4. **Danger changes hue between modes** — orange in light, mustard in dark —
|
||||
while red means "Absent." Danger should be one idea.
|
||||
5. **Two "selected" weights, not just two hues.** Nav selection is a loud
|
||||
filled pill; the selected note in the Notes list is a barely-there grey.
|
||||
filled pill; the selected note in the Notes list is a barely-there gray.
|
||||
6. **No measure.** Detail's content is pinned to a ~530 px column with ~1400 px
|
||||
of nothing beside it; Settings puts each label at far left and its segmented
|
||||
control at far right of a 1900 px row. Both symptoms of one missing rule: a
|
||||
@@ -71,7 +71,7 @@ identity the rest of the app doesn't yet know it has.
|
||||
7. **Covers in boxes.** Every comic thumbnail gets a hairline border in both
|
||||
modes; captions truncate hard with no ellipsis or fade. Art should float.
|
||||
8. **The sidebar is an alphabetical list of features.** "Command Palette" (an
|
||||
action) sits beside "Comics & Manga" (a place); "Media" is greyed with no
|
||||
action) sits beside "Comics & Manga" (a place); "Media" is grayed with no
|
||||
visible reason; no grouping, counts, or hierarchy. The single biggest
|
||||
"made of features rather than of a point of view" signal.
|
||||
9. **Redundant type signal** in Documents: the `MUS` badge (a nice small idea)
|
||||
@@ -145,7 +145,7 @@ identity (§5).
|
||||
|
||||
Recorded verbatim in spirit so the reasoning survives the session.
|
||||
|
||||
- **Color:** likes charcoal greys and blues — particularly **deep, saturated
|
||||
- **Color:** likes charcoal grays and blues — particularly **deep, saturated
|
||||
cobalt** — for the way it evokes *lines of electricity energizing circuits*.
|
||||
Doesn't hate the current lilac in isolation, but it isn't right for what
|
||||
Synchronicity should be. Wants **more color, and more saturated color**, not
|
||||
@@ -183,18 +183,18 @@ in the palette specimen, not yet a rule.
|
||||
one: selected, focused, connected, in flight, playing. When nothing is
|
||||
happening there is no cobalt — the panel is at rest. This is Direction A's
|
||||
"accent only for state" with a reason a user can feel, and it is literally
|
||||
what the app does (signal travelling between nodes). It also removes the lilac
|
||||
what the app does (signal traveling between nodes). It also removes the lilac
|
||||
at the root: a pastel *tint* of the accent used as a background is the accent
|
||||
with the current turned off. **Selection is a saturated block with light text
|
||||
on it, never a tint.**
|
||||
|
||||
**What LCARS does mechanically** (the transferable parts, no cosplay):
|
||||
|
||||
- Saturated color only ever sits on black/near-black — never on white or grey.
|
||||
- Saturated color only ever sits on black/near-black — never on white or gray.
|
||||
That is why the saturation works and never looks garish. Transfer: saturated
|
||||
hues live on charcoal, full stop.
|
||||
- Color is *structural*, not decorative: flat blocks are the layout, framing
|
||||
and labelling regions. Modern equivalent: reversed selection, section
|
||||
and labeling regions. Modern equivalent: reversed selection, section
|
||||
headers as filled bars.
|
||||
- Shape rule: round-ended bars for controls and frames, hard rectangles for
|
||||
content — the same line Direction A drew (rounded controls, sharp
|
||||
@@ -232,7 +232,7 @@ instrument-panel pairing — cockpits, VU meters), **red** for destructive /
|
||||
absent, **green** for present. One hue per meaning replaces the
|
||||
orange-vs-mustard drift.
|
||||
|
||||
**Text on charcoal is cool light grey, not white**; mono, tabular, small for
|
||||
**Text on charcoal is cool light gray, not white**; mono, tabular, small for
|
||||
every machine value. (Aviation-display discipline — see `REFERENCES.md`.)
|
||||
|
||||
**Structure by hairline and gutter, not by tinted panel.** Already the app's
|
||||
@@ -330,7 +330,7 @@ placements earn it:
|
||||
least not on the Ensemble screen, and the reason is a terminology collision
|
||||
worth keeping on the record.
|
||||
|
||||
**Icons: fewer, bigger, labelled.** The most consistent thread across the whole
|
||||
**Icons: fewer, bigger, labeled.** The most consistent thread across the whole
|
||||
document — Sublime Merge praised specifically for icon size, clarity and
|
||||
spacing ("many apps I use make the icons too small and too hard to guess at a
|
||||
glance"); Blender, Zed, Linear and Obsidian each criticised for the opposite;
|
||||
@@ -347,13 +347,13 @@ Bloomberg is too dense, and the stated reason is telling — *"I don't have an
|
||||
immediate sense of what pieces of data different parts of the UI are for."*
|
||||
That is not a row-height complaint, it is a grouping complaint. So: dense rows,
|
||||
tight controls, and generous **dark gutters between regions**, each region
|
||||
labelled. Exactly the LCARS negative-space mechanic from §5, now with an
|
||||
labeled. Exactly the LCARS negative-space mechanic from §5, now with an
|
||||
independent reason to believe it.
|
||||
|
||||
**Flat, and no shadows.** Sublime Merge's text-area drop shadow was the one
|
||||
thing disliked in an otherwise-liked UI. Combined with the LCARS "no
|
||||
skeuomorphism" note in §5: no shadows, no bevels, no glows-as-blur. The
|
||||
"glowing" quality the founder likes in Bitwig comes from *saturated colour on a
|
||||
"glowing" quality the founder likes in Bitwig comes from *saturated color on a
|
||||
dark ground*, not from a blur — a blurred glow would read as the kitsch §5's
|
||||
Tron entry warns about.
|
||||
|
||||
@@ -407,14 +407,14 @@ Three directions, same screens, same content, so the difference is the design
|
||||
and not the data — see [`palette-specimen/README.md`](palette-specimen/README.md):
|
||||
|
||||
1. **Instrument** — the restrained reading of §5. One hue, hairlines, no
|
||||
colour except state. The control: is this too quiet now that "more Bitwig"
|
||||
color except state. The control: is this too quiet now that "more Bitwig"
|
||||
is the calibration?
|
||||
2. **Console** — the Bitwig reading. Same bones, plus a saturated per-type
|
||||
colour family carried as flat blocks, the node lane at full strength, grid
|
||||
color family carried as flat blocks, the node lane at full strength, grid
|
||||
texture. Tests the hue ceiling of open question 3.
|
||||
3. **Signal** — the LCARS reading. Colour as *structure*: filled section bars,
|
||||
3. **Signal** — the LCARS reading. Color as *structure*: filled section bars,
|
||||
round-ended controls against hard rectangles, larger type contrast, wide
|
||||
black gutters. Tests whether "structural colour" survives contact with a
|
||||
black gutters. Tests whether "structural color" survives contact with a
|
||||
real file list, or tips into costume.
|
||||
|
||||
Each renders on both a charcoal ground and a cool-slate ground, which is what
|
||||
@@ -480,16 +480,16 @@ and is an improvement on what ships today; **Console is the preferred reading
|
||||
in dark mode**, and Signal crosses into costume on a charcoal documents list
|
||||
("star trek fanart") while reading much straighter on slate.
|
||||
|
||||
**Per-type colour is out.** *"I don't want to use particular colors to indicate
|
||||
**Per-type color is out.** *"I don't want to use particular colors to indicate
|
||||
particular types of document — there are too many types of document for that to
|
||||
be useful."* Decisive, and it removes the whole `--h-*` family. Document type
|
||||
becomes one more column of plain text, and the coloured edge beside it goes
|
||||
becomes one more column of plain text, and the colored edge beside it goes
|
||||
too. This restates open question 3 rather than answering it: the hue ceiling is
|
||||
now a question about **state and activity**, which is where Console's saturation
|
||||
has been re-pointed — presence, transfer, job meters, the activity track.
|
||||
|
||||
**And colour follows the same rule inside the lane.** Filling the *present*
|
||||
cell — the state 90% of cells are in — spends the entire lane's colour budget
|
||||
**And color follows the same rule inside the lane.** Filling the *present*
|
||||
cell — the state 90% of cells are in — spends the entire lane's color budget
|
||||
saying nothing, which is the type-hue mistake in miniature. Present is now
|
||||
quiet; the eye catches the exception: an empty cell where a copy is missing,
|
||||
cobalt where one is in flight, amber where a node has gone quiet. This also
|
||||
@@ -549,7 +549,7 @@ That reframing raises the stakes on the token architecture rather than lowering
|
||||
them: a preset is only a preset if every one of these differences is expressible
|
||||
as tokens, and some of them currently are not — icons on or off in the rail,
|
||||
pill versus block selection, and the section-heading grammar are structural,
|
||||
not colour. Either they become tokens too (a shape/role layer above
|
||||
not color. Either they become tokens too (a shape/role layer above
|
||||
`theme.rs`), or presets are limited to palette and the structural choices are
|
||||
made once, for all of them. That is a decision for the ADR.
|
||||
|
||||
@@ -611,12 +611,12 @@ that got there. In brief:
|
||||
explicit rule about what may be written in it.
|
||||
|
||||
**On the question §8.6 left hanging** — what "structural differences as tokens"
|
||||
means: three of the things separating the directions are not colour and not
|
||||
means: three of the things separating the directions are not color and not
|
||||
size, so `theme.rs` cannot express them. Rail icons on or off, selection as a
|
||||
full-bleed block or an inset pill, and the section-heading grammar are
|
||||
component structure. So either a second token layer above `theme.rs` describes
|
||||
shape and component style, or user-selectable presets are limited to palette.
|
||||
ADR 0032 §9 takes the second: presets recolour, structure is decided once. The
|
||||
ADR 0032 §9 takes the second: presets recolor, structure is decided once. The
|
||||
cost is that Signal — whose entire argument is structural — cannot ship as a
|
||||
preset without that layer, which is why it is deferred rather than rejected.
|
||||
`sizing::shape` is still worth building now, because `CRITIQUE.md` finding 2
|
||||
|
||||
@@ -32,7 +32,7 @@ it harder than it looks:
|
||||
red/green pairing most status UIs reach for first.
|
||||
4. **Nodes have human-chosen names**, and abbreviating one automatically to fit
|
||||
a column header invents a short name the user didn't pick. Any per-node
|
||||
column has to solve labelling, not just cells.
|
||||
column has to solve labeling, not just cells.
|
||||
5. **The interesting question is usually not "where is it" but "is it safe"** —
|
||||
is there more than one copy, is the only copy on the node most likely to be
|
||||
lost, is a copy on its way here.
|
||||
@@ -100,7 +100,7 @@ this problem actually does.
|
||||
- **Mechanism:** workload × node placement, usually as a table with node names
|
||||
in cells rather than a grid of cells, precisely because node names are long
|
||||
and the count varies.
|
||||
- **Why it's here:** the labelling problem (constraint 4) solved by not trying
|
||||
- **Why it's here:** the labeling problem (constraint 4) solved by not trying
|
||||
to make node a column.
|
||||
- **Reaction:**
|
||||
|
||||
@@ -137,7 +137,7 @@ Stated so the reading has something to argue with. None is chosen.
|
||||
[`DIRECTION.md`](DIRECTION.md) §8.6): **B in ordinary lists** — one indicator
|
||||
for this node, which is the bit the user can act on in place — **A on a
|
||||
dedicated `Presence` view**, where a column per node has the room to be
|
||||
labelled in full and the node set becomes a control rather than a layout
|
||||
labeled in full and the node set becomes a control rather than a layout
|
||||
constraint, **and C beside it** as a `Copies` fraction that turns amber at one.
|
||||
The full per-node picture also stays on the Detail view, which has both the
|
||||
room and the user's attention. Nothing here is settled; the point is that the
|
||||
@@ -145,7 +145,7 @@ place a better answer would change is now one view and one column, not the
|
||||
app's primary list.
|
||||
|
||||
E is the one most consistent with everything else the redesign decided —
|
||||
colour and marks for the exception, silence for the ordinary case — and it is
|
||||
color and marks for the exception, silence for the ordinary case — and it is
|
||||
also the only one that survives a twelve-node ensemble. It costs the ability to
|
||||
answer "where is this" by scanning, which C recovers cheaply.
|
||||
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
Software (and a few non-software things) worth spending time looking at before
|
||||
the [`palette-specimen/`](palette-specimen/) exploration, tuned to the
|
||||
preferences in [`DIRECTION.md`](DIRECTION.md) §4: charcoal + saturated cobalt,
|
||||
dense, hairline-split, developer-flavoured, one dark-leaning style, paper for
|
||||
dense, hairline-split, developer-flavored, one dark-leaning style, paper for
|
||||
reading — and *not* pastel, not desaturated white-on-slate, not turquoise, not
|
||||
libadwaita, not evocative of Discord.
|
||||
|
||||
How to look: **separate palette from bones.** Palette is the loudest thing in
|
||||
any screenshot and the cheapest to swap. For each, ask "recoloured in my
|
||||
any screenshot and the cheapest to swap. For each, ask "recolored in my
|
||||
charcoal + cobalt, would I like the structure?" — row rhythm, how sections are
|
||||
separated, where controls sit relative to labels, fill vs. hairline, how
|
||||
selection is shown. Each entry says what to look at, what to ignore, and why
|
||||
@@ -24,15 +24,15 @@ theme" / "screenshot" / "UI".
|
||||
- **Look at:** the hairline area splits (already imitated in the app); every
|
||||
editor area having its own thin header strip; the Properties editor's
|
||||
vertical icon-tab rail; the Outliner as a dense tree with tiny icons; how a
|
||||
*single* bright colour (orange in the viewport, blue in lists/buttons) does
|
||||
*single* bright color (orange in the viewport, blue in lists/buttons) does
|
||||
all selection work on uniform charcoal; tiny type at high density.
|
||||
- **Ignore:** the viewport itself; the sheer number of panels.
|
||||
- **Why:** the founder's stated positive reference; the closest existing
|
||||
"dense instrument on charcoal with hairlines" and a dark-only precedent.
|
||||
- **Reaction:** I've used blender for many years, although I'm only an amateur hobbyist 3d modeler; I'm definitely not
|
||||
an expert in all areas of the program. In general I like the multiple-types-of-charcoal-grey color scheme of the program, along with
|
||||
an expert in all areas of the program. In general I like the multiple-types-of-charcoal-gray color scheme of the program, along with
|
||||
the relatively bright saturated colors of the small icons for various interactable widgets. I also have always liked the design of
|
||||
node editors - grey boxes full of white numbers, with judicious use of relatively-satured bright colors for meter bars and titles,
|
||||
node editors - gray boxes full of white numbers, with judicious use of relatively-satured bright colors for meter bars and titles,
|
||||
white text on the background (as well as the nice curve in the lines that link nodes in the node editor). I'm not sure if anything in
|
||||
Synchronicity needs something like a node editor, or if the general design of blender is necessarily appropriate for Synchronicity
|
||||
given the very different functions of the two programs. But yeah on the whole I feel good about blender.
|
||||
@@ -58,8 +58,8 @@ I tend to turn that on. The icons also strike me as too big. I would say that th
|
||||
cobalt blue, it's a bit lighter than that. Certainly it's an effective color in that context though.
|
||||
|
||||
### Ableton Live (dark theme) — ableton.com
|
||||
- **Look at:** flat, sharp, dense; *colour blocks as structure* (clips and
|
||||
tracks are labelled by solid colour on charcoal — the most LCARS-like
|
||||
- **Look at:** flat, sharp, dense; *color blocks as structure* (clips and
|
||||
tracks are labeled by solid color on charcoal — the most LCARS-like
|
||||
mechanism in a real tool); how many saturated hues coexist without chaos
|
||||
because the ground is dark and the blocks are flat.
|
||||
- **Ignore:** the specific pastel-ish clip palette in some themes.
|
||||
@@ -85,7 +85,7 @@ synchronicity documents list.
|
||||
|
||||
### Bitwig Studio — bitwig.com
|
||||
- **Look at:** same family as Ableton but more saturated and slightly rounder;
|
||||
a good second data point on where "more colour" starts to feel like too
|
||||
a good second data point on where "more color" starts to feel like too
|
||||
much; its device panels as bordered regions.
|
||||
- **Why:** calibrates saturation against Ableton.
|
||||
- **Reaction:** also software I've never used before. Immediate gut reaction: I like it! I like it more than ableton!
|
||||
@@ -161,7 +161,7 @@ interface.
|
||||
|
||||
### Roon and Plexamp — roonlabs.com, plex.tv/plexamp
|
||||
- **Look at:** how cover art floats on near-black with *no borders*; type over
|
||||
and beside images; chrome that recedes; Plexamp's use of a saturated colour
|
||||
and beside images; chrome that recedes; Plexamp's use of a saturated color
|
||||
pulled from the art as ambient accent.
|
||||
- **Ignore:** Roon's occasional big-serif marketing pages.
|
||||
- **Why:** the media grid (Comics, Music, Pictures) currently boxes every
|
||||
@@ -177,7 +177,7 @@ interface.
|
||||
|
||||
### Zathura and Sioyek — pwmt.org/projects/zathura, sioyek.info
|
||||
- **Look at:** "dark chrome, paper page" as the *entire* reading-surface
|
||||
identity; how little chrome a PDF reader needs; Sioyek's dev-flavoured
|
||||
identity; how little chrome a PDF reader needs; Sioyek's dev-flavored
|
||||
overlays (dark, monospace, keyboard-driven).
|
||||
- **Why:** the paper/reading half of the direction, and evidence the serif
|
||||
question is independent of it.
|
||||
@@ -189,12 +189,12 @@ This desideratum needs PDF-document-specific feature work to get right, but if a
|
||||
trying to be particularly minimal with the PDF reader chrome.
|
||||
|
||||
### Aviation primary flight displays (Garmin G1000 / airliner PFD)
|
||||
- **Look at:** the *semantic colour discipline*: magenta = active flight plan,
|
||||
- **Look at:** the *semantic color discipline*: magenta = active flight plan,
|
||||
cyan = data, green = active mode, white = armed, amber = caution, red =
|
||||
warning — strict, never decorative, on black. Dense, small type, everything
|
||||
labelled, tape and numeric texture.
|
||||
labeled, tape and numeric texture.
|
||||
- **Ignore:** the specific hues (magenta/cyan aren't the target).
|
||||
- **Why:** the model for "one semantic set, one personality hue" — colour that
|
||||
- **Why:** the model for "one semantic set, one personality hue" — color that
|
||||
means something, every time.
|
||||
- **Reaction:** I really dont' know enough about flying planes to judge how effective this UI is.
|
||||
I guess consistent color discipline is a good principle, I'm not necessarily fond of their specific
|
||||
@@ -203,7 +203,7 @@ design inspiration this plane UI should be, one way or the other.
|
||||
|
||||
### Bloomberg Terminal
|
||||
- **Look at:** saturated amber/cobalt/white on black; extreme density; numbers
|
||||
as texture; colour = data type. Beloved by its users precisely for density.
|
||||
as texture; color = data type. Beloved by its users precisely for density.
|
||||
- **Ignore:** the ugliness of the widgets themselves; the amber-first palette.
|
||||
- **Why:** the far end of the density axis — useful for calibrating how far
|
||||
the machine surfaces can go.
|
||||
@@ -216,7 +216,7 @@ of what pieces of data different parts of the UI are for.
|
||||
### LCARS, late-Voyager / DS9 era — trekcore.com screencaps
|
||||
- **Look at:** *Voyager's astrometrics lab* (the blue-heavy LCARS set) and
|
||||
late-series bridge/engineering displays vs. season-1 TNG. Note: saturated
|
||||
colour only on black; flat blocks as layout; round-ended bars for controls,
|
||||
color only on black; flat blocks as layout; round-ended bars for controls,
|
||||
hard rectangles for content; small dense text and numbers; large black
|
||||
gutters between blocks.
|
||||
- **Ignore:** the elbows, the all-caps condensed face, anything that would
|
||||
@@ -239,7 +239,7 @@ with actors wearing colored reflective tape on dark fabric bodysuits on a dimly-
|
||||
honestly it looks to me to be pretty similar to any other 2000s video game that was trying to depict futuristic computers.
|
||||
|
||||
### Destiny 2 menus, No Man's Sky UI
|
||||
- **Look at:** dense, saturated colour on dark; sharp geometric containers;
|
||||
- **Look at:** dense, saturated color on dark; sharp geometric containers;
|
||||
believable-future computer feel; how much text and numbers they carry.
|
||||
- **Why:** game UIs are the richest source of "saturated, dense, dark,
|
||||
believable" — the same brief LCARS answered.
|
||||
|
||||
@@ -34,12 +34,12 @@ falls back to system faces and the layout still reads; only the type does not.
|
||||
|
||||
| | What it pushes | What it is testing |
|
||||
| --- | --- | --- |
|
||||
| **1 — Instrument** | one hue, hairlines, no colour except state | whether the restrained reading is now too quiet, given "more Bitwig" as the saturation calibration |
|
||||
| **1 — Instrument** | one hue, hairlines, no color except state | whether the restrained reading is now too quiet, given "more Bitwig" as the saturation calibration |
|
||||
| **2 — Console** | saturation spent on state and activity, on a ruled ground | the hue ceiling — open question 3, restated now that per-type hues are out |
|
||||
| **3 — Signal** | colour as structure: filled round-ended section bars, pill controls, hard-rectangle containers, wide gutters | whether structural colour survives a real file list, or tips into costume |
|
||||
| **3 — Signal** | color as structure: filled round-ended section bars, pill controls, hard-rectangle containers, wide gutters | whether structural color survives a real file list, or tips into costume |
|
||||
|
||||
Document type is a plain text column in all three: there are far too many types
|
||||
for a hue each to mean anything, and colour is reserved for state. The rails
|
||||
for a hue each to mean anything, and color is reserved for state. The rails
|
||||
differ per direction — a nav column identical across three directions would be
|
||||
evidence that none of them is pushing anything — and the slot on the right of
|
||||
each row carries live agent state rather than a count of everything the agent
|
||||
@@ -51,13 +51,13 @@ Two further mechanisms appear in all three:
|
||||
indicator per row, for this node only — here, arriving, not here — because
|
||||
that is the bit you act on without leaving the row. The survey question,
|
||||
*where is everything across the ensemble*, gets its own `Presence` tab on
|
||||
Documents, where a column per node has room to be labelled in full, the node
|
||||
Documents, where a column per node has room to be labeled in full, the node
|
||||
set is a control rather than a layout constraint, and two filters (*missing
|
||||
somewhere*, *only one copy left*) answer what people actually arrive with.
|
||||
A quiet filled cell is present, an empty one is a missing copy, hatched
|
||||
cobalt is in flight, an amber outline is a node gone quiet — present takes no
|
||||
colour, because a saturated fill on the state most cells are in spends the
|
||||
whole colour budget saying nothing. Whether cells are the right affordance at
|
||||
color, because a saturated fill on the state most cells are in spends the
|
||||
whole color budget saying nothing. Whether cells are the right affordance at
|
||||
all is still open: see
|
||||
[`../PRESENCE-AFFORDANCE.md`](../PRESENCE-AFFORDANCE.md). Home carries the
|
||||
same cell grammar over time, as a 24-hour activity track.
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
<style>
|
||||
/* ==========================================================================
|
||||
Specimen shell — deliberately hue-neutral so it cannot contaminate the
|
||||
judgement of the cobalt mockups it frames. Single visual world (a dark
|
||||
gallery wall); every colour is painted explicitly.
|
||||
judgment of the cobalt mockups it frames. Single visual world (a dark
|
||||
gallery wall); every color is painted explicitly.
|
||||
========================================================================== */
|
||||
:root{
|
||||
--wall:#0b0b0c; --wall-2:#151517; --wall-3:#1d1d20;
|
||||
@@ -242,13 +242,13 @@ a{color:inherit}
|
||||
[data-dir="signal"] .sec .cnt{background:var(--bg2);padding:3px 14px;height:19px;
|
||||
border-radius:0 var(--radius-pill) var(--radius-pill) 0;line-height:13px}
|
||||
|
||||
/* ---- document type: a column, not a colour. There are far too many types
|
||||
for a hue each to mean anything, and colour is reserved for state. ---- */
|
||||
/* ---- document type: a column, not a color. There are far too many types
|
||||
for a hue each to mean anything, and color is reserved for state. ---- */
|
||||
.ty{color:var(--t1);flex:none;overflow:hidden;text-overflow:ellipsis}
|
||||
|
||||
/* ---- the node lane: an Ableton clip lane whose columns are ensemble nodes ---- */
|
||||
/* ---- Presence: a whole view for the survey, so a column per node can be
|
||||
labelled in full and given room. Not on the main list — see
|
||||
labeled in full and given room. Not on the main list — see
|
||||
../PRESENCE-AFFORDANCE.md. ---- */
|
||||
.pgrid{display:grid;align-items:center;column-gap:0;row-gap:0;min-width:0}
|
||||
.pgrid>*{padding:0 8px;height:var(--row-h);display:flex;align-items:center;
|
||||
@@ -286,8 +286,8 @@ a{color:inherit}
|
||||
.chips .lbl{font-size:var(--fs-small);color:var(--t2);margin-right:2px}
|
||||
.lane{display:grid;grid-auto-flow:column;grid-auto-columns:26px;gap:var(--lane-gap);
|
||||
align-items:center;flex:none}
|
||||
/* Present is the ordinary case and takes no colour at all: a saturated fill on
|
||||
the common state would spend the whole lane's colour budget saying nothing.
|
||||
/* Present is the ordinary case and takes no color at all: a saturated fill on
|
||||
the common state would spend the whole lane's color budget saying nothing.
|
||||
What the eye should catch is the exception — an empty cell where a copy is
|
||||
missing, cobalt where one is moving, amber where a node has gone quiet. */
|
||||
.cell{height:13px;border:1px solid var(--line2);background:transparent;border-radius:1px}
|
||||
@@ -617,13 +617,13 @@ const NAV = [
|
||||
|
||||
const DIRS = [
|
||||
{key:'instrument',name:'1 — Instrument',
|
||||
tag:'hairlines · colour only for state, and quietly',
|
||||
note:'The restrained reading of DIRECTION §5. Presence reads in greys, cobalt appears only where something is in flight, section headings are a label and a rule. The control: with “more Bitwig” as the calibration, is this too quiet?'},
|
||||
tag:'hairlines · color only for state, and quietly',
|
||||
note:'The restrained reading of DIRECTION §5. Presence reads in grays, cobalt appears only where something is in flight, section headings are a label and a rule. The control: with “more Bitwig” as the calibration, is this too quiet?'},
|
||||
{key:'console',name:'2 — Console',
|
||||
tag:'Bitwig calibration · saturation on state and activity',
|
||||
note:'Same bones, on a ruled charcoal ground, with the saturation spent on what is happening rather than on what things are: presence, transfer, job meters, the activity track. Tests how much saturated colour the app can carry before density reads as noise — open question 3, restated now that per-type hues are out.'},
|
||||
note:'Same bones, on a ruled charcoal ground, with the saturation spent on what is happening rather than on what things are: presence, transfer, job meters, the activity track. Tests how much saturated color the app can carry before density reads as noise — open question 3, restated now that per-type hues are out.'},
|
||||
{key:'signal',name:'3 — Signal',
|
||||
tag:'colour as structure · round-ended bars · wide gutters',
|
||||
tag:'color as structure · round-ended bars · wide gutters',
|
||||
note:'The LCARS reading. Section headings become filled round-ended bars, controls take the pill, containers stay hard rectangles, gutters widen and type contrast rises. On charcoal this crosses into costume; on slate it reads much straighter. A candidate for a shipped optional theme rather than the default.'}
|
||||
];
|
||||
|
||||
@@ -674,7 +674,7 @@ function lane(states){
|
||||
}
|
||||
/* In a list, presence reduces to the one bit the user can act on: is it on this
|
||||
node. The whole ensemble's picture lives on the Presence view, where a column
|
||||
per node has the room to be labelled properly. */
|
||||
per node has the room to be labeled properly. */
|
||||
function here(states,variant){
|
||||
return `<div class="here${variant?' on-card':''}"><div class="cell" data-s="${states[0]}"></div></div>`;
|
||||
}
|
||||
@@ -1043,7 +1043,7 @@ SCREENS.parked = () => {
|
||||
|
||||
/* ---------- ensemble -----------------------------------------------------
|
||||
Co-equal peers, so: a roster where every node is the same row, and a
|
||||
reachability matrix that is symmetric by construction. No centre, no
|
||||
reachability matrix that is symmetric by construction. No center, no
|
||||
direction, no user-authored links — see DIRECTION.md 8.5.
|
||||
------------------------------------------------------------------------ */
|
||||
const LINKS = { // unordered pairs; the matrix reads each one twice
|
||||
|
||||
@@ -71,7 +71,7 @@ Consequently any node that could dial one of ours could:
|
||||
|
||||
| ALPN | Capability granted to an arbitrary caller |
|
||||
|---|---|
|
||||
| `/syn/doc-sync/…` | Read the whole document catalogue — plaintext at rest, so plaintext on the wire — **and write to it**: phase 2 of `serve` opens a reverse stream, pulls documents/properties/edges/artifacts/tombstones from the caller, and commits them via `doc_sync::promote_session` |
|
||||
| `/syn/doc-sync/…` | Read the whole document catalog — plaintext at rest, so plaintext on the wire — **and write to it**: phase 2 of `serve` opens a reverse stream, pulls documents/properties/edges/artifacts/tombstones from the caller, and commits them via `doc_sync::promote_session` |
|
||||
| `/syn/artifact-sync/…` | `serve_push` applies arbitrary artifact values through `upsert_artifact` |
|
||||
| `/syn/blob-sync/…` | Fetch any blob's ciphertext; push arbitrary blobs onto our disk |
|
||||
| `/syn/node-admin/…` | `SetSyncPolicy`, `SetDocOverride`, `TriggerSync`, `ListJobs`, `CancelJob` |
|
||||
@@ -422,14 +422,14 @@ strictly-better outcome for a much likelier failure — a phone that has been of
|
||||
for a month should not be able to stop a laptop from retiring.
|
||||
|
||||
**Gossip is not delivery.** `GossipSender::broadcast` returns when iroh-gossip
|
||||
has taken the payload, not when anyone applied it, so an acknowledgement needs
|
||||
has taken the payload, not when anyone applied it, so an acknowledgment needs
|
||||
a request/response: the retiring node dials peers and pushes the record on a
|
||||
stream that replies once it is committed. Gossip still goes out — it is how the
|
||||
*other* nodes hear promptly — but it cannot be what the wipe waits on.
|
||||
|
||||
**The delivery path must not be gated by the deny-list**, or it eats itself:
|
||||
the peer applies the record, and from that instant refuses this node's next
|
||||
connection — so a lost or truncated acknowledgement would be unrecoverable, and
|
||||
connection — so a lost or truncated acknowledgment would be unrecoverable, and
|
||||
the retiring node would conclude failure over a record that had in fact landed.
|
||||
Revocation delivery is therefore served to a revoked caller. That is safe
|
||||
because the protocol's only effect is to accept a signed revocation record,
|
||||
@@ -438,7 +438,7 @@ most a revoked node can achieve on it is to prove itself revoked again. It also
|
||||
makes the push idempotent, so "retry until acknowledged" terminates.
|
||||
|
||||
Within the peer, the order is **commit, then acknowledge**. The other way round
|
||||
risks the retiring node wiping on an acknowledgement for a record the peer then
|
||||
risks the retiring node wiping on an acknowledgment for a record the peer then
|
||||
loses, which is precisely the failure this flow exists to prevent. Committing
|
||||
first fails in the harmless direction — a wasted retry.
|
||||
|
||||
@@ -454,7 +454,7 @@ and no cancel.
|
||||
|
||||
The design first reached for the opposite — apply the record to this node's own
|
||||
deny-list at signing time, which is what would stop it syncing, and let a user
|
||||
who changed their mind back out before the first acknowledgement. Both halves
|
||||
who changed their mind back out before the first acknowledgment. Both halves
|
||||
were wrong. There is nothing to gain from the write: the store that would hold
|
||||
the row is the one the wipe is about to destroy. And there is something to
|
||||
lose — a retirement that never reached a peer would leave the row behind, and
|
||||
@@ -471,9 +471,9 @@ did not retire — nothing was written and, if no peer acknowledged, nobody was
|
||||
told. That is a coherent state to come back to; a durable half-retirement is
|
||||
not.
|
||||
|
||||
**The first acknowledgement is still the point of no return** in the sense that
|
||||
**The first acknowledgment is still the point of no return** in the sense that
|
||||
matters: after it the ensemble knows, and the only coherent thing left is to
|
||||
finish. A node that comes back up between the acknowledgement and the wipe
|
||||
finish. A node that comes back up between the acknowledgment and the wipe
|
||||
finds itself already refused by its peers, and should say so — the roster does,
|
||||
from `this_node_is_revoked` — rather than presenting it as a fault.
|
||||
|
||||
@@ -914,7 +914,7 @@ computing it independently.
|
||||
|
||||
Both are confirmed the same way on both frontends: a modal stating what is
|
||||
destroyed and, in its own paragraph, the half users get wrong — for retire,
|
||||
that the next screen cannot be cancelled; for reset, that nobody else is
|
||||
that the next screen cannot be canceled; for reset, that nobody else is
|
||||
told and a delegation cert never expires, so the other nodes go on admitting
|
||||
this one for ever.
|
||||
|
||||
@@ -948,7 +948,7 @@ computing it independently.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- ~~**Where the acknowledgement protocol lives**~~ **Settled**: its own ALPN,
|
||||
- ~~**Where the acknowledgment protocol lives**~~ **Settled**: its own ALPN,
|
||||
`/syn/membership/…` (`p2p::revocation_delivery`). The deny-list exemption is
|
||||
expressed as an associated const on `EnsembleProtocol`
|
||||
(`REVOKED_PEERS: RevokedPeers`), defaulting to `Refuse` — so a protocol
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# 0009: Two-plane sync — catalogue always replicates, content is policy-gated
|
||||
# 0009: Two-plane sync — catalog always replicates, content is policy-gated
|
||||
|
||||
**Status**: Accepted
|
||||
**Date**: 2026-07-22 (design), phased implementation through 2026-07-26
|
||||
@@ -6,7 +6,7 @@
|
||||
## Context
|
||||
|
||||
Before this design, sync controls were a pile of disconnected, mostly-unwired
|
||||
pieces: manual per-peer catalogue sync, manual per-document blob/artifact
|
||||
pieces: manual per-peer catalog sync, manual per-document blob/artifact
|
||||
pull, a `SyncPolicy` (Stream/Sync) stored in Settings but consumed by
|
||||
nothing, a `SyncDisposition` (per-peer eager push) store with no UI and no
|
||||
consumer, an unwired "Sync continuously" toggle, and an inert "Delete
|
||||
@@ -18,7 +18,7 @@ pressing buttons.
|
||||
|
||||
Split everything a node holds into two planes with different rules:
|
||||
|
||||
1. **Catalogue plane** — document rows, properties, artifact *values*
|
||||
1. **Catalog plane** — document rows, properties, artifact *values*
|
||||
(including `BlobRef` pointer values), tombstones, and presence records.
|
||||
Always replicated to every node, continuously, with no user
|
||||
configuration. This is what makes each node a complete,
|
||||
@@ -28,12 +28,12 @@ Split everything a node holds into two planes with different rules:
|
||||
where storage cost is real. See ADR 0010 for the policy itself.
|
||||
|
||||
The one deliberate upgrade hiding in the split: **artifact values move from
|
||||
the content plane to the catalogue plane.** Note bodies, `ListenProgress`,
|
||||
the content plane to the catalog plane.** Note bodies, `ListenProgress`,
|
||||
`ReadingPosition`, and bookmarks are exactly the data a user wants
|
||||
everywhere fast — small, high-value, cross-device — so the continuous sync
|
||||
pass (ADR 0012) fetches all pending artifact stubs eagerly via the existing
|
||||
`/syn/artifact-sync/…` protocol, rather than waiting for the same on-demand
|
||||
trigger as blob bytes. `Thumbnail`-kind bytes ride the catalogue plane too
|
||||
trigger as blob bytes. `Thumbnail`-kind bytes ride the catalog plane too
|
||||
(size-capped, e.g. skip anything mislabeled that exceeds 1 MiB) since
|
||||
browsing a library without cover art is a degraded experience.
|
||||
|
||||
@@ -41,7 +41,7 @@ browsing a library without cover art is a degraded experience.
|
||||
reworked. Its job — "eagerly push type T to peer P" — is subsumed by a
|
||||
cleaner rule matching the decentralized architecture: every node is
|
||||
responsible for satisfying its own policy, by pulling. When a desktop
|
||||
imports an album, the server learns about it via continuous catalogue sync
|
||||
imports an album, the server learns about it via continuous catalog sync
|
||||
and pulls the audio because *its own* policy says `Mirror(MusicTrack)`. No
|
||||
node needs a table of opinions about what other nodes should receive.
|
||||
Manual "Push now" stays as a separate, genuinely useful affordance (target
|
||||
@@ -50,9 +50,9 @@ about to go offline, NAT-challenged peer) — it just isn't a policy.
|
||||
The Artifact/Blob unification (retiring `Blob`/`BlobRole` as a separate
|
||||
ontology in favor of `ArtifactValue` variants carrying a `BlobRef` pointer,
|
||||
per `docs/data-model.md`) is what makes the plane assignment a type-system
|
||||
fact rather than a convention: the catalogue plane is simply *all artifact
|
||||
fact rather than a convention: the catalog plane is simply *all artifact
|
||||
values*, and the content plane is the bytes those pointers address. Nothing
|
||||
large can land on the catalogue plane without someone deliberately declaring
|
||||
large can land on the catalog plane without someone deliberately declaring
|
||||
a new inline kind with a large payload — a code-review-visible act.
|
||||
|
||||
## Consequences
|
||||
|
||||
@@ -6,7 +6,7 @@ TUI/CLI 2026-07-30, every surface runner-backed 2026-08-23
|
||||
|
||||
## Context
|
||||
|
||||
Every long-running operation — imports, catalogue sync, content fetch, push,
|
||||
Every long-running operation — imports, catalog sync, content fetch, push,
|
||||
eviction, purge — had its own bespoke, per-widget async state
|
||||
(`AsyncActionState`), with no shared place to see what was running, no
|
||||
history of what finished or failed, and no way for one widget to know that
|
||||
@@ -22,11 +22,11 @@ A single `Job` model, shared by every modality, lives in `lib`:
|
||||
```rust
|
||||
Job {
|
||||
id: Uuid,
|
||||
kind: JobKind, // Import | CatalogueSync | ContentFetch | Push
|
||||
kind: JobKind, // Import | CatalogSync | ContentFetch | Push
|
||||
// | Eviction | Purge
|
||||
subject: JobSubject, // human label + optional document id / peer pubkey
|
||||
origin: JobOrigin, // User | Scheduler | RemoteAdmin(NodePubkey)
|
||||
state: JobState, // Queued | Running | Done | Failed(String) | Cancelled
|
||||
state: JobState, // Queued | Running | Done | Failed(String) | Canceled
|
||||
progress: Option<JobProgress>,
|
||||
created_at / started_at / finished_at: Timestamp,
|
||||
}
|
||||
@@ -71,16 +71,16 @@ scheduler-triggered work specifically — a scheduler tick and a manual
|
||||
`cancel_job` (added 2026-08-09) goes through that same scheduler-bound
|
||||
`JobRunner`, so it's best-effort in the same lopsided way dedup is: a job
|
||||
only has a live cancellation token if it was started through that runner —
|
||||
`BackgroundSync`, and scheduler-originated `CatalogueSync`/`ContentFetch` (the
|
||||
`BackgroundSync`, and scheduler-originated `CatalogSync`/`ContentFetch` (the
|
||||
per-peer/per-pass sub-jobs `scheduler::run_all_passes` starts, each checking
|
||||
`JobHandle::is_cancelled` at its own granularity). The same two kinds
|
||||
`JobHandle::is_canceled` at its own granularity). The same two kinds
|
||||
triggered manually (`sync_documents`/`sync_document_assets`, which use
|
||||
`run_as_job`) aren't cancellable, nor is anything else `run_as_job` wraps
|
||||
`run_as_job`) aren't cancelable, nor is anything else `run_as_job` wraps
|
||||
(`Import`, `Push`, eviction, purge). The FFI method returns a plain `bool` —
|
||||
`true` if a token was found and flipped, `false` otherwise, with no attempt
|
||||
to distinguish "unknown id" from "no live runner behind it" — and the Jobs
|
||||
screen shows a Cancel button on every active row but surfaces a `false`
|
||||
result as "can't be cancelled" rather than silently reloading, matching
|
||||
result as "can't be canceled" rather than silently reloading, matching
|
||||
desktop's row (which always has a token, since desktop's single `JobRunner`
|
||||
backs every trigger).
|
||||
|
||||
@@ -125,11 +125,11 @@ Three things did *not* change, and are worth stating so the gap isn't
|
||||
rediscovered as a bug:
|
||||
|
||||
- **How promptly a cancel lands is a property of the work, not of the job.**
|
||||
Work that is handed a `ProgressReporter` and checks `is_cancelled` between
|
||||
Work that is handed a `ProgressReporter` and checks `is_canceled` between
|
||||
units stops there — the daemon's `ImportFiles` (between paths, and between
|
||||
the files inside a directory), Android's batch import, the scheduler passes.
|
||||
A single blocking P2P call — a manual pull, a push, an eviction — runs to
|
||||
the end and is *then* recorded as `Cancelled`. That is the same granularity
|
||||
the end and is *then* recorded as `Canceled`. That is the same granularity
|
||||
desktop's own document-detail buttons have had since this ADR was written;
|
||||
making it finer means threading cancellation into `asset_sync`, which is its
|
||||
own change.
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
## Context
|
||||
|
||||
Before this, every part of the two-plane promise (ADR 0009) required a
|
||||
button press: catalogue sync, artifact/thumbnail fetch, and content fetch
|
||||
button press: catalog sync, artifact/thumbnail fetch, and content fetch
|
||||
all ran only when the user triggered them. Nothing made the promise "every
|
||||
node has a complete catalogue view, and holds content per its own policy"
|
||||
node has a complete catalog view, and holds content per its own policy"
|
||||
true in the background.
|
||||
|
||||
## Decision
|
||||
@@ -16,9 +16,9 @@ true in the background.
|
||||
A background component in `lib`, driven differently per modality, runs
|
||||
three passes:
|
||||
|
||||
1. **Catalogue pass** — `sync_from_peer` (incremental, `since` = last
|
||||
1. **Catalog pass** — `sync_from_peer` (incremental, `since` = last
|
||||
successful sync per peer) against each reachable ensemble peer.
|
||||
2. **Catalogue-plane fetch pass** — after a catalogue pass, fetch all
|
||||
2. **Catalog-plane fetch pass** — after a catalog pass, fetch all
|
||||
pending artifact stubs (`/syn/artifact-sync/…`) and missing thumbnail
|
||||
bytes (existing blob-sync) from whichever peers hold them.
|
||||
3. **Content pass** — compute **wants**: blob hashes where the resolution
|
||||
@@ -33,7 +33,7 @@ copy. Every peer sync and every content fetch goes through `JobRunner`
|
||||
exactly like a manual trigger, so a scheduler tick that lands on work
|
||||
already in flight defers to it (the payoff of ADR 0011's dedup key).
|
||||
|
||||
Of the five triggers originally named for the catalogue pass, **only
|
||||
Of the five triggers originally named for the catalog pass, **only
|
||||
startup, a periodic timer, and manual "Sync now" are implemented.**
|
||||
Gossip-triggered "sync when a peer comes online" (needs hooking the gossip
|
||||
module directly) and "after any local import or policy change" (needs every
|
||||
@@ -73,7 +73,7 @@ target per `docs/sync-management.md`'s remote-administration section) had no
|
||||
driver at all until now: no periodic timer, no startup trigger, and its
|
||||
`/syn/node-admin/…` handler was never wired to a `JobRunner`, so even an
|
||||
explicit remote `TriggerSync` had nothing live to act on. A node whose
|
||||
policy said `Mirror` would sync catalogues (peers push/pull metadata
|
||||
policy said `Mirror` would sync catalogs (peers push/pull metadata
|
||||
directly) but never proactively fetch content — indistinguishable, from a
|
||||
user setting `Mirror` on it, from the feature not working.
|
||||
|
||||
|
||||
@@ -305,7 +305,7 @@ paired.
|
||||
|
||||
A scope model. The parked design on the `linkage-security` branch had
|
||||
`LinkageScope::{Companion, Full}` chosen at pairing time (`syn linkage pair
|
||||
--full`), stored per row, with a rule for reading an unrecognised scope from a
|
||||
--full`), stored per row, with a rule for reading an unrecognized scope from a
|
||||
newer node as the narrowest.
|
||||
|
||||
Nothing wanted `Full`. The only client's entire vocabulary is four commands —
|
||||
|
||||
@@ -11,7 +11,7 @@ and commit internally, per call. The natural, readable way to use them —
|
||||
looping over a list of ids — silently opened a fresh SQLite connection (and
|
||||
prepared a statement) per document. Listing N documents this way cost ~3N
|
||||
connection opens; worse, a bulk write path like doc-sync ingesting a large
|
||||
catalogue held the single SQLite writer lock across thousands of tiny
|
||||
catalog held the single SQLite writer lock across thousands of tiny
|
||||
commits in quick succession, which was enough to make a concurrent
|
||||
interactive write (a Notes save on Android) exceed its busy timeout and
|
||||
crash with `SQLITE_BUSY`. That crash is what started this work.
|
||||
@@ -84,7 +84,7 @@ crash with `SQLITE_BUSY`. That crash is what started this work.
|
||||
|
||||
## Consequences
|
||||
|
||||
- A bulk write path (doc-sync ingest, catalogue import) now costs a handful
|
||||
- A bulk write path (doc-sync ingest, catalog import) now costs a handful
|
||||
of commits instead of thousands, closing the `SQLITE_BUSY`/writer-lock-
|
||||
starvation crash this work started from.
|
||||
- A batch of related reads gets a single consistent snapshot instead of N
|
||||
|
||||
@@ -93,7 +93,7 @@ Two of those matter directly to the never-crash goal rather than being style deb
|
||||
**14 `UnsafeCallOnNullableType`** (`!!`, an unguarded crash each) and — see below — 55
|
||||
discarded `Result`s.
|
||||
|
||||
## Verified behaviour (spikes, 2026-08-08)
|
||||
## Verified behavior (spikes, 2026-08-08)
|
||||
|
||||
Everything below was run, not inferred from documentation — detekt 1.23.8 (this project's
|
||||
pinned version), `detektMain` with type resolution, on a standalone Kotlin/JVM project at
|
||||
@@ -110,7 +110,7 @@ at both versions.
|
||||
| Does per-rule `excludes: ['**/glob/**']` work? | **Yes** |
|
||||
| Does `IgnoredReturnValue` flag a discarded `Result` with **no annotation**? | **Yes**, via `returnValueTypes: ['kotlin.Result']`; and correctly does *not* flag a consumed one |
|
||||
| Does detekt validate matcher strings? | **No.** It fails loudly on a misspelled *rule* name but a bad `value:` is silently inert — see the two rows above. Any change here needs a deliberately-failing test case. |
|
||||
| Do `detektDebug`/`detektMain` exist in this project? | **Yes** (both `EXPERIMENTAL`-labelled by detekt) |
|
||||
| Do `detektDebug`/`detektMain` exist in this project? | **Yes** (both `EXPERIMENTAL`-labeled by detekt) |
|
||||
|
||||
`returnValueTypes` accepts any fully-qualified type name, so a future custom result type
|
||||
gets the same must-use enforcement as `kotlin.Result`. This design is not locked to
|
||||
|
||||
@@ -213,10 +213,10 @@ not determine an affordance.
|
||||
- ADR 0022's Kotlin-side `SyncException.detail` and `reportFailure` keep working unchanged:
|
||||
the failure slot still always holds a `SyncException`, it simply carries more variants.
|
||||
Widening `SyncError` will break `detail`'s exhaustive `when` at compile time, which is
|
||||
the intended behaviour and the reason it was written without an `else`.
|
||||
the intended behavior and the reason it was written without an `else`.
|
||||
- The prose-matching `From<Box<dyn Error>>` goes away — a silent-misclassification bug
|
||||
removed, not merely relocated.
|
||||
- The cost is ~25 hand-written `From` impls that must each make a judgement about what a
|
||||
- The cost is ~25 hand-written `From` impls that must each make a judgment about what a
|
||||
given failure means to a person. That work is unavoidable under any of the options
|
||||
considered (the blanket macro destroys the information regardless), so it is not a
|
||||
tiebreaker, but it is the bulk of the effort and it cannot be mechanical.
|
||||
|
||||
@@ -91,7 +91,7 @@ fallbacks of ADRs 0015 and 0016 are legible in the browser.
|
||||
notebooks) rather than being deleted. Two components with overlapping
|
||||
purpose is the accepted cost of not bending one to fit both.
|
||||
- Because the component owns no tab or tree semantics, two agents *can* end
|
||||
up with inconsistent tab sets or drill-in behaviour. That is deliberate —
|
||||
up with inconsistent tab sets or drill-in behavior. That is deliberate —
|
||||
cross-agent consistency here is a review concern, not something the type
|
||||
system enforces.
|
||||
- Known limits carried forward, none of which are blocking: keyboard
|
||||
|
||||
@@ -51,7 +51,7 @@ worth sharing (`short_id`, the placeholder resolvers) already are.
|
||||
node id or document id in a list row is a handle: you are scanning for which
|
||||
row, not verifying a value, and the full form is one hover or tap away. A
|
||||
blob/content hash is presented precisely so it can be compared against
|
||||
something — truncating it defeats its only purpose. This ADR is not licence to
|
||||
something — truncating it defeats its only purpose. This ADR is not license to
|
||||
start truncating blob hashes.
|
||||
|
||||
Where the full id *is* revealed, it is selectable/copyable text, matching
|
||||
@@ -60,7 +60,7 @@ every other full-hash display site.
|
||||
## Consequences
|
||||
|
||||
- Any new screen showing a node or document identity gets consistent
|
||||
truncation, placeholder styling, and reveal behaviour by using the
|
||||
truncation, placeholder styling, and reveal behavior by using the
|
||||
component, instead of re-deriving all three.
|
||||
- Truncation to 8 hex characters is a collision risk in principle. Accepted:
|
||||
these ids are scanning handles within a personal ensemble of a handful of
|
||||
|
||||
@@ -67,7 +67,7 @@ single-blob stream; artifact-sync records a `None` response the same way, and
|
||||
records a *push* as possession evidence about the pusher.
|
||||
|
||||
**Releases promulgate; peers don't have to discover evictions.** Eviction
|
||||
leaves the catalogue intact by design, so nothing about an evicted document
|
||||
leaves the catalog intact by design, so nothing about an evicted document
|
||||
changes — its `modified_at` is untouched and an incremental doc-sync will
|
||||
never mention it again. `execute_eviction` therefore logs each deleted blob to
|
||||
`content_release_log`, and doc-sync ships those as a `ContentReleaseBatch`
|
||||
@@ -76,7 +76,7 @@ receiver records each as `Absent` about the sender.
|
||||
|
||||
A release is the content-plane counterpart to an artifact tombstone and
|
||||
deliberately weaker: *"I dropped my copy"*, not *"this is gone from the
|
||||
ensemble"*. The catalogue, and every other node's copy, are untouched;
|
||||
ensemble"*. The catalog, and every other node's copy, are untouched;
|
||||
re-fetching restores it.
|
||||
|
||||
**The release log is never cleaned.** Re-acquiring a blob is not routed
|
||||
@@ -95,7 +95,7 @@ never-fetched reads `"Ref"` — a distinction the doc-level
|
||||
per artifact and overwritten by any later sync of that document. For a remote
|
||||
node, `Absent` evidence reads `"Absent"`, and `"Ref"` narrows to its true
|
||||
meaning: this node references the document and has never said either way.
|
||||
Both frontends put `Removed`/`Absent` in the error colour.
|
||||
Both frontends put `Removed`/`Absent` in the error color.
|
||||
|
||||
## Consequences
|
||||
|
||||
@@ -103,7 +103,7 @@ Both frontends put `Removed`/`Absent` in the error colour.
|
||||
`nodes_holding`, which returns only `Present` rows, so a peer known to have
|
||||
evicted stops counting toward "some other node still has this."
|
||||
- Artifact-sync can rank peers the way blob-sync always could.
|
||||
`peer_order_by_presence` takes `ContentKey`s, so the catalogue-fetch pass
|
||||
`peer_order_by_presence` takes `ContentKey`s, so the catalog-fetch pass
|
||||
ranks once over everything it wants — the thumbnails' bytes and the stubs'
|
||||
values together.
|
||||
- `DocumentAvailability` judges an unfulfilled inline stub exactly as it
|
||||
|
||||
@@ -10,7 +10,7 @@ not had this pass yet.
|
||||
The GUI's palette had never been chosen, only accumulated. `theme.rs` was a
|
||||
sound role-based structure — `bg_nav_sidebar_selected`, `text_primary`,
|
||||
`color_warning` — filled with Material 3's out-of-the-box seed purple
|
||||
(`#6750a4`) and a set of neighbours picked one call site at a time. The result
|
||||
(`#6750a4`) and a set of neighbors picked one call site at a time. The result
|
||||
was the "good stack, default settings" look shared by a large number of
|
||||
Tauri/Electron tools and by Obsidian's default theme: defensible everywhere,
|
||||
identifiable nowhere.
|
||||
@@ -30,14 +30,14 @@ outcome and the values.
|
||||
|
||||
Five facts from that process constrain everything below.
|
||||
|
||||
### Colour has to mean something, because there is too much of it to decorate with
|
||||
### Color has to mean something, because there is too much of it to decorate with
|
||||
|
||||
The first specimen gave each document type its own hue. It failed on contact
|
||||
with the real list: there are far more document types than there are hues that
|
||||
can be told apart, so the colour was decorative from the first screen. The same
|
||||
can be told apart, so the color was decorative from the first screen. The same
|
||||
argument then applied one level down — filling the *present* cell of a presence
|
||||
indicator spends the whole indicator's colour budget on the state 90% of rows
|
||||
are in. Colour is affordable only where it marks an exception.
|
||||
indicator spends the whole indicator's color budget on the state 90% of rows
|
||||
are in. Color is affordable only where it marks an exception.
|
||||
|
||||
### Saturation is wanted, and the ground is what makes it affordable
|
||||
|
||||
@@ -79,7 +79,7 @@ later as presets.
|
||||
Charcoal ground with a trace of the accent in it, saturated cobalt for state,
|
||||
hairline separation rather than tinted panels, flat throughout — no shadows, no
|
||||
bevels, no blurred glow. The glow quality wanted from Bitwig comes from
|
||||
saturated colour on a dark ground, not from a blur.
|
||||
saturated color on a dark ground, not from a blur.
|
||||
|
||||
The rejected siblings and why are in "Rejected alternatives" below.
|
||||
|
||||
@@ -89,7 +89,7 @@ Both are kept — seeing the light ground on a real document list reversed an
|
||||
earlier inclination to ship dark-only. In the **UI** they are named *Charcoal*
|
||||
and *Slate*, not Light and Dark, because neither is white and the names should
|
||||
say what the user sees. Internal identifiers (`ThemeMode::Light` / `Dark` /
|
||||
`System`, `Theme::light()` / `dark()`) do **not** change; this is a labelling
|
||||
`System`, `Theme::light()` / `dark()`) do **not** change; this is a labeling
|
||||
decision, not a refactor.
|
||||
|
||||
`ThemeMode`'s default moves from `System` to `Dark`.
|
||||
@@ -110,7 +110,7 @@ not estimated.
|
||||
| raised / groove | `#313d4c` | meter troughs, inert cells |
|
||||
| `border_default` | `#2a3441` | hairline |
|
||||
| `border_card` | `#3e4c5e` | the stronger rule under a table header |
|
||||
| `text_primary` | `#dbe3ee` | 13.10:1 — cool light grey, never white |
|
||||
| `text_primary` | `#dbe3ee` | 13.10:1 — cool light gray, never white |
|
||||
| `text_secondary` | `#8794a6` | 5.50:1 |
|
||||
| `text_tertiary` *(new)* | `#748193` | 4.28:1 — see §8 |
|
||||
| `accent` | `#4585ff` | 4.88:1 — strokes, dots, underlines, focus rings; see Amendments |
|
||||
@@ -130,7 +130,7 @@ the art or the page is what supplies the light there; a light chrome around a
|
||||
light page loses the edge of it. This is the desk metaphor applied one level
|
||||
down, and it is why the token is not simply "the darkest background". On
|
||||
Charcoal it *is* the darkest background, sharing the rail's value: the floor
|
||||
has room for one colour, and two roles landing on it is better than two values
|
||||
has room for one color, and two roles landing on it is better than two values
|
||||
1.01:1 apart. The reading surface's dark page (`#0f1217`, §5) sits just above
|
||||
that floor for the same reason.
|
||||
|
||||
@@ -157,7 +157,7 @@ that floor for the same reason.
|
||||
`selection` stays `accent` at ~25% alpha; `modal_overlay` stays a neutral black
|
||||
at ~65%.
|
||||
|
||||
### 4. What each colour is allowed to mean
|
||||
### 4. What each color is allowed to mean
|
||||
|
||||
- **Cobalt is energised** — selected, focused, in flight, playing, connected.
|
||||
When nothing is happening there is no cobalt on screen. This is why *present*
|
||||
@@ -167,7 +167,7 @@ at ~65%.
|
||||
- **Absence is none of these.** A missing copy is an empty outlined cell and
|
||||
the word "Not here", never red. This retires the shipped app's red "Absent"
|
||||
and settles `CRITIQUE.md` finding 4's orange-vs-mustard drift in passing.
|
||||
- **No colour ever encodes a category** — not document type, not agent, not
|
||||
- **No color ever encodes a category** — not document type, not agent, not
|
||||
collection. Categories get words and columns.
|
||||
- **Amber is a fill, not an outline.** An amber hairline around an empty box
|
||||
reads as damage rather than as attention.
|
||||
@@ -182,7 +182,7 @@ at ~65%.
|
||||
- **Selection is a saturated full-bleed block** with a brighter left edge and
|
||||
`text_on_accent` on it — never a tint of the accent, and never an inset
|
||||
rounded pill, which is the tell that reads as a generic web sidebar. One
|
||||
selection colour everywhere, which settles `CRITIQUE.md` finding 1's two
|
||||
selection color everywhere, which settles `CRITIQUE.md` finding 1's two
|
||||
competing hues.
|
||||
- **One section-heading grammar**, used in the content area *and* in the
|
||||
navigation rail — the rail does not get a heading style of its own.
|
||||
@@ -240,7 +240,7 @@ now changes one view and one column rather than the app's primary list.
|
||||
uses `text_secondary`; anything interactive uses `text_primary` or the
|
||||
accent, which is `CRITIQUE.md` finding 3 generalised into a rule.
|
||||
- **`accent_fill` is added** for accent surfaces that carry text, per the
|
||||
Context above. `accent` remains the stroke/mark colour.
|
||||
Context above. `accent` remains the stroke/mark color.
|
||||
- **`sizing::shape` is added**, as `CRITIQUE.md` finding 2 proposed: `CONTROL`,
|
||||
`CARD`, `DIALOG`, `PILL`, each naming which `rounded_*()` it maps to, with
|
||||
the existing call sites audited against it. Radius becomes a role, not a
|
||||
@@ -254,8 +254,8 @@ A preset may change every value in §3. It may not change §5 — icons on or of
|
||||
in the rail, block versus pill selection, or the section-heading grammar are
|
||||
structural, and making them themeable means a second token layer above
|
||||
`theme.rs` describing shape and component style. That layer is not being built
|
||||
now. The practical consequence: Charcoal, Slate, and any further recolourings
|
||||
are cheap and can ship with the `TODO.md` theming item, while the LCARS-flavoured
|
||||
now. The practical consequence: Charcoal, Slate, and any further recolorings
|
||||
are cheap and can ship with the `TODO.md` theming item, while the LCARS-flavored
|
||||
"Signal" direction — whose whole argument is structural — is a larger piece of
|
||||
work and is deferred, not rejected.
|
||||
|
||||
@@ -265,26 +265,26 @@ Android takes the same palette, the same meanings, and the same structure. The
|
||||
decisions below are the ones the phone forces that the desktop never had to
|
||||
answer.
|
||||
|
||||
**Dynamic colour is dropped.** The app shipped with `dynamicColor = true`, so on
|
||||
Android 12+ every colour above was bypassed for a scheme derived from the user's
|
||||
**Dynamic color is dropped.** The app shipped with `dynamicColor = true`, so on
|
||||
Android 12+ every color above was bypassed for a scheme derived from the user's
|
||||
wallpaper. That is not a compliance question — Material 3 treats a custom scheme
|
||||
as equally first-class, `lightColorScheme`/`darkColorScheme` exist for exactly
|
||||
this, and Spotify, Signal, Telegram and WhatsApp all take the same branch. It is
|
||||
a question of what colour is *for*. Dynamic colour suits an app whose colour is
|
||||
decoration; §4 makes colour carry meaning by hue, and a wallpaper-derived
|
||||
a question of what color is *for*. Dynamic color suits an app whose color is
|
||||
decoration; §4 makes color carry meaning by hue, and a wallpaper-derived
|
||||
`primary` reassigns the hue without reassigning the meaning. A green wallpaper
|
||||
makes "selected" and "present" the same colour; a red one collides "selected"
|
||||
makes "selected" and "present" the same color; a red one collides "selected"
|
||||
with "fault".
|
||||
|
||||
The app had already noticed this once: `LocalSuccessColor` existed with a comment
|
||||
saying it was there to survive "the dynamic-color path (where the wallpaper-derived
|
||||
scheme has no green)". Doing this pass on top of dynamic colour would have needed
|
||||
scheme has no green)". Doing this pass on top of dynamic color would have needed
|
||||
four more escape hatches — warning, `accent_fill`, `text_tertiary`, the media
|
||||
canvas — at which point the dynamic scheme supplies backgrounds and text tiers
|
||||
only, every semantic colour is overridden, and the result is neither coherent with
|
||||
only, every semantic color is overridden, and the result is neither coherent with
|
||||
the desktop nor honestly Material You.
|
||||
|
||||
The one thing genuinely given up is dynamic colour's guarantee that
|
||||
The one thing genuinely given up is dynamic color's guarantee that
|
||||
`primary`/`onPrimary` pairs are accessible whatever the wallpaper is. §3 measured
|
||||
every pair, so that cost is already paid. A wallpaper-derived scheme remains
|
||||
available later as one *preset* under §9, which is where a personalisation feature
|
||||
@@ -294,11 +294,11 @@ belongs.
|
||||
mappings are deliberate:
|
||||
|
||||
- *The tonal families collapse.* Material offers primary, secondary and tertiary
|
||||
as three distinguishable accents. §4 says no colour ever encodes a category, so
|
||||
as three distinguishable accents. §4 says no color ever encodes a category, so
|
||||
all three resolve to the one accent — a screen reaching for `tertiary` to look
|
||||
different gets the accent, which is the intended answer.
|
||||
- *Containers are not tinted.* `errorContainer` is a plain card with
|
||||
error-coloured text on it rather than a red-washed panel, per §5's hairlines and
|
||||
error-colored text on it rather than a red-washed panel, per §5's hairlines and
|
||||
gutters. `primaryContainer` is the single exception and is `accent_fill`,
|
||||
because that is §5's selection block and is meant to be saturated.
|
||||
- *`surfaceTint` is transparent*, so Material's elevation tinting cannot
|
||||
@@ -345,11 +345,11 @@ headings and their order staying Rust-side so the two cannot drift. `AgentGroup`
|
||||
|
||||
## Rejected alternatives
|
||||
|
||||
- **Instrument** — the same bones with colour used only sparingly and no rail
|
||||
- **Instrument** — the same bones with color used only sparingly and no rail
|
||||
icons. Rejected narrowly and for taste: by the end of the specimen it
|
||||
differed from Console mainly in saturation, and the more saturated reading
|
||||
plus the rail icons won. Most of Instrument is in the decision above.
|
||||
- **Signal** — colour as structure: filled round-ended section bars, pill
|
||||
- **Signal** — color as structure: filled round-ended section bars, pill
|
||||
controls against hard-rectangle containers, wide gutters. On a charcoal
|
||||
document list this crossed into costume ("star trek fanart"); on slate it
|
||||
read considerably straighter. Deferred as a candidate optional theme rather
|
||||
@@ -357,9 +357,9 @@ headings and their order staying Rust-side so the two cannot drift. `AgentGroup`
|
||||
- **Ship one dark-leaning theme and drop the light/dark split.** Attractive
|
||||
until the slate ground was rendered on a real list, at which point it was
|
||||
clearly worth keeping.
|
||||
- **Per-type colour**, **a coloured edge per row**, and **a saturated fill on
|
||||
- **Per-type color**, **a colored edge per row**, and **a saturated fill on
|
||||
the "present" state** — all three are the same mistake at different scales:
|
||||
colour spent on a category or on the common case. See §4.
|
||||
color spent on a category or on the common case. See §4.
|
||||
- **The Ensemble screen as a node-editor graph.** Drawn, and wrong: a node
|
||||
editor asserts a topology the user authors, links with direction, and
|
||||
per-link asymmetry, none of which is true of a set of co-equal peers. It is a
|
||||
@@ -386,7 +386,7 @@ layer` onward. What is done, in the order the sections above appear:
|
||||
`text_secondary`/`accent`.
|
||||
- **§2, the default.** `ThemeMode::default` is `Dark`; the Settings control
|
||||
reads Charcoal / Slate / Follow system.
|
||||
- **§4, what colour means.** Presence is `PresenceLook` in
|
||||
- **§4, what color means.** Presence is `PresenceLook` in
|
||||
`agents/documents/mod.rs`: green here, amber not-yet-told, cobalt arriving,
|
||||
and an empty outlined dot with the words "Not here" for a missing copy. Red
|
||||
is gone from presence entirely and now marks faults and destructive actions
|
||||
@@ -404,7 +404,7 @@ layer` onward. What is done, in the order the sections above appear:
|
||||
- **§8, tokens.** Done, including `sizing::shape` and an audit of every
|
||||
`rounded_*()` call site onto `CARD` / `CONTROL` / `DIALOG` / `PILL`.
|
||||
- **The reading surface** is `ReadingSurface` in settings plus `theme::Page`,
|
||||
with `Theme::on_page` recolouring a palette for a document rather than
|
||||
with `Theme::on_page` recoloring a palette for a document rather than
|
||||
threading a second value through the prose renderer. Its *control* is not on
|
||||
the Settings screen: it is a dropdown in the Notes toolbar beside the
|
||||
view-mode switcher, shown only while a preview is on screen. A setting whose
|
||||
@@ -472,9 +472,9 @@ remote, and an active `TransportAddr` is `Ip` (direct) or `Relay` (relayed).
|
||||
Building a gossip payload to obtain it would be reimplementing what the
|
||||
transport already knows.
|
||||
|
||||
The other (N-1)² cells fail on their own terms too. What a gossiped neighbour
|
||||
The other (N-1)² cells fail on their own terms too. What a gossiped neighbor
|
||||
set describes is iroh-gossip's **bounded partial mesh** — the swarm keeps a
|
||||
small neighbour set by design and drops edges it does not need — so a missing
|
||||
small neighbor set by design and drops edges it does not need — so a missing
|
||||
edge between two peers is ordinary swarm maintenance, and the matrix would
|
||||
draw it as a fault. The cells would also be stale by construction, each as
|
||||
old as its node's last announcement, which means the symmetry the matrix
|
||||
@@ -482,7 +482,7 @@ exists to demonstrate is not guaranteed: one pair, two cells, two moments,
|
||||
two possible answers. A picture whose argument its own data can contradict is
|
||||
not making the argument.
|
||||
|
||||
Underneath all of that is a product judgement: what one *other* node can see
|
||||
Underneath all of that is a product judgment: what one *other* node can see
|
||||
of a second other node is not something this app currently acts on. If that
|
||||
changes — a sync that fails for a reason only a third node's view explains —
|
||||
the trigger is that failure, and the shape to reach for is a diagnostic
|
||||
@@ -490,9 +490,9 @@ aimed at the pair in question, not a permanent grid on a screen the user
|
||||
opens for other reasons.
|
||||
|
||||
The same defect ran at 1×N in the roster's own `Connected`/`Degraded` badge,
|
||||
which read the gossip neighbour set for each peer and so called a node "degraded"
|
||||
which read the gossip neighbor set for each peer and so called a node "degraded"
|
||||
whenever the swarm had no edge to it. **That badge is removed** — on all four
|
||||
modalities, along with `MemberConnectionState` and the per-peer neighbour-id
|
||||
modalities, along with `MemberConnectionState` and the per-peer neighbor-id
|
||||
accessors behind it — rather than repointed at `remote_info`. A row on this
|
||||
screen should claim something the user can act on, and "this peer is currently
|
||||
dialable" is not that: the screen's actions already probe on demand (Ping
|
||||
@@ -501,7 +501,7 @@ answer than a dot that is stale the moment it is drawn.
|
||||
|
||||
The aggregate count survives, worded honestly. `copy::ensemble_roster` now owns
|
||||
both lines — the roster's "N total nodes · M other nodes", and a gossip
|
||||
diagnostic that says "N gossip neighbour(s)" rather than the old "N/M other
|
||||
diagnostic that says "N gossip neighbor(s)" rather than the old "N/M other
|
||||
nodes reachable now". It reports whether the mesh has formed at all, which is a
|
||||
real thing to know while bringing up a node, and claims nothing about which
|
||||
nodes are reachable.
|
||||
@@ -626,7 +626,7 @@ another hue, which is §3's point and the one thing neither correction has
|
||||
touched.
|
||||
|
||||
Two roles now share the floor. `bg_nav_sidebar` and `bg_media_canvas` are the
|
||||
same near-black, because a floor this close to black has room for one colour
|
||||
same near-black, because a floor this close to black has room for one color
|
||||
and 1.01:1 apart is not a distinction, it is a failure to be the same. They are
|
||||
never adjacent: the rail, the in-agent sidebar and the canvas alternate
|
||||
floor / ground / floor across a window, and the ground between them is what
|
||||
@@ -647,7 +647,7 @@ The re-measured set is in §3's table; the ones that mattered:
|
||||
- `accent` 5.41 → 4.88:1 against the ground, 4.94 → 4.34:1 against `bg_card`.
|
||||
Both well above the 3:1 a non-text UI component needs, so the brightening in
|
||||
the amendment above is not undone by this one.
|
||||
- `color_error` 5.21 → 4.70:1, the tightest of the status colours, still AA.
|
||||
- `color_error` 5.21 → 4.70:1, the tightest of the status colors, still AA.
|
||||
Android's `textOnError` is `bg_window` by construction and moves with it:
|
||||
5.15 → 4.70:1 on `#e0596e`, still better than white's 3.63:1.
|
||||
- The selection block, `accent_fill` `#2563eb`, is 3.28:1 against the ground —
|
||||
@@ -662,12 +662,12 @@ The re-measured set is in §3's table; the ones that mattered:
|
||||
- `text_on_accent` inverts on Charcoal. It is currently near-black, because the
|
||||
dark theme's accent was a *light* lilac; with a saturated cobalt fill it
|
||||
becomes white. Any call site that assumed dark-on-accent needs checking.
|
||||
- The Settings theme control is relabelled Charcoal / Slate / Follow system,
|
||||
- The Settings theme control is relabeled Charcoal / Slate / Follow system,
|
||||
and its default changes to Charcoal.
|
||||
- A new setting is needed for the reading surface (paper / white / dark),
|
||||
independent of the theme.
|
||||
- Android's Compose theme (`ui/theme/`) takes the same pass, per §10: the theme
|
||||
files become `SynPalette`/`SynShapes`/`Theme.kt`, and the dynamic-colour path
|
||||
files become `SynPalette`/`SynShapes`/`Theme.kt`, and the dynamic-color path
|
||||
is removed rather than kept alongside.
|
||||
- The prose typeface question is untouched here and remains ADR 0024's, to be
|
||||
settled in `design/typeface-specimen/` rather than by eye.
|
||||
|
||||
@@ -72,7 +72,7 @@ would be noise on every row of a fully-synced library.
|
||||
|
||||
The rule reads while scanning a grid at arm's length; the word reads once a card
|
||||
has caught the eye. Because both are present on a card, neither is load-bearing
|
||||
alone and the pair never rests on colour.
|
||||
alone and the pair never rests on color.
|
||||
|
||||
### Solid rule for `Remote`, dashed for `Unavailable`
|
||||
|
||||
@@ -93,7 +93,7 @@ served it during blob-sync, or answered a presence query is indistinguishable
|
||||
from one that lacks it. The honest claim is that nothing has *claimed* it, not
|
||||
that the content is gone. Every crossed-out glyph asserts the stronger thing.
|
||||
|
||||
### `Remote` spends no colour; `Unavailable` takes warning, never error
|
||||
### `Remote` spends no color; `Unavailable` takes warning, never error
|
||||
|
||||
Remote is the ordinary condition in an ensemble — CLAUDE.md's "data arriving
|
||||
from another node is the normal case in this system, not an event" — so painting
|
||||
@@ -153,7 +153,7 @@ picked them rather than a new answer being invented:
|
||||
When a name can't fit beside its own mark the *name* is elided: a row clipped
|
||||
to look unmarked would say the opposite of the truth.
|
||||
- The note **preview pane** is the full pane, with the same sentences the
|
||||
desktop and Android panes use, centred, under a rule.
|
||||
desktop and Android panes use, centered, under a rule.
|
||||
- The **pane header** — a border title already carrying the notebook's name —
|
||||
gets nothing. That is the desktop's 260px rail argument in a terminal: no
|
||||
width to spend, and the state is already said in the row.
|
||||
@@ -211,7 +211,7 @@ and a touch screen has no hover to send anything to. Android's notes tree
|
||||
therefore takes the terminal's answer instead — the word in a trailing column,
|
||||
right-aligned against the rail's inner edge, reserving nothing at all in a
|
||||
notebook that is entirely here, and eliding the *name* rather than the mark when
|
||||
one of them isn't. The one addition is a colour the other two modalities never
|
||||
one of them isn't. The one addition is a color the other two modalities never
|
||||
need: on the selected row the word is written in `textOnAccent` like everything
|
||||
else on the saturated selection block (ADR 0032 §5), which costs nothing, since
|
||||
`Remote` and `No source` are told apart by the word.
|
||||
|
||||
@@ -52,7 +52,7 @@ The first implementation of this ADR got that backwards and is worth recording
|
||||
as a trap. Every row had been `text_primary` at `MEDIUM`; the change left
|
||||
matches exactly there and dropped context to `text_secondary` at `NORMAL`. That
|
||||
is *withdrawal only* — the baseline never moved up — and it reads acceptably on
|
||||
the dark ground and too thin on the light one, because mid-greys on a light
|
||||
the dark ground and too thin on the light one, because mid-grays on a light
|
||||
ground sit perceptually closer to black than their contrast ratios suggest.
|
||||
Matches now go to `SEMIBOLD` as well, so the separation is two channels wide
|
||||
from both directions.
|
||||
@@ -206,7 +206,7 @@ to work around.
|
||||
in an actual profile, or if the tree stops being shallow — deep hierarchies are
|
||||
where adjacency-list traversal degrades and path enumeration earns its keep.
|
||||
Not before: the filter's unindexed scan (above) dominates anything the tree
|
||||
work costs, so a materialized path would optimise the cheaper half first.
|
||||
work costs, so a materialized path would optimize the cheaper half first.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ whole reason clippings hang under one root is so the Documents list shows a
|
||||
single "Clippings" row instead of five thousand of them.
|
||||
|
||||
Marking it derived to get non-purgeability would hide the one row the design
|
||||
depends on. Adding a second predicate beside `is_derived` gets the behaviour
|
||||
depends on. Adding a second predicate beside `is_derived` gets the behavior
|
||||
but leaves two overlapping classifiers to keep consistent — which is how this
|
||||
rots the second time somebody adds a type and answers only one of them.
|
||||
|
||||
@@ -162,7 +162,7 @@ becomes a predicate on one indexed column.
|
||||
fix it on its own.
|
||||
- **`SearchParams::include_derived` is now misnamed** and becomes
|
||||
`include_hidden`, along with the CLI's `--include-derived` and the
|
||||
Documents screen's "Show derived" toggle. Behaviour is unchanged; the flag
|
||||
Documents screen's "Show derived" toggle. Behavior is unchanged; the flag
|
||||
now means what it always did.
|
||||
- **`CannotPurgeDerived` becomes `CannotPurge { provenance }`** — the error a
|
||||
user sees when purging an anchor should say "this is the Clippings root",
|
||||
@@ -180,4 +180,4 @@ becomes a predicate on one indexed column.
|
||||
what the type was documented to mean before `is_derived` moved in.
|
||||
- Every existing `is_derived()` caller keeps working through
|
||||
`document.provenance.hidden()` / `.purgeable()`; there is no third
|
||||
behaviour to implement, only a better-shaped question.
|
||||
behavior to implement, only a better-shaped question.
|
||||
|
||||
@@ -200,7 +200,7 @@ stops being theoretical — noted in `TODO.md`, not blocking here.
|
||||
owns a whole pane — monospace, Tab-indent, markdown list continuation,
|
||||
paragraph jumps — and a clipping is edited in the card it is read in, beside
|
||||
a composer already using the same box for the same text. There is no
|
||||
`TextFormat` to honour either: a clipping is whatever was selected on a page.
|
||||
`TextFormat` to honor either: a clipping is whatever was selected on a page.
|
||||
A form field was the right component; the prediction was that the *editing*
|
||||
was the hard part, and the hard part was everything around it.
|
||||
- **What editing needed was the guard, not the editor**, and building it named
|
||||
|
||||
@@ -122,7 +122,7 @@ MessagePack at that size, which is the point this ADR opened with.
|
||||
|
||||
- Four response bodies now distinguish "not applicable" from "zero" through an
|
||||
`Option`, because each changes what a user is told: a single-artifact pull's
|
||||
absent catalogue counts, a deferred sync's absent totals, a note whose body
|
||||
absent catalog counts, a deferred sync's absent totals, a note whose body
|
||||
is on another node, and the indexed document page that never counts past its
|
||||
limit. A later `#[serde(default)]` on any of them would compile, round-trip,
|
||||
and quietly swap the message, so a test asserts the absence survives.
|
||||
|
||||
@@ -67,7 +67,7 @@ Sync policy is the worked example: it looks like a local preference, so
|
||||
`SettingsStore` announced nothing for as long as it existed, and all the while
|
||||
`p2p::node_admin` let a remote administrator set it on this node while the tab
|
||||
showing it sat there. A store that writes anything a screen shows should have a
|
||||
`ChangeSignal`, and marking the write is the store's job rather than a judgement
|
||||
`ChangeSignal`, and marking the write is the store's job rather than a judgment
|
||||
about which fields are interesting. `SyncStagingStore` is the one store still to
|
||||
be held to that; the screen it would serve is a sync in progress showing its own
|
||||
staged count rather than a figure from when it last looked.
|
||||
@@ -145,7 +145,7 @@ is what keeps a backgrounded app from holding a thread parked indefinitely.
|
||||
|
||||
Burst coalescing does not fall out for free, and is arranged the same way it is
|
||||
on the desktop: a wake lets a 150 ms window pass and reports where the store
|
||||
ended up, so a catalogue sync costs a handful of reloads a second rather than
|
||||
ended up, so a catalog sync costs a handful of reloads a second rather than
|
||||
one per committed transaction. One window rather than a loop waiting for quiet
|
||||
— a sync running continuously never goes quiet, and a screen that froze until
|
||||
it did would be worse than one updating a few times a second.
|
||||
@@ -224,7 +224,7 @@ fails, rather than replacing good rows with an error banner: there is no one
|
||||
waiting to be told, and the next change tries again. The explicit reloads
|
||||
behind a Refresh button keep their error paths.
|
||||
|
||||
**Not everything a screen shows is a store row.** The gossip neighbour count is
|
||||
**Not everything a screen shows is a store row.** The gossip neighbor count is
|
||||
in-memory P2P state; nothing announces it, and the Ensemble screen's `r` remains
|
||||
the way to ask. A channel for it would have to come from the P2P node, not the
|
||||
store. (Per-member live reachability was in this sentence too, until the badge
|
||||
@@ -234,7 +234,7 @@ that showed it was removed — ADR 0032's amendment on the reachability matrix.)
|
||||
and the polls this ADR set out to remove are gone. What is *deliberately* not
|
||||
following it, so the next reader doesn't take any of these for an oversight:
|
||||
|
||||
- **In-memory P2P state.** The gossip neighbour count is not a store row, so no
|
||||
- **In-memory P2P state.** The gossip neighbor count is not a store row, so no
|
||||
store channel can announce it; the Ensemble screens' Refresh is still the way
|
||||
to ask. A signal for it would have to come from `P2PNode` — see the paragraph
|
||||
above.
|
||||
|
||||
@@ -165,7 +165,7 @@ by causality rather than by timestamp. It also needs per-add identifiers that
|
||||
accumulate, a causal context to compact them against, and a wire format for
|
||||
both. The whole rest of this model already resolves concurrency by comparing
|
||||
timestamps — that is what `LastWriteWins` *is*, and what both other tombstones
|
||||
do — so an LWW-element-set is consistent with its neighbours, and the failure
|
||||
do — so an LWW-element-set is consistent with its neighbors, and the failure
|
||||
mode it retains (two nodes disagreeing within the same wire-clock second) is
|
||||
one the model accepts everywhere else.
|
||||
|
||||
@@ -180,7 +180,7 @@ one the model accepts everywhere else.
|
||||
- Neither new table has a foreign key to `documents`, matching
|
||||
`edge_tombstones`/`document_tombstones`: the point of a tombstone is that the
|
||||
thing it refers to is gone. A purged document's element rows therefore linger,
|
||||
and that is the correct behaviour — a document that comes back should not
|
||||
and that is the correct behavior — a document that comes back should not
|
||||
bring resurrected tags with it.
|
||||
- Existing databases are seeded once at open (`backfill_set_elements`): every
|
||||
element of an existing `SetUnion` row is taken as added at the row's own
|
||||
|
||||
@@ -161,6 +161,6 @@ join-time snapshot that renames never update was not worth it.
|
||||
**Drop names from gossip as well, and sync them as documents.** Names would
|
||||
then ride the admission-gated, vault-encrypted document channel, and nothing
|
||||
would need sealing. It is a much larger change — a node record document type,
|
||||
its sync policy, its conflict behaviour — for a field that has to be present
|
||||
its sync policy, its conflict behavior — for a field that has to be present
|
||||
before two nodes have synced anything, since the roster is how a user picks a
|
||||
node to sync *with*.
|
||||
|
||||
@@ -32,7 +32,7 @@ browsing a library never opens the audio output device.
|
||||
the device plays one thing at a time and the ExoPlayer lives in a
|
||||
`MediaSessionService` so it outlives every screen — but every screen read its
|
||||
*whole* state. Playing a podcast and then opening Music showed the Music bar
|
||||
greyed out (Music had no track of its own) yet scrubbed to the podcast's
|
||||
grayed out (Music had no track of its own) yet scrubbed to the podcast's
|
||||
position. The rate was shared the same way: ExoPlayer's playback speed is a
|
||||
property of the player, not of the item, so a podcast's 1.5x would have carried
|
||||
onto the next song.
|
||||
|
||||
@@ -25,7 +25,7 @@ row, not from any one control in it:
|
||||
a rate pill, Music does not, so the two agents' play buttons sat in
|
||||
different places — the one control a user reaches for without looking.
|
||||
- The metadata was left-aligned in a column of its own, so the player had no
|
||||
centre and read as three unrelated strips rather than as one instrument.
|
||||
center and read as three unrelated strips rather than as one instrument.
|
||||
- There was nowhere to put anything else. A visualization, chapter markers, a
|
||||
queue button: each would have had to displace something already in the row.
|
||||
|
||||
@@ -71,15 +71,15 @@ where one is absent.
|
||||
|
||||
**The invariants the order exists to protect**, all from the design document:
|
||||
|
||||
- Metadata is centred over the *scrubber*, not over the transport cluster.
|
||||
- Metadata is centered over the *scrubber*, not over the transport cluster.
|
||||
- The visualization slot is directly above the timeline and nowhere else, and
|
||||
collapses to nothing rather than to a reserved empty box.
|
||||
- The primary transport is anchored (left on the desktop, centred on mobile)
|
||||
- The primary transport is anchored (left on the desktop, centered on mobile)
|
||||
and the secondary cluster is anchored away from it. The desktop's action row
|
||||
is `justify_between`, deliberately *not* a centred row: an optional control
|
||||
is `justify_between`, deliberately *not* a centered row: an optional control
|
||||
appearing on the right must not move the play button.
|
||||
- Play/pause is the strongest control. Size alone does not carry that against a
|
||||
row of same-coloured glyphs, so it is filled — `transport_button_primary` on
|
||||
row of same-colored glyphs, so it is filled — `transport_button_primary` on
|
||||
the desktop, `FilledIconButton` on Android — and larger than what flanks it.
|
||||
|
||||
**Configuration is capabilities, not modes.** `PlayerCapabilities` has one
|
||||
@@ -102,7 +102,7 @@ player component contains the word "podcast" or "music".
|
||||
`Skip` is handled inside the player, because a relative seek is transport and
|
||||
the owner has no opinion about it. `Adjacent` is not: only the agent knows what
|
||||
"the next track" is — which list it came out of and how that list was sorted —
|
||||
so the player emits a request and reports what it was told about neighbours.
|
||||
so the player emits a request and reports what it was told about neighbors.
|
||||
|
||||
**No glyph carries the skip interval.** Both platforms ship icons with the
|
||||
number drawn in — Material's `Replay10`/`Forward30`, and any equivalent asset
|
||||
@@ -116,17 +116,17 @@ desktop's tooltip (`step_label`) and Android's content description
|
||||
(`stepDescription`). 15 seconds each way is the current default and lives with
|
||||
the agent, not with the player.
|
||||
|
||||
**Adjacency is captured at activation, and greys the pair out when absent.**
|
||||
**Adjacency is captured at activation, and grays the pair out when absent.**
|
||||
Music records the track list the user activated from (`playback_context` /
|
||||
`playbackContext`) rather than reading the current library level. Browsing off
|
||||
to another artist while a song plays must not silently redefine what that
|
||||
song's Next means. A track with no surrounding list — one song in a folder of
|
||||
one, a selection the screen has since forgotten — reports both neighbours
|
||||
one, a selection the screen has since forgotten — reports both neighbors
|
||||
false, and both buttons render inert rather than doing nothing when pressed.
|
||||
|
||||
**Where the two modalities differ, and why it is not drift.** The desktop puts
|
||||
the times either side of the scrubber and anchors the transport left; mobile
|
||||
puts the times below a full-width scrubber and centres the transport. A phone's
|
||||
puts the times below a full-width scrubber and centers the transport. A phone's
|
||||
~340dp content width minus two time labels leaves the scrubber a stub, and a
|
||||
left-anchored transport puts the play button under nobody's thumb. This is the
|
||||
same width-driven split the app already makes between two-column and stacked
|
||||
@@ -144,7 +144,7 @@ lives in a separate `PlaybackService`. That is its own change.
|
||||
## Open: where the desktop's primary transport sits
|
||||
|
||||
The desktop transport is the bottom-left of the action row, as the design
|
||||
document draws it. The alternative is a vertically-centred **left flank**
|
||||
document draws it. The alternative is a vertically-centered **left flank**
|
||||
beside the artwork, with the secondary cluster mirrored on the right and the
|
||||
stack dropping to three regions — a pane around 80px instead of 130px, and the
|
||||
arrangement iTunes and most desktop players settle on.
|
||||
@@ -159,8 +159,8 @@ worth arguing about against an empty region, so it waits.
|
||||
|
||||
The move is cheap when the time comes: the regions are already separate
|
||||
functions and the clusters already know nothing about each other. What it
|
||||
trades away is the metadata being dead-centre in the pane — centred over the
|
||||
scrubber and centred in the pane stop being the same place once the flanks are
|
||||
trades away is the metadata being dead-center in the pane — centered over the
|
||||
scrubber and centered in the pane stop being the same place once the flanks are
|
||||
uneven, and Music's secondary cluster is empty.
|
||||
|
||||
## Consequences
|
||||
|
||||
@@ -202,7 +202,7 @@ for it for this job, but that is tidying, not part of this decision.
|
||||
`Comic` a row of its own — but that is a change to policy resolution and to
|
||||
`has_blob_content`, and it interacts with ADR 0048's sharing rule. **Not
|
||||
decided here.**
|
||||
- **Podcast episodes stay visible**, which is the behaviour the rejected
|
||||
- **Podcast episodes stay visible**, which is the behavior the rejected
|
||||
structural rule would have broken, and is worth a test that names it.
|
||||
|
||||
## See also
|
||||
|
||||
@@ -161,11 +161,11 @@ keeping an attachment as a document of its own.
|
||||
### Purge makes bytes unreferenced; reclaiming them is a separate act
|
||||
|
||||
Purge continues not to delete blobs, which is now a position rather than an
|
||||
omission: purge is catalogue-plane (a tombstone and a delete intent), and
|
||||
omission: purge is catalog-plane (a tombstone and a delete intent), and
|
||||
blob deletion is content-plane and goes through the guard. What changes is
|
||||
that `references_to_blob` makes the resulting orphans *identifiable* — a hash
|
||||
with zero live references is unambiguously reclaimable, needs no ensemble
|
||||
evidence round (nothing local wants it and its catalogue entry is gone), and
|
||||
evidence round (nothing local wants it and its catalog entry is gone), and
|
||||
is the cheapest possible input to the "Reclaim…" flow. Sweeping them is a
|
||||
follow-on, not decided here; what this ADR fixes is that they were previously
|
||||
indistinguishable from bytes something still needed.
|
||||
@@ -185,7 +185,7 @@ indistinguishable from bytes something still needed.
|
||||
level and not only at the guard.
|
||||
- **A promoted document survives its source's purge**, locally and across the
|
||||
ensemble: separate document rows, separate tombstones, and bytes held by the
|
||||
reference count. This is the behaviour ADR 0047's promotion story assumes
|
||||
reference count. This is the behavior ADR 0047's promotion story assumes
|
||||
and it is worth an explicit test, because nothing about the two documents'
|
||||
ids relates them.
|
||||
- **Blob sync is unaffected.** A peer receiving a promoted document sees a
|
||||
|
||||
@@ -222,7 +222,7 @@ because this is the first agent that forces the question:
|
||||
`Attachment` and `Fact`, all of which ADR 0039 or the artifact model already
|
||||
made generic.
|
||||
- **Saving the same post twice is idempotent**, and so is saving a post two
|
||||
nodes both hold. That is new behaviour for this agent — clippings
|
||||
nodes both hold. That is new behavior for this agent — clippings
|
||||
deliberately do not work that way — and is worth a test that names it.
|
||||
- **A saved post and a future feed item are one document.** Which also means
|
||||
the read/archive state Feeds will add lands on a document the user may
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# 0051: Service credentials are documents on the catalogue plane, sealed with the vault key
|
||||
# 0051: Service credentials are documents on the catalog plane, sealed with the vault key
|
||||
|
||||
**Status**: Accepted — the store shipped 2026-08-30 (`ServiceCredential`
|
||||
documents, the sealed-field derivation, Podcast Index reading from it, and
|
||||
@@ -36,7 +36,7 @@ credential reachable only on the machine where it was typed is a credential
|
||||
that works by luck.
|
||||
|
||||
**It has to be encrypted at rest.** The node secret is not
|
||||
(`TODO.md`, "Encrypt the node secret at rest"), which is defence-in-depth debt
|
||||
(`TODO.md`, "Encrypt the node secret at rest"), which is defense-in-depth debt
|
||||
for a value that never leaves the device. A user's third-party API key is meant
|
||||
to travel to every node, so it is in every backup, every synced folder and every
|
||||
stolen disk that any node touches. It is the worse thing to leave plaintext, and
|
||||
@@ -65,7 +65,7 @@ ServiceCredential
|
||||
would be a cleaner-looking home and would need a wire format, a merge rule, a
|
||||
tombstone story and a place in the sync protocol — all for one table, and all
|
||||
duplicating what `p2p::doc_sync` already does. The store layer owning the
|
||||
schema (ADR 0049's neighbourhood) does not mean every concept deserves a table.
|
||||
schema (ADR 0049's neighborhood) does not mean every concept deserves a table.
|
||||
|
||||
`Provenance::Machinery` is exactly the classifier for it, and ADR 0038 already
|
||||
argues why: hidden from general listings, from search, and from wikilink
|
||||
@@ -102,7 +102,7 @@ derived from `(service, field_name)` the way `ContactMethod`'s derives from
|
||||
`(protocol, value)`. Two nodes given the same key converge on one row;
|
||||
re-entering a key overwrites rather than accumulating.
|
||||
|
||||
### It rides the catalogue plane, and is sealed inline
|
||||
### It rides the catalog plane, and is sealed inline
|
||||
|
||||
```rust
|
||||
ArtifactValue::SealedSecret { ciphertext: Vec<u8>, plaintext_hash: [u8; 32] }
|
||||
@@ -114,7 +114,7 @@ content is the *content plane*: policy-governed, fetched on demand, and legitima
|
||||
absent on a node that has not pulled it (ADR 0009). A credential absent on the
|
||||
node that happens to be awake is the exact failure this ADR exists to prevent,
|
||||
and `SyncPolicy::Mirror` would only narrow the window rather than close it. The
|
||||
catalogue plane syncs eagerly and completely, which is what something small and
|
||||
catalog plane syncs eagerly and completely, which is what something small and
|
||||
needed everywhere wants. `has_blob_content` therefore stays false for this type
|
||||
and it gets no sync-policy row — correctly, since there is no choice to offer.
|
||||
|
||||
|
||||
+3
-3
@@ -38,7 +38,7 @@ transition alone.
|
||||
| [0006](0006-nativelib-ffi-result-convention.md) | `NativeLib` FFI calls return `Result<T>` | Accepted |
|
||||
| [0007](0007-android-media-playback-via-mediasessionservice.md) | Android media playback lives in a `MediaSessionService` | Accepted |
|
||||
| [0008](0008-android-navigation-navhost-viewmodel.md) | Android navigation: `NavHost` + `ViewModel`, type-safe routes | Accepted |
|
||||
| [0009](0009-two-plane-sync-catalogue-vs-content.md) | Two-plane sync — catalogue always replicates, content is policy-gated | Accepted |
|
||||
| [0009](0009-two-plane-sync-catalog-vs-content.md) | Two-plane sync — catalog always replicates, content is policy-gated | Accepted |
|
||||
| [0010](0010-syncpolicy-three-variant-pin-exclude.md) | Three-variant `SyncPolicy`, plus per-document Pin/Exclude overrides | Accepted |
|
||||
| [0011](0011-jobs-first-class-model.md) | Jobs as a first-class model, deduplicated by (kind, subject) | Accepted |
|
||||
| [0012](0012-continuous-sync-scheduler.md) | Continuous sync scheduler — three passes, pull-only | Accepted |
|
||||
@@ -61,7 +61,7 @@ transition alone.
|
||||
| [0029](0029-content-possession-evidence-and-release-propagation.md) | Per-node content possession evidence for every artifact, with first-hand absence and promulgated eviction releases | Accepted |
|
||||
| [0030](0030-note-body-crdt-loro.md) | Note bodies merge with Loro — one CRDT document per `NoteBody`, plaintext kept beside it | Accepted |
|
||||
| [0031](0031-sync-guarded-view-state.md) | `SyncGuarded<T>` — one generic type for "alert, don't apply", not one ad hoc trio per view | Accepted |
|
||||
| [0032](0032-visual-direction.md) | Visual direction — charcoal ground, cobalt for state, colour never for categories; Charcoal/Slate grounds defaulting to Charcoal | Proposed |
|
||||
| [0032](0032-visual-direction.md) | Visual direction — charcoal ground, cobalt for state, color never for categories; Charcoal/Slate grounds defaulting to Charcoal | Proposed |
|
||||
| [0033](0033-availability-marking.md) | Marking availability — a rule, a word, and a tooltip, chosen by the room each surface has; the full enum until the point of use | Accepted |
|
||||
| [0034](0034-filtering-a-document-tree.md) | Filtering a document tree — matches in place, ancestors as context, emphasis on the match | Accepted |
|
||||
| [0035](0035-pdf-metadata-in-lib-via-hayro.md) | A PDF's own metadata is read in lib, by `hayro-syntax` | Accepted |
|
||||
@@ -80,5 +80,5 @@ transition alone.
|
||||
| [0048](0048-shared-blob-references.md) | Content shared by several documents — liveness by reference count, and sharing disclosed wherever it changes a number | Proposed |
|
||||
| [0049](0049-parentage-stays-a-field.md) | Parentage stays a field on the document; edges are for facts | Accepted |
|
||||
| [0050](0050-captured-posts.md) | Captured posts — a `Post` document beside `Snippet`, under one capture agent renamed Clippings | Proposed |
|
||||
| [0051](0051-service-credentials.md) | Service credentials are documents on the catalogue plane, sealed with the vault key | Accepted |
|
||||
| [0051](0051-service-credentials.md) | Service credentials are documents on the catalog plane, sealed with the vault key | Accepted |
|
||||
| [0052](0052-two-local-listeners-and-what-each-trusts.md) | Two local listeners, and what each one is allowed to trust | Accepted |
|
||||
|
||||
@@ -634,7 +634,7 @@ Each step is independently shippable and independently revertible.
|
||||
The palette always searches globally — every source stays reachable at all
|
||||
times — but the **active agent's results are surfaced first**.
|
||||
|
||||
Implement this as **grouping, not score bias**: render labelled sections with
|
||||
Implement this as **grouping, not score bias**: render labeled sections with
|
||||
the current agent's first, rather than boosting its items inside one flat
|
||||
ranked list. A score bonus produces the case where a clearly better match from
|
||||
another agent sits below a mediocre local one with nothing on screen explaining
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
A revision of the *user-facing* half of `docs/sync-management.md`. That
|
||||
document's model — two planes, per-node policy, pull-only reconciliation — is
|
||||
right and is built. What it did not design is how a person **sees** whether
|
||||
the model is being honoured, and that omission is why the system reads as
|
||||
the model is being honored, and that omission is why the system reads as
|
||||
broken even where it works.
|
||||
|
||||
Nothing here changes the wire protocols or the plane split. It changes what
|
||||
@@ -33,7 +33,7 @@ and the code implements it faithfully. But the UI offers, on node A:
|
||||
`settings/ensemble/mod.rs:318`) that calls
|
||||
`scheduler::sync_with_peer` (`lib/src/scheduler/mod.rs:151`).
|
||||
|
||||
`sync_with_peer` runs a bidirectional *catalogue* exchange, then runs passes 2
|
||||
`sync_with_peer` runs a bidirectional *catalog* exchange, then runs passes 2
|
||||
and 3 **against the local node's own wants**. So setting B's Music policy to
|
||||
`Mirror` on A and then pressing Sync on A produces exactly what was reported:
|
||||
B's document hierarchy fills in (doc-sync is bidirectional), and no audio
|
||||
@@ -132,7 +132,7 @@ to be holding.
|
||||
And the unit of all three is **content**: blob-backed artifacts
|
||||
(`PrimaryContent`, `Attachment`), which is what the two-plane split already
|
||||
says is the only policy-governed thing. Notes, playback positions, tags,
|
||||
thumbnails ride the catalogue plane unconditionally and must never appear in
|
||||
thumbnails ride the catalog plane unconditionally and must never appear in
|
||||
a policy UI — showing them there is what makes people ask whether policy
|
||||
"applies to documents or artifacts."
|
||||
|
||||
@@ -183,7 +183,7 @@ pub struct ContentLedger {
|
||||
}
|
||||
|
||||
pub struct TypeLedger {
|
||||
/// Everything of this type the catalogue knows about. The denominator.
|
||||
/// Everything of this type the catalog knows about. The denominator.
|
||||
pub files_total: u32, pub bytes_total: u64,
|
||||
/// On disk right now, whatever policy says.
|
||||
pub files_here: u32, pub bytes_here: u64,
|
||||
@@ -240,7 +240,7 @@ instructing a peer is a first-class act instead of an implementation detail
|
||||
behind a picker.
|
||||
|
||||
- Ensemble → per-member **"Sync"** becomes **"Sync both ways"**, and does both
|
||||
halves: the catalogue exchange and this node's own content pull, *plus* a
|
||||
halves: the catalog exchange and this node's own content pull, *plus* a
|
||||
best-effort `node_admin::trigger_sync(peer)` so the peer runs its own three
|
||||
passes. Sent *before* the local content pass, not after — a peer made to
|
||||
wait out a multi-minute download here would look, from its own screen,
|
||||
@@ -308,7 +308,7 @@ Each step is independently useful, and the first two are most of the value.
|
||||
tallies, which is why a type's "bytes stored" figure will read lower than
|
||||
before — the two numbers on that row are now commensurable.
|
||||
2. **Sync-both-ways — done.** `scheduler::sync_with_peer` sends
|
||||
`node_admin::trigger_sync` after the catalogue exchange and before its own
|
||||
`node_admin::trigger_sync` after the catalog exchange and before its own
|
||||
content pass, and reports the outcome as `RemoteReconcile` on the summary.
|
||||
One shared formatter (`copy::PeerSyncCopy`) replaced four hand-rolled status
|
||||
lines across the GUI, TUI, CLI, and Android, all of them leading with a
|
||||
@@ -399,7 +399,7 @@ Each step is independently useful, and the first two are most of the value.
|
||||
that break here. See §2.
|
||||
- ~~**Should Ensemble → Reconcile fire `TriggerSync` on the peer?**~~
|
||||
Settled: yes, and the pull-only rule is demoted to a transport invariant
|
||||
(§3.2). The button is labelled "Sync both ways" so it is not commanding
|
||||
(§3.2). The button is labeled "Sync both ways" so it is not commanding
|
||||
another node from an ambiguous label.
|
||||
- ~~**Is `Stream` still the right default for every type?**~~ Settled:
|
||||
**`KeepOnOpen` on the desktop, `Stream` on mobile.** A desktop that holds
|
||||
@@ -420,7 +420,7 @@ Each step is independently useful, and the first two are most of the value.
|
||||
press the button is the common case, not the exotic one.
|
||||
|
||||
**Queue the directive** as the near-term answer, so pressing the button is
|
||||
durable rather than best-effort. What the *right* behaviour is against a
|
||||
durable rather than best-effort. What the *right* behavior is against a
|
||||
real sleeping phone — how long a queued directive stays valid, whether it
|
||||
survives the target's policy changing underneath it, whether the user should
|
||||
see a pending queue at all — is a question for experiment rather than
|
||||
|
||||
+4
-4
@@ -13,7 +13,7 @@
|
||||
| **Edge** | A typed, directed relationship between two Documents, orthogonal to tree hierarchy |
|
||||
|
||||
The distinction between Property and Artifact is conceptual, not about size. A Property is
|
||||
data the system needs to display and organise documents at all times — titles, tags,
|
||||
data the system needs to display and organize documents at all times — titles, tags,
|
||||
immutable facts about origin. An Artifact is data representing the user's engagement with
|
||||
or the actual content of the document — note body, reading position, the PDF bytes
|
||||
themselves. Some Artifact kinds are *blob-backed*: the artifact row keeps its UUID identity
|
||||
@@ -118,7 +118,7 @@ an empty list, and no `PropertyKind` grows a sentinel value.
|
||||
|
||||
#### Property vs. Artifact: what actually decides which one
|
||||
|
||||
The Terminology table's distinction — "needed to display/organise a Document at all times"
|
||||
The Terminology table's distinction — "needed to display/organize a Document at all times"
|
||||
vs. "user engagement or content, may be absent" — is about what a field is *for*, not how
|
||||
narrowly it's named. A field scoped to one `DocumentType` (`PodcastEpisodeApiId`) is not
|
||||
automatically Artifact material just because no other type shares it, and a broadly-useful
|
||||
@@ -355,7 +355,7 @@ MergeStrategy =
|
||||
```
|
||||
|
||||
`Crdt` is a bare variant, not `Crdt(CrdtKind)`: one library and one shape (text) are in
|
||||
play, and enumerating hypothetical CRDT flavours would be generality with no second case to
|
||||
play, and enumerating hypothetical CRDT flavors would be generality with no second case to
|
||||
justify it.
|
||||
|
||||
Each `ArtifactKind` also has an `Arity`:
|
||||
@@ -905,7 +905,7 @@ a Node holds a given blob's bytes is answered by querying the blob store directl
|
||||
|
||||
### Local Document States
|
||||
|
||||
Because artifact values and blob bytes can lag behind the catalogue, a document can exist
|
||||
Because artifact values and blob bytes can lag behind the catalog, a document can exist
|
||||
on a node across a spectrum of completeness. The axes are independent — a node can have a
|
||||
note body value without having the PDF bytes, etc. — but the common progression is:
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ Gaps that both reference systems treat as core:
|
||||
## 2. Monica
|
||||
|
||||
Monica exists in two generations with materially different data models. Both are
|
||||
worth mining: v4 for its mature feature set, v5 for the modelling lessons the
|
||||
worth mining: v4 for its mature feature set, v5 for the modeling lessons the
|
||||
rewrite encodes.
|
||||
|
||||
### 2.1 Monica v4 (the `4.x` branch — the mature one)
|
||||
|
||||
@@ -113,7 +113,7 @@ unwrapped key to run at all. ADR 0052 has that argument in full.
|
||||
total transfer timeout, so one dead peer can't block a healthy one — but the worst case
|
||||
still pays each timeout in series, and multiple healthy peers go unused.
|
||||
- **Details**: Trade-offs to weigh —
|
||||
- Racing the *same* large blob across peers wastes bandwidth; cancelling in-flight
|
||||
- Racing the *same* large blob across peers wastes bandwidth; canceling in-flight
|
||||
transfers once one wins adds complexity.
|
||||
- Better middle ground: shard *distinct* missing items across peers, and/or race only the
|
||||
connect/handshake phase and commit the transfer to the first responder.
|
||||
@@ -148,7 +148,7 @@ is itself the disruption.
|
||||
|
||||
Note this is what the Ensemble screen's per-peer `Connected`/`Degraded` badge
|
||||
was reaching for and getting wrong. That badge answered "is this peer a gossip
|
||||
neighbour", which is a fact about iroh-gossip's bounded partial mesh and not
|
||||
neighbor", which is a fact about iroh-gossip's bounded partial mesh and not
|
||||
about anything the user was doing; it was removed rather than corrected (ADR
|
||||
0032's amendment on the reachability matrix). The question worth answering was
|
||||
never "which nodes can I see" in the abstract — it is "is the thing I am doing
|
||||
|
||||
@@ -88,7 +88,7 @@ Three separable layers, on top of one prerequisite.
|
||||
category of document generated by the agent rather than imported, linked by `Edge`s to
|
||||
each other and to tracks. Additive: it never touches a track's `parent_id`, so the user's
|
||||
folder tree survives untouched and the library can be discarded and rebuilt at will.
|
||||
3. **The build job** — a user-triggered, cancellable, progress-reporting long-running job
|
||||
3. **The build job** — a user-triggered, cancelable, progress-reporting long-running job
|
||||
that runs layers 1 and 2 over every `MusicTrack`, plus an incremental single-track path so
|
||||
newly imported tracks join an existing library without a full rebuild.
|
||||
|
||||
@@ -167,7 +167,7 @@ New unconditional dependency in `rust/lib/Cargo.toml` (workspace-pinned like the
|
||||
|
||||
Tags are **Properties**, not Artifacts, by `docs/data-model.md`'s stated test: a node missing
|
||||
a track's artist is in an error state that should be re-requested on next sync, not a normal
|
||||
"user hasn't engaged yet" state. They are needed to *organise* the document — the library
|
||||
"user hasn't engaged yet" state. They are needed to *organize* the document — the library
|
||||
graph is literally computed from them.
|
||||
|
||||
| Kind | Value | Merge | Notes |
|
||||
@@ -265,13 +265,13 @@ impl DocumentType {
|
||||
}
|
||||
```
|
||||
|
||||
Hand-declared, like the neighbouring `has_blob_content()`, and with the same instruction to
|
||||
Hand-declared, like the neighboring `has_blob_content()`, and with the same instruction to
|
||||
keep it in step with `docs/data-model.md`.
|
||||
|
||||
Consequences of being derived, all of which are the reason to name the category at all:
|
||||
|
||||
- **Hidden from general document lists by default.** `SearchParams` gains
|
||||
`include_derived: bool` (default `false`), honoured by `search_documents` and
|
||||
`include_derived: bool` (default `false`), honored by `search_documents` and
|
||||
`search_document_items`, with the rule that an explicit `document_types` filter naming a
|
||||
derived type overrides it — so "show me every album" is one call and the Documents agent's
|
||||
browse view doesn't fill up with four hundred albums. The Documents agent gets a "show
|
||||
@@ -480,8 +480,8 @@ pub fn build_library(
|
||||
|
||||
`build_library` takes `Option<&JobHandle>` directly rather than inventing a callback
|
||||
abstraction — `jobs` is the same crate, `JobHandle` already exposes exactly
|
||||
`report_progress` / `is_cancelled`, and tests pass `None`. It reports progress every N tracks
|
||||
(not every track — each report is a SQLite write plus a broadcast) and checks `is_cancelled`
|
||||
`report_progress` / `is_canceled`, and tests pass `None`. It reports progress every N tracks
|
||||
(not every track — each report is a SQLite write plus a broadcast) and checks `is_canceled`
|
||||
on the same cadence; cancellation leaves a partially built graph, which is valid and
|
||||
converges on the next run precisely because every id is deterministic.
|
||||
|
||||
@@ -524,7 +524,7 @@ different kind.
|
||||
rebuilt yet. It is harmless — with no edges it holds no tracks and shows up in no library
|
||||
view — and the next build removes it again, converging once every node has run one.
|
||||
|
||||
**A cancelled run must not sweep.** Half the tracks having been visited makes every
|
||||
**A canceled run must not sweep.** Half the tracks having been visited makes every
|
||||
unvisited album look empty, so sweeping a partial run would delete the rest of the
|
||||
library. There is a test for exactly this.
|
||||
5. Return `LibraryBuildSummary { tracks_scanned, tracks_reused, tracks_skipped_not_local,
|
||||
@@ -565,7 +565,7 @@ per track out of about eight.
|
||||
|
||||
The remaining per-track cost is the two `add_edge` calls and the stale-album check, all of
|
||||
which open their own connection. Batching those would need an edge-level equivalent of the
|
||||
property batch and is not obviously worth it — this is an explicit, cancellable job with a
|
||||
property batch and is not obviously worth it — this is an explicit, cancelable job with a
|
||||
progress bar, not an interactive path. The `list_blob_refs_for_documents` addition and the
|
||||
per-chunk transaction sketched earlier were **not** implemented for the same reason; revisit
|
||||
if a real library makes the wait annoying.
|
||||
@@ -612,7 +612,7 @@ dance.
|
||||
and port `notebook.rs`'s `create_notebook`/`list_notebooks`/`require_collection_parent` to
|
||||
it; add `DocumentType::is_container()` and use it for the `matches!(.., Collection)`
|
||||
ancestor-walk checks in `pictures.rs`, `music.rs`, and `comics.rs`.
|
||||
- `SearchParams::include_derived` (default `false`), honoured by `search_documents` and
|
||||
- `SearchParams::include_derived` (default `false`), honored by `search_documents` and
|
||||
`search_document_items`, overridden by an explicit derived type filter.
|
||||
- Arms in `title_property_kind`, `display_name`, and `has_blob_content` (both `false`).
|
||||
- Materialization: **not** `Directory`, as an earlier draft of this said. A derived
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
- hence the name
|
||||
|
||||
#framed[
|
||||
"Ra is a single entity distributed across the whole solar system. No part of Ra is slaved to any other part of it. No shell-Earth's core node has a noticeably differing personality from the others, or from the megastructure inside the Sun. Opinions and behaviour and available information are continuously synchronising. Eventually, there is only one Ra."
|
||||
"Ra is a single entity distributed across the whole solar system. No part of Ra is slaved to any other part of it. No shell-Earth's core node has a noticeably differing personality from the others, or from the megastructure inside the Sun. Opinions and behavior and available information are continuously synchronising. Eventually, there is only one Ra."
|
||||
- #link("https://qntm.org/ra")[Ra], by qntm
|
||||
]
|
||||
]
|
||||
|
||||
+5
-5
@@ -159,7 +159,7 @@ scheme breaks. It is also why `blake3` (already a dependency) is used rather
|
||||
than a hash of the whole entry — the entry's field order and whitespace are not
|
||||
stable across feed-rs versions, but title and date are.
|
||||
|
||||
### 2.4 Fetch behaviour
|
||||
### 2.4 Fetch behavior
|
||||
|
||||
`fetch.rs` wraps `crate::http::build_async_client` with:
|
||||
|
||||
@@ -320,7 +320,7 @@ a tombstoned id forever. If a feed's id were `uuidv5(feed_url)`, then
|
||||
*unsubscribe → resubscribe to the same URL* would derive an id the ensemble has
|
||||
already permanently killed, and the feed could never be added again. Subscribing
|
||||
is a rare, explicit, user-initiated act, so the concurrent-creation window is
|
||||
small; a v4 id makes unsubscribe-as-tombstone correct, which is the behaviour
|
||||
small; a v4 id makes unsubscribe-as-tombstone correct, which is the behavior
|
||||
that actually matters (unsubscribing on one node must not be undone by a peer
|
||||
that still has the feed).
|
||||
|
||||
@@ -562,7 +562,7 @@ override (`FeedRefreshIntervalMins`, `U32`, LWW) *is* user-scoped and would be a
|
||||
|
||||
Add one `JobKind` variant, display name `"Feed refresh"`. Subject:
|
||||
`JobSubject::Document(feed_id)` for one feed, `JobSubject::None` for a
|
||||
refresh-all pass. This buys, for free, the exact behaviour `JobKind::BackgroundSync`
|
||||
refresh-all pass. This buys, for free, the exact behavior `JobKind::BackgroundSync`
|
||||
documents: a user hammering the "Refresh" button dedups against the in-flight
|
||||
scheduled pass rather than stacking N concurrent fetches, and the whole thing
|
||||
shows up in the Jobs agent with progress like every other long-running
|
||||
@@ -574,7 +574,7 @@ a 304 short-circuiting to `RefreshOutcome::default()`. These are synced
|
||||
properties even though they describe *this node's* last fetch, which looks
|
||||
wrong for about a second and then isn't: a node that receives a peer's validator
|
||||
and gets a 304 has skipped a fetch whose items are already reaching it through
|
||||
catalogue sync, which is eager for properties and inline artifacts. The failure
|
||||
catalog sync, which is eager for properties and inline artifacts. The failure
|
||||
mode is "fetched less often", not "missing data".
|
||||
|
||||
### 5.3 Retention
|
||||
@@ -607,7 +607,7 @@ destinations: **Unread** (default) / **All** / **Starred**. The subscription
|
||||
sidebar sits alongside the list within the screen; selecting a feed scopes the
|
||||
active tab to it.
|
||||
|
||||
Per CLAUDE.md's GUI conventions: named constants for spacing/colour rather than
|
||||
Per CLAUDE.md's GUI conventions: named constants for spacing/color rather than
|
||||
in-situ literals, functional subcomponents with meaningful names rather than one
|
||||
long render, and copyable text elements. The item's `link` should use the
|
||||
standard truncated-value-with-full-value-available component pattern the project
|
||||
|
||||
+26
-26
@@ -12,8 +12,8 @@ the per-phase detail and the ADR for each.
|
||||
|
||||
| Piece | Today |
|
||||
|---|---|
|
||||
| Catalogue sync (`/syn/doc-sync/…`) | Implemented and automatic — `scheduler::run_catalogue_pass` walks every reachable peer on a timer, and the Ensemble screen's per-peer "Sync both ways" is now one trigger among several |
|
||||
| Blob / artifact value transfer | Implemented and automatic — `run_catalogue_fetch_pass` takes artifact stubs and thumbnails eagerly, `run_content_pass` takes the policy deficit; the detail screen's buttons stay for on-demand pulls |
|
||||
| Catalog sync (`/syn/doc-sync/…`) | Implemented and automatic — `scheduler::run_catalog_pass` walks every reachable peer on a timer, and the Ensemble screen's per-peer "Sync both ways" is now one trigger among several |
|
||||
| Blob / artifact value transfer | Implemented and automatic — `run_catalog_fetch_pass` takes artifact stubs and thumbnails eagerly, `run_content_pass` takes the policy deficit; the detail screen's buttons stay for on-demand pulls |
|
||||
| `SyncPolicy` (Stream/KeepOnOpen/Mirror per type) | Enforced — `content_ledger` resolves it into a deficit that the scheduler fetches and the eviction flow reads, so "want" and "keep" cannot drift apart |
|
||||
| `SyncDisposition` (per-peer eager push) | Deleted — every node satisfies its own policy by pulling; see "What happens to `SyncDisposition`" below |
|
||||
| "Sync continuously" toggle (doc detail, per peer) | Replaced by `DocumentSyncOverride` (Pin/Exclude), which is per document rather than per peer |
|
||||
@@ -25,7 +25,7 @@ the per-phase detail and the ADR for each.
|
||||
|
||||
Everything a node holds is split into two planes with different sync rules:
|
||||
|
||||
1. **The catalogue plane** — document rows, properties, artifact *values*
|
||||
1. **The catalog plane** — document rows, properties, artifact *values*
|
||||
(including the `BlobRef` pointer values of blob-backed kinds — see the
|
||||
Artifact/Blob unification in `data-model.md`), tombstones, and presence
|
||||
records. This plane is **always
|
||||
@@ -40,7 +40,7 @@ Everything a node holds is split into two planes with different sync rules:
|
||||
only plane where storage cost is real.
|
||||
|
||||
The one deliberate upgrade hiding in this split: **artifact values move from
|
||||
the content plane to the catalogue plane.** Today artifact values (note
|
||||
the content plane to the catalog plane.** Today artifact values (note
|
||||
bodies, `ListenProgress`, `ReadingPosition`, bookmarks) are fetched on demand
|
||||
alongside blobs. They are exactly the data the user wants everywhere as fast
|
||||
as possible — small, high-value, cross-device (pause a podcast on the phone,
|
||||
@@ -54,13 +54,13 @@ holds — eager fetching is a scheduler behavior, not a new invariant.
|
||||
|
||||
With the Artifact/Blob unification (phase 0 below), the plane assignment
|
||||
stops being a special-cased rule and becomes a type-system fact: the
|
||||
catalogue plane is simply *all artifact values* — for `Storage::BlobStore`
|
||||
catalog plane is simply *all artifact values* — for `Storage::BlobStore`
|
||||
kinds the value is the `BlobRef` pointer — and the content plane is the
|
||||
bytes those pointers address. Nothing large can sneak onto the catalogue
|
||||
bytes those pointers address. Nothing large can sneak onto the catalog
|
||||
plane without someone deliberately declaring a new inline kind with a large
|
||||
payload, which is a code-review-visible act.
|
||||
|
||||
`Thumbnail`-kind bytes also ride the catalogue plane: they are small, and
|
||||
`Thumbnail`-kind bytes also ride the catalog plane: they are small, and
|
||||
browsing a library without cover art is a degraded experience. A size cap
|
||||
(e.g. skip thumbnails > 1 MiB) guards against something mislabeled as a
|
||||
thumbnail.
|
||||
@@ -174,7 +174,7 @@ Resolution order for "should node N hold content for document D":
|
||||
**Deleted.** Its job ("eagerly push type T to peer P") is subsumed by a
|
||||
cleaner rule that matches the decentralized architecture: **every node
|
||||
satisfies its own policy by pulling.** When the desktop imports an album, the
|
||||
server learns about it via continuous catalogue sync and pulls the audio
|
||||
server learns about it via continuous catalog sync and pulls the audio
|
||||
because *its own* policy says `Mirror(MusicTrack)`. No node needs a table of
|
||||
opinions about what other nodes should receive; you edit the *server's* policy
|
||||
(possibly remotely) instead of editing a push-table on every other node. This
|
||||
@@ -211,12 +211,12 @@ NAT-challenged peer) and as an immediacy affordance. It just isn't a policy.
|
||||
A background component in `lib` (per node, all modalities) that makes the
|
||||
two-plane promise true without button-pressing. It runs three passes:
|
||||
|
||||
1. **Catalogue pass** — `sync_from_peer` (incremental, `since` = last
|
||||
1. **Catalog pass** — `sync_from_peer` (incremental, `since` = last
|
||||
successful sync per peer) against each reachable ensemble peer.
|
||||
Triggers: node startup; a peer's gossip announcement (a node that just
|
||||
came online syncs with it); a periodic timer (default 15 min); after any
|
||||
local import or policy change; manual "Sync now."
|
||||
2. **Catalogue-plane fetch pass** — after a catalogue pass: fetch all
|
||||
2. **Catalog-plane fetch pass** — after a catalog pass: fetch all
|
||||
pending artifact stubs (existing artifact-sync) and missing thumbnail
|
||||
bytes (existing blob-sync) from whichever peers hold them.
|
||||
3. **Content pass** — compute **wants**: blob hashes where the resolution
|
||||
@@ -241,20 +241,20 @@ charging" constraints. Defaults: on, 15 min, Wi-Fi-only for content pass.
|
||||
`docs/adr/0012-continuous-sync-scheduler.md` for exactly which triggers
|
||||
shipped vs. were deliberately deferred, and each modality's driver.
|
||||
|
||||
### On-demand catalogue refresh for one document
|
||||
### On-demand catalog refresh for one document
|
||||
|
||||
The scheduler's catalogue pass is the *whole* catalogue on a timer, which
|
||||
The scheduler's catalog pass is the *whole* catalog on a timer, which
|
||||
leaves a gap the user feels directly: between two passes, this node's idea of
|
||||
a document is however stale the last pass left it. "Sync this document to
|
||||
this node" fetching only what the local catalogue already flags as missing
|
||||
this node" fetching only what the local catalog already flags as missing
|
||||
means a document whose value changed on another node reports itself as
|
||||
already up to date — the local catalogue's opinion, presented as the
|
||||
already up to date — the local catalog's opinion, presented as the
|
||||
ensemble's.
|
||||
|
||||
So the per-document pull runs both planes, in order
|
||||
(`asset_sync::sync_document_assets_recursive`):
|
||||
|
||||
1. **Catalogue phase** (`asset_sync::refresh_document_catalogue`) — one
|
||||
1. **Catalog phase** (`asset_sync::refresh_document_catalog`) — one
|
||||
doc-sync exchange per ensemble peer, scoped to this document's subtree,
|
||||
with no `since` watermark. Filtering by the peer's watermark here would
|
||||
skip exactly the case worth checking: a peer-side change older than the
|
||||
@@ -282,7 +282,7 @@ result string says which of the two it is (`gui-app`'s `detail_view.rs`,
|
||||
|
||||
A first-class **Jobs** agent (sidebar entry, `Icon::ArrowsUpDown` or
|
||||
similar), generalizing the "Document Imports" placeholder. Everything
|
||||
long-running routes through it: imports, catalogue syncs, content fetches,
|
||||
long-running routes through it: imports, catalog syncs, content fetches,
|
||||
pushes, evictions, purge propagation. This is infrastructure the rest of this
|
||||
design assumes, so it lands first in the phasing.
|
||||
|
||||
@@ -291,11 +291,11 @@ design assumes, so it lands first in the phasing.
|
||||
```rust
|
||||
Job {
|
||||
id: Uuid,
|
||||
kind: JobKind, // Import | CatalogueSync | ContentFetch | Push
|
||||
kind: JobKind, // Import | CatalogSync | ContentFetch | Push
|
||||
// | Eviction | Purge
|
||||
subject: JobSubject, // human label + optional document id / peer pubkey
|
||||
origin: JobOrigin, // User | Scheduler | RemoteAdmin(NodePubkey)
|
||||
state: JobState, // Queued | Running | Done | Failed(String) | Cancelled
|
||||
state: JobState, // Queued | Running | Done | Failed(String) | Canceled
|
||||
progress: Option<JobProgress>, // units done/total + bytes done/total
|
||||
created_at / started_at / finished_at: Timestamp,
|
||||
}
|
||||
@@ -324,7 +324,7 @@ Job {
|
||||
│ ───────────────────────────────────────────────────│
|
||||
│ ⣾ Content fetch — Album "Blackstar" (from helios) │
|
||||
│ ██████████░░░░░░░░ 312 MiB / 512 MiB Cancel │
|
||||
│ ⣾ Catalogue sync — helios docs 214/~800 │
|
||||
│ ⣾ Catalog sync — helios docs 214/~800 │
|
||||
│ ⏸ Import — ~/comics/Akira/ queued Cancel │
|
||||
└────────────────────────────────────────────────────┘
|
||||
```
|
||||
@@ -417,10 +417,10 @@ running" badge, so a per-widget button would be a second route to something
|
||||
already one click away, whereas Android has no persistent sidebar and the link
|
||||
is the only route.
|
||||
|
||||
Not yet on either: cancelling from the widget. How promptly a cancel takes
|
||||
Not yet on either: canceling from the widget. How promptly a cancel takes
|
||||
effect is still up to the work — `asset_sync`'s functions take no
|
||||
cancellation signal, so a transfer runs to the end and the row is only
|
||||
*recorded* as `Cancelled`. The Jobs screen's Cancel already says so, and the
|
||||
*recorded* as `Canceled`. The Jobs screen's Cancel already says so, and the
|
||||
widgets simply don't offer it. See TODO.md's "Cancellation is still coarse
|
||||
for a single blocking P2P call."
|
||||
|
||||
@@ -483,7 +483,7 @@ single-subtree Eviction job (same guard, same dialog). The
|
||||
|
||||
Eviction also writes each dropped blob to `content_release_log`, which
|
||||
doc-sync ships to peers as a `ContentReleaseBatch` alongside the document
|
||||
tombstones. Without it an eviction would be invisible: the catalogue is
|
||||
tombstones. Without it an eviction would be invisible: the catalog is
|
||||
untouched by design, so an incremental sync never mentions the document again
|
||||
and every peer goes on listing this node as a holder. A release says only "I
|
||||
dropped my copy" — not "this is gone from the ensemble", which is what an
|
||||
@@ -569,7 +569,7 @@ two sections that answer the two real questions — *what's here?* and
|
||||
│ Policy for Pdf: Keep on open · Override: [ Follow policy ▾ ]
|
||||
│ [ Download now ] [ Delete Locally ]
|
||||
│
|
||||
│ Across your ensemble ↻ from catalogue + presence
|
||||
│ Across your ensemble ↻ from catalog + presence
|
||||
│ node content last confirmed keep here?
|
||||
│ helios ✓ all today 14:02 Pin ▾ [Push now]
|
||||
│ phone ✗ none — Follow ▾ [Push now]
|
||||
@@ -594,7 +594,7 @@ Android mirrors this structure in its existing detail screen sections.
|
||||
## What the Ensemble screen keeps
|
||||
|
||||
It stays the *connectivity* surface: membership, ping, "Sync now" (submits a
|
||||
CatalogueSync job), last-synced (now scheduler-fed), and a "Manage sync…" link
|
||||
CatalogSync job), last-synced (now scheduler-fed), and a "Manage sync…" link
|
||||
into the policy screen per node. Per-document and per-type controls never live
|
||||
here. Note it does not claim per-peer reachability: Ping is the probe, and ADR
|
||||
0032's amendment on the reachability matrix says why there is no passive
|
||||
@@ -654,9 +654,9 @@ earlier ones. (Per VCS style: many small commits within each.)
|
||||
**That button was, for a while, the only way to make a remote
|
||||
`Mirror`-policy node actually fetch** — and it sits behind a node picker
|
||||
on a settings tab, so in practice nobody found it. The Ensemble screen's
|
||||
per-member "Sync" pulled catalogue + content *into the local node* only,
|
||||
per-member "Sync" pulled catalog + content *into the local node* only,
|
||||
computed against the local node's own policy, leaving the remote node with
|
||||
metadata (catalogue sync is bidirectional) and no blobs until its next
|
||||
metadata (catalog sync is bidirectional) and no blobs until its next
|
||||
periodic tick. `scheduler::sync_with_peer` now sends `TriggerSync` itself
|
||||
and the button says "Sync both ways"; see the correction under "What
|
||||
happens to `SyncDisposition`" above and
|
||||
|
||||
@@ -399,7 +399,7 @@ lint-rust-nix *args:
|
||||
# Apply clippy's machine-applicable suggestions across the workspace.
|
||||
#
|
||||
# Only rewrites what clippy is confident about (needless borrows, useless
|
||||
# conversions, collapsible ifs); findings that need a judgement call — how to
|
||||
# conversions, collapsible ifs); findings that need a judgment call — how to
|
||||
# factor a complex type, whether an argument list wants a struct — are reported
|
||||
# but left alone. Review the diff: it is mechanical, not automatically right.
|
||||
[doc: "Apply clippy's mechanical fixes across the workspace"]
|
||||
|
||||
@@ -287,16 +287,16 @@ pub fn setup_document_callbacks(
|
||||
let paths = dialog.pick_files();
|
||||
|
||||
let Some(paths) = paths else {
|
||||
info!("Document import cancelled by user");
|
||||
info!("Document import canceled by user");
|
||||
return;
|
||||
};
|
||||
|
||||
let node = match local_node.get().copied() {
|
||||
Some(pk) => pk,
|
||||
None => {
|
||||
log::error!("Node not initialised — cannot import documents");
|
||||
log::error!("Node not initialized — cannot import documents");
|
||||
if let Some(ui) = ui_weak.upgrade() {
|
||||
ui.set_documents_error("Node not initialised".into());
|
||||
ui.set_documents_error("Node not initialized".into());
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -363,7 +363,7 @@ pub fn setup_document_callbacks(
|
||||
match rfd::FileDialog::new().set_title("Import Folder").pick_folder() {
|
||||
Some(path) => path.to_string_lossy().to_string().into(),
|
||||
None => {
|
||||
info!("Directory import cancelled by user");
|
||||
info!("Directory import canceled by user");
|
||||
slint::SharedString::new()
|
||||
}
|
||||
}
|
||||
@@ -382,9 +382,9 @@ pub fn setup_document_callbacks(
|
||||
let node = match local_node.get().copied() {
|
||||
Some(pk) => pk,
|
||||
None => {
|
||||
log::error!("Node not initialised — cannot import directory");
|
||||
log::error!("Node not initialized — cannot import directory");
|
||||
if let Some(ui) = ui_weak.upgrade() {
|
||||
ui.set_documents_error("Node not initialised".into());
|
||||
ui.set_documents_error("Node not initialized".into());
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -455,9 +455,9 @@ pub fn setup_document_callbacks(
|
||||
let node = match local_node.get().copied() {
|
||||
Some(pk) => pk,
|
||||
None => {
|
||||
log::error!("Node not initialised — cannot import from URL");
|
||||
log::error!("Node not initialized — cannot import from URL");
|
||||
if let Some(ui) = ui_weak.upgrade() {
|
||||
ui.set_documents_error("Node not initialised".into());
|
||||
ui.set_documents_error("Node not initialized".into());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -247,7 +247,7 @@ pub fn setup_humans_callbacks(
|
||||
return;
|
||||
}
|
||||
let Some(node) = write_node() else {
|
||||
log::error!("Node not initialised — cannot create human");
|
||||
log::error!("Node not initialized — cannot create human");
|
||||
return;
|
||||
};
|
||||
let name = full_name.to_string();
|
||||
@@ -316,7 +316,7 @@ pub fn setup_humans_callbacks(
|
||||
religion,
|
||||
notes| {
|
||||
let Some(node) = write_node() else {
|
||||
log::error!("Node not initialised — cannot save human");
|
||||
log::error!("Node not initialized — cannot save human");
|
||||
return;
|
||||
};
|
||||
let id_str = id.to_string();
|
||||
@@ -376,7 +376,7 @@ pub fn setup_humans_callbacks(
|
||||
}
|
||||
let Some(id_str) = current_human.lock().unwrap().clone() else { return };
|
||||
let Some(node) = write_node() else {
|
||||
log::error!("Node not initialised — cannot add pet");
|
||||
log::error!("Node not initialized — cannot add pet");
|
||||
return;
|
||||
};
|
||||
let name_str = name.to_string();
|
||||
@@ -411,7 +411,7 @@ pub fn setup_humans_callbacks(
|
||||
ui.on_remove_pet(move |artifact_id| {
|
||||
let Some(id_str) = current_human.lock().unwrap().clone() else { return };
|
||||
let Some(node) = write_node() else {
|
||||
log::error!("Node not initialised — cannot remove pet");
|
||||
log::error!("Node not initialized — cannot remove pet");
|
||||
return;
|
||||
};
|
||||
let artifact_str = artifact_id.to_string();
|
||||
@@ -450,7 +450,7 @@ pub fn setup_humans_callbacks(
|
||||
}
|
||||
let Some(id_str) = current_human.lock().unwrap().clone() else { return };
|
||||
let Some(node) = write_node() else {
|
||||
log::error!("Node not initialised — cannot add web presence");
|
||||
log::error!("Node not initialized — cannot add web presence");
|
||||
return;
|
||||
};
|
||||
let url_str = url.to_string();
|
||||
@@ -486,7 +486,7 @@ pub fn setup_humans_callbacks(
|
||||
ui.on_remove_web_presence(move |artifact_id| {
|
||||
let Some(id_str) = current_human.lock().unwrap().clone() else { return };
|
||||
let Some(node) = write_node() else {
|
||||
log::error!("Node not initialised — cannot remove web presence");
|
||||
log::error!("Node not initialized — cannot remove web presence");
|
||||
return;
|
||||
};
|
||||
let artifact_str = artifact_id.to_string();
|
||||
@@ -520,7 +520,7 @@ pub fn setup_humans_callbacks(
|
||||
let write_node = write_node.clone();
|
||||
ui.on_delete_human(move |id| {
|
||||
let Some(node) = write_node() else {
|
||||
log::error!("Node not initialised — cannot delete human");
|
||||
log::error!("Node not initialized — cannot delete human");
|
||||
return;
|
||||
};
|
||||
let id_str = id.to_string();
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user