---
title: Issue Tracking
description: File and rate reports of what is broken or missing, and take each to an outcome, through calls from your own tools to an issue service Turn Zero Cloud runs.
slug: /backend-packages/tztrack
order: 22
visibility: public
redirects_from: [/backend-packages/issue-tracking]
---

# Issue Tracking

Issue Tracking is an issue tracker with no user interface, operated by Turn Zero Cloud. People, agents, and programs file [reports](/concepts/glossary#issue-and-report) of what does not work or what is missing, rate their experience, and work each report through to an outcome. Every action is a call made from the caller's own tools. The package states the service's contract and includes a client and a [test double](/concepts/glossary#test-double) for an application's backend and its tests.

## Use it when

Use Issue Tracking when an application needs one queue for bug reports, feedback, the checks its test runs record, and ratings that agents file and read as readily as people do. Declare the `issue_tracking` service in the [manifest](/concepts/glossary#manifest), and the platform creates one space, the set of records one owner keeps, shared by both of the application's environments. Your backend reaches the space through the platform's [egress gateway](/concepts/glossary#egress-gateway), which presents the space's credential for it.

Use it for an application's own end users: the backend files each user's report under the user's identity and reads the queue through the client. When your backend files a report for one of your users, send that user only their own report's state. Do not send the call's whole answer: the issue in it holds other reporters' words. Your tool reads the whole space, files into it, rates in it, and settles its reports through the platform's feedback actions, naming the space ([Feedback in your application's space](/guides/feedback-and-ratings#feedback-in-your-application-s-space)).

Do not use it for work that needs boards or sprints, for notifications, or for fields your application defines. The service sends nothing, and an issue moves only through the calls and the passes, along one fixed set of allowed moves between its states. The passes are the AI runs that match new reports to known issues ([What the triage passes do](/guides/keep-your-own-issue-list#what-the-triage-passes-do)). The passes run on Turn Zero's own AI allowance, never on your application's. They read a space only where its configuration turns them on, through the contract's `configure` call ([Configure the space](/guides/keep-your-own-issue-list#5-configure-the-space)).

## What it provides

- **Spaces.** A space contains one owner's records and is identified by a universally unique identifier (UUID). Each of its tokens has one of four permission levels, from lowest to highest `file`, `work`, `record`, and `operator`, one token per holder. Every call made with a token reads and writes that space alone. Keep a `file` token in your backend and give it to no end user, device, or client: its holder names the reporter each call is for.
- **Attributed writes.** Every write names the person, agent, or program that acted. A service that relays its users' actions names each user, never itself, and a relayed read returns that user's own reports and nothing of anyone else's.
- **Issues and reports.** An issue is the one curated record of a problem. It has a short identifier such as `#12`, a kind, a state and an outcome, a priority, a component, what it waits for and who claims it, and the counts of its reports. A report is one filing in the filer's words, never edited, linked to the issue with the reason for the link.
- **The kinds.** `bug`, `gap`, `docs`, `friction`, `question`, `task`, `praise`, and `idea`; `problem`, `error`, and `mistake` file a `bug`. An `idea` takes no priority and is never fixed, and `next` never hands it out. Later requests link to it, so its report count is its demand.
- **What an issue waits for.** `waiting_for` names who or what it waits on: null, `person` (the space's owner), `person:<account>` (one member of a shared space), `reporter`, `outside:<what>`, or `evidence`, which stands exactly while the issue is `waiting`. An issue waiting for a person, the reporter, or an outside event stays open, and nothing closes it on its own. `list` and `count` filter and group by it and by the claim, `none` selecting the issues with no value.
- **Who claims it.** `claimed_by` names who works the issue: null, or a label of 1 to 200 printable characters. `next` alone writes `branch:<name>` with its lease; a session, an agent, or another worker claims through `update` under any other label. A claim while another stands is refused `lease_conflict`, and `give_back` with a reason clears it: a label claim by any caller at the `work` level, a branch's lease only with its secret. A claim untouched for a day reads as stale.
- **Matching at filing.** The service fingerprints the evidence the platform stamped on a report and links a repeat to the known issue without any AI. A report whose origin is not `workspace` or `platform` is never linked by its evidence onto a restricted issue, one marked as a security concern that the service hides from other reporters. A follow-up under the filer's own problem key, the filer's own name for the problem, is a new report on the same issue. Where the issue waits on its reporter, its `waiting_for` reading `reporter`, its reporter's follow-up clears that and returns it to the queue.
- **What a fingerprint needs.** A report links by its fingerprint only where its evidence names the call's `refusal`. Two reports of a call that was answered, naming no refusal, are never linked by it.
- **Undoing a confirmation.** Where a filer's `follow` of a likely match closes the issue its filing opened, which holds that report alone, as a duplicate, an `unmerge` of that issue also withdraws the link the `follow` made. The report then stands on its own issue alone.
- **A repeat of a declined request.** A filing's `candidates`, its likely matches, include an issue closed `declined` or `as_designed`: a request considered and not taken. It ranks after an open issue that matches as well, and a closed restricted one is never offered, whatever the filing's origin. Each candidate carries its `outcome`, null while the issue is not closed, and a fixed `sentence`, such as "Not taken.", and never the resolution's words. A `follow` onto such an issue adds the report and leaves the issue closed.
- **Why a candidate matched.** Each candidate names its issue's `kind` and `component`, and its `summary` where a trusted actor wrote one. Its `matched_evidence` names the members of the filing's evidence that equal those of the standing reports whose evidence chose the issue, and is empty where only the title's words matched.
- **Clearing a resolution.** On the service's current calls, an `update` naming `resolution: null` clears the resolution a new or open issue carries, and that `update` writes none. `settle` and `wait` write one. The `update` that `createIssueTrackingClient` makes still writes a resolution, on any state.
- **Amending your own report.** A filer corrects or withdraws a report it filed with `submit` naming `amends`, the report's number, never the issue's. The amendment is a new report that has `amends` and `withdraws`. It is linked `via: amendment` to each issue the first report belongs to at that moment. So the triager reads the two side by side, and the first report stays as filed. Naming another actor's report is refused `report_not_found`, as is a number that matches no report.
- **What an amendment names.** It names at least one of `title`, `text`, `impact`, `workaround`, `proposed_resolution`, `attachments`, and `withdraws`. Its kind and problem key are the first report's, and so are its title and impact where it names none. A withdrawal moves nothing by itself: a person reads it and settles the issue. `createIssueTrackingClient` has no `amends`, so a backend amends through the gateway with `createIssueTrackingClientV2` ([Methods and results](#methods-and-results)).
- **Images on reports and comments.** A filing and a comment can each carry up to four images, PNG, JPEG, or WebP, each at most 512 KiB, judged by their bytes and never by a name. The service stores each beside its report, under the report's own privacy and restriction, within 16 MiB a space. A read lists each image as `{ id, type, bytes, sha256 }`, and `GET /v2/attachment/<id>` under a `work` token fetches it. A retry stores no image, an operator's `redact` removes one whole, the retention drop takes a report's images with its text, and no AI pass reads one.
- **A note to reporters.** An issue can carry a note of up to 2,000 characters for all its reporters, which a read naming a reporter returns. An `update` under a `work` token or above writes or clears it on any state, and no filing sets it. Never quote a report in it, or name a reporter or a restricted issue. A `fixes` or `resolves` landing writes a note naming its commit and when it landed, which a reporter reads only while the issue is fixed or closed `done` or `verified`. Through the gateway, only `createIssueTrackingClientV2`'s reads return it.
- **Praise apart.** Praise and every other kind are kept on separate issues. A report filed as `praise` opens or joins a praise issue, and a report of another kind joins no praise issue, even under a problem key once used for praise. Where a praise report and a problem's issue meet, the service relates the two issues, unless either is restricted, and links neither report across. No merge joins a praise issue with an issue of another kind, and a praise report never reopens a fixed issue.
- **Mentions become relations.** On the service's current calls, a comment, or an `update` that sets the summary, relates its issue `relates_to` to each other issue its text names, such as `#12`. The service makes each relation itself, at most five a write, and the answer's `mentioned` lists them. It skips the issue itself, a number with no issue, a pair already related or unlinked, and a restricted issue beside one that is not. A quoted line opening with `>` links nothing, nor does a number inside a word, a path, or an entity. A report's text links nothing.
- **Who waited on a settled issue.** On the service's current calls, `settle` answers `related_open`: up to 20 entries, one for each `blocks`, `part_of`, or `relates_to` relation to an issue not closed, in either direction. Each entry names the relation's `type`, `from`, and `target`, and the other issue's `state`. `blocks` comes first, then `part_of`, then `relates_to`, each by level and then by number. A restricted issue is listed only when the settled issue is restricted too. Where 20 are listed, `read` the issue for the rest.
- **Builds and checks.** A build records a version of a release line, the sequence of versions an environment runs, written by the deploy. A check records each run of a monitored job under a stable key, and an issue opens from it only once the failures repeat. An issue a check opens reads `blocked` where the check breaks after at least three green runs and the failure is in the product or the client. A build that names no line counts toward neither. A landing records the commit that fixed an issue, with its regression test and proof.
- **Withdrawing a run.** The job that posted a check's run can withdraw it by the key it posted it under. The withdrawal stays in the check's history and closes an episode the run opened. A `key` given with it is the withdrawal's own retry key, never the run's.
- **Verification by exposure.** A fixed issue closes verified once live builds that contain the fix have exercised it a set number of times, with no report of the same point since. An issue nobody can explain waits, and the next report reopens it. A bug or a friction closed `done` by hand, with no landing, names in `fixed_in` the build that serves the fix where a returning reporter filed on it, and may name it otherwise. A later report then reopens the issue only when it comes from that build or a later one, and with no `fixed_in` it reopens nothing.
- **Passes.** Two AI passes propose and check duplicates, splits, and themes, and the triage pass judges each new issue to give it a priority from 1 to 3. A merge is applied where they agree and the evidence on at least one side was stamped by the platform or confirmed against its records. A pass never edits a report, and never moves a priority a person or a program wrote. [How an issue gets its priority](/guides/keep-your-own-issue-list#how-an-issue-gets-its-priority) explains each rule.
- **What holds a merge.** A merge the passes agree on waits while its duplicate carries a `plan`, is `fixed`, or carries a branch's claim. Once the duplicate is free, the passes apply the merge at their next write, or withdraw it where it no longer fits their limits. An agreement lapses where the master closes, a report joins either issue, or a component is set. A merge withdrawn because its master closed as a duplicate is proposed again against the surviving master. A person's `settle` applies a merge held by a plan at once, and so does `createIssueTrackingClient`'s `settle` with the act `approve`.
- **What the passes propose.** A `read` without `actor` answers `proposals`: each merge, split, or theme the passes proposed that still waits and reaches the issue. A merge reaches its duplicate and its master, a theme its issue, and a split the first issue its report is linked to. Each entry carries its `id`, its `type`, its other end, the passes' `verdicts`, its `disagreements`, its instant `at`, and `restricted`. `list` with `proposed: true` selects the issues a waiting proposal reaches.
- **How often the passes' merges were wrong.** `read_space` returns `sampled_merges`, `{ rate, audits }`: the share of the last thirty days' audited merges judged `wrong`, 0 where none was audited, and how many verdicts it counts. While `rate` is above the configuration's `propose_only_threshold`, an agreed merge waits for a person. `relay_issue_act` forwards `read_space` only under an `owner` token.
- **Ratings and asks.** The human series holds the standard Net Promoter Score question, scored 0 to 10, and the agent series an agent's rating of how difficult its task was, scored 1 to 5. The two are never combined. An ask records a request for a person's rating, and a throttle limits how often one account is asked.
- **A space's figures.** `count` returns how many issues have the state, outcome, kind, component, label, `waiting_for`, `claimed_by`, level, or requirement you name, leaving out issues with labels you name. It returns a total, or totals by any one field except the requirement. `rates` returns how many issues were filed, closed by outcome, reopened after a fix, and still open, per day or week over up to 400 steps. It also returns the time to close by kind and level. Both take an `operator` token. `relay_issue_act` forwards both under an `owner` token, and the gateway forwards neither ([Keep your own issue list](/guides/keep-your-own-issue-list)).
- **Counting through the gateway.** A deployment counts the issues its `list` answers. It pages `list` with `limit: 100` until the answered `cursor` is null, since a page can hold fewer issues than its limit while more follow. A large space is counted over several minutes, its pages paced within the space's rate. From development a `list` names its actor, so that count holds the actor's own issues alone.
- **Reads and erasure.** List, search, and export read a space. Erasure replaces a person's identity on every record and keeps every report's text, so leave out of a report anything you do not want kept.
- **A client and a test double**, which [The client](#the-client) and [Test with the double](#test-with-the-double) describe.

### States, permission levels, and moves

An issue is in one of five states: `new`, `open`, `waiting`, `fixed`, and `closed`. A filing opens it `new` where the space's passes run, and `open` where they do not, the default. Triage, or an `update` that sets its kind or component, moves a `new` issue to `open`.

`wait` parks a `new` or `open` issue as `waiting`, with what would settle it, as a green check does for the `open` issue it opened. With `waiting_for` set to `person`, `person:<account>`, `reporter`, or `outside:<what>`, it sets what the issue waits for and moves no state. An `update` giving a `waiting` issue any other value is refused `state_conflict`, `reopen` the way out.

A `fixes` or `resolves` landing recorded with `land` moves an issue to `fixed` or closes it `done`. A `narrows` landing leaves its state, and a `revert` reopens it. An `operator`'s `settle` closes an issue with its outcome, and `reopen` moves a `fixed`, `closed`, or `waiting` issue back to `open`. A new report, or one a merge moves in, reopens a `waiting` issue or one closed `unexplained`.

A closed issue carries one outcome: `verified`, `done`, `as_designed`, `declined`, `duplicate`, `superseded`, `noted`, or `unexplained`. A fixed issue closes `verified` once live builds have exercised the fix enough times with no report of the same point, and one nobody can explain closes `unexplained` after the long wait. A `praise` filing that opens its issue closes it `noted` at once.

Each token has one permission level, and a level allows its own calls and those of every level below it:

- `file` files with `submit` and confirms a repeat with `follow`, for the reporter each call names.
- `work` reads, lists, and searches issues, sets an issue's curated members with `update`, comments, relates, and splits. It sets what an issue waits for with `wait`, takes work with `next`, gives work or a label claim back, and records ratings and asks.
- `record` confirms a report's evidence and records landings, builds, shipments, exposures, and checks.
- `operator` settles, reopens, and unmerges issues, clears `restricted` marks, redacts, reads counts and rates, exports, erases an actor, reads and restores the record, configures the space, mints and revokes tokens, and deletes the space.

### Restricted issues

The service hides a restricted issue from a reporter and from no one else. Below the `work` permission level, and in every read that names a reading actor, a restricted issue is never returned. From the `work` level up, every read returns it as it returns any other issue. A filing whose origin is not `workspace` or `platform` is offered no restricted issue as a likely match, under any token. The one exception is a filing under an `operator` token that names its caller's `relayed_level` as `work` or above, as the platform's relay does at the `contribute` level.

A filing marked restricted links only to restricted issues, and otherwise opens its own restricted issue. A later confirmation of its report links it only to restricted issues too. A filing kept back from a restricted issue opens its own issue restricted as well, and so does a report a later confirmation keeps back, where its issue holds that report alone.

Where a report is kept from a restricted issue closed `done` or `verified`, the daily sweep surfaces that issue once with the label `recheck` and leaves it closed. A person reads it and reopens it or leaves it.

Code that reaches the service with its own token reads these answers:

- Under a `work` or `record` token, `list`, `search`, and the counts of `read_space` include restricted issues.
- A `list` that names `actor` answers no restricted issue, whatever the token, as the reporter's own view never does. With `proposed: true` it selects no issue by a proposal that reaches a restricted issue.
- A `read`'s `proposals` entry that reaches a restricted issue carries `restricted: true`. The calls that answer it take a `work` token, so a reporter never reads one.
- On the calls `createIssueTrackingClient` makes, an issue record carries `restricted`, `true` or `false`. Their `update` sets no `restricted`, and is refused `invalid_request` where it names one.
- On the service's current calls, an `update` that sets `restricted` to `false` is refused `grant_required` under a token below `operator`, before the key is read. Setting it to `true` needs a `work` token.

The restricted mark follows the words. When one issue is merged into another, the issue kept is restricted where either was, and its `merged` entry carries `restricted: false` where the merge set the mark. A split from a restricted issue opens restricted, a person's as well as a pass's. An `unmerge` leaves each issue's mark as it stands.

The triage pass looks for a restricted issue's likely matches, and offers a restricted issue among another issue's. A merge or a theme between a restricted issue and one that is not waits for a person, and the passes withdraw it where it no longer fits their limits. A merge or a theme of two restricted issues follows the usual limits.

The second reader is the passes' second reading of an issue's priority and of a closed issue's restricted mark. It reads a restricted issue closed `declined` or `as_designed` after its other readings in the critic's run. Where both its readings find no exposure, it sets `restricted` to `false` in one `updated` entry. Otherwise it writes one `kept` entry, a history entry recording that it kept the mark, and the mark stays. After a person redacts what it found, it reads the issue again. The calls `createIssueTrackingClient` makes return no `kept` entry.

When a pass gives up on an item, it writes a `critic_failed` entry on the item's issue. The entry names the item and why: three failed attempts, or 36 hourly runs with no answer from the model. An item given up on for want of an answer is still tried once each run, and the first answer written for it adds a `critic_cleared` entry. The calls `createIssueTrackingClient` makes return neither entry.

### How the gateway keeps both environments apart

The space is created when the manifest is first submitted. Your deployments in each environment file into it and read from it, and so do your [local runs](/concepts/glossary#local-run), under the development credential. The submission result's `issue_tracking` list gives the space, and never a token. An application that has a space for each environment keeps both until it is deleted, its production space created at its first [promote](/concepts/glossary#promote), or its first production deploy where it has one environment.

The gateway keeps the two environments apart in three ways:

1. It stamps every report your backend files with the environment the call came from and the version it serves, `local` from your own machine. On the first client's `submit` these are `evidence.environment` and `evidence.version`. On `createIssueTrackingClientV2`'s they are one `seen_in` entry, `{ line: null, version, environment }`, in place of any the call names, and an amendment carries none.
2. It puts `development:` in front of every person, agent, or program id a development call names, so a development tester never appears as one of your production users. From development, a `list`, `list_signals`, or `export` without `actor` is refused `invalid_request`, as are `read` and `search` on either client. So is a development write that names another issue by its id, and an id that already starts with `development:` or passes 200 characters once prefixed.
3. A deployment reaches the space at its binding's [grant level](/concepts/glossary#grant-level): `report` for the application's own space or a space for each environment, and `report` or `contribute` for a space of your account. At `report` it files reports and reads the space, and the gateway refuses `issue_tracking_call_refused` every call that changes an issue: `update`, `comment`, `relate`, `settle`, `signal`, `export`, `erase_actor`, `open_ask`, and `close_ask`.

Every filing on `createIssueTrackingClientV2` reaches the space with the origin `field`, whatever it names, so the service reads it as a reporter's. One naming an `evidence_basis` other than `claimed` is refused `invalid_request`, and so is one naming `relayed_level`.

At `report`, the gateway leaves a [restricted issue](#restricted-issues) out of every response, whichever space the binding names. A list or a search skips it, and a read of it is refused as for an issue the space does not hold. The response to a filing or an amendment names no restricted issue. A read's `proposals` leave out each entry that reaches one, and a reporter's own view never holds one. The counts `read_space` returns include restricted issues, because the gateway forwards a count as it comes.

At `report` a `list` that names `proposed` is refused `issue_tracking_call_refused` on either client; its other filters still work. A read at `report` leaves out each history entry that records a proposal. On the first client's calls there, every issue's `proposed` reads `false`, and a read leaves out a relation that is still only proposed.

A finding is an issue that groups other issues, its members, stored as a `task` labelled `finding`. On the first client's calls at `report`, a finding's `members` list only the issues that are in it. A finding whose own `proposed` field is true is skipped as a restricted issue is, as is one a proposal made and nobody approved.

The first client's list and search also skip a finding the passes made that a person approved and later declined by hand, though a read of it still returns it. On `createIssueTrackingClientV2` the gateway skips every finding with no report of its own, approved ones included, so a backend at `report` sees no such finding. Read findings at a higher grant level.

The `contribute` grant level adds the working calls, and `owner` reaches every call; no deployment has `owner`. The call that starts a deploy may name `commit`, 7 to 64 lower-case hexadecimal characters. After each deploy, promote, and rollback, the platform writes a build record into the application's own space naming the version, the environment, and that commit, never delaying the deploy. A malformed `commit`, or one on the call that only prepares an upload, is refused `invalid_request`. An application with a space for each environment gets no build record.

### A space of your account's own

A manifest can instead name a `space` your account created with `create_issue_space`, and the platform binds the application to it under the same three rules. [Bind an application to the space](/guides/keep-your-own-issue-list#bind-an-application-to-the-space) gives the `space` and `level` to name, the release line the platform adds, and their refusals.

### Deletion and copies

The platform keeps the space's token in [custody](/concepts/glossary#custody) under a platform-minted name, and no application and no author has it. A manifest revision that drops the kind changes nothing: the space and its records remain. The platform reads a space's records only through the feedback actions naming the space, the export, the deletion of your account, and the daily copy.

Deleting the application deletes its own space with every record in it, and ends a binding to a space of your account's own. Deleting your account deletes every space of its applications and its own. Deleting the development environment deletes nothing of a shared space, ends a development binding, and deletes a per-environment development space. A deleted space cannot be recovered.

Once a day, the platform copies each space your account holds and each application's own space into storage it holds for your account. A copy is kept 35 days, the newest until a newer one is written, and a space's copies are deleted with it. Per-environment spaces are not copied, and no action reads or restores a copy.

The daily copy lists a report's or a comment's images and carries none of their bytes; a copy the contract's `read_record` answers carries them, and a restore from it brings a redacted image back. Deleting a space deletes its images with it, and an operator removes one image whole with `redact`.

## Availability

Version 0.31.0 is the package's current version, and this page describes it. Until the library's next publish, `list_library` and `read_library_entry` can return an earlier version, the one you can take, so this page can describe a change that version lacks. The entry includes the `testing` subpath from version 0.29.0. Under the package's earlier name, `issue_tracking`, the subpath stood from version 0.1.0. The entry contains the compiled clients and double as the npm package `@turnzero/tztrack`, the package's [product statement and detailed contract](/concepts/glossary#product-statement-and-detailed-contract), and its integration guide. The contract's "Migration to" sections list what changed at each version that moved a promise.

Turn Zero Cloud runs the operated service and creates one space for every application that declares the kind. The calls, the records, and the spaces' stored bytes count against no plan quantity, and no charge attaches to a space, a call, or a record ([Plan and usage](/guides/plan-and-usage)). The service limits each space's forwarded calls per minute, and a call past the limit is refused `rate_limited`. When many reads of large spaces run at once, the service can also refuse a read `rate_limited`: wait a few seconds and call again.

## Related feature packages

The tztrack package depends on no other feature package. [Storage](/backend-packages/storage) states the rule this service applies to every write: where a service relays its users' actions, each write names the user who acted. [Account](/backend-packages/account) verifies the end users an application names as actors.

## The client

The package is published in the library as the npm package `@turnzero/tztrack`, not on the npm registry; [Use the library](/guides/use-the-library#5-install-a-package-s-compiled-code) installs it with one line. The module holds two clients: `createIssueTrackingClient`, the one a hosted backend uses for most calls, and `createIssueTrackingClientV2` for the service's current calls. The gateway forwards four of the current calls, `submit`, `read`, `list`, and `search`, so your backend can amend its users' reports. A [relay](/concepts/glossary#relay) of your account reaches the rest.

### Construction

`createIssueTrackingClient(transport)` builds the hosted backend's client, with no options. `transport(path, init)` sends each call to `POST /egress/v0/issue-tracking/v1/<call>` under the application's [platform credential](/concepts/glossary#platform-credential), `TURNZERO_CLOUD_APP_TOKEN`, and returns the response. The path sits on `TURNZERO_CLOUD_GATEWAY_URL` where the deploy sets it, and on `TURNZERO_CLOUD_API` where it does not. The gateway finds the space from the calling application and environment, and presents the space's token for it. A [local run](/concepts/glossary#local-run) reaches the same space through the platform's public gateway under the development credential, and its reports are stamped `local`. A test runs the double in its place ([Test with the double](#test-with-the-double)).

`createIssueTrackingClientV2(transport)`, also with no options, runs over the same transport, each call at `/egress/v0/issue-tracking/v2/<call>`.

### Methods and results

The gateway forwards `submit`, `read`, `list`, `search`, `read_space`, `list_signals`, and `read_ask` at the `report` grant level, each a method of the client, such as `client.submit(request)` and `client.listSignals()`. Each method takes `{ signal }` as its last argument.

- `submit({ actor, kind, source, title, body, evidence, key })` files one report, `actor` being `{ kind, id }` for the user you file for. It returns `{ issue, outcome }`, the outcome one of `filed`, `retried`, `repeated`, `reopened`, and `recovered`.
- `read(issue)` returns the issue, its history, comments, relations, and members. `list` and `search` return issues, `list` in the order `newest` or `score`, the service's rank by an issue's reports.
- On this client, a priority is 1 to 4. `excerpt` may only be `false`, and `true` is refused `invalid_request`. `recovered: true` is refused `recovery_unmatched` unless that actor's own filing under that key opened the issue and every report on the issue is that actor's.

On `createIssueTrackingClientV2` the gateway forwards `submit`, `read`, `list`, and `search` at both grant levels, and refuses every other call `issue_tracking_call_refused`. Make those calls with the first client.

- `submit({ actor, origin, kind, title, text })` files one report, and returns its `report`, whose `id` is its number, with the `issue` and the `outcome`. The gateway files it with the origin `field`, and the service refuses a `question` or a `task` from that origin, so file those with the first client.
- `submit({ actor, amends, text, withdraws })` corrects or withdraws a report that `actor` filed, `amends` being its number. It returns the outcome `amended`.
- `read({ issue, actor })`, `list({ actor })`, and `search({ query, actor })` return that user's own view: the issues their reports belong to, each with their reports and each report's `amends` and `withdraws`.

### Failures

- `IssueTrackingGatewayRefusal`, for the gateway's own refusal, which the client tells apart by the marker header. `issue_tracking_call_refused` is a call the gateway does not forward, and `issue_tracking_application_required` a credential that names no application. `issue_tracking_not_provisioned` is an environment with no space, and `issue_tracking_unavailable` a token that cannot be read, which may be retried.
- `IssueTrackingRefusal`, for the service's refusal by name in `refusal`, with `status` and `detail`. A refused call changes nothing. Its `own` is null except on a `read` naming `actor` refused `issue_not_found`, where that user's own report joined the issue the number names. There `own` is the identifier of the issue that report is on now. A read naming the same actor returns that issue. `own` never names an issue that user cannot read. It is null for another user's issue, for a restricted issue, and for a number that names no issue.
- `IssueTrackingUnreachable`, where the transport failed or the service answered a fault of its own. Retry with the same `key`.

Branch on `refusal`, never on `detail`. The [refusals page](/reference/refusals) lists the gateway's four refusals with their cause and remedy. The service's own names, `recovery_unmatched` among them, are listed in the package's contract, the entry's `tztrack.md`.

## Test with the double

`createIssueTrackingDouble(options)` from `@turnzero/tztrack/testing` answers both clients' calls in memory, and passes the same list of test cases the contract states. Seed a space with `double.createSpace()`, then take `double.gatewayView({ space })` over its `space.id`: the gateway's view, as the deployed backend reaches its space. Build `createIssueTrackingClient(view.transport)` with no options, and `createIssueTrackingClientV2(view.transport)` for the four current calls the view forwards. `reset()` empties every space, so seed a space and take its view in each test. A test runs this sample over the double.

```js
import assert from 'node:assert/strict';
import { createIssueTrackingClient, createIssueTrackingClientV2, IssueTrackingGatewayRefusal } from '@turnzero/tztrack';
import { createIssueTrackingDouble } from '@turnzero/tztrack/testing';

// One space, reached as the deployed backend reaches it: through the gateway's view.
const tracker = createIssueTrackingDouble();
const { space } = tracker.createSpace();
const view = tracker.gatewayView({ space: space.id });
const client = createIssueTrackingClient(view.transport);

// The backend files the signed-in user's report under the user's own id.
const filed = await client.submit({ actor: { kind: 'person', id: 'user-1' }, kind: 'bug', source: 'person', title: 'Export does nothing', body: 'Clicking Export downloads no file.' });
assert.equal(filed.outcome, 'filed');
// Answer the user with their own report's state alone, never the whole issue.
const reply = { issue: filed.issue.id, status: filed.issue.status };
assert.equal(reply.status, 'open');
// The same transport carries the current client's filing and the user's own amendment.
const current = createIssueTrackingClientV2(view.transport);
const first = await current.submit({ actor: { kind: 'person', id: 'user-1' }, origin: 'field', kind: 'bug', title: 'Import fails' });
const amended = await current.submit({ actor: { kind: 'person', id: 'user-1' }, amends: first.report.id, withdraws: true });
assert.equal(amended.outcome, 'amended');
// A call the gateway refuses throws its own refusal, told apart from the service's.
view.refuseNext('issue_tracking_unavailable');
await assert.rejects(client.list(), (error) => error instanceof IssueTrackingGatewayRefusal && error.refusal === 'issue_tracking_unavailable');
```

The client's refusals are those [Failures](#failures) lists. These come from the double's own setup:

| Refusal | Cause | Remedy |
| --- | --- | --- |
| `issue_tracking_call_refused`, `issue_tracking_application_required`, `issue_tracking_not_provisioned`, `issue_tracking_unavailable` | `view.refuseNext` scripted the gateway's refusal, or the call named a path the gateway does not forward, such as `delete_space`. | Call through the client, and assert the refusal where the test scripts it. |
| `issue_tracking_not_provisioned`, unscripted | The view's space does not exist: it was never seeded, or `reset()` emptied it. The view returns this refusal on its own for every call to a space that does not exist. | Seed the space with `double.createSpace()` and take its view again in each test. |
