Add requirements traceability gate and Gitea pipelines

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.
This commit is contained in:
2026-08-09 08:01:32 +02:00
parent 82a5e21ec6
commit 0f202fd3f9
19 changed files with 1913 additions and 22 deletions
+26 -6
View File
@@ -7,16 +7,30 @@ fn main() {
env_logger::init();
let ctx = pollster::block_on(GpuContext::new_headless()).unwrap();
println!("adapter: {}\n", ctx.adapter_name());
println!("{:>12} {:>10} {:>10} {:>8}", "size", "compute", "+readback", "fps");
println!(
"{:>12} {:>10} {:>10} {:>8}",
"size", "compute", "+readback", "fps"
);
for &(w, h) in &[(840u32, 692u32), (1280, 720), (1920, 1080), (2048, 1152), (3840, 2160)] {
for &(w, h) in &[
(840u32, 692u32),
(1280, 720),
(1920, 1080),
(2048, 1152),
(3840, 2160),
] {
let rt = RenderTarget::new(&ctx, w, h).unwrap();
// warm
for i in 0..10 { rt.render(i as f32 * 0.01); let _ = pollster::block_on(rt.read_pixels()); }
for i in 0..10 {
rt.render(i as f32 * 0.01);
let _ = pollster::block_on(rt.read_pixels());
}
let n = 40;
let t0 = Instant::now();
for i in 0..n { rt.render(i as f32 * 0.01); }
for i in 0..n {
rt.render(i as f32 * 0.01);
}
ctx.device.poll(wgpu::Maintain::Wait);
let compute = t0.elapsed().as_secs_f64() / n as f64;
@@ -27,7 +41,13 @@ fn main() {
}
let full = t1.elapsed().as_secs_f64() / n as f64;
println!("{:>5}x{:<6} {:>8.2}ms {:>8.2}ms {:>8.0}",
w, h, compute * 1000.0, full * 1000.0, 1.0 / full);
println!(
"{:>5}x{:<6} {:>8.2}ms {:>8.2}ms {:>8.0}",
w,
h,
compute * 1000.0,
full * 1000.0,
1.0 / full
);
}
}
+1
View File
@@ -1,3 +1,4 @@
/// TRACES: NFR-R7 | NFR-R8
/// Failures from the GPU layer.
///
/// `DeviceLost` is deliberately a distinct variant rather than folded into a
+5 -13
View File
@@ -116,6 +116,7 @@ struct Params {
/// Stands in for the develop pipeline in v0.1. What matters is the shape:
/// compute writes a texture, the texture is handed to the compositor, and
/// pixels never travel back through the CPU.
/// TRACES: FR-DEV-4 | R4
pub struct RenderTarget {
ctx: GpuContext,
texture: wgpu::Texture,
@@ -142,9 +143,7 @@ impl RenderTarget {
.device
.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("gradient"),
source: wgpu::ShaderSource::Wgsl(
include_str!("shaders/gradient.wgsl").into(),
),
source: wgpu::ShaderSource::Wgsl(include_str!("shaders/gradient.wgsl").into()),
});
let bind_group_layout =
@@ -282,12 +281,8 @@ impl RenderTarget {
return;
}
let (texture, view) = Self::create_texture(&self.ctx, width, height);
self.bind_group = Self::create_bind_group(
&self.ctx,
&self.bind_group_layout,
&view,
&self.params_buf,
);
self.bind_group =
Self::create_bind_group(&self.ctx, &self.bind_group_layout, &view, &self.params_buf);
self.texture = texture;
self.view = view;
self.width = width;
@@ -369,10 +364,7 @@ impl RenderTarget {
}
let buf = &slot.as_ref().unwrap().0;
let mut enc = self
.ctx
.device
.create_command_encoder(&Default::default());
let mut enc = self.ctx.device.create_command_encoder(&Default::default());
enc.copy_texture_to_buffer(
wgpu::ImageCopyTexture {
texture: &self.texture,