outlint-core 0.1.0

Markdown outline schema validator (core library)
Documentation
# outlint-core

Validate the header structure (outline) of Markdown documents against a
declarative schema.

This is the **pure, IO-free core**: it turns source text into normalized
values and diagnostics. It never touches the filesystem, the network, the
terminal, or the process exit status — that shell is the
[`outlint`](https://crates.io/crates/outlint) CLI crate. Callers supply the
schema text, the Markdown text, and any linked JSON Schema resources they
read themselves.

This is a 0.x release: expect breaking changes to the API, the schema
language, and the diagnostic set before 1.0. The normative specification is
[`spec/outlint-spec.md`](https://github.com/shangaslammi/outlint/blob/main/spec/outlint-spec.md);
where the two disagree, the specification wins.

## What it checks

- **Outline structure** — the section tree built from ATX and Setext
  headings, including skipped heading levels.
- **Section rules** — first-match-wins matchers (exact, glob, regex, `*`),
  `allow: false` denials, and `strict` scopes that reject unmatched
  headings.
- **Cardinality**`required` and `repeat: "min..max"` per rule, per scope.
- **Cross-section logic**`one_of`, `any_of`, `at_most_one`,
  `all_or_none`, `requires`, `conflicts`, and `ordered` constraints over
  rules addressed by id.
- **YAML frontmatter** — presence policy and delegated validation of the
  frontmatter mapping against an inline or linked JSON Schema (draft 2020-12).
  Inline schemas are self-contained and permit fragment-only references;
  linked schemas may span local files. Frontmatter also supports `fm.`
  propositions in constraints: `fm.key` presence and `fm.key=value` typed
  scalar equality under the YAML core schema, with dotted paths into nested
  mappings.

Diagnostics carry stable ids (`missing-section`, `unexpected-section`,
`ordered`, `frontmatter-schema`, …), document source anchors, and structural
schema-node addresses. Resolve a diagnostic's `schema_node` through
`loaded.locations.nodes`, then use the resulting `SourceRange::source` to find
the source text and label in `loaded.sources.documents`.

## Usage

```rust
use outlint_core::{
    load_schema, parse_markdown, DiagnosticTarget, MarkdownOptions, PreparedValidator,
};

fn main() {
    // 1. Load a schema from YAML (or JSON) source text.
    let loaded = load_schema(
        r#"
version: 1
title: "*"
sections:
  - match: "Overview"
    required: true
"#,
    )
    .expect("schema is valid");

    // 2. Compile it once; reuse for any number of documents.
    let validator = PreparedValidator::new(&loaded.schema).expect("schema compiles");

    // 3. Parse Markdown into a section tree.
    let document = parse_markdown(
        "# Widget Redesign\n\n## Usage\n",
        MarkdownOptions {
            strip_inline_markup: loaded.schema.options.strip_inline_markup,
        },
    );

    // 4. Inspect the diagnostics. The target distinguishes a heading that is
    //    really there from a schema matcher that nothing matched.
    for diagnostic in validator.validate(&document) {
        let target = match &diagnostic.target {
            DiagnosticTarget::Header(path) => path.to_string(),
            DiagnosticTarget::MissingHeader { matcher, .. } => format!("expected {matcher}"),
            DiagnosticTarget::Document => "document".to_owned(),
            DiagnosticTarget::Frontmatter { .. } => "frontmatter".to_owned(),
        };
        println!(
            "{}:{} [{}] {} ({target})",
            diagnostic.location.line,
            diagnostic.location.column,
            diagnostic.id.as_str(),
            diagnostic.message,
        );
    }
}
```

Output:

```text
1:1 [missing-section] matched 0 sections, but at least 1 are required (expected Overview)
```

`load_schema` returns `Result<LoadedSchema, InvalidSchema>`; `InvalidSchema`
carries every schema error together with the source text needed to render it:

```rust
use outlint_core::load_schema;

if let Err(invalid) = load_schema("version: 99\n") {
    for error in invalid.errors.iter() {
        let source = &invalid.sources.documents[&error.range.source];
        eprintln!(
            "{} at bytes {}..{} in {}",
            error.kind.as_str(),
            error.range.range.start.0,
            error.range.range.end.0,
            source.label.as_ref().map_or("<schema>", |label| &label.0),
        );
    }
}
```

For schemas whose `frontmatter.schema` points at an external JSON Schema file,
the caller owns the IO boundary. Use `linked_frontmatter_schema_path` to find
the root path, assign that file an absolute logical URI, then walk its local
reference graph with `json_schema_external_references`. The helper returns both
the lexical `physical_uri` to read and the `$id`-aware `logical_uri` under which
to register the contents. Ignore same-document references, deduplicate or
cycle-check reads, record read failures rather than dropping them, and place
every attempted resource in a `LinkedJsonSchemaInput` passed to
`load_schema_with_resources`. Core never retrieves remote references.

## Related

- [`outlint`]https://crates.io/crates/outlint — the command-line tool
  built on this crate.
- [Outlint specification]https://github.com/shangaslammi/outlint/blob/main/spec/outlint-spec.md
  — the schema, validation, diagnostic, and CLI contracts.

## MSRV

Rust 1.86.

## License

Licensed under either of Apache License, Version 2.0 or MIT license at your
option (`MIT OR Apache-2.0`).