ltcai 9.9.2 → 9.9.3
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 +39 -39
- package/docs/CHANGELOG.md +44 -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/curator.py +141 -0
- package/lattice_brain/graph/documents.py +111 -0
- package/lattice_brain/graph/fusion.py +179 -0
- package/lattice_brain/graph/projection.py +161 -0
- package/lattice_brain/graph/retrieval.py +19 -1
- package/lattice_brain/ingestion.py +66 -0
- package/lattice_brain/runtime/multi_agent.py +1 -1
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/automation_intelligence.py +159 -0
- package/latticeai/api/browser.py +15 -4
- package/latticeai/api/chat.py +27 -2
- package/latticeai/api/chat_agent_http.py +191 -3
- package/latticeai/api/chat_contracts.py +7 -1
- package/latticeai/api/chat_helpers.py +147 -0
- package/latticeai/api/chat_intents.py +189 -1
- package/latticeai/api/chat_stream.py +17 -1
- package/latticeai/api/funnel_metrics.py +31 -0
- package/latticeai/api/knowledge_graph.py +30 -0
- package/latticeai/api/local_files.py +90 -0
- package/latticeai/api/setup.py +134 -1
- package/latticeai/api/tools.py +33 -3
- package/latticeai/app_factory.py +14 -0
- package/latticeai/core/agent.py +91 -9
- package/latticeai/core/agent_eval.py +107 -0
- package/latticeai/core/file_generation.py +279 -2
- package/latticeai/core/legacy_compatibility.py +1 -1
- package/latticeai/core/marketplace.py +1 -1
- package/latticeai/core/workspace_os.py +1 -1
- package/latticeai/runtime/persistence_runtime.py +19 -1
- package/latticeai/runtime/router_registration.py +8 -1
- package/latticeai/services/app_context.py +3 -0
- package/latticeai/services/architecture_readiness.py +1 -1
- package/latticeai/services/automation_execution.py +266 -0
- package/latticeai/services/automation_intelligence.py +7 -0
- package/latticeai/services/command_center.py +20 -1
- package/latticeai/services/folder_watch.py +391 -0
- package/latticeai/services/funnel_metrics.py +202 -0
- package/latticeai/services/product_readiness.py +1 -1
- package/latticeai/services/search_service.py +17 -1
- package/latticeai/setup/demo_corpus.py +98 -0
- package/latticeai/tools/filesystem.py +38 -1
- package/package.json +1 -1
- package/scripts/bench_models.py +234 -5
- package/scripts/check_current_release_docs.mjs +1 -1
- 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 +28 -27
- package/static/app/assets/Act-DmdruVKV.js +1 -0
- package/static/app/assets/{Brain-BnGbytHa.js → Brain-DEY9jLVt.js} +2 -2
- package/static/app/assets/BrainHome-CeNaxjP1.js +2 -0
- package/static/app/assets/BrainSignals-CStjIqYi.js +1 -0
- package/static/app/assets/{Capture-CAKnLcb0.js → Capture-CVZ09QXi.js} +1 -1
- package/static/app/assets/CommandPalette-DepwOQFv.js +1 -0
- package/static/app/assets/Library-Bp0n-HlW.js +1 -0
- package/static/app/assets/{LivingBrain-C3VbogVo.js → LivingBrain-DxP4efJF.js} +1 -1
- package/static/app/assets/{ProductFlow-BLau7c7N.js → ProductFlow-DRbm7NEq.js} +1 -1
- package/static/app/assets/ReviewCard-C4HAO7A3.js +3 -0
- package/static/app/assets/{System-DRHc_6Su.js → System-ByQcmJW-.js} +1 -1
- package/static/app/assets/{bot-DZkDdh-A.js → bot-BNDyZLR7.js} +1 -1
- package/static/app/assets/circle-play-BkhdcHgd.js +1 -0
- package/static/app/assets/{cpu-DyvsDBbj.js → cpu-C6jjYm6i.js} +1 -1
- package/static/app/assets/{folder-open-CffoIr9t.js → folder-open-DjGIvDBQ.js} +1 -1
- package/static/app/assets/{hard-drive-C1oojrHb.js → hard-drive-BlSbwSaT.js} +1 -1
- package/static/app/assets/index-Bge3DXW7.css +2 -0
- package/static/app/assets/{index-BS_3aN1n.js → index-CHu7cgj3.js} +3 -3
- package/static/app/assets/{input-B_U7zvbR.js → input-DVDI0YR3.js} +1 -1
- package/static/app/assets/{navigation-xG-gyjbe.js → navigation-BddhEWA0.js} +1 -1
- package/static/app/assets/{network-BF13HJJF.js → network-pYQt5oBu.js} +1 -1
- package/static/app/assets/{primitives-DsuKh3M8.js → primitives-D7gCdEvS.js} +1 -1
- package/static/app/assets/shield-alert-K9RKGQeg.js +1 -0
- package/static/app/assets/{textarea-DsDL4ET8.js → textarea-sqQmoBKL.js} +1 -1
- package/static/app/assets/useFocusTrap-7EV9dFP2.js +1 -0
- package/static/app/assets/utils-uQYKXNeq.js +7 -0
- package/static/app/index.html +4 -4
- package/static/sw.js +1 -1
- package/static/app/assets/Act-Cx_gZNbE.js +0 -1
- package/static/app/assets/BrainHome-CRUMJL93.js +0 -2
- package/static/app/assets/BrainSignals-BP8-Qh2g.js +0 -1
- package/static/app/assets/CommandPalette-BoUTXBfm.js +0 -1
- package/static/app/assets/Library-PcfVUco6.js +0 -1
- package/static/app/assets/ReviewCard-CX5RqRqx.js +0 -1
- package/static/app/assets/download-C7yxYHvz.js +0 -1
- package/static/app/assets/index-DU78Kogz.css +0 -2
- package/static/app/assets/sunrise-CIP2ELJ1.js +0 -1
- package/static/app/assets/utils-Dh-Dwtkb.js +0 -7
|
@@ -25,6 +25,7 @@ The pipeline is pure (no I/O, no FastAPI); the chat layer injects an async
|
|
|
25
25
|
|
|
26
26
|
from __future__ import annotations
|
|
27
27
|
|
|
28
|
+
import ast
|
|
28
29
|
import html as html_lib
|
|
29
30
|
import json
|
|
30
31
|
import re
|
|
@@ -91,6 +92,8 @@ _EXT_FENCE_LANGS: Dict[str, Tuple[str, ...]] = {
|
|
|
91
92
|
".xml": ("xml", "svg"),
|
|
92
93
|
".csv": ("csv",),
|
|
93
94
|
".txt": ("txt", "text", "plaintext"),
|
|
95
|
+
".vue": ("vue", "html"),
|
|
96
|
+
".svelte": ("svelte", "html"),
|
|
94
97
|
}
|
|
95
98
|
|
|
96
99
|
|
|
@@ -181,6 +184,76 @@ def looks_like_refusal(content: str) -> bool:
|
|
|
181
184
|
return bool(_REFUSAL_RE.search(head)) and len(content) < 600
|
|
182
185
|
|
|
183
186
|
|
|
187
|
+
# Braced code types validated structurally (balanced delimiters, no fences).
|
|
188
|
+
_BRACED_CODE_EXTENSIONS = frozenset({".js", ".jsx", ".ts", ".tsx"})
|
|
189
|
+
# Single-file components validated by their block tags being closed.
|
|
190
|
+
_COMPONENT_EXTENSIONS = frozenset({".vue", ".svelte"})
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _strip_code_literals(text: str) -> str:
|
|
194
|
+
"""Remove string literals and comments so delimiter counting stays honest.
|
|
195
|
+
|
|
196
|
+
A cheap single-pass scanner (not a parser): quotes ('', "", ``), line
|
|
197
|
+
comments (//) and block comments (/* */) commonly contain lone braces
|
|
198
|
+
that would otherwise false-flag valid JS/TS as unbalanced.
|
|
199
|
+
"""
|
|
200
|
+
out: List[str] = []
|
|
201
|
+
i, n = 0, len(text)
|
|
202
|
+
while i < n:
|
|
203
|
+
ch = text[i]
|
|
204
|
+
nxt = text[i + 1] if i + 1 < n else ""
|
|
205
|
+
if ch == "\\":
|
|
206
|
+
i += 2 # escaped char (inside or outside a literal — always skip)
|
|
207
|
+
continue
|
|
208
|
+
if ch in ("'", '"', "`"):
|
|
209
|
+
quote = ch
|
|
210
|
+
i += 1
|
|
211
|
+
while i < n:
|
|
212
|
+
if text[i] == "\\":
|
|
213
|
+
i += 2
|
|
214
|
+
continue
|
|
215
|
+
if text[i] == quote:
|
|
216
|
+
i += 1
|
|
217
|
+
break
|
|
218
|
+
i += 1
|
|
219
|
+
continue
|
|
220
|
+
if ch == "/" and nxt == "/":
|
|
221
|
+
while i < n and text[i] != "\n":
|
|
222
|
+
i += 1
|
|
223
|
+
continue
|
|
224
|
+
if ch == "/" and nxt == "*":
|
|
225
|
+
end = text.find("*/", i + 2)
|
|
226
|
+
i = n if end < 0 else end + 2
|
|
227
|
+
continue
|
|
228
|
+
out.append(ch)
|
|
229
|
+
i += 1
|
|
230
|
+
return "".join(out)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _check_balanced_delimiters(content: str) -> Tuple[bool, str]:
|
|
234
|
+
"""Lenient count-based balance check for braced code (js/ts family).
|
|
235
|
+
|
|
236
|
+
Only *counts* are compared, never ordering, so valid-but-unusual code is
|
|
237
|
+
not rejected; a truncated file with a dangling ``{`` still fails.
|
|
238
|
+
"""
|
|
239
|
+
stripped = _strip_code_literals(content)
|
|
240
|
+
for opener, closer, label in (("{", "}", "braces"), ("(", ")", "parentheses"), ("[", "]", "brackets")):
|
|
241
|
+
if stripped.count(opener) != stripped.count(closer):
|
|
242
|
+
return False, f"unbalanced {label} ({opener}{closer}) — the file looks truncated"
|
|
243
|
+
return True, "ok"
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _check_component_blocks(content: str) -> Tuple[bool, str]:
|
|
247
|
+
"""Vue/Svelte SFC sanity: every opened block tag must be closed."""
|
|
248
|
+
lower = content.lower()
|
|
249
|
+
for tag in ("template", "script", "style"):
|
|
250
|
+
opened = len(re.findall(rf"<{tag}(?:\s[^>]*)?>", lower))
|
|
251
|
+
closed = lower.count(f"</{tag}>")
|
|
252
|
+
if opened != closed:
|
|
253
|
+
return False, f"<{tag}> block is not closed — the component looks truncated"
|
|
254
|
+
return True, "ok"
|
|
255
|
+
|
|
256
|
+
|
|
184
257
|
def validate_file_content(content: str, target_path: str) -> Tuple[bool, str]:
|
|
185
258
|
"""Structural sanity check per file type. Returns (ok, reason)."""
|
|
186
259
|
if not content.strip():
|
|
@@ -211,10 +284,28 @@ def validate_file_content(content: str, target_path: str) -> Tuple[bool, str]:
|
|
|
211
284
|
return False, f"invalid JSON: {exc}"
|
|
212
285
|
return True, "ok"
|
|
213
286
|
if ext == ".css":
|
|
287
|
+
if "```" in content:
|
|
288
|
+
return False, "output still contains Markdown fences"
|
|
214
289
|
if "{" not in content or "}" not in content:
|
|
215
290
|
return False, "no CSS rule blocks found"
|
|
216
291
|
return True, "ok"
|
|
217
|
-
if ext
|
|
292
|
+
if ext == ".py":
|
|
293
|
+
if "```" in content:
|
|
294
|
+
return False, "output still contains Markdown fences"
|
|
295
|
+
try:
|
|
296
|
+
ast.parse(content)
|
|
297
|
+
except SyntaxError as exc:
|
|
298
|
+
return False, f"invalid Python syntax: {exc.msg} (line {exc.lineno})"
|
|
299
|
+
return True, "ok"
|
|
300
|
+
if ext in _BRACED_CODE_EXTENSIONS:
|
|
301
|
+
if "```" in content:
|
|
302
|
+
return False, "output still contains Markdown fences"
|
|
303
|
+
return _check_balanced_delimiters(content)
|
|
304
|
+
if ext in _COMPONENT_EXTENSIONS:
|
|
305
|
+
if "```" in content:
|
|
306
|
+
return False, "output still contains Markdown fences"
|
|
307
|
+
return _check_component_blocks(content)
|
|
308
|
+
if ext in (".sh", ".sql"):
|
|
218
309
|
if "```" in content:
|
|
219
310
|
return False, "output still contains Markdown fences"
|
|
220
311
|
return True, "ok"
|
|
@@ -248,13 +339,31 @@ _TYPE_RULES: Dict[str, str] = {
|
|
|
248
339
|
".csv": "Produce CSV with a header row; comma-separated, one record per line.",
|
|
249
340
|
".py": "Produce complete runnable Python source code.",
|
|
250
341
|
".js": "Produce complete valid JavaScript source code.",
|
|
342
|
+
".jsx": "Produce one complete React component file in JSX.",
|
|
343
|
+
".ts": "Produce complete valid TypeScript source code.",
|
|
344
|
+
".tsx": "Produce one complete React component file in TSX (TypeScript).",
|
|
345
|
+
".vue": "Produce ONE complete Vue single-file component with closed <template>/<script>/<style> blocks.",
|
|
346
|
+
".svelte": "Produce ONE complete Svelte component; every <script>/<style> block must be closed.",
|
|
251
347
|
}
|
|
252
348
|
|
|
349
|
+
# Multi-file bundles override the standalone-HTML rule: the page must link
|
|
350
|
+
# its sibling files instead of inlining everything.
|
|
351
|
+
_BUNDLE_HTML_RULE = (
|
|
352
|
+
"Produce ONE complete HTML5 document: <!DOCTYPE html>, <html>, <head> with "
|
|
353
|
+
"<meta charset=\"utf-8\"> and a <title>, and a closed </html> tag. "
|
|
354
|
+
"This page is part of a multi-file project: link the project stylesheet(s) "
|
|
355
|
+
"with <link rel=\"stylesheet\" href=\"...\"> and load the project script(s) "
|
|
356
|
+
"with <script src=\"...\"></script> just before </body>. Reference ONLY the "
|
|
357
|
+
"project files listed below — no other external files, no inline <style> "
|
|
358
|
+
"blocks, no inline behavior scripts."
|
|
359
|
+
)
|
|
360
|
+
|
|
253
361
|
|
|
254
362
|
def build_file_generation_context(
|
|
255
363
|
target_path: str,
|
|
256
364
|
user_request: str,
|
|
257
365
|
feedback: Optional[str] = None,
|
|
366
|
+
bundle_files: Optional[List[str]] = None,
|
|
258
367
|
) -> str:
|
|
259
368
|
"""Strict, extension-aware generation instructions.
|
|
260
369
|
|
|
@@ -271,8 +380,13 @@ def build_file_generation_context(
|
|
|
271
380
|
"no text before or after the content.",
|
|
272
381
|
]
|
|
273
382
|
type_rule = _TYPE_RULES.get(ext)
|
|
383
|
+
if bundle_files and ext in (".html", ".htm"):
|
|
384
|
+
type_rule = _BUNDLE_HTML_RULE
|
|
274
385
|
if type_rule:
|
|
275
386
|
parts.append(f"- {type_rule}")
|
|
387
|
+
if bundle_files:
|
|
388
|
+
listed = ", ".join(bundle_files)
|
|
389
|
+
parts.append(f"- Project files in this bundle: {listed}")
|
|
276
390
|
first_line = _FIRST_LINE_HINTS.get(ext)
|
|
277
391
|
if first_line:
|
|
278
392
|
parts.append(f"- The very first line of your reply must be: {first_line}")
|
|
@@ -309,12 +423,29 @@ def repair_file_content(content: str, target_path: str, user_request: str) -> st
|
|
|
309
423
|
ensure_ascii=False,
|
|
310
424
|
indent=2,
|
|
311
425
|
)
|
|
426
|
+
if ext == ".py" and salvage:
|
|
427
|
+
# The repair guarantee for Python is parseability: unparseable output
|
|
428
|
+
# is preserved honestly as a commented-out draft, never as a broken
|
|
429
|
+
# module the user has to debug.
|
|
430
|
+
try:
|
|
431
|
+
ast.parse(salvage)
|
|
432
|
+
return salvage
|
|
433
|
+
except SyntaxError:
|
|
434
|
+
commented = "\n".join(f"# {line}" for line in salvage.splitlines())
|
|
435
|
+
return (
|
|
436
|
+
f"# TODO: model produced invalid Python for: {user_request}\n"
|
|
437
|
+
"# The draft below is preserved as comments — fix and uncomment.\n"
|
|
438
|
+
f"{commented}\n"
|
|
439
|
+
)
|
|
312
440
|
if salvage:
|
|
313
441
|
return salvage
|
|
314
442
|
# Nothing usable at all — leave an honest placeholder in the right format.
|
|
315
443
|
comment = {
|
|
316
444
|
".py": "# TODO: model produced no usable content for: ",
|
|
317
445
|
".js": "// TODO: model produced no usable content for: ",
|
|
446
|
+
".jsx": "// TODO: model produced no usable content for: ",
|
|
447
|
+
".ts": "// TODO: model produced no usable content for: ",
|
|
448
|
+
".tsx": "// TODO: model produced no usable content for: ",
|
|
318
449
|
".css": "/* TODO: model produced no usable content for: ",
|
|
319
450
|
".sh": "# TODO: model produced no usable content for: ",
|
|
320
451
|
".sql": "-- TODO: model produced no usable content for: ",
|
|
@@ -371,6 +502,7 @@ def _repair_html(salvage: str, user_request: str) -> str:
|
|
|
371
502
|
PREVIEWABLE_EXTENSIONS = frozenset({
|
|
372
503
|
".html", ".htm", ".md", ".markdown", ".txt", ".json", ".css", ".js",
|
|
373
504
|
".csv", ".py", ".yaml", ".yml", ".xml", ".sql", ".sh",
|
|
505
|
+
".jsx", ".ts", ".tsx", ".vue", ".svelte",
|
|
374
506
|
})
|
|
375
507
|
|
|
376
508
|
|
|
@@ -457,6 +589,145 @@ def infer_file_target(message: str) -> Optional[str]:
|
|
|
457
589
|
return None
|
|
458
590
|
|
|
459
591
|
|
|
592
|
+
# ── project manifest (multi-file bundles) ───────────────────────────────
|
|
593
|
+
|
|
594
|
+
# ``\b`` fails against Korean particles ("js로") because Hangul is ``\w`` —
|
|
595
|
+
# use ASCII lookarounds so type keywords match with or without a particle.
|
|
596
|
+
_HTML_HINT_RE = re.compile(
|
|
597
|
+
r"(?<![a-z0-9])html(?![a-z0-9])"
|
|
598
|
+
r"|웹\s*페이지|웹페이지|홈페이지|웹\s*사이트|웹사이트|website|web\s*page|landing\s*page",
|
|
599
|
+
)
|
|
600
|
+
_CSS_HINT_RE = re.compile(r"(?<![a-z0-9])css(?![a-z0-9])|스타일\s*시트|stylesheet")
|
|
601
|
+
_JS_HINT_RE = re.compile(
|
|
602
|
+
r"(?<![a-z0-9])js(?![a-z0-9])|javascript|자바스크립트|자바\s*스크립트"
|
|
603
|
+
)
|
|
604
|
+
# An explicit filename means the user is managing paths — keep the
|
|
605
|
+
# deterministic single-file flow untouched.
|
|
606
|
+
_EXPLICIT_FILENAME_RE = re.compile(
|
|
607
|
+
r"[\w-]+\.(?:html?|css|js|jsx|ts|tsx|py|json|md|txt|csv|vue|svelte)\b",
|
|
608
|
+
re.IGNORECASE,
|
|
609
|
+
)
|
|
610
|
+
_PROJECT_NAME_RE = re.compile(r"([A-Za-z][A-Za-z0-9_-]{1,30})\s*(?:앱|app\b)", re.IGNORECASE)
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
def infer_project_manifest(message: str) -> Optional[Dict[str, Any]]:
|
|
614
|
+
"""Infer a multi-file web-project manifest from a creation request.
|
|
615
|
+
|
|
616
|
+
"todo 앱 html+css+js로 만들어줘" should yield real linked files, not one
|
|
617
|
+
inlined page. Deliberately narrow and deterministic (weak local models
|
|
618
|
+
never see this decision): requires a creation verb, an HTML-page intent,
|
|
619
|
+
at least one *additional* named technology (css/js), and no explicit
|
|
620
|
+
filename. Single-type requests return ``None`` so the existing
|
|
621
|
+
single-file flow is completely unchanged.
|
|
622
|
+
"""
|
|
623
|
+
text = (message or "").strip()
|
|
624
|
+
if not text or not _CREATE_VERB_RE.search(text):
|
|
625
|
+
return None
|
|
626
|
+
if _EXPLICIT_FILENAME_RE.search(text):
|
|
627
|
+
return None
|
|
628
|
+
lower = text.lower()
|
|
629
|
+
wants_html = bool(_HTML_HINT_RE.search(lower))
|
|
630
|
+
wants_css = bool(_CSS_HINT_RE.search(lower))
|
|
631
|
+
wants_js = bool(_JS_HINT_RE.search(lower))
|
|
632
|
+
if not wants_html or not (wants_css or wants_js):
|
|
633
|
+
return None
|
|
634
|
+
|
|
635
|
+
name_match = _PROJECT_NAME_RE.search(text)
|
|
636
|
+
name = f"{name_match.group(1).lower()}-app" if name_match else "web-project"
|
|
637
|
+
|
|
638
|
+
files: List[Dict[str, str]] = []
|
|
639
|
+
html_refs: List[str] = []
|
|
640
|
+
if wants_css:
|
|
641
|
+
html_refs.append('<link rel="stylesheet" href="style.css"> in <head>')
|
|
642
|
+
if wants_js:
|
|
643
|
+
html_refs.append('<script src="app.js"></script> just before </body>')
|
|
644
|
+
files.append({
|
|
645
|
+
"path": "index.html",
|
|
646
|
+
"brief": (
|
|
647
|
+
"The main HTML page of the project. Reference the sibling files: "
|
|
648
|
+
+ " and ".join(html_refs)
|
|
649
|
+
+ ". Do not inline styles or behavior scripts."
|
|
650
|
+
),
|
|
651
|
+
})
|
|
652
|
+
if wants_css:
|
|
653
|
+
files.append({
|
|
654
|
+
"path": "style.css",
|
|
655
|
+
"brief": "All visual styles for index.html (layout, colors, typography).",
|
|
656
|
+
})
|
|
657
|
+
if wants_js:
|
|
658
|
+
files.append({
|
|
659
|
+
"path": "app.js",
|
|
660
|
+
"brief": (
|
|
661
|
+
"All page behavior for index.html as plain browser JavaScript "
|
|
662
|
+
"(no build step, no imports of missing files)."
|
|
663
|
+
),
|
|
664
|
+
})
|
|
665
|
+
return {"name": name, "kind": "web", "files": files}
|
|
666
|
+
|
|
667
|
+
|
|
668
|
+
_HTML_LOCAL_REF_RE = re.compile(
|
|
669
|
+
r"(?:href|src)\s*=\s*[\"']([^\"'#?]+)[\"']", re.IGNORECASE
|
|
670
|
+
)
|
|
671
|
+
_EXTERNAL_REF_PREFIXES = ("http://", "https://", "//", "data:", "mailto:", "tel:", "javascript:")
|
|
672
|
+
|
|
673
|
+
|
|
674
|
+
def _local_bundle_refs(html: str) -> List[str]:
|
|
675
|
+
"""File references inside an HTML document that must exist in the bundle."""
|
|
676
|
+
refs: List[str] = []
|
|
677
|
+
for ref in _HTML_LOCAL_REF_RE.findall(html or ""):
|
|
678
|
+
candidate = ref.strip()
|
|
679
|
+
if not candidate or candidate.startswith(_EXTERNAL_REF_PREFIXES):
|
|
680
|
+
continue
|
|
681
|
+
if "." not in candidate.rsplit("/", 1)[-1]:
|
|
682
|
+
continue # anchors / routes, not files
|
|
683
|
+
refs.append(candidate)
|
|
684
|
+
return refs
|
|
685
|
+
|
|
686
|
+
|
|
687
|
+
def repair_bundle_references(files: Dict[str, str]) -> Tuple[Dict[str, str], List[str]]:
|
|
688
|
+
"""Deterministically point dangling HTML refs at real bundle files.
|
|
689
|
+
|
|
690
|
+
A weak model asked for ``style.css`` sometimes links ``styles.css``. When
|
|
691
|
+
a referenced file is missing but the bundle contains exactly one file of
|
|
692
|
+
the same extension, the reference is rewritten. Returns ``(files, fixes)``.
|
|
693
|
+
"""
|
|
694
|
+
names = {p.rsplit("/", 1)[-1] for p in files}
|
|
695
|
+
fixes: List[str] = []
|
|
696
|
+
repaired = dict(files)
|
|
697
|
+
for path, content in files.items():
|
|
698
|
+
if _ext(path) not in (".html", ".htm"):
|
|
699
|
+
continue
|
|
700
|
+
updated = content
|
|
701
|
+
for ref in _local_bundle_refs(content):
|
|
702
|
+
base = ref.rsplit("/", 1)[-1]
|
|
703
|
+
if base in names:
|
|
704
|
+
continue
|
|
705
|
+
same_ext = [n for n in names if _ext(n) == _ext(base)]
|
|
706
|
+
if len(same_ext) == 1:
|
|
707
|
+
updated = updated.replace(ref, same_ext[0])
|
|
708
|
+
fixes.append(f"{path}: '{ref}' -> '{same_ext[0]}'")
|
|
709
|
+
if updated != content:
|
|
710
|
+
repaired[path] = updated
|
|
711
|
+
return repaired, fixes
|
|
712
|
+
|
|
713
|
+
|
|
714
|
+
def validate_project_bundle(files: Dict[str, str]) -> Dict[str, Any]:
|
|
715
|
+
"""Bundle-level verification: every file valid, every HTML ref resolvable."""
|
|
716
|
+
issues: List[str] = []
|
|
717
|
+
per_file: Dict[str, Dict[str, Any]] = {}
|
|
718
|
+
names = {p.rsplit("/", 1)[-1] for p in files}
|
|
719
|
+
for path, content in files.items():
|
|
720
|
+
ok, reason = validate_file_content(content, path)
|
|
721
|
+
per_file[path] = {"valid": ok, "reason": reason}
|
|
722
|
+
if not ok:
|
|
723
|
+
issues.append(f"{path}: {reason}")
|
|
724
|
+
if _ext(path) in (".html", ".htm"):
|
|
725
|
+
for ref in _local_bundle_refs(content):
|
|
726
|
+
if ref.rsplit("/", 1)[-1] not in names:
|
|
727
|
+
issues.append(f"{path}: references missing file '{ref}'")
|
|
728
|
+
return {"ok": not issues, "issues": issues, "files": per_file}
|
|
729
|
+
|
|
730
|
+
|
|
460
731
|
# ── orchestration ───────────────────────────────────────────────────────
|
|
461
732
|
|
|
462
733
|
async def generate_file_content(
|
|
@@ -465,6 +736,7 @@ async def generate_file_content(
|
|
|
465
736
|
target_path: str,
|
|
466
737
|
user_request: str,
|
|
467
738
|
max_attempts: int = 2,
|
|
739
|
+
bundle_files: Optional[List[str]] = None,
|
|
468
740
|
) -> Tuple[str, Dict[str, Any]]:
|
|
469
741
|
"""Generate validated file content with any LLM.
|
|
470
742
|
|
|
@@ -477,7 +749,9 @@ async def generate_file_content(
|
|
|
477
749
|
feedback: Optional[str] = None
|
|
478
750
|
last_candidate = ""
|
|
479
751
|
for attempt in range(1, max_attempts + 1):
|
|
480
|
-
context = build_file_generation_context(
|
|
752
|
+
context = build_file_generation_context(
|
|
753
|
+
target_path, user_request, feedback=feedback, bundle_files=bundle_files,
|
|
754
|
+
)
|
|
481
755
|
try:
|
|
482
756
|
raw = await generate(context)
|
|
483
757
|
except Exception as exc: # model backend hiccup — repair still delivers
|
|
@@ -502,8 +776,11 @@ __all__ = [
|
|
|
502
776
|
"extract_file_content",
|
|
503
777
|
"generate_file_content",
|
|
504
778
|
"infer_file_target",
|
|
779
|
+
"infer_project_manifest",
|
|
505
780
|
"looks_like_refusal",
|
|
781
|
+
"repair_bundle_references",
|
|
506
782
|
"repair_file_content",
|
|
507
783
|
"sanitize_write_content",
|
|
508
784
|
"validate_file_content",
|
|
785
|
+
"validate_project_bundle",
|
|
509
786
|
]
|
|
@@ -49,7 +49,7 @@ __all__ = [
|
|
|
49
49
|
"remove_skill_directory",
|
|
50
50
|
]
|
|
51
51
|
|
|
52
|
-
WORKSPACE_OS_VERSION = "9.9.
|
|
52
|
+
WORKSPACE_OS_VERSION = "9.9.3"
|
|
53
53
|
|
|
54
54
|
# Workspace types separate single-user Personal workspaces from shared
|
|
55
55
|
# Organization workspaces. Both keep the same local-first JSON store; the type
|
|
@@ -38,6 +38,7 @@ def build_persistence_runtime(
|
|
|
38
38
|
from latticeai.core.workspace_os import WorkspaceOSStore
|
|
39
39
|
from latticeai.services.automation_intelligence import AutomationIntelligenceService
|
|
40
40
|
from latticeai.services.brain_intelligence import BrainIntelligenceService
|
|
41
|
+
from latticeai.services.funnel_metrics import FunnelMetricsService
|
|
41
42
|
from latticeai.services.memory_service import MemoryService
|
|
42
43
|
from latticeai.services.workspace_service import WorkspaceService
|
|
43
44
|
|
|
@@ -70,11 +71,27 @@ def build_persistence_runtime(
|
|
|
70
71
|
store=workspace_os,
|
|
71
72
|
enable_graph=enable_graph,
|
|
72
73
|
)
|
|
74
|
+
# UX funnel metrics (backlog #16): cheap JSON counters under the data dir.
|
|
75
|
+
# The ingestion pipeline's audit seam is wrapped so every successful
|
|
76
|
+
# ingest bumps the funnel (and starts the TTFV clock) without touching
|
|
77
|
+
# lattice_brain internals.
|
|
78
|
+
funnel_metrics = FunnelMetricsService(Path(data_dir) / "funnel_metrics.json")
|
|
79
|
+
|
|
80
|
+
def _funnel_audit(action, detail, user):
|
|
81
|
+
try:
|
|
82
|
+
if action == "kg_ingest":
|
|
83
|
+
funnel_metrics.record_ingest(
|
|
84
|
+
duplicate=bool((detail or {}).get("duplicate"))
|
|
85
|
+
)
|
|
86
|
+
except Exception: # noqa: BLE001 — metrics must never break ingestion
|
|
87
|
+
pass
|
|
88
|
+
audit(action, detail, user)
|
|
89
|
+
|
|
73
90
|
ingestion_pipeline = IngestionPipeline(
|
|
74
91
|
knowledge_graph,
|
|
75
92
|
hooks=hooks_registry,
|
|
76
93
|
enable_graph=enable_graph,
|
|
77
|
-
audit=
|
|
94
|
+
audit=_funnel_audit,
|
|
78
95
|
)
|
|
79
96
|
device_identity = DeviceIdentity(data_dir)
|
|
80
97
|
kg_portability = KGPortabilityService(
|
|
@@ -99,4 +116,5 @@ def build_persistence_runtime(
|
|
|
99
116
|
"INGESTION_PIPELINE": ingestion_pipeline,
|
|
100
117
|
"DEVICE_IDENTITY": device_identity,
|
|
101
118
|
"KG_PORTABILITY": kg_portability,
|
|
119
|
+
"FUNNEL_METRICS": funnel_metrics,
|
|
102
120
|
}
|
|
@@ -605,6 +605,7 @@ def register_review_and_brain_tail_routers(
|
|
|
605
605
|
create_setup_router: Any,
|
|
606
606
|
model_router: Any,
|
|
607
607
|
change_proposals: Any = None,
|
|
608
|
+
knowledge_graph: Any = None,
|
|
608
609
|
) -> Any:
|
|
609
610
|
"""Register the final review/browser/brain tail routes in legacy order."""
|
|
610
611
|
|
|
@@ -644,6 +645,12 @@ def register_review_and_brain_tail_routers(
|
|
|
644
645
|
require_admin=require_admin,
|
|
645
646
|
),
|
|
646
647
|
create_garden_router(gardener=gardener, require_user=require_user),
|
|
647
|
-
create_setup_router(
|
|
648
|
+
create_setup_router(
|
|
649
|
+
model_router=model_router,
|
|
650
|
+
require_user=require_user,
|
|
651
|
+
ingestion_pipeline=ingestion_pipeline,
|
|
652
|
+
knowledge_graph=knowledge_graph,
|
|
653
|
+
workspace_service=workspace_service,
|
|
654
|
+
),
|
|
648
655
|
)
|
|
649
656
|
return brain_network
|
|
@@ -42,6 +42,9 @@ class AppContext:
|
|
|
42
42
|
hooks: Any = None
|
|
43
43
|
realtime_bus: Any = None
|
|
44
44
|
capability_registry: Any = None
|
|
45
|
+
# UX funnel metrics (latticeai.services.funnel_metrics); None in tests
|
|
46
|
+
# that don't observe the funnel — every increment site is nil-safe.
|
|
47
|
+
funnel_metrics: Any = None
|
|
45
48
|
|
|
46
49
|
# ── auth / session callables ──────────────────────────────────────────
|
|
47
50
|
require_user: Optional[Callable[..., str]] = None
|