Ports JellyTau's traceability tooling to Rust, carrying across the bug it was repaired for. That gate divided a traced count by frozen literal denominators; the requirements file outgrew them and it reported 158% coverage, so it could never fail its own threshold. Two rules, both enforced by the extractor's own tests: - denominators parsed from docs/requirements.md at run time - coverage is |traced ∩ defined| / |defined|, never a raw traced count The gate additionally fails hard on a misconfigured run — zero requirements parsed or zero files scanned — rather than reporting a plausible 0%, and on any orphan tag naming a requirement that does not exist. Adapted for DarkRoom: IDs are FR-CAT-1 / NFR-P13 / FR-DEV-3a shapes rather than JellyTau's fixed three digits, and decisions (D), spikes (S), milestone items (M) and test ids remain taggable while being excluded from the denominator — counting them inflated it by 25. Also adds dr-sync: the RemoteBackend trait and capability model, so the Nextcloud connector is one implementation rather than the only shape the engine understands. No mature Nextcloud crate exists (reqwest_dav is too thin), so the connector will be hand-rolled over reqwest per D7. Gitea workflows follow the same style: containerised, commented with the reasoning, desktop and Android on every push, plus a CI check that no core/ crate depends on the UI toolkit (ARCH §6.5a). Coverage today: 13.3% (19/143). 50 tests passing.
140 lines
4.7 KiB
Rust
140 lines
4.7 KiB
Rust
//! What a backend can do.
|
|
//!
|
|
//! Declared rather than assumed, because the operations that matter most for
|
|
//! performance are not universal (ARCH §8.1).
|
|
|
|
/// TRACES: FR-NC-4
|
|
/// How a backend reports what changed.
|
|
///
|
|
/// This is the single most consequential capability: it determines whether a
|
|
/// no-op sync over a large library costs one request or thousands.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum ChangeDetection {
|
|
/// The backend maintains a change feed; we present a cursor and receive
|
|
/// what changed since. Cheapest possible.
|
|
DeltaCursor,
|
|
|
|
/// Directory ETags propagate upward, so an unchanged parent proves an
|
|
/// unchanged subtree. Nextcloud. One request proves a whole library
|
|
/// unchanged.
|
|
PropagatingEtags,
|
|
|
|
/// ETags exist per entry but do not propagate. The tree must be walked,
|
|
/// though ETags still prevent re-downloading unchanged content.
|
|
LocalEtags,
|
|
|
|
/// Modification times only. Walk and compare — vulnerable to clock skew
|
|
/// and coarse timestamp granularity.
|
|
Timestamps,
|
|
}
|
|
|
|
/// Constraints on chunked upload.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub struct ChunkConstraints {
|
|
pub min_chunk: u64,
|
|
pub max_chunk: u64,
|
|
/// Bodies at or below this go in a single request.
|
|
pub single_shot_below: u64,
|
|
pub max_chunks: u32,
|
|
}
|
|
|
|
/// TRACES: FR-NC-3
|
|
/// Whether the server can render thumbnails, and for what.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum ServerPreviews {
|
|
/// No server-side rendering.
|
|
None,
|
|
/// Common web formats only. **This is stock Nextcloud** — no RAW preview
|
|
/// provider ships with it, so RAW thumbnails must come from local
|
|
/// embedded-preview extraction (ARCH §6.7).
|
|
CommonFormatsOnly,
|
|
/// RAW included, e.g. via the `camerarawpreviews` app or an Imaginary
|
|
/// backend. Detected per-account, never assumed.
|
|
IncludingRaw,
|
|
}
|
|
|
|
/// The full capability set.
|
|
#[derive(Debug, Clone)]
|
|
pub struct Capabilities {
|
|
pub change_detection: ChangeDetection,
|
|
/// Identity survives server-side rename and move, so a move is not
|
|
/// mistaken for delete-plus-add of a large file.
|
|
pub stable_ids: bool,
|
|
/// Byte-range reads. Without these, embedded-preview extraction is
|
|
/// impossible and remote browsing must download whole files.
|
|
pub range_reads: bool,
|
|
pub chunked_upload: Option<ChunkConstraints>,
|
|
/// Many small objects in one request.
|
|
pub bulk_upload: bool,
|
|
/// Conditional write (If-Match), for conflict-safe sidecar updates.
|
|
pub conditional_write: bool,
|
|
pub server_previews: ServerPreviews,
|
|
}
|
|
|
|
impl Capabilities {
|
|
/// The weakest backend the engine will still drive: listing and whole-file
|
|
/// transfer, nothing more. Everything degrades but stays correct.
|
|
pub fn minimal() -> Self {
|
|
Self {
|
|
change_detection: ChangeDetection::Timestamps,
|
|
stable_ids: false,
|
|
range_reads: false,
|
|
chunked_upload: None,
|
|
bulk_upload: false,
|
|
conditional_write: false,
|
|
server_previews: ServerPreviews::None,
|
|
}
|
|
}
|
|
|
|
/// Whether remote browsing can avoid downloading whole files.
|
|
///
|
|
/// Where false, the UI must not browse a remote library on a metered
|
|
/// connection without explicit consent (FR-NC-12).
|
|
pub fn can_browse_cheaply(&self) -> bool {
|
|
self.range_reads || matches!(self.server_previews, ServerPreviews::IncludingRaw)
|
|
}
|
|
|
|
/// Whether sidecar conflicts can be detected reliably.
|
|
///
|
|
/// Without conditional writes the engine falls back to comparing revision
|
|
/// counters inside the sidecar, which narrows the race but does not close
|
|
/// it — surfaced as a reduced-safety mode.
|
|
pub fn safe_concurrent_writes(&self) -> bool {
|
|
self.conditional_write
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn minimal_backend_degrades_but_stays_usable() {
|
|
let caps = Capabilities::minimal();
|
|
assert!(!caps.can_browse_cheaply());
|
|
assert!(!caps.safe_concurrent_writes());
|
|
}
|
|
|
|
#[test]
|
|
fn server_raw_previews_substitute_for_range_reads() {
|
|
// A server that renders RAW thumbnails makes browsing cheap even
|
|
// without range support.
|
|
let caps = Capabilities {
|
|
server_previews: ServerPreviews::IncludingRaw,
|
|
..Capabilities::minimal()
|
|
};
|
|
assert!(caps.can_browse_cheaply());
|
|
}
|
|
|
|
#[test]
|
|
fn common_format_previews_do_not_help_raw() {
|
|
// Stock Nextcloud: previews exist, but not for RAW, so range reads
|
|
// remain the only cheap path.
|
|
let caps = Capabilities {
|
|
server_previews: ServerPreviews::CommonFormatsOnly,
|
|
..Capabilities::minimal()
|
|
};
|
|
assert!(!caps.can_browse_cheaply());
|
|
}
|
|
}
|