# Reporting plugins

Status: the [plugin design](https://github.com/CosticaPuntaru/SnagBadger/issues/128) is human-confirmed. Implementation belongs to [capture redesign #126](https://github.com/CosticaPuntaru/SnagBadger/issues/126). This contract supersedes separate extension types for harvesters, captures and providers. Those names describe responsibilities registered by one plugin type. Capture mechanisms remain reusable implementation modules.

## Registration and settings

`ReportingPlugin` on `@snagbadger/reporting/session` owns its identity, label, capability metadata, configuration-setting schema, run-setting schema, optional tab contributions and `create(context)`. Plain objects and factory functions are supported. A plugin can register several collection/delivery capabilities. The system imports no built-ins and has no branches for known plugin IDs.

`createPluginRuntime({ plugins: [{ plugin, configuration, runSettings, capabilities, enabled }] })` creates an inert initialization snapshot. Developer configuration sets bounded values, restricts capabilities, and can replace the run schema to restrict reporter edits. Plugin configuration defaults may be locked to reporters without preventing developer configuration. Reporter updates still reject unknown, locked or out-of-range values. Plugin metadata and schema trees are copied/frozen; implementation closures remain developer-owned and must obey the resolved context.

Plugin IDs and local capability IDs are nonempty and cannot contain `/`. Capability selections use `pluginId/capabilityId`. `composeRegistrations` from integration configuration supplies complete last-registration replacement with the first position retained; the same precedence applies to compatibility registrations. `pluginCategories` exposes enabled capture alternatives and the first configured default without calling a factory. Alternatives sharing a capture category cannot both be selected. `enabled: false` removes UI and runtime access. Per-registration `capabilities` restricts the allowed local IDs.

`createPluginSelection(runtime.plugins)` provides host-owned UI bindings. `bind(id).select(localIds)` chooses enabled collection capabilities; `updateSettings(values)` validates and stores reporter choices even before selection. `snapshot()` supplies qualified IDs and settings for participating plugins. The runtime validates the complete selection, snapshots settings again before activation, and never reads later UI edits. `isSnapshotSelection` evaluates pure capability mode metadata or a plugin's `collectionMode(selected, settings)` policy; it does not acquire resources.

## Owned operations

`runtime.start(selection)` creates one instance per participating plugin for a collection attempt. The context contains only its selected capabilities, resolved configuration and fixed run settings, cancellation, scoped custom events and public `finish()`/`fail(error)` requests. Selecting screenshots never activates an unselected video capability. A browser plugin may compose independent screenshot/recording mechanisms internally and share its own stream; the host knows neither their IDs nor their resources.

The awaited lifecycle is `create` → optional `start` → `finish` → `dispose`. `ready` resolves after activation; callers use `completed` for native/resource-originated completion. Stop is idempotent, drains accepted queued event work, validates/copies output, merges it in configured plugin order and releases instances. `finish` returns canonical evidence, interactions and attachment bytes. Diagnostic-only failures isolate the failing plugin. Failure in a plugin with any selected capture capability stops the entire attempt and discards its output, preserving the caller's earlier draft/evidence. Delivery failures similarly fail their operation. Cleanup errors are recorded.

Cancellation stops waiting for acquisition, handlers or finalization; late-created instances are disposed exactly once. Disposal of an already acquired instance is awaited. UI consumers can restore controls immediately while cleanup runs, but must bind all completion/publication callbacks to the owned operation rather than the mutable current one. Old commands, subscriptions and queued events cannot act on a replacement. Async plugin `start` hooks must cooperate with cancellation and release resources acquired after their own disposal.

Pre-recording remains explicitly authorized, diagnostic-only and default off. Both the runtime policy and a harvester capability's `preRecording` flag must allow it; capture capabilities cannot opt themselves into it. Registration and tab loading never authorize instrumentation.

`runtime.startDelivery(qualifiedProviderId, { report, bundle }, settings)` validates an owned snapshot through the portable bundle importer and rejects a report that differs from the bundle's reviewed report before creating a provider instance. Its awaited `deliver(input)` hook receives that owned report/bundle, including evidence from any collection plugin. `runtime.deliver(...)` is a receipt-returning convenience. Failure/cancellation preserves the caller's input for explicit retry. Capture instances do not survive Stop to supply private data to providers. Authentication, external idempotency and real Jira/Linear integration remain with #11.

## Intermediate collection and capture adapters

The accepted architecture follow-up [#145](https://github.com/CosticaPuntaru/SnagBadger/issues/145)
adds `collection.collect()` to generic plugin collection and an optional
`PluginInstance.collect()` hook. It is distinct from terminal `finish()`:
each call returns one independently validated/copied `PluginResult<CaptureOutput>`
without implicitly retaining or accumulating that snapshot in the eventual report.
Delivery operations and legacy `ReportingSession` retain their existing interfaces.

Accepted collection requests run serially, await activation, and drain before
finalization. Stop rejects new requests immediately. Cancellation interrupts a
pending hook, drops queued collection work, discards the current snapshot and
releases resources under the existing lifetime rules. A hook must cooperate with
cancellation if its own work can outlive disposal. Calls after termination reject.
Every participating instance must supply `collect`; missing hooks are checked
before any hook runs. Hook, output and missing-hook failures use the `collect`
failure phase and the existing diagnostic-isolation/collective-capture policy.
Diagnostic failure leaves successful diagnostic output available; capture failure
discards the snapshot and terminates the attempt.

The DOM-free `@snagbadger/reporting/session` entry also exports
`createCapturePlugin`, `CapturePluginOptions` and `CapturePluginCapture`.
The factory accepts required plugin `id`/`label`, capture definitions and optional
configuration/run-setting schemas, tabs and a pure collection-mode policy.
Each definition supplies `id`, `label`, optional category/mode metadata and
`load(context): CaptureAdapter`. Registration is inert: only selected definitions
load and start, receiving the operation's fixed context. The reporting module
owns adapter translation, late-resource disposal, output validation and merging;
capture imports no reporting code. Existing exports and declarations remain.

```ts
const plugin = createCapturePlugin({
  id: 'browser',
  label: 'Browser capture',
  captures: [
    {
      id: 'frame',
      label: 'Display frame',
      mode: 'snapshot',
      load: () => selectedDisplayAdapter,
    },
  ],
});
const runtime = createPluginRuntime({ plugins: [{ plugin }] });
const attempt = runtime.start({ capabilities: ['browser/frame'] });
await attempt.ready;
const snapshot = await attempt.collect();
const final = await attempt.finish();
await runtime.dispose();
```

Only snapshot capture adapters with `repeatable: true` support intermediate
collection through this bridge. The complete selected combination is checked
before any resource collects. A one-shot snapshot uses `finish()` instead.
Normal final collection still runs on Stop/native completion; prior intermediate
snapshots are not implicitly added to it. Schema, capability and settings policy
remain caller-supplied; the SDK embeds no study-specific permissions or controls.

## Custom events

Every operation owns a FIFO custom-event queue. `context.events.on(name, handler)` subscribes and returns unsubscribe; `emit(name, data)` returns immediately with an acceptance boolean. Events carry the publishing plugin ID. `system:` names are reserved: custom broadcasts cannot forge lifecycle transitions. Every plugin uses the same public lifecycle requests.

Plugins can observe `system:ready` after activation and `system:finishing` before finalization through the same subscriptions. Only the host publishes these notifications; their data identifies collection or delivery. Cancellation is observable through the context's cancellation subscription, and terminal completion through the operation's `completed` promise.

Stop rejects all new custom publications, including follow-up events emitted by handlers that are still draining. Such `emit` calls return false; hooks should finish remaining work directly instead of chaining additional events during finalization.

Handlers run serially in publication/subscription order; asynchronous rejection is attributed to the listening plugin and follows its operation's failure policy. Stop seals new publications/subscriptions and drains accepted events before finalization. Cancellation/disposal drops pending work and removes subscriptions. A queue holds at most 1,000 pending custom publications and accepts at most 10,000 custom publications per operation; exceeding a bound reports a plugin failure. This also bounds event cycles. Publishers own payload size and must supply stable data rather than mutable application objects. Plugins remain responsible for their own byte/retention limits.

## Optional plugin UI

All tab content is plugin-supplied. Recommended tab IDs are `start`, `record` and `settings`; they are conventions, not three plugin roles. Plugins can contribute several tabs, and matching tab IDs compose plugin-owned sections. Distinct IDs create additional tabs. The first configured contribution supplies a shared tab's label; sections stay in configured order. `pluginTabs` discovers every enabled contribution independently of capability selection and does not load it.

Tab loaders are separate from runtime factories and must remain inert. The optional `@snagbadger/reporting/browser` entry supplies `mountPluginTabs` and the recommended DOM `PluginTabView` protocol. A view mounts into its section with resolved configuration, a public selection/settings binding and shell commands; it returns cleanup. The shell supplies accessible navigation, while content remains custom. Other UI consumers can interpret their own view descriptors. Framework-independent entries import no React, CSS or browser globals.

Start, Stop, Cancel and Submit belong to the shared shell. A custom section can invoke those commands but does not own the active operation. Loading/showing a section never creates a collection instance. Failed mounting and disposal release all mounted sections, even if one cleanup throws. Late loaders cannot mount after disposal. Consumers should follow labelled controls, keyboard navigation, explicit permission choices and readable errors as recommended standards.

## Maintained examples and compatibility

The capture study registers its browser plugin once through the supported capture
bridge. Its custom Start section updates the public binding; shell Start consumes
that snapshot. Shared display lifetime, permission choice, per-action observation
and limits derive from selected capabilities and fixed context values inside a
fresh plugin instance. Retained sharing uses the same plugin operation's
intermediate collection instead of intercepting adapter resources. Its Settings
tab composes Browser Capture and Local Export sections. Local ZIP export creates
a separate delivery instance consuming the canonical reviewed bundle. The
shared-stream recording alternative is a labelled fixture, not implemented video.

`createReportingRuntime`, legacy adapter registration types and `runtime.provider()` remain an explicit compatibility boundary for existing consumers. Legacy collection registrations are translated into plugins and execute through the same coordinator; they do not have a second lifecycle implementation. New consumers should use the plugin API. Existing diagnostic mechanisms, capture factories and provider interfaces remain useful implementation building blocks, not privileged extension roles. Full launcher migration, real screen video and native picker/fidelity acceptance retain their existing owners.
