ltcai 11.2.0 → 11.4.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.
Files changed (247) hide show
  1. package/README.md +46 -53
  2. package/docs/CHANGELOG.md +61 -0
  3. package/docs/COMMUNITY_AND_PLUGINS.md +1 -1
  4. package/docs/DEVELOPMENT.md +1 -1
  5. package/docs/MULTI_AGENT_RUNTIME.md +1 -1
  6. package/docs/ONBOARDING.md +1 -1
  7. package/docs/OPERATIONS.md +6 -2
  8. package/docs/PERMISSION_MODE.md +1 -1
  9. package/docs/TRUST_MODEL.md +1 -1
  10. package/docs/WHY_LATTICE.md +1 -1
  11. package/docs/kg-schema.md +2 -2
  12. package/docs/v11.3.0_PLAN.md +202 -0
  13. package/docs/v11.4.0_RUST_FOUNDATION_PLAN.md +176 -0
  14. package/lattice_brain/__init__.py +1 -1
  15. package/lattice_brain/graph/_kg_common/__init__.py +287 -0
  16. package/lattice_brain/graph/_kg_common/extraction.py +516 -0
  17. package/lattice_brain/graph/_kg_common/relations.py +161 -0
  18. package/lattice_brain/graph/_kg_common/text.py +479 -0
  19. package/lattice_brain/graph/discovery_index/__init__.py +35 -0
  20. package/lattice_brain/graph/discovery_index/cleanup.py +182 -0
  21. package/lattice_brain/graph/discovery_index/extract.py +137 -0
  22. package/lattice_brain/graph/discovery_index/scan.py +411 -0
  23. package/lattice_brain/graph/discovery_index/upsert.py +495 -0
  24. package/lattice_brain/graph/projection/__init__.py +42 -0
  25. package/lattice_brain/graph/projection/curation.py +500 -0
  26. package/lattice_brain/graph/{projection.py → projection/v2_schema.py} +15 -477
  27. package/lattice_brain/graph/retrieval/__init__.py +54 -0
  28. package/lattice_brain/graph/retrieval/context.py +197 -0
  29. package/lattice_brain/graph/retrieval/graph_view.py +319 -0
  30. package/lattice_brain/graph/retrieval/hybrid.py +488 -0
  31. package/lattice_brain/graph/retrieval/maintenance.py +121 -0
  32. package/lattice_brain/graph/retrieval/signals.py +95 -0
  33. package/lattice_brain/graph/retrieval_vector/__init__.py +42 -0
  34. package/lattice_brain/graph/retrieval_vector/fingerprint.py +97 -0
  35. package/lattice_brain/graph/retrieval_vector/indexing.py +347 -0
  36. package/lattice_brain/graph/retrieval_vector/search.py +560 -0
  37. package/lattice_brain/graph/retrieval_vector/status.py +374 -0
  38. package/lattice_brain/ingestion/__init__.py +130 -0
  39. package/lattice_brain/ingestion/_contract.py +90 -0
  40. package/lattice_brain/ingestion/constants.py +127 -0
  41. package/lattice_brain/ingestion/folder_scan.py +57 -0
  42. package/lattice_brain/ingestion/folders.py +258 -0
  43. package/lattice_brain/ingestion/hashing.py +26 -0
  44. package/lattice_brain/ingestion/jobs_api.py +107 -0
  45. package/lattice_brain/ingestion/models.py +80 -0
  46. package/lattice_brain/ingestion/pipeline.py +486 -0
  47. package/lattice_brain/ingestion/quality.py +209 -0
  48. package/lattice_brain/ingestion/routing.py +295 -0
  49. package/lattice_brain/multimodal/__init__.py +164 -0
  50. package/lattice_brain/multimodal/audio.py +77 -0
  51. package/lattice_brain/multimodal/common.py +118 -0
  52. package/lattice_brain/multimodal/images.py +498 -0
  53. package/lattice_brain/multimodal/ports.py +169 -0
  54. package/lattice_brain/multimodal/video.py +410 -0
  55. package/lattice_brain/portability/__init__.py +90 -0
  56. package/lattice_brain/portability/_contract.py +42 -0
  57. package/lattice_brain/portability/backups.py +338 -0
  58. package/lattice_brain/portability/bundles.py +136 -0
  59. package/lattice_brain/portability/constants.py +93 -0
  60. package/lattice_brain/portability/fsops.py +138 -0
  61. package/lattice_brain/portability/service.py +41 -0
  62. package/lattice_brain/{portability.py → portability/sharing.py} +44 -677
  63. package/lattice_brain/runtime/__init__.py +1 -1
  64. package/lattice_brain/runtime/multi_agent.py +1 -1
  65. package/latticeai/__init__.py +1 -1
  66. package/latticeai/api/chronicle.py +63 -0
  67. package/latticeai/core/agent/__init__.py +93 -0
  68. package/latticeai/core/agent/_contract.py +79 -0
  69. package/latticeai/core/agent/context.py +57 -0
  70. package/latticeai/core/agent/deps.py +125 -0
  71. package/latticeai/core/agent/execution.py +622 -0
  72. package/latticeai/core/agent/planning.py +145 -0
  73. package/latticeai/core/agent/recovery.py +157 -0
  74. package/latticeai/core/agent/runtime.py +210 -0
  75. package/latticeai/core/agent/verification.py +231 -0
  76. package/latticeai/core/embedding_providers/__init__.py +151 -0
  77. package/latticeai/core/embedding_providers/base.py +199 -0
  78. package/latticeai/core/embedding_providers/captions.py +162 -0
  79. package/latticeai/core/embedding_providers/profiles.py +126 -0
  80. package/latticeai/core/embedding_providers/text.py +350 -0
  81. package/latticeai/core/embedding_providers/vision.py +352 -0
  82. package/latticeai/core/file_generation/__init__.py +115 -0
  83. package/latticeai/core/file_generation/bundles.py +76 -0
  84. package/latticeai/core/file_generation/extraction.py +154 -0
  85. package/latticeai/core/file_generation/inference.py +235 -0
  86. package/latticeai/core/file_generation/orchestration.py +152 -0
  87. package/latticeai/core/file_generation/prompting.py +117 -0
  88. package/latticeai/core/file_generation/repair.py +114 -0
  89. package/latticeai/core/file_generation/sanitize.py +61 -0
  90. package/latticeai/core/file_generation/validation.py +201 -0
  91. package/latticeai/core/legacy_compatibility.py +1 -1
  92. package/latticeai/core/marketplace.py +1 -1
  93. package/latticeai/core/messages.py +9 -0
  94. package/latticeai/core/workspace_os_constants.py +1 -1
  95. package/latticeai/integrations/telegram_bot/__init__.py +123 -0
  96. package/latticeai/integrations/telegram_bot/__main__.py +17 -0
  97. package/latticeai/integrations/telegram_bot/config.py +86 -0
  98. package/latticeai/integrations/telegram_bot/dispatch.py +311 -0
  99. package/latticeai/integrations/telegram_bot/flows.py +478 -0
  100. package/latticeai/integrations/telegram_bot/helpers.py +322 -0
  101. package/latticeai/integrations/telegram_bot/screens.py +394 -0
  102. package/latticeai/models/router/__init__.py +88 -0
  103. package/latticeai/models/router/_contract.py +66 -0
  104. package/latticeai/models/router/branding.py +56 -0
  105. package/latticeai/models/router/catalog.py +69 -0
  106. package/latticeai/models/router/documents.py +199 -0
  107. package/latticeai/models/router/errors.py +37 -0
  108. package/latticeai/models/router/generation.py +258 -0
  109. package/latticeai/models/router/loading.py +291 -0
  110. package/latticeai/models/router/local_models.py +85 -0
  111. package/latticeai/models/router/registry.py +147 -0
  112. package/latticeai/runtime/build_phases/__init__.py +82 -0
  113. package/latticeai/runtime/build_phases/features.py +407 -0
  114. package/latticeai/runtime/build_phases/foundation.py +555 -0
  115. package/latticeai/runtime/build_phases/web.py +492 -0
  116. package/latticeai/runtime/runtime_context.py +1 -0
  117. package/latticeai/services/architecture_readiness.py +48 -19
  118. package/latticeai/services/brain_intelligence/__init__.py +58 -0
  119. package/latticeai/services/brain_intelligence/_contract.py +71 -0
  120. package/latticeai/services/brain_intelligence/consistency.py +193 -0
  121. package/latticeai/services/brain_intelligence/constants.py +47 -0
  122. package/latticeai/services/brain_intelligence/digest.py +258 -0
  123. package/latticeai/services/brain_intelligence/health.py +331 -0
  124. package/latticeai/services/brain_intelligence/proposals.py +264 -0
  125. package/latticeai/services/brain_intelligence/sampling.py +84 -0
  126. package/latticeai/services/brain_intelligence/service.py +48 -0
  127. package/latticeai/services/chronicle.py +557 -0
  128. package/latticeai/services/memory_service/__init__.py +52 -0
  129. package/latticeai/services/memory_service/_contract.py +100 -0
  130. package/latticeai/services/memory_service/brief.py +431 -0
  131. package/latticeai/services/memory_service/constants.py +57 -0
  132. package/latticeai/services/memory_service/maintenance.py +138 -0
  133. package/latticeai/services/memory_service/manager.py +186 -0
  134. package/latticeai/services/memory_service/proof.py +136 -0
  135. package/latticeai/services/memory_service/recall.py +225 -0
  136. package/latticeai/services/memory_service/service.py +48 -0
  137. package/latticeai/services/memory_service/stores.py +110 -0
  138. package/latticeai/services/model_runtime/__init__.py +322 -0
  139. package/latticeai/services/model_runtime/cloud.py +87 -0
  140. package/latticeai/services/model_runtime/download.py +282 -0
  141. package/latticeai/services/model_runtime/engines.py +341 -0
  142. package/latticeai/services/model_runtime/loading.py +178 -0
  143. package/latticeai/services/model_runtime/service.py +129 -0
  144. package/latticeai/services/model_runtime/state.py +131 -0
  145. package/latticeai/services/model_runtime/status.py +255 -0
  146. package/latticeai/services/product_readiness.py +15 -7
  147. package/latticeai/setup/wizard/__init__.py +126 -0
  148. package/latticeai/setup/wizard/catalog.py +172 -0
  149. package/latticeai/setup/wizard/detect.py +323 -0
  150. package/latticeai/setup/wizard/install.py +348 -0
  151. package/latticeai/setup/wizard/paths.py +168 -0
  152. package/latticeai/setup/wizard/plans.py +74 -0
  153. package/latticeai/setup/wizard/recommend.py +320 -0
  154. package/package.json +6 -2
  155. package/scripts/bump_version.py +14 -0
  156. package/scripts/capture_release_evidence.mjs +33 -21
  157. package/scripts/check_current_release_docs.mjs +1 -1
  158. package/scripts/check_i18n_namespace_coverage.mjs +41 -4
  159. package/scripts/check_max_file_lines.mjs +102 -0
  160. package/scripts/check_release_evidence_bound.mjs +30 -15
  161. package/scripts/check_screenshot_pixel_delta.py +34 -4
  162. package/scripts/check_server_i18n.mjs +1 -0
  163. package/scripts/generate_rust_parity_fixtures.py +562 -0
  164. package/scripts/lib/mock_server_fingerprint.mjs +94 -0
  165. package/scripts/release_screen_claims.json +31 -2
  166. package/src-tauri/Cargo.lock +361 -3
  167. package/src-tauri/Cargo.toml +6 -1
  168. package/src-tauri/src/backend.rs +349 -0
  169. package/src-tauri/src/folder.rs +33 -0
  170. package/src-tauri/src/main.rs +97 -399
  171. package/src-tauri/tauri.conf.json +1 -1
  172. package/static/app/asset-manifest.json +41 -37
  173. package/static/app/assets/Act-yYpYnn0v.js +1 -0
  174. package/static/app/assets/AdminConsole-DL3Cr5pL.js +1 -0
  175. package/static/app/assets/{Brain-tuhI4sOC.js → Brain-C1HBN0Wf.js} +2 -2
  176. package/static/app/assets/BrainHome-DoXRhUUC.js +2 -0
  177. package/static/app/assets/BrainSignals-6yR6ir5t.js +1 -0
  178. package/static/app/assets/Capture-CFIRsFNE.js +1 -0
  179. package/static/app/assets/Chronicle-BZbEgiwN.js +1 -0
  180. package/static/app/assets/CommandPalette-D2pMxC2I.js +1 -0
  181. package/static/app/assets/Library-DwO3yZST.js +1 -0
  182. package/static/app/assets/{LivingBrain-DBwhto14.js → LivingBrain-Jn1GK0-S.js} +1 -1
  183. package/static/app/assets/ProductFlow-B-w1R4Oo.js +1 -0
  184. package/static/app/assets/ReviewCard-6B27X8Vg.js +3 -0
  185. package/static/app/assets/System-DW8F-2xL.js +1 -0
  186. package/static/app/assets/arrow-left-DXvKg9U6.js +1 -0
  187. package/static/app/assets/{bot-Cia42c2h.js → bot-IM_E_Y12.js} +1 -1
  188. package/static/app/assets/brain-Ci1CkWjM.js +1 -0
  189. package/static/app/assets/{button-2j2Ijzgq.js → button-COwyqfHM.js} +1 -1
  190. package/static/app/assets/circle-check-DfInj-qD.js +1 -0
  191. package/static/app/assets/{circle-pause-BEFeWpVW.js → circle-pause-DEM4A1Y5.js} +1 -1
  192. package/static/app/assets/{circle-play-ujXMcHxl.js → circle-play-C9djDuLd.js} +1 -1
  193. package/static/app/assets/{cpu-k4awryFq.js → cpu-DFdo1gw-.js} +1 -1
  194. package/static/app/assets/{download-DFbLJ_ig.js → download-SnJL6oqk.js} +1 -1
  195. package/static/app/assets/{folder-open-7y_b6xkM.js → folder-open-CqZeDkjE.js} +1 -1
  196. package/static/app/assets/{hard-drive-Bidh02Kr.js → hard-drive-j1jJXYYf.js} +1 -1
  197. package/static/app/assets/{index-DwDl9-8Y.css → index-BLPb5lmE.css} +1 -1
  198. package/static/app/assets/index-_u5iUHDr.js +10 -0
  199. package/static/app/assets/input-B0lPdRQZ.js +1 -0
  200. package/static/app/assets/link-2-CoFbooHS.js +1 -0
  201. package/static/app/assets/{permissionCopy-Bpb83Hx9.js → permissionCopy-BsyLxtao.js} +1 -1
  202. package/static/app/assets/primitives-DEbN-d6p.js +1 -0
  203. package/static/app/assets/search-BybIWPNd.js +1 -0
  204. package/static/app/assets/{share-2-BH1M-WNi.js → share-2-CVtZ_ewX.js} +1 -1
  205. package/static/app/assets/{shield-alert-BlKdBXcG.js → shield-alert-CBi2GNWM.js} +1 -1
  206. package/static/app/assets/{textarea-CCWbUfFB.js → textarea-DNMpB5ih.js} +1 -1
  207. package/static/app/assets/{useFocusTrap-YdHQ7pJ1.js → useFocusTrap-C83t3GXF.js} +1 -1
  208. package/static/app/assets/useMutation-DtbJDoyz.js +1 -0
  209. package/static/app/assets/{useQuery-CXQiwbVT.js → useQuery-Dcp1OChy.js} +1 -1
  210. package/static/app/assets/utils-BlZr7Pd4.js +4 -0
  211. package/static/app/assets/workspace-jJY4RuAV.js +1 -0
  212. package/static/app/index.html +4 -4
  213. package/static/sw.js +1 -1
  214. package/lattice_brain/graph/_kg_common.py +0 -1331
  215. package/lattice_brain/graph/discovery_index.py +0 -1141
  216. package/lattice_brain/graph/retrieval.py +0 -1120
  217. package/lattice_brain/graph/retrieval_vector.py +0 -1293
  218. package/lattice_brain/ingestion.py +0 -1525
  219. package/lattice_brain/multimodal.py +0 -1258
  220. package/latticeai/core/agent.py +0 -1465
  221. package/latticeai/core/embedding_providers.py +0 -1196
  222. package/latticeai/core/file_generation.py +0 -1047
  223. package/latticeai/integrations/telegram_bot.py +0 -1390
  224. package/latticeai/models/router.py +0 -1007
  225. package/latticeai/runtime/build_phases.py +0 -1450
  226. package/latticeai/services/brain_intelligence.py +0 -1083
  227. package/latticeai/services/memory_service.py +0 -1177
  228. package/latticeai/services/model_runtime.py +0 -1281
  229. package/latticeai/setup/wizard.py +0 -1310
  230. package/static/app/assets/Act-AWf0SAKp.js +0 -1
  231. package/static/app/assets/AdminConsole-D0u8Tiyj.js +0 -1
  232. package/static/app/assets/BrainHome-Ts7G_Ila.js +0 -2
  233. package/static/app/assets/BrainSignals-jMYgQ2Ar.js +0 -1
  234. package/static/app/assets/Capture-CqOSzyPr.js +0 -1
  235. package/static/app/assets/CommandPalette-DC0Bzh-I.js +0 -1
  236. package/static/app/assets/Library-CX-bbhmK.js +0 -1
  237. package/static/app/assets/ProductFlow-BHA2cfKI.js +0 -1
  238. package/static/app/assets/ReviewCard-BUhCKRNM.js +0 -3
  239. package/static/app/assets/System-Bu2t5hn1.js +0 -1
  240. package/static/app/assets/arrow-left-Dzwa5zRb.js +0 -1
  241. package/static/app/assets/brain-DJMoqrwx.js +0 -1
  242. package/static/app/assets/index-BpYkzcVm.js +0 -10
  243. package/static/app/assets/input-DSlJJxRs.js +0 -1
  244. package/static/app/assets/primitives-BCx6TvfG.js +0 -1
  245. package/static/app/assets/search-Cgy8cCFJ.js +0 -1
  246. package/static/app/assets/utils-zqPZJxdx.js +0 -4
  247. package/static/app/assets/workspace-DXTihhfU.js +0 -1
