---
title: Configuration contract v1
description: Configure documentation corpora, ownership routing, conformance, and gates.
---

# doc-bridge config contract v1

`doc-bridge.config.ts` (or `.js`, `.mjs`, `.json`, or `package.json` → `docBridge`) is the v1 integration point for any project. Layer 0 fields are sufficient to run `index`, `query`, and MCP without an LLM.

Reconciliation summaries also expose deterministic `diagnosticsByCode` and
`diagnosticsByStatus` maps. They are additive rollups for agents and dashboards;
the canonical `diagnostics` array remains the source of evidence.

| npm package | `@agentskit/doc-bridge` |
| CLI binary | `ak-docs` |
| Config file | `doc-bridge.config.ts` |

Standalone CLI — not merged into a framework or OS CLI.

## Design rules

1. **Progressive** — only `schemaVersion` + `corpus.agent` required; everything else defaults sensibly.
2. **Plugin-shaped** — doc sites and monorepo layout are plugins, not hardcoded paths.
3. **No provider in core** — adapter config lives under `intelligence`, never required.
4. **Deterministic output** — paths in config are resolved at build time; no timestamps in index hash input.
5. **One config per repo** — monorepo root config; packages inherit via `routing` / plugin discovery.

## Discovery order

```
doc-bridge.config.ts
doc-bridge.config.mts
doc-bridge.config.js
doc-bridge.config.mjs
doc-bridge.config.json
package.json → "docBridge" field (subset, JSON only)
```

TypeScript/JavaScript configs are static in v1: `defineConfig` imports are supported, but arbitrary imports are not. YAML config files are planned.

Dynamic-loading coverage is evidence-backed. Literal strings, constant aliases,
parenthesized strings, and string concatenations are resolved without executing
repository code. Runtime-dependent `import()` and `require()` expressions remain
`partial`/`not-analyzed`, and the coverage entry records representative
file-and-line evidence for each observed loading site (up to the schema limit).

## TypeScript shape (authoritative)

```ts
import type { DocBridgeConfigV1 } from '@agentskit/doc-bridge/config'

export default {
  schemaVersion: 1,

  /** Optional display name; defaults to package.json name or directory basename */
  project?: { name?: string; root?: string }

  /** Agent-readable knowledge (required) */
  corpus: {
    agent: AgentCorpusConfig
    human?: HumanCorpusConfig | HumanCorpusConfig[]
  }

  /** Where deterministic artifacts are written */
  index?: IndexConfig

  /** Ownership, intents, change routes — monorepo plugin fills this */
  routing?: RoutingConfig

  /** CI gate presets */
  gates?: GatesConfig

  /** CLI + MCP surfaces */
  surfaces?: SurfacesConfig

  /** Optional: chat, memory, retriever — any provider */
  intelligence?: IntelligenceConfig

  /** Optional: Playbook / Registry / remote OKF bundles */
  federation?: FederationConfig

  /** Optional deterministic documentation conformance profiles */
  conformance?: ConformanceConfig

  /** Optional language analyzers and runtime-wiring coverage */
  analysis?: AnalysisConfig

  /** Optional reconciliation scope and orphan-document policy */
  reconciliation?: ReconciliationConfig

  /** Optional retrieval tuning: corpus projection, field weights, BM25 parameters */
  retrieval?: RetrievalConfig

  /** Optional resumable workflow state */
  workflow?: WorkflowConfig

  /** Optional project templates for `ak-docs render`, by template name */
  render?: RenderConfig

  /** Optional report publication privacy; private is the default */
  report?: { privacy?: 'private' | 'anonymized' }
} satisfies DocBridgeConfigV1
```

---

## `corpus.agent` (required)

Agent corpus = dense markdown for coding agents (OKF or for-agents layout).

```ts
type AgentCorpusConfig = {
  /** Root directory, relative to project root */
  root: string                    // e.g. 'docs/for-agents' | '.agent-docs' | 'docs'

  /** Jump table entrypoint (recommended) */
  index?: string                  // default: '{root}/INDEX.md'

  /**
   * Glob patterns under root, relative to root.
   * Default: ['**/*.md'] excluding INDEX conventions
   */
  include?: string[]
  exclude?: string[]              // default: ['**/node_modules/**', '**/.git/**']

  /** OKF frontmatter: require `type` field (soft warn vs hard gate) */
  okf?: {
    requireType?: boolean         // default: false (warn); true in strict preset
    allowedTypes?: string[]       // optional vocabulary enforcement
  }
}
```

### Minimal solo project

