ltcai 10.3.0 → 10.4.0

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 (143) hide show
  1. package/README.md +37 -36
  2. package/docs/CHANGELOG.md +77 -0
  3. package/docs/COMMUNITY_AND_PLUGINS.md +1 -1
  4. package/docs/DEVELOPMENT.md +1 -1
  5. package/docs/MYPY_BACKLOG.md +59 -92
  6. package/docs/ONBOARDING.md +1 -1
  7. package/docs/OPERATIONS.md +1 -1
  8. package/docs/SURFACE_PARITY.md +33 -6
  9. package/docs/TRUST_MODEL.md +1 -1
  10. package/docs/WHY_LATTICE.md +1 -1
  11. package/docs/kg-schema.md +1 -1
  12. package/lattice_brain/__init__.py +1 -1
  13. package/lattice_brain/context.py +23 -3
  14. package/lattice_brain/graph/_kg_common.py +164 -9
  15. package/lattice_brain/graph/_kg_contract.py +223 -0
  16. package/lattice_brain/graph/_kg_fsutil.py +1 -1
  17. package/lattice_brain/graph/curator.py +4 -4
  18. package/lattice_brain/graph/discovery.py +12 -1
  19. package/lattice_brain/graph/discovery_index.py +12 -1
  20. package/lattice_brain/graph/documents.py +17 -2
  21. package/lattice_brain/graph/ingest.py +12 -1
  22. package/lattice_brain/graph/proactive.py +9 -5
  23. package/lattice_brain/graph/projection.py +12 -1
  24. package/lattice_brain/graph/provenance.py +12 -1
  25. package/lattice_brain/graph/retrieval.py +12 -1
  26. package/lattice_brain/graph/retrieval_docgen.py +12 -1
  27. package/lattice_brain/graph/retrieval_reads.py +12 -1
  28. package/lattice_brain/graph/retrieval_vector.py +12 -1
  29. package/lattice_brain/graph/schema.py +6 -2
  30. package/lattice_brain/graph/write_master.py +12 -1
  31. package/lattice_brain/ingestion.py +1 -1
  32. package/lattice_brain/quality.py +5 -5
  33. package/lattice_brain/runtime/contracts.py +2 -1
  34. package/lattice_brain/runtime/multi_agent.py +1 -1
  35. package/lattice_brain/workflow.py +10 -7
  36. package/latticeai/__init__.py +1 -1
  37. package/latticeai/api/admin.py +2 -2
  38. package/latticeai/api/chat.py +19 -15
  39. package/latticeai/api/chat_agent_http.py +19 -7
  40. package/latticeai/api/chat_helpers.py +9 -8
  41. package/latticeai/api/chat_stream.py +2 -2
  42. package/latticeai/api/computer_use.py +2 -2
  43. package/latticeai/api/hooks.py +4 -2
  44. package/latticeai/api/local_files.py +8 -4
  45. package/latticeai/api/mcp.py +2 -2
  46. package/latticeai/api/models.py +5 -5
  47. package/latticeai/api/permissions.py +2 -2
  48. package/latticeai/api/search.py +5 -3
  49. package/latticeai/api/security_dashboard.py +3 -5
  50. package/latticeai/api/setup.py +3 -3
  51. package/latticeai/api/static_routes.py +7 -2
  52. package/latticeai/api/tools.py +9 -4
  53. package/latticeai/api/workspace.py +25 -22
  54. package/latticeai/app_factory.py +132 -1322
  55. package/latticeai/cli/entrypoint.py +1 -1
  56. package/latticeai/core/agent.py +28 -14
  57. package/latticeai/core/audit.py +5 -3
  58. package/latticeai/core/embedding_providers.py +19 -6
  59. package/latticeai/core/legacy_compatibility.py +1 -1
  60. package/latticeai/core/marketplace.py +2 -2
  61. package/latticeai/core/mcp_catalog.py +3 -1
  62. package/latticeai/core/model_compat.py +1 -1
  63. package/latticeai/core/plugins.py +1 -1
  64. package/latticeai/core/project_sessions.py +1 -1
  65. package/latticeai/core/run_explain.py +6 -4
  66. package/latticeai/core/workspace_os.py +0 -4
  67. package/latticeai/core/workspace_os_constants.py +1 -1
  68. package/latticeai/core/workspace_os_state.py +1 -1
  69. package/latticeai/core/workspace_snapshots.py +3 -3
  70. package/latticeai/integrations/telegram_bot.py +52 -3
  71. package/latticeai/models/router.py +61 -34
  72. package/latticeai/runtime/access_runtime.py +3 -1
  73. package/latticeai/runtime/build_phases.py +1417 -0
  74. package/latticeai/runtime/review_wiring.py +6 -1
  75. package/latticeai/runtime/runtime_context.py +276 -0
  76. package/latticeai/runtime/sso_config_runtime.py +1 -1
  77. package/latticeai/runtime/stages.py +8 -2
  78. package/latticeai/services/app_context.py +23 -0
  79. package/latticeai/services/architecture_readiness.py +1 -1
  80. package/latticeai/services/brain_intelligence.py +4 -2
  81. package/latticeai/services/cloud_streaming.py +5 -3
  82. package/latticeai/services/command_center.py +1 -1
  83. package/latticeai/services/evidence_actions.py +2 -1
  84. package/latticeai/services/hybrid_chat.py +1 -1
  85. package/latticeai/services/local_knowledge.py +25 -9
  86. package/latticeai/services/memory_service.py +8 -4
  87. package/latticeai/services/model_catalog.py +12 -12
  88. package/latticeai/services/model_engines.py +15 -17
  89. package/latticeai/services/model_loading.py +1 -1
  90. package/latticeai/services/model_runtime.py +31 -33
  91. package/latticeai/services/multimodal_streaming.py +10 -4
  92. package/latticeai/services/openai_compatible_adapter.py +4 -3
  93. package/latticeai/services/product_readiness.py +1 -1
  94. package/latticeai/services/run_executor.py +12 -3
  95. package/latticeai/services/search_service.py +1 -1
  96. package/latticeai/services/triggers.py +1 -1
  97. package/latticeai/setup/auto_setup.py +1 -1
  98. package/latticeai/setup/wizard.py +12 -3
  99. package/latticeai/tools/computer.py +3 -1
  100. package/latticeai/tools/documents.py +2 -2
  101. package/package.json +1 -1
  102. package/scripts/check_current_release_docs.mjs +1 -1
  103. package/src-tauri/Cargo.lock +1 -1
  104. package/src-tauri/Cargo.toml +1 -1
  105. package/src-tauri/tauri.conf.json +1 -1
  106. package/static/app/asset-manifest.json +36 -36
  107. package/static/app/assets/{Act-DrKIGZeJ.js → Act-BK__Y5VE.js} +1 -1
  108. package/static/app/assets/{AdminConsole-CEV1VefA.js → AdminConsole-ZrTyN8_F.js} +1 -1
  109. package/static/app/assets/{Brain-BZSXnwWU.js → Brain-CYHDXpPB.js} +1 -1
  110. package/static/app/assets/{BrainHome-CEBd86k6.js → BrainHome-bbJulb5g.js} +1 -1
  111. package/static/app/assets/{BrainSignals-O4s6R7av.js → BrainSignals-CesSPtUP.js} +1 -1
  112. package/static/app/assets/{Capture-AGbOxsg-.js → Capture-p5upXmz2.js} +1 -1
  113. package/static/app/assets/{CommandPalette-CeqBjCn2.js → CommandPalette-0zkyC0RY.js} +1 -1
  114. package/static/app/assets/{Library-CXnbNiQW.js → Library-Dt-vABI2.js} +1 -1
  115. package/static/app/assets/{LivingBrain-D9Iei2ZJ.js → LivingBrain-Bz-h6LBH.js} +1 -1
  116. package/static/app/assets/{ProductFlow-JlXiDfMM.js → ProductFlow-DAZ7DJCE.js} +1 -1
  117. package/static/app/assets/{ReviewCard-QQly2bTV.js → ReviewCard-Bv0WGbY6.js} +1 -1
  118. package/static/app/assets/{System-Bp9ZY4ER.js → System-BhrJQSD_.js} +1 -1
  119. package/static/app/assets/{activity-CxRPOQ2F.js → activity-AolpZJCH.js} +1 -1
  120. package/static/app/assets/{bot-1bcsWB2s.js → bot-CsVsT2nd.js} +1 -1
  121. package/static/app/assets/{brain-CxaeKCRM.js → brain-vuppco5y.js} +1 -1
  122. package/static/app/assets/{button-ZYFqGan7.js → button-1BP1PJZ3.js} +1 -1
  123. package/static/app/assets/{circle-pause-B5NOIqvk.js → circle-pause-5paeCOtq.js} +1 -1
  124. package/static/app/assets/{circle-play-BiJVFXf7.js → circle-play-By8mFYRV.js} +1 -1
  125. package/static/app/assets/{cpu-Vvq0OKTr.js → cpu-DvUQxdKe.js} +1 -1
  126. package/static/app/assets/{download-SEcXzkx1.js → download-CFqYG90a.js} +1 -1
  127. package/static/app/assets/{folder-open-Dw1ff_Qa.js → folder-open-CVSnp-Dp.js} +1 -1
  128. package/static/app/assets/{hard-drive-B00fR7_r.js → hard-drive-Ckr4Vd6-.js} +1 -1
  129. package/static/app/assets/{index-Dm9VXl1o.js → index--0zKF5Ow.js} +3 -3
  130. package/static/app/assets/{input-DtdWYQEa.js → input-D-JKquXU.js} +1 -1
  131. package/static/app/assets/{network-CpjBu0kI.js → network-DB88FZ2T.js} +1 -1
  132. package/static/app/assets/{primitives-CwsudBwc.js → primitives-D8U9OaIo.js} +1 -1
  133. package/static/app/assets/search-B8jsVTw_.js +1 -0
  134. package/static/app/assets/{shield-alert-CpjBc-gQ.js → shield-alert-DnsvO79P.js} +1 -1
  135. package/static/app/assets/{textarea-DUPyjHQs.js → textarea-4RchwbTa.js} +1 -1
  136. package/static/app/assets/{useFocusTrap-BWcMtCWH.js → useFocusTrap-CFJjy7cS.js} +1 -1
  137. package/static/app/assets/{useQuery-Dg5fPqTS.js → useQuery-3Reu-A4a.js} +1 -1
  138. package/static/app/assets/{users-BgcdTQGM.js → users-BTxYksxa.js} +1 -1
  139. package/static/app/assets/{utils-a3YXFtEO.js → utils-D02Vf-fw.js} +1 -1
  140. package/static/app/assets/{workspace-BMUEiXUm.js → workspace-CSJNgX-C.js} +1 -1
  141. package/static/app/index.html +3 -3
  142. package/static/sw.js +1 -1
  143. package/static/app/assets/search-CwEJWBEA.js +0 -1
