Metadata-Version: 2.4
Name: kgl-lang
Version: 0.2.0
Summary: Knowledge Graph Language — parser, linter, OpenCypher query engine (read + write), and a flat-file graph writer
Author-email: Adrian Bennett <adrian@unfoldata.com>
License: MIT
Project-URL: Homepage, https://nakagawabennett.com
Project-URL: Documentation, https://nakagawabennett.com/kgl/
Project-URL: Repository, https://github.com/adriannakagawabennett/kgl
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Text Processing :: Markup
Classifier: Topic :: Database
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: filelock
Dynamic: license-file

# KGL — Knowledge Graph Language

A plain-text file format and Python library for building file-system-based knowledge graphs. KGL makes relationships between pieces of information **first-class citizens** — not an afterthought bolted onto a document format.

```
@Person Alice Nguyen
joined: 2021-03-15
#backend #python

[mentors] → people/diana.kgl#Diana Park
    started: 2023-06
    focus: "backend systems and code review"

→ projects/search-revamp.kgl
~> teams/platform.kgl
```

No database. No graph engine. Just files, folders, and links — readable by humans and navigable by AI.

**Full documentation:** [nakagawabennett.com/kgl](https://nakagawabennett.com/kgl)

---

## Why KGL?

Existing formats optimise for data (JSON, CSV) or documents (Markdown, XML). Neither treats **relationships** as structural. KGL is built around the idea that *how things connect* is as important as *what things are*.

- **File-system as graph** — folders are namespaces, files are nodes, links are edges
- **AI-readable by design** — hard links (`→`) signal "follow this", soft links (`~>`) signal "background context"
- **Human-writable** — plain text, no tooling required to create or read
- **Schema-optional** — works without a schema, richer with one
- **Git-native** — plain text means free version history for your entire knowledge graph
- **Writable, not just readable** — `CREATE`/`MERGE`/`SET`/`DELETE` (v0.2+) make surgical, byte-precise edits to `.kgl` files directly; no export/reimport round-trip

---

## Installation

```bash
pip install kgl-lang
```

Requires Python 3.10+. No dependencies. (The PyPI distribution is
`kgl-lang` — `kgl` was already taken — but the installed command and the
Python package you import are both still just `kgl`.)

```bash
kgl --version
```

---

## Quick start

### 1. Create your first node file

```
# people/alice.kgl

@Person Alice Nguyen
role: Senior Engineer
joined: 2021-03-15
#backend #python

→ projects/search-revamp.kgl
```

### 2. Link to related files

```
# projects/search-revamp.kgl

@Project Search Revamp
status: in-progress
deadline: 2025-08-01

[staffed-by] → people/alice.kgl#Alice Nguyen
    role: lead engineer
    since: 2024-11-01
```

### 3. Explore from the command line

```bash
kgl graph ./          # overview of nodes, edges, and types
kgl lint ./           # check against schema rules
kgl query ./ "MATCH (p:Person) RETURN p.name"
```

---

## File format

### Node declaration

```
@Type Name
@Type|Type Name       # multi-type node — both types apply simultaneously
```

Each `@Type` declaration starts a new node. Everything below it belongs to that node until the next `@Type` or end of file.

### Fields

```
key: value
key: "multi-word value"
```

### Tags

```
#tag #another-tag
```

### Links

| Syntax | Meaning |
|--------|---------|
| `→ file.kgl` | Hard link — strong relationship, follow for full context |
| `→ file.kgl#Node Name` | Hard link to a specific node inside a file |
| `~> file.kgl` | Soft link — background context, optional |
| `[rel] → file.kgl` | Named relationship |
| `[rel\|rel] → file.kgl` | Multi-type relationship |
| `-> file.kgl` | ASCII alternative to `→` |

All link paths are **root-relative** — written from the graph root (the directory containing `_schema.kgl`).

### Relationship properties

```
[employs] → people/alice.kgl#Alice Nguyen
    role: lead engineer
    since: 2024-11-01
```

Indented lines immediately after a link are properties of that relationship, scoped to the edge rather than the node.

### Free text body

```
---
Prose goes here. Not parsed as fields.
Useful for context, notes, and nuance that fields can't capture.
```

### Schema (optional)

Place a `_schema.kgl` file at the graph root to define node types, required fields, and allowed relationships. Schema is **warning-only** — files without required fields are valid KGL but flagged by the linter.

```
# _schema.kgl

@NodeType Person
joined: date (required)
role: text

@RelType staffed-by
from: Project
to: Person
role: text (required)
since: date (required)
```

Field lines are `name: type` or `name: type (required)`. A `@RelType`'s
`from`/`to` list one or more node types, space-separated (`from: Project
Service` allows either type as the source). Properties on the relationship
itself are just more field lines below `from`/`to` — there's no separate
`properties:` block.

---

## CLI reference

### `kgl parse <file>`

Parse a single `.kgl` file and print each node as a structured summary.

```bash
kgl parse people/alice.kgl
kgl parse people/alice.kgl --format json
```

### `kgl load <directory>`

Load all `.kgl` files from a directory and report counts and errors.

```bash
kgl load ./
kgl load ./ --format json -v
```

### `kgl graph <directory>`

Print a summary of the loaded graph: node count, edge count, node types, and edge types.

```bash
kgl graph ./
kgl graph ./ --format json
kgl graph ./ -v          # show unresolved links
```

### `kgl lint <directory>`

Walk all `.kgl` files, load the schema, and report warnings grouped by file. Exits with code `1` if any warnings are found.

```bash
kgl lint ./
kgl lint ./ --format json
kgl lint ./ -v           # include file paths in output
```

### `kgl query <directory> "<cypher>"`

Load the graph and run an OpenCypher query against it. No external database required.

```bash
kgl query ./ "MATCH (p:Person) RETURN p.name"
kgl query ./ "MATCH (p:Person)-[r:mentors]->(q:Person) RETURN p.name, q.name, r.started"
kgl query ./ "MATCH (n:Project) WHERE n.status = \"in-progress\" RETURN n.name" --format json
kgl query ./ "MATCH (n:Person) RETURN n.name" --format csv
```

Output format options: `text` (default, aligned table), `json`, `csv`.

A `CREATE`/`MERGE`/`SET`/`DELETE` query edits `.kgl` files on disk and
requires an explicit `--write` flag to actually run — without it, the CLI
prints what it would do and exits without touching anything:

```bash
kgl query ./ 'CREATE (n:Person {name: "Priya Shah"}) INTO "people/priya.kgl"' --write
kgl query ./ 'MERGE (n:Person {name: "Priya Shah"}) INTO "people/priya.kgl" ON CREATE SET n.role = "Engineer"' --write
kgl query ./ 'MATCH (n:Person) WHERE n.name = "Priya Shah" SET n.role = "Staff Engineer"' --write
kgl query ./ 'MATCH (n:Person) WHERE n.name = "Priya Shah" DETACH DELETE n' --write
```

See "Writing to the graph" below for the full semantics.

### `kgl schema <directory>`

Show all node type and relationship type definitions loaded from `_schema.kgl` files.

```bash
kgl schema ./
kgl schema ./ --format json
```

---

## OpenCypher query support

KGL includes an in-memory OpenCypher query engine. The `.kgl` file system is the store — no external database.

### Supported clauses

| Clause | Example |
|--------|---------|
| `MATCH (n:Type)` | `MATCH (n:Person)` |
| `MATCH (n)-[r:REL]->(m)` | Directed edge with type |
| `MATCH (n)-[r]->(m)` | Directed edge, any type |
| `MATCH (n)-[r]-(m)` | Undirected edge |
| `WHERE n.field = "value"` | Equality filter |
| `WHERE n.field != "value"` | Inequality filter |
| `WHERE n.field CONTAINS "val"` | Substring filter |
| `WHERE n.field STARTS WITH "val"` | Prefix filter |
| `WHERE expr AND expr` | Logical AND |
| `WHERE expr OR expr` | Logical OR |
| `RETURN n.field` | Return field value |
| `ORDER BY n.field ASC\|DESC` | Sort results |
| `LIMIT n` | Truncate results |
| `CREATE (n:Type {...}) INTO "path"` | Create a node (see below) |
| `MERGE (n:Type {...}) INTO "path" ON CREATE SET ... ON MATCH SET ...` | Create-or-update |
| `SET n.field = value` | Update a matched node's field |
| `DELETE n` | Delete a node with no remaining relationships |
| `DETACH DELETE n` | Delete a node and strip every link elsewhere that pointed at it |

**Note:** identifiers (variable names, field names, relationship type
names) are tokenized as `[A-Za-z0-9_]+` — a hyphen inside one (`n.first-seen`,
`-[:staffed-by]->`) reads as a minus sign and breaks the query. Use
`snake_case` for anything that will ever appear in a query, not
`kebab-case` (kebab-case is fine for tags and free-text values, which
never get tokenized this way).

---

## Writing to the graph

`CREATE`, `MERGE`, `SET`, and `DELETE` make surgical edits to the exact
`.kgl` file on disk — they rewrite only the lines that changed (via
`kgl.writer`), so comments, blank lines, and unrelated nodes in the same
file are left byte-for-byte untouched.

- **`CREATE` always needs `INTO "path/to/file.kgl"`** on any brand-new
  node — the engine deliberately never guesses a file location. `INTO`
  attaches to the pattern's variable: `CREATE (n:Person {name: "..."})
  INTO "people/priya.kgl"`.
- **Every node needs a `name` property** — it's how the node is addressed
  afterward (`file.kgl#Name`), so `CREATE`/`MERGE` reject a pattern
  without one.
- **`MERGE` is a single bare node pattern** (v1) — no edges in the same
  `MERGE`. It matches on exact property equality (like Cypher `MERGE`
  everywhere — fuzzy/alias matching across near-duplicate names is up to
  the caller, not something any graph store's `MERGE` does for you), runs
  `ON CREATE SET` if nothing matched or `ON MATCH SET` if something did.
- **Creating a node and linking it to another** is two calls under one
  lock, not one: `CREATE (n:...) INTO "..."` then a second
  `CREATE (a)-[r:rel]->(b)` once both nodes exist. Relationship creation
  in a single `CREATE` pattern is supported when every node in that
  pattern already has a binding (from an earlier `MATCH`/`CREATE`/`MERGE`
  in the same query).
- **`DELETE` refuses to delete a node that still has relationships** —
  that's intentional, it stops a careless delete from silently orphaning
  references. `DETACH DELETE` walks every `.kgl` file in the graph,
  strips any link line pointing at the deleted node, and then deletes it.
  That's a whole-graph scan, not a single-file operation — more work than
  Neo4j's `DETACH DELETE`, which only ever touches the target's own edge
  records, because links here live in the *source* node's text rather
  than a separate edge table.
- **Concurrent writers are serialized** by a `.kgl.lock` file
  (`kgl.writer.write_lock`) at the graph root, held for the duration of
  one `execute_query()` call.

### Python API

```python
from kgl.model import Graph
from kgl.query import QueryEngine

graph = Graph.load("./")
engine = QueryEngine(graph, root="./")   # root required for any write

engine.run('CREATE (n:Person {name: "Priya Shah"}) INTO "people/priya.kgl"')

engine.run('''
    MERGE (n:Person {name: "Priya Shah"}) INTO "people/priya.kgl"
    ON CREATE SET n.role = "Engineer"
    ON MATCH SET n.role = "Staff Engineer"
''')

engine.run('MATCH (n:Person) WHERE n.name = "Priya Shah" DETACH DELETE n')
```

Or call `kgl.writer` directly for the same surgical edits without going
through a Cypher string at all — useful when values might contain
characters the query lexer would choke on (quotes, the string itself):

```python
from kgl import writer

node = writer.create_node(
    root="./", rel_path="people/priya.kgl", types=["Person"],
    name="Priya Shah", fields={"role": "Engineer"},
)
writer.update_node_fields(root="./", node=node, updates={"role": "Staff Engineer"})
writer.append_link(root="./", source_node=node, rel_types=["staffed-by"],
                    target_ref="projects/search-revamp.kgl#Search Revamp",
                    weight="hard", properties={})
```

---

## Python API

### Parsing

```python
from kgl.parser import parse_file, parse_string

result = parse_file("people/alice.kgl")
result = parse_string("@Person Alice\nrole: engineer")

# result.nodes  — list[RawNode]
# result.errors — list[ParseError]  (non-fatal; parsing continues on error)

for node in result.nodes:
    print(node.name, node.types, node.fields, node.tags)
    for link in node.links:
        print(link.link_type, link.rel_types, link.target_file)
```

### Loading a graph

```python
from kgl.model import Graph

graph = Graph.load("./")

# graph.nodes      — dict[id, Node]
# graph.edges      — list[Edge]
# graph.unresolved — list[str]  (link paths that couldn't be resolved)
```

### Querying

```python
from kgl.model import Graph
from kgl.query.engine import QueryEngine

graph = Graph.load("./")
engine = QueryEngine(graph)

results = engine.run("""
    MATCH (p:Person)-[r:mentors]->(q:Person)
    WHERE p.name = "Alice Nguyen"
    RETURN p.name, q.name, r.started
""")

for row in results:
    print(row)
# {"p.name": "Alice Nguyen", "q.name": "Diana Park", "r.started": "2023-06"}
```

`engine.run()` returns `list[dict[str, Any]]`. Each dict maps return variable names to their values.

### Linting

```python
from kgl.model import Graph
from kgl.schema import SchemaLoader
from kgl.linter import Linter

graph = Graph.load("./")
node_types, rel_types = SchemaLoader.load("./")
linter = Linter(node_types, rel_types)
warnings = linter.check(graph)

for w in warnings:
    print(f"{w.code}: {w.node} — {w.message}  ({w.file})")
```

---

## VS Code extension

A companion VS Code extension provides syntax highlighting and LSP-powered diagnostics for `.kgl` files. See the [VS Code documentation](https://nakagawabennett.com/kgl) for installation details.

The `kgl-lsp` command (installed alongside `kgl`) is the Language Server Protocol server used by the extension. It is not intended to be called directly.

---

## Full documentation

Complete documentation — including the graph model, schema reference, AI prompt templates, and VS Code setup — is available at:

**[nakagawabennett.com/kgl](https://nakagawabennett.com/kgl)**

---

## License

MIT © Adrian Bennett