```ts
corpus: {
  agent: { root: 'docs' }
}
```

---

## `corpus.human` (optional)

Human wiki / doc site. **Always via plugin** — doc-bridge does not ship a site generator.

```ts
type HumanCorpusConfig = {
  plugin: HumanCorpusPluginId
  /** Plugin-specific options (see plugin docs) */
  options?: Record<string, unknown>
}

type HumanCorpusPluginId =
  | 'plain-markdown'    // docs/**/*.md, frontmatter `package`/`module`/`id`
  | 'fumadocs'          // markdown scan with index routes, (group) slugs, pages allowlists
  | 'docusaurus'        // markdown scan with id/slug frontmatter + static sidebars.js
  | 'mkdocs'            // planned
  | 'vitepress'         // file routes, srcDir/docsDir, optional cleanUrls
  | 'starlight'         // Astro content routes, slug and draft frontmatter
  | 'nextra'            // content-directory routes and contentDirBasePath
  | 'custom'            // path to user plugin module
```

VitePress options accept `docsDir` (also `root` or `srcDir`), `urlPrefix`,
`cleanUrls`, and up to 64 declarative `srcExclude` glob patterns. Doc Bridge
uses `.html` routes unless `cleanUrls: true`, matching
VitePress routing without loading or executing `.vitepress/config.*`. Route
rewrites must therefore be reflected in the configured corpus or URL prefix.

Starlight options accept `contentDir` (also `docsDir` or `root`) and
`urlPrefix`. The adapter reads the standard `slug` and `draft` frontmatter,
follows the default filename sluggifier, excludes underscore-prefixed
partials, and never executes `astro.config.*`. Sites using a custom
`docsLoader({ generateId })` should add explicit `slug` frontmatter so Doc
Bridge can resolve the same public route without executing project code.

Nextra options accept `contentDir` (also `docsDir` or `root`), `urlPrefix`,
and `contentDirBasePath`. The adapter maps `index.md` and `index.mdx` to the
containing route and uses Doc Bridge `package`, `module`, or `id` frontmatter
as the join key. `urlPrefix` takes precedence over `contentDirBasePath`. Point
`contentDir` at either `content` or `src/content`; Doc Bridge never imports or
executes `next.config.*`, `_meta.*`, themes, or other project code. This
adapter targets Nextra's content-directory convention; app-router `page.mdx`
trees are not inferred by this plugin.

### Bridge to agent docs

When `corpus.human` is set, plugins MUST:

1. Emit resolvable `humanDoc` URLs/paths into `AgentHandoff`
2. Support `## Human guide` link validation (gate `human-guide-links`)
3. Map by stable id (`packageId`, `moduleId`, `slug`) — config defines the join key

```ts
corpus: {
  agent: { root: 'docs/for-agents' },
  human: {
    plugin: 'fumadocs',
    options: {
      contentDir: 'apps/web/content/docs',
      urlPrefix: '/docs',           // public path prefix
      metaFile: 'meta.json',
    },
  },
}
```

Multiple human corpora (e.g. marketing + internal):

```ts
human: [
  { plugin: 'fumadocs', options: { contentDir: 'apps/web/content/docs', urlPrefix: '/docs' } },
  { plugin: 'plain-markdown', options: { root: 'docs/internal', urlPrefix: null } },
]
```

---

## `index` (optional)

```ts
type IndexConfig = {
  /** Output path for DocBridgeIndex JSON */
  outFile?: string                // default: '.doc-bridge/index.json'

  /** Optional llms.txt alongside index */
  llmsTxt?: {
    enabled?: boolean             // default: true
    outFile?: string              // default: 'llms.txt' at project root
    preamble?: string             // project-specific intro paragraph
  }

  /** Optional Self-Describe companion artifact */
  capabilities?: {
    enabled?: boolean             // default: true
    outFile?: string              // default: '.doc-bridge/capabilities.json'
  }

  /** Hash algorithm for contentHash field */
  contentHash?: 'sha256-normalized-v1'  // only algo in v1
}
```

---

## `routing` (optional — monorepo-first)

Without `routing`, handoffs are inferred from agent corpus links only. Monorepo plugin populates ownership graph.

