ltcai 11.1.0 → 11.2.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 (118) hide show
  1. package/README.md +53 -53
  2. package/docs/CHANGELOG.md +33 -0
  3. package/docs/COMMUNITY_AND_PLUGINS.md +1 -1
  4. package/docs/DEVELOPMENT.md +1 -1
  5. package/docs/FEATURE_AUDIT_v11.2.0.md +393 -0
  6. package/docs/LAYOUT_REBUILD_SPEC.md +9 -1
  7. package/docs/ONBOARDING.md +1 -1
  8. package/docs/OPERATIONS.md +1 -1
  9. package/docs/TRUST_MODEL.md +1 -1
  10. package/docs/WHY_LATTICE.md +1 -1
  11. package/docs/architecture.md +6 -2
  12. package/docs/kg-schema.md +1 -1
  13. package/lattice_brain/__init__.py +1 -1
  14. package/lattice_brain/gates.py +125 -0
  15. package/lattice_brain/graph/fusion.py +35 -4
  16. package/lattice_brain/graph/projection.py +66 -8
  17. package/lattice_brain/graph/schema.py +9 -0
  18. package/lattice_brain/graph/store.py +9 -0
  19. package/lattice_brain/graph/vector_index/selector.py +32 -2
  20. package/lattice_brain/ingestion.py +175 -27
  21. package/lattice_brain/multimodal.py +525 -5
  22. package/lattice_brain/portability.py +169 -32
  23. package/lattice_brain/runtime/multi_agent.py +1 -1
  24. package/lattice_brain/sealed_box.py +244 -0
  25. package/lattice_brain/synthesis.py +24 -1
  26. package/latticeai/__init__.py +1 -1
  27. package/latticeai/api/brain_intelligence.py +4 -0
  28. package/latticeai/api/chat.py +11 -0
  29. package/latticeai/api/chat_helpers.py +16 -3
  30. package/latticeai/api/chat_hybrid.py +32 -1
  31. package/latticeai/api/features.py +70 -0
  32. package/latticeai/api/local_files.py +102 -0
  33. package/latticeai/api/portability.py +39 -4
  34. package/latticeai/api/review_queue.py +126 -0
  35. package/latticeai/api/search.py +16 -2
  36. package/latticeai/core/agent.py +55 -2
  37. package/latticeai/core/config.py +4 -1
  38. package/latticeai/core/context_builder.py +6 -3
  39. package/latticeai/core/legacy_compatibility.py +1 -1
  40. package/latticeai/core/marketplace.py +1 -1
  41. package/latticeai/core/messages.py +143 -0
  42. package/latticeai/core/model_compat.py +73 -2
  43. package/latticeai/core/workspace_os_constants.py +1 -1
  44. package/latticeai/models/model_providers.py +12 -4
  45. package/latticeai/runtime/build_phases.py +28 -0
  46. package/latticeai/runtime/chat_wiring.py +4 -0
  47. package/latticeai/runtime/feature_toggle_wiring.py +163 -0
  48. package/latticeai/runtime/router_registration.py +11 -0
  49. package/latticeai/services/app_context.py +8 -0
  50. package/latticeai/services/architecture_readiness.py +1 -1
  51. package/latticeai/services/automation_intelligence.py +22 -2
  52. package/latticeai/services/brain_intelligence.py +123 -7
  53. package/latticeai/services/command_center.py +10 -4
  54. package/latticeai/services/feature_toggles.py +502 -0
  55. package/latticeai/services/folder_watch.py +122 -1
  56. package/latticeai/services/hybrid_chat.py +56 -5
  57. package/latticeai/services/interop_bridges.py +978 -0
  58. package/latticeai/services/model_capability_registry.py +434 -261
  59. package/latticeai/services/model_catalog.py +95 -61
  60. package/latticeai/services/model_recommendation.py +18 -11
  61. package/latticeai/services/model_runtime.py +1 -1
  62. package/latticeai/services/multimodal_ports.py +26 -1
  63. package/latticeai/services/obsidian_bridge.py +16 -25
  64. package/latticeai/services/product_readiness.py +1 -1
  65. package/latticeai/services/search_service.py +149 -2
  66. package/latticeai/services/tool_dispatch.py +4 -0
  67. package/latticeai/setup/auto_setup.py +27 -30
  68. package/latticeai/setup/wizard.py +77 -44
  69. package/package.json +1 -1
  70. package/scripts/check_current_release_docs.mjs +1 -1
  71. package/scripts/check_server_i18n.mjs +1 -0
  72. package/scripts/release_screen_claims.json +13 -0
  73. package/scripts/verify_hf_model_registry.py +253 -218
  74. package/src-tauri/Cargo.lock +1 -1
  75. package/src-tauri/Cargo.toml +1 -1
  76. package/src-tauri/tauri.conf.json +1 -1
  77. package/static/app/asset-manifest.json +37 -37
  78. package/static/app/assets/{Act-D0HWqtn0.js → Act-AWf0SAKp.js} +1 -1
  79. package/static/app/assets/{AdminConsole-D-QDW-A4.js → AdminConsole-D0u8Tiyj.js} +1 -1
  80. package/static/app/assets/{Brain-CzCsI1mi.js → Brain-tuhI4sOC.js} +1 -1
  81. package/static/app/assets/BrainHome-Ts7G_Ila.js +2 -0
  82. package/static/app/assets/{BrainSignals-2dHQNkns.js → BrainSignals-jMYgQ2Ar.js} +1 -1
  83. package/static/app/assets/{Capture-CT8v1StE.js → Capture-CqOSzyPr.js} +1 -1
  84. package/static/app/assets/{CommandPalette-DoLXC2KH.js → CommandPalette-DC0Bzh-I.js} +1 -1
  85. package/static/app/assets/{Library-DDoxFE5c.js → Library-CX-bbhmK.js} +1 -1
  86. package/static/app/assets/{LivingBrain-BXMWIK_2.js → LivingBrain-DBwhto14.js} +1 -1
  87. package/static/app/assets/{ProductFlow-DOYf7JIs.js → ProductFlow-BHA2cfKI.js} +1 -1
  88. package/static/app/assets/{ReviewCard-COQsqidK.js → ReviewCard-BUhCKRNM.js} +1 -1
  89. package/static/app/assets/{System-BRllvYXd.js → System-Bu2t5hn1.js} +1 -1
  90. package/static/app/assets/arrow-left-Dzwa5zRb.js +1 -0
  91. package/static/app/assets/{bot-4BvN07ux.js → bot-Cia42c2h.js} +1 -1
  92. package/static/app/assets/brain-DJMoqrwx.js +1 -0
  93. package/static/app/assets/{button-CDjtnAoU.js → button-2j2Ijzgq.js} +1 -1
  94. package/static/app/assets/{circle-pause-D_RMn7tp.js → circle-pause-BEFeWpVW.js} +1 -1
  95. package/static/app/assets/{circle-play-B5OpB8ae.js → circle-play-ujXMcHxl.js} +1 -1
  96. package/static/app/assets/{cpu-BIlWInHf.js → cpu-k4awryFq.js} +1 -1
  97. package/static/app/assets/{download-BtjXfL3z.js → download-DFbLJ_ig.js} +1 -1
  98. package/static/app/assets/{folder-open-DefMpxI2.js → folder-open-7y_b6xkM.js} +1 -1
  99. package/static/app/assets/{hard-drive-BQ8NZVkw.js → hard-drive-Bidh02Kr.js} +1 -1
  100. package/static/app/assets/{index-0AvoEBzJ.js → index-BpYkzcVm.js} +3 -3
  101. package/static/app/assets/{index-vtEfYvQY.css → index-DwDl9-8Y.css} +1 -1
  102. package/static/app/assets/{input-B_5ZJ9oy.js → input-DSlJJxRs.js} +1 -1
  103. package/static/app/assets/{permissionCopy-BqZ5tsgL.js → permissionCopy-Bpb83Hx9.js} +1 -1
  104. package/static/app/assets/{primitives-CVwew78r.js → primitives-BCx6TvfG.js} +1 -1
  105. package/static/app/assets/search-Cgy8cCFJ.js +1 -0
  106. package/static/app/assets/{share-2-D5zg_0fY.js → share-2-BH1M-WNi.js} +1 -1
  107. package/static/app/assets/{shield-alert-B5pZzkUb.js → shield-alert-BlKdBXcG.js} +1 -1
  108. package/static/app/assets/{textarea-nEVIweKY.js → textarea-CCWbUfFB.js} +1 -1
  109. package/static/app/assets/{useFocusTrap-Cm99AHlz.js → useFocusTrap-YdHQ7pJ1.js} +1 -1
  110. package/static/app/assets/{useQuery-Dm__N6bL.js → useQuery-CXQiwbVT.js} +1 -1
  111. package/static/app/assets/{utils-DcDMoZIe.js → utils-zqPZJxdx.js} +2 -2
  112. package/static/app/assets/{workspace-LtRRSKTf.js → workspace-DXTihhfU.js} +1 -1
  113. package/static/app/index.html +4 -4
  114. package/static/sw.js +1 -1
  115. package/static/app/assets/BrainHome-Btns-_TA.js +0 -2
  116. package/static/app/assets/arrow-left-DnyMzss-.js +0 -1
  117. package/static/app/assets/brain-uMb_5hnO.js +0 -1
  118. package/static/app/assets/search-DkhnOKZt.js +0 -1
