# Reporting configuration and adapter contracts

The [reporting plugin contract](./plugins.md) supersedes the separate extension roles described below. This page retains the supported compatibility API: legacy collection runs through the same plugin operation coordinator. New consumers should register plugins with owned settings, capabilities and optional tab contributions.

Status: accepted configuration foundation from #81, with [capture behavior revisions](https://github.com/CosticaPuntaru/SnagBadger/issues/83#issuecomment-6150412698) implemented by #126. The user selected stable registration IDs with later-registration precedence (Q16, superseding Q14's duplicate-error preference), and isolated runtime failures with retained evidence/drafts (Q15). This document describes the first implementation of those contracts; the original contract was accepted and merged. The accepted C interface is preserved, with real collector wiring in #82–#84.

## Compose selected adapters

`@snagbadger/configuration` owns composition, selection and resolution. It imports no integrations, browser globals, React or CSS. Plain objects work directly; helpers are optional independent named exports. Presets are ordinary configuration objects/functions, with no special permission mode. IDs identify registrations within an adapter kind, not adapter implementations: one implementation can be configured in several slots. Capture registrations additionally have an optional category ID for mutually exclusive alternatives.

```ts
import {
  composeConfiguration,
  pipeConfiguration,
  removeAdapter,
} from '@snagbadger/configuration';
import {
  createReportingRuntime,
  type ReportingAdapters,
} from '@snagbadger/reporting/session';

const configuration = composeConfiguration<ReportingAdapters>(qaPreset, {
  ui: { zIndex: 40 },
  adapters: {
    harvesters: [
      {
        id: 'http',
        label: 'Network requests',
        load: () => withCompanyFiltering(httpHarvester()),
        settings: [
          {
            id: 'bodies',
            label: 'Request bodies',
            type: 'boolean',
            default: false,
            editable: false,
          },
        ],
      },
    ],
  },
});
const narrowed = pipeConfiguration(configuration, (input) =>
  removeAdapter(input, 'harvesters', 'websocket'),
);
const reporter = createReportingRuntime(narrowed);
```

`qaPreset`, `httpHarvester` and `withCompanyFiltering` above illustrate developer composition; production built-ins are delivered in #82. The executable study in `examples/playground/configuration-fixtures.ts` uses explicitly named fixture implementations and an actual wrapper that changes collected labels.

- Categories stay consumer-typed: reporting supplies harvesters, captures, providers and a caller-selected launcher type. There is no central list of every integration.
- Later scalar settings win; `ui` fields combine. Registrations combine by ID within their category. The last complete registration wins, including its loader, label, enablement and settings tree. Earlier position is retained for deterministic UI order; new IDs append. IDs are case-sensitive and nonempty. Different categories may use the same ID.
- `removeAdapter` removes an ID. `replaceAdapters` replaces a whole category; pass `[]` to clear it. Merely composing `[]` adds nothing. Neither mutates its input.
- `selectConfiguration` evaluates host-owned predicates in order, stopping at the first match. Predicates may be asynchronous. No match disables reporting unless a fallback is supplied. Predicate errors reject initialization; they never activate adapters.
- `resolveConfiguration` creates an immutable initialization snapshot, checks IDs/labels/defaults and copies nested settings. It never invokes `load`. Changing configuration requires disposing and creating another runtime. Functions/closures remain host-owned references: they must not read mutable configuration to bypass the resolved policy.
- `enabled: false` disables reporting. A disabled registration cannot be selected. Unknown selections/settings fail before loaders run. Factories/imports/loaders must be inert; resource acquisition belongs in `start`.

## Settings structure and permissions

Each registration can carry a recursive `Setting` tree. Boolean, bounded numeric and enumerated-choice nodes have IDs, labels, defaults, optional children and an explicit `editable` flag. This is portable descriptive data; UI rendering remains optional and replaceable. It can drive the accepted master/child controls without embedding React in a harvester. Short choices can use segmented controls and larger lists dropdowns.

Node IDs are path segments without dots. Nested values use dotted paths, scoped to their registration. For example `settings['harvesters/http']['payload.limit']`. Duplicate paths, invalid defaults, unknown paths, locked changes and values outside the developer's range/options are errors. Editable defaults describe developer-authorized alternatives; setting a default to false does not itself prohibit an editable true choice. For prohibition, lock it false or disable the adapter.

Child values remain preferences: a collector must treat an off parent as disabling its entire branch, and the UI retains child choices while hiding/disablement is active. The adapter's own options/factory remain the final implementation of filtering, redaction, retention and byte limits. These guarantees must be verified for every real built-in in #82/#83; settings metadata alone cannot constrain malicious developer-authored code.

## Consumer-owned adapters

Capture owns `CaptureAdapter`, `ActiveCapture`, `CaptureOutput` and `AdapterContext` in `@snagbadger/capture`. Reporting owns `Harvester` and runtime coordination in `@snagbadger/reporting/session`. Embedding owns `LauncherAdapter`; callers pass its type to `ReportingAdapters<LauncherAdapter>`. Reporting's existing `ReportProvider`, `BugReport` and portable bundle format are reused, rather than defining another delivery/report representation.

```ts
const customHarvester: Harvester = {
  start(context) {
    // Acquire only now; obey cancellation and developer bounds.
    const collector = subscribeToAllowedAppEvents(context);
    return {
      collect: () => collector.snapshot(),
      dispose: () => collector.unsubscribe(),
    };
  },
};

const wrapped: Harvester = {
  async start(context) {
    const active = await customHarvester.start(context);
    return {
      collect: async () => sanitize(await active.collect()),
      dispose: () => active.dispose(),
    };
  },
};
```

