Reconcile the three branches
Two faults textual merge could not see. Both agents added a `mod tests` to masks_ui.rs — the module boundary was an artefact of them being written apart, and the tests do not overlap, so they fold into one. And `segment` lost its context argument when the work moved to a worker, which a test written on another branch still passed.
This commit is contained in:
@@ -885,6 +885,287 @@ The person UUID is what a cross-device merge keys on, in the same way collection
|
||||
Two devices that independently name the same cluster produce two people; merging them is the
|
||||
ordinary FR-CULL-10 merge, not a special case.
|
||||
|
||||
### 3.10 Extensibility and plugins
|
||||
|
||||
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
|
||||
rebuilt.**
|
||||
|
||||
The reason FR-DEV-3c was cheap is worth stating, because it decides everything below. An
|
||||
`Operation` never touches a pixel. It publishes a descriptor and returns WGSL text and a handful of
|
||||
floats, and the fused shader does the work (ARCH §3.3, ARCH §5.2). Any extension point that can be
|
||||
given that shape — data in, data out, no execution — gets plugins for almost nothing. Any extension
|
||||
point that cannot needs a sandbox to run code in, and that is where the entire cost of this section
|
||||
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
|
||||
introduced without a recorded decision.
|
||||
|
||||
| Class | Mechanism | Covers | Executes code |
|
||||
|---|---|---|---|
|
||||
| **Declarative** | YAML declaration plus WGSL | develop operations, mask generators, scope computation, camera profiles, LUTs, presets, localisations | No — shader source only, validated before use |
|
||||
| **View** | Slint compiled at runtime into a declared slot | toolbar and panel widgets, the drawing half of a scope | On the UI thread; not sandboxed |
|
||||
| **Computational** | WebAssembly component against a versioned interface | segmentation strategies, decoders, sync backends, metadata extractors | Yes, sandboxed |
|
||||
|
||||
The expected distribution is heavily weighted to the first. A plugin author reaches for class 2 only
|
||||
to *draw* something new and for class 3 only when an algorithm cannot be expressed as GPU passes.
|
||||
|
||||
**Class 3 shall be WebAssembly and shall not be native shared objects.** Three reasons, each
|
||||
independently sufficient: a native object cannot be loaded on Android (§1.2 names it a primary
|
||||
platform) or on any future iOS build; Rust has no stable ABI, so a native plugin would be locked to
|
||||
one compiler version and one build of every crate it touches; and an in-process native object holds
|
||||
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
|
||||
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
|
||||
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
|
||||
`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.
|
||||
|
||||
One consequence is deliberate: a bundled operation and a third-party plugin are the same kind of
|
||||
thing, differing only in where the file was found. There is no second, weaker format for outsiders,
|
||||
and no path by which the bundled operations acquire capabilities plugins cannot reach.
|
||||
|
||||
The build-time path is not removed. Operations that ship with the app remain compiled, because a
|
||||
generated `match` is faster than an interpreted one and because their tests must run under
|
||||
`cargo test`. The two paths shall produce descriptors indistinguishable to everything downstream, in
|
||||
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
|
||||
answering one question: does this operation need to read a pixel other than its own?
|
||||
|
||||
| | Fragment node | Pass node |
|
||||
|---|---|---|
|
||||
| Declares | a WGSL body over `c` | a complete compute shader with input and output textures |
|
||||
| Cost | none — fuses into the existing dispatch | one full-resolution texture round-trip |
|
||||
| Reaches | per-pixel colour transforms | neighbourhood operations — blur, clarity, halation, structured grain |
|
||||
|
||||
Fragment nodes shall continue to compose into a single dispatch (ARCH §5.2). A pass node breaks that
|
||||
fusion **for itself only**: the fragments before it and after it still fuse into one dispatch each.
|
||||
A declaration shall state which it is, and the cost shall be visible to the user in the plugin
|
||||
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
|
||||
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
|
||||
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`
|
||||
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
|
||||
notices. An unrecognised attribute shall place the operation in a clearly-labelled fallback group
|
||||
and warn — never silently omit it, which would make a control that does not exist look like a
|
||||
control that was never written.
|
||||
|
||||
#### View plugins
|
||||
|
||||
**FR-PLG-3 — Declared slots, typed contracts.** 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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
without a security decision.
|
||||
|
||||
This is what makes NFR-SEC-4 hold under a plugin ecosystem. A segmentation plugin that cannot open a
|
||||
socket cannot send a photograph anywhere, and that is a structural property rather than a promise.
|
||||
|
||||
#### Versioning
|
||||
|
||||
**FR-PLG-5 — Three independent version numbers.** 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 |
|
||||
|---|---|---|---|
|
||||
| **Interface version** | the application | whether the plugin loads at all | the host changes what it offers |
|
||||
| **Plugin version** | the author | updates, provenance, and the trust record | any release |
|
||||
| **Parameter schema version** | the author | whether an existing sidecar still reads | parameters change incompatibly |
|
||||
|
||||
The common case is a plugin renaming a parameter: sidecars break while the interface version never
|
||||
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.
|
||||
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
|
||||
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.
|
||||
|
||||
**Additive changes shall not bump the version.** A new optional key is compatible because absent
|
||||
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
|
||||
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
|
||||
`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.
|
||||
|
||||
The **application** applies them, chained, at load, so that everything downstream sees only
|
||||
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
|
||||
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
|
||||
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).
|
||||
|
||||
**It shall not record an install URL.** Sidecars arrive from elsewhere — they sync (FR-NC-9), they
|
||||
travel with shared photographs, they come from other people's catalogs. A sidecar that names where
|
||||
to fetch code lets whoever wrote it choose what the user is prompted to install, which is a
|
||||
confused deputy with a friendly dialog in front of it. Resolution from identity to location is a
|
||||
decision the user made when they configured a registry, not one an incoming file makes for them.
|
||||
|
||||
The content hash carries a second benefit: "install filmic 2.1" resolves to exactly the bytes the
|
||||
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
|
||||
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.
|
||||
|
||||
Beyond preservation:
|
||||
|
||||
- **Alert, aggregated.** Missing plugins shall be reported once per import or session and listed in
|
||||
one catalog-wide view — never once per image. A folder of five hundred synced photographs sharing
|
||||
one missing plugin is one notice.
|
||||
- **Non-blocking.** Nothing here is urgent, because the edit is safe either way. The image shows a
|
||||
clear mark that an operation is unavailable; the install dialog appears when the user asks for it.
|
||||
- **Never during unattended work.** No such prompt shall interrupt a background sync or a batch
|
||||
export, where a dialog becomes either a stalled job or a reflex click.
|
||||
- **Render without, never render a guess.** The image shall be rendered omitting the unavailable
|
||||
operation. Interpreting its parameters under different semantics is forbidden: a plausible wrong
|
||||
render and a correct one look equally plausible, which makes the wrong one the more dangerous
|
||||
output.
|
||||
- **Export is gated harder than display.** Exporting an edit with an unavailable operation shall
|
||||
require explicit acknowledgement. A slightly wrong screen is recoverable; a delivered file that
|
||||
silently omits an adjustment is not.
|
||||
|
||||
*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
|
||||
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.
|
||||
|
||||
This does not fall out of FR-PLG-8. A forward-skewed operation is a **recognised** id, so the
|
||||
existing unknown-key path never sees it: its parameters parse, apply under the older meaning, render
|
||||
plausibly, and are written back — silently downgrading the edit, on every device it syncs to. The
|
||||
hazard is the write-back, not the display.
|
||||
|
||||
Quarantine means, precisely:
|
||||
|
||||
- **No apply, no edit, no save, no export** for that version. These are the operations that lose data
|
||||
or ship a wrong file.
|
||||
- **The photograph still opens.** Decoding needs no plugin, and refusing to display a file because
|
||||
one adjustment is from the future holds the photograph hostage over an edit.
|
||||
- **Per version, not per image.** A sidecar holds several versions (FR-CAT-12); one may be
|
||||
quarantined while the others open normally.
|
||||
- **An upgrade is offered**, resolved through FR-PLG-10 like any other install — and it shall be
|
||||
allowed to fail. Offline, declined, or delisted all fall back to quarantine, never to deletion and
|
||||
never to opening anyway.
|
||||
- **Bundled plugins say so.** Where the plugin ships with the application, the remedy is an
|
||||
application update, and the message shall say that rather than offering an install that cannot help.
|
||||
|
||||
*Acceptance:* a sidecar written by a newer plugin version, opened, browsed, and closed on a build
|
||||
with an older one, is byte-identical afterwards.
|
||||
|
||||
#### Distribution
|
||||
|
||||
**FR-PLG-10 — Registry resolution and verified artefacts.** 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.
|
||||
|
||||
- **An unrecognised id is the loud case.** Where no configured registry knows the plugin, the
|
||||
application shall say so and show what the sidecar claims, as text, for the user to act on
|
||||
deliberately. There shall be no one-click install of a location supplied by a file.
|
||||
- **Provenance appears in the prompt.** Plugin name, version, registry, and publisher, so "from the
|
||||
registry you trust" and "from somewhere you have never heard of" do not look alike.
|
||||
- **Offline degrades, it does not block.** With no network: say what is missing, keep the edit
|
||||
intact, open the photograph. An application whose privacy claim is local-only shall not need the
|
||||
internet to show a picture.
|
||||
|
||||
A release page on a code-hosting service is a distribution mechanism, not an identity. Binding
|
||||
identity to one would give dead links on a rename, no mirroring, no offline install, and a
|
||||
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
|
||||
`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.
|
||||
|
||||
Load-time failures shall be collected per file and reported with the key that was wrong, in the
|
||||
manner `build.rs` already reports them. **A malformed plugin shall be skipped, never fatal.** The
|
||||
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
|
||||
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.
|
||||
|
||||
*Acceptance:* with a plugin deliberately made to fail at load, at render, and at UI paint, the
|
||||
application starts, opens a photograph, names the responsible plugin, and continues.
|
||||
|
||||
---
|
||||
|
||||
## 4. Non-functional requirements
|
||||
@@ -1028,6 +1309,29 @@ 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
|
||||
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.
|
||||
|
||||
- **Sandboxed by default, with granted exceptions.** Computational plugins execute with no
|
||||
filesystem, no network, and no catalog access. Anything more is a declared capability, shown
|
||||
before installation and granted explicitly by the user (FR-PLG-4a).
|
||||
- **No plugin reaches face data.** Embeddings, detections, crops, and cluster assignments are
|
||||
outside every plugin interface, under NFR-SEC-5's structural rule: the code path does not exist,
|
||||
so no grant can create one.
|
||||
- **Artefacts are verified before they are loaded**, against the hash or signature a configured
|
||||
registry states (FR-PLG-10). An unverifiable artefact is not loaded.
|
||||
- **Nothing is fetched on a file's say-so.** Installation is resolved through user-configured
|
||||
registries; a sidecar carries identity only (FR-PLG-7).
|
||||
- **Native shared objects are not a plugin format** (FR-PLG-1). An in-process native object holds
|
||||
the application's full privileges, which would make every rule above unenforceable.
|
||||
|
||||
*Rationale:* an extensible application inherits the trust properties of its weakest plugin unless
|
||||
the boundary is structural. The same reasoning as NFR-SEC-5 applies for the same reason — a setting
|
||||
can be changed by accident or by a maintainer who has forgotten why it was there, and an absent
|
||||
capability cannot.
|
||||
|
||||
### 4.6 Execution model
|
||||
|
||||
**NFR-ARCH-1 — Named executors.** The app defines distinct executors — UI, GPU submission, decode
|
||||
@@ -1280,6 +1584,32 @@ rather than thousands.
|
||||
|
||||
---
|
||||
|
||||
### D16 — plugin licensing · **OPEN**
|
||||
|
||||
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
|
||||
the kind the GPL has never claimed, a WebAssembly component communicating over a defined interface
|
||||
is arguably at arm's length, and an interpreted Slint component compiled into the application's own
|
||||
widget tree is not.
|
||||
|
||||
This must be answered before an ecosystem exists, not after. Contributors will not adopt a plugin
|
||||
format whose licence terms are unstated, and a term introduced later cannot be applied to plugins
|
||||
already written.
|
||||
|
||||
Three questions, in order of how much they constrain the design:
|
||||
|
||||
1. **May a plugin be non-free?** If yes, the interfaces are a deliberate licence boundary and must
|
||||
be documented as one. If no, the registry (FR-PLG-10) enforces it and the default registry lists
|
||||
only GPL-compatible plugins.
|
||||
2. **Does the answer differ by class?** Declaring class 1 unambiguously data, whatever is decided
|
||||
for class 3, is defensible and costs nothing.
|
||||
3. **What does the default registry require?** Licence metadata is a field in the registry index
|
||||
either way, so the field should exist from the first release regardless of what policy is
|
||||
attached to it.
|
||||
|
||||
D16 does not block FR-PLG-2, which concerns operations shipped in this repository under D8 already.
|
||||
It blocks publishing a third-party plugin format as stable.
|
||||
|
||||
## 7. Out of scope for v1
|
||||
|
||||
Deferred deliberately. Listed so their absence reads as a decision rather than an oversight, with a
|
||||
|
||||
Reference in New Issue
Block a user