@@ -29,9 +29,9 @@ from typing import Any, Dict, Iterable, Iterator, List, Optional, Tuple
29
29
  try:
30
30
  from .schema import EdgeType, KGStoreV2, NodeType, _exec_script
31
31
  except Exception: # pragma: no cover - v2 schema is optional at import time
32
- KGStoreV2 = None # type: ignore[assignment]
33
- NodeType = None # type: ignore[assignment]
34
- EdgeType = None # type: ignore[assignment]
32
+ KGStoreV2 = None # type: ignore[assignment,misc]
33
+ NodeType = None # type: ignore[assignment,misc]
34
+ EdgeType = None # type: ignore[assignment,misc]
35
35
  _exec_script = None # type: ignore[assignment]
36
36
 
37
37
  from ..embeddings import LocalEmbeddingModel
@@ -69,9 +69,33 @@ from ._kg_constants import ( # noqa: E402
69
69
  WINDOWS_EXCLUDED_NAMES,
70
70
  )
71
71
 
72
- # Pure fs/path/hash/classification helpers → ._kg_fsutil (re-exported so the
73
- # computed __all__ below still forwards them to the graph mixins).
74
- from ._kg_fsutil import * # noqa: E402,F401,F403
72
+ # Pure fs/path/hash/classification helpers → ._kg_fsutil, re-exported so the
73
+ # static __all__ below forwards them to the graph mixins. Listed explicitly
74
+ # rather than star-imported: a star import here made every name in this
75
+ # module unverifiable to both ruff and mypy.
76
+ from ._kg_fsutil import ( # noqa: E402,F401
77
+ _current_os_type,
78
+ _drive_id_for_path,
79
+ _excluded_directory_reason,
80
+ _file_category,
81
+ _is_hidden_path,
82
+ _is_relative_to,
83
+ _node_type_for_category,
84
+ _now,
85
+ _parse_iso,
86
+ _parser_type_for_category,
87
+ _path_fingerprint,
88
+ _path_parts_lower,
89
+ _recency_score,
90
+ _root_warning,
91
+ _safe_iso_from_stat_mtime,
92
+ _sample_file,
93
+ _sensitive_file_reason,
94
+ _sha256_bytes,
95
+ _sha256_text,
96
+ _size_limit_for_category,
97
+ _slug,
98
+ )
75
99
 
