ltcai 9.0.0 → 9.1.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 (187) hide show
  1. package/README.md +80 -46
  2. package/auto_setup.py +7 -853
  3. package/desktop/electron/README.md +9 -0
  4. package/desktop/electron/main.cjs +5 -3
  5. package/docs/CHANGELOG.md +146 -2
  6. package/docs/COMMUNITY_AND_PLUGINS.md +3 -2
  7. package/docs/DEVELOPMENT.md +53 -14
  8. package/docs/LEGACY_COMPATIBILITY.md +4 -4
  9. package/docs/ONBOARDING.md +10 -2
  10. package/docs/OPERATIONS.md +8 -4
  11. package/docs/PRODUCT_DIRECTION_REVIEW.md +4 -0
  12. package/docs/TRUST_MODEL.md +5 -2
  13. package/docs/WHY_LATTICE.md +15 -10
  14. package/docs/WORKFLOW_DESIGNER.md +22 -0
  15. package/docs/kg-schema.md +13 -2
  16. package/docs/mcp-tools.md +17 -6
  17. package/docs/privacy.md +19 -3
  18. package/docs/public-deploy.md +32 -3
  19. package/docs/security-model.md +46 -11
  20. package/lattice_brain/__init__.py +1 -1
  21. package/lattice_brain/archive.py +1 -6
  22. package/lattice_brain/context.py +66 -9
  23. package/lattice_brain/graph/_kg_fsutil.py +1 -5
  24. package/lattice_brain/graph/discovery_index.py +174 -20
  25. package/lattice_brain/graph/documents.py +44 -9
  26. package/lattice_brain/graph/ingest.py +47 -20
  27. package/lattice_brain/graph/provenance.py +13 -6
  28. package/lattice_brain/graph/retrieval.py +140 -39
  29. package/lattice_brain/graph/retrieval_docgen.py +37 -4
  30. package/lattice_brain/ingestion.py +4 -7
  31. package/lattice_brain/portability.py +28 -14
  32. package/lattice_brain/runtime/agent_runtime.py +5 -9
  33. package/lattice_brain/runtime/hooks.py +30 -13
  34. package/lattice_brain/runtime/multi_agent.py +27 -8
  35. package/lattice_brain/runtime/statuses.py +10 -0
  36. package/lattice_brain/utils.py +20 -2
  37. package/lattice_brain/workflow.py +1 -5
  38. package/latticeai/__init__.py +1 -1
  39. package/latticeai/api/admin.py +1 -1
  40. package/latticeai/api/agent_registry.py +10 -8
  41. package/latticeai/api/agents.py +22 -3
  42. package/latticeai/api/auth.py +78 -16
  43. package/latticeai/api/browser.py +303 -34
  44. package/latticeai/api/chat.py +320 -850
  45. package/latticeai/api/chat_agent_http.py +356 -0
  46. package/latticeai/api/chat_contracts.py +54 -0
  47. package/latticeai/api/chat_documents.py +256 -0
  48. package/latticeai/api/chat_helpers.py +24 -1
  49. package/latticeai/api/chat_history.py +99 -0
  50. package/latticeai/api/chat_intents.py +302 -0
  51. package/latticeai/api/chat_stream.py +115 -0
  52. package/latticeai/api/computer_use.py +62 -9
  53. package/latticeai/api/health.py +9 -3
  54. package/latticeai/api/hooks.py +17 -6
  55. package/latticeai/api/knowledge_graph.py +78 -14
  56. package/latticeai/api/local_files.py +7 -2
  57. package/latticeai/api/mcp.py +102 -23
  58. package/latticeai/api/models.py +72 -33
  59. package/latticeai/api/network.py +5 -5
  60. package/latticeai/api/permissions.py +67 -26
  61. package/latticeai/api/plugins.py +21 -7
  62. package/latticeai/api/portability.py +5 -5
  63. package/latticeai/api/realtime.py +33 -5
  64. package/latticeai/api/setup.py +2 -2
  65. package/latticeai/api/static_routes.py +123 -24
  66. package/latticeai/api/tools.py +96 -14
  67. package/latticeai/api/workflow_designer.py +37 -0
  68. package/latticeai/api/workspace.py +2 -1
  69. package/latticeai/app_factory.py +110 -52
  70. package/latticeai/core/agent.py +19 -9
  71. package/latticeai/core/agent_registry.py +1 -5
  72. package/latticeai/core/config.py +23 -3
  73. package/latticeai/core/context_builder.py +21 -3
  74. package/latticeai/core/invitations.py +1 -4
  75. package/latticeai/core/io_utils.py +9 -1
  76. package/latticeai/core/legacy_compatibility.py +24 -3
  77. package/latticeai/core/marketplace.py +1 -1
  78. package/latticeai/core/plugins.py +15 -0
  79. package/latticeai/core/policy.py +1 -1
  80. package/latticeai/core/realtime.py +38 -15
  81. package/latticeai/core/sessions.py +7 -2
  82. package/latticeai/core/timeutil.py +10 -0
  83. package/latticeai/core/tool_registry.py +90 -18
  84. package/latticeai/core/users.py +1 -5
  85. package/latticeai/core/workspace_graph_trace.py +31 -5
  86. package/latticeai/core/workspace_memory.py +3 -1
  87. package/latticeai/core/workspace_os.py +18 -7
  88. package/latticeai/core/workspace_os_utils.py +0 -5
  89. package/latticeai/core/workspace_permissions.py +2 -1
  90. package/latticeai/core/workspace_plugins.py +1 -1
  91. package/latticeai/core/workspace_runs.py +14 -5
  92. package/latticeai/core/workspace_skills.py +1 -1
  93. package/latticeai/core/workspace_snapshots.py +2 -1
  94. package/latticeai/core/workspace_timeline.py +2 -1
  95. package/latticeai/integrations/telegram_bot.py +94 -43
  96. package/latticeai/models/router.py +130 -36
  97. package/latticeai/runtime/access_runtime.py +62 -4
  98. package/latticeai/runtime/automation_runtime.py +13 -7
  99. package/latticeai/runtime/brain_runtime.py +19 -7
  100. package/latticeai/runtime/chat_wiring.py +2 -0
  101. package/latticeai/runtime/config_runtime.py +58 -26
  102. package/latticeai/runtime/context_runtime.py +16 -4
  103. package/latticeai/runtime/hooks_runtime.py +1 -1
  104. package/latticeai/runtime/model_wiring.py +33 -5
  105. package/latticeai/runtime/namespace_runtime.py +62 -72
  106. package/latticeai/runtime/platform_runtime_wiring.py +4 -3
  107. package/latticeai/runtime/review_wiring.py +1 -1
  108. package/latticeai/runtime/router_registration.py +34 -1
  109. package/latticeai/runtime/security_runtime.py +110 -13
  110. package/latticeai/runtime/stages.py +27 -0
  111. package/latticeai/server_app.py +5 -1
  112. package/latticeai/services/architecture_readiness.py +74 -5
  113. package/latticeai/services/brain_automation.py +3 -26
  114. package/latticeai/services/chat_service.py +205 -15
  115. package/latticeai/services/local_knowledge.py +423 -0
  116. package/latticeai/services/memory_service.py +55 -21
  117. package/latticeai/services/model_engines.py +48 -33
  118. package/latticeai/services/model_errors.py +17 -0
  119. package/latticeai/services/model_loading.py +28 -25
  120. package/latticeai/services/model_runtime.py +228 -162
  121. package/latticeai/services/p_reinforce.py +22 -3
  122. package/latticeai/services/platform_runtime.py +83 -23
  123. package/latticeai/services/product_readiness.py +19 -17
  124. package/latticeai/services/review_queue.py +12 -0
  125. package/latticeai/services/router_context.py +1 -0
  126. package/latticeai/services/run_executor.py +4 -9
  127. package/latticeai/services/search_service.py +203 -28
  128. package/latticeai/services/tool_dispatch.py +28 -5
  129. package/latticeai/services/triggers.py +53 -4
  130. package/latticeai/services/upload_service.py +13 -2
  131. package/latticeai/setup/__init__.py +25 -0
  132. package/latticeai/setup/auto_setup.py +857 -0
  133. package/latticeai/setup/wizard.py +1264 -0
  134. package/latticeai/tools/__init__.py +278 -0
  135. package/{tools → latticeai/tools}/commands.py +67 -7
  136. package/{tools → latticeai/tools}/computer.py +1 -1
  137. package/{tools → latticeai/tools}/documents.py +1 -1
  138. package/{tools → latticeai/tools}/filesystem.py +3 -3
  139. package/latticeai/tools/knowledge.py +178 -0
  140. package/{tools → latticeai/tools}/local_files.py +1 -1
  141. package/{tools → latticeai/tools}/network.py +0 -1
  142. package/local_knowledge_api.py +4 -342
  143. package/package.json +22 -2
  144. package/scripts/brain_quality_eval.py +3 -1
  145. package/scripts/bump_version.py +3 -0
  146. package/scripts/capture_release_evidence.mjs +180 -0
  147. package/scripts/check_current_release_docs.mjs +142 -0
  148. package/scripts/check_i18n_literals.mjs +107 -31
  149. package/scripts/check_openapi_drift.mjs +56 -0
  150. package/scripts/export_openapi.py +67 -7
  151. package/scripts/i18n_literal_allowlist.json +22 -24
  152. package/scripts/run_integration_tests.mjs +72 -10
  153. package/scripts/validate_release_artifacts.py +49 -7
  154. package/scripts/wheel_smoke.py +5 -0
  155. package/setup_wizard.py +3 -1260
  156. package/src-tauri/Cargo.lock +1 -1
  157. package/src-tauri/Cargo.toml +1 -1
  158. package/src-tauri/tauri.conf.json +1 -1
  159. package/static/app/asset-manifest.json +11 -11
  160. package/static/app/assets/Act-Bzz0bUyW.js +1 -0
  161. package/static/app/assets/Brain-Dj2J20YA.js +321 -0
  162. package/static/app/assets/Capture-CqlEl1Ga.js +1 -0
  163. package/static/app/assets/Library-B03FP1Yx.js +1 -0
  164. package/static/app/assets/System-80lHW0Ux.js +1 -0
  165. package/static/app/assets/index-BiMofBTM.js +17 -0
  166. package/static/app/assets/index-Bmx9rzTc.css +2 -0
  167. package/static/app/assets/primitives-Q1A96_7v.js +1 -0
  168. package/static/app/assets/textarea-D13RtnTo.js +1 -0
  169. package/static/app/index.html +2 -2
  170. package/static/sw.js +1 -1
  171. package/tools/__init__.py +21 -269
  172. package/docs/CODE_REVIEW_2026-07-06.md +0 -764
  173. package/latticeai/runtime/app_context_runtime.py +0 -13
  174. package/latticeai/runtime/tail_wiring.py +0 -21
  175. package/scripts/com.pts.claudecode.discord.plist +0 -31
  176. package/scripts/pts-claudecode-discord-bridge.mjs +0 -207
  177. package/scripts/start-pts-claudecode-discord.sh +0 -51
  178. package/static/app/assets/Act-21lIXx2E.js +0 -1
  179. package/static/app/assets/Brain-BqUd5UJJ.js +0 -321
  180. package/static/app/assets/Capture-BA7Z2Q1u.js +0 -1
  181. package/static/app/assets/Library-bFMtyni3.js +0 -1
  182. package/static/app/assets/System-K6krGCqn.js +0 -1
  183. package/static/app/assets/index-C4R3ws30.js +0 -17
  184. package/static/app/assets/index-ChSeOB02.css +0 -2
  185. package/static/app/assets/primitives-sQU3it5I.js +0 -1
  186. package/static/app/assets/textarea-DK3Fd_lR.js +0 -1
  187. package/tools/knowledge.py +0 -95
