---
type: Service
id: https://w3id.org/ktl/curator/knowledge/services/ktl-curator-plugin
title: KTL Curator plugin
description: The Obsidian plugin entry point - scans configured bundle roots via metadataCache, computes and caches a trust report per bundle, owns the one-concept-at-a-time review session (curator-id modal, the five verdict verbs, the mtime guard on "corrected"), and exposes the Step 3 commands. With no bundle root configured or detected, and the break-glass "treat vault root as bundle" setting off, the vault has no bundle at all - no report, no queue, nothing written.
resource: src/main.ts
derivedFrom:
- https://w3id.org/ktl/curator/knowledge/services/trust-engine
- https://w3id.org/ktl/curator/knowledge/services/edits-engine
hasPart:
- https://w3id.org/ktl/curator/knowledge/services/curator-view
- https://w3id.org/ktl/curator/knowledge/services/settings-tab
- https://w3id.org/ktl/curator/knowledge/services/in-editor-aids
generated:
  by: process:ktl-librarian
  at: "2026-10-15T23:47:44Z"
status: draft
verified:
- by: process:ktl-librarian
  at: "2026-10-15T23:47:44Z"
---

# Overview

`KtlCuratorPlugin` (`src/main.ts`) is the Obsidian-facing orchestrator. It holds no rule logic of its own: every trust label comes from `trust-engine`, and every write from `edits-engine`. It is responsible for:

- Resolving configured bundle roots (`bundleRoots()`), and re-scanning on `metadataCache` "changed" and `vault` "delete"/"rename" events, debounced 300ms. With no roots configured, the private `implicitRoots()` reads the root `index.md`'s frontmatter and whether `knowledge_bundle/index.md` exists. It then defers the actual decision to `bundle.ts`'s pure `implicitBundleRoots(rootHasHeader, visibleIndexExists, treatVaultRootAsBundle)`. An absent root is explained only after checking Obsidian's live index, so a dot-folder root another plugin exposes is read like any other.
- **No bundle, by design.** `implicitBundleRoots` returns an empty list, not the whole vault, when all three of these hold:
  - the root `index.md` has no LOKF header;
  - there is no top-level `knowledge_bundle/index.md`;
  - the break-glass `treatVaultRootAsBundle` setting is off.

  So an ordinary notes vault is left alone rather than misread as one giant bundle. `hasNoBundle()` reports that state. `noBundleNotice()` explains it wherever a command or the status bar would otherwise act on a bundle that isn't there, and points at KTL Registrar's header command, the `ktl-sidecar` skill, or the break-glass setting.

- Reading each candidate concept's frontmatter and headings through `metadataCache.getFileCache` (a scan that reads no note content), building one `TrustRecord` per concept, and computing the ranked queue and health counts per bundle root.
- Prompting once, through a modal, for the curator id (`ensureCuratorId`): a slug validated against `^[a-z0-9][a-z0-9-]*$`, and refused if it contains `@`.
- The five verdict methods (`confirm`, `sendBack`, `corrected`, `retire`, `later`), each writing through `app.fileManager.processFrontMatter` and the body and `log.md` through the atomic `app.vault.process`, and upserting the day's `**Curation**` line in the bundle's `log.md`.
- The mtime guard on "Wrong - I corrected it". `recordCardOpened(file)` notes the file's `mtime` when its review card opens. `hasReviewedCard(file)` keeps the verb disabled until the card has been open at least once this session. `hasChangedSinceCardOpened(file)` is true when the `mtime` has changed since. When it has not, the card asks "The note hasn't changed - did you mean Confirm?" before it records the correction.
- Six commands: Open curator panel, Create curation policy, Record something missing, Review the next concept in the queue, Review the active note, and Look up a LOKF field. A ribbon icon and a status-bar item also open the panel, and the ribbon carries the `ktl-trust-ladder-check` icon that `curator-view.ts` draws.
- The type vocabulary (added 2026-09-14): `CuratorSettings.knownTypes`, defaulting to the manifest's `KNOWN_LOKF_TYPES`, is handed to every `buildTrustRecord` call and to `policyOverridesFor`'s `parseCurationPolicyTable`. A vocabulary-fit entry's message now reads "not one of the known vocabulary classes".
- **The settings themselves are not this module's.** `src/settings-model.ts` (import-free, added 2026-09-14) holds the `CuratorSettings` shape, `DEFAULT_SETTINGS` and `mergeSavedSettings`. `mergeSavedSettings` puts saved values over the defaults. It refreshes a `knownTypes` list still at the pre-schema `HARDCODED_LOKF_TYPES` to the manifest's, and preserves an edited one, the same rule as `ktl-registrar`'s. `loadSettings` is one call and an assignment. `main.ts` re-exports the type and the defaults. The rule moved so the smoke test could cover it. That is the boundary this plugin draws generally: logic worth testing leaves `main.ts` rather than being tested through it.
