ltcai 11.5.0 → 11.5.2

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 (182) hide show
  1. package/README.md +86 -47
  2. package/docs/CHANGELOG.md +76 -0
  3. package/docs/COMMUNITY_AND_PLUGINS.md +1 -1
  4. package/docs/DEVELOPMENT.md +1 -1
  5. package/docs/HYBRID_CLOUD_KG_STREAMING.md +8 -4
  6. package/docs/MULTI_AGENT_RUNTIME.md +1 -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/WORKFLOW_DESIGNER.md +1 -1
  12. package/docs/architecture.md +4 -4
  13. package/docs/kg-schema.md +1 -1
  14. package/docs/v11.5.1_RUST_FULL_LOOP_PLAN.md +85 -0
  15. package/docs/v11.5.2_TIGHT_SHIP_PLAN.md +144 -0
  16. package/lattice_brain/__init__.py +1 -1
  17. package/lattice_brain/graph/proactive.py +1 -1
  18. package/lattice_brain/graph/projection/v2_schema.py +0 -28
  19. package/lattice_brain/graph/vector_index/base.py +0 -3
  20. package/lattice_brain/graph/vector_index/brute_force.py +0 -4
  21. package/lattice_brain/graph/vector_index/hnsw.py +0 -3
  22. package/lattice_brain/graph/vector_index/quantized.py +0 -3
  23. package/lattice_brain/ingestion_jobs.py +0 -3
  24. package/lattice_brain/memory.py +0 -27
  25. package/lattice_brain/portability/sharing.py +0 -4
  26. package/lattice_brain/quality.py +0 -49
  27. package/lattice_brain/runtime/agent_runtime.py +1 -1
  28. package/lattice_brain/runtime/hooks.py +0 -13
  29. package/lattice_brain/runtime/multi_agent.py +1 -1
  30. package/lattice_brain/sealed_box.py +0 -4
  31. package/latticeai/__init__.py +1 -1
  32. package/latticeai/api/admin.py +12 -12
  33. package/latticeai/api/agent_worker_seam.py +389 -0
  34. package/latticeai/api/auth.py +25 -2
  35. package/latticeai/api/chat.py +3 -3
  36. package/latticeai/api/chat_agent_http.py +3 -12
  37. package/latticeai/api/chat_helpers.py +10 -13
  38. package/latticeai/api/computer_use.py +31 -42
  39. package/latticeai/api/health.py +21 -1
  40. package/latticeai/api/local_files.py +14 -0
  41. package/latticeai/api/permissions.py +38 -12
  42. package/latticeai/api/search.py +24 -0
  43. package/latticeai/api/static_routes.py +4 -1
  44. package/latticeai/cli/runtime.py +7 -3
  45. package/latticeai/core/config.py +47 -3
  46. package/latticeai/core/csrf.py +28 -2
  47. package/latticeai/core/embedding_providers/__init__.py +3 -5
  48. package/latticeai/core/embedding_providers/base.py +1 -1
  49. package/latticeai/core/embedding_providers/text.py +1 -1
  50. package/latticeai/core/enterprise.py +1 -4
  51. package/latticeai/core/http_origin.py +146 -0
  52. package/latticeai/core/invitations.py +3 -2
  53. package/latticeai/core/io_utils.py +2 -11
  54. package/latticeai/core/legacy_compatibility.py +1 -1
  55. package/latticeai/core/marketplace.py +1 -1
  56. package/latticeai/core/messages.py +40 -0
  57. package/latticeai/core/model_compat.py +2 -8
  58. package/latticeai/core/module_probe.py +37 -0
  59. package/latticeai/core/run_explain.py +5 -2
  60. package/latticeai/core/run_store.py +2 -2
  61. package/latticeai/core/security.py +13 -0
  62. package/latticeai/core/sessions.py +3 -2
  63. package/latticeai/core/sse.py +25 -0
  64. package/latticeai/core/users.py +0 -9
  65. package/latticeai/core/workspace_graph_trace.py +2 -8
  66. package/latticeai/core/workspace_os.py +0 -10
  67. package/latticeai/core/workspace_os_constants.py +1 -1
  68. package/latticeai/core/workspace_os_utils.py +34 -0
  69. package/latticeai/core/workspace_review_items.py +2 -8
  70. package/latticeai/core/workspace_runs.py +2 -8
  71. package/latticeai/core/workspace_skills.py +2 -7
  72. package/latticeai/models/router/documents.py +0 -35
  73. package/latticeai/models/router/registry.py +0 -4
  74. package/latticeai/runtime/access_runtime.py +16 -4
  75. package/latticeai/runtime/build_phases/features.py +25 -2
  76. package/latticeai/runtime/build_phases/web.py +2 -0
  77. package/latticeai/runtime/feature_toggle_wiring.py +12 -18
  78. package/latticeai/runtime/network_boundary_wiring.py +8 -19
  79. package/latticeai/runtime/permission_mode_wiring.py +6 -13
  80. package/latticeai/runtime/router_registration.py +4 -0
  81. package/latticeai/runtime/runtime_context.py +1 -13
  82. package/latticeai/runtime/service_singletons.py +55 -0
  83. package/latticeai/services/architecture_readiness.py +1 -1
  84. package/latticeai/services/brain_intelligence/proposals.py +0 -5
  85. package/latticeai/services/change_proposals.py +2 -36
  86. package/latticeai/services/chronicle.py +4 -6
  87. package/latticeai/services/command_center.py +4 -4
  88. package/latticeai/services/evidence_actions.py +5 -2
  89. package/latticeai/services/hybrid_chat.py +2 -2
  90. package/latticeai/services/hybrid_policy.py +8 -57
  91. package/latticeai/services/mode_store.py +132 -0
  92. package/latticeai/services/model_capability_registry.py +0 -15
  93. package/latticeai/services/model_catalog.py +0 -7
  94. package/latticeai/services/model_loading.py +3 -2
  95. package/latticeai/services/model_runtime/__init__.py +0 -57
  96. package/latticeai/services/model_runtime/engines.py +27 -128
  97. package/latticeai/services/model_runtime/loading.py +2 -2
  98. package/latticeai/services/network_boundary_service.py +7 -44
  99. package/latticeai/services/permission_mode_service.py +8 -56
  100. package/latticeai/services/product_readiness.py +1 -1
  101. package/latticeai/services/setup_detection.py +67 -1
  102. package/latticeai/services/tool_dispatch.py +0 -4
  103. package/latticeai/services/upload_service.py +7 -20
  104. package/latticeai/services/workspace_service.py +0 -6
  105. package/latticeai/setup/auto_setup.py +15 -26
  106. package/latticeai/setup/wizard/catalog.py +5 -2
  107. package/latticeai/setup/wizard/detect.py +6 -27
  108. package/latticeai/setup/wizard/paths.py +2 -5
  109. package/latticeai/setup/wizard/plans.py +2 -2
  110. package/package.json +2 -4
  111. package/scripts/bump_version.py +14 -1
  112. package/scripts/check_current_release_docs.mjs +1 -1
  113. package/scripts/check_legacy_debt.mjs +1 -1
  114. package/scripts/check_server_i18n.mjs +1 -0
  115. package/scripts/generate_agent_loop_fixtures.py +994 -0
  116. package/scripts/generate_rust_parity_fixtures.py +72 -161
  117. package/scripts/parity_fixture_corpus_context.py +162 -0
  118. package/scripts/parity_fixture_corpus_docgen.py +341 -0
  119. package/scripts/release_screen_claims.json +20 -0
  120. package/src-tauri/Cargo.lock +13 -7
  121. package/src-tauri/Cargo.toml +1 -1
  122. package/src-tauri/src/backend.rs +76 -2
  123. package/src-tauri/src/main.rs +13 -3
  124. package/src-tauri/tauri.conf.json +2 -2
  125. package/static/app/asset-manifest.json +41 -41
  126. package/static/app/assets/{Act-CWnxSCgN.js → Act-DcQizkl1.js} +1 -1
  127. package/static/app/assets/{AdminConsole-BEQYU6kF.js → AdminConsole-cf4npybT.js} +1 -1
  128. package/static/app/assets/{Brain-DWu1BhFg.js → Brain-3VCSHFcn.js} +1 -1
  129. package/static/app/assets/{BrainHome-95Hilr9R.js → BrainHome-Qm8eaztx.js} +1 -1
  130. package/static/app/assets/{BrainSignals-QdeqCpAF.js → BrainSignals-DS9BtKOW.js} +1 -1
  131. package/static/app/assets/{Capture-BHpCxnzb.js → Capture-DiQ219jW.js} +1 -1
  132. package/static/app/assets/{Chronicle-B4xYKoed.js → Chronicle-BGvuAchH.js} +1 -1
  133. package/static/app/assets/{CommandPalette-BVXnttSz.js → CommandPalette-Bqhm0Urn.js} +1 -1
  134. package/static/app/assets/{Library-DgYcHome.js → Library-BV6NnF0a.js} +1 -1
  135. package/static/app/assets/{LivingBrain-CrJLDbf7.js → LivingBrain-GzenJchP.js} +1 -1
  136. package/static/app/assets/{ProductFlow-DFlScKoJ.js → ProductFlow-DEP6-vML.js} +1 -1
  137. package/static/app/assets/{ReviewCard-Cy5f48Pj.js → ReviewCard-CNZ7XjWG.js} +1 -1
  138. package/static/app/assets/{System-NF8IfhTa.js → System-CieofHQa.js} +1 -1
  139. package/static/app/assets/arrow-left-kfsrk0mv.js +1 -0
  140. package/static/app/assets/{bot-CucuhLhm.js → bot-B_K1Tdmw.js} +1 -1
  141. package/static/app/assets/{brain-BBnSryW_.js → brain-DWyaV1L1.js} +1 -1
  142. package/static/app/assets/{button-C2GUj2Ai.js → button-aTn4s84A.js} +1 -1
  143. package/static/app/assets/circle-check-qqLug9nU.js +1 -0
  144. package/static/app/assets/{circle-pause-CbkWzBmG.js → circle-pause-xKgeGXkT.js} +1 -1
  145. package/static/app/assets/{circle-play-7lEaqHdJ.js → circle-play-DkT6tYPX.js} +1 -1
  146. package/static/app/assets/{cpu-DAlCXlIy.js → cpu-85xYObUC.js} +1 -1
  147. package/static/app/assets/{download-RNhuuJwh.js → download-B5Fm7YXo.js} +1 -1
  148. package/static/app/assets/{folder-open-CLW4odzM.js → folder-open-kk2Xa52u.js} +1 -1
  149. package/static/app/assets/{hard-drive-NKEiDIAJ.js → hard-drive-DkA3zBW_.js} +1 -1
  150. package/static/app/assets/{index-DMurvUuR.js → index-BMPdTmlY.js} +3 -3
  151. package/static/app/assets/index-DxmOfNRi.css +2 -0
  152. package/static/app/assets/{input-D2UhPC1X.js → input-B0nRf2jO.js} +1 -1
  153. package/static/app/assets/{link-2-6amKbP_P.js → link-2-Dwb4gnTc.js} +1 -1
  154. package/static/app/assets/{permissionCopy-Cu9TZtdR.js → permissionCopy-CQDUBrOZ.js} +1 -1
  155. package/static/app/assets/{primitives-gPsccucr.js → primitives-SNp0LRJz.js} +1 -1
  156. package/static/app/assets/search-BcHqkjoy.js +1 -0
  157. package/static/app/assets/{share-2-Bau7KkPq.js → share-2-BsrxFglO.js} +1 -1
  158. package/static/app/assets/{shield-alert-BufNYypi.js → shield-alert-5BStfp2_.js} +1 -1
  159. package/static/app/assets/{textarea-BQnVWhYs.js → textarea-Cg8IUA-k.js} +1 -1
  160. package/static/app/assets/{useFocusTrap-B3_w60si.js → useFocusTrap-CYKvE46M.js} +1 -1
  161. package/static/app/assets/{useMutation-BHhCflT6.js → useMutation-CSn9t1op.js} +1 -1
  162. package/static/app/assets/{useQuery-rBWfI-5t.js → useQuery-CY2OI2uy.js} +1 -1
  163. package/static/app/assets/{utils-V_5-wxr5.js → utils-Ddol2RWD.js} +1 -1
  164. package/static/app/assets/{workspace-K1zjYUHj.js → workspace-BqDwOz_p.js} +1 -1
  165. package/static/app/index.html +4 -4
  166. package/static/sw.js +1 -1
  167. package/desktop/electron/README.md +0 -9
  168. package/desktop/electron/main.cjs +0 -58
  169. package/desktop/electron/preload.cjs +0 -5
  170. package/latticeai/core/graph_curator.py +0 -11
  171. package/latticeai/core/hooks.py +0 -11
  172. package/latticeai/core/local_embeddings.py +0 -104
  173. package/latticeai/core/multi_agent.py +0 -11
  174. package/latticeai/core/workflow_engine.py +0 -11
  175. package/latticeai/services/ingestion.py +0 -11
  176. package/latticeai/services/kg_portability.py +0 -11
  177. package/latticeai/services/multimodal_streaming.py +0 -129
  178. package/scripts/measure_brain_home_fill.mjs +0 -141
  179. package/static/app/assets/arrow-left-DwkSYrjR.js +0 -1
  180. package/static/app/assets/circle-check-CxOVPwYq.js +0 -1
  181. package/static/app/assets/index-BLPb5lmE.css +0 -2
  182. package/static/app/assets/search-Cj_TKk_2.js +0 -1
