graphmark 0.3.2__tar.gz → 0.3.4__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.3.2 → graphmark-0.3.4}/CHANGELOG.md +60 -0
- {graphmark-0.3.2 → graphmark-0.3.4}/PKG-INFO +1 -1
- {graphmark-0.3.2 → graphmark-0.3.4}/pyproject.toml +1 -1
- {graphmark-0.3.2 → graphmark-0.3.4}/src/graphmark/graph.py +54 -11
- {graphmark-0.3.2 → graphmark-0.3.4}/tests/test_graph.py +47 -0
- {graphmark-0.3.2 → graphmark-0.3.4}/tests/test_unresolved.py +108 -5
- {graphmark-0.3.2 → graphmark-0.3.4}/.gitignore +0 -0
- {graphmark-0.3.2 → graphmark-0.3.4}/LICENSE +0 -0
- {graphmark-0.3.2 → graphmark-0.3.4}/README.md +0 -0
- {graphmark-0.3.2 → graphmark-0.3.4}/docs/ROADMAP.md +0 -0
- {graphmark-0.3.2 → graphmark-0.3.4}/src/graphmark/__init__.py +0 -0
- {graphmark-0.3.2 → graphmark-0.3.4}/src/graphmark/check.py +0 -0
- {graphmark-0.3.2 → graphmark-0.3.4}/src/graphmark/cli.py +0 -0
- {graphmark-0.3.2 → graphmark-0.3.4}/src/graphmark/config.py +0 -0
- {graphmark-0.3.2 → graphmark-0.3.4}/src/graphmark/dismiss.py +0 -0
- {graphmark-0.3.2 → graphmark-0.3.4}/src/graphmark/export.py +0 -0
- {graphmark-0.3.2 → graphmark-0.3.4}/src/graphmark/interfaces.py +0 -0
- {graphmark-0.3.2 → graphmark-0.3.4}/src/graphmark/metrics.py +0 -0
- {graphmark-0.3.2 → graphmark-0.3.4}/src/graphmark/model.py +0 -0
- {graphmark-0.3.2 → graphmark-0.3.4}/src/graphmark/parse.py +0 -0
- {graphmark-0.3.2 → graphmark-0.3.4}/src/graphmark/py.typed +0 -0
- {graphmark-0.3.2 → graphmark-0.3.4}/tests/fixtures/README.md +0 -0
- {graphmark-0.3.2 → graphmark-0.3.4}/tests/fixtures/alt/config.toml +0 -0
- {graphmark-0.3.2 → graphmark-0.3.4}/tests/fixtures/alt/expected.json +0 -0
- {graphmark-0.3.2 → graphmark-0.3.4}/tests/fixtures/alt/vault/daily/2026-07-01.md +0 -0
- {graphmark-0.3.2 → graphmark-0.3.4}/tests/fixtures/alt/vault/docs/alpha.md +0 -0
- {graphmark-0.3.2 → graphmark-0.3.4}/tests/fixtures/alt/vault/docs/center.md +0 -0
- {graphmark-0.3.2 → graphmark-0.3.4}/tests/fixtures/alt/vault/misc/echo.md +0 -0
- {graphmark-0.3.2 → graphmark-0.3.4}/tests/fixtures/alt/vault/misc/foxtrot.md +0 -0
- {graphmark-0.3.2 → graphmark-0.3.4}/tests/fixtures/alt/vault/misc/orphan.md +0 -0
- {graphmark-0.3.2 → graphmark-0.3.4}/tests/fixtures/alt/vault/misc/stub.md +0 -0
- {graphmark-0.3.2 → graphmark-0.3.4}/tests/fixtures/alt/vault/refs/beta.md +0 -0
- {graphmark-0.3.2 → graphmark-0.3.4}/tests/fixtures/alt/vault/refs/delta.md +0 -0
- {graphmark-0.3.2 → graphmark-0.3.4}/tests/fixtures/alt/vault/refs/gamma.md +0 -0
- {graphmark-0.3.2 → graphmark-0.3.4}/tests/fixtures/dismiss/expected.json +0 -0
- {graphmark-0.3.2 → graphmark-0.3.4}/tests/fixtures/dismiss/vault/.claude/data/connect-dismissed.json +0 -0
- {graphmark-0.3.2 → graphmark-0.3.4}/tests/fixtures/dismiss/vault/alpha.md +0 -0
- {graphmark-0.3.2 → graphmark-0.3.4}/tests/fixtures/dismiss/vault/beta.md +0 -0
- {graphmark-0.3.2 → graphmark-0.3.4}/tests/fixtures/dismiss/vault/gamma.md +0 -0
- {graphmark-0.3.2 → graphmark-0.3.4}/tests/fixtures/gaps/config.toml +0 -0
- {graphmark-0.3.2 → graphmark-0.3.4}/tests/fixtures/gaps/expected.json +0 -0
- {graphmark-0.3.2 → graphmark-0.3.4}/tests/fixtures/gaps/expected_oog.json +0 -0
- {graphmark-0.3.2 → graphmark-0.3.4}/tests/fixtures/gaps/similar.json +0 -0
- {graphmark-0.3.2 → graphmark-0.3.4}/tests/fixtures/gaps/similar_oog.json +0 -0
- {graphmark-0.3.2 → graphmark-0.3.4}/tests/fixtures/gaps/vault/docs/a.md +0 -0
- {graphmark-0.3.2 → graphmark-0.3.4}/tests/fixtures/gaps/vault/docs/hub.md +0 -0
- {graphmark-0.3.2 → graphmark-0.3.4}/tests/fixtures/gaps/vault/misc/c.md +0 -0
- {graphmark-0.3.2 → graphmark-0.3.4}/tests/fixtures/gaps/vault/misc/d.md +0 -0
- {graphmark-0.3.2 → graphmark-0.3.4}/tests/fixtures/gaps/vault/refs/b.md +0 -0
- {graphmark-0.3.2 → graphmark-0.3.4}/tests/fixtures/gaps/vault/refs/e.md +0 -0
- {graphmark-0.3.2 → graphmark-0.3.4}/tests/fixtures/scoped/config.toml +0 -0
- {graphmark-0.3.2 → graphmark-0.3.4}/tests/fixtures/scoped/expected.json +0 -0
- {graphmark-0.3.2 → graphmark-0.3.4}/tests/fixtures/scoped/vault/docs/one.md +0 -0
- {graphmark-0.3.2 → graphmark-0.3.4}/tests/fixtures/scoped/vault/junk/ignored.md +0 -0
- {graphmark-0.3.2 → graphmark-0.3.4}/tests/fixtures/scoped/vault/misc/excluded.md +0 -0
- {graphmark-0.3.2 → graphmark-0.3.4}/tests/fixtures/scoped/vault/refs/two.md +0 -0
- {graphmark-0.3.2 → graphmark-0.3.4}/tests/fixtures/selflink/config.toml +0 -0
- {graphmark-0.3.2 → graphmark-0.3.4}/tests/fixtures/selflink/expected.json +0 -0
- {graphmark-0.3.2 → graphmark-0.3.4}/tests/fixtures/selflink/vault/a.md +0 -0
- {graphmark-0.3.2 → graphmark-0.3.4}/tests/fixtures/selflink/vault/b.md +0 -0
- {graphmark-0.3.2 → graphmark-0.3.4}/tests/fixtures/simple/config.toml +0 -0
- {graphmark-0.3.2 → graphmark-0.3.4}/tests/fixtures/simple/expected.json +0 -0
- {graphmark-0.3.2 → graphmark-0.3.4}/tests/fixtures/simple/vault/brain/alpha.md +0 -0
- {graphmark-0.3.2 → graphmark-0.3.4}/tests/fixtures/simple/vault/brain/hub.md +0 -0
- {graphmark-0.3.2 → graphmark-0.3.4}/tests/fixtures/simple/vault/personal/beta.md +0 -0
- {graphmark-0.3.2 → graphmark-0.3.4}/tests/fixtures/simple/vault/personal/gamma.md +0 -0
- {graphmark-0.3.2 → graphmark-0.3.4}/tests/fixtures/simple/vault/reference/island.md +0 -0
- {graphmark-0.3.2 → graphmark-0.3.4}/tests/fixtures/simple/vault/reference/stub.md +0 -0
- {graphmark-0.3.2 → graphmark-0.3.4}/tests/test_build_filters.py +0 -0
- {graphmark-0.3.2 → graphmark-0.3.4}/tests/test_check.py +0 -0
- {graphmark-0.3.2 → graphmark-0.3.4}/tests/test_check_policy.py +0 -0
- {graphmark-0.3.2 → graphmark-0.3.4}/tests/test_cli.py +0 -0
- {graphmark-0.3.2 → graphmark-0.3.4}/tests/test_config.py +0 -0
- {graphmark-0.3.2 → graphmark-0.3.4}/tests/test_degenerate.py +0 -0
- {graphmark-0.3.2 → graphmark-0.3.4}/tests/test_dismiss.py +0 -0
- {graphmark-0.3.2 → graphmark-0.3.4}/tests/test_export.py +0 -0
- {graphmark-0.3.2 → graphmark-0.3.4}/tests/test_gaps.py +0 -0
- {graphmark-0.3.2 → graphmark-0.3.4}/tests/test_gaps_banding.py +0 -0
- {graphmark-0.3.2 → graphmark-0.3.4}/tests/test_gaps_oog.py +0 -0
- {graphmark-0.3.2 → graphmark-0.3.4}/tests/test_gaps_params.py +0 -0
- {graphmark-0.3.2 → graphmark-0.3.4}/tests/test_metrics.py +0 -0
- {graphmark-0.3.2 → graphmark-0.3.4}/tests/test_model.py +0 -0
- {graphmark-0.3.2 → graphmark-0.3.4}/tests/test_packaging.py +0 -0
- {graphmark-0.3.2 → graphmark-0.3.4}/tests/test_pagerank.py +0 -0
- {graphmark-0.3.2 → graphmark-0.3.4}/tests/test_parse.py +0 -0
- {graphmark-0.3.2 → graphmark-0.3.4}/tests/test_public_api.py +0 -0
- {graphmark-0.3.2 → graphmark-0.3.4}/tests/test_scoped.py +0 -0
- {graphmark-0.3.2 → graphmark-0.3.4}/tests/test_selflink.py +0 -0
- {graphmark-0.3.2 → graphmark-0.3.4}/tests/test_smoke.py +0 -0
|
@@ -1,6 +1,66 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
3
|
|
|
4
|
+
## v0.3.4 (2026-07-25)
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
- **graph**: Links to existing out-of-scope notes are not broken (#107)
|
|
9
|
+
([#108](https://github.com/cdcoonce/graphmark/pull/108),
|
|
10
|
+
[`9e386f6`](https://github.com/cdcoonce/graphmark/commit/9e386f6df700b1d866b7d50854b0cc8734a08bf5))
|
|
11
|
+
|
|
12
|
+
build() dropped unscoped folders, excluded dirs and rules files from the catalog and then forgot
|
|
13
|
+
they existed, so a link to one failed the resolver and landed in unresolved. The link is correct —
|
|
14
|
+
Obsidian follows it — it just points somewhere graphmark deliberately does not index, so there was
|
|
15
|
+
nothing for anyone to fix. On the live vault that was 11 of 155 reported breaks: 8 [[CLAUDE]], 1
|
|
16
|
+
[[AGENTS]], 2 into templates/.
|
|
17
|
+
|
|
18
|
+
The same rglob that builds the catalog now records what it skipped, so no extra I/O buys the ability
|
|
19
|
+
to tell "exists but out of scope" apart from "exists nowhere". Consulted only AFTER the resolver
|
|
20
|
+
fails — mirroring _targets_non_note_file — so an in-graph note always wins over an out-of-scope
|
|
21
|
+
namesake and keeps its edge. Any candidate suppresses: out-of-scope notes are never link targets,
|
|
22
|
+
so ambiguity among them says nothing about whether the in-graph link is broken.
|
|
23
|
+
|
|
24
|
+
Alias/anchor/.md stripping moved into a shared _strip_display so the resolver and the new check
|
|
25
|
+
cannot drift on what a display names. That refactor also strips surrounding whitespace, which the
|
|
26
|
+
resolver did not: 13 column-aligned links of the form [[folder/note | alias]] were reported broken
|
|
27
|
+
while pointing at real notes, and now resolve into edges (3570 → 3583 on the live vault). Covered
|
|
28
|
+
by its own test class.
|
|
29
|
+
|
|
30
|
+
Fixtures are untouched: only the unresolved path changes shape, no expected.json carries an
|
|
31
|
+
unresolved key, and edges only grow via the whitespace fix. uv.lock catches up to the 0.3.3
|
|
32
|
+
release bump.
|
|
33
|
+
|
|
34
|
+
Live vault: 155 → 130 unresolved (12 suppressed, 13 resolved), no genuine break lost — verified by
|
|
35
|
+
diffing the full before/after sets.
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
## v0.3.3 (2026-07-25)
|
|
39
|
+
|
|
40
|
+
### Bug Fixes
|
|
41
|
+
|
|
42
|
+
- **resolve**: [[note.md]] resolves like [[Note]]
|
|
43
|
+
([#105](https://github.com/cdcoonce/graphmark/pull/105),
|
|
44
|
+
[`916da52`](https://github.com/cdcoonce/graphmark/commit/916da52465cc3c5295a9440ae86af1ee6065e1d5))
|
|
45
|
+
|
|
46
|
+
Obsidian accepts an explicit .md extension, so [[Note.md]] and [[Note]] are the same link. graphmark
|
|
47
|
+
resolved only the second: the normalizer turned "Note.md" into the key "note md" while the file's
|
|
48
|
+
catalog key is "note", so the link never matched and was reported as broken. The path-suffix
|
|
49
|
+
branch was worse — it appends ".md" itself, so [[folder/note.md]] searched for folder/note.md.md.
|
|
50
|
+
|
|
51
|
+
Strip a case-insensitive trailing ".md" after alias/anchor stripping and before both resolution
|
|
52
|
+
branches. A missing [[Nowhere.md]] stays unresolved and an ambiguous stem stays ambiguous; a title
|
|
53
|
+
merely ending in the word "MD" is untouched.
|
|
54
|
+
|
|
55
|
+
Parity-safe: no frozen fixture uses a .md-style link, so every expected.json is unchanged (81 oracle
|
|
56
|
+
tests green). The change only adds resolutions that previously failed — it never removes one.
|
|
57
|
+
|
|
58
|
+
Also updates the test added in #101 that documented this gap as current behavior; it now asserts the
|
|
59
|
+
fix.
|
|
60
|
+
|
|
61
|
+
Closes #104
|
|
62
|
+
|
|
63
|
+
|
|
4
64
|
## v0.3.2 (2026-07-25)
|
|
5
65
|
|
|
6
66
|
### Bug Fixes
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: graphmark
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.4
|
|
4
4
|
Summary: Deterministic knowledge-graph analysis for markdown / [[wikilink]] vaults.
|
|
5
5
|
Project-URL: Homepage, https://github.com/cdcoonce/graphmark
|
|
6
6
|
Project-URL: Repository, https://github.com/cdcoonce/graphmark
|
|
@@ -25,6 +25,19 @@ def _normalize(text: str) -> str:
|
|
|
25
25
|
_FILE_SUFFIX_RE = re.compile(r"\.[A-Za-z0-9]{1,10}$")
|
|
26
26
|
|
|
27
27
|
|
|
28
|
+
def _strip_display(display: str) -> str:
|
|
29
|
+
"""Reduce a raw wikilink display to its note part: no alias, no anchor, no ``.md``.
|
|
30
|
+
|
|
31
|
+
Obsidian treats ``[[Note]]``, ``[[Note|alias]]``, ``[[Note#Section]]`` and ``[[Note.md]]`` as
|
|
32
|
+
the same link, so every place that asks "which note does this display name?" must strip the
|
|
33
|
+
same three things. Shared by the resolver and the out-of-scope check so they cannot drift.
|
|
34
|
+
"""
|
|
35
|
+
target = display.split("|")[0].split("#")[0].strip()
|
|
36
|
+
if target.lower().endswith(".md"):
|
|
37
|
+
target = target[: -len(".md")]
|
|
38
|
+
return target
|
|
39
|
+
|
|
40
|
+
|
|
28
41
|
def _targets_non_note_file(display: str) -> bool:
|
|
29
42
|
"""True for a link to a file graphmark does not index, e.g. ``[[Board.canvas]]``.
|
|
30
43
|
|
|
@@ -40,6 +53,28 @@ def _targets_non_note_file(display: str) -> bool:
|
|
|
40
53
|
return bool(match) and match.group(0).lower() != ".md"
|
|
41
54
|
|
|
42
55
|
|
|
56
|
+
def _targets_out_of_scope_note(display: str, out_of_scope: dict[str, list[str]]) -> bool:
|
|
57
|
+
"""True for a link to a markdown note that exists but is outside the configured scope.
|
|
58
|
+
|
|
59
|
+
``build`` drops unscoped folders, excluded dirs and rules files from the catalog, so links to
|
|
60
|
+
them fail the resolver — yet the link is correct, Obsidian follows it, and there is nothing for
|
|
61
|
+
anyone to fix. Reporting it as broken just fills the vault-health count with noise.
|
|
62
|
+
|
|
63
|
+
Only ever consulted after the resolver has already failed, so an in-graph note always wins over
|
|
64
|
+
an out-of-scope namesake. **Any** candidate suppresses: out-of-scope notes are never link
|
|
65
|
+
targets, so ambiguity among them says nothing about whether the in-graph link is broken.
|
|
66
|
+
"""
|
|
67
|
+
target = _strip_display(display)
|
|
68
|
+
if not target:
|
|
69
|
+
return False
|
|
70
|
+
if "/" in target:
|
|
71
|
+
suffix = target.lower() + ".md"
|
|
72
|
+
return any(
|
|
73
|
+
path.lower().endswith(suffix) for paths in out_of_scope.values() for path in paths
|
|
74
|
+
)
|
|
75
|
+
return _normalize(target) in out_of_scope
|
|
76
|
+
|
|
77
|
+
|
|
43
78
|
def _is_intra_note_reference(display: str) -> bool:
|
|
44
79
|
"""True for a link that targets no note, e.g. ``[[#Heading]]`` or ``[[#^block]]``.
|
|
45
80
|
|
|
@@ -80,10 +115,10 @@ class NormalizeResolver:
|
|
|
80
115
|
return self._flat_cache
|
|
81
116
|
|
|
82
117
|
def resolve(self, display: str, catalog: dict[str, list[str]]) -> str | None:
|
|
83
|
-
#
|
|
84
|
-
|
|
85
|
-
#
|
|
86
|
-
display = display
|
|
118
|
+
# Alias ("Note|alias"), anchor ("Note#Section") and an explicit ".md" extension all name
|
|
119
|
+
# the same note. Stripped before both branches below: the path-suffix branch appends
|
|
120
|
+
# ".md" itself, and the bare branch would otherwise normalize to the key "note md".
|
|
121
|
+
display = _strip_display(display)
|
|
87
122
|
|
|
88
123
|
if "/" in display:
|
|
89
124
|
# Path-suffix resolution: find unique rel_path ending with "display.md"
|
|
@@ -137,13 +172,19 @@ class VaultGraph:
|
|
|
137
172
|
|
|
138
173
|
scoped = set(config.scoped_folders)
|
|
139
174
|
md_files: list[Path] = []
|
|
175
|
+
# Markdown that exists but is out of scope, normalized stem → rel_paths. Collected in
|
|
176
|
+
# this same walk (no extra I/O) so a link to one can be told apart from a link to a note
|
|
177
|
+
# that exists nowhere at all.
|
|
178
|
+
out_of_scope: dict[str, list[str]] = {}
|
|
140
179
|
for path in sorted(root.rglob("*.md")):
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
180
|
+
rel = path.relative_to(root)
|
|
181
|
+
rel_parts = rel.parts
|
|
182
|
+
if (
|
|
183
|
+
(scoped and rel_parts[0] not in scoped)
|
|
184
|
+
or any(p in excluded for p in rel_parts[:-1])
|
|
185
|
+
or path.name in rules
|
|
186
|
+
):
|
|
187
|
+
out_of_scope.setdefault(_normalize(path.stem), []).append(rel.as_posix())
|
|
147
188
|
continue
|
|
148
189
|
md_files.append(path)
|
|
149
190
|
|
|
@@ -162,7 +203,9 @@ class VaultGraph:
|
|
|
162
203
|
continue
|
|
163
204
|
target = resolver.resolve(display, catalog)
|
|
164
205
|
if target is None:
|
|
165
|
-
if _targets_non_note_file(display)
|
|
206
|
+
if _targets_non_note_file(display) or _targets_out_of_scope_note(
|
|
207
|
+
display, out_of_scope
|
|
208
|
+
):
|
|
166
209
|
continue # out of scope, not a broken note link
|
|
167
210
|
# Unresolvable OR ambiguous — the Resolver protocol conflates the two, and
|
|
168
211
|
# both are equally broken from a vault-health view. Record the raw display
|
|
@@ -190,3 +190,50 @@ class TestVaultGraphBuild:
|
|
|
190
190
|
assert degree("personal/gamma.md") == 2
|
|
191
191
|
assert degree("reference/island.md") == 0
|
|
192
192
|
assert degree("reference/stub.md") == 0
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
class TestMarkdownExtensionInLinks:
|
|
196
|
+
"""Obsidian treats [[Note.md]] and [[Note]] as the same link; so must the resolver."""
|
|
197
|
+
|
|
198
|
+
def setup_method(self):
|
|
199
|
+
self.resolver = NormalizeResolver()
|
|
200
|
+
|
|
201
|
+
def _catalog(self, *rel_paths: str) -> dict[str, list[str]]:
|
|
202
|
+
return build_catalog([_doc(p) for p in rel_paths])
|
|
203
|
+
|
|
204
|
+
def test_bare_link_with_md_extension_resolves(self):
|
|
205
|
+
catalog = self._catalog("brain/alpha.md")
|
|
206
|
+
assert self.resolver.resolve("alpha.md", catalog) == "brain/alpha.md"
|
|
207
|
+
|
|
208
|
+
def test_extension_match_is_case_insensitive(self):
|
|
209
|
+
catalog = self._catalog("brain/alpha.md")
|
|
210
|
+
assert self.resolver.resolve("Alpha.MD", catalog) == "brain/alpha.md"
|
|
211
|
+
|
|
212
|
+
def test_md_extension_with_alias_resolves(self):
|
|
213
|
+
catalog = self._catalog("brain/alpha.md")
|
|
214
|
+
assert self.resolver.resolve("alpha.md|the first note", catalog) == "brain/alpha.md"
|
|
215
|
+
|
|
216
|
+
def test_md_extension_with_anchor_resolves(self):
|
|
217
|
+
catalog = self._catalog("brain/alpha.md")
|
|
218
|
+
assert self.resolver.resolve("alpha.md#Section", catalog) == "brain/alpha.md"
|
|
219
|
+
|
|
220
|
+
def test_path_style_link_with_md_extension_resolves(self):
|
|
221
|
+
catalog = self._catalog("brain/alpha.md", "personal/beta.md")
|
|
222
|
+
assert self.resolver.resolve("brain/alpha.md", catalog) == "brain/alpha.md"
|
|
223
|
+
|
|
224
|
+
def test_missing_target_with_md_extension_stays_unresolved(self):
|
|
225
|
+
catalog = self._catalog("brain/alpha.md")
|
|
226
|
+
assert self.resolver.resolve("nowhere.md", catalog) is None
|
|
227
|
+
|
|
228
|
+
def test_ambiguous_stem_stays_ambiguous_with_the_extension(self):
|
|
229
|
+
catalog = {"note": ["dir1/note.md", "dir2/note.md"]}
|
|
230
|
+
assert self.resolver.resolve("note.md", catalog) is None
|
|
231
|
+
|
|
232
|
+
def test_extensionless_links_are_unaffected(self):
|
|
233
|
+
catalog = self._catalog("brain/alpha.md")
|
|
234
|
+
assert self.resolver.resolve("alpha", catalog) == "brain/alpha.md"
|
|
235
|
+
|
|
236
|
+
def test_a_title_ending_in_md_as_a_word_is_not_stripped(self):
|
|
237
|
+
# "Doctor MD" ends with "md" but not ".md" — it must not be truncated.
|
|
238
|
+
catalog = self._catalog("org/Doctor MD.md")
|
|
239
|
+
assert self.resolver.resolve("Doctor MD", catalog) == "org/Doctor MD.md"
|
|
@@ -204,13 +204,13 @@ class TestNonMarkdownTargets:
|
|
|
204
204
|
assert graph.out_links["a.md"] == set()
|
|
205
205
|
|
|
206
206
|
def test_md_targets_are_exempt_from_the_rule(self, tmp_path):
|
|
207
|
-
# .md is
|
|
208
|
-
#
|
|
209
|
-
# ".md" (an Obsidian-compatibility gap tracked separately) — the point here is only
|
|
210
|
-
# that this rule does not silently swallow it.
|
|
207
|
+
# .md is excluded from the non-note rule, so a link ending in .md is left to the
|
|
208
|
+
# resolver — which now resolves it (a trailing ".md" is stripped, matching Obsidian).
|
|
211
209
|
_write(tmp_path, "a.md", "See [[b.md]].\n")
|
|
212
210
|
_write(tmp_path, "b.md", "Target.\n")
|
|
213
|
-
|
|
211
|
+
graph = _build(tmp_path)
|
|
212
|
+
assert graph.unresolved == {}
|
|
213
|
+
assert graph.out_links["a.md"] == {"b.md"}
|
|
214
214
|
|
|
215
215
|
def test_missing_md_target_is_still_unresolved(self, tmp_path):
|
|
216
216
|
_write(tmp_path, "a.md", "See [[Nowhere.md]].\n")
|
|
@@ -233,3 +233,106 @@ class TestNonMarkdownTargets:
|
|
|
233
233
|
def test_mixed_note_keeps_only_the_missing_note(self, tmp_path):
|
|
234
234
|
_write(tmp_path, "a.md", "[[Chart.base]] and [[Missing]] and [[#local]].\n")
|
|
235
235
|
assert _build(tmp_path).unresolved == {"a.md": ["Missing"]}
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
class TestPaddedDisplays:
|
|
239
|
+
"""Whitespace around the note part must not stop a link from resolving.
|
|
240
|
+
|
|
241
|
+
Real vaults contain column-aligned tables — ``[[folder/note | alias]]`` — where the
|
|
242
|
+
note part carries trailing spaces. Both resolution branches now see the stripped display
|
|
243
|
+
(13 such links on a live vault were reported broken while pointing at real notes).
|
|
244
|
+
"""
|
|
245
|
+
|
|
246
|
+
def test_padded_bare_display_resolves(self, tmp_path):
|
|
247
|
+
_write(tmp_path, "a.md", "See [[ b | the alias]].\n")
|
|
248
|
+
_write(tmp_path, "b.md", "Target.\n")
|
|
249
|
+
graph = _build(tmp_path)
|
|
250
|
+
assert graph.unresolved == {}
|
|
251
|
+
assert graph.out_links["a.md"] == {"b.md"}
|
|
252
|
+
|
|
253
|
+
def test_padded_path_qualified_display_resolves(self, tmp_path):
|
|
254
|
+
_write(tmp_path, "a.md", "See [[docs/b | the alias]].\n")
|
|
255
|
+
_write(tmp_path, "docs/b.md", "Target.\n")
|
|
256
|
+
graph = _build(tmp_path)
|
|
257
|
+
assert graph.unresolved == {}
|
|
258
|
+
assert graph.out_links["a.md"] == {"docs/b.md"}
|
|
259
|
+
|
|
260
|
+
def test_padded_md_extension_still_strips(self, tmp_path):
|
|
261
|
+
_write(tmp_path, "a.md", "See [[b.md | alias]].\n")
|
|
262
|
+
_write(tmp_path, "b.md", "Target.\n")
|
|
263
|
+
assert _build(tmp_path).out_links["a.md"] == {"b.md"}
|
|
264
|
+
|
|
265
|
+
def test_a_padded_missing_note_is_still_unresolved(self, tmp_path):
|
|
266
|
+
_write(tmp_path, "a.md", "See [[ Nowhere | alias]].\n")
|
|
267
|
+
assert _build(tmp_path).unresolved == {"a.md": [" Nowhere | alias"]}
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
class TestOutOfScopeNoteTargets:
|
|
271
|
+
"""A link to a note that exists but is out of graph scope is out of scope, not broken.
|
|
272
|
+
|
|
273
|
+
build() drops unscoped folders, excluded dirs and rules files from the catalog and then
|
|
274
|
+
forgets they exist, so links to them fail the resolver. The link is correct — Obsidian
|
|
275
|
+
follows it — it just points somewhere graphmark deliberately does not index. On a real
|
|
276
|
+
vault these were 7% of the reported total ([[CLAUDE]], [[AGENTS]], a templates/ note).
|
|
277
|
+
|
|
278
|
+
Consulted only AFTER the resolver fails, so nothing that resolves in-graph is affected.
|
|
279
|
+
"""
|
|
280
|
+
|
|
281
|
+
def test_link_to_a_rules_file_is_not_unresolved(self, tmp_path):
|
|
282
|
+
_write(tmp_path, "notes/a.md", "See [[CLAUDE]].\n")
|
|
283
|
+
_write(tmp_path, "CLAUDE.md", "Rules.\n")
|
|
284
|
+
graph = _build(tmp_path, rules_files=["CLAUDE.md"])
|
|
285
|
+
assert graph.unresolved == {}
|
|
286
|
+
assert graph.out_links["notes/a.md"] == set()
|
|
287
|
+
|
|
288
|
+
def test_link_into_an_unscoped_folder_is_not_unresolved(self, tmp_path):
|
|
289
|
+
_write(tmp_path, "docs/a.md", "See [[Intake Guide]].\n")
|
|
290
|
+
_write(tmp_path, "templates/Intake Guide.md", "Template.\n")
|
|
291
|
+
assert _build(tmp_path, scoped_folders=["docs"]).unresolved == {}
|
|
292
|
+
|
|
293
|
+
def test_link_into_an_excluded_dir_is_not_unresolved(self, tmp_path):
|
|
294
|
+
_write(tmp_path, "a.md", "See [[old thing]].\n")
|
|
295
|
+
_write(tmp_path, "archive/old thing.md", "Archived.\n")
|
|
296
|
+
assert _build(tmp_path, excluded_dirs=["archive"]).unresolved == {}
|
|
297
|
+
|
|
298
|
+
@pytest.mark.parametrize(
|
|
299
|
+
"display",
|
|
300
|
+
["CLAUDE", "claude", "CLAUDE|the rules", "CLAUDE#Non-negotiables", "CLAUDE.md"],
|
|
301
|
+
)
|
|
302
|
+
def test_alias_anchor_case_and_md_forms_all_suppress(self, tmp_path, display):
|
|
303
|
+
_write(tmp_path, "notes/a.md", f"See [[{display}]].\n")
|
|
304
|
+
_write(tmp_path, "CLAUDE.md", "Rules.\n")
|
|
305
|
+
assert _build(tmp_path, rules_files=["CLAUDE.md"]).unresolved == {}
|
|
306
|
+
|
|
307
|
+
def test_path_qualified_link_to_an_out_of_scope_note_is_not_unresolved(self, tmp_path):
|
|
308
|
+
_write(tmp_path, "docs/a.md", "See [[templates/data/Intake Guide]].\n")
|
|
309
|
+
_write(tmp_path, "templates/data/Intake Guide.md", "Template.\n")
|
|
310
|
+
assert _build(tmp_path, scoped_folders=["docs"]).unresolved == {}
|
|
311
|
+
|
|
312
|
+
def test_an_ambiguous_out_of_scope_stem_still_suppresses(self, tmp_path):
|
|
313
|
+
# Out-of-scope notes are never link targets, so ambiguity among them says nothing
|
|
314
|
+
# about whether the in-graph link is broken. Any candidate suppresses.
|
|
315
|
+
_write(tmp_path, "docs/a.md", "See [[shared]].\n")
|
|
316
|
+
_write(tmp_path, "templates/one/shared.md", "")
|
|
317
|
+
_write(tmp_path, "templates/two/shared.md", "")
|
|
318
|
+
assert _build(tmp_path, scoped_folders=["docs"]).unresolved == {}
|
|
319
|
+
|
|
320
|
+
def test_a_note_that_exists_nowhere_is_still_unresolved(self, tmp_path):
|
|
321
|
+
_write(tmp_path, "docs/a.md", "See [[Nowhere]].\n")
|
|
322
|
+
_write(tmp_path, "templates/Something Else.md", "")
|
|
323
|
+
assert _build(tmp_path, scoped_folders=["docs"]).unresolved == {"docs/a.md": ["Nowhere"]}
|
|
324
|
+
|
|
325
|
+
def test_an_in_scope_note_still_wins_over_an_out_of_scope_namesake(self, tmp_path):
|
|
326
|
+
# The rule only applies after the resolver fails, so the real in-graph note wins and
|
|
327
|
+
# keeps its edge.
|
|
328
|
+
_write(tmp_path, "docs/a.md", "See [[guide]].\n")
|
|
329
|
+
_write(tmp_path, "docs/guide.md", "In scope.\n")
|
|
330
|
+
_write(tmp_path, "templates/guide.md", "Out of scope.\n")
|
|
331
|
+
graph = _build(tmp_path, scoped_folders=["docs"])
|
|
332
|
+
assert graph.unresolved == {}
|
|
333
|
+
assert graph.out_links["docs/a.md"] == {"docs/guide.md"}
|
|
334
|
+
|
|
335
|
+
def test_mixed_note_keeps_only_the_real_break(self, tmp_path):
|
|
336
|
+
_write(tmp_path, "docs/a.md", "[[Intake Guide]] and [[Missing]].\n")
|
|
337
|
+
_write(tmp_path, "templates/Intake Guide.md", "Template.\n")
|
|
338
|
+
assert _build(tmp_path, scoped_folders=["docs"]).unresolved == {"docs/a.md": ["Missing"]}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{graphmark-0.3.2 → graphmark-0.3.4}/tests/fixtures/dismiss/vault/.claude/data/connect-dismissed.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|