ltcai 9.8.0 → 9.9.1
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 +45 -28
- package/bin/ltcai.js +1 -1
- package/desktop/electron/main.cjs +1 -1
- package/docs/BENCHMARKS.md +107 -0
- package/docs/CHANGELOG.md +82 -0
- package/docs/CI_AND_RELEASE_GATES.md +106 -0
- package/docs/COMMUNITY_AND_PLUGINS.md +1 -1
- package/docs/DEVELOPMENT.md +11 -8
- package/docs/LEGACY_COMPATIBILITY.md +48 -49
- package/docs/ONBOARDING.md +1 -1
- package/docs/OPERATIONS.md +15 -6
- package/docs/PERFORMANCE.md +9 -1
- package/docs/SECURITY_AUDIT.md +91 -0
- package/docs/TRUST_MODEL.md +1 -1
- package/docs/USABILITY_AUDIT.md +164 -0
- package/docs/WHY_LATTICE.md +1 -1
- package/docs/architecture.md +4 -2
- package/docs/kg-schema.md +6 -6
- package/docs/spec-vs-impl.md +8 -1
- package/lattice_brain/__init__.py +1 -1
- package/lattice_brain/graph/schema.py +1 -1
- package/lattice_brain/runtime/multi_agent.py +1 -1
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/review_queue.py +7 -0
- package/latticeai/core/agent.py +105 -8
- package/latticeai/core/agent_eval.py +99 -1
- package/latticeai/core/legacy_compatibility.py +71 -47
- package/latticeai/core/marketplace.py +1 -1
- package/latticeai/core/tool_governor.py +92 -1
- package/latticeai/core/workspace_os.py +1 -1
- package/latticeai/services/architecture_readiness.py +1 -1
- package/latticeai/services/change_proposals.py +165 -23
- package/latticeai/services/product_readiness.py +2 -2
- package/latticeai/services/tool_dispatch.py +33 -0
- package/latticeai/tools/__init__.py +1 -1
- package/latticeai/tools/knowledge.py +1 -1
- package/package.json +9 -17
- package/scripts/bench_models.py +312 -0
- package/scripts/check_bundle_budget.mjs +103 -0
- package/scripts/check_current_release_docs.mjs +20 -1
- package/scripts/check_doc_status.mjs +149 -0
- package/scripts/check_legacy_debt.mjs +91 -0
- package/scripts/generate_sbom.py +70 -0
- package/scripts/prune_release_evidence.mjs +38 -0
- package/scripts/wheel_smoke.py +22 -6
- package/src-tauri/Cargo.lock +1 -1
- package/src-tauri/Cargo.toml +1 -1
- package/src-tauri/src/main.rs +12 -12
- package/src-tauri/tauri.conf.json +1 -13
- package/static/app/asset-manifest.json +27 -11
- package/static/app/assets/Act-CcSWTQsx.js +1 -0
- package/static/app/assets/{Brain-BMkgdWnI.js → Brain-CSCDZEHd.js} +2 -2
- package/static/app/assets/BrainHome-B5QpJ1Fx.js +2 -0
- package/static/app/assets/BrainSignals-CmFD8PWk.js +1 -0
- package/static/app/assets/Capture-BiuerOio.js +1 -0
- package/static/app/assets/CommandPalette-BrqpT7K2.js +1 -0
- package/static/app/assets/Library-BcaiPbum.js +1 -0
- package/static/app/assets/LivingBrain-6FvA0H5T.js +1 -0
- package/static/app/assets/ProductFlow-Cr8bkQPw.js +1 -0
- package/static/app/assets/ReviewCard-D56TMypk.js +1 -0
- package/static/app/assets/System-D6uJacqu.js +1 -0
- package/static/app/assets/bot-DgC7xgUl.js +1 -0
- package/static/app/assets/cpu-DDGb4XHl.js +1 -0
- package/static/app/assets/download-C-Ei3GFa.js +1 -0
- package/static/app/assets/folder-open-BbVuSO9M.js +1 -0
- package/static/app/assets/hard-drive-yDZR6XYz.js +1 -0
- package/static/app/assets/index-CFRhNBuH.css +2 -0
- package/static/app/assets/index-DrZSkwrE.js +10 -0
- package/static/app/assets/input-DPIkTs9n.js +1 -0
- package/static/app/assets/navigation-B1mkmyUe.js +1 -0
- package/static/app/assets/network-0XDI1Uf_.js +1 -0
- package/static/app/assets/primitives-sIQYKyf5.js +1 -0
- package/static/app/assets/sunrise-DXMjueec.js +1 -0
- package/static/app/assets/textarea-DvnwpnqS.js +1 -0
- package/static/app/assets/utils-BrfFHAJl.js +7 -0
- package/static/app/index.html +4 -2
- package/static/sw.js +1 -1
- package/auto_setup.py +0 -21
- package/kg_schema.py +0 -13
- package/knowledge_graph.py +0 -51
- package/knowledge_graph_api.py +0 -24
- package/llm_router.py +0 -20
- package/local_knowledge_api.py +0 -16
- package/ltcai_cli.py +0 -21
- package/mcp_registry.py +0 -20
- package/p_reinforce.py +0 -16
- package/setup_wizard.py +0 -16
- package/static/app/assets/Act-Dd3z8AzF.js +0 -2
- package/static/app/assets/Capture-D2Aw9gkv.js +0 -1
- package/static/app/assets/Library-Yreq-KW5.js +0 -1
- package/static/app/assets/System-CXNmmtEo.js +0 -1
- package/static/app/assets/index-7gY9t9Sd.css +0 -2
- package/static/app/assets/index-CndfILiF.js +0 -18
- package/static/app/assets/primitives-DxsIXb6G.js +0 -1
- package/static/app/assets/textarea-DH7ne8VI.js +0 -1
- package/telegram_bot.py +0 -16
- package/tools/__init__.py +0 -29
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Model robustness benchmark harness for the Lattice agent loop.
|
|
3
|
+
|
|
4
|
+
What it measures
|
|
5
|
+
================
|
|
6
|
+
The product claim is that the Brain stays durable "across any AI model" — a
|
|
7
|
+
weaker model may emit sloppier output, but the agent loop repairs it and still
|
|
8
|
+
completes the task. This harness turns that claim into a **matrix** of three
|
|
9
|
+
numbers per model tier:
|
|
10
|
+
|
|
11
|
+
* ``success_rate`` — fraction of model outputs the loop could turn into a valid
|
|
12
|
+
action (i.e. the task can proceed).
|
|
13
|
+
* ``repair_rate`` — of the successful parses, the fraction that ONLY succeeded
|
|
14
|
+
because the loop's tolerant parser had to repair the output (fences, prose,
|
|
15
|
+
trailing commas, ``<think>`` blocks, Python-dict literals). High repair_rate
|
|
16
|
+
= "this tier leans hard on the loop's robustness".
|
|
17
|
+
* ``latency_ms`` — see the honesty note below.
|
|
18
|
+
|
|
19
|
+
It measures **real code**: every output is fed through
|
|
20
|
+
``latticeai.core.agent.extract_action_details`` — the exact parser/repair
|
|
21
|
+
function the production loop uses (``agent.py`` calls it in plan/execute/verify).
|
|
22
|
+
An ``agent-loop`` reference row additionally runs the real
|
|
23
|
+
``latticeai.core.agent_eval.run_agent_eval`` state machine over its 20
|
|
24
|
+
scripted scenarios.
|
|
25
|
+
|
|
26
|
+
Two modes
|
|
27
|
+
=========
|
|
28
|
+
1. **scripted** (default, always runnable, no model/network): a curated corpus
|
|
29
|
+
of model-realistic outputs per quality tier (frontier / mid-local /
|
|
30
|
+
weak-local). Proves the harness works and exposes the loop's repair boundary
|
|
31
|
+
deterministically.
|
|
32
|
+
2. **live** (opt-in, ``--live-endpoint``): sends a fixed set of agent prompts to
|
|
33
|
+
an OpenAI-compatible local endpoint (e.g. LM Studio / llama.cpp / vLLM) and
|
|
34
|
+
runs the *real* completions through the same parser, measuring true
|
|
35
|
+
end-to-end generation latency. Falls back to scripted with an honest message
|
|
36
|
+
if the endpoint is unreachable.
|
|
37
|
+
|
|
38
|
+
Honesty note on latency
|
|
39
|
+
========================
|
|
40
|
+
In **scripted** mode ``latency_ms`` is the *parse+repair* cost only
|
|
41
|
+
(microseconds); it is NOT model inference time and must not be read as such.
|
|
42
|
+
Real generation latency is only meaningful in **live** mode.
|
|
43
|
+
|
|
44
|
+
Usage
|
|
45
|
+
=====
|
|
46
|
+
.venv/bin/python scripts/bench_models.py # scripted matrix
|
|
47
|
+
.venv/bin/python scripts/bench_models.py --json out.json # + machine output
|
|
48
|
+
.venv/bin/python scripts/bench_models.py \
|
|
49
|
+
--live-endpoint http://127.0.0.1:1234/v1 --model my-local-model
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
from __future__ import annotations
|
|
53
|
+
|
|
54
|
+
import argparse
|
|
55
|
+
import json
|
|
56
|
+
import statistics
|
|
57
|
+
import sys
|
|
58
|
+
import time
|
|
59
|
+
import urllib.error
|
|
60
|
+
import urllib.request
|
|
61
|
+
from pathlib import Path
|
|
62
|
+
from typing import Any, Dict, List, Tuple
|
|
63
|
+
|
|
64
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
|
65
|
+
|
|
66
|
+
from latticeai.core.agent import extract_action_details # noqa: E402
|
|
67
|
+
from latticeai.core.agent_eval import run_agent_eval # noqa: E402
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# ── Scripted corpora ─────────────────────────────────────────────────────
|
|
71
|
+
# Each entry is one model output for a canonical agent turn. The tiers encode
|
|
72
|
+
# how the SAME intent degrades as model quality drops. All "should_parse=True"
|
|
73
|
+
# entries are recoverable by the real loop; "should_parse=False" entries are
|
|
74
|
+
# genuinely broken (past the repair boundary) and SHOULD fail — that is what
|
|
75
|
+
# makes success_rate < 1.0 meaningful rather than a rigged demo.
|
|
76
|
+
|
|
77
|
+
_CLEAN = [
|
|
78
|
+
('{"action": "plan", "goal": "ingest", "steps": [{"action": "write_file"}]}', True),
|
|
79
|
+
('{"action": "write_file", "args": {"path": "note.txt", "content": "hi"}}', True),
|
|
80
|
+
('{"action": "final", "message": "done"}', True),
|
|
81
|
+
('{"action": "verdict", "verdict": "PASS", "next_state": "DONE", "reason": "ok"}', True),
|
|
82
|
+
('{"action": "knowledge_graph_search", "args": {"query": "roadmap"}}', True),
|
|
83
|
+
('{"action": "read_file", "args": {"path": "README.md"}}', True),
|
|
84
|
+
]
|
|
85
|
+
|
|
86
|
+
_MID = [
|
|
87
|
+
# markdown-fenced JSON (very common)
|
|
88
|
+
('```json\n{"action": "plan", "goal": "ingest", "steps": []}\n```', True),
|
|
89
|
+
('```\n{"action": "write_file", "args": {"path": "a.txt", "content": "x"}}\n```', True),
|
|
90
|
+
# prose preamble then the object
|
|
91
|
+
('Sure! Here is the next step:\n{"action": "final", "message": "done"}', True),
|
|
92
|
+
# trailing comma before closing brace
|
|
93
|
+
('{"action": "read_file", "args": {"path": "README.md",}}', True),
|
|
94
|
+
# trailing comma in array
|
|
95
|
+
('{"action": "plan", "goal": "g", "steps": [{"action": "read_file"},]}', True),
|
|
96
|
+
('```json\n{"action": "verdict", "verdict": "PASS", "next_state": "DONE", "reason": "ok"}\n```', True),
|
|
97
|
+
]
|
|
98
|
+
|
|
99
|
+
_WEAK = [
|
|
100
|
+
# <think> reasoning block that itself contains braces, then the action
|
|
101
|
+
('<think>I should write the file {maybe}</think>\n{"action": "write_file", "args": {"path": "n.txt", "content": "c"}}', True),
|
|
102
|
+
('<reasoning>ok</reasoning> {"action": "final", "message": "done"}', True),
|
|
103
|
+
# Python dict literal (single quotes, True) — ast.literal_eval path
|
|
104
|
+
("{'action': 'read_file', 'args': {'path': 'x.txt'}}", True),
|
|
105
|
+
("{'action': 'verdict', 'verdict': 'PASS', 'next_state': 'DONE', 'reason': 'ok'}", True),
|
|
106
|
+
# fenced + trailing comma + prose all at once
|
|
107
|
+
('Here you go:\n```json\n{"action": "plan", "goal": "g", "steps": [],}\n```', True),
|
|
108
|
+
# genuinely broken: pure prose, no JSON object at all -> must fail
|
|
109
|
+
("I think we are done here, nothing else to do.", False),
|
|
110
|
+
# genuinely broken: object but missing the required "action" field
|
|
111
|
+
('{"message": "done", "status": "ok"}', False),
|
|
112
|
+
# genuinely broken: truncated / unbalanced braces past repair
|
|
113
|
+
('{"action": "write_file", "args": {"path": "n.txt"', False),
|
|
114
|
+
]
|
|
115
|
+
|
|
116
|
+
_PROFILES: Dict[str, List[Tuple[str, bool]]] = {
|
|
117
|
+
"frontier (clean JSON)": _CLEAN,
|
|
118
|
+
"mid-local (fenced/prose/commas)": _MID,
|
|
119
|
+
"weak-local (think/py-literal/broken)": _WEAK,
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _bench_corpus(corpus: List[Tuple[str, bool]]) -> Dict[str, Any]:
|
|
124
|
+
total = len(corpus)
|
|
125
|
+
parsed = 0
|
|
126
|
+
repaired = 0
|
|
127
|
+
latencies: List[float] = []
|
|
128
|
+
mismatches: List[str] = []
|
|
129
|
+
repair_kinds: Dict[str, int] = {}
|
|
130
|
+
for raw, should_parse in corpus:
|
|
131
|
+
t0 = time.perf_counter()
|
|
132
|
+
try:
|
|
133
|
+
_action, repairs = extract_action_details(raw)
|
|
134
|
+
ok = True
|
|
135
|
+
except (ValueError, Exception): # noqa: BLE001 - parser raises ValueError
|
|
136
|
+
ok = False
|
|
137
|
+
repairs = []
|
|
138
|
+
latencies.append((time.perf_counter() - t0) * 1000.0)
|
|
139
|
+
if ok:
|
|
140
|
+
parsed += 1
|
|
141
|
+
if repairs:
|
|
142
|
+
repaired += 1
|
|
143
|
+
for kind in repairs:
|
|
144
|
+
repair_kinds[kind] = repair_kinds.get(kind, 0) + 1
|
|
145
|
+
if ok != should_parse:
|
|
146
|
+
mismatches.append(raw[:60])
|
|
147
|
+
return {
|
|
148
|
+
"total": total,
|
|
149
|
+
"parsed": parsed,
|
|
150
|
+
"repaired": repaired,
|
|
151
|
+
"success_rate": round(parsed / total, 4) if total else 0.0,
|
|
152
|
+
"repair_rate": round(repaired / parsed, 4) if parsed else 0.0,
|
|
153
|
+
"latency_ms_mean": round(statistics.fmean(latencies), 4) if latencies else 0.0,
|
|
154
|
+
"repair_kinds": repair_kinds,
|
|
155
|
+
# A healthy harness has zero mismatches: every "should_parse" call
|
|
156
|
+
# matched reality. Non-empty means the corpus/parser disagree.
|
|
157
|
+
"harness_mismatches": mismatches,
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
# ── Live (OpenAI-compatible) path ────────────────────────────────────────
|
|
162
|
+
_LIVE_PROMPTS = [
|
|
163
|
+
"Return ONLY one JSON object for the next agent step: read the file README.md.",
|
|
164
|
+
"Return ONLY one JSON object: plan to ingest a document, then finish.",
|
|
165
|
+
"Return ONLY one JSON object: write 'hello' to notes.txt.",
|
|
166
|
+
"Return ONLY one JSON object: a PASS verdict moving the loop to DONE.",
|
|
167
|
+
"Return ONLY one JSON object: search the knowledge graph for 'roadmap'.",
|
|
168
|
+
]
|
|
169
|
+
|
|
170
|
+
_SYSTEM = (
|
|
171
|
+
"You are the executor of an agent loop. Every reply MUST be exactly one "
|
|
172
|
+
'JSON object with an "action" field and nothing else.'
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _openai_chat(base_url: str, model: str, prompt: str, *, timeout: float) -> Tuple[str, float]:
|
|
177
|
+
url = base_url.rstrip("/") + "/chat/completions"
|
|
178
|
+
body = json.dumps(
|
|
179
|
+
{
|
|
180
|
+
"model": model,
|
|
181
|
+
"messages": [
|
|
182
|
+
{"role": "system", "content": _SYSTEM},
|
|
183
|
+
{"role": "user", "content": prompt},
|
|
184
|
+
],
|
|
185
|
+
"temperature": 0.2,
|
|
186
|
+
"max_tokens": 256,
|
|
187
|
+
}
|
|
188
|
+
).encode("utf-8")
|
|
189
|
+
req = urllib.request.Request( # noqa: S310 - operator-supplied local endpoint
|
|
190
|
+
url, data=body, headers={"Content-Type": "application/json"}, method="POST"
|
|
191
|
+
)
|
|
192
|
+
t0 = time.perf_counter()
|
|
193
|
+
with urllib.request.urlopen(req, timeout=timeout) as res: # noqa: S310
|
|
194
|
+
payload = json.loads(res.read().decode("utf-8", errors="replace"))
|
|
195
|
+
latency = (time.perf_counter() - t0) * 1000.0
|
|
196
|
+
text = payload["choices"][0]["message"]["content"]
|
|
197
|
+
return text, latency
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _bench_live(base_url: str, model: str, *, timeout: float) -> Dict[str, Any]:
|
|
201
|
+
total = 0
|
|
202
|
+
parsed = 0
|
|
203
|
+
repaired = 0
|
|
204
|
+
latencies: List[float] = []
|
|
205
|
+
for prompt in _LIVE_PROMPTS:
|
|
206
|
+
total += 1
|
|
207
|
+
try:
|
|
208
|
+
text, latency = _openai_chat(base_url, model, prompt, timeout=timeout)
|
|
209
|
+
except (urllib.error.URLError, OSError, KeyError, ValueError) as exc:
|
|
210
|
+
return {"error": f"live endpoint unreachable/invalid: {exc}"}
|
|
211
|
+
latencies.append(latency)
|
|
212
|
+
try:
|
|
213
|
+
_action, repairs = extract_action_details(text)
|
|
214
|
+
parsed += 1
|
|
215
|
+
if repairs:
|
|
216
|
+
repaired += 1
|
|
217
|
+
except Exception: # noqa: BLE001
|
|
218
|
+
pass
|
|
219
|
+
return {
|
|
220
|
+
"total": total,
|
|
221
|
+
"parsed": parsed,
|
|
222
|
+
"repaired": repaired,
|
|
223
|
+
"success_rate": round(parsed / total, 4) if total else 0.0,
|
|
224
|
+
"repair_rate": round(repaired / parsed, 4) if parsed else 0.0,
|
|
225
|
+
"latency_ms_mean": round(statistics.fmean(latencies), 2) if latencies else 0.0,
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
# ── Reporting ────────────────────────────────────────────────────────────
|
|
230
|
+
def _fmt_row(name: str, r: Dict[str, Any]) -> str:
|
|
231
|
+
return (
|
|
232
|
+
f" {name:<40} "
|
|
233
|
+
f"success={r['success_rate']:<7} "
|
|
234
|
+
f"repair={r['repair_rate']:<7} "
|
|
235
|
+
f"n={r.get('total', '-'):<4} "
|
|
236
|
+
f"latency_ms={r['latency_ms_mean']}"
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def main(argv: List[str] | None = None) -> int:
|
|
241
|
+
parser = argparse.ArgumentParser(description="Model robustness benchmark harness")
|
|
242
|
+
parser.add_argument("--json", dest="json_out", help="write full report JSON to this path")
|
|
243
|
+
parser.add_argument("--live-endpoint", help="OpenAI-compatible base URL, e.g. http://127.0.0.1:1234/v1")
|
|
244
|
+
parser.add_argument("--model", help="model id for the live endpoint")
|
|
245
|
+
parser.add_argument("--timeout", type=float, default=30.0, help="live request timeout seconds")
|
|
246
|
+
args = parser.parse_args(argv)
|
|
247
|
+
|
|
248
|
+
report: Dict[str, Any] = {"mode": "scripted", "profiles": {}}
|
|
249
|
+
|
|
250
|
+
# Scripted matrix (always).
|
|
251
|
+
for name, corpus in _PROFILES.items():
|
|
252
|
+
report["profiles"][name] = _bench_corpus(corpus)
|
|
253
|
+
|
|
254
|
+
# Real agent-loop reference row.
|
|
255
|
+
loop = run_agent_eval()
|
|
256
|
+
report["agent_loop_reference"] = {
|
|
257
|
+
"scenarios": loop["scenarios"],
|
|
258
|
+
"passed": loop["passed"],
|
|
259
|
+
"success_rate": loop["success_rate"],
|
|
260
|
+
"recovery_rate": loop["recovery_rate"],
|
|
261
|
+
"parse_errors": loop["parse_errors"],
|
|
262
|
+
"parse_recovered": loop["parse_recovered"],
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
# Optional live row.
|
|
266
|
+
if args.live_endpoint:
|
|
267
|
+
if not args.model:
|
|
268
|
+
print("--live-endpoint requires --model", file=sys.stderr)
|
|
269
|
+
return 2
|
|
270
|
+
live = _bench_live(args.live_endpoint, args.model, timeout=args.timeout)
|
|
271
|
+
report["mode"] = "scripted+live"
|
|
272
|
+
report["live"] = {"endpoint": args.live_endpoint, "model": args.model, **live}
|
|
273
|
+
|
|
274
|
+
# Human-readable matrix.
|
|
275
|
+
print("Model robustness benchmark (real parser: extract_action_details)")
|
|
276
|
+
print("=" * 72)
|
|
277
|
+
print("Scripted tiers (latency_ms = parse+repair only, NOT model inference):")
|
|
278
|
+
mismatch_total = 0
|
|
279
|
+
for name, r in report["profiles"].items():
|
|
280
|
+
print(_fmt_row(name, r))
|
|
281
|
+
if r["repair_kinds"]:
|
|
282
|
+
print(f" repairs used: {r['repair_kinds']}")
|
|
283
|
+
mismatch_total += len(r["harness_mismatches"])
|
|
284
|
+
ref = report["agent_loop_reference"]
|
|
285
|
+
print("-" * 72)
|
|
286
|
+
print(
|
|
287
|
+
f" agent-loop (real state machine) "
|
|
288
|
+
f"success={ref['success_rate']:<7} "
|
|
289
|
+
f"recovery={ref['recovery_rate']:<7} "
|
|
290
|
+
f"scenarios={ref['scenarios']}"
|
|
291
|
+
)
|
|
292
|
+
if "live" in report:
|
|
293
|
+
lv = report["live"]
|
|
294
|
+
print("-" * 72)
|
|
295
|
+
if "error" in lv:
|
|
296
|
+
print(f" live [{lv['model']}] SKIPPED: {lv['error']}")
|
|
297
|
+
else:
|
|
298
|
+
print(_fmt_row(f"live [{lv['model']}] (real inference)", lv))
|
|
299
|
+
print("=" * 72)
|
|
300
|
+
print(f"harness self-check: {mismatch_total} corpus/parser mismatches (0 = healthy)")
|
|
301
|
+
|
|
302
|
+
if args.json_out:
|
|
303
|
+
Path(args.json_out).write_text(json.dumps(report, indent=2), encoding="utf-8")
|
|
304
|
+
print(f"wrote {args.json_out}")
|
|
305
|
+
|
|
306
|
+
# Non-zero exit only if the harness itself is inconsistent (a real bug),
|
|
307
|
+
# never because a weak tier scored low — low scores are the finding.
|
|
308
|
+
return 1 if mismatch_total else 0
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
if __name__ == "__main__":
|
|
312
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Guards the initial JavaScript payload of the /app SPA so heavy features stay
|
|
3
|
+
// behind lazy boundaries. It measures the entry chunk plus its transitive
|
|
4
|
+
// STATIC import closure (what a browser must download before first paint),
|
|
5
|
+
// gzips each file, and fails if the total exceeds the budget.
|
|
6
|
+
//
|
|
7
|
+
// The dynamic import() chunks (pages, onboarding, conversation home, command
|
|
8
|
+
// palette) are intentionally excluded — they are fetched on demand.
|
|
9
|
+
//
|
|
10
|
+
// The check builds into an isolated temp dir so it reads Vite's rich manifest
|
|
11
|
+
// (with `isEntry`/`imports`) without clobbering the production `static/app`
|
|
12
|
+
// manifest, which `build_frontend_assets.mjs` rewrites into a flat runtime map.
|
|
13
|
+
import { existsSync, readFileSync, rmSync } from "node:fs";
|
|
14
|
+
import { gzipSync } from "node:zlib";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
import { spawnSync } from "node:child_process";
|
|
17
|
+
|
|
18
|
+
const repo = join(import.meta.dirname, "..");
|
|
19
|
+
const outDir = join(repo, "node_modules", ".cache", "bundle-budget");
|
|
20
|
+
const manifestPath = join(outDir, "asset-manifest.json");
|
|
21
|
+
|
|
22
|
+
// Budget for the initial static JS closure, in gzipped bytes. Set below the
|
|
23
|
+
// pre-split baseline (632.9 kB raw / ~180 kB gzip single chunk) to lock in the
|
|
24
|
+
// code-splitting win and catch regressions that pull heavy code back onto the
|
|
25
|
+
// first-paint path. Raise it only with a deliberate, reviewed reason.
|
|
26
|
+
const INITIAL_JS_GZIP_BUDGET = 153_600; // 150 KiB
|
|
27
|
+
|
|
28
|
+
function build() {
|
|
29
|
+
rmSync(outDir, { recursive: true, force: true });
|
|
30
|
+
const result = spawnSync(
|
|
31
|
+
"npx",
|
|
32
|
+
["vite", "build", "--outDir", outDir, "--emptyOutDir"],
|
|
33
|
+
{ cwd: repo, encoding: "utf8", stdio: ["ignore", "ignore", "inherit"] },
|
|
34
|
+
);
|
|
35
|
+
if (result.status !== 0) {
|
|
36
|
+
console.error("bundle budget: vite build failed");
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
if (!existsSync(manifestPath)) {
|
|
40
|
+
console.error("bundle budget: manifest missing after build");
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function measure() {
|
|
46
|
+
const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
47
|
+
const entryKey = Object.keys(manifest).find((key) => manifest[key].isEntry);
|
|
48
|
+
if (!entryKey) {
|
|
49
|
+
console.error("bundle budget: no entry chunk in manifest");
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const seen = new Set();
|
|
54
|
+
const walk = (key) => {
|
|
55
|
+
if (seen.has(key) || !manifest[key]) return;
|
|
56
|
+
seen.add(key);
|
|
57
|
+
for (const imp of manifest[key].imports || []) walk(imp);
|
|
58
|
+
};
|
|
59
|
+
walk(entryKey);
|
|
60
|
+
|
|
61
|
+
const rows = [];
|
|
62
|
+
let rawTotal = 0;
|
|
63
|
+
let gzipTotal = 0;
|
|
64
|
+
for (const key of seen) {
|
|
65
|
+
const chunk = manifest[key];
|
|
66
|
+
if (!chunk?.file || !chunk.file.endsWith(".js")) continue;
|
|
67
|
+
const buffer = readFileSync(join(outDir, chunk.file));
|
|
68
|
+
const gzip = gzipSync(buffer).length;
|
|
69
|
+
rawTotal += buffer.length;
|
|
70
|
+
gzipTotal += gzip;
|
|
71
|
+
rows.push({ file: chunk.file, raw: buffer.length, gzip });
|
|
72
|
+
}
|
|
73
|
+
rows.sort((a, b) => b.gzip - a.gzip);
|
|
74
|
+
|
|
75
|
+
const lazyChunks = Object.values(manifest)
|
|
76
|
+
.filter((chunk) => chunk?.file?.endsWith(".js") && !seen.has(chunk.name ?? ""))
|
|
77
|
+
.filter((chunk) => !rows.some((row) => row.file === chunk.file));
|
|
78
|
+
|
|
79
|
+
return { rows, rawTotal, gzipTotal, lazyCount: lazyChunks.length };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const kib = (bytes) => `${(bytes / 1024).toFixed(1)} KiB`;
|
|
83
|
+
|
|
84
|
+
build();
|
|
85
|
+
const { rows, rawTotal, gzipTotal, lazyCount } = measure();
|
|
86
|
+
rmSync(outDir, { recursive: true, force: true });
|
|
87
|
+
|
|
88
|
+
console.log("Initial static JS (entry + static imports):");
|
|
89
|
+
for (const row of rows) {
|
|
90
|
+
console.log(` ${row.file.padEnd(40)} raw ${kib(row.raw).padStart(11)} gzip ${kib(row.gzip).padStart(11)}`);
|
|
91
|
+
}
|
|
92
|
+
console.log(` ${"TOTAL".padEnd(40)} raw ${kib(rawTotal).padStart(11)} gzip ${kib(gzipTotal).padStart(11)}`);
|
|
93
|
+
console.log(`Budget (initial JS gzip): ${kib(INITIAL_JS_GZIP_BUDGET)}`);
|
|
94
|
+
console.log(`Lazy JS chunks kept off first paint: ${lazyCount}`);
|
|
95
|
+
|
|
96
|
+
if (gzipTotal > INITIAL_JS_GZIP_BUDGET) {
|
|
97
|
+
console.error(
|
|
98
|
+
`\nbundle budget: FAIL — initial JS ${kib(gzipTotal)} gzip exceeds budget ${kib(INITIAL_JS_GZIP_BUDGET)}.\n`
|
|
99
|
+
+ "Move heavy modules behind React.lazy/dynamic import() or justify a budget bump.",
|
|
100
|
+
);
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
console.log(`\nbundle budget: PASS — initial JS ${kib(gzipTotal)} gzip within ${kib(INITIAL_JS_GZIP_BUDGET)}.`);
|
|
@@ -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 = "Clean Foundations";
|
|
10
10
|
const title = `${version} — ${releaseTheme}`;
|
|
11
11
|
const escapedVersion = version.replaceAll(".", "\\.");
|
|
12
12
|
|
|
@@ -71,6 +71,25 @@ for (const rel of [
|
|
|
71
71
|
requireIncludes(rel, version);
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
// The Release Artifact Map in ARCHITECTURE.md must name current-version
|
|
75
|
+
// artifacts exactly — this block drifted silently before 9.9.1.
|
|
76
|
+
requireIncludes("ARCHITECTURE.md", `${version} exact artifact names:`);
|
|
77
|
+
for (const artifact of [
|
|
78
|
+
`dist/ltcai-${version}-py3-none-any.whl`,
|
|
79
|
+
`dist/ltcai-${version}.tar.gz`,
|
|
80
|
+
`ltcai-${version}.tgz`,
|
|
81
|
+
`dist/ltcai-${version}.vsix`,
|
|
82
|
+
`src-tauri/target/release/bundle/dmg/Lattice AI_${version}_aarch64.dmg`,
|
|
83
|
+
]) {
|
|
84
|
+
requireIncludes("ARCHITECTURE.md", artifact);
|
|
85
|
+
}
|
|
86
|
+
const architecture = read("ARCHITECTURE.md");
|
|
87
|
+
for (const match of architecture.matchAll(/ltcai-(\d+\.\d+\.\d+)/g)) {
|
|
88
|
+
if (match[1] !== version) {
|
|
89
|
+
errors.push(`ARCHITECTURE.md: stale artifact version reference ${match[0]}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
74
93
|
requireIncludes("README.md", `The current release is **${title}**`);
|
|
75
94
|
requireIncludes("README.md", `![v${version} Living Brain walkthrough]`);
|
|
76
95
|
requireIncludes("RELEASE.md", `## v${version} — ${releaseTheme}`);
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// Repo-wide documentation status + link gate.
|
|
2
|
+
//
|
|
3
|
+
// Companion to scripts/check_current_release_docs.mjs (which pins the fixed set
|
|
4
|
+
// of current-release files). This script widens coverage to every Markdown doc:
|
|
5
|
+
//
|
|
6
|
+
// 1. Every relative Markdown link under the root canonical docs and docs/**
|
|
7
|
+
// must resolve to a file inside the repo.
|
|
8
|
+
// 2. Docs are classified by an explicit status badge:
|
|
9
|
+
// > **Status: canonical** (or `> Status: canonical`)
|
|
10
|
+
// > **Status: reference**
|
|
11
|
+
// > **Status: historical**
|
|
12
|
+
// Canonical docs are asserted to track the current package version and
|
|
13
|
+
// carry no stale "Current release" marker. Reference/historical (and
|
|
14
|
+
// unbadged) docs are allowed to preserve their point-in-time versions, so
|
|
15
|
+
// snapshots do not have to be rewritten every release.
|
|
16
|
+
//
|
|
17
|
+
// It intentionally does not force a badge onto every doc; unbadged docs are
|
|
18
|
+
// link-checked only. Add a `> **Status: ...**` badge when a doc should be held
|
|
19
|
+
// to (or exempted from) the current-version assertion.
|
|
20
|
+
|
|
21
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
22
|
+
import path from "node:path";
|
|
23
|
+
import process from "node:process";
|
|
24
|
+
|
|
25
|
+
const root = process.cwd();
|
|
26
|
+
const pkg = JSON.parse(readFileSync(path.join(root, "package.json"), "utf8"));
|
|
27
|
+
const version = pkg.version;
|
|
28
|
+
const escapedVersion = version.replaceAll(".", "\\.");
|
|
29
|
+
|
|
30
|
+
const rootCanonicalDocs = [
|
|
31
|
+
"README.md",
|
|
32
|
+
"ARCHITECTURE.md",
|
|
33
|
+
"FEATURE_STATUS.md",
|
|
34
|
+
"SECURITY.md",
|
|
35
|
+
"PRIVACY.md",
|
|
36
|
+
"AGENTS.md",
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
const errors = [];
|
|
40
|
+
|
|
41
|
+
function walkMarkdown(dir) {
|
|
42
|
+
const out = [];
|
|
43
|
+
if (!existsSync(dir)) return out;
|
|
44
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
45
|
+
const full = path.join(dir, entry.name);
|
|
46
|
+
if (entry.isDirectory()) {
|
|
47
|
+
out.push(...walkMarkdown(full));
|
|
48
|
+
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
49
|
+
out.push(full);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function collectDocs() {
|
|
56
|
+
const files = new Set();
|
|
57
|
+
for (const rel of rootCanonicalDocs) {
|
|
58
|
+
const full = path.join(root, rel);
|
|
59
|
+
if (existsSync(full)) files.add(full);
|
|
60
|
+
}
|
|
61
|
+
for (const full of walkMarkdown(path.join(root, "docs"))) {
|
|
62
|
+
files.add(full);
|
|
63
|
+
}
|
|
64
|
+
return [...files].sort();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function isExternal(target) {
|
|
68
|
+
return /^(https?:|mailto:|tel:)/i.test(target);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function relativeLinks(markdown) {
|
|
72
|
+
const out = [];
|
|
73
|
+
const linkPattern = /!?\[[^\]]*\]\(([^)]+)\)/g;
|
|
74
|
+
let match;
|
|
75
|
+
while ((match = linkPattern.exec(markdown))) {
|
|
76
|
+
out.push(match[1].trim());
|
|
77
|
+
}
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function checkLinks(file, markdown) {
|
|
82
|
+
for (const raw of relativeLinks(markdown)) {
|
|
83
|
+
const hash = raw.indexOf("#");
|
|
84
|
+
let target = (hash >= 0 ? raw.slice(0, hash) : raw).trim();
|
|
85
|
+
target = target.replace(/^<|>$/g, "").trim();
|
|
86
|
+
if (!target || isExternal(target)) continue;
|
|
87
|
+
const resolved = path.resolve(path.dirname(file), decodeURIComponent(target));
|
|
88
|
+
const rel = path.relative(root, file);
|
|
89
|
+
if (!resolved.startsWith(root)) {
|
|
90
|
+
errors.push(`${rel}: link points outside repo: ${raw}`);
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (!existsSync(resolved)) {
|
|
94
|
+
errors.push(`${rel}: missing link target: ${raw}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function statusOf(markdown) {
|
|
100
|
+
const match = markdown.match(
|
|
101
|
+
/>\s*\*{0,2}Status:\*{0,2}\s*(canonical|reference|historical)\b/i,
|
|
102
|
+
);
|
|
103
|
+
return match ? match[1].toLowerCase() : null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function checkCanonicalVersion(file, markdown) {
|
|
107
|
+
const rel = path.relative(root, file);
|
|
108
|
+
const stale = markdown.match(
|
|
109
|
+
new RegExp(`Current release:\\s+\\*\\*(?!${escapedVersion}\\b)[^*]+\\*\\*`, "i"),
|
|
110
|
+
);
|
|
111
|
+
if (stale) {
|
|
112
|
+
errors.push(`${rel}: canonical doc has stale current-release marker ${JSON.stringify(stale[0])}`);
|
|
113
|
+
}
|
|
114
|
+
if (!markdown.includes(version)) {
|
|
115
|
+
errors.push(`${rel}: canonical doc does not reference current version ${version}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const docs = collectDocs();
|
|
120
|
+
let canonical = 0;
|
|
121
|
+
let reference = 0;
|
|
122
|
+
let historical = 0;
|
|
123
|
+
|
|
124
|
+
for (const file of docs) {
|
|
125
|
+
const markdown = readFileSync(file, "utf8");
|
|
126
|
+
checkLinks(file, markdown);
|
|
127
|
+
const status = statusOf(markdown);
|
|
128
|
+
if (status === "canonical") {
|
|
129
|
+
canonical += 1;
|
|
130
|
+
checkCanonicalVersion(file, markdown);
|
|
131
|
+
} else if (status === "reference") {
|
|
132
|
+
reference += 1;
|
|
133
|
+
} else if (status === "historical") {
|
|
134
|
+
historical += 1;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (errors.length) {
|
|
139
|
+
console.error("Documentation status/link check failed:");
|
|
140
|
+
for (const error of errors) {
|
|
141
|
+
console.error(`- ${error}`);
|
|
142
|
+
}
|
|
143
|
+
process.exit(1);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
console.log(
|
|
147
|
+
`Documentation status/link check passed: ${docs.length} docs scanned ` +
|
|
148
|
+
`(${canonical} canonical @ ${version}, ${reference} reference, ${historical} historical).`,
|
|
149
|
+
);
|