@@ -1,104 +0,0 @@
1
- """Local deterministic embeddings for Lattice AI search.
2
-
3
- The v3 backend needs a local-first vector signal without introducing a cloud
4
- runtime requirement. This module provides a small feature-hashing embedder that
5
- is deterministic, cheap to run, and good enough for indexing/search tests. A
6
- future runtime can swap the implementation behind the same interface when a
7
- local model server is available.
8
- """
9
-
10
- from __future__ import annotations
11
-
12
- import hashlib
13
- import math
14
- import os
15
- import re
16
- import struct
17
- from dataclasses import dataclass
18
- from typing import Iterable, List
19
-
20
- DEFAULT_EMBEDDING_DIM = int(os.getenv("LATTICEAI_VECTOR_DIM", "384"))
21
- EMBEDDING_MODEL_ID = f"lattice-local-hash-v1:{DEFAULT_EMBEDDING_DIM}"
22
-
23
-
24
- def embedding_model_id(dim: int) -> str:
25
- return f"lattice-local-hash-v1:{int(dim)}"
26
-
27
-
28
- def _tokenize(text: str) -> List[str]:
29
- raw = str(text or "").lower()
30
- tokens = re.findall(r"[a-z0-9][a-z0-9_.:/+-]{1,}|[가-힣]{2,}", raw)
31
- features: List[str] = []
32
- for token in tokens:
33
- features.append(f"tok:{token}")
34
- if len(token) >= 5 and re.search(r"[a-z]", token):
35
- for i in range(0, len(token) - 2):
36
- features.append(f"tri:{token[i:i+3]}")
37
- if re.search(r"[가-힣]", token) and len(token) >= 3:
38
- for i in range(0, len(token) - 1):
39
- features.append(f"ko:{token[i:i+2]}")
40
- return features
41
-
42
-
43
- def _hash_to_index(feature: str, dim: int) -> tuple[int, float]:
44
- digest = hashlib.blake2b(feature.encode("utf-8"), digest_size=8).digest()
45
- value = int.from_bytes(digest, "big", signed=False)
46
- sign = 1.0 if (value & 1) == 0 else -1.0
47
- return value % dim, sign
48
-
49
-
50
- @dataclass(frozen=True)
51
- class LocalEmbeddingModel:
52
- """Deterministic feature-hashing embedder.
53
-
54
- The output vectors are L2-normalized, so cosine similarity is just a dot
55
- product. No network access, model download, or global mutable state is
56
- required.
57
- """
58
-
59
- dim: int = DEFAULT_EMBEDDING_DIM
60
- model_id: str = EMBEDDING_MODEL_ID
61
-
62
- def __post_init__(self) -> None:
63
- if self.model_id == EMBEDDING_MODEL_ID and self.dim != DEFAULT_EMBEDDING_DIM:
64
- object.__setattr__(self, "model_id", embedding_model_id(self.dim))
65
-
66
- def embed(self, text: str) -> List[float]:
67
- vector = [0.0] * self.dim
68
- features = _tokenize(text)
69
- if not features:
70
- return vector
71
- for feature in features:
72
- index, sign = _hash_to_index(feature, self.dim)
73
- vector[index] += sign
74
- norm = math.sqrt(sum(value * value for value in vector))
75
- if norm <= 0:
76
- return vector
77
- return [value / norm for value in vector]
78
-
79
- def similarity(self, left: Iterable[float], right: Iterable[float]) -> float:
80
- # strict=True: a dimension mismatch means the two vectors came from
81
- # different embedding models. Truncating to the shorter one produces a
82
- # plausible-looking similarity that is meaningless — exactly the silent
83
- # wrongness this codebase keeps finding. Callers that can hit a model
84
- # swap already handle failure and fall back to lexical search.
85
- left_v, right_v = list(left), list(right)
86
- if len(left_v) != len(right_v):
87
- raise ValueError(
88
- f"embedding dimension mismatch: {len(left_v)} vs {len(right_v)}; "
89
- "the vector index was built with a different model"
90
- )
91
- return float(sum(a * b for a, b in zip(left_v, right_v, strict=True)))
92
-
93
- def encode(self, vector: Iterable[float]) -> bytes:
94
- values = list(vector)
95
- return struct.pack(f"<{len(values)}f", *values)
96
-
97
- def decode(self, payload: bytes, dim: int | None = None) -> List[float]:
98
- if not payload:
99
- return []
100
- count = int(dim or self.dim)
101
- expected = count * 4
102
- if len(payload) != expected:
103
- count = len(payload) // 4
104
- return list(struct.unpack(f"<{count}f", payload[: count * 4]))
@@ -1,11 +0,0 @@
1
- """Compatibility shim: physically moved to ``lattice_brain.runtime.multi_agent``.
2
-
3
- Aliases itself to the physical module so identity, module-level state, and
4
- monkeypatching keep working through the old import path.
5
- """
6
-
7
- import sys
8
-
9
- import lattice_brain.runtime.multi_agent as _impl
10
-
11
- sys.modules[__name__] = _impl
@@ -1,11 +0,0 @@
1
- """Compatibility shim: physically moved to ``lattice_brain.workflow``.
2
-
3
- Aliases itself to the physical module so identity, module-level state, and
4
- monkeypatching keep working through the old import path.
5
- """
6
-
7
- import sys
8
-
9
- import lattice_brain.workflow as _impl
10
-
11
- sys.modules[__name__] = _impl
@@ -1,11 +0,0 @@
1
- """Compatibility shim: physically moved to ``lattice_brain.ingestion``.
2
-
3
- Aliases itself to the physical module so identity, module-level state, and
4
- monkeypatching keep working through the old import path.
5
- """
6
-
7
- import sys
8
-
9
- import lattice_brain.ingestion as _impl
10
-
11
- sys.modules[__name__] = _impl
@@ -1,11 +0,0 @@
1
- """Compatibility shim: physically moved to ``lattice_brain.portability``.
2
-
3
- Aliases itself to the physical module so identity, module-level state, and
4
- monkeypatching keep working through the old import path.
5
- """
6
-
7
- import sys
8
-
9
- import lattice_brain.portability as _impl
10
-
11
- sys.modules[__name__] = _impl
@@ -1,129 +0,0 @@
1
- """Multimodal / video streaming contracts behind NetworkBoundaryMode (Phase 3).
2
-
3
- Video generation and other multimodal cloud calls share the same boundary dial
4
- as text: nothing leaves the machine unless mode is CLOUD_ALLOWED *and* the
5
- hybrid policy has ``allow_multimodal=True``.
6
-
7
- This module is intentionally adapter-shaped: concrete providers (Runway, Luma,
8
- Veo-compatible endpoints, etc.) plug in later without changing the chat path.
9
- """
10
-
11
- from __future__ import annotations
12
-
13
- from dataclasses import dataclass, field
14
- from typing import Any, AsyncIterator, Dict, List, Optional, Protocol
15
-
16
- from latticeai.core.network_boundary import (
17
- NetworkBoundaryMode,
18
- normalize_network_mode,
19
- )
20
- from latticeai.services.hybrid_context import MinimalContext
21
-
22
-
23
- @dataclass
24
- class MultimodalTurnResult:
25
- user_message: str
26
- media_urls: List[str] = field(default_factory=list)
27
- media_kind: str = "video" # video | image | audio
28
- answer_text: str = ""
29
- sent_node_ids: List[str] = field(default_factory=list)
30
- provider: str = ""
31
- model: str = ""
32
- usage: Dict[str, Any] = field(default_factory=dict)
33
-
34
- def to_dict(self) -> Dict[str, Any]:
35
- return {
36
- "user_message": self.user_message,
37
- "media_urls": list(self.media_urls),
38
- "media_kind": self.media_kind,
39
- "answer_text": self.answer_text,
40
- "sent_node_ids": list(self.sent_node_ids),
41
- "provider": self.provider,
42
- "model": self.model,
43
- "usage": dict(self.usage),
44
- }
45
-
46
-
47
- class MultimodalAdapter(Protocol):
48
- # See CloudLLMAdapter.stream: an async generator is declared with `def`.
49
- def stream_media(
50
- self,
51
- *,
52
- prompt: str,
53
- context: str,
54
- model: Optional[str] = None,
55
- ) -> AsyncIterator[Any]:
56
- """Yield progress events, then a final event with media_urls.
57
-
58
- Typed as ``Any`` rather than ``Dict[str, Any]`` because adapters are
59
- supplied by callers: the bridge validates each event's shape rather
60
- than trusting the annotation.
61
- """
62
- ...
63
-
64
-
65
- class MultimodalStreamingBridge:
66
- """Boundary-gated multimodal turn orchestrator."""
67
-
68
- def __init__(self, adapter: Optional[MultimodalAdapter] = None) -> None:
69
- self._adapter = adapter
70
-
71
- async def run_turn(
72
- self,
73
- *,
74
- user_message: str,
75
- minimal: MinimalContext,
76
- mode: NetworkBoundaryMode | str,
77
- allow_multimodal: bool,
78
- model: Optional[str] = None,
79
- ) -> MultimodalTurnResult:
80
- mode = normalize_network_mode(mode)
81
- if mode != NetworkBoundaryMode.CLOUD_ALLOWED:
82
- raise PermissionError(
83
- f"multimodal cloud refused under NetworkBoundaryMode={mode.value!r}"
84
- )
85
- if not allow_multimodal:
86
- raise PermissionError(
87
- "multimodal cloud is disabled by hybrid policy (allow_multimodal=false)"
88
- )
89
- if self._adapter is None:
90
- return MultimodalTurnResult(
91
- user_message=user_message,
92
- media_urls=[],
93
- answer_text=(
94
- "[multimodal adapter not configured] "
95
- f"Would stream media grounded on {len(minimal.node_ids)} local node(s)."
96
- ),
97
- sent_node_ids=list(minimal.node_ids),
98
- provider="none",
99
- model=model or "",
100
- )
101
-
102
- urls: List[str] = []
103
- notes: List[str] = []
104
- async for event in self._adapter.stream_media(
105
- prompt=user_message,
106
- context=minimal.compact_text,
107
- model=model,
108
- ):
109
- if not isinstance(event, dict):
110
- continue
111
- if event.get("media_url"):
112
- urls.append(str(event["media_url"]))
113
- if event.get("text"):
114
- notes.append(str(event["text"]))
115
- return MultimodalTurnResult(
116
- user_message=user_message,
117
- media_urls=urls,
118
- answer_text="\n".join(notes),
119
- sent_node_ids=list(minimal.node_ids),
120
- provider=getattr(self._adapter, "provider_name", "multimodal"),
121
- model=str(model or getattr(self._adapter, "default_model", "")),
122
- )
123
-
124
-
125
- __all__ = [
126
- "MultimodalTurnResult",
127
- "MultimodalAdapter",
128
- "MultimodalStreamingBridge",
129
- ]
@@ -1,141 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Measure Brain chat home (screen 04) content fill against the viewport.
4
- *
5
- * The layout rebuild requires the empty home's content bottom to reach at
6
- * least 90% of the 920px capture viewport — not "looks better" prose.
7
- *
8
- * Walks the same onboarding path as capture_release_evidence.mjs so #/brain
9
- * is the empty first-run home against the visual mock server.
10
- *
11
- * Exit 0 when bottom/viewport >= --min-ratio (default 0.90), else 1.
12
- */
13
- import { chromium } from "playwright";
14
- import { spawn } from "node:child_process";
15
-
16
- const repoRoot = process.cwd();
17
- const port = Number(process.env.LTCAI_RELEASE_EVIDENCE_PORT || 4937);
18
- const baseURL = `http://127.0.0.1:${port}`;
19
- const minRatio = Number(process.env.LTCAI_HOME_FILL_MIN || "0.90");
20
-
21
- const server = spawn(process.execPath, ["tests/visual/mock_server.cjs"], {
22
- cwd: repoRoot,
23
- env: { ...process.env, LTCAI_VISUAL_PORT: String(port) },
24
- stdio: ["ignore", "pipe", "pipe"],
25
- });
26
-
27
- async function waitForServer() {
28
- const deadline = Date.now() + 15000;
29
- while (Date.now() < deadline) {
30
- try {
31
- const res = await fetch(`${baseURL}/health`);
32
- if (res.ok) return;
33
- } catch {
34
- /* retry */
35
- }
36
- await new Promise((r) => setTimeout(r, 150));
37
- }
38
- throw new Error(`visual server did not start on ${baseURL}`);
39
- }
40
-
41
- async function main() {
42
- await waitForServer();
43
- const browser = await chromium.launch({ headless: true });
44
- const context = await browser.newContext({
45
- viewport: { width: 1440, height: 920 },
46
- });
47
- const page = await context.newPage();
48
- page.setDefaultTimeout(15000);
49
-
50
- // Same first-run reset as release capture — without this the product flow
51
- // may already be complete and the wake button never appears.
52
- await page.addInitScript(() => {
53
- localStorage.removeItem("lattice.productFlow.complete");
54
- localStorage.removeItem("lattice.productFlow.user");
55
- localStorage.setItem("lattice.language", "ko");
56
- localStorage.setItem("lattice.mode", "basic");
57
- });
58
-
59
- await page.goto(`${baseURL}/app`, { waitUntil: "networkidle" });
60
- const korean = page.getByRole("button", { name: "한국어" });
61
- if (await korean.count()) {
62
- await korean.click();
63
- }
64
- await page.getByRole("button", { name: "Brain 지금 깨우기" }).click();
65
- await page.getByText("이 Brain의 주인을 정합니다.").waitFor();
66
- await page.getByPlaceholder(/You|나/).fill("Codex");
67
- await page.getByPlaceholder("you@local").fill("codex@local");
68
- await page.getByPlaceholder("로컬 Brain 비밀번호").fill("Lattice123");
69
- await page.getByRole("button", { name: "내 Brain 시작하기" }).click();
70
- await page.getByText("추천대로 시작하세요.").waitFor();
71
- await page.getByRole("button", { name: "추천으로 바로 시작" }).click();
72
- await page.getByText("모델을 준비하고 시작합니다.").waitFor();
73
- await page.getByRole("button", { name: "준비하고 시작하기" }).click();
74
- await page.waitForTimeout(180);
75
- await page
76
- .getByRole("button", { name: /시작|들어가|Brain 열기|Open Brain/i })
77
- .first()
78
- .click({ timeout: 5000 })
79
- .catch(() => {});
80
- await page.locator("main[aria-label='Lattice Brain']").waitFor({ timeout: 15000 });
81
- await page.waitForTimeout(250);
82
-
83
- const metrics = await page.evaluate(() => {
84
- const main = document.querySelector("main[aria-label='Lattice Brain']");
85
- const home =
86
- document.querySelector(".brain-centered-home") ||
87
- document.querySelector(".brain-home-station") ||
88
- main;
89
- const quiet = document.querySelector(
90
- "footer.brain-home-quiet, .brain-home-quiet, .brain-home-shelves",
91
- );
92
- const composer = document.querySelector(
93
- ".brain-composer, [data-testid='brain-composer'], textarea",
94
- );
95
- const box = (el) => {
96
- if (!el) return null;
97
- const r = el.getBoundingClientRect();
98
- return { top: r.top, bottom: r.bottom, height: r.height, width: r.width };
99
- };
100
- const candidates = [home, quiet, composer, main].filter(Boolean);
101
- let bottom = 0;
102
- let top = Infinity;
103
- for (const el of candidates) {
104
- const r = el.getBoundingClientRect();
105
- bottom = Math.max(bottom, r.bottom);
106
- top = Math.min(top, r.top);
107
- }
108
- return {
109
- viewport: { width: window.innerWidth, height: window.innerHeight },
110
- main: box(main),
111
- home: box(home),
112
- quiet: box(quiet),
113
- composer: box(composer),
114
- contentTop: top === Infinity ? 0 : top,
115
- contentBottom: bottom,
116
- };
117
- });
118
-
119
- await browser.close();
120
-
121
- const ratio = metrics.contentBottom / metrics.viewport.height;
122
- const pct = (100 * ratio).toFixed(1);
123
- console.log(JSON.stringify({ ...metrics, fillRatio: ratio, fillPct: `${pct}%`, minRatio }, null, 2));
124
- console.log(
125
- `brain home fill: contentBottom=${metrics.contentBottom.toFixed(1)}px / viewport=${metrics.viewport.height}px = ${pct}% (need >= ${(100 * minRatio).toFixed(0)}%)`,
126
- );
127
-
128
- if (ratio + 1e-6 < minRatio) {
129
- console.error(
130
- `brain home fill FAIL: content only reaches ${pct}% of viewport (required ${(100 * minRatio).toFixed(0)}%).`,
131
- );
132
- process.exit(1);
133
- }
134
- console.log("brain home fill PASS");
135
- }
136
-
137
- try {
138
- await main();
139
- } finally {
140
- server.kill();
141
- }
@@ -1 +0,0 @@
1
- import{_ as e}from"./utils-V_5-wxr5.js";var t=e(`arrow-left`,[[`path`,{d:`m12 19-7-7 7-7`,key:`1l729n`}],[`path`,{d:`M19 12H5`,key:`x3x0zl`}]]);export{t};
@@ -1 +0,0 @@
1
- import{_ as e}from"./utils-V_5-wxr5.js";var t=e(`circle-check`,[[`circle`,{cx:`12`,cy:`12`,r:`10`,key:`1mglay`}],[`path`,{d:`m9 12 2 2 4-4`,key:`dzmm74`}]]);export{t};