```ts
type RoutingConfig = {
  plugin?: 'pnpm-monorepo' | 'npm-workspaces' | 'yarn-workspaces' | 'nx' | 'pattern-files' | 'custom'

  options?: {
    /** Workspace globs; default from package manager */
    packages?: string[]           // e.g. ['packages/*', 'apps/*']

    /** Map package id → metadata (auto-discovered if omitted) */
    ownership?: Record<string, OwnershipEntry>

    /** Curated reading paths for tasks */
    intents?: IntentEntry[]

    /** "I want to change X" → start here */
    changes?: ChangeRouteEntry[]
  }
}

type OwnershipEntry = {
  path: string                    // 'packages/auth'
  group?: string                  // logical layer / team
  layer?: string                  // L0–L4 or custom
  purpose?: string                // one line
  checks?: string[]               // default gates for this owner
  agentDoc?: string               // override path; default inferred
  humanDoc?: string               // filled by human plugin join
}

type IntentEntry = {
  id: string                      // kebab-case
  title: string
  paths: string[]                 // read-before-editing list
}

type ChangeRouteEntry = {
  id: string                      // e.g. 'add-api-endpoint'
  title: string
  startHere: string
  relatedPackages?: string[]
}
```

`routing.plugin: 'nx'` reads `project.json` and package manifests with an `nx`
object. It does not load Nx plugins or execute workspace code. Project roots must
resolve inside the configured project root. Declared `test` targets become Nx test
checks; non-minimal gate presets also include declared `lint` targets. Explicit
`routing.options.ownership` remains authoritative. Targets created only at runtime
by Nx plugins are intentionally not inferred by this read-only adapter.

### Join keys (agent ↔ human ↔ ownership)

| Entity | Primary key | Human plugin maps via |
|--------|-------------|------------------------|
| Package / module | `id` (folder name or `package.json` name) | frontmatter `package` or path convention |
| Screen / feature | `id` in `screens/` or `features/` | MDX slug |
| Flow / recipe | `id` in `flows/` | docs slug |

Plugins document their join convention. The orphan-link gate only runs when the
corresponding gate is enabled (for example, `human-guide-links`); the `minimal`
preset does not enable that gate.

---

## `gates` (optional)

```ts
type GatesConfig = {
  preset?: 'minimal' | 'standard' | 'strict' | 'playbook'
  /** Enabled gate ids; merged with preset */
  include?: GateId[]
  exclude?: GateId[]
  /** Gate-specific options */
  options?: {
    'human-guide-links'?: { strict?: boolean }
    'index-freshness'?: { failOnDrift?: boolean }
    'okf-type'?: { requireType?: boolean }
    'docs-style'?: {
      profile?: 'google-dev-docs' | 'playbook-okf' | 'custom'
      required?: Array<
        | 'title'
        | 'purpose'
        | 'audience'
        | 'task-orientation'
        | 'examples'
        | 'owner-source'
        | 'no-stale-wording'
      >
    }
  }
}

type GateId =
  | 'index-freshness'
  | 'index-reproducible'           // opt-in; in no preset
  | 'human-guide-links'
  | 'link-rot'                    // reserved; emits a diagnostic and is not executed
  | 'okf-type'
  | 'docs-style'
  | 'routing-currency'            // reserved; emits a diagnostic and is not executed
  | 'bootstrap-size'              // reserved; emits a diagnostic and is not executed
  | 'documentation-standard-v1'    // opt-in ecosystem documentation profile
```

| Preset | Gates |
|--------|-------|
| `minimal` | `index-freshness` |
| `standard` | + `human-guide-links` in v1 |
| `strict` | + `okf-type` in v1 |

Implemented gates include `index-freshness`, `human-guide-links`, `okf-type`, `docs-style`, and the opt-in `index-reproducible` and `documentation-standard-v1`. For v1 compatibility, `link-rot`, `routing-currency`, and `bootstrap-size` remain accepted as reserved IDs; including one emits `AK_DOCS_RESERVED_GATE` and does not claim that the gate ran. Unknown IDs are rejected.

### Structural vs style validation

These gates are deterministic lint checks, not editorial grading:

| Gate | Kind | What it proves |
|------|------|----------------|
| `index-freshness` | structural | Generated index matches current docs/config |
| `index-reproducible` | structural | Every indexed path is committed, so a clean checkout rebuilds the same index |
| `human-guide-links` | structural | Local `humanDoc` links resolve through configured human-doc adapters |
| `okf-type` | OKF lint | Agent docs have required `type:` frontmatter when strict/required |
| `docs-style` | style lint | Opt-in deterministic profile checks for title, purpose, audience, examples, owner/source, task orientation, and stale wording |

### Why `index-reproducible` is opt-in

