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
8 changes: 7 additions & 1 deletion docs/book/src/reference/mcp-tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,14 +133,20 @@ Graph projection and algorithms.

| Parameter | Required | Description |
|-----------|----------|-------------|
| `algorithm` | No | `stats`, `in_degree`, `pagerank`, or `ppr` (default: `stats`) |
| `algorithm` | No | `stats`, `in_degree`, `pagerank`/`ppr`, `components`, `louvain`, or `shortest_path` (default: `stats`) |
| `type` | No | Restrict projection to this rdf:type IRI |
| `predicate` | No | Restrict projection to edges with this predicate IRI |
| `limit` | No | Max results for in_degree/pagerank (default: 20) |
| `seeds` | No | Seed entity IRIs for personalized PageRank (non-empty switches pagerank to PPR) |
| `damping` | No | PageRank damping factor (default: 0.85) |
| `max_iters` | No | PageRank max iterations (default: 100) |
| `tolerance` | No | PageRank convergence tolerance (default: 1e-6) |
| `from` / `to` | No | Source/target entity IRIs for `shortest_path` |
| `persist` | No | `louvain` only: persist `quipu:memberOfCommunity` facts, superseding any prior derivation (default: `false`). Emergent clustering, **not** an access boundary. |

The `louvain` algorithm runs deterministic modularity-based community detection
and returns `{ communities: [{ community, entities, size }], modularity }`.
Read-only unless `persist: true`.

### `quipu_context`

Expand Down
32 changes: 31 additions & 1 deletion docs/design/pagerank.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,40 @@ This spec does not invent a feature — it builds one already on the books:

## Non-goals (v1)

- Louvain / community detection (future; same Projection API).
- ~~Louvain / community detection (future; same Projection API).~~ **Delivered
(hq-zlph) — see [Community detection](#community-detection-louvain) below.**
- Approximate / streaming PageRank at write time.
- Distributed computation (single-process, "SQLite energy").

## Community detection (Louvain)

Delivered on the same Projection API as PageRank (hq-zlph, aegis-1p0 Gap 3).
`graph::louvain` runs Louvain's modularity local-moving phase over the projected
graph and returns a partition plus its Newman–Girvan modularity. Exposed as
`tool_project` `algorithm: "louvain"` (alias `"community"`).

- **Undirected weighting.** The projected directed multigraph is read undirected:
parallel edges sum into one weight, self-loops dropped, direction ignored — the
standard input shape for modularity-based clustering.
- **Determinism is a hard requirement.** Nodes are visited in ascending entity-id
order, candidate communities evaluated in ascending-label order, and ties in
modularity gain broken toward the lowest label. The same graph always yields the
same partition, independent of hash iteration order. (Hand-rolled, pure-Rust, no
C-backed clustering dependency.)
- **Opt-in persistence.** Read-only by default. With `persist: true`, each
entity's community is written as a `quipu:memberOfCommunity` fact through the
store (provenance `source = "algo:louvain"`), so clusters become queryable,
time-travelable knowledge — exactly like persisted PageRank scores.
- **Supersede on re-run.** Membership is *derived* and goes stale when the graph
changes, so a re-run bitemporally supersedes the prior derivation: changed
memberships are retracted (`valid_to` closed) and new ones asserted, reconciled
by `(entity, community-term)`. A default current-facts query returns only the
latest partition — stale clusters never accumulate.
- **Not an access boundary.** Community membership is *emergent clustering*, not a
tenancy or authorization primitive. Like `group_id` (hq-2u3: provenance, not
isolation), it must never gate access; consumers must not build access control
on `quipu:memberOfCommunity`.

## API design

A pure function in `src/graph.rs`, no `Store` coupling in the math:
Expand Down
4 changes: 2 additions & 2 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -295,15 +295,15 @@ pub fn cmd_project(args: &[String], db_path: &str) {
input["seeds"] = serde_json::Value::Array(seeds);
}

let store = match quipu::Store::open(db_path) {
let mut store = match quipu::Store::open(db_path) {
Ok(s) => s,
Err(e) => {
eprintln!("error opening store: {e}");
std::process::exit(1);
}
};

match quipu::tool_project(&store, &input) {
match quipu::tool_project(&mut store, &input) {
Ok(result) => println!("{}", serde_json::to_string_pretty(&result).unwrap()),
Err(e) => {
eprintln!("error: {e}");
Expand Down
Loading
Loading