ltcai 8.9.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.
- package/README.md +81 -58
- package/auto_setup.py +7 -904
- package/desktop/electron/README.md +9 -0
- package/desktop/electron/main.cjs +5 -3
- package/docs/CHANGELOG.md +221 -238
- package/docs/COMMUNITY_AND_PLUGINS.md +3 -2
- package/docs/DEVELOPMENT.md +53 -14
- package/docs/LEGACY_COMPATIBILITY.md +4 -4
- package/docs/ONBOARDING.md +10 -2
- package/docs/OPERATIONS.md +8 -4
- package/docs/PRODUCT_DIRECTION_REVIEW.md +4 -0
- package/docs/ROADMAP_RECOMMENDATIONS.md +61 -36
- package/docs/TRUST_MODEL.md +5 -2
- package/docs/WHY_LATTICE.md +15 -10
- package/docs/WORKFLOW_DESIGNER.md +22 -0
- package/docs/kg-schema.md +13 -2
- package/docs/mcp-tools.md +17 -6
- package/docs/privacy.md +19 -3
- package/docs/public-deploy.md +32 -3
- package/docs/security-model.md +46 -11
- package/lattice_brain/__init__.py +1 -1
- package/lattice_brain/archive.py +4 -14
- package/lattice_brain/context.py +66 -9
- package/lattice_brain/embeddings.py +38 -2
- package/lattice_brain/graph/_kg_common.py +27 -462
- package/lattice_brain/graph/_kg_constants.py +243 -0
- package/lattice_brain/graph/_kg_fsutil.py +293 -0
- package/lattice_brain/graph/discovery.py +0 -948
- package/lattice_brain/graph/discovery_index.py +1126 -0
- package/lattice_brain/graph/documents.py +44 -9
- package/lattice_brain/graph/ingest.py +47 -20
- package/lattice_brain/graph/provenance.py +13 -6
- package/lattice_brain/graph/retrieval.py +141 -610
- package/lattice_brain/graph/retrieval_docgen.py +243 -0
- package/lattice_brain/graph/retrieval_vector.py +460 -0
- package/lattice_brain/graph/store.py +6 -0
- package/lattice_brain/ingestion.py +69 -4
- package/lattice_brain/portability.py +29 -23
- package/lattice_brain/quality.py +98 -4
- package/lattice_brain/runtime/agent_runtime.py +169 -7
- package/lattice_brain/runtime/hooks.py +30 -13
- package/lattice_brain/runtime/multi_agent.py +27 -8
- package/lattice_brain/runtime/statuses.py +10 -0
- package/lattice_brain/utils.py +46 -0
- package/lattice_brain/workflow.py +1 -5
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/admin.py +1 -1
- package/latticeai/api/agent_registry.py +10 -8
- package/latticeai/api/agents.py +22 -3
- package/latticeai/api/auth.py +78 -16
- package/latticeai/api/browser.py +303 -34
- package/latticeai/api/chat.py +355 -952
- package/latticeai/api/chat_agent_http.py +356 -0
- package/latticeai/api/chat_contracts.py +54 -0
- package/latticeai/api/chat_documents.py +256 -0
- package/latticeai/api/chat_helpers.py +250 -0
- package/latticeai/api/chat_history.py +99 -0
- package/latticeai/api/chat_intents.py +302 -0
- package/latticeai/api/chat_stream.py +115 -0
- package/latticeai/api/computer_use.py +211 -40
- package/latticeai/api/health.py +9 -3
- package/latticeai/api/hooks.py +17 -6
- package/latticeai/api/knowledge_graph.py +78 -14
- package/latticeai/api/local_files.py +7 -2
- package/latticeai/api/marketplace.py +11 -0
- package/latticeai/api/mcp.py +102 -23
- package/latticeai/api/models.py +72 -33
- package/latticeai/api/network.py +5 -5
- package/latticeai/api/permissions.py +70 -29
- package/latticeai/api/plugins.py +21 -7
- package/latticeai/api/portability.py +5 -5
- package/latticeai/api/realtime.py +33 -5
- package/latticeai/api/setup.py +2 -2
- package/latticeai/api/static_routes.py +123 -24
- package/latticeai/api/tools.py +97 -14
- package/latticeai/api/workflow_designer.py +37 -0
- package/latticeai/api/workspace.py +2 -1
- package/latticeai/app_factory.py +185 -405
- package/latticeai/core/agent.py +19 -9
- package/latticeai/core/agent_registry.py +1 -5
- package/latticeai/core/config.py +23 -3
- package/latticeai/core/context_builder.py +21 -3
- package/latticeai/core/invitations.py +1 -4
- package/latticeai/core/io_utils.py +45 -0
- package/latticeai/core/legacy_compatibility.py +24 -3
- package/latticeai/core/local_embeddings.py +2 -4
- package/latticeai/core/marketplace.py +33 -2
- package/latticeai/core/mcp_catalog.py +450 -0
- package/latticeai/core/mcp_registry.py +2 -441
- package/latticeai/core/plugins.py +15 -0
- package/latticeai/core/policy.py +1 -1
- package/latticeai/core/realtime.py +38 -15
- package/latticeai/core/sessions.py +7 -2
- package/latticeai/core/timeutil.py +10 -0
- package/latticeai/core/tool_registry.py +90 -18
- package/latticeai/core/users.py +5 -14
- package/latticeai/core/workspace_graph_trace.py +31 -5
- package/latticeai/core/workspace_memory.py +3 -1
- package/latticeai/core/workspace_os.py +18 -7
- package/latticeai/core/workspace_os_utils.py +2 -21
- package/latticeai/core/workspace_permissions.py +2 -1
- package/latticeai/core/workspace_plugins.py +1 -1
- package/latticeai/core/workspace_runs.py +14 -5
- package/latticeai/core/workspace_skills.py +1 -1
- package/latticeai/core/workspace_snapshots.py +2 -1
- package/latticeai/core/workspace_timeline.py +2 -1
- package/latticeai/integrations/telegram_bot.py +96 -40
- package/latticeai/models/model_providers.py +111 -0
- package/latticeai/models/router.py +189 -173
- package/latticeai/runtime/access_runtime.py +62 -4
- package/latticeai/runtime/audit_runtime.py +27 -16
- package/latticeai/runtime/automation_runtime.py +22 -7
- package/latticeai/runtime/brain_runtime.py +19 -7
- package/latticeai/runtime/chat_wiring.py +2 -0
- package/latticeai/runtime/config_runtime.py +58 -26
- package/latticeai/runtime/context_runtime.py +16 -4
- package/latticeai/runtime/history_runtime.py +163 -0
- package/latticeai/runtime/hooks_runtime.py +1 -1
- package/latticeai/runtime/model_wiring.py +33 -5
- package/latticeai/runtime/namespace_runtime.py +163 -0
- package/latticeai/runtime/network_config_runtime.py +56 -0
- package/latticeai/runtime/platform_runtime_wiring.py +4 -3
- package/latticeai/runtime/review_wiring.py +1 -1
- package/latticeai/runtime/router_registration.py +34 -1
- package/latticeai/runtime/security_runtime.py +110 -13
- package/latticeai/runtime/sso_config_runtime.py +128 -0
- package/latticeai/runtime/stages.py +27 -0
- package/latticeai/runtime/user_key_runtime.py +106 -0
- package/latticeai/server_app.py +5 -1
- package/latticeai/services/architecture_readiness.py +74 -5
- package/latticeai/services/brain_automation.py +3 -26
- package/latticeai/services/chat_service.py +205 -15
- package/latticeai/services/local_knowledge.py +423 -0
- package/latticeai/services/memory_service.py +268 -21
- package/latticeai/services/model_engines.py +48 -33
- package/latticeai/services/model_errors.py +17 -0
- package/latticeai/services/model_loading.py +28 -25
- package/latticeai/services/model_runtime.py +228 -162
- package/latticeai/services/p_reinforce.py +22 -3
- package/latticeai/services/platform_runtime.py +92 -24
- package/latticeai/services/product_readiness.py +19 -17
- package/latticeai/services/review_queue.py +76 -11
- package/latticeai/services/router_context.py +1 -0
- package/latticeai/services/run_executor.py +25 -9
- package/latticeai/services/search_service.py +203 -28
- package/latticeai/services/setup_detection.py +80 -0
- package/latticeai/services/tool_dispatch.py +28 -5
- package/latticeai/services/triggers.py +53 -4
- package/latticeai/services/upload_service.py +13 -2
- package/latticeai/setup/__init__.py +25 -0
- package/latticeai/setup/auto_setup.py +857 -0
- package/latticeai/setup/wizard.py +1264 -0
- package/latticeai/tools/__init__.py +278 -0
- package/{tools → latticeai/tools}/commands.py +67 -7
- package/{tools → latticeai/tools}/computer.py +1 -1
- package/{tools → latticeai/tools}/documents.py +1 -1
- package/{tools → latticeai/tools}/filesystem.py +3 -3
- package/latticeai/tools/knowledge.py +178 -0
- package/{tools → latticeai/tools}/local_files.py +7 -1
- package/{tools → latticeai/tools}/network.py +0 -1
- package/local_knowledge_api.py +4 -342
- package/package.json +22 -2
- package/scripts/brain_quality_eval.py +3 -1
- package/scripts/bump_version.py +3 -0
- package/scripts/capture_release_evidence.mjs +180 -0
- package/scripts/check_current_release_docs.mjs +142 -0
- package/scripts/check_i18n_literals.mjs +107 -31
- package/scripts/check_openapi_drift.mjs +56 -0
- package/scripts/export_openapi.py +67 -7
- package/scripts/i18n_literal_allowlist.json +22 -24
- package/scripts/run_integration_tests.mjs +72 -10
- package/scripts/validate_release_artifacts.py +49 -7
- package/scripts/wheel_smoke.py +5 -0
- package/setup_wizard.py +3 -1304
- package/src-tauri/Cargo.lock +1 -1
- package/src-tauri/Cargo.toml +1 -1
- package/src-tauri/tauri.conf.json +1 -1
- package/static/app/asset-manifest.json +11 -11
- package/static/app/assets/Act-Bzz0bUyW.js +1 -0
- package/static/app/assets/Brain-Dj2J20YA.js +321 -0
- package/static/app/assets/Capture-CqlEl1Ga.js +1 -0
- package/static/app/assets/Library-B03FP1Yx.js +1 -0
- package/static/app/assets/System-80lHW0Ux.js +1 -0
- package/static/app/assets/index-BiMofBTM.js +17 -0
- package/static/app/assets/index-Bmx9rzTc.css +2 -0
- package/static/app/assets/primitives-Q1A96_7v.js +1 -0
- package/static/app/assets/textarea-D13RtnTo.js +1 -0
- package/static/app/index.html +2 -2
- package/static/css/tokens.css +4 -2
- package/static/sw.js +1 -1
- package/tools/__init__.py +21 -269
- package/docs/CODE_REVIEW_2026-07-06.md +0 -764
- package/latticeai/runtime/app_context_runtime.py +0 -13
- package/latticeai/runtime/sso_runtime.py +0 -52
- package/latticeai/runtime/tail_wiring.py +0 -21
- package/scripts/com.pts.claudecode.discord.plist +0 -31
- package/scripts/pts-claudecode-discord-bridge.mjs +0 -207
- package/scripts/start-pts-claudecode-discord.sh +0 -51
- package/static/app/assets/Act-fZokUnC0.js +0 -1
- package/static/app/assets/Brain-DtyuWubr.js +0 -321
- package/static/app/assets/Capture-D5KV3Cu7.js +0 -1
- package/static/app/assets/Library-C9kyFkSt.js +0 -1
- package/static/app/assets/System-VbChmX7r.js +0 -1
- package/static/app/assets/index-DCh5AoXt.css +0 -2
- package/static/app/assets/index-DPdcPoF0.js +0 -17
- package/static/app/assets/primitives-DFeanEV6.js +0 -1
- package/static/app/assets/textarea-CD8UNKIy.js +0 -1
- 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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
|
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
|
-
|
|
16
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
"
|
|
4
|
-
|
|
3
|
+
"findings": [
|
|
4
|
+
"JSX text: Lattice"
|
|
5
|
+
],
|
|
6
|
+
"reason": "Product brand name"
|
|
5
7
|
},
|
|
6
|
-
"frontend/src/
|
|
7
|
-
"
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
"
|
|
24
|
-
|
|
15
|
+
"findings": [
|
|
16
|
+
"JSX text: KB ·"
|
|
17
|
+
],
|
|
18
|
+
"reason": "International technical storage unit"
|
|
25
19
|
},
|
|
26
20
|
"frontend/src/pages/Library.tsx": {
|
|
27
|
-
"
|
|
28
|
-
|
|
21
|
+
"findings": [
|
|
22
|
+
"JSX text: ✓ HF"
|
|
23
|
+
],
|
|
24
|
+
"reason": "Hugging Face verification abbreviation"
|
|
29
25
|
},
|
|
30
|
-
"frontend/src/pages/
|
|
31
|
-
"
|
|
32
|
-
|
|
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
|
-
|
|
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: { ...
|
|
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);
|