secondbrain-py 0.2.1__py3-none-any.whl

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 (273) hide show
  1. brain/__init__.py +0 -0
  2. brain/__main__.py +18 -0
  3. brain/_capture_command.py +445 -0
  4. brain/_compose.py +52 -0
  5. brain/activity.py +206 -0
  6. brain/ask.py +631 -0
  7. brain/audio.py +591 -0
  8. brain/backfill/__init__.py +12 -0
  9. brain/backfill/search_extras.py +141 -0
  10. brain/backfill/source_rows.py +101 -0
  11. brain/bin/__init__.py +1 -0
  12. brain/bin/_launcher.py +107 -0
  13. brain/bin/down.py +8 -0
  14. brain/bin/launchd.py +268 -0
  15. brain/bin/monitor.py +570 -0
  16. brain/bin/rebuild.py +8 -0
  17. brain/bin/status.py +8 -0
  18. brain/bin/up.py +8 -0
  19. brain/brief.py +272 -0
  20. brain/capture.py +49 -0
  21. brain/chat.py +293 -0
  22. brain/cli.py +9760 -0
  23. brain/cli_claude.py +81 -0
  24. brain/cli_connect.py +285 -0
  25. brain/cli_demo.py +266 -0
  26. brain/config.py +1949 -0
  27. brain/connect.py +925 -0
  28. brain/db.py +540 -0
  29. brain/demo/__init__.py +452 -0
  30. brain/demo/corpus/manifest.json +403 -0
  31. brain/demo/embedder.py +74 -0
  32. brain/durations.py +84 -0
  33. brain/edit_session.py +156 -0
  34. brain/editor.py +67 -0
  35. brain/elicit/__init__.py +16 -0
  36. brain/elicit/detectors.py +250 -0
  37. brain/elicit/drafter.py +70 -0
  38. brain/elicit/queue.py +220 -0
  39. brain/elicit/schema.py +48 -0
  40. brain/elicit/session.py +445 -0
  41. brain/embedding_targets.py +54 -0
  42. brain/embeddings.py +424 -0
  43. brain/enrichment.py +808 -0
  44. brain/errors.py +357 -0
  45. brain/eval/__init__.py +129 -0
  46. brain/eval/answer_eval.py +281 -0
  47. brain/eval/baseline.py +265 -0
  48. brain/eval/concept_extraction.py +378 -0
  49. brain/eval/corpus.py +152 -0
  50. brain/eval/errors.py +19 -0
  51. brain/eval/graph_baseline.py +226 -0
  52. brain/eval/graph_retrieval.py +202 -0
  53. brain/eval/graph_runner.py +319 -0
  54. brain/eval/metrics.py +101 -0
  55. brain/eval/runner.py +223 -0
  56. brain/format.py +783 -0
  57. brain/gaps.py +390 -0
  58. brain/graph_rag/__init__.py +94 -0
  59. brain/graph_rag/_retrieval_common.py +113 -0
  60. brain/graph_rag/aggregates.py +303 -0
  61. brain/graph_rag/aliases/__init__.py +583 -0
  62. brain/graph_rag/backends/__init__.py +10 -0
  63. brain/graph_rag/backends/_age_helpers.py +473 -0
  64. brain/graph_rag/backends/age.py +782 -0
  65. brain/graph_rag/backends/base.py +272 -0
  66. brain/graph_rag/build.py +344 -0
  67. brain/graph_rag/communities.py +644 -0
  68. brain/graph_rag/communities_summary.py +437 -0
  69. brain/graph_rag/concepts.py +202 -0
  70. brain/graph_rag/cooccur.py +193 -0
  71. brain/graph_rag/cross_type.py +312 -0
  72. brain/graph_rag/extract.py +885 -0
  73. brain/graph_rag/fuse.py +371 -0
  74. brain/graph_rag/global_.py +412 -0
  75. brain/graph_rag/grouping.py +372 -0
  76. brain/graph_rag/person_resolver.py +167 -0
  77. brain/graph_rag/reconcile.py +792 -0
  78. brain/graph_rag/relational.py +353 -0
  79. brain/graph_rag/retrieve.py +526 -0
  80. brain/graph_rag/router.py +288 -0
  81. brain/graph_rag/schema.py +320 -0
  82. brain/graph_rag/sync.py +237 -0
  83. brain/graph_rag/tenancy.py +43 -0
  84. brain/graph_rag/themes.py +501 -0
  85. brain/graph_rag/weighting.py +202 -0
  86. brain/ingest/__init__.py +1926 -0
  87. brain/ingest/chunker.py +249 -0
  88. brain/ingest/docx.py +40 -0
  89. brain/ingest/gmail.py +621 -0
  90. brain/ingest/markdown.py +37 -0
  91. brain/ingest/pdf.py +61 -0
  92. brain/ingest/stdin.py +22 -0
  93. brain/ingest/sub_tokens.py +91 -0
  94. brain/ingest/text.py +16 -0
  95. brain/interactions.py +205 -0
  96. brain/maintenance.py +355 -0
  97. brain/mcp_server.py +3405 -0
  98. brain/migrations/001_init.sql +43 -0
  99. brain/migrations/002_qwen3_embedding.sql +17 -0
  100. brain/migrations/003_vault_model.sql +41 -0
  101. brain/migrations/004_relax_content_hash_uniqueness.sql +18 -0
  102. brain/migrations/005_derived_links.sql +67 -0
  103. brain/migrations/006_dedup_file_by_source_path.sql +25 -0
  104. brain/migrations/007_email_thread_and_draft.sql +15 -0
  105. brain/migrations/008_gmail_thread_unique.sql +11 -0
  106. brain/migrations/009_chunks_weighted_tsv.sql +28 -0
  107. brain/migrations/010_interactions.sql +30 -0
  108. brain/migrations/011_documents_summary.sql +23 -0
  109. brain/migrations/012_graphrag.sql +171 -0
  110. brain/migrations/013_graphrag_communities.sql +125 -0
  111. brain/migrations/014_graphrag_community_summary_hash.sql +33 -0
  112. brain/migrations/015_interactions_graph_targets.sql +89 -0
  113. brain/migrations/016_index_hygiene.sql +61 -0
  114. brain/migrations/017_elicit.sql +30 -0
  115. brain/migrations/018_review_gap_signal_kinds.sql +40 -0
  116. brain/migrations/019_search_queries.sql +35 -0
  117. brain/migrations/020_link_suggestions.sql +40 -0
  118. brain/migrations/021_timeline_doc_date.sql +34 -0
  119. brain/migrations/022_link_suggestions_undirected.sql +84 -0
  120. brain/migrations/023_search_queries_fts_count.sql +28 -0
  121. brain/quartz_overrides/__init__.py +8 -0
  122. brain/quartz_overrides/quartz/bootstrap-cli.mjs +65 -0
  123. brain/quartz_overrides/quartz/build.ts +568 -0
  124. brain/quartz_overrides/quartz/cli/args.js +152 -0
  125. brain/quartz_overrides/quartz/cli/build_partial_handler.js +544 -0
  126. brain/quartz_overrides/quartz/cli/handlers.js +636 -0
  127. brain/quartz_overrides/quartz/components/CommandPalette.tsx +172 -0
  128. brain/quartz_overrides/quartz/components/Explorer.tsx +198 -0
  129. brain/quartz_overrides/quartz/components/Footer.tsx +27 -0
  130. brain/quartz_overrides/quartz/components/Graph.tsx +468 -0
  131. brain/quartz_overrides/quartz/components/PageTitle.tsx +72 -0
  132. brain/quartz_overrides/quartz/components/RelatedDocs.tsx +38 -0
  133. brain/quartz_overrides/quartz/components/Search.tsx +161 -0
  134. brain/quartz_overrides/quartz/components/SummaryLede.tsx +72 -0
  135. brain/quartz_overrides/quartz/components/index.ts +92 -0
  136. brain/quartz_overrides/quartz/components/pages/TagContent.tsx +272 -0
  137. brain/quartz_overrides/quartz/components/scripts/commandPalette.inline.ts +665 -0
  138. brain/quartz_overrides/quartz/components/scripts/explorer.inline.ts +768 -0
  139. brain/quartz_overrides/quartz/components/scripts/graph.inline.ts +2302 -0
  140. brain/quartz_overrides/quartz/components/scripts/relatedDocs.inline.ts +163 -0
  141. brain/quartz_overrides/quartz/components/scripts/search.inline.ts +1011 -0
  142. brain/quartz_overrides/quartz/plugins/emitters/contentIndex.ts +546 -0
  143. brain/quartz_overrides/quartz/plugins/transformers/codeCopy.ts +94 -0
  144. brain/quartz_overrides/quartz/plugins/transformers/derivedFenceMark.ts +302 -0
  145. brain/quartz_overrides/quartz/plugins/transformers/emailThread.ts +148 -0
  146. brain/quartz_overrides/quartz/plugins/transformers/emptyDoorFilter.ts +213 -0
  147. brain/quartz_overrides/quartz/plugins/transformers/index.ts +114 -0
  148. brain/quartz_overrides/quartz/plugins/transformers/linkKindMark.ts +205 -0
  149. brain/quartz_overrides/quartz/plugins/transformers/linkSourceTag.ts +104 -0
  150. brain/quartz_overrides/quartz/plugins/transformers/relativeDate.ts +100 -0
  151. brain/quartz_overrides/quartz/plugins/transformers/reloadSignal.ts +131 -0
  152. brain/quartz_overrides/quartz/processors/parse.ts +371 -0
  153. brain/quartz_overrides/quartz/processors/parser_cache.ts +78 -0
  154. brain/quartz_overrides/quartz/static/brain-logo-dark.png +0 -0
  155. brain/quartz_overrides/quartz/static/brain-logo-light.png +0 -0
  156. brain/quartz_overrides/quartz/static/codeCopy.js +196 -0
  157. brain/quartz_overrides/quartz/static/emailThread.js +334 -0
  158. brain/quartz_overrides/quartz/static/favicon.ico +0 -0
  159. brain/quartz_overrides/quartz/static/icon.png +0 -0
  160. brain/quartz_overrides/quartz/static/linkSourceTag.js +104 -0
  161. brain/quartz_overrides/quartz/static/relativeDate.js +142 -0
  162. brain/quartz_overrides/quartz/static/reload.js +168 -0
  163. brain/quartz_overrides/quartz/styles/brain/_article.scss +252 -0
  164. brain/quartz_overrides/quartz/styles/brain/_atmosphere.scss +113 -0
  165. brain/quartz_overrides/quartz/styles/brain/_callouts.scss +180 -0
  166. brain/quartz_overrides/quartz/styles/brain/_cmdk.scss +7 -0
  167. brain/quartz_overrides/quartz/styles/brain/_code.scss +208 -0
  168. brain/quartz_overrides/quartz/styles/brain/_command_palette.scss +369 -0
  169. brain/quartz_overrides/quartz/styles/brain/_email_thread.scss +228 -0
  170. brain/quartz_overrides/quartz/styles/brain/_explorer.scss +142 -0
  171. brain/quartz_overrides/quartz/styles/brain/_home.scss +182 -0
  172. brain/quartz_overrides/quartz/styles/brain/_links.scss +322 -0
  173. brain/quartz_overrides/quartz/styles/brain/_marginalia.scss +117 -0
  174. brain/quartz_overrides/quartz/styles/brain/_motion.scss +175 -0
  175. brain/quartz_overrides/quartz/styles/brain/_people_hub.scss +100 -0
  176. brain/quartz_overrides/quartz/styles/brain/_related_docs.scss +137 -0
  177. brain/quartz_overrides/quartz/styles/brain/_search.scss +252 -0
  178. brain/quartz_overrides/quartz/styles/brain/_sidebar.scss +468 -0
  179. brain/quartz_overrides/quartz/styles/brain/_summary_lede.scss +56 -0
  180. brain/quartz_overrides/quartz/styles/brain/_surface.scss +43 -0
  181. brain/quartz_overrides/quartz/styles/brain/_tag_content.scss +118 -0
  182. brain/quartz_overrides/quartz/styles/brain/_tokens.scss +197 -0
  183. brain/quartz_overrides/quartz/styles/brain/_typography.scss +92 -0
  184. brain/quartz_overrides/quartz/styles/custom.scss +89 -0
  185. brain/quartz_overrides/quartz/styles/graph.scss +505 -0
  186. brain/quartz_overrides/quartz/util/ctx.ts +92 -0
  187. brain/quartz_overrides/quartz/util/fastpath_manifest.ts +608 -0
  188. brain/quartz_overrides/quartz/util/path.ts +358 -0
  189. brain/quartz_overrides/quartz/util/sourceIcons.ts +55 -0
  190. brain/quartz_overrides/quartz.config.ts +270 -0
  191. brain/quartz_overrides/quartz.layout.ts +314 -0
  192. brain/queries.py +1188 -0
  193. brain/rank_fusion.py +8 -0
  194. brain/resurface.py +210 -0
  195. brain/review/__init__.py +26 -0
  196. brain/review/emit.py +27 -0
  197. brain/review/queries.py +436 -0
  198. brain/review/render.py +196 -0
  199. brain/review/scans.py +355 -0
  200. brain/review/weekly.py +413 -0
  201. brain/search.py +704 -0
  202. brain/set_similarity.py +15 -0
  203. brain/setup.py +1205 -0
  204. brain/tags.py +56 -0
  205. brain/templates/Caddyfile.j2 +9 -0
  206. brain/templates/__init__.py +1 -0
  207. brain/templates/bin/__init__.py +1 -0
  208. brain/templates/bin/_brain-brief-fg.sh +25 -0
  209. brain/templates/bin/_brain-build-fg.sh +53 -0
  210. brain/templates/bin/_brain-watcher-fg.sh +65 -0
  211. brain/templates/bin/brain-down.sh +89 -0
  212. brain/templates/bin/brain-status.sh +83 -0
  213. brain/templates/bin/brain-up.sh +221 -0
  214. brain/templates/docker/age/Dockerfile +79 -0
  215. brain/templates/docker-compose.stock.yml.j2 +26 -0
  216. brain/templates/docker-compose.yml.j2 +34 -0
  217. brain/templates/env.example +190 -0
  218. brain/templates/launchd/__init__.py +1 -0
  219. brain/templates/launchd/com.brain.brief.plist.j2 +45 -0
  220. brain/templates/launchd/com.brain.build.plist.j2 +46 -0
  221. brain/templates/launchd/com.brain.watcher.plist.j2 +46 -0
  222. brain/templates/skill/SKILL.md +63 -0
  223. brain/templates/skill/__init__.py +1 -0
  224. brain/timeline.py +834 -0
  225. brain/todo.py +124 -0
  226. brain/uninstall.py +185 -0
  227. brain/vault/__init__.py +115 -0
  228. brain/vault/_atomic.py +25 -0
  229. brain/vault/daily_index.py +228 -0
  230. brain/vault/derived_links/__init__.py +50 -0
  231. brain/vault/derived_links/directory.py +683 -0
  232. brain/vault/derived_links/fence.py +408 -0
  233. brain/vault/derived_links/gws.py +64 -0
  234. brain/vault/derived_links/participants.py +143 -0
  235. brain/vault/derived_links/pass_runner.py +362 -0
  236. brain/vault/derived_links/rules.py +137 -0
  237. brain/vault/export.py +683 -0
  238. brain/vault/frontmatter.py +165 -0
  239. brain/vault/graph.py +620 -0
  240. brain/vault/graph_format.py +388 -0
  241. brain/vault/link_rewrite.py +235 -0
  242. brain/vault/links.py +260 -0
  243. brain/vault/note_builder.py +211 -0
  244. brain/vault/paths.py +55 -0
  245. brain/vault/quartz_overlay.py +236 -0
  246. brain/vault/rename.py +591 -0
  247. brain/vault/resolver.py +304 -0
  248. brain/vault/slug.py +127 -0
  249. brain/vault/sync.py +1513 -0
  250. brain/vault/sync_summaries.py +264 -0
  251. brain/vault/templates.py +145 -0
  252. brain/vault/watch.py +1052 -0
  253. brain/wiki/__init__.py +6 -0
  254. brain/wiki/_github_slugger.py +76 -0
  255. brain/wiki/_person_name.py +314 -0
  256. brain/wiki/build_homepage.py +541 -0
  257. brain/wiki/build_partial.py +273 -0
  258. brain/wiki/build_people.py +934 -0
  259. brain/wiki/build_related.py +758 -0
  260. brain/wiki/build_swap.py +585 -0
  261. brain/wiki/build_watcher.py +975 -0
  262. brain/wiki/edit_classifier.py +215 -0
  263. brain/wiki/errors.py +10 -0
  264. brain/wiki/fastpath_manifest.py +475 -0
  265. brain/wiki/fastpath_state.py +174 -0
  266. brain/wiki/install.py +296 -0
  267. brain/wiki/slug.py +111 -0
  268. secondbrain_py-0.2.1.dist-info/METADATA +195 -0
  269. secondbrain_py-0.2.1.dist-info/RECORD +273 -0
  270. secondbrain_py-0.2.1.dist-info/WHEEL +5 -0
  271. secondbrain_py-0.2.1.dist-info/entry_points.txt +11 -0
  272. secondbrain_py-0.2.1.dist-info/licenses/LICENSE +21 -0
  273. secondbrain_py-0.2.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,202 @@
