- description
- Build with the Trails framework — define trail contracts, open CLI/MCP surfaces, test with examples, debug errors, migrate codebases, run governance. Use when creating trails, adding surfaces, testing, debugging Trails errors, migrating to Trails, running warden, or any work involving @ontrails/* packages.
- license
- MIT
- metadata
- {"skillset.schema":"1","trails":"0.2.3","version":"0.4.0"}
- name
- trails
# Trails
Contract-first TypeScript framework. Define a trail once with typed input, Result output, examples, meta, and an implementation that establishes how it runs — then surface it on CLI, MCP, or HTTP today, with WebSocket planned on the same contract-first model.
## Quick Start
```typescript
// 1. Define a trail
const greet = trail('greet', {
input: z.object({ name: z.string().describe('Who to greet') }),
output: z.object({ message: z.string() }),
intent: 'read',
examples: [{ name: 'Basic', input: { name: 'World' }, expected: { message: 'Hello, World!' } }],
implementation: (input) => Result.ok({ message: `Hello, ${input.name}!` }),
});
// 2. Collect into topo
const graph = topo('myapp', greetModule);
// 3. Open surfaces
await surface(graph); // CLI — from @ontrails/commander
// await surface(graph); // MCP — from @ontrails/mcp
// await surface(graph, { port: 3000 }); // HTTP — from @ontrails/hono or @ontrails/http/bun
// 4. Headless execution (no surface needed)
const result = await run(graph, 'greet', { name: 'Alice' });
// 5. Test
testAll(graph); // Examples + contract suite in one line
```
## Lexicon
Use these terms — they are non-negotiable in Trails codebases.
| Term | Meaning | Not this |
|------|---------|----------|
| `trail` | Unit of work (atomic or composite) | handler, action |
| `compose` | Composition declaration and runtime verb | workflow, route |
| `topo` | Queryable graph of trails, signals, resources, and relationships | registry, collection |
| `implementation` | Authored behavior that establishes how a trail runs from validated input to Result | handler, impl |
| `surface` | The boundary-owned one-liner that opens a graph | serve, mount |
| `graph` | Local name for a topo instance | app, registry |
| `derive` | Produce canonical facts or an intermediate representation from authored truth | operation |
| `render` | Present derived facts through a surface or format | operation |
| `meta` | Trail annotations and ownership data | tags, metadata |
| `warden` | Governance enforcement | linter |
## Package Orientation
Current public packages are lockstep at the same Trails framework version.
- **Core model:** `@ontrails/core` owns Result, errors, trail/signal/entity/topo contracts, resources, layers, execution, validation, and adapter ports.
- **Surfaces:** `@ontrails/commander`, `@ontrails/mcp`, `@ontrails/hono`, and `@ontrails/http/bun` open the same topo on CLI, MCP, Hono HTTP, or Bun-native HTTP. `@ontrails/http` owns shared route derivation, OpenAPI, and the Web Fetch kernel; `@ontrails/cli` owns the framework-agnostic CLI command model; `@ontrails/vite` adapts Trails surfaces for Vite projects.
- **Infrastructure:** `@ontrails/config`, `@ontrails/permits`, `@ontrails/store`, and `@ontrails/drizzle` cover config, authorization, schema-derived stores, and Drizzle SQLite bindings.
- **Observability:** `@ontrails/observability` defines sink contracts; its `/dev` and `/otel` subpaths provide developer-state and OTel support, while `/logtape` and `/pino` are temporary sink adapters.
- **Ecosystem:** `@ontrails/testing` provides contract tests and surface harnesses; `@ontrails/topography` owns TopoGraphs, semantic diffing, lock manifests, topo-store persistence, and Wayfind graph-read query APIs; `@ontrails/warden` owns governance rules.
- **Stable install policy:** Normal 0.x releases use the npm `latest` tag. Use the exact package pins in [Getting Started](references/getting-started.md). The 0.x line permits documented API changes in minor releases; patches preserve compatibility. Existing 1.0 beta ranges require an explicit manifest migration.
## Agent Wayfinding
When saved Topography artifacts can answer a graph question, use Wayfinder before raw text search:
```bash
trails wayfind --overview --root-dir . --json
trails wayfind --trails --intent read --root-dir . --json
trails wayfind <trail-id> --contract --root-dir . --json
trails wayfind <trail-id> --deps --root-dir . --json
trails wayfind <trail-id> --impact --map --root-dir . --json
trails schema wayfind
```
- Start with `trails wayfind --overview` to learn artifact source, freshness, and graph counts.
- Use the selected operator CLI shape for filtered discovery: `trails wayfind --trails`, `--resources`, `--signals`, `--surfaces`, `--trailheads`, `--entities`, `--errors`, or `--adapter <package>`. Attach bounded related facts with `--include adapters`, `errors`, `examples`, `surfaces`, or `versions`. Topography may expose graph-query APIs beyond the operator CLI/MCP selection; check `trails schema wayfind` before constructing shell calls.
- Use `trails schema <command...>` when you need accepted CLI routes, aliases, flags, and schemas before constructing shell calls.
- Use `wayfind.describe` for a full saved entity record and `wayfind.contract` for a trail or version input/output/intent summary.
- Use `wayfind.nearby`, `wayfind.impact`, and `wayfind.diff` for relation context, blast-radius reads, and explicit saved-baseline comparison.
- Use `trails wayfind query "<phrase>"` for indexed text queries. Treat Wayfinder as graph-read only; do not assume semantic search, signposts, or implications exist in v0.
Wayfinder trails are internal by default. Host apps expose selected queries deliberately, usually as read-only operator tools or MCP resources protected by the host's authorization boundary. Fall back to `rg`, qmd, source reads, or a fresh compile when Wayfinder reports missing or stale artifacts, when the task needs source code that Topography does not derive, or when writing artifacts is outside your current authority.
## Creating Trails
### Atomic vs Composite Trails
- **Atomic trail**: does one thing. `(input, ctx) => Result`. Default choice.
- **Composite trail**: composes other trails. Declares `composes: [...]`, uses `ctx.compose()`.
- **Runnable trail**: an authored contract with an implementation. The runtime runs trails, not implementations.
### Trail ID Conventions
Dotted, lowercase, verb-last: `entity.show`, `math.add`, `search`. Dots become CLI subcommands and MCP tool name segments.
### Input Schema
Every field gets `.describe()` — this becomes `--help` text, MCP descriptions, and form labels.
```typescript
input: z.object({
name: z.string().describe('Entity name to look up'),
limit: z.number().default(20).describe('Maximum results'),
})
```
### Output Schema
Required for MCP and HTTP surfaces. Define what Result.ok returns.
### Intent and Flags
| Field | Effect |
|-------|--------|
| `intent: 'read'` | Safe, no side effects. MCP: `readOnlyHint`. |
| `intent: 'destroy'` | Irreversible. CLI: auto-adds `--dry-run`. MCP: `destructiveHint`. |
| `idempotent: true` | Safe to retry. |
### Examples
Each example is both documentation AND a test case:
- **Full match**: `expected: { ... }` — deep equals
- **Schema-only**: no expected — validates against output schema
- **Error match**: `error: 'NotFoundError'` — asserts error type
See [contract-patterns.md](references/contract-patterns.md) for detailed patterns. Copy from [trail.md](templates/trail.md) or [composition.md](templates/composition.md).
## Surfaces
Adding a surface is a `surface()` call, not an architecture change. The framework derives everything from the trail contract.
**CLI**: Flags from Zod, subcommands from dotted IDs, exit codes from error taxonomy.
```typescript
import { surface } from '@ontrails/commander';
await surface(graph);
```
Use `cli` on a trail only for canonical command overrides or trail-owned aliases that still normalize into the same trail contract. String aliases are sibling leaf aliases (`find` beside `search`); string-array aliases are absolute command paths (`['wf', 'search']`). Author app-owned compatibility routes in an exported `trailsOverlays = [surfaceOverlay({ cli: { 'wf.search': 'wayfind.search' } })]` in the app module, then pass `trailsOverlays` to the CLI surface's `overlays` option. Compile, validate, Wayfinder, and `trails schema` then inspect the same routes the runtime CLI accepts. See [CLI surface](references/cli-surface.md).
Treat aliases, future input mappings, and trailheads as **surface accommodations**: render-level fit adjustments, not alternate behavior. The trail stays the capability. A surface entry is the invocable affordance on a surface; an approach is the way a caller reaches it. Aliases add alternate approaches to the same trail, input mappings normalize surface-shaped input into the same trail input, and trailheads group several trails into one entry while preserving the selected trail ID. Use the ADR-0050 test: if the fit would change intent, permits, errors, outputs, lifecycle, side effects, or hide which trail is running, call it a trail fork and author a distinct or composing trail instead.
Classify surface-fit work before editing:
| Shape | Classification |
| --- | --- |
| One trail, another path, no input reshape | Alias |
| One trail, surface-shaped input that normalizes honestly | Input mapping |
| Many trails, one grouped entry, member trail identity preserved | Trailhead |
| Different intent, permits, errors, outputs, lifecycle, side effects, or hidden member identity | Distinct trail or composing trail |
**MCP**: Tool names from trail IDs, JSON Schema from Zod, annotations from intent, idempotency, and description.
```typescript
import { surface } from '@ontrails/mcp';
await surface(graph);
```
Dense MCP surfaces may use **trailheads** to group related trails into fewer agent-facing tools. A trailhead is surface rendering configuration, not a core `Facet` primitive and not a new domain operation. It groups and selects without merging. Author it in MCP surface options, call it with `{ trail, input }`, and expect successful results as `{ trail, output }` so the underlying trail stays visible.
```typescript
await surface(graph, {
trailheads: {
governance: {
description: 'Run project diagnostics and Warden guidance.',
mcp: { loading: 'deferred' },
trails: ['doctor', 'warden', 'warden.guide'],
},
},
mcpResources: { examples: true, surfaceMap: true },
});
```
Use `trails://surface-map` and per-trail MCP resources for cold context before guessing at grouped affordances. Adapter-kit may validate resolved derived evidence for future surface adapters, but it does not define or author trailheads. Do not invent `facet()`, `overlapsWith`, or adapter-kit `facet` config.
**HTTP**: Routes from trail IDs (dots become path segments), verbs from intent, error responses from taxonomy. Use Hono for framework portability or Bun-native HTTP when you want Bun serving without a third-party runtime; both share the `@ontrails/http` route/fetch kernel.
```typescript
import { surface } from '@ontrails/hono';
await surface(graph, { port: 3000 });
```
```typescript
import { surface } from '@ontrails/http/bun';
await surface(graph, { port: 3000 });
```
WebSocket is planned, not shipped. See the CLI surface docs, the MCP surface docs, and the HTTP package docs for derivation details.
## Resources
Resources declare infrastructure dependencies — databases, API clients, caches — as first-class primitives alongside trails and signals.
**Define** a resource with `resource()`:
```typescript
const db = resource('db.main', {
create: (resourceCtx) => Result.ok(openDatabase(resourceCtx.env?.DATABASE_URL)),
dispose: (conn) => conn.close(),
health: (conn) => conn.ping(),
mock: () => createInMemoryDb(),
});
```
The `create` factory receives `ResourceContext` (`env`, `cwd`, `workspaceRoot`, and validated `config` when the resource declares a config schema — not the full `TrailContext`). Resources are singletons, resolved once per process and cached.
**Declare** on trails with `resources: [...]`:
```typescript
const search = trail('search', {
resources: [db],
input: z.object({ query: z.string() }),
output: z.array(z.object({ id: z.string(), title: z.string() })),
implementation: async (input, ctx) => {
const conn = db.from(ctx);
return Result.ok(await conn.search(input.query));
},
});
```
**Access** via `db.from(ctx)` (typed, preferred) or `ctx.resource<Database>('db.main')` (dynamic escape hatch).
**Test** with zero config — resources with `mock` factories auto-resolve in `testAll(graph)`. Mark live-only dependencies with `unmockable: { reason }` and provide explicit overrides for examples or contracts that need them.
```typescript
testAll(graph, () => ({ resources: { 'db.main': createSpecialTestDb() } }));
```
**Governance:** The warden enforces `resource-declarations` (usage matches declarations) and `resource-exists` (resource IDs resolve in the topo).
See [contract-patterns.md](references/contract-patterns.md) for declaration patterns and [testing-patterns.md](references/testing-patterns.md) for mock strategies.
## Testing
`testAll(graph)` runs the full contract suite in one line:
1. Topo validation (composes, schemas, signals, resources)
2. Example execution (every example as an assertion)
3. Contract checks (output matches schema)
4. Detour verification (targets exist)
**TDD workflow**: Define trail with examples → run tests (red) → implement (green) → refactor.
Edge cases go in `testTrail(trail, scenarios)`. Use `createComposeContext()` to mock `ctx.compose` for composite trail unit tests. Surface integration uses `@ontrails/testing/cli`, `@ontrails/testing/mcp`, `@ontrails/testing/http`, and `@ontrails/testing/surface-parity`.
See [testing-patterns.md](references/testing-patterns.md) for the full testing API.
## Error Taxonomy
17 fixed-category error classes across 10 categories, plus the dynamic `RetryExhaustedError` wrapper, with deterministic mapping to exit codes, HTTP status, and JSON-RPC codes:
| Category | Classes | Exit | HTTP | Retry |
|----------|---------|------|------|-------|
| validation | ValidationError, AmbiguousError | 1 | 400 | No |
| not_found | NotFoundError, VersionNotSupportedError | 2 | 404 | No |
| conflict | AlreadyExistsError, ConflictError | 3 | 409 | No |
| permission | PermissionError, PermitError | 4 | 403 | No |
| timeout | TimeoutError | 5 | 504 | Yes |
عرض على GitHub