A scan walks what is on disk and has no reason to consult `.gitignore`, so a generated module or
document joins the corpus on a machine that has built and leaves it on one that has not. That is
harmless while the index is regenerated on every run, and a defect the moment the index is
committed for a gate to verify: two checkouts of the same commit produce different artifacts,
`index-freshness` reports staleness that nothing caused, and regenerating cannot fix it because the
next machine disagrees in the other direction.

`index-reproducible` asks Git which of the indexed paths it ignores, and names each one with the
rule that matched (`apps/docs-next/.gitignore:13:lib/ask-context.ts`) so the fix is one lookup
away: add the path to `safety.exclude`. It reports success, not failure, when the index is not
committed or the project is not a Git checkout — there is nothing to reproduce in either case. A
file that is both tracked and matched by an ignore rule is not flagged; being committed is the
point.

It is in no preset, because enabling it for every consumer would fail gates that pass for good
reasons. `ak-docs index` and `ak-docs doctor` report the same finding unconditionally — as a
diagnostic and a `warn` issue — so a repository learns about it before deciding to enforce it.

`docs-style` supports `google-dev-docs`, `playbook-okf`, and `custom` profiles. It is not part of the default path and does not grade prose quality; it checks for explicit structural signals. LLM critique remains planned optional behavior.

---

## `conformance` (optional)

Documentation Standard v1 is an opt-in, deterministic ecosystem profile. It validates
repository evidence without executing configured commands or making network/model calls.

```ts
type ConformanceConfig = {
  documentationStandardV1?: {
    rawSources?: string[]
    contributionPaths?: string[]
    metadata?: Array<{ path: string; contains: string[] }>
    links?: Array<{ url: string; paths: string[] }>
    quickstarts?: Array<{
      id: string
      doc: string
      test: string
      command: string
      testContains: string[]
    }>
    visuals?: string[]
    diagrams?: Array<{ path: string; contains: string[] }>
    ecosystemContract?: {
      manifest: string
      claims: string
      productId: string
    }
    exceptions?: Array<{
      ruleId: DocumentationStandardRuleId
      reason: string
      approvedBy: string
      trackingUrl: string
    }>
  }
}
```

See [Documentation Standard v1](documentation-standard-v1.md) for rule semantics,
report status, commands, and the recorded stable-publication HITL decision.

---

## `reconciliation` (optional)

```ts
type ReconciliationConfig = {
  /** Semantic comparison level; discovery still preserves raw file relations. */
  scope?: 'file' | 'module' | 'area' | 'package'
  /** Observed relation kinds that require documentation declarations. */
  requiredRelationKinds?: string[]
  /** Limit missing-declaration findings to relations between internal project entities. */
  requiredRelationTargets?: 'all' | 'internal'
  /** Emit info findings for documentation with no observed package/module join. */
  includeOrphanedDocuments?: boolean
}
```

Use `scope: 'package'` for monorepos where file imports should be compared as package-level architecture evidence. Omit `requiredRelationKinds` to require all observed kinds; an empty array intentionally disables undocumented-relation findings and must be treated as an explicit exemption.

Use `scope: 'area'` for a single-package repository. At package scope such a repository aggregates
every internal relation into one self-loop, which the comparison skips — a thousand observed
relations and nothing to report. At area scope the same relations become edges between
directories, which a declaration can confirm or fail to. On this repository that is the difference
between zero diagnostics and 177.

Use `requiredRelationTargets: 'internal'` when the repository wants package or module architecture declarations without requiring Markdown to enumerate every external library import. External relations remain in the raw snapshot and report as evidence; they simply do not generate missing-declaration findings.

The reconciliation documentation summary reports package health separately from
relation findings: `fresh` means the package has coverage documentation and no
known discrepancy; `stale` means a declared relation conflicts with observed
architecture; `missing` means no coverage document was found; and `unverified`
means coverage exists but at least one relevant relation or analyzer boundary
could not be verified. Package-level aggregation preserves the relation
endpoints used for this classification, so an undocumented relation cannot be
reported alongside a falsely `fresh` package.

The same summary keeps document inventory separate from coverage claims:
`documentCount` and `documentClassificationCounts` describe every discovered
Markdown document, while `documentedDocumentCount` and
`documentedDocumentClassificationCounts` describe documents that declare a
knowledge relation. The classification keys are analyzer output (for example
`agent`, `human`, `project`, `archive`, and `unclassified`); consumers must not
interpret total repository Markdown coverage as agent-corpus coverage. Use the
`agent` pair when measuring the configured agent documentation surface.

## `report` (optional)

```ts
report?: {
  /** Replace project identity, names, paths, snippets, and finding text in HTML output. */
  privacy?: 'private' | 'anonymized'
}
```