@@ -0,0 +1,142 @@
1
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import process from "node:process";
4
+
5
+ const root = process.cwd();
6
+ const pkg = JSON.parse(readFileSync(path.join(root, "package.json"), "utf8"));
7
+ const version = pkg.version;
8
+ const releaseDir = `output/release/v${version}`;
9
+ const releaseTheme = "Code Review Completion & Fail-Closed Runtime";
10
+ const title = `${version} — ${releaseTheme}`;
11
+ const escapedVersion = version.replaceAll(".", "\\.");
12
+
13
+ const currentReleaseFiles = [
14
+ "README.md",
15
+ "ARCHITECTURE.md",
16
+ "FEATURE_STATUS.md",
17
+ "PRIVACY.md",
18
+ "SECURITY.md",
19
+ "docs/COMMUNITY_AND_PLUGINS.md",
20
+ "docs/DEVELOPMENT.md",
21
+ "docs/ONBOARDING.md",
22
+ "docs/TRUST_MODEL.md",
23
+ "docs/WHY_LATTICE.md",
24
+ "docs/kg-schema.md",
25
+ "vscode-extension/README.md",
26
+ ];
27
+
28
+ const errors = [];
29
+
30
+ function read(rel) {
31
+ return readFileSync(path.join(root, rel), "utf8");
32
+ }
33
+
34
+ function requireIncludes(rel, needle) {
35
+ const text = read(rel);
36
+ if (!text.includes(needle)) {
37
+ errors.push(`${rel}: missing ${JSON.stringify(needle)}`);
38
+ }
39
+ }
40
+
41
+ function assertNoCurrentDrift(rel) {
42
+ const text = read(rel);
43
+ const stale = text.match(
44
+ new RegExp(`Current release:\\s+\\*\\*(?!${escapedVersion}\\b)[^*]+\\*\\*`, "i"),
45
+ );
46
+ if (stale) {
47
+ errors.push(`${rel}: stale current-release marker ${JSON.stringify(stale[0])}`);
48
+ }
49
+ for (const match of text.matchAll(/Latest\b[^\n]*\b(\d+\.\d+\.\d+)\b/gi)) {
50
+ if (match[1] !== version) {
51
+ errors.push(`${rel}: stale Latest version reference ${match[1]}`);
52
+ }
53
+ }
54
+ if (new RegExp(`current release is\\s+\\*\\*(?!${escapedVersion}\\b)`, "i").test(text)) {
55
+ errors.push(`${rel}: stale README current release sentence`);
56
+ }
57
+ }
58
+
59
+ for (const rel of currentReleaseFiles) {
60
+ assertNoCurrentDrift(rel);
61
+ }
62
+
63
+ for (const rel of [
64
+ "README.md",
65
+ "ARCHITECTURE.md",
66
+ "FEATURE_STATUS.md",
67
+ "PRIVACY.md",
68
+ "SECURITY.md",
69
+ ]) {
70
+ requireIncludes(rel, version);
71
+ }
72
+
73
+ requireIncludes("README.md", `The current release is **${title}**`);
74
+ requireIncludes("README.md", `![v${version} Living Brain walkthrough]`);
75
+ requireIncludes("RELEASE.md", `## v${version} — ${releaseTheme}`);
76
+ requireIncludes("docs/CHANGELOG.md", `## [${version}]`);
77
+ requireIncludes("RELEASE_NOTES.md", `[v${version} - ${releaseTheme}]`);
78
+ requireIncludes("CHANGELOG.md", "starts at v8.0.0");
79
+ requireIncludes("RELEASE_NOTES.md", `8.0.0 through ${version}`);
80
+ requireIncludes("SECURITY.md", `${version.split(".").slice(0, 2).join(".")}.x (latest)`);
81
+
82
+ for (const rel of ["README.md", "RELEASE.md", "docs/CHANGELOG.md", "RELEASE_NOTES.md"]) {
83
+ const text = read(rel);
84
+ for (const forbidden of ["RELEASE_NOTES_v7", "output/release/v7", "## v7.", "## [7."]) {
85
+ if (text.includes(forbidden)) {
86
+ errors.push(`${rel}: forbidden pre-8.0 release-history reference ${forbidden}`);
87
+ }
88
+ }
89
+ }
90
+
91
+ const readme = read("README.md");
92
+ const mediaRefs = [...readme.matchAll(/\]\((output\/release\/v[^)]+)\)/g)].map((m) => m[1]);
93
+ if (mediaRefs.length === 0) {
94
+ errors.push("README.md: no release evidence media references found");
95
+ }
96
+ for (const ref of mediaRefs) {
97
+ if (!ref.startsWith(`${releaseDir}/`)) {
98
+ errors.push(`README.md: release media must point at ${releaseDir}, found ${ref}`);
99
+ }
100
+ if (!existsSync(path.join(root, ref))) {
101
+ errors.push(`README.md: missing release media target ${ref}`);
102
+ }
103
+ }
104
+
105
+ const evidenceRoot = path.join(root, releaseDir);
106
+ if (!existsSync(evidenceRoot)) {
107
+ errors.push(`missing current release evidence directory ${releaseDir}`);
108
+ } else {
109
+ for (const rel of [
110
+ "SCREENSHOT_INDEX.md",
111
+ "screenshots/01-login.png",
112
+ "screenshots/02-recommended-models.png",
113
+ "screenshots/03-install-load-progress.png",
114
+ "screenshots/04-brain-chat-home.png",
115
+ "screenshots/12-review-center.png",
116
+ `gifs/v${version}-living-brain-walkthrough.gif`,
117
+ ]) {
118
+ const target = path.join(evidenceRoot, rel);
119
+ if (!existsSync(target)) {
120
+ errors.push(`missing current release evidence file ${releaseDir}/${rel}`);
121
+ }
122
+ }
123
+ }
124
+
125
+ const releaseRoot = path.join(root, "output", "release");
126
+ if (existsSync(releaseRoot)) {
127
+ for (const entry of readdirSync(releaseRoot)) {
128
+ if (/^v7\./.test(entry)) {
129
+ errors.push(`pre-8.0 release evidence should not be tracked: output/release/${entry}`);
130
+ }
131
+ }
132
+ }
133
+
134
+ if (errors.length) {
135
+ console.error("Current release documentation check failed:");
136
+ for (const error of errors) {
137
+ console.error(`- ${error}`);
138
+ }
139
+ process.exit(1);
140
+ }
141
+
142
+ console.log(`Current release documentation is synchronized for ${version}.`);
@@ -1,16 +1,38 @@
1
1
  #!/usr/bin/env node
