Put the plugin API post-v1, and let the matrix count it that way
The register said two things about plugins. §7 had listed "Plugin API" as deferred since the first draft, in a bare row; §3.10 then specified it in 23 clauses that counted against coverage. Twenty-one of them had no implementation of any kind, and could not have: no crate loads anything at runtime. The coverage figure was measuring the contradiction. Decided 2026-09-19: §7 is right. §3.10 stays as the design of record, each of its clauses is marked "(post-v1)" on its defining line, and NFR-SEC-6 — which exists only for plugins — goes with them, as does D16. The traceability tool learns the marker. A deferred requirement is still defined, so a tag naming it is not an orphan, but it leaves the denominator and is listed in its own table rather than under "not yet tagged". The marker must sit on the definition line; a mention of "post-v1" in prose changes nothing, and where an ID is defined twice the deferral on either line wins. Both are tested. Coverage moves from 72.2% of 194 to 80.6% of 170 without a line of application code changing, which is the honest figure: it now measures what v1 owes.
This commit is contained in:
+37
-26
@@ -1337,6 +1337,14 @@ ordinary FR-CULL-10 merge, not a special case.
|
||||
|
||||
### 3.10 Extensibility and plugins
|
||||
|
||||
> **Post-v1, decided 2026-09-19.** Every clause in this section, and NFR-SEC-6 which exists for
|
||||
> it, is marked `(post-v1)` on its defining line and is outside the v1 count. The section stays as
|
||||
> the design of record — an extension point that is built as though a plugin might one day reach it
|
||||
> is cheaper than one retrofitted — but nothing here is owed a tag, and §7's row is the statement
|
||||
> of scope. The audit that prompted this found the register saying both things at once: §7 had
|
||||
> deferred "Plugin API" in a bare row since the first draft while these 23 clauses counted against
|
||||
> coverage, which measured the contradiction rather than the software. D16 defers with the section.
|
||||
|
||||
FR-DEV-3c already buys one form of this: an operation is added by writing a declaration, and the
|
||||
develop panel grows its controls without a frontend change. This section extends that property past
|
||||
the compiler. **A plugin is a file dropped into a directory; the application uses it without being
|
||||
@@ -1352,7 +1360,7 @@ lives.
|
||||
So the classes are ordered by expense, and the rule is: **an extension point is declarative unless
|
||||
it is demonstrably impossible to make it so.**
|
||||
|
||||
**FR-PLG-1 — Three plugin classes.** The app shall support exactly three, and no fourth shall be
|
||||
**FR-PLG-1 — Three plugin classes.** *(post-v1)* The app shall support exactly three, and no fourth shall be
|
||||
introduced without a recorded decision.
|
||||
|
||||
| Class | Mechanism | Covers | Executes code |
|
||||
@@ -1371,7 +1379,7 @@ one compiler version and one build of every crate it touches; and an in-process
|
||||
the whole application's privileges, which would make NFR-SEC-4 a promise about third-party code
|
||||
rather than a property of the system.
|
||||
|
||||
**FR-PLG-1a — Plugins orchestrate; the GPU does pixels.** No plugin interface shall pass
|
||||
**FR-PLG-1a — Plugins orchestrate; the GPU does pixels.** *(post-v1)* No plugin interface shall pass
|
||||
full-resolution pixel data across a sandbox boundary. A computational plugin receives handles and
|
||||
small buffers, and expresses per-pixel work as class-1 shader passes it declares. This is what keeps
|
||||
WebAssembly's arithmetic penalty irrelevant, and it is also ARCH §6.1 applied to plugins: a plugin
|
||||
@@ -1379,7 +1387,7 @@ must not be the reason a result round-trips through the CPU.
|
||||
|
||||
#### Declarative plugins
|
||||
|
||||
**FR-PLG-2 — The node declaration is the plugin format.** The schema documented in
|
||||
**FR-PLG-2 — The node declaration is the plugin format.** *(post-v1)* The schema documented in
|
||||
`core/dr-pipeline/ops/README.md` — parameters, uniform expressions, WGSL, helpers, activity,
|
||||
presentation, attributes, order, tests — shall be readable at **load time** as well as build time,
|
||||
from a plugin directory, with no change to what a declaration means.
|
||||
@@ -1394,7 +1402,7 @@ generated `match` is faster than an interpreted one and because their tests must
|
||||
the same way and for the same reason that a declared node is today indistinguishable from a
|
||||
hand-written one.
|
||||
|
||||
**FR-PLG-2a — Fragment nodes and pass nodes.** Two templates, and a plugin author chooses by
|
||||
**FR-PLG-2a — Fragment nodes and pass nodes.** *(post-v1)* Two templates, and a plugin author chooses by
|
||||
answering one question: does this operation need to read a pixel other than its own?
|
||||
|
||||
| | Fragment node | Pass node |
|
||||
@@ -1409,20 +1417,20 @@ A declaration shall state which it is, and the cost shall be visible to the user
|
||||
listing, because a chain of pass nodes is how a fast application becomes a slow one without any
|
||||
single decision having been wrong.
|
||||
|
||||
**FR-PLG-2b — Mask generators are declarative.** `Linear` and `Radial` mask sources are already
|
||||
**FR-PLG-2b — Mask generators are declarative.** *(post-v1)* `Linear` and `Radial` mask sources are already
|
||||
geometry in normalised coordinates rasterised by a shader (ARCH §5.4). A plugin shall be able to
|
||||
contribute a mask generator on the same terms — declared parameters plus a WGSL function from
|
||||
normalised coordinates to coverage — reaching luminosity-range, colour-range, and further gradient
|
||||
forms with no code. Mask sources that are *identity into a segmentation* (`Regions`, `Subject`)
|
||||
are not declarative and belong to class 3.
|
||||
|
||||
**FR-PLG-2c — Scopes split at the existing seam.** A scope plugin is a class-1 compute shader
|
||||
**FR-PLG-2c — Scopes split at the existing seam.** *(post-v1)* A scope plugin is a class-1 compute shader
|
||||
producing a small bin buffer plus a class-2 view drawing it. This is the split already in place for
|
||||
the histogram — `dr-gpu` counts, `dr-ui` shapes, Slint draws — and it holds for waveform,
|
||||
vectorscope and RGB parade without change. The counting half shall not read back full-resolution
|
||||
pixels (ARCH §5.5).
|
||||
|
||||
**FR-PLG-2d — The vocabularies stay closed.** `WidgetKind`, `attributes`, and the parameter `kind`
|
||||
**FR-PLG-2d — The vocabularies stay closed.** *(post-v1)* `WidgetKind`, `attributes`, and the parameter `kind`
|
||||
list remain closed enumerations, and a plugin may use them but shall not extend them. The reason
|
||||
given in the node README strengthens rather than weakens here: a typo that creates a new category
|
||||
containing exactly one control is indistinguishable from a deliberate new category until somebody
|
||||
@@ -1432,7 +1440,7 @@ control that was never written.
|
||||
|
||||
#### View plugins
|
||||
|
||||
**FR-PLG-3 — Declared slots, typed contracts.** A view plugin shall be a Slint component compiled at
|
||||
**FR-PLG-3 — Declared slots, typed contracts.** *(post-v1)* A view plugin shall be a Slint component compiled at
|
||||
runtime and instantiated into a **named slot** the application declares — not a licence to draw
|
||||
anywhere in the window. Each slot states the data it provides and the callbacks it accepts, and a
|
||||
component that does not match its slot's contract shall be rejected at load with a message naming
|
||||
@@ -1441,7 +1449,7 @@ the mismatch.
|
||||
Slots are a closed list under the same reasoning as FR-PLG-2d, and the composition rules of
|
||||
ARCH §4.3a continue to apply: a slot describes what a view *is for*, never how much room it has.
|
||||
|
||||
**FR-PLG-3a — A view plugin cannot be trusted with the UI thread.** Class 2 is the one class with no
|
||||
**FR-PLG-3a — A view plugin cannot be trusted with the UI thread.** *(post-v1)* Class 2 is the one class with no
|
||||
sandbox — an interpreted component runs on the UI executor and can violate NFR-ARCH-1 by looping.
|
||||
The application shall therefore watchdog slot rendering, disable a component that exceeds a stated
|
||||
budget, and report which plugin was disabled. A view plugin that fails shall leave the slot empty
|
||||
@@ -1449,12 +1457,12 @@ and the application usable; it shall never take down the window.
|
||||
|
||||
#### Computational plugins
|
||||
|
||||
**FR-PLG-4 — One interface per extension point, versioned.** Each class-3 extension point shall
|
||||
**FR-PLG-4 — One interface per extension point, versioned.** *(post-v1)* Each class-3 extension point shall
|
||||
define an explicit interface, versioned independently, and a plugin shall declare which version it
|
||||
implements. Interfaces are the only surface a computational plugin can reach: there is no ambient
|
||||
filesystem, no network, and no access to the catalog.
|
||||
|
||||
**FR-PLG-4a — Capabilities are granted, never assumed.** A plugin that needs to read a file or
|
||||
**FR-PLG-4a — Capabilities are granted, never assumed.** *(post-v1)* A plugin that needs to read a file or
|
||||
reach the network shall declare the capability, and the user shall grant it explicitly with the
|
||||
reason shown. A plugin's declared capabilities shall be visible before installation, and a plugin
|
||||
that requests none — which is the expected case for a segmentation strategy — shall be installable
|
||||
@@ -1465,7 +1473,7 @@ socket cannot send a photograph anywhere, and that is a structural property rath
|
||||
|
||||
#### Versioning
|
||||
|
||||
**FR-PLG-5 — Three independent version numbers.** Conflating any two of these produces a wrong
|
||||
**FR-PLG-5 — Three independent version numbers.** *(post-v1)* Conflating any two of these produces a wrong
|
||||
answer in both directions, so the format shall carry all three.
|
||||
|
||||
| Number | Owned by | Governs | Moves when |
|
||||
@@ -1478,11 +1486,11 @@ The common case is a plugin renaming a parameter: sidecars break while the inter
|
||||
moves. The converse also occurs. Binding sidecar compatibility to the interface version would be
|
||||
wrong in both.
|
||||
|
||||
**FR-PLG-5a — Declared, never inferred.** A plugin shall state its interface version explicitly.
|
||||
**FR-PLG-5a — Declared, never inferred.** *(post-v1)* A plugin shall state its interface version explicitly.
|
||||
Deducing it from which keys are present produces files that are ambiguous between two versions, and
|
||||
the ambiguity surfaces years later as a wrong render rather than as an error.
|
||||
|
||||
**FR-PLG-5b — A supported window, and a written policy.** The application shall support the current
|
||||
**FR-PLG-5b — A supported window, and a written policy.** *(post-v1)* The application shall support the current
|
||||
interface version and at least one predecessor, and the deprecation policy shall be stated in the
|
||||
plugin authoring documentation rather than decided per release under pressure.
|
||||
|
||||
@@ -1490,13 +1498,13 @@ plugin authoring documentation rather than decided per release under pressure.
|
||||
means default — the rule `active:`, `presentation:` and `define:` already follow. Holding that
|
||||
discipline is what keeps the version number nearly stationary.
|
||||
|
||||
**FR-PLG-5c — Adapt at the boundary, normalise inward.** A plugin declaring an older interface
|
||||
**FR-PLG-5c — Adapt at the boundary, normalise inward.** *(post-v1)* A plugin declaring an older interface
|
||||
version shall be adapted at load into the current internal representation, and nothing downstream
|
||||
shall be able to tell. Version branches threaded through the pipeline are how this becomes
|
||||
unmaintainable; the single adaptation point is the same discipline that lets a declared node and a
|
||||
hand-written one be one thing by the time anything reads them.
|
||||
|
||||
**FR-PLG-6 — Migrations are data.** Because every parameter is an `f32` addressed by a flat
|
||||
**FR-PLG-6 — Migrations are data.** *(post-v1)* Because every parameter is an `f32` addressed by a flat
|
||||
`op_id.param_id` key, a schema migration is a rewrite table rather than code. A plugin shall be able
|
||||
to declare migrations between consecutive parameter schema versions, supporting at minimum rename,
|
||||
rescale, and default-for-a-new-parameter.
|
||||
@@ -1505,14 +1513,14 @@ The **application** applies them, chained, at load, so that everything downstrea
|
||||
current-schema parameters. Migrations shall be testable through the same declared-test mechanism as
|
||||
the node itself.
|
||||
|
||||
**FR-PLG-6a — `params_version` is a promise, not a hint.** Bumping it locks every older build out of
|
||||
**FR-PLG-6a — `params_version` is a promise, not a hint.** *(post-v1)* Bumping it locks every older build out of
|
||||
the edits that use it (FR-PLG-9). It shall be bumped only when an older build would genuinely
|
||||
*misread* the file, and never merely because a parameter was added — absence already means default,
|
||||
which already means neutral. This obligation belongs in the authoring documentation in as many words.
|
||||
|
||||
#### Sidecars
|
||||
|
||||
**FR-PLG-7 — The sidecar records identity, not location.** Each version block shall record, for
|
||||
**FR-PLG-7 — The sidecar records identity, not location.** *(post-v1)* Each version block shall record, for
|
||||
every plugin it depends on: the plugin id, its version, its parameter schema version, and a content
|
||||
hash of the artefact. These merge key-wise like every other line in the format (FR-NC-9).
|
||||
|
||||
@@ -1526,7 +1534,7 @@ The content hash carries a second benefit: "install filmic 2.1" resolves to exac
|
||||
original edit was rendered with, which makes substitution and silent render drift detectable rather
|
||||
than merely regrettable.
|
||||
|
||||
**FR-PLG-8 — A missing plugin shall never cost an edit.** The sidecar already preserves lines it
|
||||
**FR-PLG-8 — A missing plugin shall never cost an edit.** *(post-v1)* The sidecar already preserves lines it
|
||||
does not understand verbatim and writes them back untouched, so a machine lacking a plugin cannot
|
||||
destroy an edit that uses it. That property is now load-bearing and shall be treated as such.
|
||||
|
||||
@@ -1550,7 +1558,7 @@ Beyond preservation:
|
||||
*Acceptance:* a sidecar written with a plugin installed, opened and saved on a machine without it,
|
||||
is byte-identical to the original.
|
||||
|
||||
**FR-PLG-9 — Forward skew is quarantined, not guessed.** Where a version block names a parameter
|
||||
**FR-PLG-9 — Forward skew is quarantined, not guessed.** *(post-v1)* Where a version block names a parameter
|
||||
schema version **newer** than the installed plugin declares, the application shall divert that
|
||||
operation's parameters into the preserved-verbatim path *before applying any of them*, and mark the
|
||||
version quarantined.
|
||||
@@ -1579,7 +1587,7 @@ with an older one, is byte-identical afterwards.
|
||||
|
||||
#### Distribution
|
||||
|
||||
**FR-PLG-10 — Registry resolution and verified artefacts.** Installation shall resolve a plugin id
|
||||
**FR-PLG-10 — Registry resolution and verified artefacts.** *(post-v1)* Installation shall resolve a plugin id
|
||||
through a registry the user has configured, with a default registry shipped. The application shall
|
||||
verify the artefact against the hash or signature the registry states before loading it.
|
||||
|
||||
@@ -1598,7 +1606,7 @@ dependency on one company's availability.
|
||||
|
||||
#### Authoring and operations
|
||||
|
||||
**FR-PLG-11 — Plugins are validated, and validation is the author's tool.** A declared plugin's
|
||||
**FR-PLG-11 — Plugins are validated, and validation is the author's tool.** *(post-v1)* A declared plugin's
|
||||
`tests:` shall be runnable outside the application, against the same interpreter that loads it, via
|
||||
a command-line validator. The application shall ship a scaffold command producing a minimal working
|
||||
plugin of each class.
|
||||
@@ -1608,7 +1616,7 @@ manner `build.rs` already reports them. **A malformed plugin shall be skipped, n
|
||||
build script's exit-on-error discipline is right for an author with a compiler open and wrong for a
|
||||
user opening their library.
|
||||
|
||||
**FR-PLG-12 — Failure is attributable and revocable.** The application shall record per-plugin
|
||||
**FR-PLG-12 — Failure is attributable and revocable.** *(post-v1)* The application shall record per-plugin
|
||||
timing and error counts, surface them in the plugin listing, and allow any plugin to be disabled
|
||||
without uninstalling it — including on the next launch after a crash, so a plugin that prevents
|
||||
startup can be disabled by someone who cannot start the application.
|
||||
@@ -1771,7 +1779,7 @@ user. The prohibition is therefore structural rather than configurable: the code
|
||||
upload an embedding to anyone but the user's own server do not exist. A setting can be changed by
|
||||
accident, or by a future maintainer who has forgotten why it was there; an absent code path cannot.
|
||||
|
||||
**NFR-SEC-6 — Third-party code runs inside a boundary, not beside the application.** FR-PLG-1
|
||||
**NFR-SEC-6 — Third-party code runs inside a boundary, not beside the application.** *(post-v1)* FR-PLG-1
|
||||
admits code the user did not write and the project did not review. The privacy properties asserted
|
||||
elsewhere in §4.5 are properties of *this* codebase, and none of them survives a plugin that can
|
||||
open a socket.
|
||||
@@ -2104,7 +2112,10 @@ Deferred under D12 until FR-CULL-8a exists and spot removal's disc has, in its o
|
||||
finished and used. The first cut, when it comes, is "clone from a neighbouring frame" as a spot
|
||||
source; the face-aware proposal is a layer over that.
|
||||
|
||||
### D16 — plugin licensing · **OPEN**
|
||||
### D16 — plugin licensing · **OPEN, post-v1**
|
||||
|
||||
> Deferred with §3.10 on 2026-09-19. Still to be answered before the format is published as
|
||||
> stable, which is now a post-v1 event; nothing in v1 waits on it.
|
||||
|
||||
D8 puts the application under GPLv3. §3.10 admits third-party plugins in three forms, and the
|
||||
derivative-work question is answered differently for each — a YAML-and-WGSL declaration is data of
|
||||
@@ -2148,7 +2159,7 @@ note where deferring now constrains the design later.
|
||||
| AI subject masking | Deferred per D11. Note darktable shipped this in 5.6 (June 2026), so the gap is now visible. **Conditions for deferring safely:** AI denoise ships in v1 (FR-DEV-3g ✓), manual masking is excellent including GPU-rasterised drawn masks (ARCH §6.11 ✓), and the product has a clear differentiator (culling, §3.9 ✓). When it does land, copy darktable's shape — prompt-point segmentation producing an *editable* mask that behaves like a hand-drawn one — not Adobe's opaque version. |
|
||||
| AI upscaling | Deferred. Lower priority than denoise, which has no manual fallback. |
|
||||
| Video | — |
|
||||
| Plugin API | — |
|
||||
| Plugin API | **Post-v1**, decided 2026-09-19. Specified in full in §3.10 as the design of record; every clause there is marked `(post-v1)` and sits outside the coverage denominator. D16 (plugin licensing) defers with it. FR-DEV-3c's compile-time declarations are not plugins and remain in v1. |
|
||||
| Multi-user / server-side catalog | — |
|
||||
| Watermarking | Cheap *if* the export pipeline anticipates a compositing stage; expensive to retrofit otherwise. Consider reserving the stage now. |
|
||||
| Multiple catalogs, catalog merge | Interacts with NFR-OPS-3: preferences must not live in the catalog. |
|
||||
|
||||
+37
-31
@@ -3,7 +3,7 @@
|
||||
<!-- GENERATED FILE — do not edit by hand. -->
|
||||
<!-- Regenerate: cargo run -p traceability -- report -->
|
||||
|
||||
Denominators are parsed from [`requirements.md`](requirements.md) at run time, never hardcoded. Coverage is the intersection of tagged and defined IDs over defined IDs, so it cannot exceed 100%.
|
||||
Denominators are parsed from [`requirements.md`](requirements.md) at run time, never hardcoded. Coverage is the intersection of tagged and defined IDs over defined IDs, so it cannot exceed 100%. A requirement whose definition line carries `(post-v1)` is defined but deferred: outside the denominator, listed below, and never an orphan.
|
||||
|
||||
## Summary
|
||||
|
||||
@@ -11,16 +11,17 @@ Denominators are parsed from [`requirements.md`](requirements.md) at run time, n
|
||||
|---|---|
|
||||
| Source files scanned | 354 |
|
||||
| TRACES tags found | 1497 |
|
||||
| Requirements defined | 194 |
|
||||
| Requirements covered | 140 |
|
||||
| **Coverage** | **72.2%** (140/194) |
|
||||
| Requirements defined | 170 |
|
||||
| Requirements deferred (post-v1) | 24 |
|
||||
| Requirements covered | 137 |
|
||||
| **Coverage** | **80.6%** (137/170) |
|
||||
|
||||
### By type
|
||||
|
||||
| Type | Covered | Defined |
|
||||
|---|---|---|
|
||||
| FR | 105 | 138 |
|
||||
| NFR | 32 | 49 |
|
||||
| FR | 102 | 115 |
|
||||
| NFR | 32 | 48 |
|
||||
| R | 3 | 7 |
|
||||
|
||||
## Orphan tags
|
||||
@@ -123,9 +124,6 @@ _None._
|
||||
| FR-PLAT-WIN-1 | [`platform/dr-plat/src/dirs.rs:1`](../platform/dr-plat/src/dirs.rs#L1) |
|
||||
| FR-PLAT-WIN-2 | [`apps/darkroom-desktop/build.rs:1`](../apps/darkroom-desktop/build.rs#L1), [`apps/darkroom-desktop/src/main.rs:6`](../apps/darkroom-desktop/src/main.rs#L6), [`ui/dr-ui/src/launch_ui.rs:819`](../ui/dr-ui/src/launch_ui.rs#L819) |
|
||||
| FR-PLAT-WIN-3 | [`apps/darkroom-desktop/src/main.rs:19`](../apps/darkroom-desktop/src/main.rs#L19) |
|
||||
| FR-PLG-2 | [`core/dr-pipeline/src/declared/decl.rs:1`](../core/dr-pipeline/src/declared/decl.rs#L1), [`core/dr-pipeline/src/declared/expr.rs:152`](../core/dr-pipeline/src/declared/expr.rs#L152), [`core/dr-pipeline/src/declared/expr.rs:1`](../core/dr-pipeline/src/declared/expr.rs#L1), [`core/dr-pipeline/src/declared/mod.rs:1`](../core/dr-pipeline/src/declared/mod.rs#L1), [`core/dr-pipeline/src/declared/mod.rs:82`](../core/dr-pipeline/src/declared/mod.rs#L82), [`core/dr-pipeline/src/descriptor.rs:15`](../core/dr-pipeline/src/descriptor.rs#L15), [`core/dr-pipeline/src/descriptor.rs:734`](../core/dr-pipeline/src/descriptor.rs#L734), [`core/dr-pipeline/src/operation.rs:232`](../core/dr-pipeline/src/operation.rs#L232), [`core/dr-pipeline/tests/declared_parity.rs:1`](../core/dr-pipeline/tests/declared_parity.rs#L1), [`core/dr-pipeline/tests/declared_parity.rs:240`](../core/dr-pipeline/tests/declared_parity.rs#L240), [`core/dr-pipeline/tests/declared_parity.rs:305`](../core/dr-pipeline/tests/declared_parity.rs#L305), [`core/dr-pipeline/tests/declared_parity.rs:358`](../core/dr-pipeline/tests/declared_parity.rs#L358), [`core/dr-pipeline/tests/declared_parity.rs:418`](../core/dr-pipeline/tests/declared_parity.rs#L418) |
|
||||
| FR-PLG-2d | [`core/dr-pipeline/src/declared/decl.rs:112`](../core/dr-pipeline/src/declared/decl.rs#L112), [`core/dr-pipeline/src/declared/decl.rs:154`](../core/dr-pipeline/src/declared/decl.rs#L154), [`core/dr-pipeline/src/declared/decl.rs:1`](../core/dr-pipeline/src/declared/decl.rs#L1), [`core/dr-pipeline/src/declared/decl.rs:422`](../core/dr-pipeline/src/declared/decl.rs#L422), [`core/dr-pipeline/src/declared/decl.rs:67`](../core/dr-pipeline/src/declared/decl.rs#L67), [`core/dr-pipeline/src/declared/mod.rs:1`](../core/dr-pipeline/src/declared/mod.rs#L1), [`core/dr-pipeline/src/declared/mod.rs:384`](../core/dr-pipeline/src/declared/mod.rs#L384), [`core/dr-pipeline/src/declared/mod.rs:403`](../core/dr-pipeline/src/declared/mod.rs#L403) |
|
||||
| FR-PLG-8 | [`core/dr-pipeline/src/sidecar.rs:2298`](../core/dr-pipeline/src/sidecar.rs#L2298), [`core/dr-pipeline/src/sidecar.rs:2331`](../core/dr-pipeline/src/sidecar.rs#L2331) |
|
||||
| FR-RAW-1 | [`core/dr-decode/src/lib.rs:243`](../core/dr-decode/src/lib.rs#L243), [`core/dr-types/src/lib.rs:132`](../core/dr-types/src/lib.rs#L132), [`core/dr-types/src/lib.rs:203`](../core/dr-types/src/lib.rs#L203) |
|
||||
| FR-RAW-3 | [`core/dr-decode/src/lib.rs:139`](../core/dr-decode/src/lib.rs#L139), [`core/dr-decode/src/lib.rs:510`](../core/dr-decode/src/lib.rs#L510), [`core/dr-decode/src/locate.rs:1366`](../core/dr-decode/src/locate.rs#L1366) |
|
||||
| FR-RAW-4 | [`core/dr-decode/src/error.rs:1`](../core/dr-decode/src/error.rs#L1), [`core/dr-decode/src/error.rs:30`](../core/dr-decode/src/error.rs#L30), [`ui/dr-ui/src/lib.rs:262`](../ui/dr-ui/src/lib.rs#L262) |
|
||||
@@ -174,9 +172,38 @@ _None._
|
||||
| R4 | [`core/dr-gpu/src/lib.rs:365`](../core/dr-gpu/src/lib.rs#L365) |
|
||||
| R6 | [`core/dr-sync-nextcloud/src/lib.rs:1`](../core/dr-sync-nextcloud/src/lib.rs#L1) |
|
||||
|
||||
## Deferred (post-v1)
|
||||
|
||||
Defined in `requirements.md` and marked `(post-v1)` on the defining line. Not in the denominator; a tag naming one is recorded here rather than counted.
|
||||
|
||||
- FR-PLG-1
|
||||
- FR-PLG-10
|
||||
- FR-PLG-11
|
||||
- FR-PLG-12
|
||||
- FR-PLG-1a
|
||||
- FR-PLG-2 — tagged in [`core/dr-pipeline/src/declared/decl.rs:1`](../core/dr-pipeline/src/declared/decl.rs#L1), [`core/dr-pipeline/src/declared/expr.rs:152`](../core/dr-pipeline/src/declared/expr.rs#L152), [`core/dr-pipeline/src/declared/expr.rs:1`](../core/dr-pipeline/src/declared/expr.rs#L1), [`core/dr-pipeline/src/declared/mod.rs:1`](../core/dr-pipeline/src/declared/mod.rs#L1), [`core/dr-pipeline/src/declared/mod.rs:82`](../core/dr-pipeline/src/declared/mod.rs#L82), [`core/dr-pipeline/src/descriptor.rs:15`](../core/dr-pipeline/src/descriptor.rs#L15), [`core/dr-pipeline/src/descriptor.rs:734`](../core/dr-pipeline/src/descriptor.rs#L734), [`core/dr-pipeline/src/operation.rs:232`](../core/dr-pipeline/src/operation.rs#L232), [`core/dr-pipeline/tests/declared_parity.rs:1`](../core/dr-pipeline/tests/declared_parity.rs#L1), [`core/dr-pipeline/tests/declared_parity.rs:240`](../core/dr-pipeline/tests/declared_parity.rs#L240), [`core/dr-pipeline/tests/declared_parity.rs:305`](../core/dr-pipeline/tests/declared_parity.rs#L305), [`core/dr-pipeline/tests/declared_parity.rs:358`](../core/dr-pipeline/tests/declared_parity.rs#L358), [`core/dr-pipeline/tests/declared_parity.rs:418`](../core/dr-pipeline/tests/declared_parity.rs#L418)
|
||||
- FR-PLG-2a
|
||||
- FR-PLG-2b
|
||||
- FR-PLG-2c
|
||||
- FR-PLG-2d — tagged in [`core/dr-pipeline/src/declared/decl.rs:112`](../core/dr-pipeline/src/declared/decl.rs#L112), [`core/dr-pipeline/src/declared/decl.rs:154`](../core/dr-pipeline/src/declared/decl.rs#L154), [`core/dr-pipeline/src/declared/decl.rs:1`](../core/dr-pipeline/src/declared/decl.rs#L1), [`core/dr-pipeline/src/declared/decl.rs:422`](../core/dr-pipeline/src/declared/decl.rs#L422), [`core/dr-pipeline/src/declared/decl.rs:67`](../core/dr-pipeline/src/declared/decl.rs#L67), [`core/dr-pipeline/src/declared/mod.rs:1`](../core/dr-pipeline/src/declared/mod.rs#L1), [`core/dr-pipeline/src/declared/mod.rs:384`](../core/dr-pipeline/src/declared/mod.rs#L384), [`core/dr-pipeline/src/declared/mod.rs:403`](../core/dr-pipeline/src/declared/mod.rs#L403)
|
||||
- FR-PLG-3
|
||||
- FR-PLG-3a
|
||||
- FR-PLG-4
|
||||
- FR-PLG-4a
|
||||
- FR-PLG-5
|
||||
- FR-PLG-5a
|
||||
- FR-PLG-5b
|
||||
- FR-PLG-5c
|
||||
- FR-PLG-6
|
||||
- FR-PLG-6a
|
||||
- FR-PLG-7
|
||||
- FR-PLG-8 — tagged in [`core/dr-pipeline/src/sidecar.rs:2298`](../core/dr-pipeline/src/sidecar.rs#L2298), [`core/dr-pipeline/src/sidecar.rs:2331`](../core/dr-pipeline/src/sidecar.rs#L2331)
|
||||
- FR-PLG-9
|
||||
- NFR-SEC-6
|
||||
|
||||
## Not yet tagged
|
||||
|
||||
54 of 194 requirements have no implementation tag. Expected while the codebase is young; each should gain one as it is built.
|
||||
33 of 170 requirements have no implementation tag. Expected while the codebase is young; each should gain one as it is built.
|
||||
|
||||
<details><summary>Show untagged requirements</summary>
|
||||
|
||||
@@ -192,26 +219,6 @@ _None._
|
||||
- FR-NC-11
|
||||
- FR-PLAT-AND-1
|
||||
- FR-PLAT-LIN-3
|
||||
- FR-PLG-1
|
||||
- FR-PLG-10
|
||||
- FR-PLG-11
|
||||
- FR-PLG-12
|
||||
- FR-PLG-1a
|
||||
- FR-PLG-2a
|
||||
- FR-PLG-2b
|
||||
- FR-PLG-2c
|
||||
- FR-PLG-3
|
||||
- FR-PLG-3a
|
||||
- FR-PLG-4
|
||||
- FR-PLG-4a
|
||||
- FR-PLG-5
|
||||
- FR-PLG-5a
|
||||
- FR-PLG-5b
|
||||
- FR-PLG-5c
|
||||
- FR-PLG-6
|
||||
- FR-PLG-6a
|
||||
- FR-PLG-7
|
||||
- FR-PLG-9
|
||||
- FR-RAW-2
|
||||
- NFR-A11Y-1
|
||||
- NFR-ARCH-1
|
||||
@@ -229,7 +236,6 @@ _None._
|
||||
- NFR-P8
|
||||
- NFR-R3
|
||||
- NFR-RES-3
|
||||
- NFR-SEC-6
|
||||
- R1
|
||||
- R2
|
||||
- R5
|
||||
|
||||
@@ -69,8 +69,14 @@ pub struct TraceEntry {
|
||||
/// What `requirements.md` defines — the coverage denominators.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct DefinedRequirements {
|
||||
/// Requirements in scope: the denominator.
|
||||
pub ids: BTreeSet<String>,
|
||||
pub by_type: BTreeMap<String, usize>,
|
||||
/// Requirements the register defines but marks `(post-v1)` on the line
|
||||
/// that defines them. Still defined — a tag naming one is not an orphan —
|
||||
/// but outside the denominator, and reported in their own table so that
|
||||
/// leaving the count is visible rather than a way of hiding.
|
||||
pub deferred: BTreeSet<String>,
|
||||
}
|
||||
|
||||
impl DefinedRequirements {
|
||||
@@ -90,6 +96,11 @@ pub struct Coverage {
|
||||
pub orphaned: Vec<String>,
|
||||
/// Defined but never tagged anywhere.
|
||||
pub untraced: Vec<String>,
|
||||
/// Tagged in source although the register defers the requirement. Not
|
||||
/// covered — a deferred clause is not in the denominator — and not an
|
||||
/// orphan either; listed so the code that anticipates post-v1 work is
|
||||
/// findable.
|
||||
pub deferred_tagged: Vec<String>,
|
||||
}
|
||||
|
||||
/// Parse the requirement IDs a markdown document *defines*.
|
||||
@@ -103,15 +114,28 @@ pub struct Coverage {
|
||||
/// definitions, inflating the denominator. IDs are deduplicated because a
|
||||
/// requirement may legitimately appear in both a definition and a summary
|
||||
/// table.
|
||||
///
|
||||
/// A definition line carrying [`DEFERRED_MARKER`] defines the requirement
|
||||
/// as **deferred**: it exists, but it is outside the count. The marker sits
|
||||
/// on the definition line and nowhere else, so a prose mention of "post-v1"
|
||||
/// three paragraphs down changes nothing. Where the same ID is defined twice
|
||||
/// — once in a summary table, once in prose — deferral on either line wins,
|
||||
/// because the alternative is a clause that is deferred in one place and
|
||||
/// counted in another.
|
||||
pub fn parse_defined_requirements(markdown: &str) -> DefinedRequirements {
|
||||
let mut ids = BTreeSet::new();
|
||||
let mut deferred = BTreeSet::new();
|
||||
|
||||
for line in markdown.lines() {
|
||||
let trimmed = line.trim_start();
|
||||
let is_deferred = trimmed.contains(DEFERRED_MARKER);
|
||||
|
||||
// Form 1: a bolded definition, e.g. `**FR-CAT-1 — Scan.**`
|
||||
if let Some(rest) = trimmed.strip_prefix("**") {
|
||||
if let Some(id) = leading_id(rest) {
|
||||
if is_deferred {
|
||||
deferred.insert(id.clone());
|
||||
}
|
||||
ids.insert(id);
|
||||
continue;
|
||||
}
|
||||
@@ -121,6 +145,9 @@ pub fn parse_defined_requirements(markdown: &str) -> DefinedRequirements {
|
||||
if let Some(rest) = trimmed.strip_prefix('|') {
|
||||
let cell = rest.trim().trim_start_matches("**");
|
||||
if let Some(id) = leading_id(cell) {
|
||||
if is_deferred {
|
||||
deferred.insert(id.clone());
|
||||
}
|
||||
ids.insert(id);
|
||||
}
|
||||
}
|
||||
@@ -129,16 +156,33 @@ pub fn parse_defined_requirements(markdown: &str) -> DefinedRequirements {
|
||||
// Only requirement types enter the register. Decisions, spikes, milestone
|
||||
// items and test ids are all taggable, but none is a requirement, and
|
||||
// counting them would inflate the denominator.
|
||||
let ids: BTreeSet<String> = ids.into_iter().filter(|id| is_requirement(id)).collect();
|
||||
let deferred: BTreeSet<String> = deferred
|
||||
.into_iter()
|
||||
.filter(|id| is_requirement(id))
|
||||
.collect();
|
||||
let ids: BTreeSet<String> = ids
|
||||
.into_iter()
|
||||
.filter(|id| is_requirement(id) && !deferred.contains(id))
|
||||
.collect();
|
||||
|
||||
let mut by_type: BTreeMap<String, usize> = BTreeMap::new();
|
||||
for id in &ids {
|
||||
*by_type.entry(type_of(id).to_string()).or_insert(0) += 1;
|
||||
}
|
||||
|
||||
DefinedRequirements { ids, by_type }
|
||||
DefinedRequirements {
|
||||
ids,
|
||||
by_type,
|
||||
deferred,
|
||||
}
|
||||
}
|
||||
|
||||
/// The text that, on a definition line, takes a requirement out of the
|
||||
/// denominator. Written `*(post-v1)*` after the bold declaration in
|
||||
/// `requirements.md`; only the parenthesised part is matched, so the
|
||||
/// emphasis around it is a matter of style.
|
||||
pub const DEFERRED_MARKER: &str = "(post-v1)";
|
||||
|
||||
/// Extract a requirement ID anchored at the start of `s`.
|
||||
///
|
||||
/// Accepts `FR-CAT-1`, `NFR-P13`, `R1`, `FR-DEV-3a` — DarkRoom uses
|
||||
@@ -316,7 +360,13 @@ pub fn compute_coverage(traced: &BTreeSet<String>, defined: &DefinedRequirements
|
||||
|
||||
let orphaned: Vec<String> = traced_reqs
|
||||
.iter()
|
||||
.filter(|id| !defined.ids.contains(**id))
|
||||
.filter(|id| !defined.ids.contains(**id) && !defined.deferred.contains(**id))
|
||||
.map(|s| s.to_string())
|
||||
.collect();
|
||||
|
||||
let deferred_tagged: Vec<String> = traced_reqs
|
||||
.iter()
|
||||
.filter(|id| defined.deferred.contains(**id))
|
||||
.map(|s| s.to_string())
|
||||
.collect();
|
||||
|
||||
@@ -338,6 +388,7 @@ pub fn compute_coverage(traced: &BTreeSet<String>, defined: &DefinedRequirements
|
||||
},
|
||||
orphaned,
|
||||
untraced,
|
||||
deferred_tagged,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -490,6 +541,50 @@ This is discussed in FR-CAT-1 and also FR-CAT-1 again.
|
||||
assert!(!defined.ids.contains("S1"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_deferred_definition_leaves_the_denominator_but_stays_defined() {
|
||||
// FR-PLG-1 is defined and marked post-v1 on its own line. It must
|
||||
// not count, must not be "untagged", and a tag naming it must not be
|
||||
// an orphan — it is a real ID, just not one v1 is measured against.
|
||||
let md = "\
|
||||
**FR-CAT-1 — Scan.** The app shall scan roots.
|
||||
|
||||
**FR-PLG-1 — Three plugin classes.** *(post-v1)* The app shall support three.
|
||||
|
||||
This paragraph says post-v1 about FR-CAT-1 and changes nothing.
|
||||
";
|
||||
let defined = parse_defined_requirements(md);
|
||||
assert_eq!(defined.total(), 1);
|
||||
assert!(defined.ids.contains("FR-CAT-1"));
|
||||
assert!(!defined.ids.contains("FR-PLG-1"));
|
||||
assert!(defined.deferred.contains("FR-PLG-1"));
|
||||
|
||||
let traced: BTreeSet<String> = ["FR-CAT-1", "FR-PLG-1"]
|
||||
.iter()
|
||||
.map(|s| s.to_string())
|
||||
.collect();
|
||||
let cov = compute_coverage(&traced, &defined);
|
||||
assert_eq!((cov.covered, cov.total), (1, 1));
|
||||
assert!(
|
||||
cov.orphaned.is_empty(),
|
||||
"a deferred ID is defined, not a typo"
|
||||
);
|
||||
assert!(cov.untraced.is_empty(), "a deferred ID is not owed a tag");
|
||||
assert_eq!(cov.deferred_tagged, vec!["FR-PLG-1".to_string()]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deferral_on_either_definition_line_wins() {
|
||||
// Defined in a summary table without the marker and in prose with it.
|
||||
let md = "\
|
||||
| R7 | Judge anywhere | criterion |
|
||||
**R7 — Judge anywhere.** *(post-v1)*
|
||||
";
|
||||
let defined = parse_defined_requirements(md);
|
||||
assert_eq!(defined.total(), 0);
|
||||
assert!(defined.deferred.contains("R7"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tagging_a_decision_neither_covers_nor_orphans() {
|
||||
let defined = parse_defined_requirements("**FR-CAT-1 — Scan.**\n");
|
||||
|
||||
@@ -238,6 +238,9 @@ fn print_summary(
|
||||
println!("files scanned {}", files.len());
|
||||
println!("tags found {}", entries.len());
|
||||
println!("requirements {}", defined.total());
|
||||
if !defined.deferred.is_empty() {
|
||||
println!("deferred {} (post-v1)", defined.deferred.len());
|
||||
}
|
||||
println!(
|
||||
"coverage {:.1}% ({}/{})",
|
||||
cov.percent, cov.covered, cov.total
|
||||
@@ -260,13 +263,19 @@ fn render(
|
||||
m.push_str(
|
||||
"Denominators are parsed from [`requirements.md`](requirements.md) at run time, \
|
||||
never hardcoded. Coverage is the intersection of tagged and defined IDs over \
|
||||
defined IDs, so it cannot exceed 100%.\n\n",
|
||||
defined IDs, so it cannot exceed 100%. A requirement whose definition line carries \
|
||||
`(post-v1)` is defined but deferred: outside the denominator, listed below, and \
|
||||
never an orphan.\n\n",
|
||||
);
|
||||
|
||||
m.push_str("## Summary\n\n| Metric | Value |\n|---|---|\n");
|
||||
m.push_str(&format!("| Source files scanned | {} |\n", files.len()));
|
||||
m.push_str(&format!("| TRACES tags found | {} |\n", entries.len()));
|
||||
m.push_str(&format!("| Requirements defined | {} |\n", defined.total()));
|
||||
m.push_str(&format!(
|
||||
"| Requirements deferred (post-v1) | {} |\n",
|
||||
defined.deferred.len()
|
||||
));
|
||||
m.push_str(&format!("| Requirements covered | {} |\n", cov.covered));
|
||||
m.push_str(&format!(
|
||||
"| **Coverage** | **{:.1}%** ({}/{}) |\n\n",
|
||||
@@ -324,6 +333,31 @@ fn render(
|
||||
}
|
||||
m.push('\n');
|
||||
|
||||
m.push_str("## Deferred (post-v1)\n\n");
|
||||
m.push_str(
|
||||
"Defined in `requirements.md` and marked `(post-v1)` on the defining line. Not in \
|
||||
the denominator; a tag naming one is recorded here rather than counted.\n\n",
|
||||
);
|
||||
if defined.deferred.is_empty() {
|
||||
m.push_str("_None._\n\n");
|
||||
} else {
|
||||
for id in &defined.deferred {
|
||||
if cov.deferred_tagged.contains(id) {
|
||||
let mut locs: Vec<String> = entries
|
||||
.iter()
|
||||
.filter(|e| e.requirements.contains(id))
|
||||
.map(|e| format!("[`{}:{}`](../{}#L{})", e.file, e.line, e.file, e.line))
|
||||
.collect();
|
||||
locs.sort();
|
||||
locs.dedup();
|
||||
m.push_str(&format!("- {id} — tagged in {}\n", locs.join(", ")));
|
||||
} else {
|
||||
m.push_str(&format!("- {id}\n"));
|
||||
}
|
||||
}
|
||||
m.push('\n');
|
||||
}
|
||||
|
||||
m.push_str("## Not yet tagged\n\n");
|
||||
m.push_str(&format!(
|
||||
"{} of {} requirements have no implementation tag. Expected while the \
|
||||
|
||||
Reference in New Issue
Block a user