`private` is the default and keeps local evidence useful for debugging. `anonymized` is intended for reports shared outside the repository: it preserves counts, relation kinds, topology, and coverage status while removing project-specific identity and evidence content. The generated HTML and every lazy chunk use the same mode.

## `analysis.areas` (optional)

```ts
areas?: {
  /** Directory levels below a source root that form an area. Default 1. */
  depth?: number
  /** Directories that contain areas rather than being one. */
  roots?: string[]
  /** Glob patterns for directories that hold code without being a unit of architecture. */
  exclude?: string[]
}
```

An **area** is the unit of architecture between a package and a file: a directory that groups
modules. `src` is not an area in any useful sense; `src/query` is. So `roots` names the
directories that hold areas — by default `src`, `lib`, `app`, `source`, `server`, `client`,
`packages`, `apps` — and `depth` says how many levels below such a root an area sits.

`exclude` names what the convention gets wrong in the other direction. A monorepo where every
package keeps `tests/` and `fixtures/` beside `src/` derives an area per directory, and
connectivity then asks for a document about a folder of test data — on one 26-package monorepo,
43 of 81 undocumented areas were exactly that. A candidate matching an exclude pattern is not
derived, and its modules fall to the most specific area that still encloses them, or to none,
which is the honest answer for a folder of fixtures.

Areas are derived, never declared, with one exception that matters: **any path an ownership record
names becomes an area**, whatever the convention says — including a path `exclude` matches, because
a person saying a directory is a unit outranks a pattern saying it is not. A configuration that reads
`path: "src/mcp"` is a human stating that the directory is a unit, and the graph should have an
entity for it. Such an area carries `metadata.ownershipId`, which is what lets an agent document
declaring `id` plus `editRoot` resolve to the thing it owns.

Each module belongs to exactly one area — the most specific one containing it — so containment
stays a tree and an aggregation at area scope has one answer per module. Nested areas keep their
shape: `area:src` holds what sits directly in `src`, with `area:src/query` recorded as its child.

An ownership path that no observed module or document lives under is reported as
`OWNERSHIP_PATH_UNOBSERVED` with status `stale-or-unverified`. A renamed directory is otherwise
invisible: the handoff still resolves, it just points an agent somewhere that no longer holds what
it claims.

## `retrieval` (optional)

```ts
type RetrievalConfig = {
  corpus?: {
    /** Project repository documents and modules into `index.knowledge`. Default: true. */
    enabled?: boolean
  }
  /** Per-field BM25 multipliers. Unknown field names are ignored. */
  weights?: {
    id?: number
    symbols?: number
    title?: number
    path?: number
    tags?: number
    description?: number
    body?: number
  }
  /** BM25 parameters: `k1` term-frequency saturation, `b` length-normalization strength (0–1). */
  params?: { k1?: number; b?: number }
}
```

Search ranks records with field-weighted BM25 plus boosts for exact identity, so an agent that
types an exported symbol, a file path or a package name lands on that thing rather than on
whatever mentions it most. The defaults are:

| Field | Weight | Why |
| --- | --- | --- |
| `id` | 8 | The query names the record |
| `symbols` | 7 | An agent that types an exported name wants the module that defines it |
| `title` | 6 | A heading is what a document is about |
| `path` | 4 | Location is identity for a module |
| `tags` | 3 | Audience and kind |
| `description` | 2 | A summary a human wrote |
| `body` | 1 | A passing mention is the weakest evidence |

Tuning is configuration, not code: the resolved weights, parameters and stopword-lexicon version
are recorded in `index.retrieval`, so a retuned ranking is a different artifact with a different
content hash rather than a silent behaviour change. Re-run `ak-docs index` after changing them,
and re-approve the [retrieval benchmark](../bench/README.md) baseline if you gate on it.

`corpus.enabled: false` keeps the index to the curated agent corpus only. The index is then
smaller and builds faster, at the cost of the retrieval it exists for: an exported-symbol or
file-path query has nothing to resolve against. Turn it off only for a repository whose source is
not the thing agents ask about.

## `render` (optional)

```ts
type RenderConfig = {
  /** Project templates that replace the bundled ones, by name, as paths relative to the project root. */
  templates?: Partial<Record<'llms.txt' | 'area' | 'ownership' | 'change-digest' | 'overlay-review', string>>
}
```