@@ -0,0 +1,102 @@
1
+ #!/usr/bin/env node
2
+ // Max file length gate (11.3.0): no first-party source file over 1,000 lines.
3
+ //
4
+ // Why a gate and not a habit: `frontend/src/styles.css` reached 10,956 lines and
5
+ // twenty-seven other files passed 1,000 before anyone noticed. Long files are
6
+ // not a style opinion here — they are slow to review, they burn an agent's
7
+ // context on the 90% of the file that is irrelevant to the change, and two
8
+ // people editing different features of the same file conflict every time.
9
+ //
10
+ // The rule: every git-tracked *.py *.ts *.tsx *.js *.mjs *.cjs *.css *.rs file —
11
+ // tests included — stays at or under 1,000 lines. Split by cohesion, not by
12
+ // cutting at line 1,000: a module that needs more than a thousand lines is doing
13
+ // more than one job.
14
+ //
15
+ // Exit 0 clean, 1 when a file is over the limit, 2 when the check cannot run.
16
+ import { execFileSync } from "node:child_process";
17
+ import { readFileSync } from "node:fs";
18
+ import { join } from "node:path";
19
+ import process from "node:process";
20
+
21
+ const repo = join(import.meta.dirname, "..");
22
+
23
+ const MAX_LINES = 1000;
24
+
25
+ // `*.rs` joined in 11.4.0 with the Rust foundation (rust/ workspace +
26
+ // src-tauri). Build output — rust/target, src-tauri/target — is gitignored and
27
+ // therefore never listed here in the first place.
28
+ const EXTENSIONS = ["*.py", "*.ts", "*.tsx", "*.js", "*.mjs", "*.cjs", "*.css", "*.rs"];
29
+
30
+ // Paths that are not written by hand. Each entry names why it is here; nothing
31
+ // gets added to this list to make a hand-written file pass.
32
+ const EXCLUDED = [
33
+ // Generated from the OpenAPI schema by `npm run frontend:openapi`.
34
+ "frontend/src/api/openapi.ts",
35
+ // Vite build output. Editing it by hand is already a mistake (v10.1.1).
36
+ "static/app/",
37
+ // Third-party libraries shipped as-is.
38
+ "static/vendor/",
39
+ // Tauri's generated platform scaffolding.
40
+ "src-tauri/gen/",
41
+ ];
42
+
43
+ function isExcluded(file) {
44
+ return EXCLUDED.some((prefix) =>
45
+ prefix.endsWith("/") ? file.startsWith(prefix) : file === prefix,
46
+ );
47
+ }
48
+
49
+ /** `wc -l` semantics: how many newline-terminated lines the file holds. */
50
+ function lineCount(file) {
51
+ const text = readFileSync(join(repo, file), "utf8");
52
+ if (!text) return 0;
53
+ const parts = text.split("\n");
54
+ return text.endsWith("\n") ? parts.length - 1 : parts.length;
55
+ }
56
+
57
+ // `--others --exclude-standard` adds files that exist but are not committed yet
58
+ // while still honouring .gitignore. Without it a brand-new 3,000-line module
59
+ // passes the gate on the branch that introduces it and only fails after the
60
+ // commit that was supposed to be reviewed against it.
61
+ let tracked;
62
+ try {
63
+ const listed = execFileSync(
64
+ "git",
65
+ ["ls-files", "--cached", "--others", "--exclude-standard", "--", ...EXTENSIONS],
66
+ { cwd: repo, encoding: "utf8", maxBuffer: 32 * 1024 * 1024 },
67
+ );
68
+ tracked = [...new Set(listed.split("\n").map((line) => line.trim()).filter(Boolean))];
69
+ } catch (error) {
70
+ console.error(`max file lines: could not list tracked files (${error.message})`);
71
+ process.exit(2);
72
+ }
73
+
74
+ const oversized = [];
75
+ for (const file of tracked) {
76
+ if (isExcluded(file)) continue;
77
+ let lines;
78
+ try {
79
+ lines = lineCount(file);
80
+ } catch {
81
+ // A tracked path that is not readable (deleted in the working tree, a
82
+ // submodule pointer) is not this gate's business.
83
+ continue;
84
+ }
85
+ if (lines > MAX_LINES) oversized.push({ file, lines });
86
+ }
87
+
88
+ if (oversized.length) {
89
+ oversized.sort((a, b) => b.lines - a.lines);
90
+ console.error(
91
+ `max file lines: ${oversized.length} file(s) over ${MAX_LINES} lines.\n` +
92
+ oversized.map(({ file, lines }) => ` ${String(lines).padStart(6)} ${file}`).join("\n") +
93
+ "\n\n Split each into cohesive modules (target ≤700 lines) and re-run.\n" +
94
+ " Generated or vendored output belongs in the EXCLUDED list in\n" +
95
+ " scripts/check_max_file_lines.mjs, with a comment saying why.",
96
+ );
97
+ process.exit(1);
98
+ }
99
+
100
+ console.log(
101
+ `max file lines ok: ${tracked.length} tracked file(s) scanned, none over ${MAX_LINES} lines`,
102
+ );
@@ -9,14 +9,18 @@
9
9
  * 3. The pre-capture guard only compares frontend/src mtime vs manifest mtime,
