graphmark 0.2.0__tar.gz → 0.3.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.
Files changed (95) hide show
  1. graphmark-0.3.0/CHANGELOG.md +746 -0
  2. graphmark-0.3.0/PKG-INFO +216 -0
  3. graphmark-0.3.0/README.md +189 -0
  4. {graphmark-0.2.0 → graphmark-0.3.0}/pyproject.toml +11 -2
  5. graphmark-0.3.0/src/graphmark/__init__.py +118 -0
  6. graphmark-0.3.0/src/graphmark/check.py +65 -0
  7. graphmark-0.3.0/src/graphmark/cli.py +158 -0
  8. graphmark-0.3.0/src/graphmark/config.py +122 -0
  9. {graphmark-0.2.0 → graphmark-0.3.0}/src/graphmark/export.py +1 -1
  10. {graphmark-0.2.0 → graphmark-0.3.0}/src/graphmark/graph.py +21 -3
  11. {graphmark-0.2.0 → graphmark-0.3.0}/src/graphmark/metrics.py +41 -5
  12. {graphmark-0.2.0 → graphmark-0.3.0}/src/graphmark/parse.py +4 -1
  13. graphmark-0.3.0/src/graphmark/py.typed +0 -0
  14. graphmark-0.3.0/tests/test_build_filters.py +139 -0
  15. graphmark-0.3.0/tests/test_check.py +184 -0
  16. graphmark-0.3.0/tests/test_check_policy.py +95 -0
  17. {graphmark-0.2.0 → graphmark-0.3.0}/tests/test_cli.py +146 -0
  18. {graphmark-0.2.0 → graphmark-0.3.0}/tests/test_config.py +30 -0
  19. graphmark-0.3.0/tests/test_degenerate.py +176 -0
  20. {graphmark-0.2.0 → graphmark-0.3.0}/tests/test_dismiss.py +46 -0
  21. graphmark-0.3.0/tests/test_gaps_banding.py +88 -0
  22. graphmark-0.3.0/tests/test_gaps_params.py +154 -0
  23. {graphmark-0.2.0 → graphmark-0.3.0}/tests/test_graph.py +31 -0
  24. graphmark-0.3.0/tests/test_packaging.py +77 -0
  25. {graphmark-0.2.0 → graphmark-0.3.0}/tests/test_pagerank.py +38 -0
  26. {graphmark-0.2.0 → graphmark-0.3.0}/tests/test_parse.py +47 -0
  27. graphmark-0.3.0/tests/test_public_api.py +140 -0
  28. graphmark-0.3.0/tests/test_unresolved.py +132 -0
  29. graphmark-0.2.0/PKG-INFO +0 -112
  30. graphmark-0.2.0/README.md +0 -86
  31. graphmark-0.2.0/src/graphmark/__init__.py +0 -8
  32. graphmark-0.2.0/src/graphmark/cli.py +0 -113
  33. graphmark-0.2.0/src/graphmark/config.py +0 -49
  34. graphmark-0.2.0/tests/test_gaps_banding.py +0 -29
  35. {graphmark-0.2.0 → graphmark-0.3.0}/.gitignore +0 -0
  36. {graphmark-0.2.0 → graphmark-0.3.0}/LICENSE +0 -0
  37. {graphmark-0.2.0 → graphmark-0.3.0}/docs/ROADMAP.md +0 -0
  38. {graphmark-0.2.0 → graphmark-0.3.0}/src/graphmark/dismiss.py +0 -0
  39. {graphmark-0.2.0 → graphmark-0.3.0}/src/graphmark/interfaces.py +0 -0
  40. {graphmark-0.2.0 → graphmark-0.3.0}/src/graphmark/model.py +0 -0
  41. {graphmark-0.2.0 → graphmark-0.3.0}/tests/fixtures/README.md +0 -0
  42. {graphmark-0.2.0 → graphmark-0.3.0}/tests/fixtures/alt/config.toml +0 -0
  43. {graphmark-0.2.0 → graphmark-0.3.0}/tests/fixtures/alt/expected.json +0 -0
  44. {graphmark-0.2.0 → graphmark-0.3.0}/tests/fixtures/alt/vault/daily/2026-07-01.md +0 -0
  45. {graphmark-0.2.0 → graphmark-0.3.0}/tests/fixtures/alt/vault/docs/alpha.md +0 -0
  46. {graphmark-0.2.0 → graphmark-0.3.0}/tests/fixtures/alt/vault/docs/center.md +0 -0
  47. {graphmark-0.2.0 → graphmark-0.3.0}/tests/fixtures/alt/vault/misc/echo.md +0 -0
  48. {graphmark-0.2.0 → graphmark-0.3.0}/tests/fixtures/alt/vault/misc/foxtrot.md +0 -0
  49. {graphmark-0.2.0 → graphmark-0.3.0}/tests/fixtures/alt/vault/misc/orphan.md +0 -0
  50. {graphmark-0.2.0 → graphmark-0.3.0}/tests/fixtures/alt/vault/misc/stub.md +0 -0
  51. {graphmark-0.2.0 → graphmark-0.3.0}/tests/fixtures/alt/vault/refs/beta.md +0 -0
  52. {graphmark-0.2.0 → graphmark-0.3.0}/tests/fixtures/alt/vault/refs/delta.md +0 -0
  53. {graphmark-0.2.0 → graphmark-0.3.0}/tests/fixtures/alt/vault/refs/gamma.md +0 -0
  54. {graphmark-0.2.0 → graphmark-0.3.0}/tests/fixtures/dismiss/expected.json +0 -0
  55. {graphmark-0.2.0 → graphmark-0.3.0}/tests/fixtures/dismiss/vault/.claude/data/connect-dismissed.json +0 -0
  56. {graphmark-0.2.0 → graphmark-0.3.0}/tests/fixtures/dismiss/vault/alpha.md +0 -0
  57. {graphmark-0.2.0 → graphmark-0.3.0}/tests/fixtures/dismiss/vault/beta.md +0 -0
  58. {graphmark-0.2.0 → graphmark-0.3.0}/tests/fixtures/dismiss/vault/gamma.md +0 -0
  59. {graphmark-0.2.0 → graphmark-0.3.0}/tests/fixtures/gaps/config.toml +0 -0
  60. {graphmark-0.2.0 → graphmark-0.3.0}/tests/fixtures/gaps/expected.json +0 -0
  61. {graphmark-0.2.0 → graphmark-0.3.0}/tests/fixtures/gaps/expected_oog.json +0 -0
  62. {graphmark-0.2.0 → graphmark-0.3.0}/tests/fixtures/gaps/similar.json +0 -0
  63. {graphmark-0.2.0 → graphmark-0.3.0}/tests/fixtures/gaps/similar_oog.json +0 -0
  64. {graphmark-0.2.0 → graphmark-0.3.0}/tests/fixtures/gaps/vault/docs/a.md +0 -0
  65. {graphmark-0.2.0 → graphmark-0.3.0}/tests/fixtures/gaps/vault/docs/hub.md +0 -0
  66. {graphmark-0.2.0 → graphmark-0.3.0}/tests/fixtures/gaps/vault/misc/c.md +0 -0
  67. {graphmark-0.2.0 → graphmark-0.3.0}/tests/fixtures/gaps/vault/misc/d.md +0 -0
  68. {graphmark-0.2.0 → graphmark-0.3.0}/tests/fixtures/gaps/vault/refs/b.md +0 -0
  69. {graphmark-0.2.0 → graphmark-0.3.0}/tests/fixtures/gaps/vault/refs/e.md +0 -0
  70. {graphmark-0.2.0 → graphmark-0.3.0}/tests/fixtures/scoped/config.toml +0 -0
  71. {graphmark-0.2.0 → graphmark-0.3.0}/tests/fixtures/scoped/expected.json +0 -0
  72. {graphmark-0.2.0 → graphmark-0.3.0}/tests/fixtures/scoped/vault/docs/one.md +0 -0
  73. {graphmark-0.2.0 → graphmark-0.3.0}/tests/fixtures/scoped/vault/junk/ignored.md +0 -0
  74. {graphmark-0.2.0 → graphmark-0.3.0}/tests/fixtures/scoped/vault/misc/excluded.md +0 -0
  75. {graphmark-0.2.0 → graphmark-0.3.0}/tests/fixtures/scoped/vault/refs/two.md +0 -0
  76. {graphmark-0.2.0 → graphmark-0.3.0}/tests/fixtures/selflink/config.toml +0 -0
  77. {graphmark-0.2.0 → graphmark-0.3.0}/tests/fixtures/selflink/expected.json +0 -0
  78. {graphmark-0.2.0 → graphmark-0.3.0}/tests/fixtures/selflink/vault/a.md +0 -0
  79. {graphmark-0.2.0 → graphmark-0.3.0}/tests/fixtures/selflink/vault/b.md +0 -0
  80. {graphmark-0.2.0 → graphmark-0.3.0}/tests/fixtures/simple/config.toml +0 -0
  81. {graphmark-0.2.0 → graphmark-0.3.0}/tests/fixtures/simple/expected.json +0 -0
  82. {graphmark-0.2.0 → graphmark-0.3.0}/tests/fixtures/simple/vault/brain/alpha.md +0 -0
  83. {graphmark-0.2.0 → graphmark-0.3.0}/tests/fixtures/simple/vault/brain/hub.md +0 -0
  84. {graphmark-0.2.0 → graphmark-0.3.0}/tests/fixtures/simple/vault/personal/beta.md +0 -0
  85. {graphmark-0.2.0 → graphmark-0.3.0}/tests/fixtures/simple/vault/personal/gamma.md +0 -0
  86. {graphmark-0.2.0 → graphmark-0.3.0}/tests/fixtures/simple/vault/reference/island.md +0 -0
  87. {graphmark-0.2.0 → graphmark-0.3.0}/tests/fixtures/simple/vault/reference/stub.md +0 -0
  88. {graphmark-0.2.0 → graphmark-0.3.0}/tests/test_export.py +0 -0
  89. {graphmark-0.2.0 → graphmark-0.3.0}/tests/test_gaps.py +0 -0
  90. {graphmark-0.2.0 → graphmark-0.3.0}/tests/test_gaps_oog.py +0 -0
  91. {graphmark-0.2.0 → graphmark-0.3.0}/tests/test_metrics.py +0 -0
  92. {graphmark-0.2.0 → graphmark-0.3.0}/tests/test_model.py +0 -0
  93. {graphmark-0.2.0 → graphmark-0.3.0}/tests/test_scoped.py +0 -0
  94. {graphmark-0.2.0 → graphmark-0.3.0}/tests/test_selflink.py +0 -0
  95. {graphmark-0.2.0 → graphmark-0.3.0}/tests/test_smoke.py +0 -0