`ak-docs render <name>` renders the canonical artifacts as Markdown from a bundled template; a
path under `templates` replaces that template entirely, without a code change. Templates use
knap syntax and see only the variables Doc Bridge computes — see [Render v1](./render-v1.md) for
each template's variables and `ak-docs render <name> --print-template` for the bundled source.
The `llms.txt` override is also what `ak-docs index` writes.

```json
{
  "render": {
    "templates": { "area": "templates/area.md" }
  }
}
```

## `safety` (optional)

```ts
type RepositorySafetyConfig = {
  /** Additional project-relative glob patterns excluded from repository discovery. */
  exclude?: string[]
  maxFiles?: number
  maxBytes?: number
  maxTimeMs?: number
  maxMemoryMb?: number
  redactSecrets?: boolean
}
```

Discovery always excludes unsafe or generated trees by default, including
`.git`, `node_modules`, `dist`, `build`, `coverage`, `.doc-bridge`, `.next`,
`out`, `.turbo`, `.svelte-kit`, `.mcpb-build`, and `.mcpb-output`, plus common
secret files. `safety.exclude` adds project-specific patterns; it does not
replace the built-in safety boundary. Excluded files remain outside the
snapshot and are represented by analyzer coverage when relevant.

## `analysis` (optional)

```ts
type AnalysisConfig = {
  /** Ordered language/framework analyzer plugins. */
  plugins?: Array<{
    id: string
    enabled?: boolean
    order?: number
    options?: Record<string, unknown>
    reason?: string
  }>
  jsTs?: {
    /** Property-access methods considered runtime wiring entry points. */
    runtimeWiringMethods?: string[]
    /** Additional adapter methods that represent runtime wiring. */
    runtimeWiringAdapters?: Array<{ id: string; methods: string[] }>
    /** Include test/spec modules in runtime-wiring coverage. Default: false. */
    includeTestRuntimeWiring?: boolean
  }
}
```

The JS/TS analyzer defaults to `register`, `use`, `mount`, and `attach`.
Generic APIs such as `bind` and `listen` are intentionally opt-in to avoid
classifying ordinary function binding and server startup as architecture.
When a configured method receives an identifier bound to a
static import, Doc Bridge records a `runtime-wiring` relation with
`metadata.detection: 'runtime-wiring-static'`. Reflective, computed, or
otherwise unbound targets remain explicit `not-analyzed` coverage entries.
Dynamic `import()` and `require()` targets are resolved when their specifier is
a literal, a `const` string binding, a parenthesized static expression, or a
concatenation of other statically known strings. Expressions that depend on
runtime values remain explicit `not-analyzed` coverage entries; Doc Bridge does
not execute repository code to guess their targets.
Test/spec modules are excluded from this signal by default because their
registrations usually construct fixtures rather than production architecture;
set `includeTestRuntimeWiring: true` when test wiring is part of the contract.
This keeps runtime behavior useful without claiming that arbitrary dependency
injection or reflection was resolved. Analyzer plugins must implement the
versioned contract in `docs/spec/analyzer-plugin-v1.md`; malformed or
unsupported output remains explicit `not-analyzed` evidence.

`workflow.stateDir` optionally relocates the content-addressed workflow
artifacts. Runs are resumable and idempotent; pipeline and analyzer versions
are part of the run identity.

## `surfaces` (optional)

```ts
type SurfacesConfig = {
  cli?: {
    /** Published binary name (package.json bin) */
    bin?: string                    // default: 'ak-docs'
    /** Default output: json | text */
    defaultFormat?: 'json' | 'text'
  }

  mcp?: {
    enabled?: boolean               // default: true
    /** Tool ids to expose; default: core set */
    tools?: McpToolId[]
    transport?: 'stdio' | 'http'
    http?: { port?: number; path?: string }
  }
}

type McpToolId =
  | 'handoff.resolve'
  | 'doc.search'                   // deterministic index search
  | 'doc.get'
  | 'gate.status'
  | 'playbook.pattern.get'         // requires federation
```

---

## `intelligence` (optional — any provider)

Absent `intelligence` = Layer 0 only. AgentsKit is the **reference implementation** of this block.

