ltcai 10.7.0 → 10.9.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 -40
- package/docs/CHANGELOG.md +68 -0
- package/docs/COMMUNITY_AND_PLUGINS.md +1 -1
- package/docs/DEVELOPMENT.md +1 -1
- package/docs/ONBOARDING.md +1 -1
- package/docs/OPERATIONS.md +1 -1
- package/docs/TRUST_MODEL.md +1 -1
- package/docs/WHY_LATTICE.md +1 -1
- package/docs/kg-schema.md +1 -1
- package/lattice_brain/__init__.py +1 -1
- package/lattice_brain/graph/retrieval_vector.py +69 -33
- package/lattice_brain/runtime/multi_agent.py +96 -15
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/admin.py +9 -6
- package/latticeai/api/auth.py +37 -38
- package/latticeai/api/browser.py +7 -6
- package/latticeai/api/chat.py +9 -6
- package/latticeai/api/chat_agent_http.py +2 -1
- package/latticeai/api/chat_history.py +4 -2
- package/latticeai/api/chat_intents.py +14 -14
- package/latticeai/api/knowledge_graph.py +5 -4
- package/latticeai/api/local_files.py +25 -15
- package/latticeai/api/mcp.py +15 -11
- package/latticeai/api/memory.py +2 -1
- package/latticeai/api/models.py +39 -24
- package/latticeai/api/network_boundary.py +9 -4
- package/latticeai/api/portability.py +27 -19
- package/latticeai/api/project_sessions.py +7 -5
- package/latticeai/api/review_queue.py +12 -7
- package/latticeai/api/setup.py +10 -11
- package/latticeai/api/static_routes.py +62 -46
- package/latticeai/api/tools.py +7 -6
- package/latticeai/core/file_generation.py +141 -8
- package/latticeai/core/legacy_compatibility.py +1 -1
- package/latticeai/core/marketplace.py +1 -1
- package/latticeai/core/mcp_registry.py +20 -4
- package/latticeai/core/messages.py +452 -0
- package/latticeai/core/workspace_computer_memory.py +84 -0
- package/latticeai/core/workspace_indexing.py +102 -0
- package/latticeai/core/workspace_onboarding.py +104 -0
- package/latticeai/core/workspace_os.py +24 -207
- package/latticeai/core/workspace_os_constants.py +1 -1
- package/latticeai/core/workspace_relationships.py +99 -0
- package/latticeai/integrations/telegram_bot.py +20 -18
- package/latticeai/services/architecture_readiness.py +1 -1
- package/latticeai/services/model_loading.py +63 -27
- package/latticeai/services/product_readiness.py +1 -1
- package/package.json +3 -2
- package/scripts/check_current_release_docs.mjs +1 -1
- package/scripts/check_screenshot_pixel_delta.py +92 -10
- package/scripts/check_server_i18n.mjs +96 -0
- package/scripts/release_screen_claims.json +46 -0
- 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-DIbkoJqs.js → Act-CS9IeqUX.js} +1 -1
- package/static/app/assets/{AdminConsole-Cbi6Jre6.js → AdminConsole-3UkIEWGA.js} +1 -1
- package/static/app/assets/{Brain-BPfuXZ71.js → Brain-B22EmNqS.js} +1 -1
- package/static/app/assets/{BrainHome-87XaAS4V.js → BrainHome-CMDqgJF4.js} +2 -2
- package/static/app/assets/{BrainSignals-ExpcdiUi.js → BrainSignals-BeE8RJo3.js} +1 -1
- package/static/app/assets/{Capture-Bxe4WiVX.js → Capture-ZX9bQh68.js} +1 -1
- package/static/app/assets/{CommandPalette-B1PV5Yd4.js → CommandPalette-86m4FCcN.js} +1 -1
- package/static/app/assets/{Library-BRlJJmwZ.js → Library-Bhz5LUca.js} +1 -1
- package/static/app/assets/{LivingBrain-D61qG3WY.js → LivingBrain-BSa0wpFG.js} +1 -1
- package/static/app/assets/ProductFlow-IP4Q-5aQ.js +1 -0
- package/static/app/assets/{ReviewCard-BoiWv0Y1.js → ReviewCard-BepjSDpN.js} +1 -1
- package/static/app/assets/{System-Ci0HEtTJ.js → System-Bwx1h_jT.js} +1 -1
- package/static/app/assets/arrow-left-ig7AZU8B.js +1 -0
- package/static/app/assets/{bot-BaPt4PPn.js → bot-DQj0-LkM.js} +1 -1
- package/static/app/assets/{brain-BSkWaj3i.js → brain-D8OEwmVj.js} +1 -1
- package/static/app/assets/{button-CKwtO1O0.js → button-CmTknyAP.js} +1 -1
- package/static/app/assets/{circle-pause-CiUc5RRk.js → circle-pause-yTCWRziJ.js} +1 -1
- package/static/app/assets/{circle-play-BaTDxzR9.js → circle-play-Ccrva84R.js} +1 -1
- package/static/app/assets/{cpu-Dm1gHuP1.js → cpu-CbJqWTlS.js} +1 -1
- package/static/app/assets/{download-C32oGU-C.js → download-CkSzbzU-.js} +1 -1
- package/static/app/assets/{folder-open-Ch2jLVdD.js → folder-open-CKyjQ4PU.js} +1 -1
- package/static/app/assets/{hard-drive-BBjA5st8.js → hard-drive-DAzk9um0.js} +1 -1
- package/static/app/assets/index-BfD-jhA9.css +2 -0
- package/static/app/assets/{index-J6h01X78.js → index-CxOcwsHV.js} +3 -3
- package/static/app/assets/{input-y6ABdKWY.js → input-DcMETmZ7.js} +1 -1
- package/static/app/assets/{permissionCopy-BEtc0Ihd.js → permissionCopy-BVf13_25.js} +1 -1
- package/static/app/assets/{primitives-GSFXF2Pd.js → primitives-CQV9Q2YM.js} +1 -1
- package/static/app/assets/search-CXGASMMH.js +1 -0
- package/static/app/assets/{share-2-BgGq2A63.js → share-2-COWCHNZm.js} +1 -1
- package/static/app/assets/{shield-alert-BYA491cj.js → shield-alert-BDrvilyK.js} +1 -1
- package/static/app/assets/{textarea-BAcim3fs.js → textarea-rUmsc8cP.js} +1 -1
- package/static/app/assets/{useFocusTrap-qLg24jiv.js → useFocusTrap-Bi5UY_8v.js} +1 -1
- package/static/app/assets/{useQuery-xkAu9eDX.js → useQuery-C-AicB-3.js} +1 -1
- package/static/app/assets/{utils-CogdofAA.js → utils-BMwWg78e.js} +1 -1
- package/static/app/assets/{workspace-BMvfx9_R.js → workspace-Y93tls8P.js} +1 -1
- package/static/app/index.html +4 -4
- package/static/sw.js +1 -1
- package/static/app/assets/ProductFlow-BxjxI8Aa.js +0 -1
- package/static/app/assets/arrow-left-DArdBUxd.js +0 -1
- package/static/app/assets/index-BV7Yf6Eq.css +0 -2
- package/static/app/assets/search-CaIZKyo_.js +0 -1
|
@@ -78,36 +78,22 @@ def _get_model_runtime_deps(runtime_state: Any):
|
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
|
|
81
|
-
|
|
81
|
+
def _prepare_model_sources(
|
|
82
|
+
deps: Any,
|
|
83
|
+
parsed_provider: str,
|
|
84
|
+
parsed_model: str,
|
|
82
85
|
model_id: str,
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
adapter_path: Optional[str] = None,
|
|
87
|
-
draft_model_id: Optional[str] = None,
|
|
88
|
-
allow_download: bool = False,
|
|
89
|
-
*,
|
|
90
|
-
runtime_state: Any,
|
|
91
|
-
) -> Dict[str, object]:
|
|
92
|
-
deps = _get_model_runtime_deps(runtime_state)
|
|
93
|
-
model_id = deps["normalize_local_model_request"](model_id, engine)
|
|
94
|
-
if not model_id:
|
|
95
|
-
raise ModelRuntimeError(status_code=400, detail="모델 식별자가 비어 있습니다.")
|
|
96
|
-
|
|
97
|
-
resolution = deps["_ModelResolution"].from_request(
|
|
98
|
-
model_id,
|
|
99
|
-
engine=engine,
|
|
100
|
-
user_email=user_email or deps["get_current_user"](request),
|
|
101
|
-
engine_aliases=deps["MODEL_ENGINE_ALIASES"],
|
|
102
|
-
)
|
|
86
|
+
allow_download: bool,
|
|
87
|
+
) -> Dict[str, Any]:
|
|
88
|
+
"""Make sure the engine and weights this model needs exist locally.
|
|
103
89
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
compatibility = deps["_model_runtime_compatibility"](parsed_model, engine=parsed_provider)
|
|
108
|
-
if compatibility.get("supported") is False:
|
|
109
|
-
raise ModelRuntimeError(status_code=400, detail=compatibility)
|
|
90
|
+
Every branch here shells out, downloads, or waits on a child server, so this
|
|
91
|
+
is deliberately a plain sync function: callers run it on a worker thread
|
|
92
|
+
(see :func:`prepare_and_load_model`) rather than on the event loop.
|
|
110
93
|
|
|
94
|
+
Returns the four values the caller needs back, including the two the
|
|
95
|
+
LM Studio branch rewrites (``parsed_model`` and ``model_id``).
|
|
96
|
+
"""
|
|
111
97
|
local_engines = {"local_mlx", "ollama", "vllm", "lmstudio", "llamacpp"}
|
|
112
98
|
install_result: Dict[str, object] = {}
|
|
113
99
|
download_result: Optional[Dict[str, object]] = None
|
|
@@ -169,6 +155,56 @@ async def prepare_and_load_model(
|
|
|
169
155
|
model_id = f"lmstudio:{resolved_model}"
|
|
170
156
|
download_result = ensured
|
|
171
157
|
|
|
158
|
+
return {
|
|
159
|
+
"install_result": install_result,
|
|
160
|
+
"download_result": download_result,
|
|
161
|
+
"parsed_model": parsed_model,
|
|
162
|
+
"model_id": model_id,
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
async def prepare_and_load_model(
|
|
167
|
+
model_id: str,
|
|
168
|
+
request: Any,
|
|
169
|
+
engine: Optional[str] = None,
|
|
170
|
+
user_email: Optional[str] = None,
|
|
171
|
+
adapter_path: Optional[str] = None,
|
|
172
|
+
draft_model_id: Optional[str] = None,
|
|
173
|
+
allow_download: bool = False,
|
|
174
|
+
*,
|
|
175
|
+
runtime_state: Any,
|
|
176
|
+
) -> Dict[str, object]:
|
|
177
|
+
deps = _get_model_runtime_deps(runtime_state)
|
|
178
|
+
model_id = deps["normalize_local_model_request"](model_id, engine)
|
|
179
|
+
if not model_id:
|
|
180
|
+
raise ModelRuntimeError(status_code=400, detail="모델 식별자가 비어 있습니다.")
|
|
181
|
+
|
|
182
|
+
resolution = deps["_ModelResolution"].from_request(
|
|
183
|
+
model_id,
|
|
184
|
+
engine=engine,
|
|
185
|
+
user_email=user_email or deps["get_current_user"](request),
|
|
186
|
+
engine_aliases=deps["MODEL_ENGINE_ALIASES"],
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
parsed_provider, parsed_model = deps["parse_model_ref"](model_id)
|
|
190
|
+
if parsed_provider == "mlx":
|
|
191
|
+
parsed_provider = "local_mlx"
|
|
192
|
+
compatibility = deps["_model_runtime_compatibility"](parsed_model, engine=parsed_provider)
|
|
193
|
+
if compatibility.get("supported") is False:
|
|
194
|
+
raise ModelRuntimeError(status_code=400, detail=compatibility)
|
|
195
|
+
|
|
196
|
+
# Engine install, model download and local-server startup are all blocking
|
|
197
|
+
# and all measured in minutes. Run on the event loop they froze the whole
|
|
198
|
+
# server for the duration — the UI could not even poll /health to find out
|
|
199
|
+
# why. One hop to a worker thread; the async shape above is unchanged.
|
|
200
|
+
sources = await asyncio.to_thread(
|
|
201
|
+
_prepare_model_sources, deps, parsed_provider, parsed_model, model_id, allow_download
|
|
202
|
+
)
|
|
203
|
+
install_result = sources["install_result"]
|
|
204
|
+
download_result = sources["download_result"]
|
|
205
|
+
parsed_model = sources["parsed_model"]
|
|
206
|
+
model_id = sources["model_id"]
|
|
207
|
+
|
|
172
208
|
effective_email = (user_email or deps["get_current_user"](request) or "").strip()
|
|
173
209
|
user_api_key = deps["get_user_api_key"](effective_email, parsed_provider) if parsed_provider != "local_mlx" else None
|
|
174
210
|
msg = await deps["router"].load_model(
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ltcai",
|
|
3
|
-
"version": "10.
|
|
3
|
+
"version": "10.9.0",
|
|
4
4
|
"description": "Lattice AI — local-first Digital Brain that keeps your knowledge durable across any AI model.",
|
|
5
5
|
"homepage": "https://github.com/TaeSooPark-PTS/LatticeAI#readme",
|
|
6
6
|
"repository": {
|
|
@@ -24,9 +24,10 @@
|
|
|
24
24
|
"build:assets": "vite build && node scripts/build_frontend_assets.mjs",
|
|
25
25
|
"build:python": "node scripts/run_python.mjs -m build",
|
|
26
26
|
"check:python": "node scripts/run_python.mjs scripts/check_python.py",
|
|
27
|
-
"lint": "npm run lint:python && node --check tests/visual/mock_server.cjs && node --check tests/visual/v3.spec.js && npm run lint:frontend && npm run frontend:openapi:check && node scripts/check_i18n_literals.mjs && npm run check:i18n-namespaces && npm run check:bundle && npm run check:legacy-debt && npm run test:browser-extension && npm run test:vscode-extension && node scripts/check_release_evidence_bound.mjs",
|
|
27
|
+
"lint": "npm run lint:python && node --check tests/visual/mock_server.cjs && node --check tests/visual/v3.spec.js && npm run lint:frontend && npm run frontend:openapi:check && node scripts/check_i18n_literals.mjs && npm run check:i18n-namespaces && npm run check:bundle && npm run check:legacy-debt && npm run check:server-i18n && npm run test:browser-extension && npm run test:vscode-extension && node scripts/check_release_evidence_bound.mjs",
|
|
28
28
|
"check:release-evidence-bound": "node scripts/check_release_evidence_bound.mjs",
|
|
29
29
|
"check:legacy-debt": "node scripts/check_legacy_debt.mjs",
|
|
30
|
+
"check:server-i18n": "node scripts/check_server_i18n.mjs",
|
|
30
31
|
"lint:python": "node scripts/run_python.mjs -m ruff check . && npm run typecheck:python",
|
|
31
32
|
"lint:frontend": "node scripts/lint_frontend.mjs",
|
|
32
33
|
"check:bundle": "node scripts/check_bundle_budget.mjs",
|
|
@@ -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 = "Never Blocks";
|
|
10
10
|
const title = `${version} — ${releaseTheme}`;
|
|
11
11
|
const escapedVersion = version.replaceAll(".", "\\.");
|
|
12
12
|
|
|
@@ -279,8 +279,18 @@ def _default_baseline(root: Path, current: Path) -> Optional[Path]:
|
|
|
279
279
|
return path
|
|
280
280
|
|
|
281
281
|
|
|
282
|
-
def _pixel_delta_pct(
|
|
283
|
-
|
|
282
|
+
def _pixel_delta_pct(
|
|
283
|
+
a: Path,
|
|
284
|
+
b: Path,
|
|
285
|
+
region: Optional[List[float]] = None,
|
|
286
|
+
) -> Tuple[float, int, int, int, int]:
|
|
287
|
+
"""Return (changed_pct, changed_px, total_px, height_a, height_b).
|
|
288
|
+
|
|
289
|
+
``region`` is ``[x0, y0, x1, y1]`` as fractions of the aligned canvas. It
|
|
290
|
+
narrows the comparison to the part of the screen a release says it
|
|
291
|
+
changed, so an animated element elsewhere cannot mask a real change — or
|
|
292
|
+
manufacture one. Heights are always reported for the full images.
|
|
293
|
+
"""
|
|
284
294
|
with Image.open(a) as im_a, Image.open(b) as im_b:
|
|
285
295
|
im_a = im_a.convert("RGB")
|
|
286
296
|
im_b = im_b.convert("RGB")
|
|
@@ -292,6 +302,18 @@ def _pixel_delta_pct(a: Path, b: Path) -> Tuple[float, int, int, int, int]:
|
|
|
292
302
|
canvas_b = Image.new("RGB", (w, h), (0, 0, 0))
|
|
293
303
|
canvas_a.paste(im_a, (0, 0))
|
|
294
304
|
canvas_b.paste(im_b, (0, 0))
|
|
305
|
+
if region:
|
|
306
|
+
x0, y0, x1, y1 = region
|
|
307
|
+
box = (
|
|
308
|
+
max(0, min(w, int(round(w * x0)))),
|
|
309
|
+
max(0, min(h, int(round(h * y0)))),
|
|
310
|
+
max(0, min(w, int(round(w * x1)))),
|
|
311
|
+
max(0, min(h, int(round(h * y1)))),
|
|
312
|
+
)
|
|
313
|
+
if box[2] > box[0] and box[3] > box[1]:
|
|
314
|
+
canvas_a = canvas_a.crop(box)
|
|
315
|
+
canvas_b = canvas_b.crop(box)
|
|
316
|
+
w, h = canvas_a.size
|
|
295
317
|
diff = ImageChops.difference(canvas_a, canvas_b)
|
|
296
318
|
# Any channel delta > 8 counts as changed (ignore sub-pixel AA noise).
|
|
297
319
|
# Count on RGB getdata: a pixel is changed if any channel exceeds the
|
|
@@ -304,6 +326,33 @@ def _pixel_delta_pct(a: Path, b: Path) -> Tuple[float, int, int, int, int]:
|
|
|
304
326
|
return pct, changed, total, h_a, h_b
|
|
305
327
|
|
|
306
328
|
|
|
329
|
+
CLAIMS_PATH = "scripts/release_screen_claims.json"
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def _load_screen_claims(root: Path, version: str) -> Optional[Dict[str, Dict[str, object]]]:
|
|
333
|
+
"""What this release says it changed, or None for "all twelve".
|
|
334
|
+
|
|
335
|
+
A version with no entry is gated on every screen, so forgetting to write a
|
|
336
|
+
claim makes this check stricter rather than weaker.
|
|
337
|
+
"""
|
|
338
|
+
path = root / CLAIMS_PATH
|
|
339
|
+
if not path.is_file():
|
|
340
|
+
return None
|
|
341
|
+
try:
|
|
342
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
343
|
+
except ValueError as exc:
|
|
344
|
+
print(f"pixel delta: {CLAIMS_PATH} is not valid JSON ({exc})", file=sys.stderr)
|
|
345
|
+
return None
|
|
346
|
+
entry = data.get(_normalize_version(version))
|
|
347
|
+
if not isinstance(entry, dict):
|
|
348
|
+
return None
|
|
349
|
+
return {
|
|
350
|
+
name: (options if isinstance(options, dict) else {})
|
|
351
|
+
for name, options in entry.items()
|
|
352
|
+
if name in SHOTS
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
|
|
307
356
|
def main(argv: Optional[List[str]] = None) -> int:
|
|
308
357
|
root = _repo_root()
|
|
309
358
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
@@ -379,10 +428,21 @@ def main(argv: Optional[List[str]] = None) -> int:
|
|
|
379
428
|
)
|
|
380
429
|
print("-" * 64)
|
|
381
430
|
|
|
431
|
+
claims = _load_screen_claims(root, _package_version(root))
|
|
432
|
+
if claims is None:
|
|
433
|
+
print(f"pixel delta: no claim for this version in {CLAIMS_PATH} — gating all {len(SHOTS)} screens")
|
|
434
|
+
else:
|
|
435
|
+
print(
|
|
436
|
+
f"pixel delta: gating {len(claims)} claimed screen(s); "
|
|
437
|
+
f"{len(SHOTS) - len(claims)} unclaimed screen(s) measured but not gated"
|
|
438
|
+
)
|
|
439
|
+
|
|
382
440
|
failures: List[str] = []
|
|
383
441
|
results: List[Dict[str, object]] = []
|
|
384
442
|
|
|
385
443
|
for name in SHOTS:
|
|
444
|
+
claimed = claims is None or name in SHOTS and name in claims
|
|
445
|
+
options = (claims or {}).get(name, {}) if claims is not None else {}
|
|
386
446
|
cur = current / name
|
|
387
447
|
base = baseline / name
|
|
388
448
|
if not cur.is_file():
|
|
@@ -394,19 +454,27 @@ def main(argv: Optional[List[str]] = None) -> int:
|
|
|
394
454
|
print(f"{name:<32} {'—':>8} {'—':>6} {'—':>6} FAIL")
|
|
395
455
|
continue
|
|
396
456
|
|
|
397
|
-
|
|
457
|
+
region = options.get("region") if isinstance(options.get("region"), list) else None
|
|
458
|
+
pct, changed, total, h_cur, h_base = _pixel_delta_pct(cur, base, region)
|
|
398
459
|
height_delta = abs(h_cur - h_base)
|
|
399
460
|
live = name in LIVING_BRAIN
|
|
400
|
-
|
|
461
|
+
# A claim that names its region has already excluded the animated part
|
|
462
|
+
# of the screen, so the live threshold no longer buys anything there.
|
|
463
|
+
default_min = args.min_pct_live if (live and not region) else args.min_pct
|
|
464
|
+
override = options.get("min_pct")
|
|
465
|
+
min_pct = float(override) if isinstance(override, (int, float)) else default_min
|
|
401
466
|
# Height growth alone proves layout rebuild for full-page shots.
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
if not
|
|
467
|
+
moved = pct >= min_pct or height_delta >= args.min_height_px
|
|
468
|
+
ok = moved or not claimed
|
|
469
|
+
tag = "ok" if moved else ("—" if not claimed else "FAIL")
|
|
470
|
+
if claimed and not moved:
|
|
405
471
|
failures.append(
|
|
406
472
|
f"{name}: pixel={pct:.2f}% (need>={min_pct:.1f}%) "
|
|
407
473
|
f"height_delta={height_delta}px (need>={args.min_height_px})"
|
|
474
|
+
+ (f" region={region}" if region else "")
|
|
408
475
|
)
|
|
409
|
-
|
|
476
|
+
marker = "" if claimed else " (unclaimed)"
|
|
477
|
+
print(f"{name:<32} {pct:7.2f}% {height_delta:5d}p {min_pct:5.1f}% {tag:>5}{marker}")
|
|
410
478
|
results.append(
|
|
411
479
|
{
|
|
412
480
|
"file": name,
|
|
@@ -416,18 +484,32 @@ def main(argv: Optional[List[str]] = None) -> int:
|
|
|
416
484
|
"height_delta": height_delta,
|
|
417
485
|
"min_pct": min_pct,
|
|
418
486
|
"living_brain": live,
|
|
487
|
+
"region": region,
|
|
488
|
+
"claimed": claimed,
|
|
489
|
+
"moved": moved,
|
|
419
490
|
"pass": ok,
|
|
420
491
|
}
|
|
421
492
|
)
|
|
422
493
|
|
|
423
494
|
print("-" * 64)
|
|
495
|
+
gated = sum(1 for item in results if item["claimed"])
|
|
424
496
|
if failures:
|
|
425
|
-
print(f"pixel delta: FAIL — {len(failures)}/{
|
|
497
|
+
print(f"pixel delta: FAIL — {len(failures)}/{gated} claimed screen(s) below threshold")
|
|
426
498
|
for item in failures:
|
|
427
499
|
print(f" - {item}", file=sys.stderr)
|
|
428
500
|
return 1
|
|
429
501
|
|
|
430
|
-
|
|
502
|
+
# An unclaimed screen that moved is not a failure, but it is worth saying:
|
|
503
|
+
# it is either an unintended side effect or a claim someone forgot to write.
|
|
504
|
+
surprises = [
|
|
505
|
+
f"{item['file']} ({item['pixel_pct']:.2f}%)"
|
|
506
|
+
for item in results
|
|
507
|
+
if not item["claimed"] and item["moved"]
|
|
508
|
+
]
|
|
509
|
+
if surprises:
|
|
510
|
+
print(f"pixel delta: note — unclaimed screens that changed anyway: {', '.join(surprises)}")
|
|
511
|
+
|
|
512
|
+
print(f"pixel delta: PASS — all {gated} claimed screen(s) moved vs baseline v{expected_version}")
|
|
431
513
|
return 0
|
|
432
514
|
|
|
433
515
|
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Server-side i18n ratchet.
|
|
4
|
+
*
|
|
5
|
+
* Every string the API hands a person used to be a literal at the raise site,
|
|
6
|
+
* written in whichever language the author happened to be thinking in. One user
|
|
7
|
+
* could get `"사용자를 찾을 수 없습니다."` from one router and, two files over,
|
|
8
|
+
* `"Knowledge Graph ingestion is disabled."` — half the product in a language
|
|
9
|
+
* they never chose.
|
|
10
|
+
*
|
|
11
|
+
* `latticeai/core/messages.py` fixed the mechanism; this gate keeps the fix from
|
|
12
|
+
* eroding. Routers listed in LOCALIZED below are done: they must raise through
|
|
13
|
+
* `http_error(...)` / `translate(...)`, never a literal. Routers not yet listed
|
|
14
|
+
* are honestly *not* claimed to be localized — adding one to the list is how a
|
|
15
|
+
* migration is declared finished, and from then on it cannot slip back.
|
|
16
|
+
*
|
|
17
|
+
* Pass-throughs (`detail=str(exc)`, `detail=exc.to_detail()`, a variable) are
|
|
18
|
+
* allowed: the wording belongs to the service that raised, and moving it here
|
|
19
|
+
* would only relocate the problem.
|
|
20
|
+
*/
|
|
21
|
+
import { readFileSync, readdirSync } from "node:fs";
|
|
22
|
+
import { join } from "node:path";
|
|
23
|
+
|
|
24
|
+
const API_DIR = "latticeai/api";
|
|
25
|
+
|
|
26
|
+
// Routers whose user-facing details are in the message catalog. Append here
|
|
27
|
+
// when a router is migrated — never remove. Module names carry no extension:
|
|
28
|
+
// `check_legacy_debt.mjs` rejects a bare "<module>.py" literal in scripts/,
|
|
29
|
+
// which is how the removed root shims used to be referenced.
|
|
30
|
+
const LOCALIZED = [
|
|
31
|
+
"admin",
|
|
32
|
+
"auth",
|
|
33
|
+
"browser",
|
|
34
|
+
"chat",
|
|
35
|
+
"chat_history",
|
|
36
|
+
"chat_intents",
|
|
37
|
+
"knowledge_graph",
|
|
38
|
+
"local_files",
|
|
39
|
+
"mcp",
|
|
40
|
+
"memory",
|
|
41
|
+
"models",
|
|
42
|
+
"network_boundary",
|
|
43
|
+
"portability",
|
|
44
|
+
"project_sessions",
|
|
45
|
+
"review_queue",
|
|
46
|
+
"setup",
|
|
47
|
+
"tools",
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
const failures = [];
|
|
51
|
+
const known = new Set(readdirSync(API_DIR).filter((name) => name.endsWith(".py")));
|
|
52
|
+
|
|
53
|
+
for (const name of LOCALIZED) {
|
|
54
|
+
const file = `${name}.py`;
|
|
55
|
+
if (!known.has(file)) {
|
|
56
|
+
failures.push(`${file}: listed as localized but no such router exists`);
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
const lines = readFileSync(join(API_DIR, file), "utf8").split("\n");
|
|
60
|
+
lines.forEach((line, index) => {
|
|
61
|
+
const match = line.match(/detail\s*=\s*(.+)$/);
|
|
62
|
+
if (!match) return;
|
|
63
|
+
const value = match[1].trim();
|
|
64
|
+
// Anything that is not a quoted literal is a pass-through: `str(exc)`, a
|
|
65
|
+
// variable, `translate(...)`, a dict. Those carry no wording of their own.
|
|
66
|
+
if (!/^f?"/.test(value)) return;
|
|
67
|
+
// A literal used as an argument to a string method — `"; ".join(errors)` —
|
|
68
|
+
// is a separator, not a message.
|
|
69
|
+
if (/^f?"[^"]*"\s*\./.test(value)) return;
|
|
70
|
+
// An empty string carries no wording either.
|
|
71
|
+
if (/^f?"\s*"/.test(value)) return;
|
|
72
|
+
failures.push(
|
|
73
|
+
`${file}:${index + 1}: literal error detail — use http_error("<key>", …) `
|
|
74
|
+
+ `with a key in latticeai/core/messages.py\n ${line.trim()}`,
|
|
75
|
+
);
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (failures.length) {
|
|
80
|
+
console.error("Server i18n gate failed:\n");
|
|
81
|
+
for (const failure of failures) console.error(" " + failure);
|
|
82
|
+
console.error(
|
|
83
|
+
`\n${failures.length} literal detail(s) in routers declared localized.`
|
|
84
|
+
+ "\nAdd the message to latticeai/core/messages.py (both ko and en) and raise"
|
|
85
|
+
+ "\nvia http_error(status, key, resolve_language(request)).",
|
|
86
|
+
);
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const remaining = [...known].filter(
|
|
91
|
+
(entry) => entry !== "__init__.py" && !LOCALIZED.includes(entry.replace(/\.py$/, "")),
|
|
92
|
+
).length;
|
|
93
|
+
console.log(
|
|
94
|
+
`Server i18n gate: ${LOCALIZED.length} router(s) localized and locked; `
|
|
95
|
+
+ `${remaining} not yet claimed.`,
|
|
96
|
+
);
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_comment": [
|
|
3
|
+
"Which screens each release claims to have changed, and where.",
|
|
4
|
+
"",
|
|
5
|
+
"The pixel gate used to require all twelve screens to move on every",
|
|
6
|
+
"release. That was right for 10.6.x/10.7.0, which were explicitly",
|
|
7
|
+
"'rearrange every screen' releases, and wrong as a standing rule: a",
|
|
8
|
+
"release that deliberately touches four screens would have to churn the",
|
|
9
|
+
"other eight or fail — which is the dishonesty the gate exists to catch,",
|
|
10
|
+
"inverted.",
|
|
11
|
+
"",
|
|
12
|
+
"A release lists the screens it claims to have changed. Claimed screens",
|
|
13
|
+
"are gated exactly as before. Unclaimed screens are still measured and",
|
|
14
|
+
"printed, so an unintended change is visible, but do not fail the run.",
|
|
15
|
+
"A version with no entry here is gated on all twelve — forgetting to",
|
|
16
|
+
"write a claim makes the gate stricter, never weaker.",
|
|
17
|
+
"",
|
|
18
|
+
"'region' narrows the comparison to [x0, y0, x1, y1] as fractions of the",
|
|
19
|
+
"image. Use it when a screen carries an animated element that dominates a",
|
|
20
|
+
"whole-image delta: naming where the change is makes the claim more",
|
|
21
|
+
"specific, not less."
|
|
22
|
+
],
|
|
23
|
+
|
|
24
|
+
"10.8.0": {
|
|
25
|
+
"04-brain-chat-home.png": {
|
|
26
|
+
"_why": "Elevated brain-chat-home visual layout with glassmorphic station card, ambient radial mesh aura, gradient title, glowing dot prompt cards, and refined toolbar styling.",
|
|
27
|
+
"min_pct": 1.5
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
"10.9.0": {
|
|
32
|
+
"_why": [
|
|
33
|
+
"This release claims no screen redesign, and the empty claim is the",
|
|
34
|
+
"honest statement of that: 10.9.0 is a correctness release (event-loop",
|
|
35
|
+
"blocking, a focus ring that faded in too slowly to see, an organism",
|
|
36
|
+
"stuck in 'thinking', localized server errors).",
|
|
37
|
+
"",
|
|
38
|
+
"Its two visual changes are both invisible at the capture viewport by",
|
|
39
|
+
"construction. The welcome step's compaction lives under",
|
|
40
|
+
"`@media (max-height: 820px)` and captures run at 1440x900; the capture",
|
|
41
|
+
"pills' focus border only differs while an input inside them has focus,",
|
|
42
|
+
"which no screenshot holds. `tests/visual/v3.spec.js` asserts both",
|
|
43
|
+
"directly instead — a pixel diff is the wrong instrument for either."
|
|
44
|
+
]
|
|
45
|
+
}
|
|
46
|
+
}
|
package/src-tauri/Cargo.lock
CHANGED
package/src-tauri/Cargo.toml
CHANGED
|
@@ -1,45 +1,45 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "10.
|
|
2
|
+
"version": "10.9.0",
|
|
3
3
|
"generated_at": "vite",
|
|
4
4
|
"entrypoints": {
|
|
5
|
-
"app": "/static/app/assets/index-
|
|
5
|
+
"app": "/static/app/assets/index-CxOcwsHV.js"
|
|
6
6
|
},
|
|
7
7
|
"assets": {
|
|
8
8
|
"../node_modules/@tauri-apps/api/core.js": "/static/app/assets/core-CwxXejkd.js",
|
|
9
|
-
"_BrainSignals-
|
|
10
|
-
"_LivingBrain-
|
|
11
|
-
"_ReviewCard-
|
|
12
|
-
"_arrow-left-
|
|
13
|
-
"_bot-
|
|
14
|
-
"_brain-
|
|
15
|
-
"_button-
|
|
16
|
-
"_circle-pause-
|
|
17
|
-
"_circle-play-
|
|
18
|
-
"_cpu-
|
|
19
|
-
"_download-
|
|
20
|
-
"_folder-open-
|
|
21
|
-
"_hard-drive-
|
|
22
|
-
"_input-
|
|
23
|
-
"_permissionCopy-
|
|
24
|
-
"_primitives-
|
|
25
|
-
"_search-
|
|
26
|
-
"_share-2-
|
|
27
|
-
"_shield-alert-
|
|
28
|
-
"_textarea-
|
|
29
|
-
"_useFocusTrap-
|
|
30
|
-
"_useQuery-
|
|
31
|
-
"_utils-
|
|
32
|
-
"_workspace-
|
|
33
|
-
"index.html": "/static/app/assets/index-
|
|
34
|
-
"assets/index-
|
|
35
|
-
"src/components/ProductFlow.tsx": "/static/app/assets/ProductFlow-
|
|
36
|
-
"src/features/admin/AdminConsole.tsx": "/static/app/assets/AdminConsole-
|
|
37
|
-
"src/features/brain/BrainHome.tsx": "/static/app/assets/BrainHome-
|
|
38
|
-
"src/features/command/CommandPalette.tsx": "/static/app/assets/CommandPalette-
|
|
39
|
-
"src/pages/Act.tsx": "/static/app/assets/Act-
|
|
40
|
-
"src/pages/Brain.tsx": "/static/app/assets/Brain-
|
|
41
|
-
"src/pages/Capture.tsx": "/static/app/assets/Capture-
|
|
42
|
-
"src/pages/Library.tsx": "/static/app/assets/Library-
|
|
43
|
-
"src/pages/System.tsx": "/static/app/assets/System-
|
|
9
|
+
"_BrainSignals-BeE8RJo3.js": "/static/app/assets/BrainSignals-BeE8RJo3.js",
|
|
10
|
+
"_LivingBrain-BSa0wpFG.js": "/static/app/assets/LivingBrain-BSa0wpFG.js",
|
|
11
|
+
"_ReviewCard-BepjSDpN.js": "/static/app/assets/ReviewCard-BepjSDpN.js",
|
|
12
|
+
"_arrow-left-ig7AZU8B.js": "/static/app/assets/arrow-left-ig7AZU8B.js",
|
|
13
|
+
"_bot-DQj0-LkM.js": "/static/app/assets/bot-DQj0-LkM.js",
|
|
14
|
+
"_brain-D8OEwmVj.js": "/static/app/assets/brain-D8OEwmVj.js",
|
|
15
|
+
"_button-CmTknyAP.js": "/static/app/assets/button-CmTknyAP.js",
|
|
16
|
+
"_circle-pause-yTCWRziJ.js": "/static/app/assets/circle-pause-yTCWRziJ.js",
|
|
17
|
+
"_circle-play-Ccrva84R.js": "/static/app/assets/circle-play-Ccrva84R.js",
|
|
18
|
+
"_cpu-CbJqWTlS.js": "/static/app/assets/cpu-CbJqWTlS.js",
|
|
19
|
+
"_download-CkSzbzU-.js": "/static/app/assets/download-CkSzbzU-.js",
|
|
20
|
+
"_folder-open-CKyjQ4PU.js": "/static/app/assets/folder-open-CKyjQ4PU.js",
|
|
21
|
+
"_hard-drive-DAzk9um0.js": "/static/app/assets/hard-drive-DAzk9um0.js",
|
|
22
|
+
"_input-DcMETmZ7.js": "/static/app/assets/input-DcMETmZ7.js",
|
|
23
|
+
"_permissionCopy-BVf13_25.js": "/static/app/assets/permissionCopy-BVf13_25.js",
|
|
24
|
+
"_primitives-CQV9Q2YM.js": "/static/app/assets/primitives-CQV9Q2YM.js",
|
|
25
|
+
"_search-CXGASMMH.js": "/static/app/assets/search-CXGASMMH.js",
|
|
26
|
+
"_share-2-COWCHNZm.js": "/static/app/assets/share-2-COWCHNZm.js",
|
|
27
|
+
"_shield-alert-BDrvilyK.js": "/static/app/assets/shield-alert-BDrvilyK.js",
|
|
28
|
+
"_textarea-rUmsc8cP.js": "/static/app/assets/textarea-rUmsc8cP.js",
|
|
29
|
+
"_useFocusTrap-Bi5UY_8v.js": "/static/app/assets/useFocusTrap-Bi5UY_8v.js",
|
|
30
|
+
"_useQuery-C-AicB-3.js": "/static/app/assets/useQuery-C-AicB-3.js",
|
|
31
|
+
"_utils-BMwWg78e.js": "/static/app/assets/utils-BMwWg78e.js",
|
|
32
|
+
"_workspace-Y93tls8P.js": "/static/app/assets/workspace-Y93tls8P.js",
|
|
33
|
+
"index.html": "/static/app/assets/index-CxOcwsHV.js",
|
|
34
|
+
"assets/index-BfD-jhA9.css": "/static/app/assets/index-BfD-jhA9.css",
|
|
35
|
+
"src/components/ProductFlow.tsx": "/static/app/assets/ProductFlow-IP4Q-5aQ.js",
|
|
36
|
+
"src/features/admin/AdminConsole.tsx": "/static/app/assets/AdminConsole-3UkIEWGA.js",
|
|
37
|
+
"src/features/brain/BrainHome.tsx": "/static/app/assets/BrainHome-CMDqgJF4.js",
|
|
38
|
+
"src/features/command/CommandPalette.tsx": "/static/app/assets/CommandPalette-86m4FCcN.js",
|
|
39
|
+
"src/pages/Act.tsx": "/static/app/assets/Act-CS9IeqUX.js",
|
|
40
|
+
"src/pages/Brain.tsx": "/static/app/assets/Brain-B22EmNqS.js",
|
|
41
|
+
"src/pages/Capture.tsx": "/static/app/assets/Capture-ZX9bQh68.js",
|
|
42
|
+
"src/pages/Library.tsx": "/static/app/assets/Library-Bhz5LUca.js",
|
|
43
|
+
"src/pages/System.tsx": "/static/app/assets/System-Bwx1h_jT.js"
|
|
44
44
|
}
|
|
45
45
|
}
|