2
2
  import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
3
3
  import { join, relative } from "node:path";
4
+ import ts from "typescript";
4
5
 
5
6
  const repo = join(import.meta.dirname, "..");
6
- const roots = [
7
- join(repo, "frontend", "src"),
8
- ];
7
+ const roots = [join(repo, "frontend", "src")];
9
8
  const allowlistPath = join(repo, "scripts", "i18n_literal_allowlist.json");
10
9
  const allowlist = existsSync(allowlistPath)
11
10
  ? JSON.parse(readFileSync(allowlistPath, "utf8"))
12
11
  : {};
13
12
 
13
+ const localizedAttributes = new Set([
14
+ "alt",
15
+ "aria-label",
16
+ "description",
17
+ "detail",
18
+ "empty",
19
+ "label",
20
+ "placeholder",
21
+ "successLabel",
22
+ "title",
23
+ "tooltip",
24
+ ]);
25
+ const localizedProperties = new Set([
26
+ "description",
27
+ "detail",
28
+ "empty",
29
+ "label",
30
+ "placeholder",
31
+ "successLabel",
32
+ "title",
33
+ "tooltip",
34
+ ]);
35
+
14
36
  function walk(dir) {
15
37
  if (!existsSync(dir)) return [];
16
38
  const out = [];
@@ -18,41 +40,95 @@ function walk(dir) {
18
40
  const path = join(dir, name);
19
41
  const stat = statSync(path);
20
42
  if (stat.isDirectory()) out.push(...walk(path));
21
- else if ((name.endsWith(".tsx") || name.endsWith(".ts")) && name !== "openapi.ts") {
22
- out.push(path);
23
- }
43
+ else if ((name.endsWith(".tsx") || name.endsWith(".ts")) && name !== "openapi.ts" && !/\.test\.[tj]sx?$/.test(name)) out.push(path);
24
44
  }
25
45
  return out;
26
46
  }
