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:
2026-09-19 12:25:03 +02:00
parent c921852d89
commit c826fed605
4 changed files with 207 additions and 61 deletions
+37 -26
View File
@@ -1337,6 +1337,14 @@ ordinary FR-CULL-10 merge, not a special case.
### 3.10 Extensibility and plugins ### 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 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 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 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 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.** 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. introduced without a recorded decision.
| Class | Mechanism | Covers | Executes code | | 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 the whole application's privileges, which would make NFR-SEC-4 a promise about third-party code
rather than a property of the system. 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 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 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 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 #### 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, `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, 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. 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 the same way and for the same reason that a declared node is today indistinguishable from a
hand-written one. 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? answering one question: does this operation need to read a pixel other than its own?
| | Fragment node | Pass node | | | 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 listing, because a chain of pass nodes is how a fast application becomes a slow one without any
single decision having been wrong. 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 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 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 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`) forms with no code. Mask sources that are *identity into a segmentation* (`Regions`, `Subject`)
are not declarative and belong to class 3. 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 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, 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 vectorscope and RGB parade without change. The counting half shall not read back full-resolution
pixels (ARCH §5.5). 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 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 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 containing exactly one control is indistinguishable from a deliberate new category until somebody
@@ -1432,7 +1440,7 @@ control that was never written.
#### View plugins #### 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 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 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 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 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. 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. 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 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 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 #### 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 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 implements. Interfaces are the only surface a computational plugin can reach: there is no ambient
filesystem, no network, and no access to the catalog. 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 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 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 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 #### 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. answer in both directions, so the format shall carry all three.
| Number | Owned by | Governs | Moves when | | 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 moves. The converse also occurs. Binding sidecar compatibility to the interface version would be
wrong in both. 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 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. 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 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. 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 means default — the rule `active:`, `presentation:` and `define:` already follow. Holding that
discipline is what keeps the version number nearly stationary. 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 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 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 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. 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 `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, to declare migrations between consecutive parameter schema versions, supporting at minimum rename,
rescale, and default-for-a-new-parameter. 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 current-schema parameters. Migrations shall be testable through the same declared-test mechanism as
the node itself. 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 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, *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. which already means neutral. This obligation belongs in the authoring documentation in as many words.
#### Sidecars #### 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 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). 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 original edit was rendered with, which makes substitution and silent render drift detectable rather
than merely regrettable. 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 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. 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, *Acceptance:* a sidecar written with a plugin installed, opened and saved on a machine without it,
is byte-identical to the original. 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 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 operation's parameters into the preserved-verbatim path *before applying any of them*, and mark the
version quarantined. version quarantined.
@@ -1579,7 +1587,7 @@ with an older one, is byte-identical afterwards.
#### Distribution #### 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 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. 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 #### 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 `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 a command-line validator. The application shall ship a scaffold command producing a minimal working
plugin of each class. 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 build script's exit-on-error discipline is right for an author with a compiler open and wrong for a
user opening their library. 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 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 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. 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 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. 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 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 elsewhere in §4.5 are properties of *this* codebase, and none of them survives a plugin that can
open a socket. 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 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. 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 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 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 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. | | AI upscaling | Deferred. Lower priority than denoise, which has no manual fallback. |
| Video | — | | 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 | — | | Multi-user / server-side catalog | — |
| Watermarking | Cheap *if* the export pipeline anticipates a compositing stage; expensive to retrofit otherwise. Consider reserving the stage now. | | 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. | | Multiple catalogs, catalog merge | Interacts with NFR-OPS-3: preferences must not live in the catalog. |
+37 -31
View File
@@ -3,7 +3,7 @@
<!-- GENERATED FILE — do not edit by hand. --> <!-- GENERATED FILE — do not edit by hand. -->
<!-- Regenerate: cargo run -p traceability -- report --> <!-- 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 ## Summary
@@ -11,16 +11,17 @@ Denominators are parsed from [`requirements.md`](requirements.md) at run time, n
|---|---| |---|---|
| Source files scanned | 354 | | Source files scanned | 354 |
| TRACES tags found | 1497 | | TRACES tags found | 1497 |
| Requirements defined | 194 | | Requirements defined | 170 |
| Requirements covered | 140 | | Requirements deferred (post-v1) | 24 |
| **Coverage** | **72.2%** (140/194) | | Requirements covered | 137 |
| **Coverage** | **80.6%** (137/170) |
### By type ### By type
| Type | Covered | Defined | | Type | Covered | Defined |
|---|---|---| |---|---|---|
| FR | 105 | 138 | | FR | 102 | 115 |
| NFR | 32 | 49 | | NFR | 32 | 48 |
| R | 3 | 7 | | R | 3 | 7 |
## Orphan tags ## 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-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-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-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-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-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) | | 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) | | 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) | | 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 ## 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> <details><summary>Show untagged requirements</summary>
@@ -192,26 +219,6 @@ _None._
- FR-NC-11 - FR-NC-11
- FR-PLAT-AND-1 - FR-PLAT-AND-1
- FR-PLAT-LIN-3 - 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 - FR-RAW-2
- NFR-A11Y-1 - NFR-A11Y-1
- NFR-ARCH-1 - NFR-ARCH-1
@@ -229,7 +236,6 @@ _None._
- NFR-P8 - NFR-P8
- NFR-R3 - NFR-R3
- NFR-RES-3 - NFR-RES-3
- NFR-SEC-6
- R1 - R1
- R2 - R2
- R5 - R5
+98 -3
View File
@@ -69,8 +69,14 @@ pub struct TraceEntry {
/// What `requirements.md` defines — the coverage denominators. /// What `requirements.md` defines — the coverage denominators.
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]
pub struct DefinedRequirements { pub struct DefinedRequirements {
/// Requirements in scope: the denominator.
pub ids: BTreeSet<String>, pub ids: BTreeSet<String>,
pub by_type: BTreeMap<String, usize>, 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 { impl DefinedRequirements {
@@ -90,6 +96,11 @@ pub struct Coverage {
pub orphaned: Vec<String>, pub orphaned: Vec<String>,
/// Defined but never tagged anywhere. /// Defined but never tagged anywhere.
pub untraced: Vec<String>, 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*. /// Parse the requirement IDs a markdown document *defines*.
@@ -103,15 +114,28 @@ pub struct Coverage {
/// definitions, inflating the denominator. IDs are deduplicated because a /// definitions, inflating the denominator. IDs are deduplicated because a
/// requirement may legitimately appear in both a definition and a summary /// requirement may legitimately appear in both a definition and a summary
/// table. /// 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 { pub fn parse_defined_requirements(markdown: &str) -> DefinedRequirements {
let mut ids = BTreeSet::new(); let mut ids = BTreeSet::new();
let mut deferred = BTreeSet::new();
for line in markdown.lines() { for line in markdown.lines() {
let trimmed = line.trim_start(); let trimmed = line.trim_start();
let is_deferred = trimmed.contains(DEFERRED_MARKER);
// Form 1: a bolded definition, e.g. `**FR-CAT-1 — Scan.**` // Form 1: a bolded definition, e.g. `**FR-CAT-1 — Scan.**`
if let Some(rest) = trimmed.strip_prefix("**") { if let Some(rest) = trimmed.strip_prefix("**") {
if let Some(id) = leading_id(rest) { if let Some(id) = leading_id(rest) {
if is_deferred {
deferred.insert(id.clone());
}
ids.insert(id); ids.insert(id);
continue; continue;
} }
@@ -121,6 +145,9 @@ pub fn parse_defined_requirements(markdown: &str) -> DefinedRequirements {
if let Some(rest) = trimmed.strip_prefix('|') { if let Some(rest) = trimmed.strip_prefix('|') {
let cell = rest.trim().trim_start_matches("**"); let cell = rest.trim().trim_start_matches("**");
if let Some(id) = leading_id(cell) { if let Some(id) = leading_id(cell) {
if is_deferred {
deferred.insert(id.clone());
}
ids.insert(id); ids.insert(id);
} }
} }
@@ -129,16 +156,33 @@ pub fn parse_defined_requirements(markdown: &str) -> DefinedRequirements {
// Only requirement types enter the register. Decisions, spikes, milestone // Only requirement types enter the register. Decisions, spikes, milestone
// items and test ids are all taggable, but none is a requirement, and // items and test ids are all taggable, but none is a requirement, and
// counting them would inflate the denominator. // 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(); let mut by_type: BTreeMap<String, usize> = BTreeMap::new();
for id in &ids { for id in &ids {
*by_type.entry(type_of(id).to_string()).or_insert(0) += 1; *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`. /// Extract a requirement ID anchored at the start of `s`.
/// ///
/// Accepts `FR-CAT-1`, `NFR-P13`, `R1`, `FR-DEV-3a` — DarkRoom uses /// 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 let orphaned: Vec<String> = traced_reqs
.iter() .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()) .map(|s| s.to_string())
.collect(); .collect();
@@ -338,6 +388,7 @@ pub fn compute_coverage(traced: &BTreeSet<String>, defined: &DefinedRequirements
}, },
orphaned, orphaned,
untraced, 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")); 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] #[test]
fn tagging_a_decision_neither_covers_nor_orphans() { fn tagging_a_decision_neither_covers_nor_orphans() {
let defined = parse_defined_requirements("**FR-CAT-1 — Scan.**\n"); let defined = parse_defined_requirements("**FR-CAT-1 — Scan.**\n");
+35 -1
View File
@@ -238,6 +238,9 @@ fn print_summary(
println!("files scanned {}", files.len()); println!("files scanned {}", files.len());
println!("tags found {}", entries.len()); println!("tags found {}", entries.len());
println!("requirements {}", defined.total()); println!("requirements {}", defined.total());
if !defined.deferred.is_empty() {
println!("deferred {} (post-v1)", defined.deferred.len());
}
println!( println!(
"coverage {:.1}% ({}/{})", "coverage {:.1}% ({}/{})",
cov.percent, cov.covered, cov.total cov.percent, cov.covered, cov.total
@@ -260,13 +263,19 @@ fn render(
m.push_str( m.push_str(
"Denominators are parsed from [`requirements.md`](requirements.md) at run time, \ "Denominators are parsed from [`requirements.md`](requirements.md) at run time, \
never hardcoded. Coverage is the intersection of tagged and defined IDs over \ 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("## Summary\n\n| Metric | Value |\n|---|---|\n");
m.push_str(&format!("| Source files scanned | {} |\n", files.len())); m.push_str(&format!("| Source files scanned | {} |\n", files.len()));
m.push_str(&format!("| TRACES tags found | {} |\n", entries.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 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!("| Requirements covered | {} |\n", cov.covered));
m.push_str(&format!( m.push_str(&format!(
"| **Coverage** | **{:.1}%** ({}/{}) |\n\n", "| **Coverage** | **{:.1}%** ({}/{}) |\n\n",
@@ -324,6 +333,31 @@ fn render(
} }
m.push('\n'); 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("## Not yet tagged\n\n");
m.push_str(&format!( m.push_str(&format!(
"{} of {} requirements have no implementation tag. Expected while the \ "{} of {} requirements have no implementation tag. Expected while the \