# Skelantic

**A declarative file-tree processing and governance engine for Python.**

[![PyPI version](https://badge.fury.io/py/skelantic.svg)](https://badge.fury.io/py/skelantic)
[![CI](https://github.com/ghackenberg/skelantic/actions/workflows/ci.yml/badge.svg)](https://github.com/ghackenberg/skelantic/actions)

Skelantic allows you to enforce repository structures, parse unstructured text files (like Markdown) into strictly typed Pydantic models using "Skeletal Templates", and execute Python-based processor functions over your file tree to ensure referential integrity and consistency.

It is particularly useful for:
* **AI Coding Agents:** Providing strict guardrails and architecture enforcement for autonomous agents.
* **Documentation Governance:** Ensuring that documentation folders conform to a specific structure and that cross-references are valid.
* **Declarative Repositories:** Managing complex, heavily structured projects (like Data Lakes, Content Management, or Architecture Decision Records) where files must adhere to strict templates.

---

## 🚀 Quick Start

### 1. Installation

Install Skelantic via pip:

```bash
pip install skelantic
```

### 2. Define the Repository Structure

Skelantic operates on a "Strict-by-Default" (Default-Deny) philosophy. By defining cascading `.skelantic/config.yaml` files throughout your repository, you explicitly allow files and directories. Anything not explicitly permitted is flagged as an error.

Create a `.skelantic/config.yaml` at the root of your project:

```yaml
description: "Global configuration"
files:
  "README.md":
    description: "Main Readme"
directories:
  "docs":
    description: "Documentation directory"
    directories:
      "issues":
        description: "Task tracking"
        files:
          "{slug}.md":
            description: "An issue file"
            template: ".skelantic/templates/issue.md"
```

### 3. Write a Skeletal Template

Skeletal Templates verify the contents of text files and extract data using a custom, readable AST. For a full list of available variables, block structures, and multiline extractions, see the [Skeletal Templates Syntax Guide](docs/templates.md).

Create `.skelantic/templates/issue.md`:

```markdown
# Issue: {{title:str}}
Status: {{status:enum[OPEN,IN_PROGRESS,DONE]}}

## Description
[[block:description]]
{{text}}
[[/block]]
```

### 4. Generate Pydantic Models

Before writing your custom logic, use the Skelantic CLI to automatically generate strongly-typed Pydantic models from your Skeletal Templates:

```bash
skelantic generate -o "my_project/models"
```

This will create Python files (e.g., `my_project/models/issue.py`) containing the strictly-typed representation of your template:

```python
# Auto-generated by Skelantic
from pydantic import BaseModel
from typing import Optional

class IssueTemplate(BaseModel):
    title: Optional[str] = None
    status: Optional[str] = None
    description: Optional[str] = None
```

### 5. Write Processors

Processors are written in pure Python and execute validation logic. Use the `@processor` decorator to bind them to specific files matching a glob pattern. 

Skelantic automatically instantiates your generated Pydantic models and injects them, along with context and trace loggers, directly into your function. For a deep dive into the injected parameters, the different phases, and cross-file relational checks, see [Writing Processors & Understanding Context](docs/processors.md) and the [File System Nodes API](docs/nodes.md).

```python
from typing import List, Callable, cast
from skelantic.commons.decorators import processor
from skelantic.commons.context import LinterContext
from skelantic.commons.nodes import FileNode
from my_project.models.issue import IssueTemplate # Your generated model!

@processor(match="docs/issues/*.md", phase=2)
def validate_issue_status(node: FileNode, ctx: LinterContext, tracer: Callable[[str], None] = lambda x: None) -> List[str]:
    tracer(f"Validating issue: {node.rel_path.name}")

    # 1. Fetch the raw data extracted from the template
    full_path = node.rel_path.as_posix()
    data = ctx.extracted_data.get(full_path, {})
    if not data:
        return []

    # 2. Cast the raw data into your strictly-typed Pydantic model
    model = IssueTemplate.model_validate(data)

    # 3. Perform type-safe business logic validations
    if model.status == "DONE" and not "resolution" in (model.description or "").lower():
        return ["DONE issues must have a resolution in their description."]

    # You can also use ctx.extracted_data to iterate through other files in the workspace
    # to perform relational checks (e.g. checking if linked files exist).

    return []
```

### 6. Run the Engine

Use the Skelantic CLI to execute the workflow engine against your repository. 

```bash
# Run the engine to validate the repository
# -p specifies the package containing your @processor functions
# -m specifies the package containing your generated Pydantic models
skelantic run -p "my_project.processors" -m "my_project.models"
```
## ⚙️ Advanced Features

* **Dependency Injection:** The engine automatically injects arguments into your processor functions based on their signature (`node`, `ctx`, `tracer`, and dynamic path variables like `slug`).
* **Strictness Flags:** In your `config.yaml`, use `optional: true` for warnings instead of errors, `silent: true` to suppress warnings, or `authorize: false` to apply patterns without implicitly authorizing existence.
* **Pydantic Integration:** Skelantic natively supports Pydantic. If you map a Pydantic model to a template, the engine will automatically instantiate and inject the strongly-typed model into your processors.

## 🤝 Contributing

If you want to contribute to the development of the Skelantic engine itself, please see our [CONTRIBUTING.md](CONTRIBUTING.md) guide.

## 📚 More Documentation

* [Writing Processors & Understanding Context](docs/processors.md)
* [File System Nodes API (FSNode)](docs/nodes.md)
* [Skeletal Templates Syntax Guide](docs/templates.md)