Wrappers return the same interface as the wrapped adapter. No separate “adapter for adapters” registry or mandatory inheritance is needed. A capture additionally declares snapshot/recording mode. A launcher mounts using open/stop callbacks, updates idle/recording/open state and provides disposal; the browser embedding implementation owns its host DOM/style isolation. A provider implements the existing `deliver({ report, bundle })` method. Registration loaders can defer importing these implementations; the core does not import built-ins.

Capture output contains canonical evidence/interactions and attachment bytes keyed by evidence ID. Output is validated against existing report decoders. Attachment bytes are copied before returning; unknown attachment keys and conflicting/invalid evidence are failures. Adapters own unique evidence/interaction IDs, for example by incorporating their registration/session identity. Successful results merge in selection order, independently of asynchronous startup order. A conflicting adapter's complete output is rejected rather than overwriting prior evidence.

## Activation, cancellation and cleanup

```ts
const session = reporter.start({
  harvesters: ['http', 'app'],
  captures: ['screenshot', 'steps'],
});
await session.ready;
// The optional reporting UI now lets the reporter reproduce the issue.
const result = await session.finish();
// result.evidence, result.interactions, result.attachments, result.failures
await reporter.dispose();
```

Runtime construction is inactive. `start` is an explicit interaction boundary and exposes the session immediately, so `cancel` is available during asynchronous loading/activation. Only selected enabled adapters load and start. The runtime permits one active session and rejects overlapping starts. Repeated finish/cancel calls settle the same result; cancellation notifies adapters and abandons waiting on pending activation/collection. No cancellation exceptions become artificial adapter failures. Active resources are disposed once, on finish/cancel/dispose, and another session receives fresh instances.

`AdapterContext.cancellation` is a portable subscription interface. Adapters bridge it to AbortController or their library's teardown without requiring DOM globals in functions-only imports. A noncooperative startup that resolves after cancellation has its resource disposed when it arrives. Cancellation cannot physically stop arbitrary custom work; adapters must cooperate. Such late cleanup may occur after the cancellation result and its cleanup errors are not retroactively included in that settled result. Real integrations require cancellation/resource conformance tests.

Cancellation abandons activation and collection waits, but waits for cleanup of resources already acquired. `dispose()` must settle after releasing its resources. If custom cleanup never settles, finish/cancel/runtime disposal remain pending and the runtime rejects another session until cleanup completes. This deliberately preserves resource exclusivity; there is no timeout that silently abandons live resources. Cleanup errors that reject are isolated. Prompt cancellation therefore depends on cooperative, terminating cleanup, and every real adapter needs a cleanup conformance test.

`start({ preRecording: true, harvesters: [...] })` is explicitly opted-in diagnostics and rejects capture selections. Developer configuration must first allow it with `preRecording: true` (default permission is false); this permission itself never starts collection. Context marks the phase and supplies the registration ID. Finish/cancel/dispose stop it; a subsequent session defaults to ordinary recording. The reporting UI owns the default-off/reset checkbox. Reload persistence/activation and carrying pre-recorded diagnostics into the main report remain #82/#84 work; this runtime never automatically reloads, installs listeners or stores an opt-in. HTTP/WebSocket/browser/app are separate registrations; existing sockets cannot yield unobserved earlier messages.

## Failure and submission behavior

Configuration/selection errors prevent activation. Runtime start/collect/dispose errors produce `{ kind, id, phase, message }` failures. Diagnostic-harvester errors retain successful output. A selected capture error instead stops the complete attempt and discards its evidence, interactions and attachments; the UI shows the error while the caller preserves its prior draft and attachments. This supersedes the original mixed-success capture behavior. Session `completed` resolves on normal finish, cancellation, capture failure or resource-originated completion. Capture adapters can invoke context `finish` for native Stop sharing and `fail` for asynchronous capture failure. UI and providers must clearly represent unavailable evidence. Each adapter owns its cleanup and host-behavior preservation, including restoring wrapped functions only when it still owns the wrapper.

Providers load only on explicit `runtime.provider(id)`. Delivery uses the existing submission/bundle helpers and their explicit sanitization policy. The runtime never mutates or clears a report draft; the caller retains it after failed delivery and retries deliberately. It does not claim cross-provider retry/idempotency, cancellation of external delivery, authentication, shared-device PIN security or ticket retrieval. These remain with #11. `runtime.dispose()` disables further session/provider access; resources returned by a provider loader must remain inert until delivery.

## Review and remaining work

Run `pnpm dev:examples`, then open `playground/branding/?page=configuration`. The contract study shows the original HTTP registration replaced by a wrapped custom one, untouched app registration, isolated failure, custom capture and custom local provider using a real portable bundle. All collection/delivery adapters there are fixtures: it does not capture host traffic/screenshots or contact providers.

Direct tests cover composition, selector failure/no-match, immutable settings/defaults/permissions, inactivity, built-in/custom lifecycle parity, pre-recording reset, cancellation during startup, late cleanup, runtime reuse/disposal, and isolated failures. Browser verification exercises the study twice and preserves the accepted reporter route.

This slice establishes composition and portable lifecycle contracts, not the production in-page SDK. #82 provides four actual harvesters and retention/privacy/host-behavior verification; #83 provides real screenshots/steps/visual replay and capture-specific permission/navigation decisions; #84 wires the accepted drawer, local export/import and report draft. UI descriptors, custom renderers, provider credentials and launcher installation are consumed there through their respective seams. Human review settles #81 before dependent delivery; no merge/release/deployment is implied.

Capture alternatives use distinct registration IDs and the same `category`. `captureCategories` exposes enabled alternatives and the first configured default; selecting two captures in the same category fails before loading. The accepted recording category offers either DOM replay or screen video. See the [capture experiment](../prototypes/capture-experiment.md) for shared display sources, screenshot timing and current video limitations.