76
100
 
77
101
  def _clean_text(text: str) -> str:
@@ -613,9 +637,11 @@ def _llm_extract_concepts(text: str, limit: int = 12) -> Optional[List[str]]:
613
637
  return None
614
638
 
615
639
 
640
+ # Triples carry a numeric ``weight``/``confidence`` alongside string fields,
641
+ # so the value type is Any rather than str.
616
642
  def _llm_extract_triples(
617
643
  text: str, concepts: List[str], limit: int = 20
618
- ) -> Optional[List[Dict[str, str]]]:
644
+ ) -> Optional[List[Dict[str, Any]]]:
619
645
  router = get_llm_router()
620
646
  if not ENABLE_LLM_EXTRACTION or not router:
621
647
  return None
@@ -647,7 +673,7 @@ def _llm_extract_triples(
647
673
  raw = re.sub(r"\s*```$", "", raw)
648
674
  parsed = json.loads(raw)
649
675
  if isinstance(parsed, list):
650
- triples = []
676
+ triples: List[Dict[str, Any]] = []
651
677
  for item in parsed[:limit]:
652
678
  if isinstance(item, dict) and "subject" in item and "object" in item:
653
679
  relation = str(item.get("relation", "관련됨"))
@@ -1173,4 +1199,133 @@ def _topic_candidates(text: str, limit: int = 8) -> List[str]:
1173
1199
  return list(seen.values())[:limit]
1174
1200
 
1175
1201
 
1176
- __all__ = [name for name in globals() if not name.startswith("__")]
1202
+ # Static export list. This used to be `[name for name in globals() if not
1203
+ # name.startswith("__")]`, which is invisible to a type checker: every
1204
+ # `from ._kg_common import *` consumer then had *no* resolvable names, and
1205
+ # mypy reported ~750 spurious `name-defined` errors across the graph
1206
+ # package. `tests/unit/test_kg_common_exports.py` asserts this list still
1207
+ # equals what the computed expression would produce, so it cannot drift.
1208
+ __all__ = [
1209
+ "Any",
1210
+ "COMMON_EXCLUDED_DIRS",
1211
+ "COMMON_EXCLUDED_FILE_NAMES",
1212
+ "COMMON_EXCLUDED_FILE_SUFFIXES",
1213
+ "COOCCURRENCE_CONCEPT_LIMIT",
1214
+ "COOCCURRENCE_EDGE_WEIGHT",
1215
+ "Counter",
1216
+ "Dict",
1217
+ "EDGE_VERB",
1218
+ "ENABLE_LLM_EXTRACTION",
1219
+ "EdgeType",
1220
+ "GRAPH_SCHEMA_VERSION",
1221
+ "Iterable",
1222
+ "Iterator",
1223
+ "KGStoreV2",
1224
+ "LINUX_EXCLUDED_PREFIXES",
1225
+ "LOCAL_CODE_EXTENSIONS",
1226
+ "LOCAL_DOCUMENT_EXTENSIONS",
1227
+ "LOCAL_IMAGE_EXTENSIONS",
1228
+ "LOCAL_SIZE_LIMITS",
1229
+ "LOCAL_SLIDE_EXTENSIONS",
1230
+ "LOCAL_SPREADSHEET_EXTENSIONS",
1231
+ "LOCAL_SUPPORTED_EXTENSIONS",
1232
+ "LOCAL_TEXT_EXTENSIONS",
1233
+ "List",
1234
+ "LocalEmbeddingModel",
1235
+ "MACOS_EXCLUDED_PREFIXES",
1236
+ "NodeType",
1237
+ "Optional",
1238
+ "Path",
1239
+ "SENSITIVE_PATH_KEYWORDS",
1240
+ "Tuple",
1241
+ "VERB_EDGE_WEIGHT",
1242
+ "WINDOWS_EXCLUDED_NAMES",
1243
+ "_CHUNK_STRATEGIES",
1244
+ "_CODE_BLANK_RUN_RE",
1245
+ "_CODE_BOUNDARY_LINE_RE",
1246
+ "_CODE_CHUNK_EXTENSIONS",
1247
+ "_CONCEPT_STOP",
1248
+ "_KG_DB_FORMAT_KEY",
1249
+ "_KG_DB_FORMAT_VERSION",
1250
+ "_LLM_EXTRACT_CONCEPT_PROMPT",
1251
+ "_LLM_EXTRACT_TRIPLE_PROMPT",
1252
+ "_MARKDOWN_CHUNK_EXTENSIONS",
1253
+ "_MARKDOWN_HEADING_RE",
1254
+ "_MARKDOWN_MIN_SECTION_CHARS",
1255
+ "_NOT_PERSON_WORDS",
1256
+ "_PROJECTION_VERSION",
1257
+ "_PROSE_CHUNK_EXTENSIONS",
1258
+ "_PROSE_MIN_SPAN_RATIO",
1259
+ "_PROSE_STRONG_BOUNDARY_RE",
1260
+ "_PROSE_WEAK_BOUNDARY_RE",
1261
+ "_READ_FROM_V2_DEFAULT",
1262
+ "_V2_WRITE_MASTER_KEY",
1263
+ "_chunks",
1264
+ "_classify_node_type",
1265
+ "_clean_text",
1266
+ "_code_chunks",
1267
+ "_code_segment_spans",
1268
+ "_current_os_type",
1269
+ "_drive_id_for_path",
1270
+ "_excluded_directory_reason",
1271
+ "_exec_script",
1272
+ "_extract_concepts",
1273
+ "_extract_concepts_rules",
1274
+ "_extract_triples",
1275
+ "_extract_triples_rules",
1276
+ "_file_category",
1277
+ "_infer_edge",
1278
+ "_is_hidden_path",
1279
+ "_is_relative_to",
1280
+ "_json",
1281
+ "_last_boundary",
1282
+ "_llm_extract_concepts",
1283
+ "_llm_extract_triples",
1284
+ "_markdown_chunks",
1285
+ "_markdown_section_spans",
1286
+ "_merge_small_sections",
1287
+ "_node_type_for_category",
1288
+ "_now",
1289
+ "_parse_iso",
1290
+ "_parser_type_for_category",
1291
+ "_path_fingerprint",
1292
+ "_path_parts_lower",
1293
+ "_plain_windows",
1294
+ "_prose_chunks",
1295
+ "_recency_score",
1296
+ "_root_warning",
1297
+ "_safe_iso_from_stat_mtime",
1298
+ "_safe_loads",
1299
+ "_sample_file",
1300
+ "_semantic_items",
1301
+ "_sensitive_file_reason",
1302
+ "_sha256_bytes",
1303
+ "_sha256_text",
1304
+ "_size_limit_for_category",
1305
+ "_slug",
1306
+ "_topic_candidates",
1307
+ "asyncio",
1308
+ "chunk_strategy_for",
1309
+ "citation_locator",
1310
+ "contextmanager",
1311
+ "datetime",
1312
+ "get_llm_router",
1313
+ "hashlib",
1314
+ "infer_edge_relation",
1315
+ "json",
1316
+ "logging",
1317
+ "math",
1318
+ "os",
1319
+ "page_for_offset",
1320
+ "pdf_page_offsets",
1321
+ "platform",
1322
+ "quiet",
1323
+ "re",
1324
+ "set_llm_router",
1325
+ "shutil",
1326
+ "sqlite3",
1327
+ "time",
1328
+ "typed_chunk_meta_fields",
1329
+ "typed_chunks",
1330
+ "zipfile",
1331
+ ]
@@ -0,0 +1,223 @@
1
+ """The seam eleven graph mixins share.
2
+
3
+ ``KnowledgeGraphStore`` is assembled from eleven mixins that freely call each
4
+ other's methods — ``ingest`` calls ``_upsert_node`` from ``write_master``,
5
+ ``write_master`` calls ``_v2_project_node`` from ``projection``, everything
6
+ calls ``_connect`` from the store itself. That contract existed, but nowhere in
7
+ the code: a reader had to grep eleven files to learn what a mixin may assume,
8
+ and a type checker reported 229 ``attr-defined`` errors because from *its*
9
+ position each mixin is a bare class calling methods it does not have.
10
+
11
+ This module writes the contract down. It is **typing-only** — the mixins alias
12
+ it to ``object`` at runtime, so the MRO and every method resolution are
13
+ byte-for-byte what they were. What changes is that the shared surface is now 23
14
+ declared names in one file instead of an unwritten convention, and adding a
15
+ cross-mixin call without declaring it here is a type error.
16
+
17
+ Signatures mirror the real implementations; ``tests/unit/test_kg_contract.py``
18
+ asserts the concrete store still provides every member.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import sqlite3
24
+ from contextlib import AbstractContextManager
25
+ from pathlib import Path
26
+ from typing import Any, Dict, Iterable, List, Optional, Sequence, Tuple
27
+
28
+
29
+ class KnowledgeGraphCore:
30
+ """What any graph mixin may assume about ``self``.
31
+
32
+ Never instantiated and never inherited at runtime — see the module
33
+ docstring. Members are declared, not implemented, on purpose: the concrete
34
+ implementation lives in whichever mixin owns it.
35
+ """
36
+
37
+ # ── State owned by KnowledgeGraphStore.__init__ ──────────────────────────
38
+ db_path: Path
39
+ blob_dir: Path
40
+ storage_engine: Any
41
+ _embedding_model: Any
42
+ _read_from_v2: bool
43
+ _v2_projection_available: bool
44
+
45
+ # ── store.py: connection + read-table resolution ─────────────────────────
46
+ def _connect(self) -> AbstractContextManager[sqlite3.Connection]:
47
+ """Transactional connection that is *closed* when the block exits."""
48
+ raise NotImplementedError
49
+
50
+ def _read_tables(self) -> Tuple[str, ...]:
51
+ """``(nodes_table, edges_table)`` — legacy tables or the v2 views."""
52
+ raise NotImplementedError
53
+
54
+ # ── write_master.py: the single write door ───────────────────────────────
55
+ def _upsert_node(
56
+ self,
57
+ conn: sqlite3.Connection,
58
+ node_id: str,
59
+ node_type: str,
60
+ title: str,
61
+ summary: str = "",
62
+ metadata: Optional[Dict[str, Any]] = None,
63
+ raw: Optional[Dict[str, Any]] = None,
64
+ owner: Optional[str] = None,
65
+ workspace_id: Optional[str] = None,
66
+ visibility: Optional[str] = None,
67
+ ) -> str:
68
+ raise NotImplementedError
69
+
70
+ def _upsert_edge(
71
+ self,
72
+ conn: sqlite3.Connection,
73
+ from_node: str,
74
+ to_node: str,
75
+ edge_type: str,
76
+ weight: float = 1.0,
77
+ metadata: Optional[Dict[str, Any]] = None,
78
+ *,
79
+ legacy_label: Optional[str] = None,
80
+ ) -> str:
81
+ raise NotImplementedError
82
+
83
+ def _upsert_chunk(
84
+ self,
85
+ conn: sqlite3.Connection,
86
+ *,
87
+ chunk_id: str,
88
+ source_node: str,
89
+ text: str,
90
+ metadata: Optional[Dict[str, Any]] = None,
91
+ ) -> None:
92
+ raise NotImplementedError
93
+
94
+ def _upsert_vector_item(
95
+ self,
96
+ conn: sqlite3.Connection,
97
+ *,
98
+ item_id: str,
99
+ item_type: str,
100
+ source_node: str,
101
+ text: str,
102
+ metadata: Optional[Dict[str, Any]] = None,
103
+ ) -> bool:
104
+ raise NotImplementedError
105
+
106
+ def _vector_text_for_node(
107
+ self,
108
+ *,
109
+ title: str,
110
+ summary: str = "",
111
+ metadata: Optional[Dict[str, Any]] = None,
112
+ ) -> str:
113
+ raise NotImplementedError
114
+
115
+ # ── projection.py: legacy ↔ v2 projection + keyword index ────────────────
116
+ def _v2_project_node(
117
+ self,
118
+ conn: sqlite3.Connection,
119
+ node_id: str,
120
+ node_type: str,
121
+ title: str,
122
+ summary: Optional[str],
123
+ metadata_json: Optional[str],
124
+ *,
125
+ created_at: Optional[str] = None,
126
+ updated_at: Optional[str] = None,
127
+ owner: Optional[str] = None,
128
+ workspace_id: Optional[str] = None,
129
+ visibility: Optional[str] = None,
130
+ strict: bool = False,
131
+ ) -> None:
132
+ raise NotImplementedError
133
+
134
+ def _v2_project_edge(
135
+ self,
136
+ conn: sqlite3.Connection,
137
+ from_node: str,
138
+ to_node: str,
139
+ edge_type: str,
140
+ weight: float,
141
+ metadata_json: Optional[str],
142
+ *,
143
+ edge_id: Optional[str] = None,
144
+ created_at: Optional[str] = None,
145
+ strict: bool = False,
146
+ legacy_type: Optional[str] = None,
147
+ ) -> None:
148
+ raise NotImplementedError
149
+
150
+ def _v2_delete_nodes(self, conn: sqlite3.Connection, ids: Any) -> None:
151
+ raise NotImplementedError
152
+
153
+ def _v2_delete_edges_from(self, conn: sqlite3.Connection, node_id: str) -> None:
154
+ raise NotImplementedError
155
+
156
+ def _fts_match_ids(
157
+ self, conn: sqlite3.Connection, query: str, limit: int
158
+ ) -> List[str]:
159
+ raise NotImplementedError
160
+
161
+ # ── retrieval_reads.py: workspace scoping ────────────────────────────────
162
+ def filter_scoped_nodes(
163
+ self,
164
+ items: Any,
165
+ allowed_workspaces: Any,
166
+ *,
167
+ id_key: str = "id",
168
+ include_legacy_global: bool = False,
169
+ ) -> Any:
170
+ raise NotImplementedError
171
+
172
+ # ── documents.py: structured-document ingestion ──────────────────────────
173
+ def _document_structure(self, path: Path, ext: str) -> Dict[str, Any]:
174
+ raise NotImplementedError
175
+
176
+ def _ingest_structure_nodes(
177
+ self,
178
+ conn: sqlite3.Connection,
179
+ file_id: str,
180
+ filename: str,
181
+ structure: Dict[str, Any],
182
+ *,
183
+ owner: Optional[str] = None,
184
+ workspace_id: Optional[str] = None,
185
+ ) -> None:
186
+ raise NotImplementedError
187
+
188
+ # ── discovery.py / discovery_index.py: local filesystem indexing ─────────
189
+ def _iter_local_scan_entries(
190
+ self, root: Path, *, max_files: int
191
+ ) -> Iterable[Dict[str, Any]]:
192
+ raise NotImplementedError
193
+
194
+ def _local_file_decision(
195
+ self, path: Path, root: Path, stat: Any
196
+ ) -> Dict[str, Any]:
197
+ raise NotImplementedError
198
+
199
+ def _delete_local_file_graph(
200
+ self, conn: sqlite3.Connection, file_node_id: Optional[str]
201
+ ) -> None:
202
+ raise NotImplementedError
203
+
204
+ def _cleanup_local_graph_orphans(
205
+ self, conn: sqlite3.Connection, source_id: str
206
+ ) -> None:
207
+ raise NotImplementedError
208
+
209
+
210
+ #: Every name a mixin may reach for through ``self``. Kept in sync by
211
+ #: ``tests/unit/test_kg_contract.py`` — the test derives it from the class, so
212
+ #: this constant cannot drift from the declarations above.
213
+ KG_CORE_MEMBERS: Sequence[str] = tuple(
214
+ sorted(
215
+ name
216
+ for name in vars(KnowledgeGraphCore)
217
+ if not name.startswith("__")
218
+ )
219
+ + sorted(getattr(KnowledgeGraphCore, "__annotations__", {}))
220
+ )
221
+
222
+
223
+ __all__ = ["KG_CORE_MEMBERS", "KnowledgeGraphCore"]
@@ -192,7 +192,7 @@ def _excluded_directory_reason(
192
192
  if os_type == "macos":
193
193
  home_library = Path.home() / "Library"
194
194
  try:
195
- root_is_library = bool(root) and _is_relative_to(
195
+ root_is_library = root is not None and _is_relative_to(
196
196
  root.expanduser().resolve(), home_library.expanduser().resolve()
197
197
  )
198
198
  if (
@@ -525,7 +525,7 @@ def plan_concept_noise_reduction(
525
525
  remove: List[Dict[str, Any]] = []
526
526
  keep: List[Dict[str, Any]] = []
527
527
  for concept in concepts:
528
- entry = {
528
+ entry: Dict[str, Any] = {
529
529
  "id": str(concept.get("id") or ""),
530
530
  "label": concept.get("label"),
531
531
  "df": int(concept.get("df") or 0),
@@ -534,7 +534,7 @@ def plan_concept_noise_reduction(
534
534
  if not entry["heuristic"]:
535
535
  keep.append({**entry, "reason": "user_created_protected"})
536
536
  continue
537
- df = entry["df"]
537
+ df = int(entry["df"])
538
538
  if df < int(min_doc_frequency):
539
539
  df_ratio = (df / total) if total else 0.0
540
540
  remove.append({**entry, "df_ratio": round(df_ratio, 4), "reason": "below_frequency_floor"})
@@ -578,7 +578,7 @@ def plan_relation_noise_reduction(
578
578
  keep: List[Dict[str, Any]] = []
579
579
  demote: List[Dict[str, Any]] = []
580
580
  for edge in edges:
581
- entry = {
581
+ entry: Dict[str, Any] = {
582
582
  "id": str(edge.get("id") or ""),
583
583
  "from": edge.get("from"),
584
584
  "to": edge.get("to"),
@@ -600,7 +600,7 @@ def plan_relation_noise_reduction(
600
600
  if not entry["evidence"]:
601
601
  keep.append({**entry, "reason": "unknown_evidence"})
602
602
  continue
603
- if entry["weight"] < float(min_cooccurrence_weight):
603
+ if float(entry["weight"]) < float(min_cooccurrence_weight):
604
604
  demote.append({**entry, "reason": "weak_cooccurrence"})
605
605
  continue
606
606
  if degree > int(max_cooccurrence_degree):
@@ -1,12 +1,23 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from typing import TYPE_CHECKING
4
+
3
5
  from ..quiet import quiet
4
6
 
5
7
  # ruff: noqa: F403,F405
6
8
  from ._kg_common import * # noqa: F403,F401
7
9
 
10
+ # The cross-mixin surface (`_connect`, `_upsert_node`, …) is declared in
11
+ # `_kg_contract.KnowledgeGraphCore`. It is a typing-only base: at runtime this
12
+ # is `object`, so the MRO of `KnowledgeGraphStore` is unchanged.
13
+ if TYPE_CHECKING:
14
+ from ._kg_contract import KnowledgeGraphCore as _Core
15
+ else:
16
+ _Core = object
17
+
18
+
8
19
 
9
- class KnowledgeGraphDiscoveryMixin:
20
+ class KnowledgeGraphDiscoveryMixin(_Core):
10
21
  def discover_local_roots(self) -> Dict[str, Any]:
11
22
  """Return safe, cross-platform starting points for structure browsing."""
12
23
  os_type = _current_os_type()
@@ -1,8 +1,19 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from typing import TYPE_CHECKING
4
+
3
5
  # ruff: noqa: F403,F405
4
6
  from ._kg_common import * # noqa: F403,F401
5
7
 
8
+ # The cross-mixin surface (`_connect`, `_upsert_node`, …) is declared in
9
+ # `_kg_contract.KnowledgeGraphCore`. It is a typing-only base: at runtime this
10
+ # is `object`, so the MRO of `KnowledgeGraphStore` is unchanged.
11
+ if TYPE_CHECKING:
12
+ from ._kg_contract import KnowledgeGraphCore as _Core
13
+ else:
14
+ _Core = object
15
+
16
+
6
17
 
7
18
  def _local_scoped_slug(prefix: str, value: str, workspace_id: Optional[str]) -> str:
8
19
  """Preserve legacy IDs while isolating newly workspace-scoped nodes."""
@@ -13,7 +24,7 @@ def _local_scoped_slug(prefix: str, value: str, workspace_id: Optional[str]) ->
13
24
  return f"{prefix}:{scope}:{slug}"
14
25
 
15
26
 
16
- class KnowledgeGraphLocalIndexMixin:
27
+ class KnowledgeGraphLocalIndexMixin(_Core):
17
28
  """Local file → graph indexing (text extraction, node/index upserts,
18
29
  graph-node deletion, orphan cleanup, and the index_local_folder driver),
19
30
  split out of discovery. Composed into KnowledgeGraphStore alongside
@@ -1,12 +1,23 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from typing import TYPE_CHECKING
4
+
3
5
  from ..quiet import quiet
4
6
 
5
7
  # ruff: noqa: F403,F405
6
8
  from ._kg_common import * # noqa: F403,F401
7
9
 
10
+ # The cross-mixin surface (`_connect`, `_upsert_node`, …) is declared in
11
+ # `_kg_contract.KnowledgeGraphCore`. It is a typing-only base: at runtime this
12
+ # is `object`, so the MRO of `KnowledgeGraphStore` is unchanged.
13
+ if TYPE_CHECKING:
14
+ from ._kg_contract import KnowledgeGraphCore as _Core
15
+ else:
16
+ _Core = object
17
+
18
+
8
19
 
9
- class KnowledgeGraphDocumentsMixin:
20
+ class KnowledgeGraphDocumentsMixin(_Core):
10
21
  def find_documents_by_uri_prefix(self, prefix: str) -> List[Dict[str, Any]]:
11
22
  """Content nodes whose ``metadata.source_uri`` starts with ``prefix``.
12
23
 
@@ -246,7 +257,11 @@ class KnowledgeGraphDocumentsMixin:
246
257
 
247
258
  prs = Presentation(str(path))
248
259
  for slide_index, slide in enumerate(prs.slides, start=1):
249
- slide_info = {"index": slide_index, "shapes": [], "texts": []}
260
+ slide_info: Dict[str, Any] = {
261
+ "index": slide_index,
262
+ "shapes": [],
263
+ "texts": [],
264
+ }
250
265
  for shape_index, shape in enumerate(slide.shapes, start=1):
251
266
  shape_info = {
252
267
  "index": shape_index,
@@ -1,11 +1,22 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from typing import TYPE_CHECKING
4
+
3
5
  from ..quiet import quiet
4
6
  from ..sensitivity import stamp_sensitivity
5
7
 
6
8
  # ruff: noqa: F403,F405
7
9
  from ._kg_common import * # noqa: F403,F401
8
10
 
11
+ # The cross-mixin surface (`_connect`, `_upsert_node`, …) is declared in
12
+ # `_kg_contract.KnowledgeGraphCore`. It is a typing-only base: at runtime this
13
+ # is `object`, so the MRO of `KnowledgeGraphStore` is unchanged.
14
+ if TYPE_CHECKING:
15
+ from ._kg_contract import KnowledgeGraphCore as _Core
16
+ else:
17
+ _Core = object
18
+
19
+
9
20
 
10
21
  def _scoped_slug_id(prefix: str, value: str, workspace_id: Optional[str]) -> str:
11
22
  """Keep legacy IDs unchanged while isolating every newly scoped identity."""
@@ -42,7 +53,7 @@ def _triple_edge_metadata(triple: Dict[str, Any]) -> Dict[str, Any]:
42
53
  return metadata
43
54
 
44
55
 
45
- class KnowledgeGraphIngestMixin:
56
+ class KnowledgeGraphIngestMixin(_Core):
46
57
  def ingest_message(
47
58
  self,
48
59
  role: str,
@@ -191,11 +191,13 @@ class ProactiveBrain:
191
191
  for indices in token_index.values():
192
192
  if len(indices) < 2 or len(indices) > 50: # skip too-common tokens
193
193
  continue
194
- for i, left in enumerate(indices):
195
- for right in indices[i + 1:]:
196
- cooccur[(left, right)] = cooccur.get((left, right), 0) + 1
194
+ for i, left_idx in enumerate(indices):
195
+ for right_idx in indices[i + 1:]:
196
+ cooccur[(left_idx, right_idx)] = (
197
+ cooccur.get((left_idx, right_idx), 0) + 1
198
+ )
197
199
 
198
- near_pairs = []
200
+ near_pairs: List[Dict[str, Any]] = []
199
201
  for (li, ri), shared in sorted(cooccur.items(), key=lambda kv: -kv[1]):
200
202
  if shared < 3:
201
203
  continue
@@ -219,7 +221,7 @@ class ProactiveBrain:
219
221
  )
220
222
  if len(near_pairs) >= max_pairs:
221
223
  break
222
- near_pairs.sort(key=lambda p: p["similarity"], reverse=True)
224
+ near_pairs.sort(key=lambda p: float(p["similarity"]), reverse=True)
223
225
 
224
226
  return {
225
227
  "nodes_scanned": len(nodes),
@@ -467,6 +469,8 @@ class ProactiveBrain:
467
469
  mode = "applied"
468
470
  for group in groups:
469
471
  try:
472
+ if merge_fn is None: # guarded by apply_supported above
473
+ raise RuntimeError("store has no merge_nodes")
470
474
  result = merge_fn(group["keep"], group["remove"])
471
475
  applied.append({"keep": group["keep"], "result": result})
472
476
  except Exception as exc: # keep going; report per-group failure
@@ -1,10 +1,21 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from typing import TYPE_CHECKING
4
+
3
5
  from ..quiet import quiet
4
6
 
5
7
  # ruff: noqa: F403,F405
6
8
  from ._kg_common import * # noqa: F403,F401
7
9
 
10
+ # The cross-mixin surface (`_connect`, `_upsert_node`, …) is declared in
11
+ # `_kg_contract.KnowledgeGraphCore`. It is a typing-only base: at runtime this
12
+ # is `object`, so the MRO of `KnowledgeGraphStore` is unchanged.
13
+ if TYPE_CHECKING:
14
+ from ._kg_contract import KnowledgeGraphCore as _Core
15
+ else:
16
+ _Core = object
17
+
18
+
8
19
  # ── promotion review mode (review 2026-07-25 Wave 4) ─────────────────────────
9
20
  # When enabled, curate() parks would-be Topic promotions in graph_meta for a
10
21
  # human decision instead of writing them immediately. Explicit review_mode=
@@ -22,7 +33,7 @@ def _promotion_review_default() -> bool:
22
33
  return os.getenv(_PROMOTION_REVIEW_ENV, "").strip().lower() in ("1", "true", "yes")
23
34
 
24
35
 
25
- class KnowledgeGraphProjectionMixin:
36
+ class KnowledgeGraphProjectionMixin(_Core):
26
37
  _FTS_SQL = """
27
38
  CREATE VIRTUAL TABLE IF NOT EXISTS node_fts USING fts5(
28
39
  node_id UNINDEXED, title, summary, metadata, tokenize='trigram'
@@ -1,10 +1,21 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from typing import TYPE_CHECKING
4
+
3
5
  # ruff: noqa: F403,F405
4
6
  from ._kg_common import * # noqa: F403,F401
5
7
 
8
+ # The cross-mixin surface (`_connect`, `_upsert_node`, …) is declared in
9
+ # `_kg_contract.KnowledgeGraphCore`. It is a typing-only base: at runtime this
10
+ # is `object`, so the MRO of `KnowledgeGraphStore` is unchanged.
11
+ if TYPE_CHECKING:
12
+ from ._kg_contract import KnowledgeGraphCore as _Core
13
+ else:
14
+ _Core = object
15
+
16
+
6
17
 
7
- class KnowledgeGraphProvenanceMixin:
18
+ class KnowledgeGraphProvenanceMixin(_Core):
8
19
  def record_provenance(
9
20
  self,
10
21
  *,