ltcai 11.2.0 → 11.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 (247) hide show
  1. package/README.md +46 -53
  2. package/docs/CHANGELOG.md +61 -0
  3. package/docs/COMMUNITY_AND_PLUGINS.md +1 -1
  4. package/docs/DEVELOPMENT.md +1 -1
  5. package/docs/MULTI_AGENT_RUNTIME.md +1 -1
  6. package/docs/ONBOARDING.md +1 -1
  7. package/docs/OPERATIONS.md +6 -2
  8. package/docs/PERMISSION_MODE.md +1 -1
  9. package/docs/TRUST_MODEL.md +1 -1
  10. package/docs/WHY_LATTICE.md +1 -1
  11. package/docs/kg-schema.md +2 -2
  12. package/docs/v11.3.0_PLAN.md +202 -0
  13. package/docs/v11.4.0_RUST_FOUNDATION_PLAN.md +176 -0
  14. package/lattice_brain/__init__.py +1 -1
  15. package/lattice_brain/graph/_kg_common/__init__.py +287 -0
  16. package/lattice_brain/graph/_kg_common/extraction.py +516 -0
  17. package/lattice_brain/graph/_kg_common/relations.py +161 -0
  18. package/lattice_brain/graph/_kg_common/text.py +479 -0
  19. package/lattice_brain/graph/discovery_index/__init__.py +35 -0
  20. package/lattice_brain/graph/discovery_index/cleanup.py +182 -0
  21. package/lattice_brain/graph/discovery_index/extract.py +137 -0
  22. package/lattice_brain/graph/discovery_index/scan.py +411 -0
  23. package/lattice_brain/graph/discovery_index/upsert.py +495 -0
  24. package/lattice_brain/graph/projection/__init__.py +42 -0
  25. package/lattice_brain/graph/projection/curation.py +500 -0
  26. package/lattice_brain/graph/{projection.py → projection/v2_schema.py} +15 -477
  27. package/lattice_brain/graph/retrieval/__init__.py +54 -0
  28. package/lattice_brain/graph/retrieval/context.py +197 -0
  29. package/lattice_brain/graph/retrieval/graph_view.py +319 -0
  30. package/lattice_brain/graph/retrieval/hybrid.py +488 -0
  31. package/lattice_brain/graph/retrieval/maintenance.py +121 -0
  32. package/lattice_brain/graph/retrieval/signals.py +95 -0
  33. package/lattice_brain/graph/retrieval_vector/__init__.py +42 -0
  34. package/lattice_brain/graph/retrieval_vector/fingerprint.py +97 -0
  35. package/lattice_brain/graph/retrieval_vector/indexing.py +347 -0
  36. package/lattice_brain/graph/retrieval_vector/search.py +560 -0
  37. package/lattice_brain/graph/retrieval_vector/status.py +374 -0
  38. package/lattice_brain/ingestion/__init__.py +130 -0
  39. package/lattice_brain/ingestion/_contract.py +90 -0
  40. package/lattice_brain/ingestion/constants.py +127 -0
  41. package/lattice_brain/ingestion/folder_scan.py +57 -0
  42. package/lattice_brain/ingestion/folders.py +258 -0
  43. package/lattice_brain/ingestion/hashing.py +26 -0
  44. package/lattice_brain/ingestion/jobs_api.py +107 -0
  45. package/lattice_brain/ingestion/models.py +80 -0
  46. package/lattice_brain/ingestion/pipeline.py +486 -0
  47. package/lattice_brain/ingestion/quality.py +209 -0
  48. package/lattice_brain/ingestion/routing.py +295 -0
  49. package/lattice_brain/multimodal/__init__.py +164 -0
  50. package/lattice_brain/multimodal/audio.py +77 -0
  51. package/lattice_brain/multimodal/common.py +118 -0
  52. package/lattice_brain/multimodal/images.py +498 -0
  53. package/lattice_brain/multimodal/ports.py +169 -0
  54. package/lattice_brain/multimodal/video.py +410 -0
  55. package/lattice_brain/portability/__init__.py +90 -0
  56. package/lattice_brain/portability/_contract.py +42 -0
  57. package/lattice_brain/portability/backups.py +338 -0
  58. package/lattice_brain/portability/bundles.py +136 -0
  59. package/lattice_brain/portability/constants.py +93 -0
  60. package/lattice_brain/portability/fsops.py +138 -0
  61. package/lattice_brain/portability/service.py +41 -0
  62. package/lattice_brain/{portability.py → portability/sharing.py} +44 -677
  63. package/lattice_brain/runtime/__init__.py +1 -1
  64. package/lattice_brain/runtime/multi_agent.py +1 -1
  65. package/latticeai/__init__.py +1 -1
  66. package/latticeai/api/chronicle.py +63 -0
  67. package/latticeai/core/agent/__init__.py +93 -0
  68. package/latticeai/core/agent/_contract.py +79 -0
  69. package/latticeai/core/agent/context.py +57 -0
  70. package/latticeai/core/agent/deps.py +125 -0
  71. package/latticeai/core/agent/execution.py +622 -0
  72. package/latticeai/core/agent/planning.py +145 -0
  73. package/latticeai/core/agent/recovery.py +157 -0
  74. package/latticeai/core/agent/runtime.py +210 -0
  75. package/latticeai/core/agent/verification.py +231 -0
  76. package/latticeai/core/embedding_providers/__init__.py +151 -0
  77. package/latticeai/core/embedding_providers/base.py +199 -0
  78. package/latticeai/core/embedding_providers/captions.py +162 -0
  79. package/latticeai/core/embedding_providers/profiles.py +126 -0
  80. package/latticeai/core/embedding_providers/text.py +350 -0
  81. package/latticeai/core/embedding_providers/vision.py +352 -0
  82. package/latticeai/core/file_generation/__init__.py +115 -0
  83. package/latticeai/core/file_generation/bundles.py +76 -0
  84. package/latticeai/core/file_generation/extraction.py +154 -0
  85. package/latticeai/core/file_generation/inference.py +235 -0
  86. package/latticeai/core/file_generation/orchestration.py +152 -0
  87. package/latticeai/core/file_generation/prompting.py +117 -0
  88. package/latticeai/core/file_generation/repair.py +114 -0
  89. package/latticeai/core/file_generation/sanitize.py +61 -0
  90. package/latticeai/core/file_generation/validation.py +201 -0
  91. package/latticeai/core/legacy_compatibility.py +1 -1
  92. package/latticeai/core/marketplace.py +1 -1
  93. package/latticeai/core/messages.py +9 -0
  94. package/latticeai/core/workspace_os_constants.py +1 -1
  95. package/latticeai/integrations/telegram_bot/__init__.py +123 -0
  96. package/latticeai/integrations/telegram_bot/__main__.py +17 -0
  97. package/latticeai/integrations/telegram_bot/config.py +86 -0
  98. package/latticeai/integrations/telegram_bot/dispatch.py +311 -0
  99. package/latticeai/integrations/telegram_bot/flows.py +478 -0
  100. package/latticeai/integrations/telegram_bot/helpers.py +322 -0
  101. package/latticeai/integrations/telegram_bot/screens.py +394 -0
  102. package/latticeai/models/router/__init__.py +88 -0
  103. package/latticeai/models/router/_contract.py +66 -0
  104. package/latticeai/models/router/branding.py +56 -0
  105. package/latticeai/models/router/catalog.py +69 -0
  106. package/latticeai/models/router/documents.py +199 -0
  107. package/latticeai/models/router/errors.py +37 -0
  108. package/latticeai/models/router/generation.py +258 -0
  109. package/latticeai/models/router/loading.py +291 -0
  110. package/latticeai/models/router/local_models.py +85 -0
  111. package/latticeai/models/router/registry.py +147 -0
  112. package/latticeai/runtime/build_phases/__init__.py +82 -0
  113. package/latticeai/runtime/build_phases/features.py +407 -0
  114. package/latticeai/runtime/build_phases/foundation.py +555 -0
  115. package/latticeai/runtime/build_phases/web.py +492 -0
  116. package/latticeai/runtime/runtime_context.py +1 -0
  117. package/latticeai/services/architecture_readiness.py +48 -19
  118. package/latticeai/services/brain_intelligence/__init__.py +58 -0
  119. package/latticeai/services/brain_intelligence/_contract.py +71 -0
  120. package/latticeai/services/brain_intelligence/consistency.py +193 -0
  121. package/latticeai/services/brain_intelligence/constants.py +47 -0
  122. package/latticeai/services/brain_intelligence/digest.py +258 -0
  123. package/latticeai/services/brain_intelligence/health.py +331 -0
  124. package/latticeai/services/brain_intelligence/proposals.py +264 -0
  125. package/latticeai/services/brain_intelligence/sampling.py +84 -0
  126. package/latticeai/services/brain_intelligence/service.py +48 -0
  127. package/latticeai/services/chronicle.py +557 -0
  128. package/latticeai/services/memory_service/__init__.py +52 -0
  129. package/latticeai/services/memory_service/_contract.py +100 -0
  130. package/latticeai/services/memory_service/brief.py +431 -0
  131. package/latticeai/services/memory_service/constants.py +57 -0
  132. package/latticeai/services/memory_service/maintenance.py +138 -0
  133. package/latticeai/services/memory_service/manager.py +186 -0
  134. package/latticeai/services/memory_service/proof.py +136 -0
  135. package/latticeai/services/memory_service/recall.py +225 -0
  136. package/latticeai/services/memory_service/service.py +48 -0
  137. package/latticeai/services/memory_service/stores.py +110 -0
  138. package/latticeai/services/model_runtime/__init__.py +322 -0
  139. package/latticeai/services/model_runtime/cloud.py +87 -0
  140. package/latticeai/services/model_runtime/download.py +282 -0
  141. package/latticeai/services/model_runtime/engines.py +341 -0
  142. package/latticeai/services/model_runtime/loading.py +178 -0
  143. package/latticeai/services/model_runtime/service.py +129 -0
  144. package/latticeai/services/model_runtime/state.py +131 -0
  145. package/latticeai/services/model_runtime/status.py +255 -0
  146. package/latticeai/services/product_readiness.py +15 -7
  147. package/latticeai/setup/wizard/__init__.py +126 -0
  148. package/latticeai/setup/wizard/catalog.py +172 -0
  149. package/latticeai/setup/wizard/detect.py +323 -0
  150. package/latticeai/setup/wizard/install.py +348 -0
  151. package/latticeai/setup/wizard/paths.py +168 -0
  152. package/latticeai/setup/wizard/plans.py +74 -0
  153. package/latticeai/setup/wizard/recommend.py +320 -0
  154. package/package.json +6 -2
  155. package/scripts/bump_version.py +14 -0
  156. package/scripts/capture_release_evidence.mjs +33 -21
  157. package/scripts/check_current_release_docs.mjs +1 -1
  158. package/scripts/check_i18n_namespace_coverage.mjs +41 -4
  159. package/scripts/check_max_file_lines.mjs +102 -0
  160. package/scripts/check_release_evidence_bound.mjs +30 -15
  161. package/scripts/check_screenshot_pixel_delta.py +34 -4
  162. package/scripts/check_server_i18n.mjs +1 -0
  163. package/scripts/generate_rust_parity_fixtures.py +562 -0
  164. package/scripts/lib/mock_server_fingerprint.mjs +94 -0
  165. package/scripts/release_screen_claims.json +31 -2
  166. package/src-tauri/Cargo.lock +361 -3
  167. package/src-tauri/Cargo.toml +6 -1
  168. package/src-tauri/src/backend.rs +349 -0
  169. package/src-tauri/src/folder.rs +33 -0
  170. package/src-tauri/src/main.rs +97 -399
  171. package/src-tauri/tauri.conf.json +1 -1
  172. package/static/app/asset-manifest.json +41 -37
  173. package/static/app/assets/Act-yYpYnn0v.js +1 -0
  174. package/static/app/assets/AdminConsole-DL3Cr5pL.js +1 -0
  175. package/static/app/assets/{Brain-tuhI4sOC.js → Brain-C1HBN0Wf.js} +2 -2
  176. package/static/app/assets/BrainHome-DoXRhUUC.js +2 -0
  177. package/static/app/assets/BrainSignals-6yR6ir5t.js +1 -0
  178. package/static/app/assets/Capture-CFIRsFNE.js +1 -0
  179. package/static/app/assets/Chronicle-BZbEgiwN.js +1 -0
  180. package/static/app/assets/CommandPalette-D2pMxC2I.js +1 -0
  181. package/static/app/assets/Library-DwO3yZST.js +1 -0
  182. package/static/app/assets/{LivingBrain-DBwhto14.js → LivingBrain-Jn1GK0-S.js} +1 -1
  183. package/static/app/assets/ProductFlow-B-w1R4Oo.js +1 -0
  184. package/static/app/assets/ReviewCard-6B27X8Vg.js +3 -0
  185. package/static/app/assets/System-DW8F-2xL.js +1 -0
  186. package/static/app/assets/arrow-left-DXvKg9U6.js +1 -0
  187. package/static/app/assets/{bot-Cia42c2h.js → bot-IM_E_Y12.js} +1 -1
  188. package/static/app/assets/brain-Ci1CkWjM.js +1 -0
  189. package/static/app/assets/{button-2j2Ijzgq.js → button-COwyqfHM.js} +1 -1
  190. package/static/app/assets/circle-check-DfInj-qD.js +1 -0
  191. package/static/app/assets/{circle-pause-BEFeWpVW.js → circle-pause-DEM4A1Y5.js} +1 -1
  192. package/static/app/assets/{circle-play-ujXMcHxl.js → circle-play-C9djDuLd.js} +1 -1
  193. package/static/app/assets/{cpu-k4awryFq.js → cpu-DFdo1gw-.js} +1 -1
  194. package/static/app/assets/{download-DFbLJ_ig.js → download-SnJL6oqk.js} +1 -1
  195. package/static/app/assets/{folder-open-7y_b6xkM.js → folder-open-CqZeDkjE.js} +1 -1
  196. package/static/app/assets/{hard-drive-Bidh02Kr.js → hard-drive-j1jJXYYf.js} +1 -1
  197. package/static/app/assets/{index-DwDl9-8Y.css → index-BLPb5lmE.css} +1 -1
  198. package/static/app/assets/index-_u5iUHDr.js +10 -0
  199. package/static/app/assets/input-B0lPdRQZ.js +1 -0
  200. package/static/app/assets/link-2-CoFbooHS.js +1 -0
  201. package/static/app/assets/{permissionCopy-Bpb83Hx9.js → permissionCopy-BsyLxtao.js} +1 -1
  202. package/static/app/assets/primitives-DEbN-d6p.js +1 -0
  203. package/static/app/assets/search-BybIWPNd.js +1 -0
  204. package/static/app/assets/{share-2-BH1M-WNi.js → share-2-CVtZ_ewX.js} +1 -1
  205. package/static/app/assets/{shield-alert-BlKdBXcG.js → shield-alert-CBi2GNWM.js} +1 -1
  206. package/static/app/assets/{textarea-CCWbUfFB.js → textarea-DNMpB5ih.js} +1 -1
  207. package/static/app/assets/{useFocusTrap-YdHQ7pJ1.js → useFocusTrap-C83t3GXF.js} +1 -1
  208. package/static/app/assets/useMutation-DtbJDoyz.js +1 -0
  209. package/static/app/assets/{useQuery-CXQiwbVT.js → useQuery-Dcp1OChy.js} +1 -1
  210. package/static/app/assets/utils-BlZr7Pd4.js +4 -0
  211. package/static/app/assets/workspace-jJY4RuAV.js +1 -0
  212. package/static/app/index.html +4 -4
  213. package/static/sw.js +1 -1
  214. package/lattice_brain/graph/_kg_common.py +0 -1331
  215. package/lattice_brain/graph/discovery_index.py +0 -1141
  216. package/lattice_brain/graph/retrieval.py +0 -1120
  217. package/lattice_brain/graph/retrieval_vector.py +0 -1293
  218. package/lattice_brain/ingestion.py +0 -1525
  219. package/lattice_brain/multimodal.py +0 -1258
  220. package/latticeai/core/agent.py +0 -1465
  221. package/latticeai/core/embedding_providers.py +0 -1196
  222. package/latticeai/core/file_generation.py +0 -1047
  223. package/latticeai/integrations/telegram_bot.py +0 -1390
  224. package/latticeai/models/router.py +0 -1007
  225. package/latticeai/runtime/build_phases.py +0 -1450
  226. package/latticeai/services/brain_intelligence.py +0 -1083
  227. package/latticeai/services/memory_service.py +0 -1177
  228. package/latticeai/services/model_runtime.py +0 -1281
  229. package/latticeai/setup/wizard.py +0 -1310
  230. package/static/app/assets/Act-AWf0SAKp.js +0 -1
  231. package/static/app/assets/AdminConsole-D0u8Tiyj.js +0 -1
  232. package/static/app/assets/BrainHome-Ts7G_Ila.js +0 -2
  233. package/static/app/assets/BrainSignals-jMYgQ2Ar.js +0 -1
  234. package/static/app/assets/Capture-CqOSzyPr.js +0 -1
  235. package/static/app/assets/CommandPalette-DC0Bzh-I.js +0 -1
  236. package/static/app/assets/Library-CX-bbhmK.js +0 -1
  237. package/static/app/assets/ProductFlow-BHA2cfKI.js +0 -1
  238. package/static/app/assets/ReviewCard-BUhCKRNM.js +0 -3
  239. package/static/app/assets/System-Bu2t5hn1.js +0 -1
  240. package/static/app/assets/arrow-left-Dzwa5zRb.js +0 -1
  241. package/static/app/assets/brain-DJMoqrwx.js +0 -1
  242. package/static/app/assets/index-BpYkzcVm.js +0 -10
  243. package/static/app/assets/input-DSlJJxRs.js +0 -1
  244. package/static/app/assets/primitives-BCx6TvfG.js +0 -1
  245. package/static/app/assets/search-Cgy8cCFJ.js +0 -1
  246. package/static/app/assets/utils-zqPZJxdx.js +0 -4
  247. package/static/app/assets/workspace-DXTihhfU.js +0 -1
