omnitrack 0.3.0

Universal issue-tracker provider contracts and clients (Linear, Jira, ...) for Rust, in one crate.
Documentation
# Architecture

## Workspace layout

```text
issue-providers-rs/
├── crates/
│   ├── core/      # issue-provider-core — neutral contracts + builders
│   └── linear/    # issue-provider-linear — Linear driver
└── examples/      # fluent-builder usage
```

Each provider is its own crate depending only on `issue-provider-core`. Adding Jira means adding `crates/jira` with a `jira()` helper — no edits to core.

## Composition: fluent registry

Providers register into a registry through a fluent builder. The entry point is `provider()` in core; each provider crate exposes a free constructor (`linear()`, later `jira()`):

```rust
use issue_provider_core::{provider, IssueResult};
use issue_provider_linear::linear;

let registry = provider()
    .register(linear())?
    // .register(jira())?
    .build();
```

`register` rejects duplicate provider ids. `descriptors()` reports each provider's id, display name, and capabilities.

## Capabilities

A provider declares which capabilities it supports via its `ProviderDescriptor`. Each capability is a provider-neutral trait:

`Issues`, `Projects`, `Milestones`, `Cycles`, `Teams`, `Users`, `Labels`.

Every capability trait offers `get(id)` and `list(page)` returning `BoxFuture<IssueResult<…>>`. A `TransportNotConfigured*` default implementation returns a `TransportNotConfigured` error, so a provider compiles before its transport is wired.

## Normalized models

Resources are immutable value objects with `make()` constructors and accessors. `Issue` adds fluent `with_project / with_milestone / with_assignee / with_priority` setters. Ids are newtypes (`IssueId`, `ProjectId`, …) to avoid mixing identifiers across entities.

## Pagination

`list` takes `Option<PageRequest>` (built via `pagination().after(cursor).limit(n).build()`) and returns `Page<T>` with `items()` and an optional `next()` cursor. Providers translate this to their native cursor/offset scheme.

## Errors

`IssueError { kind, message }` with `ErrorKind` (`TransportNotConfigured`, `Transport`, `Decode`, `NotFound`, `Unauthorized`, `RateLimited`, `Provider`). Built via `error().of(kind, msg)`. `IssueResult<T>` is the crate result alias.

## Persistence boundary

The crate fetches and normalizes only. The consumer (e.g. unified-dev) maps normalized models into its own storage and runs its own sync worker, cursors, and commands. This mirrors how `akira-billing` is an API client while the app owns storage.

## Roadmap (post-P1)

- P2: real GraphQL transport for Linear + capability implementations + delta queries.
- P3: Jira driver.
- Optional: middleware, rate-limit observation, telemetry (mirroring `vcs-providers-rs`).

---

Prev: [Overview](./00-overview.md) · [Index](./README.md) · Next: [Contracts](./02-contracts.md)