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.
- package/README.md +46 -53
- package/docs/CHANGELOG.md +61 -0
- package/docs/COMMUNITY_AND_PLUGINS.md +1 -1
- package/docs/DEVELOPMENT.md +1 -1
- package/docs/MULTI_AGENT_RUNTIME.md +1 -1
- package/docs/ONBOARDING.md +1 -1
- package/docs/OPERATIONS.md +6 -2
- package/docs/PERMISSION_MODE.md +1 -1
- package/docs/TRUST_MODEL.md +1 -1
- package/docs/WHY_LATTICE.md +1 -1
- package/docs/kg-schema.md +2 -2
- package/docs/v11.3.0_PLAN.md +202 -0
- package/docs/v11.4.0_RUST_FOUNDATION_PLAN.md +176 -0
- package/lattice_brain/__init__.py +1 -1
- package/lattice_brain/graph/_kg_common/__init__.py +287 -0
- package/lattice_brain/graph/_kg_common/extraction.py +516 -0
- package/lattice_brain/graph/_kg_common/relations.py +161 -0
- package/lattice_brain/graph/_kg_common/text.py +479 -0
- package/lattice_brain/graph/discovery_index/__init__.py +35 -0
- package/lattice_brain/graph/discovery_index/cleanup.py +182 -0
- package/lattice_brain/graph/discovery_index/extract.py +137 -0
- package/lattice_brain/graph/discovery_index/scan.py +411 -0
- package/lattice_brain/graph/discovery_index/upsert.py +495 -0
- package/lattice_brain/graph/projection/__init__.py +42 -0
- package/lattice_brain/graph/projection/curation.py +500 -0
- package/lattice_brain/graph/{projection.py → projection/v2_schema.py} +15 -477
- package/lattice_brain/graph/retrieval/__init__.py +54 -0
- package/lattice_brain/graph/retrieval/context.py +197 -0
- package/lattice_brain/graph/retrieval/graph_view.py +319 -0
- package/lattice_brain/graph/retrieval/hybrid.py +488 -0
- package/lattice_brain/graph/retrieval/maintenance.py +121 -0
- package/lattice_brain/graph/retrieval/signals.py +95 -0
- package/lattice_brain/graph/retrieval_vector/__init__.py +42 -0
- package/lattice_brain/graph/retrieval_vector/fingerprint.py +97 -0
- package/lattice_brain/graph/retrieval_vector/indexing.py +347 -0
- package/lattice_brain/graph/retrieval_vector/search.py +560 -0
- package/lattice_brain/graph/retrieval_vector/status.py +374 -0
- package/lattice_brain/ingestion/__init__.py +130 -0
- package/lattice_brain/ingestion/_contract.py +90 -0
- package/lattice_brain/ingestion/constants.py +127 -0
- package/lattice_brain/ingestion/folder_scan.py +57 -0
- package/lattice_brain/ingestion/folders.py +258 -0
- package/lattice_brain/ingestion/hashing.py +26 -0
- package/lattice_brain/ingestion/jobs_api.py +107 -0
- package/lattice_brain/ingestion/models.py +80 -0
- package/lattice_brain/ingestion/pipeline.py +486 -0
- package/lattice_brain/ingestion/quality.py +209 -0
- package/lattice_brain/ingestion/routing.py +295 -0
- package/lattice_brain/multimodal/__init__.py +164 -0
- package/lattice_brain/multimodal/audio.py +77 -0
- package/lattice_brain/multimodal/common.py +118 -0
- package/lattice_brain/multimodal/images.py +498 -0
- package/lattice_brain/multimodal/ports.py +169 -0
- package/lattice_brain/multimodal/video.py +410 -0
- package/lattice_brain/portability/__init__.py +90 -0
- package/lattice_brain/portability/_contract.py +42 -0
- package/lattice_brain/portability/backups.py +338 -0
- package/lattice_brain/portability/bundles.py +136 -0
- package/lattice_brain/portability/constants.py +93 -0
- package/lattice_brain/portability/fsops.py +138 -0
- package/lattice_brain/portability/service.py +41 -0
- package/lattice_brain/{portability.py → portability/sharing.py} +44 -677
- package/lattice_brain/runtime/__init__.py +1 -1
- package/lattice_brain/runtime/multi_agent.py +1 -1
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/chronicle.py +63 -0
- package/latticeai/core/agent/__init__.py +93 -0
- package/latticeai/core/agent/_contract.py +79 -0
- package/latticeai/core/agent/context.py +57 -0
- package/latticeai/core/agent/deps.py +125 -0
- package/latticeai/core/agent/execution.py +622 -0
- package/latticeai/core/agent/planning.py +145 -0
- package/latticeai/core/agent/recovery.py +157 -0
- package/latticeai/core/agent/runtime.py +210 -0
- package/latticeai/core/agent/verification.py +231 -0
- package/latticeai/core/embedding_providers/__init__.py +151 -0
- package/latticeai/core/embedding_providers/base.py +199 -0
- package/latticeai/core/embedding_providers/captions.py +162 -0
- package/latticeai/core/embedding_providers/profiles.py +126 -0
- package/latticeai/core/embedding_providers/text.py +350 -0
- package/latticeai/core/embedding_providers/vision.py +352 -0
- package/latticeai/core/file_generation/__init__.py +115 -0
- package/latticeai/core/file_generation/bundles.py +76 -0
- package/latticeai/core/file_generation/extraction.py +154 -0
- package/latticeai/core/file_generation/inference.py +235 -0
- package/latticeai/core/file_generation/orchestration.py +152 -0
- package/latticeai/core/file_generation/prompting.py +117 -0
- package/latticeai/core/file_generation/repair.py +114 -0
- package/latticeai/core/file_generation/sanitize.py +61 -0
- package/latticeai/core/file_generation/validation.py +201 -0
- package/latticeai/core/legacy_compatibility.py +1 -1
- package/latticeai/core/marketplace.py +1 -1
- package/latticeai/core/messages.py +9 -0
- package/latticeai/core/workspace_os_constants.py +1 -1
- package/latticeai/integrations/telegram_bot/__init__.py +123 -0
- package/latticeai/integrations/telegram_bot/__main__.py +17 -0
- package/latticeai/integrations/telegram_bot/config.py +86 -0
- package/latticeai/integrations/telegram_bot/dispatch.py +311 -0
- package/latticeai/integrations/telegram_bot/flows.py +478 -0
- package/latticeai/integrations/telegram_bot/helpers.py +322 -0
- package/latticeai/integrations/telegram_bot/screens.py +394 -0
- package/latticeai/models/router/__init__.py +88 -0
- package/latticeai/models/router/_contract.py +66 -0
- package/latticeai/models/router/branding.py +56 -0
- package/latticeai/models/router/catalog.py +69 -0
- package/latticeai/models/router/documents.py +199 -0
- package/latticeai/models/router/errors.py +37 -0
- package/latticeai/models/router/generation.py +258 -0
- package/latticeai/models/router/loading.py +291 -0
- package/latticeai/models/router/local_models.py +85 -0
- package/latticeai/models/router/registry.py +147 -0
- package/latticeai/runtime/build_phases/__init__.py +82 -0
- package/latticeai/runtime/build_phases/features.py +407 -0
- package/latticeai/runtime/build_phases/foundation.py +555 -0
- package/latticeai/runtime/build_phases/web.py +492 -0
- package/latticeai/runtime/runtime_context.py +1 -0
- package/latticeai/services/architecture_readiness.py +48 -19
- package/latticeai/services/brain_intelligence/__init__.py +58 -0
- package/latticeai/services/brain_intelligence/_contract.py +71 -0
- package/latticeai/services/brain_intelligence/consistency.py +193 -0
- package/latticeai/services/brain_intelligence/constants.py +47 -0
- package/latticeai/services/brain_intelligence/digest.py +258 -0
- package/latticeai/services/brain_intelligence/health.py +331 -0
- package/latticeai/services/brain_intelligence/proposals.py +264 -0
- package/latticeai/services/brain_intelligence/sampling.py +84 -0
- package/latticeai/services/brain_intelligence/service.py +48 -0
- package/latticeai/services/chronicle.py +557 -0
- package/latticeai/services/memory_service/__init__.py +52 -0
- package/latticeai/services/memory_service/_contract.py +100 -0
- package/latticeai/services/memory_service/brief.py +431 -0
- package/latticeai/services/memory_service/constants.py +57 -0
- package/latticeai/services/memory_service/maintenance.py +138 -0
- package/latticeai/services/memory_service/manager.py +186 -0
- package/latticeai/services/memory_service/proof.py +136 -0
- package/latticeai/services/memory_service/recall.py +225 -0
- package/latticeai/services/memory_service/service.py +48 -0
- package/latticeai/services/memory_service/stores.py +110 -0
- package/latticeai/services/model_runtime/__init__.py +322 -0
- package/latticeai/services/model_runtime/cloud.py +87 -0
- package/latticeai/services/model_runtime/download.py +282 -0
- package/latticeai/services/model_runtime/engines.py +341 -0
- package/latticeai/services/model_runtime/loading.py +178 -0
- package/latticeai/services/model_runtime/service.py +129 -0
- package/latticeai/services/model_runtime/state.py +131 -0
- package/latticeai/services/model_runtime/status.py +255 -0
- package/latticeai/services/product_readiness.py +15 -7
- package/latticeai/setup/wizard/__init__.py +126 -0
- package/latticeai/setup/wizard/catalog.py +172 -0
- package/latticeai/setup/wizard/detect.py +323 -0
- package/latticeai/setup/wizard/install.py +348 -0
- package/latticeai/setup/wizard/paths.py +168 -0
- package/latticeai/setup/wizard/plans.py +74 -0
- package/latticeai/setup/wizard/recommend.py +320 -0
- package/package.json +6 -2
- package/scripts/bump_version.py +14 -0
- package/scripts/capture_release_evidence.mjs +33 -21
- package/scripts/check_current_release_docs.mjs +1 -1
- package/scripts/check_i18n_namespace_coverage.mjs +41 -4
- package/scripts/check_max_file_lines.mjs +102 -0
- package/scripts/check_release_evidence_bound.mjs +30 -15
- package/scripts/check_screenshot_pixel_delta.py +34 -4
- package/scripts/check_server_i18n.mjs +1 -0
- package/scripts/generate_rust_parity_fixtures.py +562 -0
- package/scripts/lib/mock_server_fingerprint.mjs +94 -0
- package/scripts/release_screen_claims.json +31 -2
- package/src-tauri/Cargo.lock +361 -3
- package/src-tauri/Cargo.toml +6 -1
- package/src-tauri/src/backend.rs +349 -0
- package/src-tauri/src/folder.rs +33 -0
- package/src-tauri/src/main.rs +97 -399
- package/src-tauri/tauri.conf.json +1 -1
- package/static/app/asset-manifest.json +41 -37
- package/static/app/assets/Act-yYpYnn0v.js +1 -0
- package/static/app/assets/AdminConsole-DL3Cr5pL.js +1 -0
- package/static/app/assets/{Brain-tuhI4sOC.js → Brain-C1HBN0Wf.js} +2 -2
- package/static/app/assets/BrainHome-DoXRhUUC.js +2 -0
- package/static/app/assets/BrainSignals-6yR6ir5t.js +1 -0
- package/static/app/assets/Capture-CFIRsFNE.js +1 -0
- package/static/app/assets/Chronicle-BZbEgiwN.js +1 -0
- package/static/app/assets/CommandPalette-D2pMxC2I.js +1 -0
- package/static/app/assets/Library-DwO3yZST.js +1 -0
- package/static/app/assets/{LivingBrain-DBwhto14.js → LivingBrain-Jn1GK0-S.js} +1 -1
- package/static/app/assets/ProductFlow-B-w1R4Oo.js +1 -0
- package/static/app/assets/ReviewCard-6B27X8Vg.js +3 -0
- package/static/app/assets/System-DW8F-2xL.js +1 -0
- package/static/app/assets/arrow-left-DXvKg9U6.js +1 -0
- package/static/app/assets/{bot-Cia42c2h.js → bot-IM_E_Y12.js} +1 -1
- package/static/app/assets/brain-Ci1CkWjM.js +1 -0
- package/static/app/assets/{button-2j2Ijzgq.js → button-COwyqfHM.js} +1 -1
- package/static/app/assets/circle-check-DfInj-qD.js +1 -0
- package/static/app/assets/{circle-pause-BEFeWpVW.js → circle-pause-DEM4A1Y5.js} +1 -1
- package/static/app/assets/{circle-play-ujXMcHxl.js → circle-play-C9djDuLd.js} +1 -1
- package/static/app/assets/{cpu-k4awryFq.js → cpu-DFdo1gw-.js} +1 -1
- package/static/app/assets/{download-DFbLJ_ig.js → download-SnJL6oqk.js} +1 -1
- package/static/app/assets/{folder-open-7y_b6xkM.js → folder-open-CqZeDkjE.js} +1 -1
- package/static/app/assets/{hard-drive-Bidh02Kr.js → hard-drive-j1jJXYYf.js} +1 -1
- package/static/app/assets/{index-DwDl9-8Y.css → index-BLPb5lmE.css} +1 -1
- package/static/app/assets/index-_u5iUHDr.js +10 -0
- package/static/app/assets/input-B0lPdRQZ.js +1 -0
- package/static/app/assets/link-2-CoFbooHS.js +1 -0
- package/static/app/assets/{permissionCopy-Bpb83Hx9.js → permissionCopy-BsyLxtao.js} +1 -1
- package/static/app/assets/primitives-DEbN-d6p.js +1 -0
- package/static/app/assets/search-BybIWPNd.js +1 -0
- package/static/app/assets/{share-2-BH1M-WNi.js → share-2-CVtZ_ewX.js} +1 -1
- package/static/app/assets/{shield-alert-BlKdBXcG.js → shield-alert-CBi2GNWM.js} +1 -1
- package/static/app/assets/{textarea-CCWbUfFB.js → textarea-DNMpB5ih.js} +1 -1
- package/static/app/assets/{useFocusTrap-YdHQ7pJ1.js → useFocusTrap-C83t3GXF.js} +1 -1
- package/static/app/assets/useMutation-DtbJDoyz.js +1 -0
- package/static/app/assets/{useQuery-CXQiwbVT.js → useQuery-Dcp1OChy.js} +1 -1
- package/static/app/assets/utils-BlZr7Pd4.js +4 -0
- package/static/app/assets/workspace-jJY4RuAV.js +1 -0
- package/static/app/index.html +4 -4
- package/static/sw.js +1 -1
- package/lattice_brain/graph/_kg_common.py +0 -1331
- package/lattice_brain/graph/discovery_index.py +0 -1141
- package/lattice_brain/graph/retrieval.py +0 -1120
- package/lattice_brain/graph/retrieval_vector.py +0 -1293
- package/lattice_brain/ingestion.py +0 -1525
- package/lattice_brain/multimodal.py +0 -1258
- package/latticeai/core/agent.py +0 -1465
- package/latticeai/core/embedding_providers.py +0 -1196
- package/latticeai/core/file_generation.py +0 -1047
- package/latticeai/integrations/telegram_bot.py +0 -1390
- package/latticeai/models/router.py +0 -1007
- package/latticeai/runtime/build_phases.py +0 -1450
- package/latticeai/services/brain_intelligence.py +0 -1083
- package/latticeai/services/memory_service.py +0 -1177
- package/latticeai/services/model_runtime.py +0 -1281
- package/latticeai/setup/wizard.py +0 -1310
- package/static/app/assets/Act-AWf0SAKp.js +0 -1
- package/static/app/assets/AdminConsole-D0u8Tiyj.js +0 -1
- package/static/app/assets/BrainHome-Ts7G_Ila.js +0 -2
- package/static/app/assets/BrainSignals-jMYgQ2Ar.js +0 -1
- package/static/app/assets/Capture-CqOSzyPr.js +0 -1
- package/static/app/assets/CommandPalette-DC0Bzh-I.js +0 -1
- package/static/app/assets/Library-CX-bbhmK.js +0 -1
- package/static/app/assets/ProductFlow-BHA2cfKI.js +0 -1
- package/static/app/assets/ReviewCard-BUhCKRNM.js +0 -3
- package/static/app/assets/System-Bu2t5hn1.js +0 -1
- package/static/app/assets/arrow-left-Dzwa5zRb.js +0 -1
- package/static/app/assets/brain-DJMoqrwx.js +0 -1
- package/static/app/assets/index-BpYkzcVm.js +0 -10
- package/static/app/assets/input-DSlJJxRs.js +0 -1
- package/static/app/assets/primitives-BCx6TvfG.js +0 -1
- package/static/app/assets/search-Cgy8cCFJ.js +0 -1
- package/static/app/assets/utils-zqPZJxdx.js +0 -4
- 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
|
|
13
|
-
*
|
|
14
|
-
*
|
|
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
|
|
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
|
-
|
|
64
|
-
|
|
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
|
-
|
|
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 =
|
|
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:
|
|
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
|
-
`
|
|
109
|
-
|
|
110
|
-
"
|
|
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
|
-
|
|
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
|
-
|
|
203
|
-
|
|
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
|