10
10
  * so it still passes — while screenshots show an older UI than the build.
11
11
  *
12
- * Also: capture hits tests/visual/mock_server.cjs for API payloads. Changing
13
- * the mock after capture (e.g. taller Act panel → 1846px vs committed 1721px)
14
- * left asset-manifest.sha256 alone, so this gate used to stay green while
12
+ * Also: capture hits the visual mock server for API payloads. Changing the mock
13
+ * after capture (e.g. taller Act panel → 1846px vs committed 1721px) left
14
+ * asset-manifest.sha256 alone, so this gate used to stay green while
15
15
  * SCREENSHOT_INDEX.md described a UI the tree no longer produces.
16
16
  *
17
+ * The mock server is an entry file plus the route modules it composes, and the
18
+ * fingerprint covers all of them — see scripts/lib/mock_server_fingerprint.mjs,
19
+ * shared with capture so the recorded and the checked digest cannot drift.
20
+ *
17
21
  * Capture records asset-manifest.sha256 and mock-server.sha256 in
18
22
  * SCREENSHOT_INDEX.md. This script fails (exit 1) when either binding is
19
- * missing or does not match the live file. Wired into `npm run lint` so a
23
+ * missing or does not match the live files. Wired into `npm run lint` so a
20
24
  * green lint cannot ship stale screenshots.
