Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions .changeset/frontmatter-schema-validation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
"@inkeep/open-knowledge-core": minor
"@inkeep/open-knowledge-server": minor
"@inkeep/open-knowledge-app": minor
"@inkeep/open-knowledge": minor
---

Validate document frontmatter against JSON Schema files with the new `frontmatter` content-rules plugin.

**Schema mappings in `config.yml`.** Enable the plugin in Settings → Plugins and map schema files to doc sets with globs — `contentRules.frontmatter.schemas` entries pair an `appliesTo` glob (or list; a leading `!` excludes) with a `file` path. Schema files are ordinary draft-07 JSON Schema, readable by any external tool; `.ok/schemas/` is only the default home.

**Warnings everywhere, blocking nowhere.** Violations (`frontmatter/required`, `frontmatter/enum`, `frontmatter/type`, …) surface in the editor gutter, the Problems panel, `ok lint`, the MCP `lint` tool, and as advisories on agent writes — a violation never blocks or changes a write. Broken schema files and invalid globs report on the config channel (Settings panel, audit `warnings`), never as document problems.

**No-code editing.** The plugin's settings panel manages the glob → file mappings, and a per-field editor edits each schema (type, required, allowed values, pattern) without touching raw JSON — keywords the editor doesn't model are preserved verbatim, and editing a mapping whose file doesn't exist yet creates it.

**Schema-aware property panel.** Fields constrained by an `enum` render as a select, and array fields with `items.enum` render as toggleable value chips, so valid values are picked instead of typed.

**Agents learn the contract at read time.** `exec` listings and reads advertise which schema files govern a doc or folder (`schemas: …`), resolved server-side.

**Discovery UX.** The glob editor names its grammar with an example pattern, an explicit Edit button opens each schema on the WYSIWYG Fields view, and a glob that matches zero docs warns on the config channel alongside the invalid/suspicious-glob warnings. The Problems panel names the plugins doing the checking and, when none are enabled, says so and links to Settings → Plugins instead of a misleading "no problems".
126 changes: 126 additions & 0 deletions docs/content/advanced/content-rules/frontmatter.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
---
title: Frontmatter schemas
icon: LuBraces
description: Validate document frontmatter against standard JSON Schema files — scoped to folders with globs, with schema-driven selects in the property panel and a no-code schema editor.
---

Frontmatter schemas is a content-rules linter that validates each document's YAML frontmatter against [JSON Schema](https://json-schema.org/) (draft-07) files. The schema files are plain, portable JSON Schema — nothing OK-specific in them — so the same files work with any external tool that understands the standard.

This page covers the schema mappings, the schema files, the no-code editors, and what agents see. For the linter-agnostic parts — where problems appear, the Problems panel, `ok lint`, and agent advisories — see the [content rules overview](/docs/advanced/content-rules/overview).

## Schema mappings

Which docs validate against which schema lives in the project's `config.yml` (shared via git):

```yaml
contentRules:
frontmatter:
enabled: true
schemas:
- appliesTo: "docs/**"
file: ".ok/schemas/doc.schema.json"
- appliesTo:
- "specs/**"
- "!**/index"
file: ".ok/schemas/spec.schema.json"
```

- **`file`** is a project-root-relative path to a JSON Schema file. Schemas can live anywhere in the project; `.ok/schemas/` is the default home the settings editor creates them in.
- **`appliesTo`** is a glob or list of globs matched against content-relative, extension-less doc paths (`docs/guide`, not `docs/guide.md`). A leading `!` excludes, and a doc is in scope when it matches a positive glob and no negated one. A list with no positive glob gains an implicit `**`, so `["!drafts/**"]` means every doc except drafts; an absent or empty `appliesTo` means **every doc**.
- **Bad globs surface on the config channel, never per-doc.** One that can't compile matches nothing and is reported as a configuration problem — it never silently widens a mapping. One that compiles but can't match a normalized doc path gets an advisory instead: a trailing slash (`docs/`), a leading slash (`/docs/**`), or a file extension (`docs/**/*.md`), none of which appear in the paths being matched.
- **Every matching mapping validates.** Two schemas both in scope for a doc act as a conjunction — the frontmatter must satisfy both. One file mapped from several entries still validates once.
- Each mapping has its own **`enabled`** toggle, so you can park a mapping without deleting it. Absent means enabled — set `enabled: false` to park one.

