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.
Files changed (97) hide show
  1. package/README.md +45 -28
  2. package/bin/ltcai.js +1 -1
  3. package/desktop/electron/main.cjs +1 -1
  4. package/docs/BENCHMARKS.md +107 -0
  5. package/docs/CHANGELOG.md +82 -0
  6. package/docs/CI_AND_RELEASE_GATES.md +106 -0
  7. package/docs/COMMUNITY_AND_PLUGINS.md +1 -1
  8. package/docs/DEVELOPMENT.md +11 -8
  9. package/docs/LEGACY_COMPATIBILITY.md +48 -49
  10. package/docs/ONBOARDING.md +1 -1
  11. package/docs/OPERATIONS.md +15 -6
  12. package/docs/PERFORMANCE.md +9 -1
  13. package/docs/SECURITY_AUDIT.md +91 -0
  14. package/docs/TRUST_MODEL.md +1 -1
  15. package/docs/USABILITY_AUDIT.md +164 -0
  16. package/docs/WHY_LATTICE.md +1 -1
  17. package/docs/architecture.md +4 -2
  18. package/docs/kg-schema.md +6 -6
  19. package/docs/spec-vs-impl.md +8 -1
  20. package/lattice_brain/__init__.py +1 -1
  21. package/lattice_brain/graph/schema.py +1 -1
  22. package/lattice_brain/runtime/multi_agent.py +1 -1
  23. package/latticeai/__init__.py +1 -1
  24. package/latticeai/api/review_queue.py +7 -0
  25. package/latticeai/core/agent.py +105 -8
  26. package/latticeai/core/agent_eval.py +99 -1
  27. package/latticeai/core/legacy_compatibility.py +71 -47
  28. package/latticeai/core/marketplace.py +1 -1
  29. package/latticeai/core/tool_governor.py +92 -1
  30. package/latticeai/core/workspace_os.py +1 -1
  31. package/latticeai/services/architecture_readiness.py +1 -1
  32. package/latticeai/services/change_proposals.py +165 -23
  33. package/latticeai/services/product_readiness.py +2 -2
  34. package/latticeai/services/tool_dispatch.py +33 -0
  35. package/latticeai/tools/__init__.py +1 -1
  36. package/latticeai/tools/knowledge.py +1 -1
  37. package/package.json +9 -17
  38. package/scripts/bench_models.py +312 -0
  39. package/scripts/check_bundle_budget.mjs +103 -0
  40. package/scripts/check_current_release_docs.mjs +20 -1
  41. package/scripts/check_doc_status.mjs +149 -0
  42. package/scripts/check_legacy_debt.mjs +91 -0
  43. package/scripts/generate_sbom.py +70 -0
  44. package/scripts/prune_release_evidence.mjs +38 -0
  45. package/scripts/wheel_smoke.py +22 -6
  46. package/src-tauri/Cargo.lock +1 -1
  47. package/src-tauri/Cargo.toml +1 -1
  48. package/src-tauri/src/main.rs +12 -12
  49. package/src-tauri/tauri.conf.json +1 -13
  50. package/static/app/asset-manifest.json +27 -11
  51. package/static/app/assets/Act-CcSWTQsx.js +1 -0
  52. package/static/app/assets/{Brain-BMkgdWnI.js → Brain-CSCDZEHd.js} +2 -2
  53. package/static/app/assets/BrainHome-B5QpJ1Fx.js +2 -0
  54. package/static/app/assets/BrainSignals-CmFD8PWk.js +1 -0
  55. package/static/app/assets/Capture-BiuerOio.js +1 -0
  56. package/static/app/assets/CommandPalette-BrqpT7K2.js +1 -0
  57. package/static/app/assets/Library-BcaiPbum.js +1 -0
  58. package/static/app/assets/LivingBrain-6FvA0H5T.js +1 -0
  59. package/static/app/assets/ProductFlow-Cr8bkQPw.js +1 -0
  60. package/static/app/assets/ReviewCard-D56TMypk.js +1 -0
  61. package/static/app/assets/System-D6uJacqu.js +1 -0
  62. package/static/app/assets/bot-DgC7xgUl.js +1 -0
  63. package/static/app/assets/cpu-DDGb4XHl.js +1 -0
  64. package/static/app/assets/download-C-Ei3GFa.js +1 -0
  65. package/static/app/assets/folder-open-BbVuSO9M.js +1 -0
  66. package/static/app/assets/hard-drive-yDZR6XYz.js +1 -0
  67. package/static/app/assets/index-CFRhNBuH.css +2 -0
  68. package/static/app/assets/index-DrZSkwrE.js +10 -0
  69. package/static/app/assets/input-DPIkTs9n.js +1 -0
  70. package/static/app/assets/navigation-B1mkmyUe.js +1 -0
  71. package/static/app/assets/network-0XDI1Uf_.js +1 -0
  72. package/static/app/assets/primitives-sIQYKyf5.js +1 -0
  73. package/static/app/assets/sunrise-DXMjueec.js +1 -0
  74. package/static/app/assets/textarea-DvnwpnqS.js +1 -0
  75. package/static/app/assets/utils-BrfFHAJl.js +7 -0
  76. package/static/app/index.html +4 -2
  77. package/static/sw.js +1 -1
  78. package/auto_setup.py +0 -21
  79. package/kg_schema.py +0 -13
  80. package/knowledge_graph.py +0 -51
  81. package/knowledge_graph_api.py +0 -24
  82. package/llm_router.py +0 -20
  83. package/local_knowledge_api.py +0 -16
  84. package/ltcai_cli.py +0 -21
  85. package/mcp_registry.py +0 -20
  86. package/p_reinforce.py +0 -16
  87. package/setup_wizard.py +0 -16
  88. package/static/app/assets/Act-Dd3z8AzF.js +0 -2
  89. package/static/app/assets/Capture-D2Aw9gkv.js +0 -1
  90. package/static/app/assets/Library-Yreq-KW5.js +0 -1
  91. package/static/app/assets/System-CXNmmtEo.js +0 -1
  92. package/static/app/assets/index-7gY9t9Sd.css +0 -2
  93. package/static/app/assets/index-CndfILiF.js +0 -18
  94. package/static/app/assets/primitives-DxsIXb6G.js +0 -1
  95. package/static/app/assets/textarea-DH7ne8VI.js +0 -1
  96. package/telegram_bot.py +0 -16
  97. 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 = "Honest Knowledge Pipeline";
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
+ );