# Local tickets
Fossil's tickets are artifacts, and artifacts are forever: every typo,
duplicate and piece of spam a tracker accepts becomes part of the
repository's permanent, synchronized history. That is the right property
for the tickets that matter — provenance you can trust decades later —
and the wrong one for an open intake door.
Quarry adds a second tier in front of the artifact log. A **local
ticket** lives in the forge's own database (oltp), never in the zit
store. It renders in the same tracker views, wears the same fields and
vocabulary, and can be triaged like any other ticket — but it does not
sync, it can be deleted outright, and it enters the fossil only when an
admin **promotes** it.
## The two tiers
| | local ticket | fossil ticket |
|--------------------|-------------------------|---------------------------|
| lives in | forge database (oltp) | zit store (artifacts) |
| filed by | anyone holding cap `n` | signed-in accounts, `n` |
| deletable | yes — discard, gone | no (shun is the only out) |
| travels with clone | no | yes |
| promoted | by an admin, one click | is the promotion target |
## Who may file
The gate is fossil's own capability system — no new settings surface.
`ticket.intake` (the local door) requires capability `n`, resolved
through the category closure:
- **Stock behaviour**: any signed-in account holds the `anonymous`
category (`hmnc` — n included), so members file local tickets freely.
- **Open intake to strangers**: grant `n` to the `nobody` category for
that repository in settings. Visitors then file local tickets without
an account. The web form adds a honeypot field and a per-address rate
limit; a discarded spam ticket vanishes without trace.
- **Close intake**: blank `n` from the categories, as fossil admins
always have.
Filing *directly into the fossil* takes `a` — the same letter
`ticket.promote` and `ticket.discard` take, because all three touch the
permanent record. The web form has always worked this way: it offers
the "record in the fossil now" box only to a caller holding `a`, and
files everybody else through intake.
`ticket.new` itself used to take `n`, so the API disagreed with the
form beside it: any Reader could POST straight into the artifact log
and bypass the tier entirely. That is how eleven tickets from an
account with no check-in letter came to be permanent history on this
forge. The two doors carry the same letter now.
Nothing is taken away. `n` still files — through intake — and an admin
promotes. A forge with no local tier has no oltp, and so no accounts
and no capability enforcement at all, which is exactly where the old
direct door has to stay open.
## Where a repository's tickets go by default
Settings → **Tickets** picks the tier new tickets land in:
- **on this forge, promoted deliberately** — the default, and what a
public tracker wants. Nothing enters the artifact log until an admin
promotes it.
- **straight into the code history** — what a repository dogfooding the
store wants. An admin's ticket becomes an artifact at once.
It moves a DEFAULT and never a gate. Writing the artifact log takes `a`
whatever the setting says, and a filer without `a` still goes through
intake — so the setting cannot reopen the door `ticket.new` closed.
## Attachments
A local ticket takes attachments like any other ticket. The upload is
the same two steps (`attachment.begin`, then the PUT, then
`attachment.commit`), the same letter (`b`), the same bucket and the
same policy; what differs is where the stub goes. A ticket in the
artifact log gets an A card at once. A local ticket has no artifact to
hang one on, so the forge HOLDS the stub — name, size, type, SHA-256,
who — in its own database against the local id, and the file lists,
describes and links as any upload does (`local: true` on the commit's
answer, no artifact hash yet). Removal is the same ownership rule and
touches nothing in the log, because nothing in the log was written.
`ticket.promote` writes one A card per held file onto the artifact the
ticket became, each as its uploader's, and applies the held rule to the
uploader then — that is the moment the file first appears in the DAG.
The promotion answers `attached` with the count and `attachErrors` with
any card it could not write; a file that failed stays held under the
local id, so nothing is lost. Attaching to a promoted local id is
refused with the artifact named: the files belong there now.
The new-ticket form says which answer is in force, because a page that
promises "new tickets start local" on a tracker that does the opposite
is telling its reader the wrong thing at the moment it matters.
## The fields a tracker defines
A fossil tracker's schema is the repository's own: it may define
whatever columns it likes, and across the stores this forge mirrors
they do — every one carries `type`, `severity`, `priority`,
`subsystem` and `foundin`, and several add `assignee`, `closer`,
`submitter` and `closedate`. Quarry has always READ all of them. It
could write only fossil's defaults, which made it a second-class
client of its own stores: a tracker with an assignee could be read here
and edited only in fossil.
Both write ops — `ticket.new` and `ticket.set`, and the local
`ticket.intake` — take a generic `fields` map beside the named inputs,
and the forms render one text input per column under
`fields.<name>`. Three rules bound it:
- **The tracker's vocabulary is the gate.** A name the tracker does not
already carry is refused. Fossil creates a column on demand, so an
unchecked map would make every ticket write a schema migration, and
one typo would leave a tracker with an `assigne` column forever. The
vocabulary is read from the tickets themselves (`ticket.config`
reports it as `fields`): there is no verb that reports a schema, so
what the tickets carry is the answer, and a column nobody ever set is
one no form needs to offer.
- **A field with its own input is refused here.** One value with two
doors is a disagreement waiting for its first save.
- **What fossil withholds, quarry does not author.** `private_contact`
and `contact` are labelled "Not publicly visible" in fossil's own
template; writing one through a forge form would put an address into
a permanent, synchronized record from a page that never showed it.
An empty value CLEARS the field — the only way to unset one somebody
set by mistake. A local ticket is gated on the same vocabulary as the
fossil tier, because promotion lands it in that tracker.
## Identity and links
Local tickets mint random 40-hex ids, so `/r/{repo}/ticket/{id}` and
`[/tktview/…]` references work identically across tiers. Listings mark
local rows with a `local` chip and filter on `is:local`.
## Promotion
Promotion is deliberate distillation, not replay. An admin promoting a
local ticket writes **two artifacts**: one `ticket new` carrying the
folded field state and description, with provenance recorded in the
text (reported by whom, when, promoted by whom); and, when discussion
exists, one `ticket set` whose comment collates it verbatim, each entry
prefixed with its author and date. The change-by-change history of the
local tier is working noise and stays out of the permanent log — that
is the point of the tier.
After promotion the local id becomes a tombstone: the old URL redirects
to the fossil ticket forever, and the mapping is the only thing kept.
## What to know before relying on it
Local tickets live in one forge's data directory. They do not clone, do
not sync, and are not recovered by rebuilding a forge from the store —
back up the oltp database if the intake queue matters to you. Anything
whose loss would hurt belongs in the fossil: promote it.