@@ -0,0 +1,71 @@
1
+ """The seam the Brain Intelligence mixins share.
2
+
3
+ ``BrainIntelligenceService`` is assembled from the proposal, sampling, health,
4
+ digest and consistency mixins. Each reads constructor state it does not own,
5
+ and several call methods another mixin implements — ``_graph_sample`` feeds
6
+ almost everything, ``_proactive`` gates the graph-layer surfaces, and the
7
+ garden view composes the contradiction scan. That contract existed as an
8
+ unwritten convention inside one class; this module writes it down.
9
+
10
+ Typing-only, exactly like :mod:`lattice_brain.portability._contract`: every
11
+ body below is a bare ``raise NotImplementedError`` that the composed class
12
+ always overrides, so the MRO and every method resolution stay what the
13
+ single-file class had. Adding a cross-mixin call without declaring it here is a
14
+ type error.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from typing import Any, Dict, List, Optional
20
+
21
+
22
+ class BrainIntelligenceCore:
23
+ """What any Brain Intelligence mixin may assume about ``self``.
24
+
25
+ Members are declared, not implemented: the implementation lives in
26
+ ``service.py`` (constructor state) or in whichever mixin owns the method.
27
+ """
28
+
29
+ # ── State owned by BrainIntelligenceService.__init__ ─────────────────────
30
+ _kg: Any
31
+ _memory: Any
32
+ _enable_graph: bool
33
+ _memory_quality: Any
34
+ _edge_quality: Any
35
+ _proactive_brain: Any
36
+ _review_queue_service: Any
37
+ _synthesizer: Any
38
+
39
+ # ── proposals.py: the review-queue seam every write door goes through ────
40
+ def _review_queue(self) -> Any:
41
+ raise NotImplementedError
42
+
43
+ def _synthesis(self) -> Any:
44
+ raise NotImplementedError
45
+
46
+ @staticmethod
47
+ def _no_queue(detail: str) -> Dict[str, Any]:
48
+ raise NotImplementedError
49
+
50
+ def importance_report(
51
+ self, *, user_email: Optional[str] = None, workspace_id: Optional[str] = None
52
+ ) -> Dict[str, Any]:
53
+ raise NotImplementedError
54
+
55
+ # ── sampling.py: the graph slice and the memory tier ─────────────────────
56
+ def _proactive(self) -> Any:
57
+ raise NotImplementedError
58
+
59
+ def _graph_sample(self, *, workspace_id: Optional[str]) -> Dict[str, Any]:
60
+ raise NotImplementedError
61
+
62
+ def _workspace_memories(
63
+ self, *, user_email: Optional[str], workspace_id: Optional[str]
64
+ ) -> List[Dict[str, Any]]:
65
+ raise NotImplementedError
66
+
67
+ # ── consistency.py: what the garden view composes ────────────────────────
68
+ def contradictions(
69
+ self, *, user_email: Optional[str] = None, workspace_id: Optional[str] = None
70
+ ) -> Dict[str, Any]:
71
+ raise NotImplementedError
@@ -0,0 +1,193 @@
1
+ """What the Brain disagrees with itself about, and what it says twice.
2
+
3
+ Two scans that wire the previously dormant quality layer
4
+ (:mod:`lattice_brain.quality`) to real stores:
5
+
6
+ * :meth:`contradictions` — four independent signals in one list: negation and
7
+ preference conflicts between workspace memories, temporal contradictions,
8
+ explicit ``CONTRADICTS`` edges already recorded in the graph, and the graph
9
+ layer's own node-content detector. Each item carries the signal that found
10
+ it, so a reviewer can weigh them differently.
11
+ * :meth:`consolidate` — duplicate memories and duplicate edges. Dry-run by
12
+ default, consent-first like every Brain automation. ``apply=True`` prunes
13
+ only exact duplicate *workspace memories*, through the audited MemoryService
14
+ path; graph content is never mutated here — the node merge plan is surfaced
15
+ for a governed apply path to adopt.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ from typing import Any, Dict, List, Optional
21
+
22
+ from latticeai.core.timeutil import now_iso as _now
23
+
24
+ from ._contract import BrainIntelligenceCore as _Core
25
+ from .constants import LOGGER
26
+
27
+
28
+ class BrainConsistencyMixin(_Core):
29
+ """Contradiction and consolidation scans. Mixed into the service."""
30
+
31
+ # ── contradictions ───────────────────────────────────────────────────
32
+
33
+ def contradictions(
34
+ self, *, user_email: Optional[str] = None, workspace_id: Optional[str] = None
35
+ ) -> Dict[str, Any]:
36
+ memories = self._workspace_memories(user_email=user_email, workspace_id=workspace_id)
37
+ memory_rows = [
38
+ {
39
+ "id": str(m.get("id") or f"mem-{i}"),
40
+ "content": str(m.get("content") or ""),
41
+ "score": 0.6,
42
+ "source": "workspace",
43
+ "timestamp": m.get("created_at") or m.get("timestamp") or 0,
44
+ }
45
+ for i, m in enumerate(memories)
46
+ if str(m.get("content") or "").strip()
47
+ ]
48
+
49
+ conflicts: List[Dict[str, Any]] = []
50
+ candidates = self._memory_quality.extract_candidates(memory_rows)
51
+ for candidate in self._memory_quality.detect_conflicts(candidates):
52
+ pair_conflicts = [c for c in candidate.conflicts if c.startswith("conflict:contradicts:")]
53
+ for marker in pair_conflicts:
54
+ other_id = marker.rsplit(":", 1)[-1]
55
+ if any(
56
+ c["kind"] == "memory_pair"
57
+ and {c["left_id"], c["right_id"]} == {candidate.id, other_id}
58
+ for c in conflicts
59
+ ):
60
+ continue
61
+ other: Dict[str, Any] | None = next(
62
+ (r for r in memory_rows if r["id"] == other_id), None
63
+ )
64
+ conflicts.append({
65
+ "kind": "memory_pair",
66
+ "left_id": candidate.id,
67
+ "left_content": candidate.content[:200],
68
+ "right_id": other_id,
69
+ "right_content": (other or {}).get("content", "")[:200],
70
+ "signal": "preference_negation",
71
+ })
72
+
73
+ temporal = self._memory_quality.detect_temporal_contradictions(memory_rows)
74
+ temporal_items = [
75
+ {
76
+ "kind": "temporal",
77
+ "id": item.get("id"),
78
+ "content": str(item.get("content") or "")[:200],
79
+ "signal": item.get("proactive_flag"),
80
+ }
81
+ for item in temporal
82
+ ]
83
+
84
+ edge_items: List[Dict[str, Any]] = []
85
+ sample = self._graph_sample(workspace_id=workspace_id)
86
+ for edge in sample["edges"]:
87
+ if "CONTRADICT" in str(edge.get("type") or "").upper():
88
+ edge_items.append({
89
+ "kind": "graph_edge",
90
+ "id": edge.get("id"),
91
+ "source": edge.get("source") or edge.get("from_node"),
92
+ "target": edge.get("target") or edge.get("to_node"),
93
+ "signal": "contradicts_edge",
94
+ })
95
+
96
+ # v9.6.x additive: graph-layer node-content contradictions (proactive
97
+ # detector over node title/summary), on top of the memory + edge scans.
98
+ graph_pair_items: List[Dict[str, Any]] = []
99
+ proactive = self._proactive()
100
+ if proactive is not None:
101
+ try:
102
+ graph_result = proactive.detect_contradictions(workspace_id=workspace_id)
103
+ graph_pair_items = [
104
+ {"kind": "graph_node_pair", **pair}
105
+ for pair in graph_result.get("node_pairs") or []
106
+ ]
107
+ except Exception:
108
+ LOGGER.exception("graph contradiction scan failed")
109
+
110
+ items = conflicts + temporal_items + edge_items + graph_pair_items
111
+ return {
112
+ "items": items,
113
+ "count": len(items),
114
+ "sources": {
115
+ "memory_pairs": len(conflicts),
116
+ "temporal": len(temporal_items),
117
+ "graph_edges": len(edge_items),
118
+ "graph_node_pairs": len(graph_pair_items),
119
+ },
120
+ "memories_scanned": len(memory_rows),
121
+ "generated_at": _now(),
122
+ }
123
+
124
+ # ── consolidation ────────────────────────────────────────────────────
125
+
126
+ def consolidate(
127
+ self,
128
+ *,
129
+ apply: bool = False,
130
+ user_email: Optional[str] = None,
131
+ workspace_id: Optional[str] = None,
132
+ ) -> Dict[str, Any]:
133
+ memories = self._workspace_memories(user_email=user_email, workspace_id=workspace_id)
134
+ memory_rows = [
135
+ {
136
+ "id": str(m.get("id") or f"mem-{i}"),
137
+ "content": str(m.get("content") or ""),
138
+ "score": 0.6,
139
+ "source": "workspace",
140
+ }
141
+ for i, m in enumerate(memories)
142
+ if str(m.get("content") or "").strip()
143
+ ]
144
+ candidates = self._memory_quality.extract_candidates(memory_rows)
145
+ kept = self._memory_quality.dedupe(candidates)
146
+ kept_ids = {c.id for c in kept}
147
+ duplicate_memory_ids = [c.id for c in candidates if c.id not in kept_ids]
148
+
149
+ sample = self._graph_sample(workspace_id=workspace_id)
150
+ duplicate_edge_ids = [
151
+ edge_id
152
+ for edge_id in self._edge_quality.detect_duplicate_edges(sample["edges"])
153
+ if edge_id
154
+ ]
155
+
156
+ pruned = 0
157
+ if apply and duplicate_memory_ids and self._memory is not None:
158
+ try:
159
+ result = self._memory.prune(
160
+ ids=duplicate_memory_ids,
161
+ user_email=user_email,
162
+ workspace_id=workspace_id,
163
+ )
164
+ pruned = int(result.get("count") or 0)
165
+ except Exception:
166
+ LOGGER.exception("consolidation prune failed")
167
+
168
+ # v9.6.x additive: graph-layer node merge plan. Always dry-run from
169
+ # this service — graph content changes stay proposal-first; the plan
170
+ # is surfaced so a governed apply path can adopt it later.
171
+ graph_consolidation: Optional[Dict[str, Any]] = None
172
+ proactive = self._proactive()
173
+ if proactive is not None:
174
+ try:
175
+ graph_consolidation = proactive.consolidate_duplicates(
176
+ workspace_id=workspace_id, dry_run=True
177
+ )
178
+ except Exception:
179
+ LOGGER.exception("graph consolidation plan failed")
180
+
181
+ return {
182
+ "mode": "applied" if apply else "dry_run",
183
+ "memories_scanned": len(memory_rows),
184
+ "duplicate_memories": duplicate_memory_ids,
185
+ "duplicate_memory_count": len(duplicate_memory_ids),
186
+ "pruned": pruned,
187
+ # Graph edges are reported for review only; consolidation never
188
+ # mutates graph content directly.
189
+ "duplicate_edges": duplicate_edge_ids[:50],
190
+ "duplicate_edge_count": len(duplicate_edge_ids),
191
+ "graph_consolidation": graph_consolidation,
192
+ "generated_at": _now(),
193
+ }
@@ -0,0 +1,47 @@
1
+ """Windows, sample sizes, and the two readings every surface shares.
2
+
3
+ Leaf module: literals and pure functions only, no service and no store. The
4
+ health report, the insights digest, the garden and the contradiction scan all
5
+ measure against the same windows, which is what makes their numbers
6
+ comparable.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import logging
12
+ from datetime import datetime, timezone
13
+ from typing import Any, Optional
14
+
15
+ LOGGER = logging.getLogger(__name__)
16
+
17
+ _STALE_DAYS = 45
18
+ _RECENT_DAYS = 7
19
+ _GRAPH_SAMPLE_LIMIT = 800
20
+
21
+
22
+ def _parse_ts(value: Any) -> Optional[datetime]:
23
+ text = str(value or "").strip()
24
+ if not text:
25
+ return None
26
+ try:
27
+ parsed = datetime.fromisoformat(text.replace("Z", "+00:00"))
28
+ except ValueError:
29
+ return None
30
+ if parsed.tzinfo is None:
31
+ parsed = parsed.replace(tzinfo=timezone.utc)
32
+ return parsed
33
+
34
+
35
+ def _no_graph_reason(graph_available: bool) -> str:
36
+ """Why a graph-derived health dimension has nothing to say.
37
+
38
+ Two different situations that both end in ``status: "unavailable"``: the
39
+ graph could not be read at all, and the graph read fine but holds nothing
40
+ yet. Telling them apart is the difference between "something is broken"
41
+ and "you have not saved anything yet".
42
+ """
43
+ return (
44
+ "no knowledge saved yet"
45
+ if graph_available
46
+ else "the knowledge graph could not be read"
47
+ )
@@ -0,0 +1,258 @@
1
+ """What the Brain would tell you if you asked "what's going on in there?"
2
+
3
+ Three read-only views over the same graph slice, each answering a different
4
+ shape of that question:
5
+
6
+ * :meth:`insights` — the proactive digest: recent growth, trending types, stale
7
+ knowledge, orphans, and questions grounded in real node titles;
8
+ * :meth:`garden_overview` — the same knowledge as four beds a gardener tends
9
+ (new growth, weeds, neglect, load-bearing), because "how healthy is my
10
+ knowledge?" in aggregate is not a question anyone acts on;
11
+ * :meth:`graph_duplicates` / :meth:`quality_report` — the graph layer's own
12
+ proactive scans, surfaced without applying anything.
13
+
14
+ Honest when empty: an unavailable graph produces empty beds and
15
+ ``available: False``, never invented plants.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ from datetime import datetime, timedelta, timezone
21
+ from typing import Any, Dict, List, Optional
22
+
23
+ from latticeai.core.timeutil import now_iso as _now
24
+
25
+ from ._contract import BrainIntelligenceCore as _Core
26
+ from .constants import _RECENT_DAYS, _STALE_DAYS, LOGGER, _parse_ts
27
+
28
+
29
+ class BrainDigestMixin(_Core):
30
+ """Insights, garden, and the graph-layer quality reads."""
31
+
32
+ # ── insights digest ──────────────────────────────────────────────────
33
+
34
+ def insights(
35
+ self, *, user_email: Optional[str] = None, workspace_id: Optional[str] = None
36
+ ) -> Dict[str, Any]:
37
+ sample = self._graph_sample(workspace_id=workspace_id)
38
+ nodes, edges = sample["nodes"], sample["edges"]
39
+ now = datetime.now(timezone.utc)
40
+ recent_cutoff = now - timedelta(days=_RECENT_DAYS)
41
+ stale_cutoff = now - timedelta(days=_STALE_DAYS)
42
+
43
+ recent_nodes: List[Dict[str, Any]] = []
44
+ stale_nodes: List[Dict[str, Any]] = []
45
+ type_counts: Dict[str, int] = {}
46
+ for node in nodes:
47
+ node_type = str(node.get("type") or "node")
48
+ ts = _parse_ts(node.get("updated_at"))
49
+ if ts is not None and ts >= recent_cutoff:
50
+ recent_nodes.append(node)
51
+ type_counts[node_type] = type_counts.get(node_type, 0) + 1
52
+ elif ts is not None and ts < stale_cutoff:
53
+ stale_nodes.append(node)
54
+
55
+ connected = set()
56
+ for edge in edges:
57
+ connected.add(str(edge.get("source") or edge.get("from_node") or ""))
58
+ connected.add(str(edge.get("target") or edge.get("to_node") or ""))
59
+ orphans = [n for n in nodes if str(n.get("id")) not in connected]
60
+
61
+ def _slim(node: Dict[str, Any]) -> Dict[str, Any]:
62
+ return {
63
+ "id": node.get("id"),
64
+ "type": node.get("type"),
65
+ "title": str(node.get("title") or "")[:120],
66
+ "updated_at": node.get("updated_at"),
67
+ }
68
+
69
+ trending = sorted(type_counts.items(), key=lambda kv: kv[1], reverse=True)[:5]
70
+ suggested_questions = [
71
+ f"{str(node.get('title') or '').strip()[:60]}에 대해 지금까지 알고 있는 것을 정리해줘"
72
+ for node in recent_nodes[:3]
73
+ if str(node.get("title") or "").strip()
74
+ ]
75
+
76
+ return {
77
+ "window_days": _RECENT_DAYS,
78
+ "activity": {
79
+ "recent_nodes": len(recent_nodes),
80
+ "recent_samples": [_slim(n) for n in recent_nodes[:8]],
81
+ "trending_types": [{"type": t, "count": c} for t, c in trending],
82
+ },
83
+ "attention": {
84
+ "stale_nodes": len(stale_nodes),
85
+ "stale_samples": [_slim(n) for n in stale_nodes[:8]],
86
+ "orphan_nodes": len(orphans),
87
+ "orphan_samples": [_slim(n) for n in orphans[:8]],
88
+ },
89
+ "suggested_questions": suggested_questions,
90
+ "graph_available": sample["available"],
91
+ "generated_at": _now(),
92
+ }
93
+
94
+ def garden_overview(
95
+ self, *, user_email: Optional[str] = None, workspace_id: Optional[str] = None,
96
+ limit: int = 8,
97
+ ) -> Dict[str, Any]:
98
+ """The knowledge garden in four beds (v9.9.7).
99
+
100
+ Living Brain answers "how healthy is my knowledge?" in aggregate.
101
+ A gardener asks four concrete questions instead, and this answers all
102
+ four from one workspace-scoped graph sample plus the memory tier:
103
+
104
+ * **recent** — what came in lately (the garden's new growth);
105
+ * **contradictions** — what disagrees with itself (needs weeding);
106
+ * **stale** — what has not been touched in a long time;
107
+ * **frequent** — what the rest of the graph leans on most (by degree).
108
+
109
+ Read-only and honest: when the graph is unavailable every bed is empty
110
+ and ``available`` is false — the view never invents plants.
111
+ """
112
+ # Explicit 0 clamps to 1 — `limit or 8` would silently re-expand it.
113
+ try:
114
+ limit = max(1, min(int(limit), 50))
115
+ except (TypeError, ValueError):
116
+ limit = 8
117
+ sample = self._graph_sample(workspace_id=workspace_id)
118
+ nodes, edges = sample["nodes"], sample["edges"]
119
+ now = datetime.now(timezone.utc)
120
+ recent_cutoff = now - timedelta(days=_RECENT_DAYS)
121
+ stale_cutoff = now - timedelta(days=_STALE_DAYS)
122
+
123
+ def _slim(node: Dict[str, Any], **extra: Any) -> Dict[str, Any]:
124
+ return {
125
+ "id": node.get("id"),
126
+ "type": node.get("type"),
127
+ "title": str(node.get("title") or "")[:120],
128
+ "updated_at": node.get("updated_at"),
129
+ **extra,
130
+ }
131
+
132
+ recent: List[Dict[str, Any]] = []
133
+ stale: List[Dict[str, Any]] = []
134
+ for node in nodes:
135
+ # Chunks are retrieval plumbing, not knowledge a gardener tends.
136
+ if str(node.get("type") or "") == "Chunk":
137
+ continue
138
+ ts = _parse_ts(node.get("updated_at"))
139
+ if ts is None:
140
+ continue
141
+ if ts >= recent_cutoff:
142
+ recent.append(node)
143
+ elif ts < stale_cutoff:
144
+ stale.append(node)
145
+ recent.sort(key=lambda n: str(n.get("updated_at") or ""), reverse=True)
146
+ stale.sort(key=lambda n: str(n.get("updated_at") or ""))
147
+
148
+ # "Frequent" is degree, not a guess: how many relations actually point
149
+ # at a node. Chunks are retrieval plumbing, never garden plants.
150
+ degree: Dict[str, int] = {}
151
+ for edge in edges:
152
+ for key in ("source", "target"):
153
+ node_id = str(edge.get(key) or "")
154
+ if node_id:
155
+ degree[node_id] = degree.get(node_id, 0) + 1
156
+ by_id = {str(node.get("id")): node for node in nodes}
157
+ frequent = [
158
+ _slim(by_id[node_id], degree=count)
159
+ for node_id, count in sorted(degree.items(), key=lambda kv: kv[1], reverse=True)
160
+ if node_id in by_id and str(by_id[node_id].get("type") or "") != "Chunk"
161
+ ][:limit]
162
+
163
+ contradiction_items: List[Dict[str, Any]] = []
164
+ contradiction_count = 0
165
+ try:
166
+ found = self.contradictions(user_email=user_email, workspace_id=workspace_id)
167
+ items = found.get("items") if isinstance(found, dict) else None
168
+ if isinstance(items, list):
169
+ contradiction_count = len(items)
170
+ contradiction_items = items[:limit]
171
+ except Exception: # noqa: BLE001 — one empty bed, never a broken view
172
+ LOGGER.exception("garden overview contradictions failed")
173
+
174
+ return {
175
+ "available": sample["available"],
176
+ "window_days": _RECENT_DAYS,
177
+ "stale_threshold_days": _STALE_DAYS,
178
+ "beds": {
179
+ "recent": {
180
+ "count": len(recent),
181
+ "items": [_slim(node) for node in recent[:limit]],
182
+ },
183
+ "contradictions": {
184
+ "count": contradiction_count,
185
+ "items": contradiction_items,
186
+ },
187
+ "stale": {
188
+ "count": len(stale),
189
+ "items": [_slim(node) for node in stale[:limit]],
190
+ },
191
+ "frequent": {
192
+ "count": len(frequent),
193
+ "items": frequent,
194
+ },
195
+ },
196
+ "generated_at": _now(),
197
+ }
198
+
199
+ # ── graph-layer proactive quality (v9.6.x) ───────────────────────────
200
+
201
+ def graph_duplicates(
202
+ self, *, user_email: Optional[str] = None, workspace_id: Optional[str] = None
203
+ ) -> Dict[str, Any]:
204
+ """Duplicate graph nodes (exact groups + near pairs) — read only."""
205
+ proactive = self._proactive()
206
+ if proactive is None:
207
+ return {
208
+ "available": False,
209
+ "exact_groups": [],
210
+ "near_pairs": [],
211
+ "exact_duplicate_nodes": 0,
212
+ "nodes_scanned": 0,
213
+ "generated_at": _now(),
214
+ }
215
+ try:
216
+ result = dict(proactive.find_duplicates(workspace_id=workspace_id))
217
+ except Exception as exc:
218
+ LOGGER.exception("graph duplicates scan failed")
219
+ return {
220
+ "available": False,
221
+ "error": str(exc),
222
+ "exact_groups": [],
223
+ "near_pairs": [],
224
+ "exact_duplicate_nodes": 0,
225
+ "nodes_scanned": 0,
226
+ "generated_at": _now(),
227
+ }
228
+ result["available"] = True
229
+ result["generated_at"] = _now()
230
+ return result
231
+
232
+ def quality_report(
233
+ self, *, user_email: Optional[str] = None, workspace_id: Optional[str] = None
234
+ ) -> Dict[str, Any]:
235
+ """Combined graph quality report: duplicates, contradictions, stale
236
+ nodes, edge quality — one workspace-scoped graph sample."""
237
+ proactive = self._proactive()
238
+ if proactive is None:
239
+ return {"available": False, "generated_at": _now()}
240
+ try:
241
+ result = dict(proactive.quality_report(workspace_id=workspace_id))
242
+ except Exception as exc:
243
+ LOGGER.exception("graph quality report failed")
244
+ return {"available": False, "error": str(exc), "generated_at": _now()}
245
+ result["available"] = True
246
+ # v11.1.0: decay is part of quality, and "the Brain is tidying up" is a
247
+ # state the user is entitled to see rather than a background surprise.
248
+ importance = self.importance_report(
249
+ user_email=user_email, workspace_id=workspace_id
250
+ )
251
+ candidates = len(importance.get("candidates") or [])
252
+ result["importance"] = importance
253
+ result["tidying"] = bool(importance.get("available")) and candidates > 0
254
+ summary = dict(result.get("summary") or {})
255
+ summary["consolidation_candidates"] = candidates
256
+ result["summary"] = summary
257
+ result["generated_at"] = _now()
258
+ return result