Metadata-Version: 2.4
Name: forktex-knowledge
Version: 0.1.0
Summary: Keep documentation as JSON: typed records, rendered to markdown, retrievable within a budget.
License-Expression: AGPL-3.0-or-later OR LicenseRef-ForkTex-Commercial
License-File: LICENSE
License-File: NOTICE
Author: FORKTEX
Author-email: info@forktex.com
Requires-Python: >=3.14,<4.0
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Documentation
Classifier: Topic :: Software Development :: Documentation
Classifier: Topic :: Text Processing :: Markup :: Markdown
Classifier: Typing :: Typed
Requires-Dist: pydantic (>=2.11)
Project-URL: Bug Tracker, https://github.com/forktex/forktex-knowledge/issues
Project-URL: Changelog, https://github.com/forktex/forktex-knowledge/blob/master/CHANGELOG.md
Project-URL: Documentation, https://github.com/forktex/forktex-knowledge/tree/master/docs
Project-URL: Homepage, https://forktex.com
Project-URL: Repository, https://github.com/forktex/forktex-knowledge
Description-Content-Type: text/markdown

# forktex-knowledge

Keep documentation as JSON: typed records, rendered to markdown, retrievable
within a token budget.

Markdown is a fine thing to *read* and a poor thing to *maintain* once there is
structure to keep consistent — a rule needs a severity and something that proves
it, a document needs typed links to others, and prose cannot be checked. So the
source of truth is JSON validated by Pydantic models, and the markdown is
generated from it.

```bash
pip install forktex-knowledge
```

## Three moves

**1. JSON is the source.** One record per file. Each names the model it loads
into, so a tree nests however suits the material and nothing infers a type from
a directory name:

```json
{
  "$model": "Document",
  "id": "practice.python",
  "title": "Writing Python here",
  "updated": "2026-08-24",
  "tags": ["python"],
  "summary": "...",
  "rules": [
    {
      "id": "python:target-314",
      "title": "Target 3.14",
      "statement": "...",
      "severity": "blocker",
      "enforcement": "make lint fails on the wrong target"
    }
  ]
}
```

`id` is handcrafted and never derived. Renaming a file or moving it between
folders breaks nothing, because everything cites the id and the filesystem only
groups.

**2. Load it through a repository**, never by touching files:

```python
from pathlib import Path
from forktex_knowledge import FileRepository

repo = FileRepository(Path("sources"))
doc = repo.read("engineering/python.json").document
docs = [loaded.document for loaded in repo.read_all() if loaded.ok]
```

**3. Export with `to_markdown()`**, inherited from `Renderable`, so a whole
document and any piece of one render the same way:

```python
doc.to_markdown()             # the page
doc.rules[0].to_markdown()    # just that rule
```

## The CLI, over a `Site`

Four verbs are the same for any corpus, so they ship here. What varies is the
`Site`. Two fields are required — `sources` and `output` — and everything else
has a default you can leave alone:

| Field | Default | What it is |
|---|---|---|
| `banner` | `""` | Comment block stamped on every page. Usually a licence header, which is why the library supplies none. |
| `generated_note` | a DO-NOT-EDIT line | Marks a page as generated. Must contain `{source}`. |
| `build_hint` | `"rebuild"` | How to rerun the build, quoted in staleness findings — `make build`, `just build`, a script. |
| `link_pattern` | `[[id]]` | The citation syntax. Defaulted because `Document` renders it. |
| `index_title` / `index_name` / `index_intro` | `Knowledge` / `README.md` / a sentence | The generated index page. |
| `ungrouped` | `"general"` | Index section for a record with no folder above it. |

```python
# main.py
from pathlib import Path
from forktex_knowledge import Site, run

ROOT = Path(__file__).resolve().parent

raise SystemExit(run(Site(
    sources=ROOT / "sources",
    output=ROOT / "markdown",
    banner="<!-- generated; do not edit -->",
)))
```

```bash
python main.py check --strict --drift   # validate, and fail on a stale page
python main.py build                    # render everything
python main.py ask "how do I add a route?"
python main.py ask --rules --tag python
python main.py new my-doc --updated 2026-08-24
```

`check --drift` is the load-bearing one: it regenerates every page and byte-diffs
it, so a hand-edited page fails rather than surviving as a second author of the
same fact.

## Retrieval

Two questions, and only one needs ranking:

```python
from forktex_knowledge import ground, rules

rules(docs)                                  # every rule, worst severity first
ground(docs, "add a route", budget_tokens=4000)
```

`rules` has no ranking on purpose — the whole normative set is usually a few
thousand tokens, and dropping a blocker to save a few hundred is a bad trade.
`ground` ranks (BM25, length-normalised so the longest document does not win on
volume), injects briefs rather than whole pages, and **names what it dropped** —
a silently cut context reads exactly like a complete one.

## Storage is a port

The core knows nothing about files. `Repository` is four methods over an opaque
key, so a database or an API adapter needs no change to the models:

```python
class Repository(Protocol):
    def keys(self) -> list[str]: ...
    def read(self, key: str) -> Loaded: ...
    def write(self, key: str, document: Document) -> None: ...
    def delete(self, key: str) -> None: ...
```

`FileRepository` is the one implementation shipped. An in-memory one is about ten
lines.

## Document types

`Document` is doctrine: rules, worked examples, a principle. Two more ship because
they are shaped differently, and forcing them into `Document` loses what makes
them worth keeping:

- **`Research`** — findings true on a date. `as_of` is required and renders as a
  warning above the first figure, because prices and measurements decay.
- **`Decision`** — an ADR. `alternatives` is the field that earns it its own type:
  a decision without its rejected options gets re-litigated.

Register your own with `@known`:

```python
from forktex_knowledge import Document, known

@known
class Runbook(Document):
    oncall: str
```

## Licence

Dual-licensed: AGPL-3.0-or-later, or a commercial licence from FORKTEX S.R.L.
See [LICENSE](LICENSE) and [NOTICE](NOTICE). For proprietary or SaaS use where
the AGPL's obligations cannot be met, contact info@forktex.com.