1
+ """Derive-time edge weighting + generic-entity suppression (wave G1, GraphRAG).
2
+
3
+ Pure logic, no DB. Two derive-time concerns the spec assigns to this module
4
+ (spec §8 ``weighting.py — normalized lift + generic suppression``):
5
+
6
+ 1. **Normalized lift** — the single normative edge-weight metric, in ``(0, 1]``
7
+ (spec §4 D4: PMI rejected because it can go negative). The design names the
8
+ metric but not its formula; this module makes the canonical, spec-faithful
9
+ choice and documents the derivation:
10
+
11
+ lift(A, B) = P(A, B) / (P(A) · P(B)) # ∈ [0, ∞)
12
+
13
+ Given fixed marginals, the co-document count cannot exceed the rarer
14
+ entity's document count, so the maximum attainable lift is
15
+ ``1 / max(P(A), P(B))``. Dividing lift by that maximum normalizes it into
16
+ ``(0, 1]`` and — because the corpus size ``N`` cancels — collapses to a
17
+ clean document-count ratio:
18
+
19
+ normalized_lift(A, B) = co_df(A, B) / min(df(A), df(B)) # ∈ (0, 1]
20
+
21
+ where ``df(X)`` is X's document frequency and ``co_df(A, B)`` the count of
22
+ documents in which A and B co-occur. The value is ``1.0`` exactly when one
23
+ entity always appears with the other (its document set is a subset of the
24
+ other's), and strictly positive whenever they ever co-occur. This satisfies
25
+ the migration-012 ``CHECK (weight > 0 AND weight <= 1)`` by construction.
26
+
27
+ 2. **Generic-entity suppression** — entities that appear in almost every
28
+ document carry no thematic signal, so edges touching them are *dropped* at
29
+ derive time (spec §6b "exclude … generic"; §4 D4 "generic-suppressed at
30
+ derive time"). An entity is **generic** when its document frequency exceeds
31
+ the absolute cap ``round(GENERIC_DF × tenant_corpus_N)`` (spec §6b step 2;
32
+ ``BRAIN_GRAPH_GENERIC_DF`` default ``0.30``, spec §10). The spec frames this
33
+ as exclusion (drop), not down-weighting, so :func:`edge_weight` returns
34
+ ``None`` for a suppressed edge — the row is simply not materialized.
35
+
36
+ **Versioning.** :data:`WEIGHTING_VERSION` is the derive-time algorithm version
37
+ that feeds ``graph_index_state.suppress_ver`` (migration 012; spec §7 step 1).
38
+ A change to the weighting/suppression *semantics* bumps the constant; a change
39
+ to the suppression *config* (the ``GENERIC_DF`` ratio) changes the suppression
40
+ outcome too, so :func:`suppress_ver` folds both into the watermark string,
41
+ forcing a re-derive when either moves.
42
+ """
43
+ from __future__ import annotations
44
+
45
+ from ..errors import WeightingError
46
+
47
+ __all__ = [
48
+ "DEFAULT_GENERIC_DF",
49
+ "WEIGHTING_VERSION",
50
+ "edge_weight",
51
+ "generic_df_cap",
52
+ "is_generic_entity",
53
+ "is_suppressed_edge",
54
+ "normalized_lift",
55
+ "suppress_ver",
56
+ ]
57
+
58
+ # Derive-time weighting/suppression algorithm version. Feeds
59
+ # ``graph_index_state.suppress_ver`` (spec §7). Bump when the lift formula or
60
+ # the suppression rule changes so reconcile re-derives affected documents.
61
+ WEIGHTING_VERSION = "nlift-v1"
62
+
63
+ # Default generic-entity document-frequency ratio: an entity appearing in more
64
+ # than this fraction of the tenant corpus is treated as generic and its edges
65
+ # are suppressed. Mirrors ``BRAIN_GRAPH_GENERIC_DF`` (spec §10, default 0.30).
66
+ DEFAULT_GENERIC_DF = 0.30
67
+
68
+
69
+ def normalized_lift(co_doc_count: int, src_doc_count: int, dst_doc_count: int) -> float:
70
+ """Normalized lift in ``(0, 1]`` = ``co_doc_count / min(src_df, dst_df)``.
71
+
72
+ See the module docstring for the derivation. The result is provably in
73
+ ``(0, 1]`` once the guards below pass, so the migration-012 weight ``CHECK``
74
+ cannot be violated.
75
+
76
+ Args:
77
+ co_doc_count: Documents in which the two entities co-occur (``>= 1``).
78
+ src_doc_count: Document frequency of one endpoint (``>= 1``).
79
+ dst_doc_count: Document frequency of the other endpoint (``>= 1``).
80
+
81
+ Returns:
82
+ The normalized-lift edge weight.
83
+
84
+ Raises:
85
+ WeightingError: if ``co_doc_count < 1`` (no edge), either marginal is
86
+ ``< 1``, or ``co_doc_count`` exceeds ``min(src_df, dst_df)`` (a pair
87
+ cannot co-occur in more documents than its rarer entity appears in).
88
+ """
89
+ if co_doc_count < 1:
90
+ raise WeightingError(
91
+ f"co_doc_count must be >= 1 for an edge to exist (got {co_doc_count})"
92
+ )
93
+ if src_doc_count < 1 or dst_doc_count < 1:
94
+ raise WeightingError(
95
+ "endpoint document frequencies must be >= 1 "
96
+ f"(got src={src_doc_count}, dst={dst_doc_count})"
97
+ )
98
+ min_df = min(src_doc_count, dst_doc_count)
99
+ if co_doc_count > min_df:
100
+ raise WeightingError(
101
+ f"co_doc_count ({co_doc_count}) cannot exceed the rarer endpoint's "
102
+ f"document frequency ({min_df})"
103
+ )
104
+ return co_doc_count / min_df
105
+
106
+
107
+ def generic_df_cap(corpus_doc_count: int, generic_df_ratio: float = DEFAULT_GENERIC_DF) -> int:
108
+ """Absolute generic-entity document-frequency cap (spec §6b step 2).
109
+
110
+ ``round(corpus_doc_count × generic_df_ratio)``. An entity whose document
111
+ frequency exceeds this cap is generic (see :func:`is_generic_entity`).
112
+ ``round`` uses Python's banker's rounding (round-half-to-even), matching the
113
+ spec's plain "round".
114
+
115
+ Args:
116
+ corpus_doc_count: Number of documents in the tenant corpus (``>= 0``).
117
+ generic_df_ratio: Generic fraction in ``(0, 1]``
118
+ (default :data:`DEFAULT_GENERIC_DF`).
119
+
120
+ Returns:
121
+ The absolute document-frequency cap.
122
+
123
+ Raises:
124
+ WeightingError: if ``corpus_doc_count < 0`` or ``generic_df_ratio`` is
125
+ outside ``(0, 1]``.
126
+ """
127
+ if corpus_doc_count < 0:
128
+ raise WeightingError(
129
+ f"corpus_doc_count must be >= 0 (got {corpus_doc_count})"
130
+ )
131
+ if not 0.0 < generic_df_ratio <= 1.0:
132
+ raise WeightingError(
133
+ f"generic_df_ratio must be in (0, 1] (got {generic_df_ratio})"
134
+ )
135
+ return round(corpus_doc_count * generic_df_ratio)
136
+
137
+
138
+ def is_generic_entity(entity_doc_count: int, cap: int) -> bool:
139
+ """True when an entity's document frequency exceeds the generic ``cap``.
140
+
141
+ Strictly greater-than: an entity sitting exactly at the cap is kept (spec
142
+ §6b — only entities that co-occur with *almost everything* are excluded).
143
+ """
144
+ return entity_doc_count > cap
145
+
146
+
147
+ def is_suppressed_edge(src_doc_count: int, dst_doc_count: int, cap: int) -> bool:
148
+ """True when *either* endpoint is generic, so the edge is suppressed."""
149
+ return is_generic_entity(src_doc_count, cap) or is_generic_entity(dst_doc_count, cap)
150
+
151
+
152
+ def edge_weight(
153
+ co_doc_count: int,
154
+ src_doc_count: int,
155
+ dst_doc_count: int,
156
+ *,
157
+ cap: int,
158
+ ) -> float | None:
159
+ """Derive-time edge weight, or ``None`` when the edge is suppressed.
160
+
161
+ Returns ``None`` when either endpoint is generic (document frequency above
162
+ ``cap``); otherwise the :func:`normalized_lift` weight. The ``None`` signal
163
+ means "do not materialize this edge" (spec §6b excludes generic entities).
164
+
165
+ Args:
166
+ co_doc_count: Documents in which the two entities co-occur.
167
+ src_doc_count: Document frequency of one endpoint.
168
+ dst_doc_count: Document frequency of the other endpoint.
169
+ cap: Absolute generic cap (see :func:`generic_df_cap`).
170
+
171
+ Raises:
172
+ WeightingError: propagated from :func:`normalized_lift` for a
173
+ non-suppressed edge with impossible counts.
174
+ """
175
+ if is_suppressed_edge(src_doc_count, dst_doc_count, cap):
176
+ return None
177
+ return normalized_lift(co_doc_count, src_doc_count, dst_doc_count)
178
+
179
+
180
+ def suppress_ver(generic_df_ratio: float = DEFAULT_GENERIC_DF) -> str:
181
+ """Composite derive-time version for ``graph_index_state.suppress_ver``.
182
+
183
+ Folds the algorithm version (:data:`WEIGHTING_VERSION`) together with the
184
+ config that changes the suppression outcome (the ``GENERIC_DF`` ratio), so
185
+ a ratio change forces reconcile to re-derive (spec §7 watermark).
186
+
187
+ The ratio is rendered with ``repr`` (``!r``), i.e. Python's shortest
188
+ round-trippable float string, for two reasons:
189
+
190
+ * **Collision-free** — ``repr`` is injective over floats (two *distinct*
191
+ floats always render to distinct strings), so two different
192
+ ``GENERIC_DF`` ratios can never fold to the same ``suppress_ver`` and
193
+ silently skip a re-derive. The old ``:g`` format rounded to 6 significant
194
+ figures, so e.g. ``0.1234561`` and ``0.1234562`` both rendered
195
+ ``0.123456`` — a watermark collision that would skip a legitimate
196
+ suppression change.
197
+ * **Stable across spellings** — equal floats render identically
198
+ (``0.30`` and ``0.3`` are the same float ⇒ both ``0.3``), so an
199
+ equivalent re-spelling of the config does not spuriously force a
200
+ re-derive.
201
+ """
202
+ return f"{WEIGHTING_VERSION}:gdf={generic_df_ratio!r}"