## Schema files

Standard draft-07 only: an absent `$schema` is treated as draft-07, and a file declaring another dialect is skipped with a configuration problem. A schema file that is missing, malformed, or refused by the validator is also a **configuration problem** — it shows at the top of the Problems panel's project scope, in `ok lint` warnings, and on the MCP `lint` tool, never as a per-doc diagnostic.

A doc with no frontmatter block (or unparsable YAML) validates as an empty object — so `required` fields still report on brand-new docs. The reverse also works: a schema with `"maxProperties": 0` enforces that docs carry no frontmatter at all.

## What a finding looks like

Take the schema above governing `docs/**`, and a doc that misses one required field and misspells an enum value:

```markdown
---
status: shipped
---

# Guide
```

`ok lint` (and the Problems panel) reports both violations, each carrying source `frontmatter` and the violated JSON Schema keyword as its code:

```text
docs/guide.md
1:1 warning Frontmatter property "owner" is required frontmatter/required
2:1 warning Frontmatter property "status" must be one of: draft, review, published (got "shipped") frontmatter/enum

2 problems (0 errors, 2 warnings) across 1 file.
```

As a diagnostic in `ok lint --json` or the MCP `lint` tool's structured content (0-based, end-exclusive ranges — the text report is 1-based):

```json
{
"range": { "start": { "line": 1, "character": 0 }, "end": { "line": 1, "character": 15 } },
"severity": "warning",
"source": "frontmatter",
"code": "enum",
"message": "Frontmatter property \"status\" must be one of: draft, review, published (got \"shipped\")"
}
```

**Anchoring.** A violation on a key underlines that key's line (the `enum` finding above sits on line 2, where `status:` is written); a missing `required` field anchors to the opening `---` fence; a doc with no fence anchors to the top. And a broken schema file is never a per-doc finding — it rides the configuration channel instead, as a `!` line in `ok lint`:

```text
! frontmatter schema .ok/schemas/missing.schema.json: cannot read (ENOENT: no such file or directory, …)
```

Frontmatter findings are always **warnings** — there's no severity promotion yet, so `ok lint --errors-only` won't gate on them — and none carry `fixes` (choosing the right value is a human decision), so `ok lint --fix` and the MCP `fix: true` leave them in place, still reported.

## The settings editor

**Settings ▸ Plugins ▸ Frontmatter schemas** (once the plugin is enabled) lists the project's schema files: search, an **Only modified** filter, and **New schema** to scaffold a fresh file in `.ok/schemas/`. Each entry shows a plain-language summary of its scope ("everything under docs/ — except any doc named index"), an editor for the `appliesTo` globs, an enable toggle, and delete for schema files OK manages.

## The schema editor

Opening a schema file in the editor (any `*.schema.json`, or a `.json` under `.ok/schemas/`) gets a **Source / Fields** toggle, like the markdownlint config's rule browser. **Source** shows the raw JSON; **Fields** is a no-code editor: each frontmatter field as a row with its type (string, number, boolean, enum, array, object), required toggle, description, allowed values, and pattern — recursing into object fields and array-of-object elements for nested frontmatter.

Edits are **non-destructive**: the editor merges only the field you changed, and keywords it doesn't model (`allOf`, `if`/`then`, `x-` extensions, …) survive verbatim — with a note when the schema carries root-level advanced rules the Fields view doesn't show. Your hand-written schema stays yours.

## The property panel

The document panel's property editor reads the same schemas through the same `appliesTo` matching as the linter, so the two can never disagree. A field constrained by `enum` renders as a select instead of free text; an array field with `items.enum` renders as a multi-select. When several schemas govern a doc, the offered values are the intersection — and if no value could satisfy them all, the field falls back to free text while the linter reports the conflict.

## AI agents

