graphmark 0.1.1__tar.gz → 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- graphmark-0.2.0/PKG-INFO +112 -0
- graphmark-0.2.0/README.md +86 -0
- graphmark-0.2.0/docs/ROADMAP.md +111 -0
- {graphmark-0.1.1 → graphmark-0.2.0}/pyproject.toml +24 -1
- graphmark-0.2.0/src/graphmark/__init__.py +8 -0
- {graphmark-0.1.1 → graphmark-0.2.0}/src/graphmark/cli.py +16 -4
- {graphmark-0.1.1 → graphmark-0.2.0}/src/graphmark/config.py +13 -8
- {graphmark-0.1.1 → graphmark-0.2.0}/src/graphmark/dismiss.py +10 -1
- {graphmark-0.1.1 → graphmark-0.2.0}/src/graphmark/export.py +11 -2
- {graphmark-0.1.1 → graphmark-0.2.0}/src/graphmark/graph.py +18 -1
- {graphmark-0.1.1 → graphmark-0.2.0}/src/graphmark/interfaces.py +11 -0
- {graphmark-0.1.1 → graphmark-0.2.0}/src/graphmark/metrics.py +31 -4
- graphmark-0.2.0/src/graphmark/model.py +20 -0
- {graphmark-0.1.1 → graphmark-0.2.0}/src/graphmark/parse.py +27 -4
- {graphmark-0.1.1 → graphmark-0.2.0}/tests/test_cli.py +32 -0
- {graphmark-0.1.1 → graphmark-0.2.0}/tests/test_config.py +21 -2
- {graphmark-0.1.1 → graphmark-0.2.0}/tests/test_dismiss.py +16 -0
- {graphmark-0.1.1 → graphmark-0.2.0}/tests/test_export.py +19 -0
- {graphmark-0.1.1 → graphmark-0.2.0}/tests/test_gaps.py +9 -0
- graphmark-0.2.0/tests/test_gaps_banding.py +29 -0
- {graphmark-0.1.1 → graphmark-0.2.0}/tests/test_graph.py +30 -0
- {graphmark-0.1.1 → graphmark-0.2.0}/tests/test_metrics.py +17 -0
- graphmark-0.2.0/tests/test_model.py +22 -0
- {graphmark-0.1.1 → graphmark-0.2.0}/tests/test_parse.py +28 -0
- {graphmark-0.1.1 → graphmark-0.2.0}/tests/test_smoke.py +4 -1
- graphmark-0.1.1/PKG-INFO +0 -57
- graphmark-0.1.1/README.md +0 -31
- graphmark-0.1.1/docs/ROADMAP.md +0 -73
- graphmark-0.1.1/src/graphmark/__init__.py +0 -3
- graphmark-0.1.1/src/graphmark/model.py +0 -44
- {graphmark-0.1.1 → graphmark-0.2.0}/.gitignore +0 -0
- {graphmark-0.1.1 → graphmark-0.2.0}/LICENSE +0 -0
- {graphmark-0.1.1 → graphmark-0.2.0}/tests/fixtures/README.md +0 -0
- {graphmark-0.1.1 → graphmark-0.2.0}/tests/fixtures/alt/config.toml +0 -0
- {graphmark-0.1.1 → graphmark-0.2.0}/tests/fixtures/alt/expected.json +0 -0
- {graphmark-0.1.1 → graphmark-0.2.0}/tests/fixtures/alt/vault/daily/2026-07-01.md +0 -0
- {graphmark-0.1.1 → graphmark-0.2.0}/tests/fixtures/alt/vault/docs/alpha.md +0 -0
- {graphmark-0.1.1 → graphmark-0.2.0}/tests/fixtures/alt/vault/docs/center.md +0 -0
- {graphmark-0.1.1 → graphmark-0.2.0}/tests/fixtures/alt/vault/misc/echo.md +0 -0
- {graphmark-0.1.1 → graphmark-0.2.0}/tests/fixtures/alt/vault/misc/foxtrot.md +0 -0
- {graphmark-0.1.1 → graphmark-0.2.0}/tests/fixtures/alt/vault/misc/orphan.md +0 -0
- {graphmark-0.1.1 → graphmark-0.2.0}/tests/fixtures/alt/vault/misc/stub.md +0 -0
- {graphmark-0.1.1 → graphmark-0.2.0}/tests/fixtures/alt/vault/refs/beta.md +0 -0
- {graphmark-0.1.1 → graphmark-0.2.0}/tests/fixtures/alt/vault/refs/delta.md +0 -0
- {graphmark-0.1.1 → graphmark-0.2.0}/tests/fixtures/alt/vault/refs/gamma.md +0 -0
- {graphmark-0.1.1 → graphmark-0.2.0}/tests/fixtures/dismiss/expected.json +0 -0
- {graphmark-0.1.1 → graphmark-0.2.0}/tests/fixtures/dismiss/vault/.claude/data/connect-dismissed.json +0 -0
- {graphmark-0.1.1 → graphmark-0.2.0}/tests/fixtures/dismiss/vault/alpha.md +0 -0
- {graphmark-0.1.1 → graphmark-0.2.0}/tests/fixtures/dismiss/vault/beta.md +0 -0
- {graphmark-0.1.1 → graphmark-0.2.0}/tests/fixtures/dismiss/vault/gamma.md +0 -0
- {graphmark-0.1.1 → graphmark-0.2.0}/tests/fixtures/gaps/config.toml +0 -0
- {graphmark-0.1.1 → graphmark-0.2.0}/tests/fixtures/gaps/expected.json +0 -0
- {graphmark-0.1.1 → graphmark-0.2.0}/tests/fixtures/gaps/expected_oog.json +0 -0
- {graphmark-0.1.1 → graphmark-0.2.0}/tests/fixtures/gaps/similar.json +0 -0
- {graphmark-0.1.1 → graphmark-0.2.0}/tests/fixtures/gaps/similar_oog.json +0 -0
- {graphmark-0.1.1 → graphmark-0.2.0}/tests/fixtures/gaps/vault/docs/a.md +0 -0
- {graphmark-0.1.1 → graphmark-0.2.0}/tests/fixtures/gaps/vault/docs/hub.md +0 -0
- {graphmark-0.1.1 → graphmark-0.2.0}/tests/fixtures/gaps/vault/misc/c.md +0 -0
- {graphmark-0.1.1 → graphmark-0.2.0}/tests/fixtures/gaps/vault/misc/d.md +0 -0
- {graphmark-0.1.1 → graphmark-0.2.0}/tests/fixtures/gaps/vault/refs/b.md +0 -0
- {graphmark-0.1.1 → graphmark-0.2.0}/tests/fixtures/gaps/vault/refs/e.md +0 -0
- {graphmark-0.1.1 → graphmark-0.2.0}/tests/fixtures/scoped/config.toml +0 -0
- {graphmark-0.1.1 → graphmark-0.2.0}/tests/fixtures/scoped/expected.json +0 -0
- {graphmark-0.1.1 → graphmark-0.2.0}/tests/fixtures/scoped/vault/docs/one.md +0 -0
- {graphmark-0.1.1 → graphmark-0.2.0}/tests/fixtures/scoped/vault/junk/ignored.md +0 -0
- {graphmark-0.1.1 → graphmark-0.2.0}/tests/fixtures/scoped/vault/misc/excluded.md +0 -0
- {graphmark-0.1.1 → graphmark-0.2.0}/tests/fixtures/scoped/vault/refs/two.md +0 -0
- {graphmark-0.1.1 → graphmark-0.2.0}/tests/fixtures/selflink/config.toml +0 -0
- {graphmark-0.1.1 → graphmark-0.2.0}/tests/fixtures/selflink/expected.json +0 -0
- {graphmark-0.1.1 → graphmark-0.2.0}/tests/fixtures/selflink/vault/a.md +0 -0
- {graphmark-0.1.1 → graphmark-0.2.0}/tests/fixtures/selflink/vault/b.md +0 -0
- {graphmark-0.1.1 → graphmark-0.2.0}/tests/fixtures/simple/config.toml +0 -0
- {graphmark-0.1.1 → graphmark-0.2.0}/tests/fixtures/simple/expected.json +0 -0
- {graphmark-0.1.1 → graphmark-0.2.0}/tests/fixtures/simple/vault/brain/alpha.md +0 -0
- {graphmark-0.1.1 → graphmark-0.2.0}/tests/fixtures/simple/vault/brain/hub.md +0 -0
- {graphmark-0.1.1 → graphmark-0.2.0}/tests/fixtures/simple/vault/personal/beta.md +0 -0
- {graphmark-0.1.1 → graphmark-0.2.0}/tests/fixtures/simple/vault/personal/gamma.md +0 -0
- {graphmark-0.1.1 → graphmark-0.2.0}/tests/fixtures/simple/vault/reference/island.md +0 -0
- {graphmark-0.1.1 → graphmark-0.2.0}/tests/fixtures/simple/vault/reference/stub.md +0 -0
- {graphmark-0.1.1 → graphmark-0.2.0}/tests/test_gaps_oog.py +0 -0
- {graphmark-0.1.1 → graphmark-0.2.0}/tests/test_pagerank.py +0 -0
- {graphmark-0.1.1 → graphmark-0.2.0}/tests/test_scoped.py +0 -0
- {graphmark-0.1.1 → graphmark-0.2.0}/tests/test_selflink.py +0 -0
graphmark-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: graphmark
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Deterministic knowledge-graph analysis for markdown / [[wikilink]] vaults.
|
|
5
|
+
Project-URL: Homepage, https://github.com/cdcoonce/graphmark
|
|
6
|
+
Project-URL: Repository, https://github.com/cdcoonce/graphmark
|
|
7
|
+
Project-URL: Issues, https://github.com/cdcoonce/graphmark/issues
|
|
8
|
+
Author-email: Charles Coonce <charlescoonce@gmail.com>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: graph,knowledge-graph,markdown,obsidian,pagerank,wikilink
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Scientific/Engineering :: Information Analysis
|
|
19
|
+
Classifier: Topic :: Text Processing :: Markup :: Markdown
|
|
20
|
+
Requires-Python: >=3.11
|
|
21
|
+
Requires-Dist: networkx>=3.0
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
24
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# graphmark
|
|
28
|
+
|
|
29
|
+
Deterministic knowledge-graph analysis for markdown / `[[wikilink]]` vaults — orphans, hubs,
|
|
30
|
+
clusters, bridges, siloed notes, neighborhoods, PageRank, and link-gap suggestions over your notes,
|
|
31
|
+
driven by a small config so it works on any Obsidian-family vault.
|
|
32
|
+
|
|
33
|
+
> Status: **v0.1.1 on PyPI.** The engine is complete and pinned by a frozen differential oracle —
|
|
34
|
+
> structural outputs reproduce a proven reference implementation exactly (shape, ordering,
|
|
35
|
+
> tie-breaking). See `CLAUDE.md` for the architecture and the reference-parity contract.
|
|
36
|
+
|
|
37
|
+
## Install
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install graphmark # or: uv pip install graphmark
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Dev setup:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
uv pip install -e ".[dev]"
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Test
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
uv run --extra dev ruff check . && uv run --extra dev ruff format --check . && uv run --extra dev pytest -q
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## CLI
|
|
56
|
+
|
|
57
|
+
Every command takes `--config PATH` (TOML) and/or `--root PATH` (vault root; overrides the config's
|
|
58
|
+
root). Output is deterministic JSON (DOT for `export dot`).
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
graphmark stats --root /path/to/vault
|
|
62
|
+
graphmark orphans --config configs/my-brain.toml
|
|
63
|
+
graphmark hubs --n 10 --root /path/to/vault
|
|
64
|
+
graphmark clusters --root /path/to/vault
|
|
65
|
+
graphmark bridges --root /path/to/vault
|
|
66
|
+
graphmark siloed --root /path/to/vault
|
|
67
|
+
graphmark neighborhood --note a/b.md --depth 2 --root /path/to/vault
|
|
68
|
+
graphmark pagerank --n 10 --alpha 0.85 --root /path/to/vault
|
|
69
|
+
graphmark export dot --root /path/to/vault > graph.dot
|
|
70
|
+
graphmark gaps --root /path/to/vault
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Note: `gaps` is a **library-first** metric — it ranks and filters link-gap candidates over a
|
|
74
|
+
similarity function you inject (`metrics.gaps(graph, similar_fn, ...)`); the package itself ships no
|
|
75
|
+
embeddings. The CLI subcommand has no similarity source to inject, so `graphmark gaps` prints
|
|
76
|
+
guidance to stderr and exits 2 rather than silently returning `[]`; use the library API (below).
|
|
77
|
+
|
|
78
|
+
## Library
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
from pathlib import Path
|
|
82
|
+
|
|
83
|
+
from graphmark.config import VaultConfig
|
|
84
|
+
from graphmark.graph import NormalizeResolver, VaultGraph
|
|
85
|
+
from graphmark.metrics import gaps, hubs, stats
|
|
86
|
+
from graphmark.parse import WikilinkExtractor
|
|
87
|
+
|
|
88
|
+
config = VaultConfig(root=Path("/path/to/vault"))
|
|
89
|
+
graph = VaultGraph.build(config, WikilinkExtractor(), NormalizeResolver())
|
|
90
|
+
|
|
91
|
+
print(stats(graph))
|
|
92
|
+
print(hubs(graph, n=10))
|
|
93
|
+
|
|
94
|
+
# gaps: you supply similarity — graphmark owns the deterministic ranking/filtering.
|
|
95
|
+
print(gaps(graph, similar_fn=my_similarity, threshold=0.6, k=8))
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
`dismiss.py` provides a content-hash dismissal store for gap suggestions: a dismissed pair stays
|
|
99
|
+
suppressed only while both notes exist with unchanged content.
|
|
100
|
+
|
|
101
|
+
## Development & releases
|
|
102
|
+
|
|
103
|
+
Two long-lived branches: **`dev`** is the integration branch (open PRs against it); **`main`** is
|
|
104
|
+
the release branch. Both are gated by CI (`ruff check` + `ruff format --check` + `pytest -q` on
|
|
105
|
+
Ubuntu and macOS).
|
|
106
|
+
|
|
107
|
+
Releases are automated by conventional commits. When `dev` is promoted to `main`,
|
|
108
|
+
`.github/workflows/release.yml` runs [python-semantic-release](https://python-semantic-release.readthedocs.io):
|
|
109
|
+
it reads the commit history since the last tag, bumps the version, updates `CHANGELOG.md`, tags
|
|
110
|
+
`v<version>`, cuts a GitHub release, and publishes to PyPI via OIDC Trusted Publishing. So the
|
|
111
|
+
version is a function of your commit messages — `feat:` bumps the minor, `fix:`/`perf:` the patch
|
|
112
|
+
(pre-1.0, breaking changes bump the minor, not the major). No manual version edits, no tokens.
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# graphmark
|
|
2
|
+
|
|
3
|
+
Deterministic knowledge-graph analysis for markdown / `[[wikilink]]` vaults — orphans, hubs,
|
|
4
|
+
clusters, bridges, siloed notes, neighborhoods, PageRank, and link-gap suggestions over your notes,
|
|
5
|
+
driven by a small config so it works on any Obsidian-family vault.
|
|
6
|
+
|
|
7
|
+
> Status: **v0.1.1 on PyPI.** The engine is complete and pinned by a frozen differential oracle —
|
|
8
|
+
> structural outputs reproduce a proven reference implementation exactly (shape, ordering,
|
|
9
|
+
> tie-breaking). See `CLAUDE.md` for the architecture and the reference-parity contract.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pip install graphmark # or: uv pip install graphmark
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Dev setup:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
uv pip install -e ".[dev]"
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Test
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
uv run --extra dev ruff check . && uv run --extra dev ruff format --check . && uv run --extra dev pytest -q
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## CLI
|
|
30
|
+
|
|
31
|
+
Every command takes `--config PATH` (TOML) and/or `--root PATH` (vault root; overrides the config's
|
|
32
|
+
root). Output is deterministic JSON (DOT for `export dot`).
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
graphmark stats --root /path/to/vault
|
|
36
|
+
graphmark orphans --config configs/my-brain.toml
|
|
37
|
+
graphmark hubs --n 10 --root /path/to/vault
|
|
38
|
+
graphmark clusters --root /path/to/vault
|
|
39
|
+
graphmark bridges --root /path/to/vault
|
|
40
|
+
graphmark siloed --root /path/to/vault
|
|
41
|
+
graphmark neighborhood --note a/b.md --depth 2 --root /path/to/vault
|
|
42
|
+
graphmark pagerank --n 10 --alpha 0.85 --root /path/to/vault
|
|
43
|
+
graphmark export dot --root /path/to/vault > graph.dot
|
|
44
|
+
graphmark gaps --root /path/to/vault
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Note: `gaps` is a **library-first** metric — it ranks and filters link-gap candidates over a
|
|
48
|
+
similarity function you inject (`metrics.gaps(graph, similar_fn, ...)`); the package itself ships no
|
|
49
|
+
embeddings. The CLI subcommand has no similarity source to inject, so `graphmark gaps` prints
|
|
50
|
+
guidance to stderr and exits 2 rather than silently returning `[]`; use the library API (below).
|
|
51
|
+
|
|
52
|
+
## Library
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
from pathlib import Path
|
|
56
|
+
|
|
57
|
+
from graphmark.config import VaultConfig
|
|
58
|
+
from graphmark.graph import NormalizeResolver, VaultGraph
|
|
59
|
+
from graphmark.metrics import gaps, hubs, stats
|
|
60
|
+
from graphmark.parse import WikilinkExtractor
|
|
61
|
+
|
|
62
|
+
config = VaultConfig(root=Path("/path/to/vault"))
|
|
63
|
+
graph = VaultGraph.build(config, WikilinkExtractor(), NormalizeResolver())
|
|
64
|
+
|
|
65
|
+
print(stats(graph))
|
|
66
|
+
print(hubs(graph, n=10))
|
|
67
|
+
|
|
68
|
+
# gaps: you supply similarity — graphmark owns the deterministic ranking/filtering.
|
|
69
|
+
print(gaps(graph, similar_fn=my_similarity, threshold=0.6, k=8))
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
`dismiss.py` provides a content-hash dismissal store for gap suggestions: a dismissed pair stays
|
|
73
|
+
suppressed only while both notes exist with unchanged content.
|
|
74
|
+
|
|
75
|
+
## Development & releases
|
|
76
|
+
|
|
77
|
+
Two long-lived branches: **`dev`** is the integration branch (open PRs against it); **`main`** is
|
|
78
|
+
the release branch. Both are gated by CI (`ruff check` + `ruff format --check` + `pytest -q` on
|
|
79
|
+
Ubuntu and macOS).
|
|
80
|
+
|
|
81
|
+
Releases are automated by conventional commits. When `dev` is promoted to `main`,
|
|
82
|
+
`.github/workflows/release.yml` runs [python-semantic-release](https://python-semantic-release.readthedocs.io):
|
|
83
|
+
it reads the commit history since the last tag, bumps the version, updates `CHANGELOG.md`, tags
|
|
84
|
+
`v<version>`, cuts a GitHub release, and publishes to PyPI via OIDC Trusted Publishing. So the
|
|
85
|
+
version is a function of your commit messages — `feat:` bumps the minor, `fix:`/`perf:` the patch
|
|
86
|
+
(pre-1.0, breaking changes bump the minor, not the major). No manual version edits, no tokens.
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# Roadmap — graphmark (for `afk-driver --expand`)
|
|
2
|
+
|
|
3
|
+
> Read **verbatim** by `afk-driver --expand` and injected into the feature-proposing agent's prompt
|
|
4
|
+
> alongside the live code. Write at the altitude of **intent** — name directions and gaps; let the
|
|
5
|
+
> expander (and the code it reads) propose specifics. **Every proposal must state which Track it
|
|
6
|
+
> advances** — that is how a human decides whether a green gate is sufficient.
|
|
7
|
+
|
|
8
|
+
## Vision
|
|
9
|
+
|
|
10
|
+
A general, deterministic **knowledge-graph analysis library + CLI** for markdown / `[[wikilink]]`
|
|
11
|
+
vaults — orphans, hubs, clusters, bridges, siloed notes, neighborhoods, PageRank, and link-gap
|
|
12
|
+
suggestions — driven by a small config so it works on **any** Obsidian-family vault. Published on
|
|
13
|
+
PyPI; generalized from a proven vault-specific engine (`brain_map.py`).
|
|
14
|
+
|
|
15
|
+
The central fact of this repo: **correctness is pinned by a frozen differential oracle.** Structural
|
|
16
|
+
outputs are the verbatim results of the reference engine, PageRank is checked against networkx, and
|
|
17
|
+
live-vault parity diffing backs the fixtures. The executor cannot regenerate the oracle — so a
|
|
18
|
+
**green gate proves correctness**, and Track A/C work is autonomous-safe.
|
|
19
|
+
|
|
20
|
+
Identity (settled 2026-07-19): graphmark is **personal infrastructure with a public correctness
|
|
21
|
+
story** — the priority consumer is the owner's vault seam (`graph_cli.py`), not external adoption.
|
|
22
|
+
|
|
23
|
+
## What's shipped (baseline — do not re-propose)
|
|
24
|
+
|
|
25
|
+
- **The full engine, v0.1.1 on PyPI**: `parse.py` (wikilink extraction, code-span skipping),
|
|
26
|
+
`graph.py` (catalog + `NormalizeResolver` + `VaultGraph.build`), `metrics.py` (stats, orphans,
|
|
27
|
+
hubs, clusters, bridges, siloed_notes, neighborhood, pure-python pagerank, gaps),
|
|
28
|
+
`export.py` (JSON/DOT), `cli.py`, `config.py` (`VaultConfig` + `load_config`), and
|
|
29
|
+
`dismiss.py` (content-hash weaklink dismissal store).
|
|
30
|
+
- **Six frozen differential fixtures** — simple, alt, scoped, selflink, gaps (+ out-of-graph),
|
|
31
|
+
dismiss — plus live-vault parity diffs against the reference engine.
|
|
32
|
+
- **`gaps()` with injected similarity** — graphmark owns the deterministic ranking/filtering
|
|
33
|
+
(already-linked / self / threshold / max-score / prefix / dismissed filters; reciprocal dedup;
|
|
34
|
+
novelty-first ordering); the embedding source is injected by the caller. The gate stays
|
|
35
|
+
fastembed-free.
|
|
36
|
+
- **Packaging** — PyPI Trusted Publishing on `v*` tags; networkx-only runtime dependency.
|
|
37
|
+
- **Reintegration** — the-vault's `graph_cli.py` consumes the package and injects the vault's
|
|
38
|
+
similarity function; /connect and /garden run on it.
|
|
39
|
+
|
|
40
|
+
## Direction
|
|
41
|
+
|
|
42
|
+
### Track A — Engine parity (CLOSED at v0.1.1 — do not re-propose engine work)
|
|
43
|
+
|
|
44
|
+
### Track C — Hardening & honest surface (gate-verifiable; autonomous-safe)
|
|
45
|
+
|
|
46
|
+
_Where we are:_ v0.1.1 is correct on its parity paths but carries correctness debt and dead
|
|
47
|
+
surface no consumer exercises.
|
|
48
|
+
_Where we're going:_ **v0.2.0 — every error path tested, every public knob real.**
|
|
49
|
+
|
|
50
|
+
- Version single-sourcing (pyproject / `__init__.__version__` / smoke-test drift).
|
|
51
|
+
- Robustness: non-UTF8 note handling in `parse_document`; JSON-corruption guards in
|
|
52
|
+
`dismiss.load_dismissed` / `active_dismissed_sigs`; `load_config` clear error on missing keys;
|
|
53
|
+
DOT identifier escaping in `to_dot`; defined behavior for `neighborhood` on an unknown note.
|
|
54
|
+
- Cut or wire dead surface: `model.Edge/Graph/Finding` (never consumed — **removed in the 0.2 API
|
|
55
|
+
cut; `Document` is the only model type**); the CLI `gaps` stub
|
|
56
|
+
(null `similar_fn` — always `[]`); the silent no-op config knobs `wikilink_pattern` and
|
|
57
|
+
`orphan_min_chars` (wire them or delete them); make the dismissal-store path injectable instead
|
|
58
|
+
of hardcoding `.claude/data/connect-dismissed.json`.
|
|
59
|
+
- Move the validated gaps banding policy (threshold / max-score / k / hub-degree defaults) from the
|
|
60
|
+
consumer's argparse into the package so any consumer gets the proven band.
|
|
61
|
+
- A `Similarity` Protocol in `interfaces.py` typing the injected `similar_fn`.
|
|
62
|
+
|
|
63
|
+
_The principle:_ each slice is single-purpose with its asserting test **pre-frozen by the
|
|
64
|
+
conductor before dispatch** — the oracle-first discipline that closed the zero-test hole.
|
|
65
|
+
|
|
66
|
+
### Track D — `graphmark check`: the deterministic CI gate (feature track)
|
|
67
|
+
|
|
68
|
+
The one unserved niche the ecosystem offers: **headless vault-health gating**. Obsidian's official
|
|
69
|
+
CLI requires the desktop app; the dormant Python incumbent has no CLI; link checkers do no graph
|
|
70
|
+
metrics.
|
|
71
|
+
|
|
72
|
+
- `graphmark check --config ...` — thresholds (max orphans, max unresolved links, max siloed
|
|
73
|
+
notes, ...), non-zero exit on breach, byte-stable JSON report for diffing across runs.
|
|
74
|
+
- Config-driven policy block (`[check]` in the TOML) so the gate is declarative.
|
|
75
|
+
- Later: a thin GitHub Action wrapper. The owner's vault dogfoods the gate first.
|
|
76
|
+
|
|
77
|
+
### Track B — Judgment the oracle can't cover (human-validated)
|
|
78
|
+
|
|
79
|
+
- New fixtures on a **different schema** than my-brain have hand-authored expected values → a human
|
|
80
|
+
confirms them. Public API naming, packaging/release, and documentation voice are Track B.
|
|
81
|
+
|
|
82
|
+
## Principles every proposal must respect
|
|
83
|
+
|
|
84
|
+
- **The oracle is the spec.** Never edit an `expected.json` to pass a test. Match it; if it looks
|
|
85
|
+
wrong, flag it.
|
|
86
|
+
- **Pre-frozen asserting tests.** New modules/behaviors ship only when the conductor has committed
|
|
87
|
+
the failing test before the slice runs — a green gate must prove the new code, not just tolerate it.
|
|
88
|
+
- **The gaps contract:** graphmark owns deterministic ranking/filtering; similarity is **injected**;
|
|
89
|
+
the package and its gate stay embedding-free (no fastembed/numpy/scipy in deps).
|
|
90
|
+
- **Implement within the seeded interfaces.** Do not redesign `model`/`interfaces`/`config`
|
|
91
|
+
boundaries.
|
|
92
|
+
- **Dependency-light.** Shipped runtime dep is `networkx` only.
|
|
93
|
+
- **Keep the layers separate** — engine (vault-agnostic) / config (the domain seam) / surface
|
|
94
|
+
(JSON, DOT, CLI) / dismissal store. See `CLAUDE.md`.
|
|
95
|
+
- **TDD, tracer-bullet slices.** `afk-sized` = single-purpose, independently testable; larger is
|
|
96
|
+
`needs-decomposition`.
|
|
97
|
+
|
|
98
|
+
## Non-goals (CLOSED as dropped 2026-07-19 — do not propose)
|
|
99
|
+
|
|
100
|
+
Evaluated against the ecosystem and dropped for zero consumer pull; do not re-litigate:
|
|
101
|
+
|
|
102
|
+
- Embeddings / semantic search / any similarity backend **inside** the package.
|
|
103
|
+
- Betweenness centrality, Louvain/Leiden community detection.
|
|
104
|
+
- CSV/GML/Cypher export; incremental/cached builds; any LLM "propose" pass.
|
|
105
|
+
- MCP server or retrieval/search surface.
|
|
106
|
+
- Alternate link-syntax adapters (Logseq, `[](.)` markdown) — the pluggable interfaces exist so
|
|
107
|
+
these _can_ be added on demand; do not build them speculatively.
|
|
108
|
+
- Re-platforming (no swapping networkx; no async/parallel rewrites).
|
|
109
|
+
- Performance work without a benchmark showing pain (the live consumer is a ~340-note vault). The
|
|
110
|
+
known ceilings — O(L·N) path-suffix resolution, O(A·(V+E)) `siloed_notes`, per-metric graph
|
|
111
|
+
rebuilds — are documented, accepted, and not to be optimized speculatively.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "graphmark"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.2.0"
|
|
4
4
|
description = "Deterministic knowledge-graph analysis for markdown / [[wikilink]] vaults."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.11"
|
|
@@ -50,3 +50,26 @@ select = ["E", "F", "I", "UP", "B", "SIM"]
|
|
|
50
50
|
|
|
51
51
|
[tool.pytest.ini_options]
|
|
52
52
|
testpaths = ["tests"]
|
|
53
|
+
|
|
54
|
+
[tool.semantic_release]
|
|
55
|
+
# Deployment is driven by conventional-commit history: when dev is promoted to main, the
|
|
56
|
+
# release workflow (.github/workflows/release.yml) runs python-semantic-release, which reads the
|
|
57
|
+
# commits since the last tag, decides the bump, updates the version below, writes CHANGELOG.md,
|
|
58
|
+
# commits, tags v<version>, and cuts a GitHub release. The same workflow then publishes to PyPI.
|
|
59
|
+
version_toml = ["pyproject.toml:project.version"]
|
|
60
|
+
# The semantic-release action runs in its own container without uv; leave the build to the
|
|
61
|
+
# workflow's own uv-based publish step (release.yml), so this is intentionally empty.
|
|
62
|
+
build_command = ""
|
|
63
|
+
tag_format = "v{version}"
|
|
64
|
+
commit_message = "chore(release): {version} [skip ci]"
|
|
65
|
+
# Pre-1.0: a breaking change bumps the minor, not the major, until the API is declared stable.
|
|
66
|
+
major_on_zero = false
|
|
67
|
+
allow_zero_version = true
|
|
68
|
+
|
|
69
|
+
[tool.semantic_release.branches.main]
|
|
70
|
+
match = "main"
|
|
71
|
+
|
|
72
|
+
[tool.semantic_release.commit_parser_options]
|
|
73
|
+
# feat -> minor, fix/perf -> patch; these types appear in the changelog.
|
|
74
|
+
minor_tags = ["feat"]
|
|
75
|
+
patch_tags = ["fix", "perf"]
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""graphmark — deterministic knowledge-graph analysis for markdown/wikilink vaults."""
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
__version__ = version("graphmark")
|
|
7
|
+
except PackageNotFoundError: # un-installed source checkout
|
|
8
|
+
__version__ = "0.0.0+unknown"
|
|
@@ -12,7 +12,6 @@ from graphmark.graph import NormalizeResolver, VaultGraph
|
|
|
12
12
|
from graphmark.metrics import (
|
|
13
13
|
bridges,
|
|
14
14
|
clusters,
|
|
15
|
-
gaps,
|
|
16
15
|
hubs,
|
|
17
16
|
neighborhood,
|
|
18
17
|
orphans,
|
|
@@ -73,6 +72,16 @@ def main() -> None:
|
|
|
73
72
|
parser.print_help()
|
|
74
73
|
sys.exit(1)
|
|
75
74
|
|
|
75
|
+
if args.command == "gaps":
|
|
76
|
+
# gaps needs a caller-injected similarity source the CLI can't supply; it is
|
|
77
|
+
# library-only. Signpost the library API rather than silently printing [].
|
|
78
|
+
print(
|
|
79
|
+
"gaps requires an injected similarity source; use the library API "
|
|
80
|
+
"(graphmark.metrics.gaps) — see README",
|
|
81
|
+
file=sys.stderr,
|
|
82
|
+
)
|
|
83
|
+
sys.exit(2)
|
|
84
|
+
|
|
76
85
|
graph, config = _load(args)
|
|
77
86
|
|
|
78
87
|
if args.command == "stats":
|
|
@@ -88,13 +97,16 @@ def main() -> None:
|
|
|
88
97
|
elif args.command == "siloed":
|
|
89
98
|
print(to_json(siloed_notes(graph)))
|
|
90
99
|
elif args.command == "neighborhood":
|
|
91
|
-
|
|
100
|
+
try:
|
|
101
|
+
result = neighborhood(graph, args.note, depth=args.depth)
|
|
102
|
+
except ValueError as e:
|
|
103
|
+
print(f"error: {e}", file=sys.stderr)
|
|
104
|
+
sys.exit(2)
|
|
105
|
+
print(to_json(result))
|
|
92
106
|
elif args.command == "pagerank":
|
|
93
107
|
print(to_json(pagerank(graph, n=args.n, alpha=args.alpha)))
|
|
94
108
|
elif args.command == "export" and args.format == "dot":
|
|
95
109
|
print(to_dot(graph))
|
|
96
|
-
elif args.command == "gaps":
|
|
97
|
-
print(to_json(gaps(graph, lambda _rel, _k: [])))
|
|
98
110
|
|
|
99
111
|
|
|
100
112
|
if __name__ == "__main__":
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"""Vault configuration — the domain seam that makes the engine general.
|
|
2
2
|
|
|
3
|
-
``VaultConfig`` holds every vault-specific policy the engine consults. ``load_config``
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
``VaultConfig`` holds every vault-specific policy the engine consults. ``load_config`` reads a
|
|
4
|
+
TOML file into a ``VaultConfig`` (paths resolved relative to the TOML's directory). Fixture tests
|
|
5
|
+
may construct ``VaultConfig`` directly.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
from __future__ import annotations
|
|
@@ -20,16 +20,23 @@ class VaultConfig:
|
|
|
20
20
|
scoped_folders: list[str] = field(default_factory=list)
|
|
21
21
|
excluded_dirs: list[str] = field(default_factory=list)
|
|
22
22
|
rules_files: list[str] = field(default_factory=lambda: ["CLAUDE.md", "CLAUDE.local.md"])
|
|
23
|
-
wikilink_pattern: str = r"\[\[(.+?)\]\]"
|
|
24
|
-
orphan_min_chars: int = 300
|
|
25
23
|
transient_prefixes: tuple[str, ...] = ()
|
|
26
24
|
|
|
27
25
|
|
|
28
26
|
def load_config(path: Path) -> VaultConfig:
|
|
29
|
-
"""Load a VaultConfig from a TOML file.
|
|
27
|
+
"""Load a VaultConfig from a TOML file.
|
|
28
|
+
|
|
29
|
+
``root`` is the only required key (resolved relative to the TOML's directory). Every other
|
|
30
|
+
key that maps to a ``VaultConfig`` field is optional and falls back to the dataclass default;
|
|
31
|
+
any other key in the TOML is silently ignored. A TOML missing ``root`` raises ``ValueError``
|
|
32
|
+
naming the file and the missing key.
|
|
33
|
+
"""
|
|
30
34
|
with open(path, "rb") as f:
|
|
31
35
|
data = tomllib.load(f)
|
|
32
36
|
|
|
37
|
+
if "root" not in data:
|
|
38
|
+
raise ValueError(f"config {path}: missing required key 'root'")
|
|
39
|
+
|
|
33
40
|
toml_dir = path.parent
|
|
34
41
|
root = toml_dir / data["root"]
|
|
35
42
|
|
|
@@ -38,7 +45,5 @@ def load_config(path: Path) -> VaultConfig:
|
|
|
38
45
|
scoped_folders=data.get("scoped_folders", []),
|
|
39
46
|
excluded_dirs=data.get("excluded_dirs", []),
|
|
40
47
|
rules_files=data.get("rules_files", ["CLAUDE.md", "CLAUDE.local.md"]),
|
|
41
|
-
wikilink_pattern=data.get("wikilink_pattern", r"\[\[(.+?)\]\]"),
|
|
42
|
-
orphan_min_chars=data.get("orphan_min_chars", 300),
|
|
43
48
|
transient_prefixes=tuple(data.get("transient_prefixes", [])),
|
|
44
49
|
)
|
|
@@ -37,10 +37,19 @@ def record_dismissal(root: Path, a: str, b: str, *, path: str = _DEFAULT_PATH) -
|
|
|
37
37
|
|
|
38
38
|
|
|
39
39
|
def load_dismissed(root: Path, *, path: str = _DEFAULT_PATH) -> dict:
|
|
40
|
+
"""Load the dismissal store, treating a corrupt or unreadable store as empty.
|
|
41
|
+
|
|
42
|
+
Mirrors ``record_dismissal``'s guard: invalid JSON, an unreadable file, or a non-dict
|
|
43
|
+
payload all mean "no active dismissals", never a crash.
|
|
44
|
+
"""
|
|
40
45
|
dismissed_file = root / path
|
|
41
46
|
if not dismissed_file.exists():
|
|
42
47
|
return {}
|
|
43
|
-
|
|
48
|
+
try:
|
|
49
|
+
data = json.loads(dismissed_file.read_text())
|
|
50
|
+
except (json.JSONDecodeError, OSError):
|
|
51
|
+
return {}
|
|
52
|
+
return data if isinstance(data, dict) else {}
|
|
44
53
|
|
|
45
54
|
|
|
46
55
|
def active_dismissed_sigs(root: Path, *, path: str = _DEFAULT_PATH) -> set[str]:
|
|
@@ -12,13 +12,22 @@ def to_json(obj) -> str:
|
|
|
12
12
|
return json.dumps(obj)
|
|
13
13
|
|
|
14
14
|
|
|
15
|
+
def _dot_quote(s: str) -> str:
|
|
16
|
+
"""Escape a string for use inside a double-quoted DOT identifier.
|
|
17
|
+
|
|
18
|
+
Backslash must be escaped before the quote so an already-present ``\\`` is not
|
|
19
|
+
double-counted. Paths without special characters are returned unchanged.
|
|
20
|
+
"""
|
|
21
|
+
return s.replace("\\", "\\\\").replace('"', '\\"')
|
|
22
|
+
|
|
23
|
+
|
|
15
24
|
def to_dot(graph: VaultGraph) -> str:
|
|
16
25
|
"""Emit a Graphviz digraph containing every node and directed edge."""
|
|
17
26
|
lines = ["digraph G {"]
|
|
18
27
|
for node in sorted(graph.nodes):
|
|
19
|
-
lines.append(f' "{node}";')
|
|
28
|
+
lines.append(f' "{_dot_quote(node)}";')
|
|
20
29
|
for src in sorted(graph.out_links):
|
|
21
30
|
for dst in sorted(graph.out_links[src]):
|
|
22
|
-
lines.append(f' "{src}" -> "{dst}";')
|
|
31
|
+
lines.append(f' "{_dot_quote(src)}" -> "{_dot_quote(dst)}";')
|
|
23
32
|
lines.append("}")
|
|
24
33
|
return "\n".join(lines)
|
|
@@ -30,6 +30,23 @@ def build_catalog(docs: list[Document]) -> dict[str, list[str]]:
|
|
|
30
30
|
class NormalizeResolver:
|
|
31
31
|
"""Resolves wikilink displays via normalized basename, with path-suffix fallback."""
|
|
32
32
|
|
|
33
|
+
def __init__(self) -> None:
|
|
34
|
+
# Cache the flattened path list per catalog identity. catalog is invariant for a whole
|
|
35
|
+
# VaultGraph.build(), so folder-style links reuse one flatten instead of rebuilding it
|
|
36
|
+
# on every call. Keyed by id() and single-slot: a new catalog evicts the previous.
|
|
37
|
+
self._flat_cache_id: int | None = None
|
|
38
|
+
self._flat_cache: list[str] | None = None
|
|
39
|
+
|
|
40
|
+
@staticmethod
|
|
41
|
+
def _compute_flat(catalog: dict[str, list[str]]) -> list[str]:
|
|
42
|
+
return [p for paths in catalog.values() for p in paths]
|
|
43
|
+
|
|
44
|
+
def _flatten_paths(self, catalog: dict[str, list[str]]) -> list[str]:
|
|
45
|
+
if self._flat_cache_id != id(catalog) or self._flat_cache is None:
|
|
46
|
+
self._flat_cache = self._compute_flat(catalog)
|
|
47
|
+
self._flat_cache_id = id(catalog)
|
|
48
|
+
return self._flat_cache
|
|
49
|
+
|
|
33
50
|
def resolve(self, display: str, catalog: dict[str, list[str]]) -> str | None:
|
|
34
51
|
# Strip alias: "Note|alias" → "Note"
|
|
35
52
|
display = display.split("|")[0]
|
|
@@ -39,7 +56,7 @@ class NormalizeResolver:
|
|
|
39
56
|
if "/" in display:
|
|
40
57
|
# Path-suffix resolution: find unique rel_path ending with "display.md"
|
|
41
58
|
suffix = display.lower() + ".md"
|
|
42
|
-
all_paths =
|
|
59
|
+
all_paths = self._flatten_paths(catalog)
|
|
43
60
|
matches = [p for p in all_paths if p.lower().endswith(suffix)]
|
|
44
61
|
return matches[0] if len(matches) == 1 else None
|
|
45
62
|
|
|
@@ -21,6 +21,17 @@ class LinkExtractor(Protocol):
|
|
|
21
21
|
...
|
|
22
22
|
|
|
23
23
|
|
|
24
|
+
class Similarity(Protocol):
|
|
25
|
+
"""Injected similarity source for ``gaps()`` — the package's similarity seam.
|
|
26
|
+
|
|
27
|
+
graphmark owns the deterministic ranking/filtering; the similarity *source* is supplied by
|
|
28
|
+
the caller. An implementation returns up to ``k`` ``(rel_path, score)`` pairs most similar to
|
|
29
|
+
the note at ``rel_path`` (higher score = more similar). No embeddings ship in this package.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __call__(self, rel_path: str, k: int) -> list[tuple[str, float]]: ...
|
|
33
|
+
|
|
34
|
+
|
|
24
35
|
class Resolver(Protocol):
|
|
25
36
|
"""Resolves a link display to a target note."""
|
|
26
37
|
|
|
@@ -10,6 +10,16 @@ import networkx as nx
|
|
|
10
10
|
|
|
11
11
|
from graphmark.config import VaultConfig
|
|
12
12
|
from graphmark.graph import VaultGraph
|
|
13
|
+
from graphmark.interfaces import Similarity
|
|
14
|
+
|
|
15
|
+
# Validated gaps banding policy — the band proven in daily /connect + /garden use on the owner's
|
|
16
|
+
# live vault (~340 notes). Ships in-package so any consumer gets the tuned band instead of
|
|
17
|
+
# re-deriving policy; consumers opt in by passing these to gaps(). gaps()'s own signature defaults
|
|
18
|
+
# are intentionally left unchanged (non-breaking).
|
|
19
|
+
GAPS_DEFAULT_THRESHOLD = 0.6
|
|
20
|
+
GAPS_DEFAULT_MAX_SCORE = 0.92
|
|
21
|
+
GAPS_DEFAULT_K = 8
|
|
22
|
+
GAPS_DEFAULT_HUB_DEGREE = 40
|
|
13
23
|
|
|
14
24
|
|
|
15
25
|
def _undirected(graph: VaultGraph) -> nx.Graph:
|
|
@@ -85,7 +95,13 @@ def siloed_notes(graph: VaultGraph) -> list[str]:
|
|
|
85
95
|
for bridge in nx.articulation_points(G):
|
|
86
96
|
H = G.copy()
|
|
87
97
|
H.remove_node(bridge)
|
|
88
|
-
|
|
98
|
+
# Largest component is the mainland; the rest are islands. When components tie for
|
|
99
|
+
# largest, break the tie deterministically by member order (the component whose sorted
|
|
100
|
+
# members are lexicographically smallest is the mainland) rather than relying on
|
|
101
|
+
# nx.connected_components' incidental traversal order. NOTE: this tie-break is
|
|
102
|
+
# graphmark's own defined convention — it is unverified against the reference engine,
|
|
103
|
+
# which no frozen fixture exercises; a future divergence is caught by this rule + test.
|
|
104
|
+
components = sorted(nx.connected_components(H), key=lambda c: (-len(c), sorted(c)))
|
|
89
105
|
for component in components[1:]:
|
|
90
106
|
island_nodes.update(component)
|
|
91
107
|
return sorted(island_nodes)
|
|
@@ -133,7 +149,7 @@ def pagerank(graph: VaultGraph, n: int = 10, alpha: float = 0.85) -> list[list]:
|
|
|
133
149
|
|
|
134
150
|
def gaps(
|
|
135
151
|
graph: VaultGraph,
|
|
136
|
-
similar_fn,
|
|
152
|
+
similar_fn: Similarity,
|
|
137
153
|
threshold: float = 0.0,
|
|
138
154
|
k: int = 5,
|
|
139
155
|
note: str | None = None,
|
|
@@ -143,7 +159,12 @@ def gaps(
|
|
|
143
159
|
hub_degree: int | None = None,
|
|
144
160
|
targets: list | None = None,
|
|
145
161
|
) -> list[dict]:
|
|
146
|
-
"""Return link-gap suggestions ranked by novelty (non-hub, cross-folder, score-desc).
|
|
162
|
+
"""Return link-gap suggestions ranked by novelty (non-hub, cross-folder, score-desc).
|
|
163
|
+
|
|
164
|
+
``similar_fn`` is the injected similarity source (see ``interfaces.Similarity``): given a
|
|
165
|
+
``rel_path`` and ``k`` it returns up to ``k`` ``(other_rel_path, score)`` pairs. graphmark
|
|
166
|
+
owns all deterministic ranking/filtering; the similarity source is the caller's.
|
|
167
|
+
"""
|
|
147
168
|
if note is not None:
|
|
148
169
|
target_list = [note]
|
|
149
170
|
elif targets is not None:
|
|
@@ -195,7 +216,13 @@ def gaps(
|
|
|
195
216
|
|
|
196
217
|
|
|
197
218
|
def neighborhood(graph: VaultGraph, note: str, depth: int = 1) -> dict:
|
|
198
|
-
"""Return out/back neighbors (and two_hop when depth>=2) for a note.
|
|
219
|
+
"""Return out/back neighbors (and two_hop when depth>=2) for a note.
|
|
220
|
+
|
|
221
|
+
Raises ``ValueError`` if ``note`` is not a node in the graph, so a typo is distinguishable
|
|
222
|
+
from a genuinely isolated note.
|
|
223
|
+
"""
|
|
224
|
+
if note not in graph.nodes:
|
|
225
|
+
raise ValueError(f"unknown note: {note}")
|
|
199
226
|
out = sorted(graph.out_links.get(note, set()))
|
|
200
227
|
back = sorted(graph.back_links.get(note, set()))
|
|
201
228
|
result: dict = {"note": note, "out": out, "back": back}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Core data model for graphmark.
|
|
2
|
+
|
|
3
|
+
``Document`` is the only model type the engine consumes; the resolved graph is
|
|
4
|
+
``graph.VaultGraph``. Do not rename ``Document``'s type or fields.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
|
|
11
|
+
__all__ = ["Document"]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class Document:
|
|
16
|
+
"""A single note in the vault."""
|
|
17
|
+
|
|
18
|
+
rel_path: str # posix rel-path from vault root, e.g. "brain/North Star.md"
|
|
19
|
+
text: str
|
|
20
|
+
frontmatter: dict
|