21
25
  *
22
26
  * Exit 0 bound, 1 stale/missing, 2 could not run.
@@ -28,6 +32,12 @@ import { createHash } from "node:crypto";
28
32
  import fs from "node:fs";
29
33
  import path from "node:path";
30
34
 
35
+ import {
36
+ MOCK_SERVER_ENTRY,
37
+ MOCK_SERVER_LABEL,
38
+ mockServerFingerprint,
39
+ } from "./lib/mock_server_fingerprint.mjs";
40
+
31
41
  const repoRoot = process.cwd();
32
42
 
33
43
  if (process.env.LTCAI_SKIP_RELEASE_EVIDENCE_BOUND === "1") {
@@ -43,7 +53,6 @@ const root =
43
53
  path.join(repoRoot, "output", "release", `v${version}`);
44
54
  const indexPath = path.join(root, "SCREENSHOT_INDEX.md");
45
55
  const manifestPath = path.join(repoRoot, "static", "app", "asset-manifest.json");
46
- const mockServerPath = path.join(repoRoot, "tests", "visual", "mock_server.cjs");
47
56
 
48
57
  function fail(message, code = 1) {
49
58
  console.error(message);
@@ -60,8 +69,9 @@ if (!fs.existsSync(indexPath)) {
60
69
  if (!fs.existsSync(manifestPath)) {
61
70
  fail("release evidence binding: static/app/asset-manifest.json missing", 2);
62
71
  }
63
- if (!fs.existsSync(mockServerPath)) {
64
- fail("release evidence binding: tests/visual/mock_server.cjs missing", 2);
72
+ const mockFp = mockServerFingerprint(repoRoot);
73
+ if (!mockFp) {
74
+ fail(`release evidence binding: ${MOCK_SERVER_ENTRY} missing`, 2);
65
75
  }
66
76
 
67
77
  const index = fs.readFileSync(indexPath, "utf8");
@@ -77,7 +87,7 @@ const mockMatch = index.match(/mock-server\.sha256:\s*`?([0-9a-f]{64})`?/i);
77
87
  if (!mockMatch) {
78
88
  fail(
79
89
  "release evidence binding: SCREENSHOT_INDEX.md has no mock-server.sha256.\n" +
80
- " Capture data comes from tests/visual/mock_server.cjs; re-run\n" +
90
+ ` Capture data comes from ${MOCK_SERVER_LABEL}; re-run\n` +
81
91
  " npm run release:evidence so the index records the mock fingerprint.",
82
92
  );
83
93
  }
@@ -97,20 +107,25 @@ if (recorded !== current) {
97
107
  }
98
108
 
99
109
  const recordedMock = mockMatch[1].toLowerCase();
100
- const currentMock = sha256File(mockServerPath);
101
- const mockMtime = fs.statSync(mockServerPath).mtime.toISOString();
110
+ const currentMock = mockFp.sha256;
102
111
 
103
112
  if (recordedMock !== currentMock) {
104
113
  fail(
105
- "release evidence binding: screenshots are stale relative to mock_server.cjs.\n" +
114
+ "release evidence binding: the mock server changed since capture.\n" +
106
115
  ` recorded mock-server.sha256: ${recordedMock}\n` +
107
116
  ` current mock-server.sha256: ${currentMock}\n` +
108
- ` current mtime: ${mockMtime}\n` +
109
- " Mock API payloads changed after capture. Run: npm run release:evidence\n" +
110
- " (bump version first if vX.Y.Z is already tagged — see docs/OPERATIONS.md).",
117
+ ` covers: ${MOCK_SERVER_LABEL} (${mockFp.files} file(s), ${mockFp.bytes} bytes)\n` +
118
+ ` newest mtime: ${mockFp.mtime}\n` +
119
+ " Mock API payloads changed after capture, so the screenshots may show\n" +
120
+ " responses the tree no longer produces. Re-capture the evidence:\n" +
121
+ " npm run release:evidence\n" +
122
+ " (bump version first if vX.Y.Z is already tagged — see docs/OPERATIONS.md).\n" +
123
+ " v11.3.0 widened this fingerprint from the entry file alone to the entry\n" +
124
+ " plus its route modules, so an evidence set captured before v11.3.0\n" +
125
+ " reports a mismatch until it is re-captured.",
111
126
  );
112
127
  }
113
128
 
114
129
  console.log(
115
- `release evidence binding ok: asset=${current.slice(0, 12)}… mock=${currentMock.slice(0, 12)}… mtime=${mtime} dir=${path.relative(repoRoot, root)}`,
130
+ `release evidence binding ok: asset=${current.slice(0, 12)}… mock=${currentMock.slice(0, 12)}… (${mockFp.files} mock file(s)) mtime=${mtime} dir=${path.relative(repoRoot, root)}`,
116
131
  );
@@ -45,6 +45,7 @@ SHOTS = [
45
45
  "10-admin-console.png",
46
46
  "11-knowledge-journey.png",
47
47
  "12-review-center.png",
48
+ "13-chronicle.png",
48
49
  ]
49
50
 
50
51
  # LivingBrain animation noise can hit ~7.9% with zero layout change.
@@ -183,11 +184,10 @@ def _extract_baseline_from_git(root: Path, version: str) -> Optional[Path]:
183
184
 
184
185
  cache = root / "output" / ".cache" / "screenshot-baseline" / f"v{version}" / "screenshots"
185
186
  cache.mkdir(parents=True, exist_ok=True)
186
- extracted = 0
187
+ missing: List[str] = []
187
188
  for name in SHOTS:
188
189
  dest = cache / name
189
190
  if dest.is_file() and dest.stat().st_size > 0:
190
- extracted += 1
191
191
  continue
192
192
  git_path = f"output/release/v{version}/screenshots/{name}"
193
193
  result = subprocess.run(
@@ -197,11 +197,19 @@ def _extract_baseline_from_git(root: Path, version: str) -> Optional[Path]:
197
197
  check=False,
198
198
  )
199
199
  if result.returncode != 0 or not result.stdout:
200
+ missing.append(name)
200
201
  continue
201
202
  dest.write_bytes(result.stdout)
202
- extracted += 1
203
- if extracted < len(SHOTS):
203
+ # A screen the baseline release never captured is a screen introduced by
204
+ # the current release (the comparison loop reports it as "new" and does
205
+ # not gate it). Only a baseline with no core capture at all is broken.
206
+ if "01-login.png" in missing:
204
207
  return None
208
+ if missing:
209
+ print(
210
+ f"pixel delta: baseline v{version} lacks {len(missing)} screen(s) "
211
+ f"introduced later: {', '.join(missing)}"
212
+ )
205
213
  return cache
206
214
 
207
215
 
@@ -450,6 +458,28 @@ def main(argv: Optional[List[str]] = None) -> int:
450
458
  print(f"{name:<32} {'—':>8} {'—':>6} {'—':>6} FAIL")
451
459
  continue
452
460
  if not base.is_file():
461
+ # No baseline image: either the screen is new this release
462
+ # (legitimate — nothing to diff against, report and move on) or a
463
+ # claim names a screen the baseline should have had (a typo — the
464
+ # claim gate must fail loudly rather than skip it).
465
+ if claims is not None and name not in claims:
466
+ print(f"{name:<32} {'new':>8} {'—':>6} {'—':>6} {'—':>5} (no baseline)")
467
+ results.append(
468
+ {
469
+ "file": name,
470
+ "pixel_pct": None,
471
+ "height_current": None,
472
+ "height_baseline": None,
473
+ "height_delta": None,
474
+ "min_pct": None,
475
+ "living_brain": name in LIVING_BRAIN,
476
+ "region": None,
477
+ "claimed": False,
478
+ "moved": None,
479
+ "pass": True,
480
+ }
481
+ )
482
+ continue
453
483
  failures.append(f"{name}: missing in baseline")
454
484
  print(f"{name:<32} {'—':>8} {'—':>6} {'—':>6} FAIL")
455
485
  continue
@@ -34,6 +34,7 @@ const LOCALIZED = [
34
34
  "chat",
35
35
  "chat_history",
36
36
  "chat_intents",
37
+ "chronicle",
37
38
  "features",
38
39
  "knowledge_graph",
39
40
  "local_files",