ltcai 11.0.1 → 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.
- package/README.md +55 -43
- package/docs/CHANGELOG.md +61 -0
- package/docs/COMMUNITY_AND_PLUGINS.md +1 -1
- package/docs/DEVELOPMENT.md +1 -1
- package/docs/FEATURE_AUDIT_v11.2.0.md +393 -0
- package/docs/LAYOUT_REBUILD_SPEC.md +9 -1
- package/docs/ONBOARDING.md +1 -1
- package/docs/OPERATIONS.md +1 -1
- package/docs/PERFORMANCE.md +71 -18
- package/docs/TRUST_MODEL.md +1 -1
- package/docs/WHY_LATTICE.md +1 -1
- package/docs/architecture.md +6 -2
- package/docs/kg-schema.md +1 -1
- package/lattice_brain/__init__.py +1 -1
- package/lattice_brain/embeddings.py +12 -37
- package/lattice_brain/gates.py +125 -0
- package/lattice_brain/graph/discovery_index.py +30 -32
- package/lattice_brain/graph/fusion.py +35 -4
- package/lattice_brain/graph/image_vectors.py +230 -0
- package/lattice_brain/graph/ingest.py +11 -5
- package/lattice_brain/graph/projection.py +66 -8
- package/lattice_brain/graph/provenance.py +27 -2
- package/lattice_brain/graph/retrieval.py +113 -2
- package/lattice_brain/graph/retrieval_docgen.py +6 -6
- package/lattice_brain/graph/schema.py +18 -0
- package/lattice_brain/graph/store.py +9 -0
- package/lattice_brain/graph/vector_index/selector.py +32 -2
- package/lattice_brain/ingestion.py +363 -10
- package/lattice_brain/multimodal.py +1258 -0
- package/lattice_brain/portability.py +169 -32
- package/lattice_brain/runtime/multi_agent.py +1 -1
- package/lattice_brain/sealed_box.py +244 -0
- package/lattice_brain/self_model.py +77 -22
- package/lattice_brain/synthesis.py +24 -1
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/brain_intelligence.py +4 -0
- package/latticeai/api/chat.py +11 -0
- package/latticeai/api/chat_helpers.py +16 -3
- package/latticeai/api/chat_hybrid.py +32 -1
- package/latticeai/api/features.py +70 -0
- package/latticeai/api/local_files.py +102 -0
- package/latticeai/api/memory.py +128 -1
- package/latticeai/api/portability.py +39 -4
- package/latticeai/api/review_queue.py +126 -0
- package/latticeai/api/search.py +16 -2
- package/latticeai/core/agent.py +59 -2
- package/latticeai/core/agent_prompts.py +66 -0
- package/latticeai/core/config.py +4 -1
- package/latticeai/core/context_builder.py +98 -11
- package/latticeai/core/embedding_providers.py +528 -0
- package/latticeai/core/legacy_compatibility.py +1 -1
- package/latticeai/core/marketplace.py +1 -1
- package/latticeai/core/messages.py +180 -0
- package/latticeai/core/model_compat.py +73 -2
- package/latticeai/core/workspace_os.py +43 -0
- package/latticeai/core/workspace_os_constants.py +1 -1
- package/latticeai/core/workspace_reorganization.py +335 -0
- package/latticeai/models/model_providers.py +12 -4
- package/latticeai/runtime/build_phases.py +33 -2
- package/latticeai/runtime/chat_wiring.py +4 -0
- package/latticeai/runtime/feature_toggle_wiring.py +163 -0
- package/latticeai/runtime/persistence_runtime.py +41 -4
- package/latticeai/runtime/router_registration.py +11 -0
- package/latticeai/runtime/runtime_context.py +1 -0
- package/latticeai/services/app_context.py +8 -0
- package/latticeai/services/architecture_readiness.py +1 -1
- package/latticeai/services/automation_intelligence.py +22 -2
- package/latticeai/services/brain_intelligence.py +123 -7
- package/latticeai/services/change_proposals.py +50 -10
- package/latticeai/services/command_center.py +10 -4
- package/latticeai/services/feature_toggles.py +502 -0
- package/latticeai/services/folder_watch.py +122 -1
- package/latticeai/services/hybrid_chat.py +56 -5
- package/latticeai/services/interop_bridges.py +978 -0
- package/latticeai/services/memory_service.py +34 -0
- package/latticeai/services/model_capability_registry.py +434 -261
- package/latticeai/services/model_catalog.py +95 -61
- package/latticeai/services/model_recommendation.py +18 -11
- package/latticeai/services/model_runtime.py +1 -1
- package/latticeai/services/multimodal_ports.py +112 -0
- package/latticeai/services/obsidian_bridge.py +16 -25
- package/latticeai/services/product_readiness.py +1 -1
- package/latticeai/services/search_service.py +149 -2
- package/latticeai/services/self_model_service.py +171 -0
- package/latticeai/services/tool_dispatch.py +4 -0
- package/latticeai/services/voice_capture.py +27 -1
- package/latticeai/setup/auto_setup.py +27 -30
- package/latticeai/setup/wizard.py +77 -44
- package/package.json +1 -1
- package/scripts/check_current_release_docs.mjs +1 -1
- package/scripts/check_server_i18n.mjs +1 -0
- package/scripts/release_screen_claims.json +22 -0
- package/scripts/verify_hf_model_registry.py +253 -218
- package/src-tauri/Cargo.lock +1 -1
- package/src-tauri/Cargo.toml +1 -1
- package/src-tauri/tauri.conf.json +1 -1
- package/static/app/asset-manifest.json +37 -37
- package/static/app/assets/{Act-D4zSxFR-.js → Act-AWf0SAKp.js} +1 -1
- package/static/app/assets/{AdminConsole-w5jBfPt2.js → AdminConsole-D0u8Tiyj.js} +1 -1
- package/static/app/assets/{Brain-C2EqQg74.js → Brain-tuhI4sOC.js} +1 -1
- package/static/app/assets/BrainHome-Ts7G_Ila.js +2 -0
- package/static/app/assets/BrainSignals-jMYgQ2Ar.js +1 -0
- package/static/app/assets/{Capture-DPqpGK8d.js → Capture-CqOSzyPr.js} +1 -1
- package/static/app/assets/{CommandPalette-CNf7h5fp.js → CommandPalette-DC0Bzh-I.js} +1 -1
- package/static/app/assets/{Library-BN0HYOfc.js → Library-CX-bbhmK.js} +1 -1
- package/static/app/assets/{LivingBrain-Dfq_wEDI.js → LivingBrain-DBwhto14.js} +1 -1
- package/static/app/assets/{ProductFlow-B-3O0rNV.js → ProductFlow-BHA2cfKI.js} +1 -1
- package/static/app/assets/{ReviewCard-gZ-tdqFM.js → ReviewCard-BUhCKRNM.js} +1 -1
- package/static/app/assets/{System-BElUcSSw.js → System-Bu2t5hn1.js} +1 -1
- package/static/app/assets/arrow-left-Dzwa5zRb.js +1 -0
- package/static/app/assets/{bot--qYHMtkP.js → bot-Cia42c2h.js} +1 -1
- package/static/app/assets/brain-DJMoqrwx.js +1 -0
- package/static/app/assets/{button-51Z3rsuv.js → button-2j2Ijzgq.js} +1 -1
- package/static/app/assets/{circle-pause-CMIiMaQl.js → circle-pause-BEFeWpVW.js} +1 -1
- package/static/app/assets/{circle-play-DZoO_cfG.js → circle-play-ujXMcHxl.js} +1 -1
- package/static/app/assets/{cpu-Bs6uc9W9.js → cpu-k4awryFq.js} +1 -1
- package/static/app/assets/{download-G-2olkWz.js → download-DFbLJ_ig.js} +1 -1
- package/static/app/assets/{folder-open-CTOspnmb.js → folder-open-7y_b6xkM.js} +1 -1
- package/static/app/assets/{hard-drive-CewHWJhn.js → hard-drive-Bidh02Kr.js} +1 -1
- package/static/app/assets/{index-D7Rr-J2Y.js → index-BpYkzcVm.js} +3 -3
- package/static/app/assets/index-DwDl9-8Y.css +2 -0
- package/static/app/assets/{input-D4w_BZWl.js → input-DSlJJxRs.js} +1 -1
- package/static/app/assets/{permissionCopy-CosBEXAZ.js → permissionCopy-Bpb83Hx9.js} +1 -1
- package/static/app/assets/{primitives-d0g9pvzS.js → primitives-BCx6TvfG.js} +1 -1
- package/static/app/assets/search-Cgy8cCFJ.js +1 -0
- package/static/app/assets/{share-2-NmD7e_oV.js → share-2-BH1M-WNi.js} +1 -1
- package/static/app/assets/{shield-alert-CcQeMuju.js → shield-alert-BlKdBXcG.js} +1 -1
- package/static/app/assets/{textarea-BPAJDc-0.js → textarea-CCWbUfFB.js} +1 -1
- package/static/app/assets/{useFocusTrap-C7YLdTBC.js → useFocusTrap-YdHQ7pJ1.js} +1 -1
- package/static/app/assets/{useQuery-DRyD9opW.js → useQuery-CXQiwbVT.js} +1 -1
- package/static/app/assets/{utils-DG1_ExrP.js → utils-zqPZJxdx.js} +2 -2
- package/static/app/assets/{workspace-CWVf3gsI.js → workspace-DXTihhfU.js} +1 -1
- package/static/app/index.html +4 -4
- package/static/sw.js +1 -1
- package/static/app/assets/BrainHome-CvXS6XiQ.js +0 -2
- package/static/app/assets/BrainSignals-DOE_KhOU.js +0 -1
- package/static/app/assets/arrow-left-CFNIMjhv.js +0 -1
- package/static/app/assets/brain-DDCLjRqO.js +0 -1
- package/static/app/assets/index-CkzokZAj.css +0 -2
- package/static/app/assets/search-BLCYt75v.js +0 -1
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""Self-Model service — the Brain's profile of its owner (v11.1.0 Track 4).
|
|
2
|
+
|
|
3
|
+
Router-facing façade over :mod:`lattice_brain.self_model`. It owns two things
|
|
4
|
+
the Brain Core deliberately does not:
|
|
5
|
+
|
|
6
|
+
* **where the collaborators come from.** The knowledge graph and the review
|
|
7
|
+
queue are read off the ``MemoryService`` the memory router already holds
|
|
8
|
+
(the same ``WorkspaceOSStore`` every other ``ReviewQueueService`` in the
|
|
9
|
+
process is built over), so the Self-Model routes need no extra wiring at the
|
|
10
|
+
composition root and proposals land in the one inbox the user reads.
|
|
11
|
+
* **what "unavailable" means.** A Brain with the graph disabled reports
|
|
12
|
+
``available: False`` with a reason instead of pretending it has no profile,
|
|
13
|
+
and a write attempted without a graph raises so the API answers 409 rather
|
|
14
|
+
than silently doing nothing.
|
|
15
|
+
|
|
16
|
+
The consent model is the product's: extraction proposes, the user approves,
|
|
17
|
+
and only the *user's own* edits write directly.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
from typing import Any, Dict, List, Optional
|
|
23
|
+
|
|
24
|
+
from lattice_brain.self_model import (
|
|
25
|
+
DEFAULT_SUMMARY_TOKENS,
|
|
26
|
+
KIND_ORDER,
|
|
27
|
+
SelfModelError,
|
|
28
|
+
apply_self_model_proposal,
|
|
29
|
+
delete_self_model_fact,
|
|
30
|
+
list_self_model,
|
|
31
|
+
propose_self_model,
|
|
32
|
+
self_model_summary,
|
|
33
|
+
upsert_self_model_fact,
|
|
34
|
+
)
|
|
35
|
+
from latticeai.core.timeutil import now_iso as _now
|
|
36
|
+
|
|
37
|
+
GRAPH_UNAVAILABLE = "self-model needs the knowledge graph"
|
|
38
|
+
QUEUE_UNAVAILABLE = "self-model proposals need the review queue"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class SelfModelService:
|
|
42
|
+
"""Read/propose/apply/edit the Self-Model subgraph."""
|
|
43
|
+
|
|
44
|
+
def __init__(
|
|
45
|
+
self,
|
|
46
|
+
*,
|
|
47
|
+
memory_service: Any = None,
|
|
48
|
+
knowledge_graph: Any = None,
|
|
49
|
+
review_queue: Any = None,
|
|
50
|
+
enable_graph: bool = True,
|
|
51
|
+
summary_tokens: int = DEFAULT_SUMMARY_TOKENS,
|
|
52
|
+
) -> None:
|
|
53
|
+
self._memory = memory_service
|
|
54
|
+
self._graph = knowledge_graph
|
|
55
|
+
self._queue = review_queue
|
|
56
|
+
self._enable_graph = bool(enable_graph)
|
|
57
|
+
self._summary_tokens = int(summary_tokens)
|
|
58
|
+
|
|
59
|
+
# ── collaborators ────────────────────────────────────────────────────
|
|
60
|
+
def _kg(self) -> Any:
|
|
61
|
+
"""The knowledge graph, or ``None`` when this Brain has none."""
|
|
62
|
+
if not self._enable_graph:
|
|
63
|
+
return None
|
|
64
|
+
if self._graph is None:
|
|
65
|
+
self._graph = getattr(self._memory, "_kg", None)
|
|
66
|
+
return self._graph
|
|
67
|
+
|
|
68
|
+
def _review_queue(self) -> Any:
|
|
69
|
+
"""The review queue proposals are written to, or ``None``."""
|
|
70
|
+
if self._queue is not None:
|
|
71
|
+
return self._queue
|
|
72
|
+
store = getattr(self._memory, "_store", None)
|
|
73
|
+
if store is None or not hasattr(store, "create_review_item"):
|
|
74
|
+
return None
|
|
75
|
+
from latticeai.services.review_queue import ReviewQueueService
|
|
76
|
+
|
|
77
|
+
self._queue = ReviewQueueService(store=store)
|
|
78
|
+
return self._queue
|
|
79
|
+
|
|
80
|
+
@staticmethod
|
|
81
|
+
def _unavailable(detail: str) -> Dict[str, Any]:
|
|
82
|
+
return {"available": False, "detail": detail, "generated_at": _now()}
|
|
83
|
+
|
|
84
|
+
def _require_graph(self) -> Any:
|
|
85
|
+
graph = self._kg()
|
|
86
|
+
if graph is None:
|
|
87
|
+
raise SelfModelError(GRAPH_UNAVAILABLE, code="graph_unavailable")
|
|
88
|
+
return graph
|
|
89
|
+
|
|
90
|
+
# ── reads ────────────────────────────────────────────────────────────
|
|
91
|
+
def profile(
|
|
92
|
+
self, *, user_email: Optional[str] = None, workspace_id: Optional[str] = None
|
|
93
|
+
) -> Dict[str, Any]:
|
|
94
|
+
"""Everything the Brain holds about its owner, plus the injected text."""
|
|
95
|
+
graph = self._kg()
|
|
96
|
+
if graph is None:
|
|
97
|
+
return {**self._unavailable(GRAPH_UNAVAILABLE), "facts": [], "summary": ""}
|
|
98
|
+
listing = dict(list_self_model(graph, workspace_id=workspace_id))
|
|
99
|
+
listing["summary"] = self_model_summary(
|
|
100
|
+
graph, limit_tokens=self._summary_tokens, workspace_id=workspace_id
|
|
101
|
+
)
|
|
102
|
+
listing["summary_tokens"] = self._summary_tokens
|
|
103
|
+
listing["user_email"] = user_email
|
|
104
|
+
listing["kind_options"] = list(KIND_ORDER)
|
|
105
|
+
return listing
|
|
106
|
+
|
|
107
|
+
def summary(self, *, workspace_id: Optional[str] = None) -> str:
|
|
108
|
+
"""Injection-ready summary (empty string when there is nothing to say)."""
|
|
109
|
+
graph = self._kg()
|
|
110
|
+
if graph is None:
|
|
111
|
+
return ""
|
|
112
|
+
return self_model_summary(
|
|
113
|
+
graph, limit_tokens=self._summary_tokens, workspace_id=workspace_id
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
# ── proposal path (agents / background) ──────────────────────────────
|
|
117
|
+
def propose(
|
|
118
|
+
self,
|
|
119
|
+
text: str,
|
|
120
|
+
*,
|
|
121
|
+
texts: Optional[List[str]] = None,
|
|
122
|
+
source: Optional[str] = None,
|
|
123
|
+
user_email: Optional[str] = None,
|
|
124
|
+
workspace_id: Optional[str] = None,
|
|
125
|
+
max_proposals: int = 5,
|
|
126
|
+
) -> Dict[str, Any]:
|
|
127
|
+
"""Read text and raise a review proposal per new fact. Never writes."""
|
|
128
|
+
graph = self._kg()
|
|
129
|
+
if graph is None:
|
|
130
|
+
return self._unavailable(GRAPH_UNAVAILABLE)
|
|
131
|
+
queue = self._review_queue()
|
|
132
|
+
if queue is None:
|
|
133
|
+
return self._unavailable(QUEUE_UNAVAILABLE)
|
|
134
|
+
return propose_self_model(
|
|
135
|
+
graph,
|
|
136
|
+
queue,
|
|
137
|
+
text=text,
|
|
138
|
+
texts=texts,
|
|
139
|
+
source=source,
|
|
140
|
+
workspace_id=workspace_id,
|
|
141
|
+
user_email=user_email,
|
|
142
|
+
max_proposals=max_proposals,
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
def apply(
|
|
146
|
+
self, item_id: str, *, workspace_id: Optional[str] = None
|
|
147
|
+
) -> Dict[str, Any]:
|
|
148
|
+
"""Approve one Self-Model proposal and write the fact into the graph."""
|
|
149
|
+
graph = self._require_graph()
|
|
150
|
+
queue = self._review_queue()
|
|
151
|
+
if queue is None:
|
|
152
|
+
raise SelfModelError(QUEUE_UNAVAILABLE, code="queue_unavailable")
|
|
153
|
+
return apply_self_model_proposal(
|
|
154
|
+
graph, queue, item_id, workspace_id=workspace_id
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
# ── user-initiated edits (direct) ────────────────────────────────────
|
|
158
|
+
def upsert(
|
|
159
|
+
self, *, kind: str, text: str, workspace_id: Optional[str] = None
|
|
160
|
+
) -> Dict[str, Any]:
|
|
161
|
+
"""A person editing their own profile writes straight through."""
|
|
162
|
+
return upsert_self_model_fact(
|
|
163
|
+
self._require_graph(), kind=kind, text=text, workspace_id=workspace_id
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
def delete(self, node_id: str) -> Dict[str, Any]:
|
|
167
|
+
"""Forget one fact about the user, permanently."""
|
|
168
|
+
return delete_self_model_fact(self._require_graph(), node_id)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
__all__ = ["GRAPH_UNAVAILABLE", "QUEUE_UNAVAILABLE", "SelfModelService"]
|
|
@@ -486,6 +486,7 @@ def build_agent_runtime(
|
|
|
486
486
|
brain_memory: Any = None,
|
|
487
487
|
dispatch_service: ToolDispatchService = DEFAULT_TOOL_DISPATCH_SERVICE,
|
|
488
488
|
permission_mode: Any = None,
|
|
489
|
+
self_model_summary: Any = None,
|
|
489
490
|
) -> SingleAgentRuntime:
|
|
490
491
|
ensure_agent_root()
|
|
491
492
|
mode = permission_mode if permission_mode is not None else dispatch_service.permission_mode
|
|
@@ -513,6 +514,9 @@ def build_agent_runtime(
|
|
|
513
514
|
hooks=hooks,
|
|
514
515
|
brain_memory=brain_memory,
|
|
515
516
|
permission_mode=mode,
|
|
517
|
+
# v11.2.0: what the Brain knows about its owner reaches the agent loop.
|
|
518
|
+
# ``None`` keeps the pre-11.2.0 prompt bytes exactly.
|
|
519
|
+
self_model_summary=self_model_summary,
|
|
516
520
|
)
|
|
517
521
|
return SingleAgentRuntime(deps)
|
|
518
522
|
|
|
@@ -18,13 +18,23 @@ to become text. So transcription is an **injected, optional port**:
|
|
|
18
18
|
Nothing here reaches for a cloud speech API, and nothing here installs a model
|
|
19
19
|
behind the user's back — an absent transcriber is a reported state, not a
|
|
20
20
|
prompt to download something.
|
|
21
|
+
|
|
22
|
+
v11.1.0: that same injected port is now the *only* transcription seam in the
|
|
23
|
+
product. When multi-modal ingestion is enabled, a ``.m4a`` found by a folder
|
|
24
|
+
scan is transcribed by whatever this service was given — see
|
|
25
|
+
:meth:`VoiceCaptureService.multimodal_ports`. One transcriber, one honesty
|
|
26
|
+
story: if a voice memo cannot become text here, a scanned recording cannot
|
|
27
|
+
either, and both say so the same way.
|
|
21
28
|
"""
|
|
22
29
|
|
|
23
30
|
from __future__ import annotations
|
|
24
31
|
|
|
25
32
|
import logging
|
|
26
33
|
from pathlib import Path
|
|
27
|
-
from typing import Any, Callable, Dict, Optional
|
|
34
|
+
from typing import TYPE_CHECKING, Any, Callable, Dict, Optional
|
|
35
|
+
|
|
36
|
+
if TYPE_CHECKING: # import-time isolation: the port is a callable, not a class
|
|
37
|
+
from lattice_brain.multimodal import MultimodalPorts
|
|
28
38
|
|
|
29
39
|
LOGGER = logging.getLogger(__name__)
|
|
30
40
|
|
|
@@ -75,6 +85,19 @@ class VoiceCaptureService:
|
|
|
75
85
|
),
|
|
76
86
|
}
|
|
77
87
|
|
|
88
|
+
def multimodal_ports(self) -> "MultimodalPorts":
|
|
89
|
+
"""This service's transcriber, shaped for the ingestion pipeline.
|
|
90
|
+
|
|
91
|
+
The pipeline lives in Brain Core and must not import ``latticeai``, so
|
|
92
|
+
the capability travels as a plain callable rather than as this class.
|
|
93
|
+
Handing over ``None`` when no transcriber is configured is the point:
|
|
94
|
+
the pipeline then degrades exactly as :meth:`capture` does instead of
|
|
95
|
+
inventing a transcript of its own.
|
|
96
|
+
"""
|
|
97
|
+
from lattice_brain.multimodal import MultimodalPorts
|
|
98
|
+
|
|
99
|
+
return MultimodalPorts(transcriber=self._transcriber)
|
|
100
|
+
|
|
78
101
|
# ── transcription ────────────────────────────────────────────────────
|
|
79
102
|
def _transcribe(self, path: Path) -> str:
|
|
80
103
|
if self._transcriber is None:
|
|
@@ -163,6 +186,9 @@ class VoiceCaptureService:
|
|
|
163
186
|
conversation_id=conversation_id,
|
|
164
187
|
metadata={
|
|
165
188
|
"capture": "voice",
|
|
189
|
+
# Same key the multi-modal ingest door writes, so a memo and a
|
|
190
|
+
# scanned recording are one kind of thing in the graph.
|
|
191
|
+
"modality": "audio",
|
|
166
192
|
"audio_path": str(path),
|
|
167
193
|
"audio_bytes": size,
|
|
168
194
|
"transcription": transcription_state,
|
|
@@ -399,36 +399,36 @@ class Recommendation:
|
|
|
399
399
|
return asdict(self)
|
|
400
400
|
|
|
401
401
|
|
|
402
|
-
# 모델
|
|
402
|
+
# 모델 카탈로그 — RAM/VRAM 내림차순. 첫 번째로 조건을 만족하는 항목이 선택된다.
|
|
403
|
+
# 모든 repo id 는 2026-08-10 Hugging Face API 로 확인했다(존재·비gated·정확한 대소문자).
|
|
404
|
+
# OS 오버헤드(~4-6 GB) + KV 캐시 여유를 감안한 보수적 RAM 임계값.
|
|
403
405
|
_MODEL_CATALOG: List[Dict[str, Any]] = [
|
|
404
|
-
# (
|
|
405
|
-
# OS 오버헤드(~4-6 GB) + KV 캐시 여유를 감안한 보수적 RAM 임계값
|
|
406
|
+
# 대형 (48GB+): 최상위 dense / Gemma 4 최대 모델
|
|
406
407
|
{"ram": 64 * 1024, "vram": 32 * 1024,
|
|
407
408
|
"id": "mlx-community/gemma-4-31b-it-4bit", "q": "4bit", "multimodal": True},
|
|
408
|
-
{"ram": 64 * 1024, "vram": 32 * 1024,
|
|
409
|
-
"id": "Qwen/Qwen3-VL-30B-A3B-Instruct", "q": "q4_K_M", "multimodal": True},
|
|
410
409
|
{"ram": 48 * 1024, "vram": 24 * 1024,
|
|
411
|
-
"id": "mlx-community/
|
|
410
|
+
"id": "mlx-community/Qwen3.6-27B-4bit", "q": "4bit", "multimodal": True},
|
|
411
|
+
# MoE (32GB+)
|
|
412
|
+
{"ram": 40 * 1024, "vram": 24 * 1024,
|
|
413
|
+
"id": "mlx-community/Qwen3.6-35B-A3B-4bit", "q": "4bit", "multimodal": True},
|
|
412
414
|
{"ram": 32 * 1024, "vram": 16 * 1024,
|
|
413
415
|
"id": "mlx-community/gemma-4-26b-a4b-it-4bit", "q": "4bit", "multimodal": True},
|
|
414
|
-
|
|
415
|
-
"id": "Qwen/Qwen3-VL-30B-A3B-Instruct", "q": "q4_K_M", "multimodal": True},
|
|
416
|
-
{"ram": 24 * 1024, "vram": 12 * 1024,
|
|
417
|
-
"id": "mlx-community/Llama-4-Scout-17B-16E-Instruct-4bit", "q": "4bit", "multimodal": True},
|
|
418
|
-
{"ram": 16 * 1024, "vram": 8 * 1024,
|
|
419
|
-
"id": "mlx-community/gemma-4-12b-it-4bit", "q": "4bit", "multimodal": True},
|
|
420
|
-
{"ram": 32 * 1024, "vram": 16 * 1024,
|
|
421
|
-
"id": "Qwen/Qwen3-VL-8B-Instruct", "q": "q5_K_M", "multimodal": True},
|
|
416
|
+
# 범용 (24GB)
|
|
422
417
|
{"ram": 24 * 1024, "vram": 12 * 1024,
|
|
423
|
-
"id": "
|
|
418
|
+
"id": "mlx-community/gpt-oss-20b-MXFP4-Q8", "q": "MXFP4-Q8", "multimodal": False},
|
|
419
|
+
# 중형 (16GB)
|
|
424
420
|
{"ram": 16 * 1024, "vram": 8 * 1024,
|
|
425
|
-
"id": "
|
|
421
|
+
"id": "mlx-community/gemma-4-12B-it-4bit", "q": "4bit", "multimodal": True},
|
|
426
422
|
{"ram": 12 * 1024, "vram": 6 * 1024,
|
|
427
|
-
"id": "
|
|
423
|
+
"id": "mlx-community/Qwen3.5-9B-MLX-4bit", "q": "4bit", "multimodal": True},
|
|
424
|
+
# 경량 (10GB) / 초경량 (8GB 이하)
|
|
425
|
+
{"ram": 10 * 1024, "vram": 6 * 1024,
|
|
426
|
+
"id": "mlx-community/gemma-4-e4b-it-4bit", "q": "4bit", "multimodal": True},
|
|
428
427
|
{"ram": 8 * 1024, "vram": 4 * 1024,
|
|
429
|
-
"id": "
|
|
428
|
+
"id": "mlx-community/gemma-4-e2b-it-4bit", "q": "4bit", "multimodal": True},
|
|
429
|
+
# 최소 사양: 사진은 못 읽지만 한국어로 대답은 한다.
|
|
430
430
|
{"ram": 4 * 1024, "vram": 0,
|
|
431
|
-
"id": "
|
|
431
|
+
"id": "mlx-community/LFM2.5-2.6B-4bit", "q": "4bit", "multimodal": False},
|
|
432
432
|
]
|
|
433
433
|
|
|
434
434
|
|
|
@@ -622,17 +622,14 @@ def plan(profile: SystemProfile, rec: Recommendation) -> InstallPlan:
|
|
|
622
622
|
# 모델 가중치 풀
|
|
623
623
|
model_command = ["huggingface-cli", "download", rec.model_id, "--quiet"]
|
|
624
624
|
if rec.runtime == "ollama":
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
model_command = ["ollama", "pull", "qwen3-vl:8b"]
|
|
634
|
-
elif "qwen3-vl-4b" in lower:
|
|
635
|
-
model_command = ["ollama", "pull", "qwen3-vl:4b"]
|
|
625
|
+
# Local import: this module keeps a stdlib-only import graph so it can be
|
|
626
|
+
# run before the app is wired. The alias table is the single source of
|
|
627
|
+
# truth for per-engine repo ids — the old hardcoded if/elif chain here
|
|
628
|
+
# drifted from it and still named Llama 4 Scout and Qwen3-VL.
|
|
629
|
+
from latticeai.services.model_catalog import MODEL_ENGINE_ALIASES
|
|
630
|
+
ollama_target = (MODEL_ENGINE_ALIASES.get(rec.model_id.lower()) or {}).get("ollama")
|
|
631
|
+
if ollama_target:
|
|
632
|
+
model_command = ["ollama", "pull", ollama_target]
|
|
636
633
|
elif rec.runtime == "lmstudio":
|
|
637
634
|
model_command = ["lms", "get", rec.model_id]
|
|
638
635
|
steps.append(InstallStep(
|
|
@@ -562,47 +562,63 @@ def scan_environment() -> Dict[str, Any]:
|
|
|
562
562
|
|
|
563
563
|
# ── Model Catalog ─────────────────────────────────────────────────────────────
|
|
564
564
|
# (model_id, display_name, size_gb, tag, description, min_ram_gb)
|
|
565
|
+
# 11.2.0: 모든 repo id 를 2026-08-10 Hugging Face API 로 확인했다 — 존재 여부,
|
|
566
|
+
# gated 여부, 정확한 대소문자, siblings 합계 크기. 크기 값은 측정값이다.
|
|
567
|
+
# 삭제: Qwen3-VL 전 라인업(2025-10, Qwen3.5/3.6 이 상위 세대), Llama 4 Scout
|
|
568
|
+
# (vLLM/LM Studio 경로가 gated 인 meta-llama 저장소를 가리켰다).
|
|
565
569
|
_MODEL_CATALOG = [
|
|
566
|
-
("mlx-community/
|
|
567
|
-
("mlx-community/gemma-4-e2b-it-4bit", "Gemma 4 E2B", 3.6, "VLM", "
|
|
568
|
-
("mlx-community/
|
|
569
|
-
("mlx-community/
|
|
570
|
-
("mlx-community/
|
|
571
|
-
("mlx-community/
|
|
572
|
-
("mlx-community/gemma-4-
|
|
573
|
-
("mlx-community/Qwen3-
|
|
570
|
+
("mlx-community/LFM2.5-2.6B-4bit", "LFM2.5 2.6B", 1.5, "LLM", "가장 가벼움 · 한국어 대화 (사진은 못 읽음)", 4),
|
|
571
|
+
("mlx-community/gemma-4-e2b-it-4bit", "Gemma 4 E2B", 3.6, "VLM", "사진을 읽는 가장 작은 모델", 8),
|
|
572
|
+
("mlx-community/gemma-4-e4b-it-4bit", "Gemma 4 E4B", 5.2, "VLM", "E2B 보다 한 단계 위 · 여전히 가벼움", 10),
|
|
573
|
+
("mlx-community/Qwen3.5-9B-MLX-4bit", "Qwen3.5 9B", 6.0, "VLM", "중형 멀티모달 · 균형 추천", 12),
|
|
574
|
+
("mlx-community/gemma-4-12B-it-4bit", "Gemma 4 12B", 6.8, "VLM", "Gemma 4 기본 추천 · 4bit", 16),
|
|
575
|
+
("mlx-community/gpt-oss-20b-MXFP4-Q8", "GPT-OSS 20B", 12.1, "LLM", "범용 · 다운로드 최다 (사진은 못 읽음)", 24),
|
|
576
|
+
("mlx-community/gemma-4-26b-a4b-it-4bit", "Gemma 4 26B A4B", 15.4, "VLM", "MoE · 대형 추천", 32),
|
|
577
|
+
("mlx-community/Qwen3.6-27B-4bit", "Qwen3.6 27B", 16.1, "VLM+", "dense 최상위 · 느리지만 일정함", 48),
|
|
578
|
+
("mlx-community/gemma-4-31b-it-4bit", "Gemma 4 31B", 18.4, "VLM+", "Gemma 4 최대 모델", 48),
|
|
579
|
+
("mlx-community/Qwen3.6-35B-A3B-4bit", "Qwen3.6 35B A3B", 20.4, "VLM+", "MoE 최상위", 48),
|
|
574
580
|
]
|
|
575
581
|
|
|
576
582
|
_CROSS_PLATFORM_MODEL_CATALOG: Dict[str, List[Tuple[str, str, float, str, str, int]]] = {
|
|
577
583
|
"ollama": [
|
|
578
|
-
("ollama:
|
|
579
|
-
("ollama:
|
|
584
|
+
("ollama:hf.co/LiquidAI/LFM2.5-2.6B-GGUF:Q4_K_M", "LFM2.5 2.6B Q4", 1.7, "LLM", "가장 가벼움 · 한국어 대화", 4),
|
|
585
|
+
("ollama:hf.co/ggml-org/gemma-4-E2B-it-GGUF:Q4_K_M", "Gemma 4 E2B Q4", 3.8, "VLM", "Hugging Face GGUF 기반 Gemma 4", 8),
|
|
586
|
+
("ollama:hf.co/ggml-org/gemma-4-E4B-it-GGUF:Q4_K_M", "Gemma 4 E4B Q4", 5.4, "VLM", "Hugging Face GGUF 기반 Gemma 4", 10),
|
|
580
587
|
("ollama:hf.co/ggml-org/gemma-4-12B-it-GGUF:Q4_K_M", "Gemma 4 12B Q4", 7.9, "VLM", "Hugging Face GGUF 기반 Gemma 4", 16),
|
|
581
|
-
("ollama:hf.co/ggml-org/
|
|
582
|
-
("ollama:hf.co/ggml-org/
|
|
588
|
+
("ollama:hf.co/ggml-org/gpt-oss-20b-GGUF:Q4_K_M", "GPT-OSS 20B Q4", 12.5, "LLM", "범용 · 다운로드 최다", 24),
|
|
589
|
+
("ollama:hf.co/ggml-org/gemma-4-26B-A4B-it-GGUF:Q4_K_M", "Gemma 4 26B Q4", 16.0, "VLM", "MoE · 대형 추천", 32),
|
|
590
|
+
("ollama:hf.co/ggml-org/Qwen3.6-27B-GGUF:Q4_K_M", "Qwen3.6 27B Q4", 16.6, "VLM+", "dense 최상위", 48),
|
|
591
|
+
("ollama:hf.co/ggml-org/gemma-4-31B-it-GGUF:Q4_K_M", "Gemma 4 31B Q4", 18.7, "VLM+", "Gemma 4 최대 모델", 48),
|
|
583
592
|
],
|
|
584
593
|
"lmstudio": [
|
|
585
|
-
("lmstudio:
|
|
586
|
-
("lmstudio:
|
|
587
|
-
("lmstudio:ggml-org/gemma-4-
|
|
588
|
-
("lmstudio:ggml-org/gemma-4-
|
|
589
|
-
("lmstudio:
|
|
590
|
-
("lmstudio:
|
|
594
|
+
("lmstudio:LiquidAI/LFM2.5-2.6B-GGUF", "LFM2.5 2.6B", 1.7, "LLM", "가장 가벼움 · 한국어 대화", 4),
|
|
595
|
+
("lmstudio:ggml-org/gemma-4-E2B-it-GGUF", "Gemma 4 E2B", 3.8, "VLM", "LM Studio GGUF Gemma 4", 8),
|
|
596
|
+
("lmstudio:ggml-org/gemma-4-E4B-it-GGUF", "Gemma 4 E4B", 5.4, "VLM", "LM Studio GGUF Gemma 4", 10),
|
|
597
|
+
("lmstudio:ggml-org/gemma-4-12B-it-GGUF", "Gemma 4 12B", 7.9, "VLM", "LM Studio GGUF Gemma 4", 16),
|
|
598
|
+
("lmstudio:ggml-org/gpt-oss-20b-GGUF", "GPT-OSS 20B", 12.5, "LLM", "범용 · 다운로드 최다", 24),
|
|
599
|
+
("lmstudio:ggml-org/gemma-4-26B-A4B-it-GGUF", "Gemma 4 26B", 16.0, "VLM", "MoE · 대형 추천", 32),
|
|
600
|
+
("lmstudio:ggml-org/Qwen3.6-27B-GGUF", "Qwen3.6 27B", 16.6, "VLM+", "dense 최상위", 48),
|
|
601
|
+
("lmstudio:ggml-org/gemma-4-31B-it-GGUF", "Gemma 4 31B", 18.7, "VLM+", "Gemma 4 최대 모델", 48),
|
|
591
602
|
],
|
|
592
603
|
"vllm": [
|
|
593
|
-
("vllm:
|
|
594
|
-
("vllm:
|
|
595
|
-
("vllm:google/gemma-4-
|
|
596
|
-
("vllm:Qwen/Qwen3-
|
|
597
|
-
("vllm:
|
|
598
|
-
("vllm:
|
|
604
|
+
("vllm:LiquidAI/LFM2.5-2.6B", "LFM2.5 2.6B", 5.2, "LLM", "가장 가벼움 · 한국어 대화", 8),
|
|
605
|
+
("vllm:google/gemma-4-E2B-it", "Gemma 4 E2B", 6.0, "VLM", "내 컴퓨터 GPU 실행 도구 권장", 12),
|
|
606
|
+
("vllm:google/gemma-4-E4B-it", "Gemma 4 E4B", 9.0, "VLM", "내 컴퓨터 GPU 실행 도구 권장", 16),
|
|
607
|
+
("vllm:Qwen/Qwen3.5-9B", "Qwen3.5 9B", 18.0, "VLM", "중형 멀티모달 · 균형 추천", 24),
|
|
608
|
+
("vllm:google/gemma-4-12B-it", "Gemma 4 12B", 24.0, "VLM", "Gemma 4 기본 추천", 32),
|
|
609
|
+
("vllm:openai/gpt-oss-20b", "GPT-OSS 20B", 13.5, "LLM", "범용 · 다운로드 최다", 24),
|
|
610
|
+
("vllm:Qwen/Qwen3.6-27B", "Qwen3.6 27B", 54.0, "VLM+", "dense 최상위 · 24GB+ VRAM 권장", 64),
|
|
611
|
+
("vllm:Qwen/Qwen3.6-35B-A3B", "Qwen3.6 35B A3B", 70.0, "VLM+", "MoE 최상위 · 24GB+ VRAM 권장", 80),
|
|
599
612
|
],
|
|
600
613
|
"llamacpp": [
|
|
601
|
-
("llamacpp:
|
|
602
|
-
("llamacpp:
|
|
614
|
+
("llamacpp:LiquidAI/LFM2.5-2.6B-GGUF", "LFM2.5 2.6B GGUF", 1.7, "GGUF", "CPU/Vulkan 백업 · 가장 가벼움", 4),
|
|
615
|
+
("llamacpp:ggml-org/gemma-4-E2B-it-GGUF", "Gemma 4 E2B GGUF", 3.8, "GGUF", "Gemma 4 E2B Q4_K_M", 8),
|
|
616
|
+
("llamacpp:ggml-org/gemma-4-E4B-it-GGUF", "Gemma 4 E4B GGUF", 5.4, "GGUF", "Gemma 4 E4B Q4_K_M", 10),
|
|
603
617
|
("llamacpp:ggml-org/gemma-4-12B-it-GGUF", "Gemma 4 12B GGUF", 7.9, "GGUF", "Gemma 4 12B Q4_K_M", 16),
|
|
618
|
+
("llamacpp:ggml-org/gpt-oss-20b-GGUF", "GPT-OSS 20B GGUF", 12.5, "GGUF", "범용 · 다운로드 최다", 24),
|
|
619
|
+
("llamacpp:ggml-org/gemma-4-26B-A4B-it-GGUF", "Gemma 4 26B GGUF", 16.0, "GGUF", "MoE · 대형 추천", 32),
|
|
620
|
+
("llamacpp:ggml-org/Qwen3.6-27B-GGUF", "Qwen3.6 27B GGUF", 16.6, "GGUF", "dense 최상위", 48),
|
|
604
621
|
("llamacpp:ggml-org/gemma-4-31B-it-GGUF", "Gemma 4 31B GGUF", 18.7, "GGUF", "Gemma 4 31B Q4_K_M", 48),
|
|
605
|
-
("llamacpp:ggml-org/Llama-4-Scout-17B-16E-Instruct-GGUF", "Llama 4 Scout GGUF", 12.0, "GGUF", "Meta 최신 멀티모달 Scout", 24),
|
|
606
622
|
],
|
|
607
623
|
}
|
|
608
624
|
|
|
@@ -612,37 +628,51 @@ _VERSIONED_MODEL_PATTERNS = (
|
|
|
612
628
|
("llama", re.compile(r"\bllama[-\s]?(\d+(?:\.\d+)?)", re.IGNORECASE)),
|
|
613
629
|
)
|
|
614
630
|
|
|
631
|
+
# RAM(GB) 내림차순 티어. 첫 번째로 조건을 만족하는 항목이 "이 컴퓨터에서 가장
|
|
632
|
+
# 좋은 모델"이 된다. 초경량(≤8) / 경량(16) / 중형(24) / MoE(32) / 대형(48).
|
|
615
633
|
_BEST_MODEL_TIERS: Dict[str, List[Tuple[int, str]]] = {
|
|
616
634
|
"local_mlx": [
|
|
617
635
|
(48, "mlx-community/gemma-4-31b-it-4bit"),
|
|
618
636
|
(32, "mlx-community/gemma-4-26b-a4b-it-4bit"),
|
|
619
|
-
(
|
|
620
|
-
(16, "mlx-community/Qwen3-
|
|
621
|
-
(
|
|
637
|
+
(24, "mlx-community/gemma-4-12B-it-4bit"),
|
|
638
|
+
(16, "mlx-community/Qwen3.5-9B-MLX-4bit"),
|
|
639
|
+
(8, "mlx-community/gemma-4-e2b-it-4bit"),
|
|
640
|
+
(4, "mlx-community/LFM2.5-2.6B-4bit"),
|
|
622
641
|
],
|
|
623
642
|
"ollama": [
|
|
624
643
|
(48, "ollama:hf.co/ggml-org/gemma-4-31B-it-GGUF:Q4_K_M"),
|
|
625
|
-
(
|
|
626
|
-
(
|
|
627
|
-
(
|
|
644
|
+
(32, "ollama:hf.co/ggml-org/gemma-4-26B-A4B-it-GGUF:Q4_K_M"),
|
|
645
|
+
(24, "ollama:hf.co/ggml-org/gemma-4-12B-it-GGUF:Q4_K_M"),
|
|
646
|
+
(16, "ollama:hf.co/ggml-org/gemma-4-E4B-it-GGUF:Q4_K_M"),
|
|
647
|
+
(8, "ollama:hf.co/ggml-org/gemma-4-E2B-it-GGUF:Q4_K_M"),
|
|
648
|
+
(4, "ollama:hf.co/LiquidAI/LFM2.5-2.6B-GGUF:Q4_K_M"),
|
|
628
649
|
],
|
|
629
650
|
"lmstudio": [
|
|
630
651
|
(48, "lmstudio:ggml-org/gemma-4-31B-it-GGUF"),
|
|
631
|
-
(
|
|
632
|
-
(
|
|
633
|
-
(
|
|
652
|
+
(32, "lmstudio:ggml-org/gemma-4-26B-A4B-it-GGUF"),
|
|
653
|
+
(24, "lmstudio:ggml-org/gemma-4-12B-it-GGUF"),
|
|
654
|
+
(16, "lmstudio:ggml-org/gemma-4-E4B-it-GGUF"),
|
|
655
|
+
(8, "lmstudio:ggml-org/gemma-4-E2B-it-GGUF"),
|
|
656
|
+
(4, "lmstudio:LiquidAI/LFM2.5-2.6B-GGUF"),
|
|
634
657
|
],
|
|
658
|
+
# vLLM serves the upstream bf16 repos, which are 3-4x the MLX 4-bit builds
|
|
659
|
+
# (Qwen3.6-27B is 54GB, not 16GB), so its thresholds sit far higher than the
|
|
660
|
+
# local_mlx ones. Picking by RAM alone would nominate a model no consumer
|
|
661
|
+
# GPU can hold.
|
|
635
662
|
"vllm": [
|
|
636
|
-
(
|
|
637
|
-
(
|
|
638
|
-
(
|
|
639
|
-
(
|
|
663
|
+
(96, "vllm:Qwen/Qwen3.6-27B"),
|
|
664
|
+
(32, "vllm:google/gemma-4-12B-it"),
|
|
665
|
+
(24, "vllm:Qwen/Qwen3.5-9B"),
|
|
666
|
+
(16, "vllm:google/gemma-4-E4B-it"),
|
|
667
|
+
(8, "vllm:LiquidAI/LFM2.5-2.6B"),
|
|
640
668
|
],
|
|
641
669
|
"llamacpp": [
|
|
642
670
|
(48, "llamacpp:ggml-org/gemma-4-31B-it-GGUF"),
|
|
643
|
-
(
|
|
644
|
-
(
|
|
645
|
-
(
|
|
671
|
+
(32, "llamacpp:ggml-org/gemma-4-26B-A4B-it-GGUF"),
|
|
672
|
+
(24, "llamacpp:ggml-org/gemma-4-12B-it-GGUF"),
|
|
673
|
+
(16, "llamacpp:ggml-org/gemma-4-E4B-it-GGUF"),
|
|
674
|
+
(8, "llamacpp:ggml-org/gemma-4-E2B-it-GGUF"),
|
|
675
|
+
(4, "llamacpp:LiquidAI/LFM2.5-2.6B-GGUF"),
|
|
646
676
|
],
|
|
647
677
|
}
|
|
648
678
|
|
|
@@ -656,7 +686,10 @@ def _catalog_row_family_version(row: Tuple[str, str, float, str, str, int]) -> T
|
|
|
656
686
|
for family, pattern in _VERSIONED_MODEL_PATTERNS:
|
|
657
687
|
match = pattern.search(text)
|
|
658
688
|
if match:
|
|
659
|
-
version
|
|
689
|
+
# Major version only — see model_catalog._model_family_version. The
|
|
690
|
+
# filter hides superseded *generations*; Qwen3.5 and Qwen3.6 are one
|
|
691
|
+
# generation filling two different RAM tiers and must coexist.
|
|
692
|
+
version = _version_tuple(match.group(1))[:1]
|
|
660
693
|
if version:
|
|
661
694
|
return family, version
|
|
662
695
|
return None
|
package/package.json
CHANGED
|
@@ -6,7 +6,7 @@ const root = process.cwd();
|
|
|
6
6
|
const pkg = JSON.parse(readFileSync(path.join(root, "package.json"), "utf8"));
|
|
7
7
|
const version = pkg.version;
|
|
8
8
|
const releaseDir = `output/release/v${version}`;
|
|
9
|
-
const releaseTheme = "
|
|
9
|
+
const releaseTheme = "All Systems On";
|
|
10
10
|
const title = `${version} — ${releaseTheme}`;
|
|
11
11
|
const escapedVersion = version.replaceAll(".", "\\.");
|
|
12
12
|
|
|
@@ -21,6 +21,28 @@
|
|
|
21
21
|
"specific, not less."
|
|
22
22
|
],
|
|
23
23
|
|
|
24
|
+
"11.2.0": {
|
|
25
|
+
"04-brain-chat-home.png": {
|
|
26
|
+
"_why": [
|
|
27
|
+
"The dock rail gained a fourth item (기능) opening the feature-toggle",
|
|
28
|
+
"drawer. The rail change itself is small; the LivingBrain animation",
|
|
29
|
+
"noise threshold (8%) may exceed it, so the screen is claimed with the",
|
|
30
|
+
"rail region only."
|
|
31
|
+
],
|
|
32
|
+
"region": [0.0, 0.25, 0.12, 0.85],
|
|
33
|
+
"min_pct": 0.5
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
"11.1.0": {
|
|
38
|
+
"_why": [
|
|
39
|
+
"Feature release with no intentional change to the twelve captured",
|
|
40
|
+
"screens: the new surfaces (proposals, thumbnails) render inside",
|
|
41
|
+
"existing panels and the capture corpus carries no image citations.",
|
|
42
|
+
"All twelve measured but not gated."
|
|
43
|
+
]
|
|
44
|
+
},
|
|
45
|
+
|
|
24
46
|
"11.0.1": {
|
|
25
47
|
"_why": [
|
|
26
48
|
"Defect-fix and branch-coverage release: no intentional UI change.",
|