Beyond the shared `lint` tool and write advisories (see the [overview](/docs/advanced/content-rules/overview#ai-agents)), reads advertise the contract up front: `exec` listings and reads carry a `schemas:` entry naming the schema files that govern each doc — and each folder, so an agent learns the expected shape **before** writing the first doc there. An `exec("ls docs/")` comes back with:

```text
- **docs/** (directory) — schemas: .ok/schemas/doc.schema.json — 2 md files — most recent: guide.md (docs/guide.md, 2026-07-21)
- **docs/guide.md** (docs/guide.md) — status: shipped — schemas: .ok/schemas/doc.schema.json — backlinks: 0
```

The server resolves the globs; agents never evaluate `appliesTo` themselves. And when a write violates a governing schema, the write still lands and the response carries the violations as `lint-violation` advisories (1-based `line`/`column`), so the agent can correct its own frontmatter in a follow-up edit:

```json
"warnings": [
{
"kind": "lint-violation",
"source": "frontmatter",
"code": "required",
"message": "Frontmatter property \"owner\" is required",
"severity": "warning",
"line": 1,
"column": 1
}
]
```

## See also

- [Content rules overview](/docs/advanced/content-rules/overview): where problems show up, the Problems panel, `ok lint`, and agents
- [markdownlint](/docs/advanced/content-rules/markdownlint): the sibling linter for markdown style
- [Configuration reference](/docs/reference/configuration): `config.yml` and what's shared via git
- [JSON Schema draft-07](https://json-schema.org/specification-links#draft-7): the schema dialect
39 changes: 35 additions & 4 deletions docs/content/advanced/content-rules/markdownlint.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ icon: LuListChecks
description: The markdownlint linter for content rules — VS Code-parity defaults, your project's native `.markdownlint.*` config, and a no-code browser for all 53 rules.
---

markdownlint is the first content-rules linter — the standard [markdownlint](https://github.com/DavidAnson/markdownlint) engine, the same one behind `markdownlint-cli2` and the popular editor extensions. This page covers its defaults, how it reads your project's config, and the built-in rule browser. For the linter-agnostic parts — where problems appear, the Problems panel, `ok lint`, and agent advisories — see the [content rules overview](/docs/advanced/content-rules/overview).
markdownlint is the content-rules linter for markdown style — the standard [markdownlint](https://github.com/DavidAnson/markdownlint) engine, the same one behind `markdownlint-cli2` and the popular editor extensions.

This page covers its defaults, how it reads your project's config, and the built-in rule browser. For the linter-agnostic parts — where problems appear, the Problems panel, `ok lint`, and agent advisories — see the [content rules overview](/docs/advanced/content-rules/overview). For validating frontmatter rather than markdown style, see [Frontmatter schemas](/docs/advanced/content-rules/frontmatter).

## Default rules

Expand All @@ -29,17 +31,46 @@ The rules live in your project's own native markdownlint file — `.markdownlint

**Settings ▸ Plugins ▸ markdownlint** (markdownlint appears there once the plugin is enabled) lists the full catalog — all 53 rules, generated from the installed engine's own config schema, so it always matches what actually runs. Search by id, alias, or name; browse by category (Headings, Lists, Whitespace, Code, Links & images, Style); or check **Only modified** to see just the rules your config changes. Expanding a rule reveals a link to its upstream documentation and typed editors for each of its options.

The same browser also opens **directly on the config file**. Open your project's root `.markdownlint.json` or `.markdownlint.jsonc` in the editor (reveal hidden files to find it) and use the **Source / Rules** toggle: **Source** — the default — shows the raw file read-only, and **Rules** is the same no-code browser. Because rule edits target the project's governing root config, **Rules** is enabled only for that root file; open a nested or otherwise non-governing `.markdownlint.*` and **Rules** is disabled with a tooltip while **Source** still shows the file. Your Source/Rules choice is remembered separately from the document editor's own Visual/Markdown mode.
The same browser also opens **directly on the config file**. Open your project's root `.markdownlint.json` or `.markdownlint.jsonc` in the editor (reveal hidden files to find it) and use the **Source / Rules** toggle: **Source**, the default, shows the raw file read-only; **Rules** is the same no-code browser. Your choice is remembered separately from the document editor's own Visual/Markdown mode.

That toggle appears for JSON and JSONC configs only — a `.markdownlint.yaml`, `.markdownlint.yml`, or `.markdownlintrc` opens in the read-only file preview instead. And because rule edits target the project's governing root config, **Rules** is enabled only for that file; open a nested JSON config and **Rules** is disabled with a tooltip while **Source** still shows it.

Every edit writes back to your native `.markdownlint.*` file, and a rule you configured under an alias stays under that alias. JSON and JSONC files get minimal text edits, so comments and formatting survive; a YAML config is re-serialized, which drops its comments. Severity strings are shown as a read-only badge. If the project has no file yet, the first change creates a `.markdownlint.json` seeded with the defaults — from then on that file is the whole story for OK and every other markdownlint tool.

## What a finding looks like

A markdownlint finding carries the rule id as its `code` under source `markdownlint` — rendered as `markdownlint/MD010` in reports — with the upstream engine's message. In `ok lint` text output:

```text
docs/guide.md
7:5 warning Hard tabs: Column: 5 markdownlint/MD010
```

And as a diagnostic in `ok lint --json` (or the MCP `lint` tool's structured content), where an auto-fixable rule also carries its `fixes` — the exact text edits `--fix` or the editor's **Fix** action would apply:

```json
{
"range": { "start": { "line": 6, "character": 4 }, "end": { "line": 6, "character": 5 } },
"severity": "warning",
"source": "markdownlint",
"code": "MD010",
"message": "Hard tabs: Column: 5",
"fixes": [
{ "range": { "start": { "line": 6, "character": 4 }, "end": { "line": 6, "character": 5 } }, "newText": " " }
]
}
```

Every edit writes back to your native `.markdownlint.*` file, preserving what you wrote: JSON and JSONC files get minimal text edits (comments and formatting survive), and a rule you configured under an alias stays under that alias. Severity strings are shown as a read-only badge. If the project has no file yet, the first change creates a `.markdownlint.json` seeded with the defaults — from then on that file is the whole story for OK and every other markdownlint tool.
JSON ranges are 0-based and end-exclusive; the text report is 1-based. A rule that can't be auto-fixed simply has no `fixes` key. The shared report shapes — file grouping, counts, configuration warnings — are on the [overview](/docs/advanced/content-rules/overview#what-it-returns).

## Auto-fix and severities

- **Auto-fix covers only some rules.** markdownlint can mechanically fix problems like hard tabs, trailing whitespace, list-marker style, and blank-line spacing — but not ones that need a human decision, such as inline HTML (`MD033`) or a missing top-level heading (`MD041`). In source mode the **Fix** action appears only on a problem markdownlint can fix; `ok lint --fix` applies every fixable problem across the project and leaves the rest untouched, still reported. There's no "fix everything" — the rest always need a manual edit.
- **Auto-fix covers only some rules.** markdownlint can mechanically fix problems like hard tabs, trailing whitespace, list-marker style, and blank-line spacing — but not ones that need a human decision, such as inline HTML (`MD033`) or a missing top-level heading (`MD041`). In source mode the **Fix** action appears only on a problem markdownlint can fix; the Problems panel offers **Fix all** to apply every fixable problem in the current scope at once; and `ok lint --fix` does the same across the whole project. Whatever's left is still reported, untouched — no affordance resolves the rules that need a human decision.
- **Severities.** A rule reports as a warning unless your config promotes it to `"error"`. Set `"error"` as the rule's value or via a `severity` key, then gate CI on just those rules with `ok lint --errors-only`.

## See also

- [Content rules overview](/docs/advanced/content-rules/overview): where problems show up, the Problems panel, `ok lint`, and agents
- [Frontmatter schemas](/docs/advanced/content-rules/frontmatter): the sibling linter for frontmatter validation
- [Configuration reference](/docs/reference/configuration): `config.yml` and what's shared via git
- [markdownlint rules](https://github.com/DavidAnson/markdownlint/blob/main/doc/Rules.md): the upstream rule reference
2 changes: 1 addition & 1 deletion docs/content/advanced/content-rules/meta.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"title": "Content rules",
"icon": "LuListChecks",
"pages": ["overview", "markdownlint"]
"pages": ["overview", "markdownlint", "frontmatter"]
}
Loading