# Programmatic API

ztrack is a CLI first, but its core is a library you can import — to run a check from code, read the
validated model, or drive issue CRUD from a script or dashboard.

> Stability: since **1.0.0** ztrack follows semver — the surfaces documented here break only at a
> major version (see README.md's Stability & dependencies for the full story).
> The package **root** (`import … from 'ztrack'`) is the
> supported surface — it is a hand-curated subset, not a blanket re-export. Other `ztrack/*`
> subpaths are deeper building blocks (documented below) and may change; `ztrack/preset-kit` is the
> one stable deep subpath (see [Preset reference](PRESETS.md)).

## Run a check from code

`checkTracker` validates the live tracker store and returns structured findings — the same pipeline
as `ztrack check`.

```js
import { checkTracker } from 'ztrack';

const result = await checkTracker({ projectRoot: process.cwd() });
// result.ok             → boolean (no error-severity findings)
// result.findings       → [{ code, severity, message, issueId?, acId?, origin?, ... }]
// result.export         → the validated root ({ issues: [...] }) — this IS the snapshot
// result.examinedIssues → set ONLY when validation failed before `export` could be populated
// result.loadedIssueIds → the ids found and handed to validation, whether or not validation then
//                         passed — set by checkTracker (ids the loader found in the live backend)
//                         AND by checkTrackerRoot (ids read straight off an --input/exported
//                         root's `issues` array; ZTB-36); lets a caller tell "not present" from
//                         "loaded but schema-invalid" without guessing from `export` (which is
//                         unset on a shape failure)

if (!result.ok) {
  for (const f of result.findings) console.log(`${f.severity} ${f.code}: ${f.message}`);
  process.exitCode = 1;
}
```

**`examinedIssues?: number`.** How many issue records were actually examined, set *only* when the
root failed validation before `export` could be populated (a shape-invalid root, or a whole-input
parse failure) — a best-effort count read straight off the raw, pre-validation candidate root
(`{ issues: [...] }.length`), not the typed one. On a success path this is left `undefined`; a
reader should prefer `result.export.issues.length` and fall back to `examinedIssues` only when
`export` is absent. It exists so a summary line reports an honest count instead of falling back to
`0` while findings simultaneously cite `root.issues.<n>` for an issue the `0` count implies never
existed.

Every `Finding` carries an optional `origin: { path: string; line?: number }` — where its issue
record lives on disk (a directory-relative path for an `issue-per-file` source, or the containing
file plus the section's starting line for a `document` source). A programmatic consumer can use it
to route a finding straight to the file (and line) that produced it — e.g. group findings by file
for an editor integration, or open the exact location in a PR comment — without re-deriving it from
`issueId`.

`TrackerCheckOptions`: `{ projectRoot?, config?, issues?, sources?, failOnWarning?, categories?, verifyCommits?, now?, phase? }`.
- `issues: ['A-1']` scopes the check to named issues; `sources: ['backlog']` scopes it to declared
  source(s) by config `name`/path/path-basename (mirrors `ztrack check --source`, which is
  repeatable and comma-separated — see docs/SOURCES.md; any selector matching zero sources throws,
  naming it and listing the available names, even when other array entries matched); `phase: 'gate'`
  runs only the continuous-gate rules
  (skip transition/promotion checks); `verifyCommits: false` is the escape hatch for shallow/CI
  checkouts. `failOnWarning` is accepted here but has no effect on `result.ok` — the
  warnings-also-gate decision (and the acknowledged-findings-never-count rule) is a CLI-only
  concern, applied by `ztrack check --fail-on-warning` on top of whatever `checkTracker` returns;
  a programmatic caller that wants the same behavior re-derives it from `result.findings`.

To validate an already-exported root (no disk read), use `checkTrackerRoot(root, options)`. Since
ZTB-36 it honors `options.issues` too — scoping validation to those ids *within* the root (a
requested id absent from the root is reported via `loadedIssueIds`, not silently dropped) — and
sets `result.loadedIssueIds` from the ids it can read off `root.issues`, same contract as
`checkTracker` above.

> **Driving an agent to completion from code.** There is no programmatic loop primitive — the ralph
> loop (`ztrack loop` + the Stop hook) is a CLI/harness construct (see the [Guide](GUIDE.md#3-usage-drive-an-agent-to-green)).
> In an orchestrated/headless setup, *be* the loop: run `checkTracker` each iteration and treat
> `!result.ok` as "not done — keep working" (the snippet above is the gate). `checkTracker` is the
> same oracle the loop and CI use, so an in-code loop enforces exactly what the CLI does.

## Read / write issues

`createTrackerClient` is the programmatic form of the issue CLI.

```js
import { createTrackerClient } from 'ztrack';

const client = createTrackerClient({ projectRoot: process.cwd() });
const list = await client.issue.list({ state: 'open' });
const issue = await client.issue.view('A-1', { json: 'identifier,title,state,body' });
await client.issue.create({ title: 'New case', body: '## Acceptance Criteria\n\n- [ ] dev/01 v1 …' });
```

A runnable example ships at [`demos/sdk-api/run.mjs`](../demos/sdk-api/run.mjs).

## Export and parse

```js
import { exportTrackerRoot } from 'ztrack';                 // validated root, no findings
import { parseRawIssueMarkdown, renderPresetCanonicalIssueMarkdown } from 'ztrack';

const root = await exportTrackerRoot({ projectRoot });
```

## The exports map

The package exposes a deliberately small set of entry points. The root re-exports everything most
callers need; the subpaths exist only where they carry their own weight (the preset-authoring
mechanism, two demoed narrow imports, and the world-integration extension).

| Import | Purpose | Audience |
|---|---|---|
| `ztrack` (root) | **The supported public API**: `checkTracker`, `checkTrackerRoot`, `createTrackerClient`, `exportTrackerRoot`, `serveTrackerApi`, `parseRawIssueMarkdown`/`renderPresetCanonicalIssueMarkdown`, config helpers (`loadTrackerConfig`, `projectRootFrom`, `trackerConfigPath`, …), and types (`TrackerCheckResult`, `Finding`, `CoreRoot`, `Preset`, …) | app / tooling authors |
| `ztrack/preset-kit` | Mechanism to author a **standalone preset** (schema/parse/rules) | preset authors → [PRESETS.md](PRESETS.md) |
| `ztrack/visualizer-kit` | `VisualizerExtension` (bounded dashboard render slots + operational-block policy) + `Payload` (the `/api/board` wire shape) for a repo-owned `extension.tsx` | dashboard extension authors → VISUALIZER.md |
| `ztrack/check` | `checkTracker` / `checkFile` directly (also on the root) | tooling |
| `ztrack/sdk` | `createTrackerClient` directly (also on the root) | tooling |
| `ztrack/world-annotations`, `ztrack/world-source-books` | the world-integration extension a world-using preset imports | [EVIDENCE.md § mirrored world](EVIDENCE.md#advanced-validating-against-a-mirrored-world) |

Prefer the package **root**. Everything else a CLI subcommand needs (mcp, lint, tx, attest, dsse, …)
is an internal module, intentionally **not** a published entry point.

## CommonJS

The package is ESM. From a CommonJS module, use a dynamic import:

```js
const { checkTracker } = await import('ztrack');
```

## GraphQL API server

`serveTrackerApi` (and `ztrack api serve` / `ztrack api query` on the CLI) exposes the tracker over
GraphQL for a dashboard backend. See [Architecture](../ARCHITECTURE.md) for the schema.