@@ -55,6 +55,13 @@ _SIGNATURE_SIMILARITY = 0.6
55
55
  _MIN_SUGGESTION_CONFIDENCE = 0.35
56
56
  _LOW_CONFIDENCE_THRESHOLD = 0.5
57
57
  _KG_GROUNDING_LIMIT = 5
58
+ #: Charged when the graph *was* consulted and found nothing related. v11.2.0
59
+ #: fixes a dead policy: the floor above was unreachable for recurring questions
60
+ #: (the cheapest possible question — asked twice, one phrasing, no recipe —
61
+ #: scored 0.475), so the suppression branch could only ever be exercised by a
62
+ #: test that moved the constant. Absence of grounding is real evidence, and
63
+ #: charging for it is what makes the floor mean something again.
64
+ _UNGROUNDED_PENALTY = 0.15
58
65
 
59
66
 
60
67
  def _question_confidence(
@@ -67,7 +74,13 @@ def _question_confidence(
67
74
 
68
75
  Evidence factors: how often the question repeats, how many distinct
69
76
  phrasings exist, whether it maps onto a known starter-recipe intent, and
70
- (when the graph is available) how many Brain nodes relate to it.
77
+ what the Brain knows about it.
78
+
79
+ That last factor is three-valued on purpose. ``None`` means the graph could
80
+ not be consulted — unknown, so nothing is added or taken away. A positive
81
+ count is supporting evidence. **Zero** is not the same as unknown: the
82
+ graph was asked, and it knows nothing about this question, which is a
83
+ reason to be less sure rather than a reason to be neutral.
71
84
  """
72
85
  score = 0.3 + 0.5 * min(1.0, (int(count) - 1) / 4)
73
86
  score += min(0.15, 0.05 * len(examples or []))
@@ -75,13 +88,16 @@ def _question_confidence(
75
88
  score += 0.15
76
89
  if kg_related:
77
90
  score += min(0.2, 0.05 * int(kg_related))
91
+ elif kg_related == 0:
92
+ score -= _UNGROUNDED_PENALTY
78
93
  factors = {
79
94
  "repeat_count": int(count),
80
95
  "distinct_examples": len(examples or []),
81
96
  "intent_match": bool(recipe_id),
82
97
  "kg_related_nodes": kg_related,
98
+ "kg_grounded": None if kg_related is None else bool(kg_related),
83
99
  }
84
- return round(min(1.0, score), 2), factors
100
+ return round(max(0.0, min(1.0, score)), 2), factors
85
101
 
86
102
 
87
103
  def _source_confidence(indexed: int, watch_enabled: bool) -> tuple:
@@ -357,6 +373,10 @@ class AutomationIntelligenceService:
357
373
  pattern["count"], pattern["examples"], recipe_id, kg_related
358
374
  )
359
375
  if confidence < _MIN_SUGGESTION_CONFIDENCE:
376
+ # Asked a couple of times, phrased one way, matching no recipe,
377
+ # and the Brain knows nothing about it — not enough to propose
378
+ # automating. Counted, so the surface can say how many were held
379
+ # back rather than silently showing fewer.
360
380
  suppressed_low_confidence += 1
361
381
  continue
362
382
  if recipe_id:
@@ -52,6 +52,21 @@ def _parse_ts(value: Any) -> Optional[datetime]:
52
52
  return parsed
53
53
 
54
54
 
55
+ def _no_graph_reason(graph_available: bool) -> str:
56
+ """Why a graph-derived health dimension has nothing to say.
57
+
58
+ Two different situations that both end in ``status: "unavailable"``: the
59
+ graph could not be read at all, and the graph read fine but holds nothing
60
+ yet. Telling them apart is the difference between "something is broken"
61
+ and "you have not saved anything yet".
62
+ """
63
+ return (
64
+ "no knowledge saved yet"
65
+ if graph_available
66
+ else "the knowledge graph could not be read"
67
+ )
68
+
69
+
55
70
  class BrainIntelligenceService:
56
71
  def __init__(
57
72
  self,
@@ -395,7 +410,11 @@ class BrainIntelligenceService:
395
410
  "stale_threshold_days": _STALE_DAYS,
396
411
  }
397
412
  else:
398
- dimensions["freshness"] = {"status": "unavailable", "score": None}
413
+ dimensions["freshness"] = {
414
+ "status": "unavailable",
415
+ "score": None,
416
+ "reason": _no_graph_reason(sample["available"]),
417
+ }
399
418
 
400
419
  # Connectivity — orphan nodes are knowledge the Brain cannot reason
401
420
  # across; a well-tended graph keeps them rare.
@@ -414,7 +433,11 @@ class BrainIntelligenceService:
414
433
  "edges": len(edges),
415
434
  }
416
435
  else:
417
- dimensions["connectivity"] = {"status": "unavailable", "score": None}
436
+ dimensions["connectivity"] = {
437
+ "status": "unavailable",
438
+ "score": None,
439
+ "reason": _no_graph_reason(sample["available"]),
440
+ }
418
441
 
419
442
  # Embedding coverage — semantic recall only works for indexed items.
420
443
  index_status: Dict[str, Any] = {}
@@ -425,7 +448,23 @@ class BrainIntelligenceService:
425
448
  LOGGER.exception("brain intelligence index status failed")
426
449
  index_status = {"error": str(exc)}
427
450
  scale = index_status.get("scale") or {}
428
- if "coverage_ratio" in scale:
451
+ indexable = scale.get("source_items", index_status.get("source_items"))
452
+ if "coverage_ratio" not in scale:
453
+ dimensions["embedding_coverage"] = {
454
+ "status": "unavailable",
455
+ "score": None,
456
+ "reason": "this knowledge store does not report vector index coverage",
457
+ }
458
+ elif indexable == 0:
459
+ # An empty index covers 100% of nothing. Scoring that as a perfect
460
+ # 100 is how a brand-new Brain used to grade itself "excellent"
461
+ # off its only measurable dimension (audit v11.2.0, Finding 3).
462
+ dimensions["embedding_coverage"] = {
463
+ "status": "unavailable",
464
+ "score": None,
465
+ "reason": "no indexable items yet",
466
+ }
467
+ else:
429
468
  dimensions["embedding_coverage"] = {
430
469
  "status": "ok",
431
470
  "score": round(float(scale["coverage_ratio"]) * 100),
@@ -433,8 +472,6 @@ class BrainIntelligenceService:
433
472
  "pending_items": scale.get("pending_items"),
434
473
  "needs_reindex": index_status.get("status") == "needs_reindex",
435
474
  }
436
- else:
437
- dimensions["embedding_coverage"] = {"status": "unavailable", "score": None}
438
475
 
439
476
  # Edge quality + contradiction pressure — reuses the quality layer.
440
477
  if sample["available"] and edges:
@@ -450,7 +487,15 @@ class BrainIntelligenceService:
450
487
  "contradiction_edges": len(contradiction_edges),
451
488
  }
452
489
  else:
453
- dimensions["consistency"] = {"status": "unavailable", "score": None}
490
+ dimensions["consistency"] = {
491
+ "status": "unavailable",
492
+ "score": None,
493
+ "reason": (
494
+ _no_graph_reason(sample["available"])
495
+ if not (sample["available"] and nodes)
496
+ else "no relationships recorded yet"
497
+ ),
498
+ }
454
499
 
455
500
  scores = [d["score"] for d in dimensions.values() if d.get("score") is not None]
456
501
  overall = round(sum(scores) / len(scores)) if scores else None
@@ -461,6 +506,29 @@ class BrainIntelligenceService:
461
506
  else "attention" if overall >= 50
462
507
  else "critical"
463
508
  )
509
+ # What the verdict rests on. A composite averages only what could be
510
+ # measured, so the count of measured dimensions is part of the answer
511
+ # rather than a footnote — and when nothing could be measured the
512
+ # report says so instead of leaving a bare null (the 9.9.7 rule: a
513
+ # "—" always states why).
514
+ unmeasured = sorted(
515
+ name for name, dim in dimensions.items() if dim.get("score") is None
516
+ )
517
+ coverage: Dict[str, Any] = {
518
+ "measured": len(scores),
519
+ "total": len(dimensions),
520
+ "unavailable": unmeasured,
521
+ "partial": bool(unmeasured),
522
+ }
523
+ reason: Optional[str] = None
524
+ if overall is None:
525
+ reason = (
526
+ "no health dimension could be measured yet — "
527
+ + "; ".join(
528
+ f"{name}: {dimensions[name].get('reason') or 'unavailable'}"
529
+ for name in unmeasured
530
+ )
531
+ )
464
532
 
465
533
  actions: List[Dict[str, str]] = []
466
534
  emb = dimensions["embedding_coverage"]
@@ -490,14 +558,18 @@ class BrainIntelligenceService:
490
558
  "reason": f"{cons_dim['contradiction_edges']} contradiction edges recorded in the graph.",
491
559
  })
492
560
 
493
- return {
561
+ report: Dict[str, Any] = {
494
562
  "overall_score": overall,
495
563
  "grade": grade,
496
564
  "dimensions": dimensions,
565
+ "coverage": coverage,
497
566
  "recommended_actions": actions,
498
567
  "graph_available": sample["available"],
499
568
  "generated_at": _now(),
500
569
  }
570
+ if reason is not None:
571
+ report["reason"] = reason
572
+ return report
501
573
 
502
574
  # ── vector freshness (v9.8.0) ────────────────────────────────────────
503
575
 
@@ -511,7 +583,51 @@ class BrainIntelligenceService:
511
583
  raises. The vector index is store-global (not workspace-partitioned);
512
584
  scope arguments are accepted for router symmetry but do not narrow
513
585
  the report.
586
+
587
+ Since v11.2.0 a store that can split its backlog also gets a
588
+ ``breakdown`` key (see :meth:`_freshness_breakdown`). It is additive:
589
+ the four keys above keep their meaning and their types, so the
590
+ freshness chip that reads ``pending_items`` is untouched, and a store
591
+ without the split simply has no ``breakdown``.
514
592
  """
593
+ payload = self._vector_freshness_contract()
594
+ breakdown = self._freshness_breakdown()
595
+ if breakdown is not None:
596
+ payload["breakdown"] = breakdown
597
+ return payload
598
+
599
+ def _freshness_breakdown(self) -> Optional[Dict[str, Any]]:
600
+ """``vector_freshness_breakdown()`` from the store, or ``None``.
601
+
602
+ "12 pending" hides two different situations — twelve items never
603
+ embedded (a new import) and twelve whose text changed under an
604
+ existing embedding (edits, where current answers are quietly wrong).
605
+ The store has always known the difference; until now nothing asked it.
606
+
607
+ ``None`` covers every reason the split is not available (graph off,
608
+ an older store, an unreadable index), because a caller can only act on
609
+ numbers that were really measured — never on zeros standing in for
610
+ them.
611
+ """
612
+ if not self._enable_graph or self._kg is None:
613
+ return None
614
+ breakdown_fn = getattr(self._kg, "vector_freshness_breakdown", None)
615
+ if not callable(breakdown_fn):
616
+ return None
617
+ try:
618
+ raw = breakdown_fn()
619
+ except Exception:
620
+ LOGGER.exception("vector freshness breakdown read failed")
621
+ return None
622
+ # An empty or unrecognisable answer is "not measured", which is what
623
+ # ``None`` already means here — publishing an empty block would claim
624
+ # a split nobody computed.
625
+ if not isinstance(raw, dict) or not raw:
626
+ return None
627
+ return dict(raw)
628
+
629
+ def _vector_freshness_contract(self) -> Dict[str, Any]:
630
+ """The four-key freshness payload, unchanged since v9.8.0."""
515
631
 
516
632
  def _unavailable(detail: str) -> Dict[str, Any]:
517
633
  return {
@@ -214,11 +214,17 @@ class CommandCenterService:
214
214
  except Exception:
215
215
  LOGGER.exception("command center health read failed")
216
216
  return {"available": False}
217
- overall = report.get("overall") or {}
217
+ # ``BrainIntelligenceService.health_report()`` publishes the score and
218
+ # the grade at the top level. This used to read them out of a nested
219
+ # ``overall`` block that no producer has ever written, so the section
220
+ # was permanently unavailable: Today's Briefing showed a dash with no
221
+ # reason beside it, and the "check-health" quick action below could
222
+ # never fire. Read the shape the real collaborator returns.
223
+ score = report.get("overall_score")
218
224
  return {
219
- "available": overall.get("score") is not None,
220
- "grade": overall.get("grade"),
221
- "score": overall.get("score"),
225
+ "available": score is not None,
226
+ "grade": report.get("grade"),
227
+ "score": score,
222
228
  "recommended_actions": list(report.get("recommended_actions") or [])[:3],
223
229
  }
224
230