@@ -0,0 +1,746 @@
1
+ # CHANGELOG
2
+
3
+
4
+ ## v0.3.0 (2026-07-25)
5
+
6
+ ### Bug Fixes
7
+
8
+ - Validate the vault root and handle config errors cleanly in the CLI
9
+ ([#81](https://github.com/cdcoonce/graphmark/pull/81),
10
+ [`7aaae1d`](https://github.com/cdcoonce/graphmark/commit/7aaae1d08530e2996636432a661180984c2221c6))
11
+
12
+ Three failures at the same boundary:
13
+
14
+ 1. rglob on a missing dir silently yields nothing, so a typo'd --root produced a structurally valid
15
+ zero-metrics graph with exit 0 — indistinguishable from an empty vault. VaultGraph.build now
16
+ raises ValueError when root is not a directory. An existing-but-empty vault stays legitimate. 2.
17
+ cli._load caught nothing: a missing config file, malformed TOML, or a config missing 'root' each
18
+ dumped a raw traceback. It now catches OSError / TOMLDecodeError / ValueError and exits 2 with a
19
+ one-line stderr message, matching the existing error convention. 3. load_config raised for a
20
+ missing 'root' key BEFORE the --root override applied, so the shipped configs/my-brain.toml
21
+ (policy-only, no root key) was unusable from the package's own CLI. load_config grows a
22
+ keyword-only root_override; when given it wins over any root key and makes that key optional, so
23
+ the override is applied during the load rather than patched on after.
24
+
25
+ Closes #64
26
+
27
+ - **cli**: Unify usage errors on exit 2; keep help off stdout
28
+ ([#89](https://github.com/cdcoonce/graphmark/pull/89),
29
+ [`a5c208d`](https://github.com/cdcoonce/graphmark/commit/a5c208d1c8be2033870605341b6c898bb6b78d87))
30
+
31
+ Three conflicting conventions coexisted: missing --config/--root exited 1 via a hand-rolled print, a
32
+ bare 'graphmark' printed full help to STDOUT while exiting 1 (success-shaped output with a
33
+ failure-shaped code — piping captured help as if it were data), while argparse's own errors and
34
+ the CLI's deliberate error paths exited 2.
35
+
36
+ Unify on argparse's convention: the missing-source check moves into main() as parser.error(), and a
37
+ missing command prints help to stderr and exits 2. One rule now holds — 0 success, 2 usage error —
38
+ which also leaves exit 1 free for the domain outcome Track D reserves it for (a "check" threshold
39
+ breach).
40
+
41
+ Closes #72
42
+
43
+ - **gaps**: Single-source the weaklink signature format
44
+ ([#91](https://github.com/cdcoonce/graphmark/pull/91),
45
+ [`dd695e2`](https://github.com/cdcoonce/graphmark/commit/dd695e2b953154321c7c53d1618f6d7ea01d8ae5))
46
+
47
+ The dismissal-signature format was written out twice — inline in metrics.gaps() and again as
48
+ dismiss.weaklink_sig() — with nothing tying them together. The format is load-bearing across both
49
+ modules: gaps() emits sig, callers persist it via record_dismissal, and feed active_dismissed_sigs
50
+ back in as dismissed=. A drift in either definition would silently un-suppress every recorded
51
+ dismissal, since the mismatch produces no error, just suggestions reappearing.
52
+
53
+ metrics now imports and calls dismiss.weaklink_sig. The import direction is engine -> store, but
54
+ weaklink_sig is a pure string helper (no IO, no extra deps, no graph knowledge) and dismiss
55
+ imports nothing from metrics, so no cycle and no real layering violation.
56
+
57
+ Add round-trip tests covering the loop end to end: a gaps()-emitted sig equals weaklink_sig(), and
58
+ suggest -> record_dismissal -> active_dismissed_sigs -> gaps(dismissed=...) actually suppresses
59
+ the suggestion.
60
+
61
+ Drive-by: restore the comment ordering in metrics.py, where the gaps banding comment had been
62
+ orphaned above _MAX_ITER.
63
+
64
+ Closes #74
65
+
66
+ - **pagerank**: Validate alpha and raise on non-convergence
67
+ ([#82](https://github.com/cdcoonce/graphmark/pull/82),
68
+ [`6b9dcaa`](https://github.com/cdcoonce/graphmark/commit/6b9dcaa544448714d67a691af4385b2da0b1e976))
69
+
70
+ pagerank accepted any alpha and always returned after 100 iterations, converged or not — while its
71
+ own docstring claims it 'matches networkx _pagerank_python', which rejects nothing-burger alphas
72
+ and raises PowerIterationFailedConvergence at max_iter. Verified: alpha=1.5 returned
73
+ plausible-looking values on the simple fixture (and negative, million-scale values on other
74
+ topologies), presented as real scores; a 200-node chain at alpha=0.999 returned unconverged
75
+ numbers silently.
76
+
77
+ Raise ValueError for alpha outside (0, 1), and raise nx.PowerIterationFailedConvergence (networkx's
78
+ own type, since networkx is already the sole runtime dep) when tolerance is never reached — via a
79
+ for/else so the success path is unchanged. The CLI catches both for its pagerank branch and exits
80
+ 2. Fixture pagerank outputs converge well inside 100 iterations, so every oracle is
81
+ byte-identical.
82
+
83
+ Closes #65
84
+
85
+ - **parse**: Tolerate CRLF and EOF-terminated frontmatter delimiters
86
+ ([#80](https://github.com/cdcoonce/graphmark/pull/80),
87
+ [`7a97662`](https://github.com/cdcoonce/graphmark/commit/7a9766264bb00ff1545aa1c75517654c5e8e737d))
88
+
89
+ _FM_RE only matched LF, so on a CRLF note (Windows / git autocrlf) the delimiter line is '---\r\n',
90
+ the regex failed, and the entire frontmatter block stayed in the body — turning a frontmatter
91
+ wikilink like 'related: "[[X]]"' into a phantom graph edge. Every metric
92
+ (orphans/hubs/clusters/pagerank/gaps) then differed purely by line-ending convention, silently.
93
+
94
+ Accept \r?\n on all three delimiter lines, and also accept a closing '---' at EOF with no trailing
95
+ newline (a frontmatter-only note, which previously failed to parse at all). _parse_frontmatter
96
+ already handles \r via splitlines. Frozen fixtures are LF, so all oracles are byte-identical.
97
+
98
+ Closes #63
99
+
100
+ ### Continuous Integration
101
+
102
+ - Test the advertised python matrix; ship CHANGELOG.md in the sdist
103
+ ([#87](https://github.com/cdcoonce/graphmark/pull/87),
104
+ [`695c2fb`](https://github.com/cdcoonce/graphmark/commit/695c2fb34f1330de009a6b6f93d71ed420e3f637))
105
+
106
+ The classifiers advertise 3.11/3.12/3.13 but CI's matrix covered only OS, so 'uv run' resolved a
107
+ single interpreter and two of the three claimed versions never executed — an untested promise to
108
+ anyone who installs on the strength of the classifier. This repo already learned the lesson on the
109
+ OS axis (the macOS leg exists because the afk gate is Linux-only); the python axis had the same
110
+ hole.
111
+
112
+ Add python: [3.11, 3.12, 3.13] to the matrix, passed to setup-uv's python-version, and name the jobs
113
+ so a failing leg is identifiable. Six legs, fail-fast already false. Verified locally: the full
114
+ suite passes on all three (251 tests each).
115
+
116
+ Also add CHANGELOG.md to the sdist include list — semantic-release maintains it on every release but
117
+ sdist consumers never received it — with a gate-enforced test (removing it from the include list
118
+ fails).
119
+
120
+ Closes #70
121
+
122
+ ### Documentation
123
+
124
+ - Rewrite the README around the current surface; accept str paths
125
+ ([#96](https://github.com/cdcoonce/graphmark/pull/96),
126
+ [`a5e151b`](https://github.com/cdcoonce/graphmark/commit/a5e151b0dbb1b8b6d1872b0f90b235e025d1efc6))
127
+
128
+ The README was the PyPI landing page and it opened with "Status: v0.1.1 on PyPI" while the package
129
+ was 0.2.0 — and the drift was structural, since semantic-release bumps pyproject while the README
130
+ hardcoded a number. Replace it with a PyPI version badge that cannot drift, and document the
131
+ surface shipped since: the graphmark check CI gate, the top-level build() quickstart,
132
+ GAPS_DEFAULT_BAND, the Similarity protocol, the dismiss store API, graph.unresolved, py.typed, and
133
+ the config reference. The CLAUDE.md pointer becomes an absolute URL, since that file is neither
134
+ shipped nor linkable from PyPI.
135
+
136
+ Executing every example surfaced two real footguns, fixed here rather than papered over: -
137
+ load_config("vault.toml") crashed with AttributeError: 'str' object has no attribute 'parent'. It
138
+ now accepts str | Path for both the config path and root_override, matching build(), which already
139
+ took str. - VaultConfig(root="/path") survived construction and failed much later on the first
140
+ Path operation. __post_init__ now coerces, so the declared type is honest.
141
+
142
+ Every code block in the README was run against the simple fixture and its output matched before
143
+ commit.
144
+
145
+ Closes #79
146
+
147
+ ### Features
148
+
149
+ - Add graphmark.build() and curated top-level re-exports
150
+ ([#95](https://github.com/cdcoonce/graphmark/pull/95),
151
+ [`9a0cdbf`](https://github.com/cdcoonce/graphmark/commit/9a0cdbf6d9fbad1fd035c2d81c7e94b3c03cbf23))
152
+
153
+ Getting a graph took four submodule imports plus the tribal knowledge that WikilinkExtractor pairs
154
+ with NormalizeResolver — a pairing with no defaults that the CLI and the live vault consumer each
155
+ re-implemented.
156
+
157
+ Add graphmark.build(source, *, extractor=None, resolver=None) accepting a vault-root str/Path or a
158
+ full VaultConfig and defaulting the extractor/resolver pair, plus __all__ re-exports of the whole
159
+ public surface: config and CheckPolicy, the graph and model types, the three Protocols, every
160
+ metric, the gaps band constants, run_check, the dismissal-store helpers, and the exporters.
161
+ Driving it from a TOML is build(load_config(path)) — no path/config-file guessing by extension.
162
+
163
+ The quickstart is now three lines:
164
+
165
+ import graphmark graph = graphmark.build("/path/to/vault") print(graphmark.stats(graph))
166
+
167
+ cli._load calls the same helper, deleting the duplicated construction. Purely additive —
168
+ VaultGraph.build and every submodule import keep working unchanged, verified by test.
169
+
170
+ Closes #78
171
+
172
+ - **check**: Add the graphmark check vault-health gate
173
+ ([#94](https://github.com/cdcoonce/graphmark/pull/94),
174
+ [`0a4d234`](https://github.com/cdcoonce/graphmark/commit/0a4d2349c87299c192fff1a5b76084707314f934))
175
+
176
+ Track D slice 3 of 3, completing the deterministic CI gate the roadmap names as the one unserved
177
+ ecosystem niche: Obsidian's official CLI needs the desktop app, the dormant python incumbent has
178
+ no CLI, and link checkers do no graph metrics.
179
+
180
+ New check.py evaluates config.check against a built graph. It is policy evaluation rather than a
181
+ structural metric, so it lives beside the engine instead of inside metrics.py, composing orphans
182
+ (which honors transient_prefixes, so scratch notes cannot fail the gate), the unresolved-link
183
+ count from slice 1, and siloed_notes.
184
+
185
+ The contract: - exit 0 = every enforced threshold passes; exit 1 = at least one breach, reserved for
186
+ breach ALONE; exit 2 = usage or config error. A policy that enforces nothing exits 2, never 0 — a
187
+ gate with nothing to check must not be able to report green. A bad vault root or a typo'd [check]
188
+ key is likewise 2, so CI can tell "your vault is unhealthy" from "your config is wrong". - stdout
189
+ is exactly one line, the JSON report; stderr carries one human-readable line per breach and never
190
+ pollutes stdout. - thresholds are inclusive: actual == limit passes. - the report is byte-stable —
191
+ key insertion order is fixed and checks appear in CheckPolicy field-declaration order — so runs
192
+ over an unchanged vault diff to nothing. A test pins it against a literal.
193
+
194
+ Closes #77
195
+
196
+ - **cli**: Add --version and help text for every subcommand and flag
197
+ ([#88](https://github.com/cdcoonce/graphmark/pull/88),
198
+ [`7abfb5d`](https://github.com/cdcoonce/graphmark/commit/7abfb5d681db15da0b7efb233d32fb93d5edbf3a))
199
+
200
+ A PyPI-published CLI could not report its own version, and 'graphmark --help' printed a bare choices
201
+ dump — none of the 10 subcommands or 5 flags carried a help string, so 'graphmark hubs --help'
202
+ explained nothing and users had to read the source to learn what 'siloed' or 'bridges' meant.
203
+
204
+ Add --version (sourced from the already single-sourced __version__), a parser description naming the
205
+ stdout-is-JSON/stderr-is-errors contract, a one-line help for each subcommand, and help for --n,
206
+ --note, --depth, --alpha, and the export format argument.
207
+
208
+ Closes #71
209
+
210
+ - **config**: Add the [check] policy block for vault-health gating
211
+ ([#93](https://github.com/cdcoonce/graphmark/pull/93),
212
+ [`377ecf9`](https://github.com/cdcoonce/graphmark/commit/377ecf9ac742d554de623974be31d58ee537e0b1))
213
+
214
+ Track D slice 2 of 3. load_config read only flat top-level keys and documented unknown keys as
215
+ silently ignored, so a [check] table written today was swallowed whole and the planned gate had
216
+ nothing to read.
217
+
218
+ Add a frozen CheckPolicy dataclass (max_orphans, max_unresolved_links, max_siloed; None = not
219
+ enforced) plus a check field on VaultConfig defaulting to all-None, and parse the optional [check]
220
+ table.
221
+
222
+ The block is deliberately STRICT, unlike the rest of the file: an unknown key inside [check], a
223
+ negative value, a non-integer, or a boolean all raise ValueError naming the file, the key, and the
224
+ valid keys. A silently-ignored typo like max_orphan would leave a CI gate reporting green forever,
225
+ which is the worst failure a gate can have. The documented leniency everywhere else in the TOML is
226
+ preserved.
227
+
228
+ CheckPolicy.is_configured() reports whether anything is enforced at all, so slice 3 can refuse to
229
+ report green on an empty policy rather than trivially passing. Field declaration order is the
230
+ report order and thus part of the check contract.
231
+
232
+ Extending the seeded config surface is directed by docs/ROADMAP.md's Track D section, which
233
+ CLAUDE.md's exception clause defers to.
234
+
235
+ Closes #76
236
+
237
+ - **gaps**: Add GAPS_DEFAULT_BAND and parameterize the signature annotations
238
+ ([#90](https://github.com/cdcoonce/graphmark/pull/90),
239
+ [`a5d0004`](https://github.com/cdcoonce/graphmark/commit/a5d0004e0a9d75ef2604b3fa893a3b6ec54412f8))
240
+
241
+ Opting into the validated banding policy took four keyword arguments referencing four separate
242
+ constants — exactly what the live consumer hand-copies. Add GAPS_DEFAULT_BAND so the opt-in is one
243
+ gesture: gaps(graph, fn, **GAPS_DEFAULT_BAND).
244
+
245
+ Parameterize the loose annotations for IDE/type-checker help, now that the package ships py.typed:
246
+ dismissed set|frozenset -> Collection[str], exclude_prefixes tuple -> tuple[str, ...], targets
247
+ list|None -> Sequence[str] | None. Runtime behavior is unchanged.
248
+
249
+ Document the note/targets precedence rather than making the conflict raise. The issue proposed
250
+ raising ValueError, conditional on verifying no consumer relies on the precedence — verification
251
+ found one that does: the-vault's graph_cli.py computes `targets = siloed_notes(graph) if
252
+ args.near_bridges else None` and passes it alongside an optional `note=args.note` in the same
253
+ call, so `--gaps --note X --near-bridges` passes both. Raising would break /connect and /garden at
254
+ runtime. Passing both is a legitimate pattern (compute one unconditionally, keep the other
255
+ optional), so the docstring now states that note wins as the more specific scope, and a test pins
256
+ it.
257
+
258
+ Closes #73
259
+
260
+ - **graph**: Record unresolved links instead of dropping them silently
261
+ ([#92](https://github.com/cdcoonce/graphmark/pull/92),
262
+ [`32e5a1d`](https://github.com/cdcoonce/graphmark/commit/32e5a1d57167e1e645814a108472cc458725f8c1))
263
+
264
+ build() discarded every unresolvable link with no record anywhere, so "how many broken links does
265
+ this vault have" — the flagship check threshold from the roadmap, and the one vault-health signal
266
+ ordinary link checkers already cover — was uncomputable from any graphmark surface.
267
+
268
+ Add VaultGraph.unresolved: dict[str, list[str]] mapping a rel_path to the raw link displays in it
269
+ that resolved to nothing, in extraction order; notes with none are absent. The constructor
270
+ parameter is optional and defaults to {}, so three-argument construction keeps working. No
271
+ Resolver/LinkExtractor protocol change and no model.py change.
272
+
273
+ Semantics the reference engine never defined for us, now pinned by test: - an AMBIGUOUS bare link
274
+ counts as unresolved (the Resolver returns None for both cases; both are equally broken from a
275
+ health view); - a resolved SELF-link does NOT count (it resolved, it is merely not an edge); -
276
+ every OCCURRENCE counts, so three [[Missing]] links contribute 3.
277
+
278
+ Purely additive: every frozen expected.json is untouched, and the differential invariant covers only
279
+ metrics shared with the reference.
280
+
281
+ Closes #75
282
+
283
+ - **packaging**: Ship the PEP 561 py.typed marker
284
+ ([#86](https://github.com/cdcoonce/graphmark/pull/86),
285
+ [`e289fcf`](https://github.com/cdcoonce/graphmark/commit/e289fcfefe0b47c590619b3d1afa1d70b6fd17a5))
286
+
287
+ The package is fully annotated — including the three Protocols that are its design seams
288
+ (LinkExtractor, Resolver, and the Similarity protocol shipped in 0.2.0 to type the injected
289
+ similar_fn) — but shipped no py.typed, so PEP 561 told mypy/pyright to treat every graphmark
290
+ import as untyped Any. The entire payoff of the annotation work was zero for consumers, starting
291
+ with the-vault's graph_cli.py.
292
+
293
+ Add the marker (hatchling's packages=['src/graphmark'] ships it with no build-config change),
294
+ declare the 'Typing :: Typed' classifier, and annotate the one gap, export.to_json(obj: object).
295
+
296
+ The guarantee is gate-enforced rather than assumed: new tests/test_packaging.py builds the real
297
+ wheel and sdist and asserts the marker is inside each. Removing the marker fails 3 of them.
298
+
299
+ Closes #69
300
+
301
+ ### Testing
302
+
303
+ - Pin degenerate-vault behavior and enforce the networkx PageRank claim
304
+ ([#85](https://github.com/cdcoonce/graphmark/pull/85),
305
+ [`5124db8`](https://github.com/cdcoonce/graphmark/commit/5124db83e2c31af12a1bbfcc7baf9a869eecf5c5))
306
+
307
+ Two untested regions closed.
308
+
309
+ Degenerate vaults: no test built an empty vault, so the density guard (notes > 0) and the pagerank N
310
+ == 0 guard never executed — yet pointing the CLI at an empty or wrong directory is a likely first
311
+ contact for a PyPI user. Pin the full empty-vault surface (stats all-zero, every list metric
312
+ empty, pagerank [], gaps [], a valid empty digraph, neighborhood raising, CLI exit 0 with valid
313
+ JSON) plus the smallest non-empty case, a single unlinked note.
314
+
315
+ networkx parity: docs/ROADMAP.md claims 'PageRank is checked against networkx' but nothing enforced
316
+ it. Cross-check both fixtures at three alphas (0.5/0.85/0.95) against networkx's _pagerank_python
317
+ — the pure python implementation graphmark's docstring actually claims parity with, since
318
+ nx.pagerank dispatches to a scipy backend and this package ships no numpy/scipy. Measured
319
+ agreement is ~7e-7; the assertion uses 1e-5, far below any teleport or convergence mutation.
320
+
321
+ Mutation-verified: dropping dangling redistribution fails 9 tests, (1-alpha) -> alpha fails 7,
322
+ loosening the convergence tolerance fails 6 (the fixture tolerance alone could not catch this),
323
+ and corrupting the empty-graph guard fails 1.
324
+
325
+ Closes #68
326
+
327
+ - **build**: Cover the excluded_dirs and rules_files selection filters
328
+ ([#83](https://github.com/cdcoonce/graphmark/pull/83),
329
+ [`d8e5e3c`](https://github.com/cdcoonce/graphmark/commit/d8e5e3c24303497ffd81d72ef200ff52bf1e28cb))
330
+
331
+ Both filters in VaultGraph.build were mutation-dead: every fixture config sets
332
+ excluded_dirs=['.git'] but no fixture vault holds a note under an excluded dir, and no fixture
333
+ vault contains a rules file — so deleting either filter, or widening rel_parts[:-1] to rel_parts,
334
+ left all 192 tests green. The live consumer depends on both daily; a silent regression would pull
335
+ rules files and archived notes into the graph, corrupting orphans/stats/gaps behind a green gate.
336
+
337
+ Add 15 tests building real vaults under tmp_path (no frozen-fixture edits) covering: exclusion by
338
+ name and at any depth, multiple excluded dirs, dirs-only semantics (a FILE whose name matches an
339
+ entry stays — pinning the [:-1] slice), rules files at root and nested, custom and empty
340
+ rules_files, no-edge/no-back-edge contribution from filtered notes, and all three filters
341
+ composing with scoped_folders.
342
+
343
+ Mutation-verified: deleting the excluded_dirs filter fails 7 tests,
344
+
345
+ deleting the rules_files filter fails 5, and rel_parts[:-1] -> rel_parts fails 1.
346
+
347
+ Closes #66
348
+
349
+ - **gaps**: Cover the parameters the frozen oracle never exercises
350
+ ([#84](https://github.com/cdcoonce/graphmark/pull/84),
351
+ [`5924e96`](https://github.com/cdcoonce/graphmark/commit/5924e966f83861ff1e724ff238aa39d940a99554))
352
+
353
+ The gaps/ fixture pins the ranking algorithm end-to-end but always calls gaps() the same way: no
354
+ note=, no targets=, no exclude_prefixes, no self-pairs, and fixture scores that never sit exactly
355
+ on the threshold or max_score bound (0.55-0.95 vs bounds 0.6/0.92). Every one of those was a
356
+ mutation the suite could not kill.
357
+
358
+ Add 18 tests using an in-memory graph and a recording Similarity stub, so each knob is isolated and
359
+ the scan itself is observable: inclusive score boundaries, note=/targets= scoping (asserted via
360
+ which rel_paths the similarity source was actually asked about), exclude_prefixes on both the
361
+ source and candidate side, the self-pair skip, already-linked suppression through a back-link, and
362
+ k pass-through.
363
+
364
+ Mutation-verified: threshold < -> <=, max_score > -> >=, dropping the self-pair skip, dropping
365
+ either exclude_prefixes filter, and ignoring note= scoping each fail 1-2 tests.
366
+
367
+ Closes #67
368
+
369
+
370
+ ## v0.2.0 (2026-07-25)
371
+
372
+ ### Bug Fixes
373
+
374
+ - **ci**: Let release.yml build with uv; skip semantic-release's own build
375
+ ([`6989689`](https://github.com/cdcoonce/graphmark/commit/6989689a0672d79df143eee68a31ce6493573209))
376
+
377
+ The python-semantic-release action runs in a container without uv, so build_command='uv build'
378
+ failed with exit 127 and aborted the release before any tag/publish. Set build_command='' so
379
+ semantic-release only versions/tags/changelogs, and the workflow's own uv-based step (running on
380
+ the runner host where uv is installed) builds and publishes.
381
+
382
+ - **cli**: Gaps subcommand exits 2 with guidance instead of printing []
383
+ ([#59](https://github.com/cdcoonce/graphmark/pull/59),
384
+ [`81b5fc1`](https://github.com/cdcoonce/graphmark/commit/81b5fc1dfb82c727ddd07e84c76d01598886b60a))
385
+
386
+ The gaps subcommand hardwired an empty similarity source and always printed [], so the package's
387
+ most distinctive metric looked broken from its own command line. gaps needs a caller-injected
388
+ similarity source the CLI can't supply, so 'graphmark gaps' now prints 'gaps requires an injected
389
+ similarity source; use the library API (graphmark.metrics.gaps) — see README' to stderr and exits
390
+ 2 (honest signpost, chosen option). Stdout stays clean. README updated to match; drops the dead
391
+ gaps import.
392
+
393
+ Closes #31
394
+
395
+ - **config**: Clear error when required 'root' key is missing
396
+ ([#49](https://github.com/cdcoonce/graphmark/pull/49),
397
+ [`70b275d`](https://github.com/cdcoonce/graphmark/commit/70b275d0173f9f229caa1e9e047f561843a781d1))
398
+
399
+ load_config indexed data['root'] unguarded, so a TOML without the only required key failed with a
400
+ bare KeyError naming neither the file nor what was expected. Raise ValueError(f"config {path}:
401
+ missing required key 'root'") instead, and document root as required / all other keys as optional
402
+ in the docstring.
403
+
404
+ Closes #26
405
+
406
+ - **config**: Remove silent no-op knobs wikilink_pattern and orphan_min_chars
407
+ ([#55](https://github.com/cdcoonce/graphmark/pull/55),
408
+ [`98590ef`](https://github.com/cdcoonce/graphmark/commit/98590ef0ec7525d2eeab21987f8f311cb460bfb4))
409
+
410
+ Both fields were loaded from TOML and never consulted: _WIKILINK_RE is hardcoded in parse.py and
411
+ orphans() consults only transient_prefixes. A config that set them lied to its author. Remove them
412
+ from VaultConfig, load_config, and the reference configs/my-brain.toml. Removal is the
413
+ behavior-preserving option (wiring orphan_min_chars=300 would change orphans() against the frozen
414
+ oracles; wiring wikilink_pattern re-opens link-extraction parity). Unknown TOML keys are now
415
+ documented as silently ignored. Human-approved config-schema break.
416
+
417
+ Closes #30
418
+
419
+ - **dismiss**: Treat a corrupt store as empty in load_dismissed
420
+ ([#52](https://github.com/cdcoonce/graphmark/pull/52),
421
+ [`6b5bdd1`](https://github.com/cdcoonce/graphmark/commit/6b5bdd140736c11e7872d3e54fac9f6e0aad8eaf))
422
+
423
+ load_dismissed indexed json.loads unguarded while record_dismissal already fell back to {} on
424
+ (JSONDecodeError, OSError) — the two halves of the same store disagreed about failure, so a
425
+ corrupt store crashed gap filtering. Mirror the existing guard: invalid JSON, an unreadable file,
426
+ or a non-dict payload all return {}, so active_dismissed_sigs returns an empty set. A corrupt
427
+ store means 'no active dismissals', never a crash.
428
+
429
+ Closes #24
430
+
431
+ - **export**: Escape quotes and backslashes in DOT output
432
+ ([#48](https://github.com/cdcoonce/graphmark/pull/48),
433
+ [`bbaab3c`](https://github.com/cdcoonce/graphmark/commit/bbaab3c8b5fa56d5ff643dd1a59679bd0f3de2c9))
434
+
435
+ to_dot interpolated raw note paths into quoted DOT identifiers with no escaping, so a filename
436
+ containing " or \ produced invalid DOT that Graphviz rejects. Add a _dot_quote helper (backslash
437
+ first, then quote) applied to every emitted identifier. Output for paths without special
438
+ characters is byte-identical to before.
439
+
440
+ Closes #25
441
+
442
+ - **neighborhood**: Defined error for an unknown note
443
+ ([#50](https://github.com/cdcoonce/graphmark/pull/50),
444
+ [`eceb4c5`](https://github.com/cdcoonce/graphmark/commit/eceb4c5c5b3ae7b6612ed08f69744f8c00ab1bbb))
445
+
446
+ neighborhood() on a nonexistent note silently returned empty out/back lists, so a typo in --note was
447
+ indistinguishable from a genuinely isolated note. metrics.neighborhood now raises
448
+ ValueError('unknown note: <note>') when the note is not in graph.nodes; the CLI catches it, prints
449
+ to stderr, and exits 2 (stdout stays JSON-only). Known-note output is byte-identical.
450
+
451
+ Closes #27
452
+
453
+ - **parse**: Decode non-UTF8 notes with replacement instead of crashing
454
+ ([#51](https://github.com/cdcoonce/graphmark/pull/51),
455
+ [`f42ad7b`](https://github.com/cdcoonce/graphmark/commit/f42ad7b23e0dbfac0681f775f37a2a489519ca16))
456
+
457
+ parse_document read notes with a bare read_text(encoding='utf-8'), so a single non-UTF8 file
458
+ anywhere in the vault propagated UnicodeDecodeError and killed every graphmark command with a
459
+ traceback. Read bytes and decode with errors='replace' on failure, keeping the note in the graph
460
+ (node counts stay truthful) and emitting exactly one warning line per affected file to stderr —
461
+ never stdout, so the JSON surface stays clean. Valid vaults are byte-identical and silent.
462
+
463
+ Closes #23
464
+
465
+ - **parse**: Fenced-code stripping respects fence length, not just char
466
+ ([#53](https://github.com/cdcoonce/graphmark/pull/53),
467
+ [`4b375fe`](https://github.com/cdcoonce/graphmark/commit/4b375fe151800df49d868bdfe4c063c7f1bf858f))
468
+
469
+ _strip_fenced_blocks tracked only which character opened a fence, so a shorter nested fence of the
470
+ same character (a 3-backtick example inside a 4-backtick outer fence) closed the fence prematurely
471
+ and leaked wikilinks inside code as real links — violating the documented 'ignore links inside
472
+ code spans' invariant. Track the opening fence's character AND length; only close on the same
473
+ character with length >= the opening length (CommonMark's fence-closing rule).
474
+
475
+ Closes #33
476
+
477
+ - **version**: Single-source __version__ from package metadata
478
+ ([#47](https://github.com/cdcoonce/graphmark/pull/47),
479
+ [`1db7e8d`](https://github.com/cdcoonce/graphmark/commit/1db7e8d2cfd89c91cdd1f75dd5a586a46064a063))
480
+
481
+ Derive __version__ via importlib.metadata.version('graphmark') with a PackageNotFoundError fallback
482
+ of 0.0.0+unknown for un-installed source checkouts. Smoke test now compares against
483
+ importlib.metadata instead of a hardcoded literal, so the wheel can never again self-report a
484
+ stale version (published 0.1.1 wheel reported 0.1.0).
485
+
486
+ Closes #22
487
+
488
+ ### Chores
489
+
490
+ - Commit dev-cycle telemetry and post-release uv.lock
491
+ ([`9379914`](https://github.com/cdcoonce/graphmark/commit/937991436e7db0d74d58f35811b72f9e43f26253))
492
+
493
+ docs/dev-cycle/ is the build-#1 afk telemetry dataset (42 attestation rows, quarantine log,
494
+ last-run) — tracked, not ignored, per the dogfood directive. uv.lock carries the 0.1.0->0.1.1 bump
495
+ left behind by the release commit.
496
+
497
+ - Sync uv.lock for 0.2.0
498
+ ([`4232227`](https://github.com/cdcoonce/graphmark/commit/4232227f9176e0576327d9642f811642bbcf0320))
499
+
500
+ - **afk**: Add missing [model] tier map to config
501
+ ([`458762f`](https://github.com/cdcoonce/graphmark/commit/458762f701a18549d8a325d9454938aa312ab3ff))
502
+
503
+ The driver requires model.cheap when default_tier resolves to 'cheap' (its default). The config had
504
+ no [model] section, so every scheduled cycle aborted at config load with ConfigError, blocking the
505
+ entire promoted backlog (#23-#32, all afk:cheap). Mirror afk-cockpit's tier map: cheap=sonnet,
506
+ frontier=opus.
507
+
508
+ ### Continuous Integration
509
+
510
+ - Add per-PR gate on ubuntu+macos; gate before build in publish
511
+ ([`0d76209`](https://github.com/cdcoonce/graphmark/commit/0d76209252df1562a0681b6e622ed713e66b1bb3))
512
+
513
+ The afk gate runs Linux-only, so OS-specific escapes only surfaced at release. ci.yml mirrors the
514
+ gate command exactly on both OSes for every push/PR. publish.yml now gates before building the
515
+ artifact it publishes.
516
+
517
+ - Automate releases on dev->main with semantic-release; bump to 0.2.0
518
+ ([`d6c2c04`](https://github.com/cdcoonce/graphmark/commit/d6c2c045517dbe4c69aec5495a61ec2010764aa6))
519
+
520
+ Adopt a dev/main branch model with deploy-on-promotion. dev is the integration branch; promoting dev
521
+ to main triggers release.yml, which runs the gate, then python-semantic-release reads the
522
+ conventional-commit history to bump the version, update CHANGELOG.md, tag v<version>, cut a GitHub
523
+ release, and publish to PyPI via OIDC Trusted Publishing.
524
+
525
+ - pyproject: baseline version 0.2.0 (the honest-surface API cut) + a [tool.semantic_release] config
526
+ (major_on_zero=false so pre-1.0 breaking changes bump the minor; version single-sourced in
527
+ pyproject). - ci.yml: gate main and dev (plus PRs). - release.yml: new; replaces the tag-triggered
528
+ publish.yml (its trusted publish now runs inline only when a release is actually cut). -
529
+ CHANGELOG.md: seeded; henceforth generated by semantic-release. - README: document the branch
530
+ model + automated release flow.
531
+
532
+ ### Documentation
533
+
534
+ - Seeded-interface rule yields to human-triaged issue direction
535
+ ([`a88bc28`](https://github.com/cdcoonce/graphmark/commit/a88bc28501be7f08a1a9ee8467919b764bba5c31))
536
+
537
+ Issues #29/#30 are triaged decisions that change the seeded surface (remove dead model dataclasses,
538
+ remove no-op config knobs). The standing CLAUDE.md non-negotiable and the afk agent_prompt both
539
+ said 'do not redesign the seeded interfaces' unconditionally, which would put the executor in a
540
+ contract conflict on those slices. The rule now names the exception: an issue that explicitly
541
+ directs a seeded-surface change (a recorded human triage) wins.
542
+
543
+ - Truth-up contract docs post-0.1.1
544
+ ([`6ff2c48`](https://github.com/cdcoonce/graphmark/commit/6ff2c48a083f31de5516892f9f32b183c376587b))
545
+
546
+ README/ROADMAP/CLAUDE.md described a pre-ship repo: engine 'being built', 5/10 CLI commands with
547
+ wrong syntax, load_config 'unimplemented', dismiss.py and siloed absent from the contract. ROADMAP
548
+ is the afk --expand grounding doc, so the stale copy would have proposed already-built work.
549
+
550
+ - README: v0.1.1 status, all 10 subcommands with real flags, library usage, gaps CLI-stub caveat -
551
+ ROADMAP: Track A closed; Track C (hardening -> 0.2) and Track D (graphmark check CI gate) added;
552
+ non-goals closed as dropped - CLAUDE.md: dismiss.py added as the fourth part, siloed added to the
553
+ parity list, the gaps injected-similarity contract codified - config.py: docstring no longer
554
+ claims load_config is unimplemented
555
+
556
+ ### Features
557
+
558
+ - **gaps**: Ship validated banding policy as package-level constants
559
+ ([#60](https://github.com/cdcoonce/graphmark/pull/60),
560
+ [`2d01499`](https://github.com/cdcoonce/graphmark/commit/2d014997eea239bd1823bef25529cacf08e62a84))
561
+
562
+ The validated gaps band (threshold 0.6, max_score 0.92, k 8, hub_degree 40) — proven in daily
563
+ /connect + /garden use on the owner's live vault — lived only in the consumer's argparse defaults,
564
+ so any second consumer re-derived policy from scratch and gaps()'s own defaults are unvalidated.
565
+ Ship the band in-package as named constants (GAPS_DEFAULT_*) with provenance recorded in a
566
+ comment. Chosen the non-breaking shape: gaps()'s signature defaults are untouched, so the frozen
567
+ gaps fixtures (which pass explicit params) stay byte-identical; consumers opt in.
568
+
569
+ Closes #32
570
+
571
+ - **interfaces**: Add Similarity Protocol typing the injected similar_fn
572
+ ([#54](https://github.com/cdcoonce/graphmark/pull/54),
573
+ [`eefd039`](https://github.com/cdcoonce/graphmark/commit/eefd0393dcfefa16165391db82457d1115fca0f2))
574
+
575
+ gaps()'s injected similar_fn is the package's similarity seam but was untyped — its contract was
576
+ only discoverable by reading metrics.py. Add a Similarity Protocol to interfaces.py (where
577
+ LinkExtractor/Resolver already live) codifying the (rel_path, k) -> list[(rel_path, score)] shape,
578
+ annotate metrics.gaps' parameter, and reference the contract in its docstring. Additive typing
579
+ only; no runtime behavior change.
580
+
581
+ Closes #28
582
+
583
+ ### Performance Improvements
584
+
585
+ - **graph**: Cache flattened path list for folder-style link resolution
586
+ ([#56](https://github.com/cdcoonce/graphmark/pull/56),
587
+ [`958c642`](https://github.com/cdcoonce/graphmark/commit/958c6426be4c033b3c8ef379f9eae50dac84573e))
588
+
589
+ NormalizeResolver.resolve rebuilt the full flattened path list ([p for paths in catalog.values() for
590
+ p in paths]) on every folder-style link, making resolution O(notes x folder-links). catalog is
591
+ invariant for a whole VaultGraph.build(), so cache the flattened list keyed on id(catalog)
592
+ (single-slot: a new catalog evicts the previous). Signature and Resolver Protocol unchanged;
593
+ results are behavior-preserving.
594
+
595
+ Closes #34
596
+
597
+ ### Refactoring
598
+
599
+ - **model**: Remove dead seeded dataclasses Edge/Graph/Finding (0.2 API cut)
600
+ ([#58](https://github.com/cdcoonce/graphmark/pull/58),
601
+ [`3f32d3a`](https://github.com/cdcoonce/graphmark/commit/3f32d3af692b49497aee53cd52811b7c4c8d7490))
602
+
603
+ model.Edge, model.Graph, and model.Finding were dead public surface — nothing in the package or any
604
+ consumer constructed or imported them (the engine uses graph.VaultGraph; Document is the only
605
+ model type in use). Dead 'seeded boundary' types misled library users about the real API. Remove
606
+ all three as the 0.2 honest-surface cut; Document stays. Add model.__all__ = ['Document'] and a
607
+ public-surface test so the dead types can't silently reappear. Sync CLAUDE.md ('a Graph' -> 'a
608
+ VaultGraph') and docs/ROADMAP.md. Human-approved public-API break.
609
+
610
+ Closes #29
611
+
612
+ ### Testing
613
+
614
+ - **siloed**: Pin equal-size component tie-break; make it explicit
615
+ ([#57](https://github.com/cdcoonce/graphmark/pull/57),
616
+ [`cde5ecd`](https://github.com/cdcoonce/graphmark/commit/cde5ecd20f0dc692ecf19afb0ae74717510404cb))
617
+
618
+ siloed_notes picked the largest post-bridge-removal component as mainland; when two components tied
619
+ for largest, which became mainland depended silently on nx.connected_components' traversal order.
620
+ No frozen fixture exercises a tie, so it was unverified. Break ties explicitly by member order
621
+ (lexicographically-smallest-membered component is mainland) and add an inline test constructing a
622
+ single articulation point whose removal yields two equal-size components, asserting deterministic
623
+ output. Non-tie cases (all frozen oracles) are byte-identical since size dominates the sort key.
624
+
625
+ Closes #35
626
+
627
+
628
+ ## v0.1.1 (2026-07-01)
629
+
630
+ ### Chores
631
+
632
+ - Package metadata + MIT license for PyPI publish
633
+ ([`ef30b45`](https://github.com/cdcoonce/graphmark/commit/ef30b45f7a5f92727a3fca5d34c920c7bb7a6e0e))
634
+
635
+ Add license (MIT), authors, keywords, classifiers, project URLs; add LICENSE file; constrain the
636
+ sdist to real package contents (was bundling .afk agent transcripts → 1.2MB, now 26KB). Wheel
637
+ verified installing in a clean env with the graphmark console script working end-to-end.
638
+
639
+ Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
640
+
641
+ ### Continuous Integration
642
+
643
+ - Add PyPI trusted-publishing workflow (tag v* -> publish, no token)
644
+ ([`c46ec3b`](https://github.com/cdcoonce/graphmark/commit/c46ec3b012fbe285a92e334318e171786bd7ae69))
645
+
646
+ Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
647
+
648
+ - Drop environment requirement from publish workflow for zero-config trusted publishing
649
+ ([`485a784`](https://github.com/cdcoonce/graphmark/commit/485a784bcc2c932223cf4b4632db450fbb855ab5))
650
+
651
+ Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
652
+
653
+ ### Testing
654
+
655
+ - **fixtures**: Freeze alt `siloed` oracle for afk #6 (issue #11)
656
+ ([`563aa61`](https://github.com/cdcoonce/graphmark/commit/563aa617f703dad386bf73e9506ee6727454ba01))
657
+
658
+ Computed by running brain_map.py's OWN siloed_notes() on the alt graph (the reference engine IS the
659
+ spec). It deliberately includes pre-existing orphans (daily/orphan/stub) alongside the nodes cut
660
+ off behind articulation point echo (delta/foxtrot); graphmark must reproduce this verbatim
661
+ (parity, quirk included). Guessing the "intuitive" [delta,foxtrot] would have frozen a wrong
662
+ oracle — compute from the reference, never guess (afk-agent-system#654).
663
+
664
+ Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
665
+
666
+ - **fixtures**: Freeze alt-fixture pagerank oracle for afk #4
667
+ ([`803e6f1`](https://github.com/cdcoonce/graphmark/commit/803e6f1ec4e1a37914509b4b44c94807d07b172f))
668
+
669
+ Generated via networkx's pure-python reference (_pagerank_python, alpha=0.85) and cross-checked to
670
+ reproduce simple/expected.json's pagerank exactly, so the same trusted method backs both oracles.
671
+ A correct pure-python power-iteration impl must now match BOTH fixtures — closing the
672
+ single-fixture overfitting gap where #4's pagerank could otherwise be hardcoded to simple's six
673
+ values.
674
+
675
+ Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
676
+
677
+ - **fixtures**: Pre-freeze alt vault oracle + config.toml for afk #3
678
+ ([`d889dcf`](https://github.com/cdcoonce/graphmark/commit/d889dcfe3efc5905c0f3879284f67186273af903))
679
+
680
+ Hand-authored, human-conductor-frozen BEFORE afk dispatch so the executor asserts against oracles it
681
+ cannot write — the same trust model as the brain_map-generated simple/expected.json. brain_map.py
682
+ is hardcoded to my-brain and cannot generate an oracle for a foreign vault, so slice #3
683
+ (generalization) had no ungameable oracle; the executor's first attempt manufactured its own
684
+ expected.json and was correctly quarantined.
685
+
686
+ - tests/fixtures/alt/: 10-note vault with different folder names (docs/refs/misc/daily) and distinct
687
+ topology (2 clusters sized 4 & 3, an articulation point, a transient daily note, an
688
+ unresolved-link orphan) to prove graphmark is not hardcoded to my-brain. -
689
+ tests/fixtures/alt/expected.json: structural keys only; verified byte-for-byte against the trusted
690
+ engine. pagerank deferred to #4 (networkx is its independent oracle, runnable on any vault). -
691
+ tests/fixtures/simple/config.toml: drives the engine through load_config over the original
692
+ oracle's vault, so #3 can assert config-path == hardcoded-path.
693
+
694
+ Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
695
+
696
+ - **fixtures**: Pre-freeze dismiss/ oracle for afk #7 (issue #12)
697
+ ([`3ab046c`](https://github.com/cdcoonce/graphmark/commit/3ab046c28cacf462102367353981154076cb8ba1))
698
+
699
+ Active-sig set computed by brain_map.py's OWN active_dismissed_sigs() (reference engine = spec), per
700
+ afk-agent-system#654. connect-dismissed.json records three weaklink dismissals: alpha|beta
701
+ (correct hashes → ACTIVE), alpha|gamma (stale gamma hash → STALE), alpha|delta (delta.md missing →
702
+ STALE). Reference output: active = ['weaklink|alpha.md|beta.md'].
703
+
704
+ Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
705
+
706
+ - **fixtures**: Pre-freeze gaps/ oracle + fake similar_fn for afk #8 (issue #13)
707
+ ([`28b4e2f`](https://github.com/cdcoonce/graphmark/commit/28b4e2f99a33005240f1dc55c8755e140f199d0a))
708
+
709
+ Oracle computed by feeding graphmark's OWN built graph into brain_map.py's gaps() (reference engine
710
+ = spec), per afk-agent-system#654. similar.json is a deterministic fake similar_fn (NO embeddings
711
+ — the embedding source is injected; graphmark owns the ranking/filter algorithm). Exercises every
712
+ branch: already-linked, threshold, max_score, dismiss, reciprocal dedup, cross-folder ranking, and
713
+ hub demotion (hub<->d 0.80 ranks below d<->a 0.75 because docs/hub.md is a hub).
714
+
715
+ Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
716
+
717
+ - **fixtures**: Pre-freeze out-of-graph gaps oracle for afk #10 (defensive degree)
718
+ ([`27be377`](https://github.com/cdcoonce/graphmark/commit/27be377e88547b7eb4b6d7a0952656c47d54469b))
719
+
720
+ A live vault run of the graph_cli adapter crashed graphmark's gaps(): _hub() calls G.degree(r),
721
+ which raises for a note returned by similar_fn that is NOT a graph node (semantic index scope >
722
+ graph scope). brain_map treats unknown nodes as degree 0 (non-hub). Oracle computed via brain_map;
723
+ graphmark must match.
724
+
725
+ Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
726
+
727
+ - **fixtures**: Pre-freeze scoped/ include-list oracle for afk #5 (issue #10)
728
+ ([`bb02fc8`](https://github.com/cdcoonce/graphmark/commit/bb02fc859822511602bd07fefb92ff60bafdfa3c))
729
+
730
+ Human-conductor-frozen before dispatch, per the oracle-pre-freeze discipline (afk-agent-system#654).
731
+ scoped_folders=['docs','refs'] include-list: only docs/one.md + refs/two.md are in the graph;
732
+ misc/ and junk/ notes (and the link originating in misc/) must vanish. Verified against the
733
+ trusted engine by simulating the include-list via excluded_dirs (equivalent). Fails on current
734
+ main (scans all → notes=4), passes once scoped_folders is honored.
735
+
736
+ Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
737
+
738
+ - **fixtures**: Pre-freeze selflink/ oracle for afk #9 (self-edge parity fix)
739
+ ([`fb6c014`](https://github.com/cdcoonce/graphmark/commit/fb6c014ed71e76b41176b90bd22756705b6a56a1))
740
+
741
+ A live-vault diff of graphmark vs brain_map found graphmark's ONLY structural divergence: it creates
742
+ a self-loop for a note that links to itself ([[Key Decisions]] inside Key Decisions.md), while
743
+ brain_map drops self-links. This fixture pins the correct behavior: a.md links to [[a]] and [[b]]
744
+ -> exactly one edge a->b, no self-loop. Frozen before dispatch (afk-agent-system#654).
745
+
746
+ Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>