27
47
 
28
- const rawLocalizedProps = /\b(?:aria-label|placeholder|title)=["'][^"'{]*[A-Za-z][^"']*["']/g;
29
- const rawJsxText = />\s*([A-Z][A-Za-z0-9][^<>{}\n]{2,})\s*</g;
30
- const rawComponentCopy = /\b(?:title|detail|description|successLabel|empty)=["'][^"'{]*[A-Za-z][^"']*["']/g;
48
+ function propertyName(node) {
49
+ if (ts.isIdentifier(node) || ts.isStringLiteral(node)) return node.text;
50
+ return "";
51
+ }
52
+
53
+ function literalText(node) {
54
+ if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) return node.text.trim();
55
+ if (ts.isTemplateExpression(node)) {
56
+ return [node.head.text, ...node.templateSpans.map((span) => span.literal.text)].join(" ").trim();
57
+ }
58
+ return "";
59
+ }
60
+
61
+ function renderedLiterals(expression) {
62
+ if (!expression) return [];
63
+ const direct = literalText(expression);
64
+ if (direct) return [direct];
65
+ if (ts.isParenthesizedExpression(expression)) return renderedLiterals(expression.expression);
66
+ if (ts.isConditionalExpression(expression)) {
67
+ return [...renderedLiterals(expression.whenTrue), ...renderedLiterals(expression.whenFalse)];
68
+ }
69
+ if (ts.isBinaryExpression(expression) && [ts.SyntaxKind.AmpersandAmpersandToken, ts.SyntaxKind.BarBarToken, ts.SyntaxKind.QuestionQuestionToken].includes(expression.operatorToken.kind)) {
70
+ return renderedLiterals(expression.right);
71
+ }
72
+ return [];
73
+ }
74
+
75
+ function looksLocalized(text) {
76
+ return /[A-Za-z\u3131-\uD79D]/u.test(text);
77
+ }
78
+
79
+ function inspect(file) {
80
+ const source = readFileSync(file, "utf8");
81
+ const sourceFile = ts.createSourceFile(
82
+ file,
83
+ source,
84
+ ts.ScriptTarget.Latest,
85
+ true,
86
+ file.endsWith(".tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS,
87
+ );
88
+ const findings = [];
89
+ const add = (kind, node, value) => {
90
+ const normalized = String(value || "").trim();
91
+ if (!looksLocalized(normalized)) return;
92
+ const { line } = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
93
+ findings.push({ id: `${kind}: ${normalized}`, line: line + 1 });
94
+ };
95
+
96
+ const visit = (node) => {
97
+ if (ts.isJsxText(node)) add("JSX text", node, node.getText(sourceFile));
98
+
99
+ if (ts.isJsxAttribute(node) && localizedAttributes.has(node.name.text)) {
100
+ if (node.initializer && ts.isStringLiteral(node.initializer)) add(`JSX ${node.name.text}`, node, node.initializer.text);
101
+ if (node.initializer && ts.isJsxExpression(node.initializer)) {
102
+ for (const value of renderedLiterals(node.initializer.expression)) add(`JSX ${node.name.text}`, node, value);
103
+ }
104
+ }
105
+
106
+ if (ts.isJsxExpression(node) && !ts.isJsxAttribute(node.parent)) {
107
+ for (const value of renderedLiterals(node.expression)) add("JSX expression", node, value);
108
+ }
109
+
110
+ if (file.endsWith(".tsx") && ts.isPropertyAssignment(node) && localizedProperties.has(propertyName(node.name))) {
111
+ for (const value of renderedLiterals(node.initializer)) add(`property ${propertyName(node.name)}`, node, value);
112
+ }
113
+
114
+ ts.forEachChild(node, visit);
115
+ };
116
+ visit(sourceFile);
117
+ return findings;
118
+ }
119
+
31
120
  let failures = 0;
32
121
  let allowed = 0;
33
-
34
122
  for (const file of roots.flatMap(walk)) {
35
- const text = readFileSync(file, "utf8");
36
- const matches = [
37
- ...(text.match(rawLocalizedProps) || []),
38
- ...(text.match(rawComponentCopy) || []),
39
- ];
40
- for (const match of text.matchAll(rawJsxText)) {
41
- const literal = match[1].trim();
42
- if (!literal || /^[A-Z][a-z]*$/.test(literal) || /^Lattice\b/.test(literal)) continue;
43
- matches.push(`JSX text: ${literal}`);
44
- }
45
- if (!matches.length) continue;
46
123
  const rel = relative(repo, file);
47
- const budget = Number(allowlist[rel]?.maxFindings || 0);
48
- if (budget >= matches.length) {
49
- allowed += matches.length;
50
- continue;
51
- }
52
- const newMatches = matches.slice(budget);
53
- failures += newMatches.length;
54
- for (const match of newMatches) {
55
- console.error(`${rel}: hardcoded localized prop: ${match}`);
124
+ const accepted = new Set(allowlist[rel]?.findings || []);
125
+ for (const finding of inspect(file)) {
126
+ if (accepted.has(finding.id)) {
127
+ allowed += 1;
128
+ continue;
129
+ }
130
+ failures += 1;
131
+ console.error(`${rel}:${finding.line}: hardcoded localized copy: ${finding.id}`);
56
132
  }
57
133
  }
58
134
 
@@ -61,4 +137,4 @@ if (failures) {
61
137
  process.exit(1);
62
138
  }
63
139
 
64
- console.log(`i18n literal check: localized props use translation keys (${allowed} allowlisted legacy literal(s))`);
140
+ console.log(`i18n literal check: localized copy uses translation keys (${allowed} allowlisted technical literal(s))`);
@@ -0,0 +1,56 @@
1
+ #!/usr/bin/env node
2
+ import { spawnSync } from "node:child_process";
3
+ import { mkdtempSync, readFileSync, rmSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { dirname, join, resolve } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+
8
+ const repo = resolve(dirname(fileURLToPath(import.meta.url)), "..");
9
+ const tempRoot = mkdtempSync(join(tmpdir(), "ltcai-openapi-check-"));
10
+ const generatedSchema = join(tempRoot, "openapi.json");
11
+ const generatedTypes = join(tempRoot, "openapi.ts");
12
+
13
+ function runNode(script, args) {
14
+ const result = spawnSync(process.execPath, [script, ...args], {
15
+ cwd: repo,
16
+ env: process.env,
17
+ stdio: "inherit",
18
+ });
19
+ if (result.error) throw result.error;
20
+ if (result.status !== 0) {
21
+ throw new Error(`${script} exited with status ${result.status ?? "unknown"}`);
22
+ }
23
+ }
24
+
25
+ function matches(left, right) {
26
+ return readFileSync(left).equals(readFileSync(right));
27
+ }
28
+
29
+ try {
30
+ runNode(join(repo, "scripts", "run_python.mjs"), [
31
+ "scripts/export_openapi.py",
32
+ generatedSchema,
33
+ ]);
34
+ runNode(join(repo, "node_modules", "openapi-typescript", "bin", "cli.js"), [
35
+ generatedSchema,
36
+ "-o",
37
+ generatedTypes,
38
+ ]);
39
+
40
+ const committedSchema = join(repo, "frontend", "openapi.json");
41
+ const committedTypes = join(repo, "frontend", "src", "api", "openapi.ts");
42
+ const drift = [];
43
+ if (!matches(generatedSchema, committedSchema)) drift.push("frontend/openapi.json");
44
+ if (!matches(generatedTypes, committedTypes)) drift.push("frontend/src/api/openapi.ts");
45
+
46
+ if (drift.length) {
47
+ console.error("OpenAPI generated artifacts are stale:");
48
+ for (const path of drift) console.error(`- ${path}`);
49
+ console.error("Run `npm run frontend:openapi` and commit both generated files.");
50
+ process.exitCode = 1;
51
+ } else {
52
+ console.log("OpenAPI schema and TypeScript client are synchronized.");
53
+ }
54
+ } finally {
55
+ rmSync(tempRoot, { recursive: true, force: true });
56
+ }
@@ -6,22 +6,82 @@ from __future__ import annotations
6
6
  import json
7
7
  import os
8
8
  import sys
9
+ import tempfile
10
+ from contextlib import contextmanager
9
11
  from pathlib import Path
12
+ from typing import Iterator, Mapping
10
13
 
11
14
  REPO = Path(__file__).resolve().parents[1]
12
15
  sys.path.insert(0, str(REPO))
13
16
 
14
17
 
15
- def main() -> int:
16
- from latticeai.app_factory import create_app
18
+ @contextmanager
19
+ def isolated_runtime_environment(root: Path) -> Iterator[Mapping[str, str]]:
20
+ """Point every runtime/user-state path at a disposable directory.
21
+
22
+ OpenAPI generation constructs the real application so route wiring stays
23
+ authoritative. That construction must not inspect or mutate the caller's
24
+ HOME, Brain, keyring, agent workspace, or local Lattice data.
25
+ """
26
+
27
+ home = root / "home"
28
+ paths = {
29
+ "HOME": home,
30
+ "USERPROFILE": home,
31
+ "XDG_CACHE_HOME": root / "cache",
32
+ "XDG_CONFIG_HOME": root / "config",
33
+ "XDG_DATA_HOME": root / "share",
34
+ "TMPDIR": root / "tmp",
35
+ "TEMP": root / "tmp",
36
+ "TMP": root / "tmp",
37
+ "LATTICEAI_DATA_DIR": root / "data",
38
+ "LATTICEAI_BRAIN_DIR": root / "brain",
39
+ "LATTICEAI_AGENT_ROOT": root / "agent-workspace",
40
+ "LATTICEAI_OBSIDIAN_VAULT_DIR": root / "vault",
41
+ "LATTICEAI_STATIC_DIR": root / "static",
42
+ }
43
+ for path in paths.values():
44
+ path.mkdir(parents=True, exist_ok=True)
45
+
46
+ overrides = {
47
+ **{key: str(value) for key, value in paths.items()},
48
+ "LATTICEAI_MODE": "local",
49
+ "LATTICEAI_STORAGE_ENGINE": "sqlite",
50
+ "LATTICEAI_POSTGRES_DSN": "",
51
+ "LATTICEAI_REQUIRE_AUTH": "false",
52
+ "LATTICEAI_TUNNEL": "false",
53
+ "LATTICEAI_ENABLE_TELEGRAM": "false",
54
+ "LATTICEAI_AUTOLOAD_MODELS": "false",
55
+ "LATTICEAI_ALLOW_MODEL_DOWNLOADS": "false",
56
+ "LATTICEAI_AUTO_READ_CHAT_PATHS": "false",
57
+ "LATTICEAI_DISCORD_PERMISSION_WEBHOOK": "",
58
+ "LATTICEAI_DISCORD_BOT_TOKEN": "",
59
+ "OIDC_DISCOVERY_URL": "",
60
+ "PYTHON_KEYRING_BACKEND": "keyring.backends.null.Keyring",
61
+ }
62
+ previous = {key: os.environ.get(key) for key in overrides}
63
+ os.environ.update(overrides)
64
+ try:
65
+ yield overrides
66
+ finally:
67
+ for key, value in previous.items():
68
+ if value is None:
69
+ os.environ.pop(key, None)
70
+ else:
71
+ os.environ[key] = value
17
72
 
73
+
74
+ def main() -> int:
18
75
  target = Path(sys.argv[1] if len(sys.argv) > 1 else "frontend/openapi.json")
19
76
  target.parent.mkdir(parents=True, exist_ok=True)
20
- os.environ.setdefault("LATTICEAI_REQUIRE_AUTH", "false")
21
- os.environ.setdefault("LATTICEAI_TUNNEL", "false")
22
- os.environ.setdefault("LATTICEAI_AUTOLOAD_MODELS", "false")
23
- app = create_app()
24
- schema = app.openapi()
77
+ with tempfile.TemporaryDirectory(prefix="ltcai-openapi-") as temp_dir:
78
+ with isolated_runtime_environment(Path(temp_dir)):
79
+ # Import only after isolation is active in case a future dependency
80
+ # starts reading environment or user paths during module import.
81
+ from latticeai.app_factory import create_app
82
+
83
+ app = create_app()
84
+ schema = app.openapi()
25
85
  target.write_text(json.dumps(schema, sort_keys=True, indent=2) + "\n", encoding="utf-8")
26
86
  print(f"wrote {target} with {len(schema.get('paths', {}))} paths")
27
87
  return 0
@@ -1,34 +1,32 @@
1
1
  {
2
2
  "frontend/src/App.tsx": {
3
- "maxFindings": 1,
4
- "reason": "legacy shell aria label pending translation-key migration"
3
+ "findings": [
4
+ "JSX text: Lattice"
5
+ ],
6
+ "reason": "Product brand name"
5
7
  },
6
- "frontend/src/components/LivingBrain.tsx": {
7
- "maxFindings": 1,
8
- "reason": "legacy visual component aria label pending translation-key migration"
9
- },
10
- "frontend/src/components/primitives.tsx": {
11
- "maxFindings": 1,
12
- "reason": "shared empty-state fallback pending translation-key migration"
13
- },
14
- "frontend/src/pages/Act.tsx": {
15
- "maxFindings": 41,
16
- "reason": "large legacy automation page pending incremental copy migration"
17
- },
18
- "frontend/src/pages/Brain.tsx": {
19
- "maxFindings": 25,
20
- "reason": "large legacy memory page pending incremental copy migration"
8
+ "frontend/src/features/brain/MessageMarkdown.tsx": {
9
+ "findings": [
10
+ "JSX expression: text"
11
+ ],
12
+ "reason": "Technical fallback language identifier for untyped code blocks"
21
13
  },
22
14
  "frontend/src/pages/Capture.tsx": {
23
- "maxFindings": 2,
24
- "reason": "technical type-name literals are intentionally preserved for now"
15
+ "findings": [
16
+ "JSX text: KB ·"
17
+ ],
18
+ "reason": "International technical storage unit"
25
19
  },
26
20
  "frontend/src/pages/Library.tsx": {
27
- "maxFindings": 21,
28
- "reason": "large legacy model/plugin page pending incremental copy migration"
21
+ "findings": [
22
+ "JSX text: HF"
23
+ ],
24
+ "reason": "Hugging Face verification abbreviation"
29
25
  },
30
- "frontend/src/pages/System.tsx": {
31
- "maxFindings": 118,
32
- "reason": "large legacy system/admin page pending incremental copy migration"
26
+ "frontend/src/pages/brain/CytoscapeGraph.tsx": {
27
+ "findings": [
28
+ "property label: data(displayLabel)"
29
+ ],
30
+ "reason": "Cytoscape data-expression syntax, not user-facing copy"
33
31
  }
34
32
  }
@@ -1,21 +1,87 @@
1
1
  #!/usr/bin/env node
2
2
  import { spawn } from "node:child_process";
3
- import { existsSync } from "node:fs";
3
+ import { existsSync, mkdirSync, mkdtempSync, rmSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
4
5
  import { join } from "node:path";
5
6
 
6
7
  const host = process.env.LTCAI_TEST_HOST || "127.0.0.1";
8
+ if (!new Set(["127.0.0.1", "localhost"]).has(host)) {
9
+ throw new Error(`LTCAI_TEST_HOST must be loopback, received: ${host}`);
10
+ }
7
11
  const port = process.env.LTCAI_TEST_PORT || "8899";
8
- const baseUrl = process.env.LTCAI_TEST_BASE_URL || `http://${host}:${port}`;
12
+ if (!/^\d+$/.test(port) || Number(port) < 1 || Number(port) > 65535) {
13
+ throw new Error(`LTCAI_TEST_PORT must be an integer from 1 to 65535, received: ${port}`);
14
+ }
15
+ // This runner owns the server lifecycle; never redirect its tests to a caller-
16
+ // supplied live server through LTCAI_TEST_BASE_URL.
17
+ const baseUrl = `http://${host}:${port}`;
9
18
  const venvPython = join(process.cwd(), ".venv", "bin", "python");
10
19
  const python = process.env.PYTHON || (existsSync(venvPython) ? venvPython : "python");
11
20
 
21
+ // Integration tests must never discover or mutate the developer's real HOME,
22
+ // ~/.ltcai, Brain vault, or agent workspace. Keep every user-state path under
23
+ // one disposable root and pass the exact same environment to the server and
24
+ // pytest process.
25
+ const sandboxRoot = mkdtempSync(join(tmpdir(), "ltcai-integration-"));
26
+ const sandbox = {
27
+ home: join(sandboxRoot, "home"),
28
+ data: join(sandboxRoot, "data"),
29
+ brain: join(sandboxRoot, "brain"),
30
+ agent: join(sandboxRoot, "agent-workspace"),
31
+ vault: join(sandboxRoot, "vault"),
32
+ cache: join(sandboxRoot, "cache"),
33
+ config: join(sandboxRoot, "config"),
34
+ tmp: join(sandboxRoot, "tmp"),
35
+ };
36
+ for (const path of Object.values(sandbox)) mkdirSync(path, { recursive: true });
37
+
38
+ const isolatedEnv = {
39
+ ...process.env,
40
+ HOME: sandbox.home,
41
+ USERPROFILE: sandbox.home,
42
+ XDG_CACHE_HOME: sandbox.cache,
43
+ XDG_CONFIG_HOME: sandbox.config,
44
+ XDG_DATA_HOME: join(sandboxRoot, "share"),
45
+ TMPDIR: sandbox.tmp,
46
+ TEMP: sandbox.tmp,
47
+ TMP: sandbox.tmp,
48
+ LATTICEAI_MODE: "local",
49
+ LATTICEAI_HOST: host,
50
+ LATTICEAI_PORT: port,
51
+ LATTICEAI_DATA_DIR: sandbox.data,
52
+ LATTICEAI_BRAIN_DIR: sandbox.brain,
53
+ LATTICEAI_AGENT_ROOT: sandbox.agent,
54
+ LATTICEAI_OBSIDIAN_VAULT_DIR: sandbox.vault,
55
+ LATTICEAI_STORAGE_ENGINE: "sqlite",
56
+ LATTICEAI_POSTGRES_DSN: "",
57
+ LATTICEAI_REQUIRE_AUTH: "false",
58
+ LATTICEAI_ENABLE_TELEGRAM: "false",
59
+ LATTICEAI_AUTOLOAD_MODELS: "false",
60
+ LATTICEAI_ALLOW_MODEL_DOWNLOADS: "false",
61
+ LATTICEAI_AUTO_READ_CHAT_PATHS: "false",
62
+ LATTICEAI_TUNNEL: "false",
63
+ LATTICEAI_DISCORD_PERMISSION_WEBHOOK: "",
64
+ LATTICEAI_DISCORD_BOT_TOKEN: "",
65
+ OIDC_DISCOVERY_URL: "",
66
+ PYTHON_KEYRING_BACKEND: "keyring.backends.null.Keyring",
67
+ };
68
+
69
+ let sandboxCleaned = false;
70
+ function cleanupSandbox() {
71
+ if (sandboxCleaned) return;
72
+ sandboxCleaned = true;
73
+ rmSync(sandboxRoot, { recursive: true, force: true });
74
+ }
75
+ process.once("exit", cleanupSandbox);
76
+
12
77
  function run(command, args, options = {}) {
13
- return new Promise((resolve) => {
78
+ return new Promise((resolve, reject) => {
14
79
  const child = spawn(command, args, {
15
80
  stdio: "inherit",
16
- env: { ...process.env, ...options.env },
81
+ env: { ...isolatedEnv, ...options.env },
17
82
  cwd: options.cwd || process.cwd(),
18
83
  });
84
+ child.on("error", reject);
19
85
  child.on("close", (code, signal) => resolve({ code, signal }));
20
86
  });
21
87
  }
@@ -62,12 +128,7 @@ const server = spawn(
62
128
  ["-m", "uvicorn", "server:app", "--host", host, "--port", port],
63
129
  {
64
130
  cwd: process.cwd(),
65
- env: {
66
- ...process.env,
67
- LATTICEAI_MODE: process.env.LATTICEAI_MODE || "test",
68
- LATTICEAI_HOST: host,
69
- LATTICEAI_PORT: port,
70
- },
131
+ env: isolatedEnv,
71
132
  stdio: ["ignore", "pipe", "pipe"],
72
133
  },
73
134
  );
@@ -86,6 +147,7 @@ try {
86
147
  console.error(String(error?.message || error));
87
148
  } finally {
88
149
  await stop(server);
150
+ cleanupSandbox();
89
151
  }
90
152
 
91
153
  process.exit(exitCode);