```ts
type IntelligenceConfig = {
  /** When false, ignore all other intelligence fields */
  enabled?: boolean                 // default: false

  adapter?: {
    /** Provider id or path to custom adapter module */
    provider: 'openai' | 'anthropic' | 'ollama' | 'openrouter' | 'custom'
    model?: string
    /** Env var name for API key; never inline secrets in config */
    apiKeyEnv?: string
    baseUrl?: string
    options?: Record<string, unknown>
  }

  chat?: {
    enabled?: boolean
    /** Corpus scopes for retrieval */
    sources?: ('agent' | 'human' | 'federation')[]
    /** Prefer deterministic handoff before RAG when confidence high */
    handoffFirst?: boolean          // default: true
  }

  retriever?: {
    enabled?: boolean
    /** local | remote | bm25-only */
    mode?: 'local' | 'remote' | 'bm25'
    embedModel?: string
    chunkSize?: number
  }

  memory?: {
    enabled?: boolean
    adapters?: MemoryAdapterId[]
    ingestDir?: string              // default: '.agent-memory'
    classify?: boolean              // default: false — opt-in
    promote?: {
      enabled?: boolean
      targets?: ('agent' | 'human' | 'agents-md')[]
      requireApproval?: boolean     // default: true
    }
  }

  /** Reference runtime; custom path for non-AgentsKit engines later */
  runtime?: 'agentskit' | 'custom'
  runtimeModule?: string

  registry?: {
    enabled?: boolean
    agentId?: string
    agentRoot?: string
    runnerModule?: string
    cli?: {
      /** Executable name or absolute path; arguments are passed without a shell. */
      command: string
      args?: string[]
    }
    deterministic?: boolean
    maxInputBytes?: number
    timeoutMs?: number
    maxTokens?: number
    maxResponseBytes?: number
    maxConcurrency?: number
    /** Byte budget of one enrichment context pack. Default 65536 (4096..4000000). */
    maxPackBytes?: number
    /**
     * Which installed agent plays which enrichment role (see Enrichment overlay v1).
     * Default: the configured agent as curator only. The adjudicator must be a
     * different identity from the curator and the reviewer.
     */
    roles?: {
      curator?: EnrichmentRole
      reviewer?: EnrichmentRole
      adjudicator?: EnrichmentRole
    }
  }
}

type EnrichmentRole = {
  enabled?: boolean               // default: true when the role is declared; curator on by default
  agentId?: string                // default: intelligence.registry.agentId
  promptVersion?: string          // default: '1'; part of every proposal id and cache key
}

type MemoryAdapterId =
  | 'playbook-memory'              // .agent-memory/MEMORY.md layout
  | 'cursor-rules'                 // .cursor/rules/*.mdc
  | 'session-export'               // manual JSON/md drop folder
  | 'bootstrap-delta'              // git diff on AGENTS.md
```

Registry agents are advisory and must come from the AgentsKit Registry. The
adapter redacts secrets, validates the proposal contract, bounds execution and
response size, and requires human approval before any change is applied. The
default agent is `ecosystem-doc-bridge-corpus-scanner`; compatible Registry
agents may be selected explicitly.

---

## `federation` (optional — ecosystem)

Not required for any project. Private dogfood enables this profile to validate scale; public consumers do not depend on private repos.

```ts
type FederationConfig = {
  enabled?: boolean
  sources?: FederationSource[]
}

type FederationSource = {
  id: string                        // 'playbook' | 'agentskit' | custom
  llmsTxt?: string                  // URL or local path
  rawBaseUrl?: string               // e.g. 'https://playbook.agentskit.io/raw'
  includeInRetriever?: boolean
  includeInChat?: boolean
}
```

Layer 0 exports a local retriever helper over `DocBridgeIndex`:

```ts
import { createDocBridgeRetriever } from '@agentskit/doc-bridge'

const retriever = createDocBridgeRetriever(index, { property: 'my-project' })
const chunks = retriever.retrieve('auth ownership')
```

Chunk keys are stable: `{property}:{type}:{id}`. Deterministic CLI search and exact handoff resolution stay first; semantic/RAG runtimes can inject this retriever and add federated sources later.

---

## `AgentHandoff` emission rules

When `ak-docs query <target> --agent` runs:

```ts
type AgentHandoffV1 = {
  type: 'agent-handoff'
  schemaVersion: 1
  source: string                    // index.outFile path
  target: { type: TargetType; id: string }
  startHere: string
  readBeforeEditing: string[]
  editRoots: string[]
  checks: string[]
  humanDoc?: string | null
  playbookPatterns?: string[]       // only if federation enabled
  notes: string[]
}

type TargetType =
  | 'package' | 'module' | 'app'
  | 'screen' | 'flow' | 'component'
  | 'intent' | 'change'
  | 'search'                       // --agent on search picks best match
```

**Merge precedence** (later wins):

1. Agent corpus frontmatter (`checks`, `humanDoc`)
2. `routing.ownership[id]`
3. `gates.preset` default checks
4. Global `handoff.defaults` (if set in future minor version)

---

## Example configs

### 1. Solo library (minimal)

```ts
// doc-bridge.config.ts
import { defineConfig } from '@agentskit/doc-bridge'

export default defineConfig({
  schemaVersion: 1,
  corpus: {
    agent: {
      root: 'docs',
      index: 'docs/INDEX.md',
    },
  },
})
```

### 2. pnpm monorepo + ownership

```ts
import { defineConfig } from '@agentskit/doc-bridge'

export default defineConfig({
  schemaVersion: 1,
  corpus: {
    agent: { root: 'docs/for-agents' },
  },
  routing: {
    plugin: 'pnpm-monorepo',
    options: {
      packages: ['packages/*', 'apps/*'],
    },
  },
  gates: { preset: 'standard' },
})
```

### 3. Monorepo + Fumadocs (standard profile)

```ts
import { defineConfig } from '@agentskit/doc-bridge'

export default defineConfig({
  schemaVersion: 1,
  corpus: {
    agent: { root: 'docs/for-agents' },
    human: {
      plugin: 'fumadocs',
      options: {
        contentDir: 'apps/web/content/docs',
        urlPrefix: '/docs',
      },
    },
  },
  routing: { plugin: 'pnpm-monorepo' },
  gates: { preset: 'standard' },
  intelligence: {
    enabled: true,
    adapter: { provider: 'ollama', model: 'llama3', apiKeyEnv: '' },
    chat: { enabled: true, handoffFirst: true },
  },
})
```

### 4. Docusaurus + optional memory

```ts
import { defineConfig } from '@agentskit/doc-bridge'

export default defineConfig({
  schemaVersion: 1,
  corpus: {
    agent: { root: '.agent-docs' },
    human: {
      plugin: 'docusaurus',
      options: {
        docsDir: 'website/docs',
        sidebarsFile: 'website/sidebars.js',
        urlPrefix: '/docs',
      },
    },
  },
  intelligence: {
    enabled: true,
    adapter: { provider: 'openrouter', model: 'openai/gpt-4o-mini', apiKeyEnv: 'OPENROUTER_API_KEY' },
    chat: { enabled: true, sources: ['agent', 'human'] },
    memory: {
      enabled: true,
      adapters: ['cursor-rules', 'playbook-memory'],
      classify: true,
      promote: { enabled: true, requireApproval: true },
    },
  },
})
```

### 5. JSON config (no TypeScript)

```json
{
  "schemaVersion": 1,
  "corpus": {
    "agent": { "root": "docs/for-agents" }
  },
  "routing": {
    "plugin": "npm-workspaces"
  },
  "gates": { "preset": "minimal" }
}
```

---

## CLI mapping

| Command | Config sections used |
|---------|---------------------|
| `ak-docs init` | scaffolds minimal `corpus.agent` + optional plugin prompt |
| `ak-docs index` | `corpus`, `routing`, `index`, plugins |
| `ak-docs query <t> --agent` | `index` + `routing` + handoff merge |
| `ak-docs search <q>` | `index` |
| `ak-docs retrieve <q>` | `index` + `federation` |
| `ak-docs gate run` | `gates` |
| `ak-docs conformance run documentation-standard-v1` | `corpus`, `routing`, `index`, `conformance` |
| `ak-docs mcp` | `surfaces.mcp` |
| `ak-docs chat` | planned; `intelligence.*` |
| `ak-docs memory ingest` | deterministic local ingest; `MemoryCandidate[]` |
| `ak-docs memory classify` | deterministic route classifier |
| `ak-docs memory promote` | draft promotion + safety scan |
| `ak-docs registry topology` | doc-curator topology |
| `ak-docs playbook draft` | draft Playbook feedback payload |

---

## Validation

- Config validated with **Zod** at CLI startup (`ak-docs validate-config`).
- Unknown `schemaVersion` → hard error with migration link.
- Unknown plugin id → hard error listing built-in + `custom` path.
- `intelligence.enabled: true` without `adapter` → warn + chat/memory subcommands disabled.

---

## Versioning

| Version | Scope |
|---------|-------|
| **v1** | This document. Additive fields only in v1.x. |
| **v2** | Breaking changes require `schemaVersion: 2` + codemod. |

---

## See also

- [POSITIONING.md](../POSITIONING.md)
- [AgentHandoff v1](../schemas/agent-handoff-v1.md)
- [DocBridgeIndex v1](../schemas/doc-bridge-index-v1.md)
- [MemoryCandidate v1](../schemas/memory-candidate-v1.md)
