ltcai 11.5.2 → 11.7.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 +93 -148
- package/bin/ltcai.js +234 -24
- package/docs/CHANGELOG.md +119 -0
- package/docs/COMMUNITY_AND_PLUGINS.md +1 -1
- package/docs/DEVELOPMENT.md +8 -6
- package/docs/ENTERPRISE.md +2 -1
- package/docs/MULTI_AGENT_RUNTIME.md +12 -5
- package/docs/ONBOARDING.md +1 -1
- package/docs/OPERATIONS.md +6 -3
- package/docs/REALTIME_COLLABORATION.md +1 -1
- package/docs/TRUST_MODEL.md +1 -1
- package/docs/WHY_LATTICE.md +1 -1
- package/docs/WORKFLOW_DESIGNER.md +3 -2
- package/docs/kg-schema.md +1 -1
- package/docs/v11.6.0_ONE_DOOR_PLAN.md +170 -0
- package/lattice_brain/__init__.py +42 -79
- package/lattice_brain/graph/__init__.py +9 -24
- package/lattice_brain/graph/_kg_common/__init__.py +13 -24
- package/lattice_brain/ingestion/__init__.py +19 -58
- package/lattice_brain/ingestion/pipeline.py +20 -398
- package/lattice_brain/multimodal/__init__.py +7 -18
- package/lattice_brain/multimodal/images.py +6 -264
- package/lattice_brain/multimodal/video.py +8 -247
- package/lattice_brain/runtime/__init__.py +8 -79
- package/lattice_brain/runtime/hooks.py +22 -584
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/agent_worker_seam.py +7 -80
- package/latticeai/api/health.py +6 -20
- package/latticeai/api/local_files.py +16 -613
- package/latticeai/api/models.py +12 -87
- package/latticeai/api/search.py +17 -255
- package/latticeai/api/tools.py +69 -750
- package/latticeai/api/voice_capture.py +8 -70
- package/latticeai/api/worker_compute.py +842 -0
- package/latticeai/api/worker_seams.py +216 -0
- package/latticeai/app_factory.py +23 -232
- package/latticeai/cli/entrypoint.py +36 -249
- package/latticeai/core/agent_permission.py +16 -91
- package/latticeai/core/messages.py +89 -531
- package/latticeai/runtime/access_runtime.py +0 -14
- package/latticeai/runtime/bootstrap.py +10 -21
- package/latticeai/runtime/brain_runtime.py +35 -43
- package/latticeai/runtime/build_phases/__init__.py +21 -37
- package/latticeai/runtime/build_phases/features.py +99 -389
- package/latticeai/runtime/build_phases/foundation.py +82 -392
- package/latticeai/runtime/build_phases/web.py +91 -408
- package/latticeai/runtime/build_phases/worker_profile.py +244 -0
- package/latticeai/runtime/lifespan_runtime.py +7 -16
- package/latticeai/runtime/platform_services_runtime.py +1 -15
- package/latticeai/runtime/runtime_context.py +13 -130
- package/latticeai/runtime/security_runtime.py +10 -103
- package/latticeai/services/architecture_readiness.py +82 -43
- package/latticeai/services/model_runtime/__init__.py +2 -11
- package/latticeai/services/model_runtime/service.py +4 -28
- package/latticeai/services/p_reinforce.py +10 -261
- package/latticeai/services/product_readiness.py +31 -38
- package/latticeai/services/search_service.py +37 -795
- package/latticeai/services/tool_dispatch.py +30 -386
- package/latticeai/services/voice_capture.py +13 -107
- package/latticeai/tools/__init__.py +22 -49
- package/latticeai/tools/commands.py +0 -163
- package/latticeai/tools/computer.py +0 -39
- package/latticeai/tools/documents.py +1 -134
- package/latticeai/tools/filesystem.py +1 -247
- package/latticeai/tools/knowledge.py +1 -52
- package/latticeai/tools/local_files.py +0 -20
- package/latticeai/worker_app.py +75 -0
- package/package.json +2 -3
- package/requirements.txt +0 -5
- package/scripts/agent_eval.py +16 -28
- package/scripts/brain_quality_eval.py +20 -183
- package/scripts/bump_version.py +5 -5
- package/scripts/check_current_release_docs.mjs +7 -4
- package/scripts/check_openapi_drift.mjs +10 -0
- package/scripts/check_server_i18n.mjs +2 -18
- package/scripts/compose_openapi.py +377 -0
- package/scripts/export_openapi.py +32 -4
- package/scripts/gen_messages_catalog_fixture.py +302 -0
- package/scripts/gen_openapi_fragments.py +365 -0
- package/scripts/gen_redact_fixture.py +196 -0
- package/scripts/gen_worker_allowlist_fixture.py +115 -0
- package/scripts/generate_agent_parity_fixtures.py +33 -14
- package/scripts/openapi_route_families.json +2161 -0
- package/scripts/release_screen_claims.json +54 -0
- package/scripts/run_integration_tests.mjs +134 -29
- package/scripts/run_sidecar_e2e.mjs +104 -11
- package/scripts/wheel_smoke.py +32 -22
- package/src-tauri/Cargo.lock +375 -9
- package/src-tauri/Cargo.toml +1 -1
- package/src-tauri/src/backend.rs +151 -45
- package/src-tauri/src/main.rs +18 -18
- package/src-tauri/src/topology.rs +58 -88
- package/src-tauri/tauri.conf.json +1 -2
- package/static/app/asset-manifest.json +41 -41
- package/static/app/assets/{Act-DcQizkl1.js → Act-BPcVAbOL.js} +1 -1
- package/static/app/assets/AdminConsole-Bw1ATQL0.js +1 -0
- package/static/app/assets/{Brain-3VCSHFcn.js → Brain-CT92Kos0.js} +2 -2
- package/static/app/assets/{BrainHome-Qm8eaztx.js → BrainHome-CFBkt1K_.js} +1 -1
- package/static/app/assets/{BrainSignals-DS9BtKOW.js → BrainSignals-ReLWF2H8.js} +1 -1
- package/static/app/assets/Capture-BsTokYkk.js +1 -0
- package/static/app/assets/{Chronicle-BGvuAchH.js → Chronicle-B6f0T9id.js} +1 -1
- package/static/app/assets/{CommandPalette-Bqhm0Urn.js → CommandPalette-CuvjTv1u.js} +1 -1
- package/static/app/assets/{Library-BV6NnF0a.js → Library-BGJbG9Hd.js} +1 -1
- package/static/app/assets/{LivingBrain-GzenJchP.js → LivingBrain-DGYK_Jsa.js} +1 -1
- package/static/app/assets/{ProductFlow-DEP6-vML.js → ProductFlow-DXBC6brE.js} +1 -1
- package/static/app/assets/{ReviewCard-CNZ7XjWG.js → ReviewCard-HXRle3qq.js} +2 -2
- package/static/app/assets/System-CMHSO9qM.js +1 -0
- package/static/app/assets/arrow-left-BfmkskWx.js +1 -0
- package/static/app/assets/{bot-B_K1Tdmw.js → bot-Cn8bWRuq.js} +1 -1
- package/static/app/assets/{brain-DWyaV1L1.js → brain-CQJberbE.js} +1 -1
- package/static/app/assets/{button-aTn4s84A.js → button-Ct9f2_oT.js} +1 -1
- package/static/app/assets/circle-check-DruOxB-4.js +1 -0
- package/static/app/assets/{circle-pause-xKgeGXkT.js → circle-pause-CmzC_apg.js} +1 -1
- package/static/app/assets/{circle-play-DkT6tYPX.js → circle-play-D8mW2aQ7.js} +1 -1
- package/static/app/assets/{cpu-85xYObUC.js → cpu-DZcdd0PZ.js} +1 -1
- package/static/app/assets/{download-B5Fm7YXo.js → download-bv1KEPGQ.js} +1 -1
- package/static/app/assets/{folder-open-kk2Xa52u.js → folder-open-d-Pip5gr.js} +1 -1
- package/static/app/assets/{hard-drive-DkA3zBW_.js → hard-drive-D20iavUb.js} +1 -1
- package/static/app/assets/index-D9x-kSNy.css +2 -0
- package/static/app/assets/{index-BMPdTmlY.js → index-Do83hDzJ.js} +3 -3
- package/static/app/assets/{input-B0nRf2jO.js → input-BLXVNmj1.js} +1 -1
- package/static/app/assets/{link-2-Dwb4gnTc.js → link-2-BPJOFlAy.js} +1 -1
- package/static/app/assets/{permissionCopy-CQDUBrOZ.js → permissionCopy-ChdJd493.js} +1 -1
- package/static/app/assets/primitives-Cv5tbZBY.js +1 -0
- package/static/app/assets/search-CT9aho2j.js +1 -0
- package/static/app/assets/{share-2-BsrxFglO.js → share-2-YNX_NtMU.js} +1 -1
- package/static/app/assets/{shield-alert-5BStfp2_.js → shield-alert-DuQ3zrVL.js} +1 -1
- package/static/app/assets/{textarea-Cg8IUA-k.js → textarea-DqwLnli4.js} +1 -1
- package/static/app/assets/{useFocusTrap-CYKvE46M.js → useFocusTrap-ZVI98jaW.js} +1 -1
- package/static/app/assets/{useMutation-CSn9t1op.js → useMutation-CVC4qv_D.js} +1 -1
- package/static/app/assets/{useQuery-CY2OI2uy.js → useQuery-C7BeG4HU.js} +1 -1
- package/static/app/assets/{utils-Ddol2RWD.js → utils-CiFtIdZq.js} +1 -1
- package/static/app/assets/{workspace-BqDwOz_p.js → workspace-DQz9vIId.js} +1 -1
- package/static/app/index.html +4 -4
- package/static/sw.js +1 -1
- package/lattice_brain/archive.py +0 -522
- package/lattice_brain/context.py +0 -325
- package/lattice_brain/conversations.py +0 -382
- package/lattice_brain/core.py +0 -82
- package/lattice_brain/graph/_kg_contract.py +0 -249
- package/lattice_brain/graph/curator.py +0 -676
- package/lattice_brain/graph/discovery.py +0 -595
- package/lattice_brain/graph/discovery_index/__init__.py +0 -35
- package/lattice_brain/graph/discovery_index/cleanup.py +0 -182
- package/lattice_brain/graph/discovery_index/extract.py +0 -137
- package/lattice_brain/graph/discovery_index/scan.py +0 -411
- package/lattice_brain/graph/discovery_index/upsert.py +0 -495
- package/lattice_brain/graph/documents.py +0 -380
- package/lattice_brain/graph/fusion.py +0 -395
- package/lattice_brain/graph/identity.py +0 -175
- package/lattice_brain/graph/image_vectors.py +0 -230
- package/lattice_brain/graph/ingest.py +0 -829
- package/lattice_brain/graph/network.py +0 -205
- package/lattice_brain/graph/proactive.py +0 -724
- package/lattice_brain/graph/projection/__init__.py +0 -42
- package/lattice_brain/graph/projection/curation.py +0 -500
- package/lattice_brain/graph/projection/v2_schema.py +0 -518
- package/lattice_brain/graph/provenance.py +0 -524
- package/lattice_brain/graph/rerank.py +0 -163
- package/lattice_brain/graph/retrieval/__init__.py +0 -54
- package/lattice_brain/graph/retrieval/context.py +0 -197
- package/lattice_brain/graph/retrieval/graph_view.py +0 -319
- package/lattice_brain/graph/retrieval/hybrid.py +0 -488
- package/lattice_brain/graph/retrieval/maintenance.py +0 -121
- package/lattice_brain/graph/retrieval/signals.py +0 -95
- package/lattice_brain/graph/retrieval_docgen.py +0 -253
- package/lattice_brain/graph/retrieval_policy.py +0 -180
- package/lattice_brain/graph/retrieval_reads.py +0 -769
- package/lattice_brain/graph/retrieval_vector/__init__.py +0 -42
- package/lattice_brain/graph/retrieval_vector/fingerprint.py +0 -97
- package/lattice_brain/graph/retrieval_vector/indexing.py +0 -347
- package/lattice_brain/graph/retrieval_vector/search.py +0 -560
- package/lattice_brain/graph/retrieval_vector/status.py +0 -374
- package/lattice_brain/graph/schema.py +0 -792
- package/lattice_brain/graph/store.py +0 -268
- package/lattice_brain/graph/vector_index/__init__.py +0 -85
- package/lattice_brain/graph/vector_index/base.py +0 -167
- package/lattice_brain/graph/vector_index/brute_force.py +0 -110
- package/lattice_brain/graph/vector_index/hnsw.py +0 -290
- package/lattice_brain/graph/vector_index/jobs.py +0 -287
- package/lattice_brain/graph/vector_index/quantized.py +0 -148
- package/lattice_brain/graph/vector_index/selector.py +0 -161
- package/lattice_brain/graph/write_master.py +0 -308
- package/lattice_brain/ingestion/_contract.py +0 -90
- package/lattice_brain/ingestion/folder_scan.py +0 -57
- package/lattice_brain/ingestion/folders.py +0 -258
- package/lattice_brain/ingestion/jobs_api.py +0 -107
- package/lattice_brain/ingestion/routing.py +0 -295
- package/lattice_brain/ingestion_jobs.py +0 -380
- package/lattice_brain/memory.py +0 -75
- package/lattice_brain/portability/__init__.py +0 -90
- package/lattice_brain/portability/_contract.py +0 -42
- package/lattice_brain/portability/backups.py +0 -338
- package/lattice_brain/portability/bundles.py +0 -136
- package/lattice_brain/portability/constants.py +0 -93
- package/lattice_brain/portability/fsops.py +0 -138
- package/lattice_brain/portability/service.py +0 -41
- package/lattice_brain/portability/sharing.py +0 -710
- package/lattice_brain/quality.py +0 -543
- package/lattice_brain/retrieval_benchmark_fixtures.py +0 -92
- package/lattice_brain/runtime/agent_runtime.py +0 -859
- package/lattice_brain/runtime/contracts.py +0 -460
- package/lattice_brain/runtime/multi_agent.py +0 -942
- package/lattice_brain/runtime/statuses.py +0 -10
- package/lattice_brain/sealed_box.py +0 -240
- package/lattice_brain/self_model.py +0 -675
- package/lattice_brain/sensitivity.py +0 -94
- package/lattice_brain/storage/__init__.py +0 -22
- package/lattice_brain/storage/base.py +0 -100
- package/lattice_brain/storage/docker.py +0 -105
- package/lattice_brain/storage/factory.py +0 -31
- package/lattice_brain/storage/migration.py +0 -191
- package/lattice_brain/storage/postgres.py +0 -123
- package/lattice_brain/storage/sqlite.py +0 -143
- package/lattice_brain/synthesis.py +0 -824
- package/lattice_brain/workflow.py +0 -497
- package/latticeai/api/admin.py +0 -471
- package/latticeai/api/agent_registry.py +0 -105
- package/latticeai/api/agents.py +0 -228
- package/latticeai/api/auth.py +0 -383
- package/latticeai/api/automation_intelligence.py +0 -401
- package/latticeai/api/brain_intelligence.py +0 -199
- package/latticeai/api/browser.py +0 -493
- package/latticeai/api/change_proposals.py +0 -89
- package/latticeai/api/chat.py +0 -572
- package/latticeai/api/chat_agent_http.py +0 -892
- package/latticeai/api/chat_contracts.py +0 -73
- package/latticeai/api/chat_documents.py +0 -276
- package/latticeai/api/chat_helpers.py +0 -460
- package/latticeai/api/chat_history.py +0 -101
- package/latticeai/api/chat_hybrid.py +0 -113
- package/latticeai/api/chat_intents.py +0 -646
- package/latticeai/api/chat_stream.py +0 -216
- package/latticeai/api/chronicle.py +0 -63
- package/latticeai/api/command_center.py +0 -51
- package/latticeai/api/computer_use.py +0 -474
- package/latticeai/api/evidence_actions.py +0 -48
- package/latticeai/api/features.py +0 -70
- package/latticeai/api/funnel_metrics.py +0 -31
- package/latticeai/api/garden.py +0 -34
- package/latticeai/api/hooks.py +0 -165
- package/latticeai/api/index_jobs.py +0 -145
- package/latticeai/api/invitations.py +0 -100
- package/latticeai/api/knowledge_graph.py +0 -536
- package/latticeai/api/marketplace.py +0 -105
- package/latticeai/api/mcp.py +0 -482
- package/latticeai/api/memory.py +0 -270
- package/latticeai/api/network.py +0 -81
- package/latticeai/api/network_boundary.py +0 -225
- package/latticeai/api/permission_mode.py +0 -61
- package/latticeai/api/permissions.py +0 -436
- package/latticeai/api/plugins.py +0 -126
- package/latticeai/api/portability.py +0 -391
- package/latticeai/api/project_sessions.py +0 -114
- package/latticeai/api/realtime.py +0 -118
- package/latticeai/api/review_queue.py +0 -364
- package/latticeai/api/security_dashboard.py +0 -604
- package/latticeai/api/setup.py +0 -319
- package/latticeai/api/static_routes.py +0 -354
- package/latticeai/api/ui_redirects.py +0 -26
- package/latticeai/api/workflow_designer.py +0 -394
- package/latticeai/api/workspace.py +0 -856
- package/latticeai/api/workspace_scope.py +0 -125
- package/latticeai/core/agent/__init__.py +0 -93
- package/latticeai/core/agent/_contract.py +0 -79
- package/latticeai/core/agent/context.py +0 -57
- package/latticeai/core/agent/deps.py +0 -125
- package/latticeai/core/agent/execution.py +0 -622
- package/latticeai/core/agent/planning.py +0 -145
- package/latticeai/core/agent/recovery.py +0 -157
- package/latticeai/core/agent/runtime.py +0 -210
- package/latticeai/core/agent/verification.py +0 -231
- package/latticeai/core/agent_eval.py +0 -739
- package/latticeai/core/agent_helpers.py +0 -493
- package/latticeai/core/agent_profiles.py +0 -110
- package/latticeai/core/agent_prompts.py +0 -171
- package/latticeai/core/agent_registry.py +0 -232
- package/latticeai/core/agent_state.py +0 -41
- package/latticeai/core/agent_trace.py +0 -104
- package/latticeai/core/artifact_ledger.py +0 -109
- package/latticeai/core/audit.py +0 -260
- package/latticeai/core/builtin_hooks.py +0 -105
- package/latticeai/core/context_builder.py +0 -394
- package/latticeai/core/document_generator.py +0 -103
- package/latticeai/core/enterprise.py +0 -154
- package/latticeai/core/enterprise_admin.py +0 -158
- package/latticeai/core/file_generation/__init__.py +0 -115
- package/latticeai/core/file_generation/bundles.py +0 -76
- package/latticeai/core/file_generation/extraction.py +0 -154
- package/latticeai/core/file_generation/inference.py +0 -235
- package/latticeai/core/file_generation/orchestration.py +0 -152
- package/latticeai/core/file_generation/prompting.py +0 -117
- package/latticeai/core/file_generation/repair.py +0 -114
- package/latticeai/core/file_generation/sanitize.py +0 -61
- package/latticeai/core/file_generation/validation.py +0 -201
- package/latticeai/core/invitations.py +0 -132
- package/latticeai/core/legacy_compatibility.py +0 -243
- package/latticeai/core/logging_safety.py +0 -46
- package/latticeai/core/marketplace.py +0 -293
- package/latticeai/core/mcp_catalog.py +0 -452
- package/latticeai/core/mcp_registry.py +0 -506
- package/latticeai/core/network_boundary.py +0 -168
- package/latticeai/core/oidc.py +0 -208
- package/latticeai/core/plugins.py +0 -432
- package/latticeai/core/product_hardening.py +0 -218
- package/latticeai/core/project_sessions.py +0 -337
- package/latticeai/core/realtime.py +0 -238
- package/latticeai/core/run_explain.py +0 -426
- package/latticeai/core/run_store.py +0 -252
- package/latticeai/core/timezones.py +0 -80
- package/latticeai/core/workspace_computer_memory.py +0 -84
- package/latticeai/core/workspace_graph_trace.py +0 -155
- package/latticeai/core/workspace_indexing.py +0 -102
- package/latticeai/core/workspace_memory.py +0 -77
- package/latticeai/core/workspace_onboarding.py +0 -104
- package/latticeai/core/workspace_os.py +0 -978
- package/latticeai/core/workspace_os_constants.py +0 -126
- package/latticeai/core/workspace_os_state.py +0 -180
- package/latticeai/core/workspace_os_utils.py +0 -103
- package/latticeai/core/workspace_permissions.py +0 -101
- package/latticeai/core/workspace_plugins.py +0 -97
- package/latticeai/core/workspace_relationships.py +0 -99
- package/latticeai/core/workspace_reorganization.py +0 -335
- package/latticeai/core/workspace_review_items.py +0 -112
- package/latticeai/core/workspace_runs.py +0 -726
- package/latticeai/core/workspace_skills.py +0 -109
- package/latticeai/core/workspace_snapshots.py +0 -198
- package/latticeai/core/workspace_timeline.py +0 -110
- package/latticeai/integrations/__init__.py +0 -0
- package/latticeai/integrations/telegram_bot/__init__.py +0 -123
- package/latticeai/integrations/telegram_bot/__main__.py +0 -17
- package/latticeai/integrations/telegram_bot/config.py +0 -86
- package/latticeai/integrations/telegram_bot/dispatch.py +0 -311
- package/latticeai/integrations/telegram_bot/flows.py +0 -478
- package/latticeai/integrations/telegram_bot/helpers.py +0 -322
- package/latticeai/integrations/telegram_bot/screens.py +0 -394
- package/latticeai/runtime/audit_runtime.py +0 -76
- package/latticeai/runtime/automation_runtime.py +0 -81
- package/latticeai/runtime/chat_wiring.py +0 -141
- package/latticeai/runtime/context_runtime.py +0 -66
- package/latticeai/runtime/feature_toggle_wiring.py +0 -157
- package/latticeai/runtime/history_runtime.py +0 -163
- package/latticeai/runtime/history_writer.py +0 -138
- package/latticeai/runtime/hooks_runtime.py +0 -77
- package/latticeai/runtime/model_wiring.py +0 -68
- package/latticeai/runtime/namespace_runtime.py +0 -163
- package/latticeai/runtime/network_boundary_wiring.py +0 -117
- package/latticeai/runtime/network_config_runtime.py +0 -56
- package/latticeai/runtime/permission_mode_wiring.py +0 -112
- package/latticeai/runtime/persistence_runtime.py +0 -159
- package/latticeai/runtime/platform_runtime_wiring.py +0 -89
- package/latticeai/runtime/review_wiring.py +0 -42
- package/latticeai/runtime/router_registration.py +0 -693
- package/latticeai/runtime/service_singletons.py +0 -55
- package/latticeai/runtime/sso_config_runtime.py +0 -128
- package/latticeai/runtime/user_key_runtime.py +0 -106
- package/latticeai/runtime/web_runtime.py +0 -92
- package/latticeai/server_app.py +0 -51
- package/latticeai/services/app_context.py +0 -130
- package/latticeai/services/automation_execution.py +0 -266
- package/latticeai/services/automation_intelligence.py +0 -614
- package/latticeai/services/brain_automation.py +0 -191
- package/latticeai/services/brain_intelligence/__init__.py +0 -58
- package/latticeai/services/brain_intelligence/_contract.py +0 -71
- package/latticeai/services/brain_intelligence/consistency.py +0 -193
- package/latticeai/services/brain_intelligence/constants.py +0 -47
- package/latticeai/services/brain_intelligence/digest.py +0 -258
- package/latticeai/services/brain_intelligence/health.py +0 -331
- package/latticeai/services/brain_intelligence/proposals.py +0 -259
- package/latticeai/services/brain_intelligence/sampling.py +0 -84
- package/latticeai/services/brain_intelligence/service.py +0 -48
- package/latticeai/services/change_proposals.py +0 -471
- package/latticeai/services/chat_service.py +0 -243
- package/latticeai/services/chronicle.py +0 -555
- package/latticeai/services/cloud_egress_audit.py +0 -85
- package/latticeai/services/cloud_extraction.py +0 -129
- package/latticeai/services/cloud_streaming.py +0 -268
- package/latticeai/services/cloud_token_guard.py +0 -84
- package/latticeai/services/command_center.py +0 -548
- package/latticeai/services/evidence_actions.py +0 -258
- package/latticeai/services/feature_toggles.py +0 -502
- package/latticeai/services/folder_watch.py +0 -520
- package/latticeai/services/funnel_metrics.py +0 -307
- package/latticeai/services/hybrid_chat.py +0 -316
- package/latticeai/services/hybrid_context.py +0 -228
- package/latticeai/services/hybrid_policy.py +0 -129
- package/latticeai/services/interop_bridges.py +0 -978
- package/latticeai/services/local_knowledge.py +0 -465
- package/latticeai/services/memory_service/__init__.py +0 -52
- package/latticeai/services/memory_service/_contract.py +0 -100
- package/latticeai/services/memory_service/brief.py +0 -431
- package/latticeai/services/memory_service/constants.py +0 -57
- package/latticeai/services/memory_service/maintenance.py +0 -138
- package/latticeai/services/memory_service/manager.py +0 -186
- package/latticeai/services/memory_service/proof.py +0 -136
- package/latticeai/services/memory_service/recall.py +0 -225
- package/latticeai/services/memory_service/service.py +0 -48
- package/latticeai/services/memory_service/stores.py +0 -110
- package/latticeai/services/mode_store.py +0 -132
- package/latticeai/services/model_recommendation.py +0 -224
- package/latticeai/services/model_runtime/cloud.py +0 -87
- package/latticeai/services/network_boundary_service.py +0 -117
- package/latticeai/services/obsidian_bridge.py +0 -609
- package/latticeai/services/openai_compatible_adapter.py +0 -101
- package/latticeai/services/permission_mode_service.py +0 -122
- package/latticeai/services/platform_runtime.py +0 -366
- package/latticeai/services/review_queue.py +0 -380
- package/latticeai/services/router_context.py +0 -59
- package/latticeai/services/run_executor.py +0 -387
- package/latticeai/services/self_model_service.py +0 -171
- package/latticeai/services/setup_detection.py +0 -147
- package/latticeai/services/triggers.py +0 -378
- package/latticeai/services/upload_service.py +0 -172
- package/latticeai/services/workspace_service.py +0 -165
- package/latticeai/setup/__init__.py +0 -25
- package/latticeai/setup/auto_setup.py +0 -846
- package/latticeai/setup/demo_corpus.py +0 -98
- package/latticeai/setup/wizard/__init__.py +0 -126
- package/latticeai/setup/wizard/catalog.py +0 -175
- package/latticeai/setup/wizard/detect.py +0 -302
- package/latticeai/setup/wizard/install.py +0 -348
- package/latticeai/setup/wizard/paths.py +0 -165
- package/latticeai/setup/wizard/plans.py +0 -74
- package/latticeai/setup/wizard/recommend.py +0 -320
- package/scripts/bench_agent_smoke.py +0 -409
- package/scripts/bench_models.py +0 -540
- package/scripts/bench_vector_index.py +0 -295
- package/scripts/funnel_soft_gate.py +0 -192
- package/scripts/generate_agent_loop_fixtures.py +0 -994
- package/scripts/generate_rust_parity_fixtures.py +0 -908
- package/scripts/migrate_brain_storage.py +0 -57
- package/scripts/parity_fixture_corpus_context.py +0 -162
- package/scripts/parity_fixture_corpus_docgen.py +0 -341
- package/scripts/profile_kg.py +0 -355
- package/server.py +0 -30
- package/static/app/assets/AdminConsole-cf4npybT.js +0 -1
- package/static/app/assets/Capture-DiQ219jW.js +0 -1
- package/static/app/assets/System-CieofHQa.js +0 -1
- package/static/app/assets/arrow-left-kfsrk0mv.js +0 -1
- package/static/app/assets/circle-check-qqLug9nU.js +0 -1
- package/static/app/assets/index-DxmOfNRi.css +0 -2
- package/static/app/assets/primitives-SNp0LRJz.js +0 -1
- package/static/app/assets/search-BcHqkjoy.js +0 -1
|
@@ -1,395 +0,0 @@
|
|
|
1
|
-
"""Query-class aware retrieval fusion weights (backlog #5, review §7.2 E).
|
|
2
|
-
|
|
3
|
-
Hybrid retrieval fuses keyword/lexical, vector, and graph channels. A single
|
|
4
|
-
static weight set treats "이 함수 왜 죽어?" and "어제 회의에서 뭐 결정했지?"
|
|
5
|
-
identically, which is measurably wrong: code questions live on exact
|
|
6
|
-
identifiers (lexical), person questions lean on relationship edges (graph),
|
|
7
|
-
recency questions need fresher lexical hits over semantic neighbors.
|
|
8
|
-
|
|
9
|
-
This module is deliberately tiny and deterministic:
|
|
10
|
-
|
|
11
|
-
* :func:`classify_query` — pure ko/en heuristics → ``fact | code | person |
|
|
12
|
-
recency`` (``fact`` is the fallback class and its weights are identical to
|
|
13
|
-
the historical defaults, so fact-class behavior is byte-compatible).
|
|
14
|
-
* :data:`DEFAULT_FUSION_WEIGHTS` — the per-class weight table. ``keyword`` /
|
|
15
|
-
``vector`` / ``graph`` feed the three-channel service fusion
|
|
16
|
-
(``SearchService.hybrid_search``); ``alpha`` is the vector share for the
|
|
17
|
-
two-channel graph-layer fusion (``KnowledgeGraphStore.hybrid_search``).
|
|
18
|
-
* :func:`fusion_profile` — classify + resolve weights, honoring the
|
|
19
|
-
``LATTICEAI_FUSION_WEIGHTS`` env override (JSON, partial per-class merges,
|
|
20
|
-
e.g. ``{"code": {"alpha": 0.2}}``).
|
|
21
|
-
|
|
22
|
-
Never uses ``or`` on scores (score 0 is falsy but valid) and never raises:
|
|
23
|
-
a malformed override is ignored, an unknown class falls back to ``fact``.
|
|
24
|
-
"""
|
|
25
|
-
|
|
26
|
-
from __future__ import annotations
|
|
27
|
-
|
|
28
|
-
import json
|
|
29
|
-
import os
|
|
30
|
-
import re
|
|
31
|
-
from typing import Any, Callable, Dict, List, Mapping, Optional, Sequence, Tuple
|
|
32
|
-
|
|
33
|
-
from ..gates import FeatureGate
|
|
34
|
-
from ..quiet import quiet
|
|
35
|
-
|
|
36
|
-
QUERY_CLASSES = ("fact", "code", "person", "recency")
|
|
37
|
-
|
|
38
|
-
FUSION_WEIGHTS_ENV = "LATTICEAI_FUSION_WEIGHTS"
|
|
39
|
-
|
|
40
|
-
# ── fusion strategy (v11.1.0) ────────────────────────────────────────────────
|
|
41
|
-
# Linear alpha fusion needs the two channels' scores to be comparable. They
|
|
42
|
-
# are not: the lexical channel scores 1/rank while the vector channel scores a
|
|
43
|
-
# max-normalized cosine, so a query where every cosine lands near 0.9 flattens
|
|
44
|
-
# the vector channel to noise. Reciprocal Rank Fusion ignores score magnitudes
|
|
45
|
-
# entirely and fuses *positions*, which is exactly the property that survives
|
|
46
|
-
# incomparable scales.
|
|
47
|
-
#
|
|
48
|
-
# It is opt-in, per query class, and off everywhere by default: alpha fusion
|
|
49
|
-
# is what every existing ranking assertion in this repo describes, and a
|
|
50
|
-
# ranking change dressed up as a bug fix is how retrieval quality regressions
|
|
51
|
-
# get shipped. Turn it on with LATTICEAI_FUSION_STRATEGY=rrf (all classes) or
|
|
52
|
-
# a JSON object like {"code": "rrf"} (per class).
|
|
53
|
-
FUSION_STRATEGY_ENV = "LATTICEAI_FUSION_STRATEGY"
|
|
54
|
-
#: The one-switch form of the per-class table above, for the settings panel:
|
|
55
|
-
#: "combine by rank instead of score", everywhere. It is applied as the *base*
|
|
56
|
-
#: of the table, so the per-class ``LATTICEAI_FUSION_STRATEGY`` config — the
|
|
57
|
-
#: more specific statement — still wins over it, and an install that touched
|
|
58
|
-
#: neither is byte-identical to what it was.
|
|
59
|
-
FUSION_RRF_ENV = "LATTICEAI_FUSION_RRF"
|
|
60
|
-
FUSION_STRATEGIES = ("alpha", "rrf")
|
|
61
|
-
DEFAULT_FUSION_STRATEGY: Dict[str, str] = dict.fromkeys(QUERY_CLASSES, "alpha")
|
|
62
|
-
FUSION_RRF_GATE = FeatureGate(
|
|
63
|
-
FUSION_RRF_ENV,
|
|
64
|
-
default=False,
|
|
65
|
-
name="fusion_rrf",
|
|
66
|
-
detail="Search channels are combined by rank rather than by score.",
|
|
67
|
-
)
|
|
68
|
-
#: The smoothing constant from the original RRF paper (Cormack et al., 2009).
|
|
69
|
-
#: Larger k flattens the curve, so rank 1 wins by less.
|
|
70
|
-
DEFAULT_RRF_K = 60
|
|
71
|
-
|
|
72
|
-
# Per-class fusion weights. "fact" mirrors the pre-fusion-gate defaults
|
|
73
|
-
# (SearchService DEFAULT_HYBRID_WEIGHTS + graph-layer alpha=0.6) so the
|
|
74
|
-
# fallback class introduces zero behavioral drift.
|
|
75
|
-
DEFAULT_FUSION_WEIGHTS: Dict[str, Dict[str, float]] = {
|
|
76
|
-
"fact": {"keyword": 0.35, "vector": 0.40, "graph": 0.25, "alpha": 0.60},
|
|
77
|
-
# Code recall lives on exact identifiers/tokens → lexical dominates.
|
|
78
|
-
"code": {"keyword": 0.55, "vector": 0.25, "graph": 0.20, "alpha": 0.35},
|
|
79
|
-
# Person questions are relationship questions → graph channel leads.
|
|
80
|
-
"person": {"keyword": 0.30, "vector": 0.30, "graph": 0.40, "alpha": 0.45},
|
|
81
|
-
# Recency questions want fresh literal hits over semantic neighbors.
|
|
82
|
-
"recency": {"keyword": 0.45, "vector": 0.35, "graph": 0.20, "alpha": 0.50},
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
# ── query-class heuristics (pure, ko/en) ─────────────────────────────────────
|
|
86
|
-
|
|
87
|
-
_CODE_FENCE_RE = re.compile(r"```|`[^`\n]+`")
|
|
88
|
-
_CODE_IDENT_RE = re.compile(
|
|
89
|
-
# ``\b`` fails between an identifier and an attached Korean particle
|
|
90
|
-
# ("ingest_folder가"), so use explicit ASCII-boundary lookbehinds instead.
|
|
91
|
-
r"[A-Za-z_][A-Za-z0-9_]*\(\)" # call syntax foo()
|
|
92
|
-
r"|(?<![A-Za-z0-9_])[a-z0-9]+_[a-z0-9_]+" # snake_case
|
|
93
|
-
r"|(?<![A-Za-z0-9_])[a-z]+[A-Z][A-Za-z0-9]*" # camelCase
|
|
94
|
-
r"|(?<![A-Za-z0-9_])[\w-]+\.(?:py|js|jsx|ts|tsx|json|yaml|yml|css|html|sql|sh|go|rs|java|rb)(?![A-Za-z0-9])"
|
|
95
|
-
)
|
|
96
|
-
_CODE_WORD_RE = re.compile(
|
|
97
|
-
r"\b(?:def|class|import|function|traceback|exception|stack\s*trace|"
|
|
98
|
-
r"bug|error|null|undefined|api|sql|regex)\b"
|
|
99
|
-
r"|코드|함수|버그|에러|오류|스택|컴파일|빌드\s*실패|구현",
|
|
100
|
-
re.IGNORECASE,
|
|
101
|
-
)
|
|
102
|
-
_PERSON_RE = re.compile(
|
|
103
|
-
r"\b(?:who|whom|whose)\b|누구|어떤\s*사람|담당자|팀원|동료|만난\s*사람"
|
|
104
|
-
r"|[\w.+-]+@[\w-]+\.[\w.]+" # an email address names a person
|
|
105
|
-
r"|[가-힣]{2,4}\s*(?:님|씨)(?:\s|$|[을를이가은는의??.!,])",
|
|
106
|
-
re.IGNORECASE,
|
|
107
|
-
)
|
|
108
|
-
_RECENCY_RE = re.compile(
|
|
109
|
-
r"최근|어제|오늘|그저께|방금|아까|지난\s*주|지난주|지난\s*달|지난달"
|
|
110
|
-
r"|이번\s*주|이번주|이번\s*달|이번달"
|
|
111
|
-
r"|\b(?:recent|recently|yesterday|today|latest|last\s+(?:week|month|night|meeting))\b",
|
|
112
|
-
re.IGNORECASE,
|
|
113
|
-
)
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
def classify_query(query: Any) -> str:
|
|
117
|
-
"""Classify a retrieval query into fact / code / person / recency.
|
|
118
|
-
|
|
119
|
-
Precedence: code (strongest structural signal) → recency (explicit time
|
|
120
|
-
words) → person → fact. Deterministic, never raises, empty → fact.
|
|
121
|
-
"""
|
|
122
|
-
text = str(query or "").strip()
|
|
123
|
-
if not text:
|
|
124
|
-
return "fact"
|
|
125
|
-
if _CODE_FENCE_RE.search(text) or _CODE_IDENT_RE.search(text) or _CODE_WORD_RE.search(text):
|
|
126
|
-
return "code"
|
|
127
|
-
if _RECENCY_RE.search(text):
|
|
128
|
-
return "recency"
|
|
129
|
-
if _PERSON_RE.search(text):
|
|
130
|
-
return "person"
|
|
131
|
-
return "fact"
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
def _env_overrides() -> Dict[str, Dict[str, float]]:
|
|
135
|
-
"""Parse LATTICEAI_FUSION_WEIGHTS (JSON) → per-class partial overrides."""
|
|
136
|
-
raw = os.getenv(FUSION_WEIGHTS_ENV, "").strip()
|
|
137
|
-
if not raw:
|
|
138
|
-
return {}
|
|
139
|
-
try:
|
|
140
|
-
payload = json.loads(raw)
|
|
141
|
-
except (ValueError, TypeError):
|
|
142
|
-
return {}
|
|
143
|
-
if not isinstance(payload, dict):
|
|
144
|
-
return {}
|
|
145
|
-
overrides: Dict[str, Dict[str, float]] = {}
|
|
146
|
-
for cls, table in payload.items():
|
|
147
|
-
if cls not in DEFAULT_FUSION_WEIGHTS or not isinstance(table, dict):
|
|
148
|
-
continue
|
|
149
|
-
cleaned: Dict[str, float] = {}
|
|
150
|
-
for key, value in table.items():
|
|
151
|
-
if key not in ("keyword", "vector", "graph", "alpha"):
|
|
152
|
-
continue
|
|
153
|
-
try:
|
|
154
|
-
cleaned[key] = max(0.0, min(1.0, float(value)))
|
|
155
|
-
except (TypeError, ValueError):
|
|
156
|
-
quiet()
|
|
157
|
-
continue
|
|
158
|
-
if cleaned:
|
|
159
|
-
overrides[str(cls)] = cleaned
|
|
160
|
-
return overrides
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
def fusion_weight_table(
|
|
164
|
-
overrides: Optional[Mapping[str, Mapping[str, float]]] = None,
|
|
165
|
-
) -> Dict[str, Dict[str, float]]:
|
|
166
|
-
"""Full per-class table: defaults ← env override ← caller override."""
|
|
167
|
-
table = {cls: dict(weights) for cls, weights in DEFAULT_FUSION_WEIGHTS.items()}
|
|
168
|
-
for source in (_env_overrides(), overrides or {}):
|
|
169
|
-
for cls, partial in source.items():
|
|
170
|
-
if cls in table and isinstance(partial, Mapping):
|
|
171
|
-
for key, value in partial.items():
|
|
172
|
-
if key in table[cls]:
|
|
173
|
-
try:
|
|
174
|
-
table[cls][key] = max(0.0, min(1.0, float(value)))
|
|
175
|
-
except (TypeError, ValueError):
|
|
176
|
-
quiet()
|
|
177
|
-
continue
|
|
178
|
-
return table
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
def _env_strategy_overrides() -> Dict[str, str]:
|
|
182
|
-
"""Parse ``LATTICEAI_FUSION_STRATEGY`` → per-class strategy overrides.
|
|
183
|
-
|
|
184
|
-
Two accepted forms: a bare strategy name (``rrf``) applies to every class,
|
|
185
|
-
a JSON object (``{"code": "rrf"}``) applies per class. Anything else —
|
|
186
|
-
a typo, malformed JSON, an unknown strategy — is ignored rather than
|
|
187
|
-
raising, so a bad env var degrades to the default fusion instead of
|
|
188
|
-
taking every search down.
|
|
189
|
-
"""
|
|
190
|
-
raw = os.getenv(FUSION_STRATEGY_ENV, "").strip()
|
|
191
|
-
if not raw:
|
|
192
|
-
return {}
|
|
193
|
-
if raw.lower() in FUSION_STRATEGIES:
|
|
194
|
-
return dict.fromkeys(QUERY_CLASSES, raw.lower())
|
|
195
|
-
try:
|
|
196
|
-
payload = json.loads(raw)
|
|
197
|
-
except (ValueError, TypeError):
|
|
198
|
-
return {}
|
|
199
|
-
if not isinstance(payload, dict):
|
|
200
|
-
return {}
|
|
201
|
-
return {
|
|
202
|
-
str(cls): str(value).lower()
|
|
203
|
-
for cls, value in payload.items()
|
|
204
|
-
if cls in DEFAULT_FUSION_STRATEGY and str(value).lower() in FUSION_STRATEGIES
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
def fusion_strategy_table(
|
|
209
|
-
overrides: Optional[Mapping[str, str]] = None,
|
|
210
|
-
) -> Dict[str, str]:
|
|
211
|
-
"""Full per-class table: defaults ← simple switch ← env override ← caller.
|
|
212
|
-
|
|
213
|
-
The simple switch (:data:`FUSION_RRF_GATE`, the settings panel's "combine by
|
|
214
|
-
rank") sits *under* the per-class env config on purpose: a person who wrote
|
|
215
|
-
``{"code": "alpha"}`` said something specific, and a single global flag must
|
|
216
|
-
not overrule it.
|
|
217
|
-
"""
|
|
218
|
-
table = dict(DEFAULT_FUSION_STRATEGY)
|
|
219
|
-
if FUSION_RRF_GATE.enabled():
|
|
220
|
-
table = dict.fromkeys(QUERY_CLASSES, "rrf")
|
|
221
|
-
for source in (_env_strategy_overrides(), overrides or {}):
|
|
222
|
-
for cls, value in source.items():
|
|
223
|
-
if cls in table and str(value).lower() in FUSION_STRATEGIES:
|
|
224
|
-
table[cls] = str(value).lower()
|
|
225
|
-
return table
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
def rrf_score(rank: int, *, k: int = DEFAULT_RRF_K) -> float:
|
|
229
|
-
"""One channel's contribution for an item it ranked at ``rank`` (1-based)."""
|
|
230
|
-
return 1.0 / (int(k) + max(1, int(rank)))
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
def rrf_fuse(
|
|
234
|
-
rankings: Mapping[str, Sequence[str]],
|
|
235
|
-
*,
|
|
236
|
-
k: int = DEFAULT_RRF_K,
|
|
237
|
-
weights: Optional[Mapping[str, float]] = None,
|
|
238
|
-
) -> Dict[str, float]:
|
|
239
|
-
"""Reciprocal Rank Fusion over per-channel id rankings.
|
|
240
|
-
|
|
241
|
-
``rankings`` maps a channel name to its ids in rank order (best first).
|
|
242
|
-
An id absent from a channel simply contributes nothing from it — RRF has
|
|
243
|
-
no notion of a "missing score" to guess at, which is the other reason it
|
|
244
|
-
survives channels that fail independently.
|
|
245
|
-
|
|
246
|
-
``weights`` optionally scales a channel's contribution (a channel missing
|
|
247
|
-
from the mapping weighs 1.0). Deterministic and pure.
|
|
248
|
-
"""
|
|
249
|
-
fused: Dict[str, float] = {}
|
|
250
|
-
for channel, ordered in rankings.items():
|
|
251
|
-
weight = 1.0 if weights is None else float(weights.get(channel, 1.0))
|
|
252
|
-
for rank, item_id in enumerate(ordered, start=1):
|
|
253
|
-
fused[item_id] = fused.get(item_id, 0.0) + weight * rrf_score(rank, k=k)
|
|
254
|
-
return fused
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
# ── graph traversal candidate expansion (v11.1.0) ────────────────────────────
|
|
258
|
-
# Both retrieval channels can only return what they matched. A note that
|
|
259
|
-
# never says "postgres" but is one CONTAINS edge away from the one that does
|
|
260
|
-
# is unreachable by either — and it is often the answer. Expansion walks one
|
|
261
|
-
# hop out from the top hits and offers those neighbours as candidates.
|
|
262
|
-
#
|
|
263
|
-
# It is capped, off by default, and reports its own counts, because the
|
|
264
|
-
# failure mode is dilution: an unbounded expansion turns a precise answer into
|
|
265
|
-
# a tour of the graph.
|
|
266
|
-
GRAPH_EXPANSION_ENV = "LATTICEAI_GRAPH_EXPANSION"
|
|
267
|
-
#: Resolved when asked so the settings panel can move it without a restart; the
|
|
268
|
-
#: env parsing is the same set of words the hand-written check used.
|
|
269
|
-
GRAPH_EXPANSION_GATE = FeatureGate(
|
|
270
|
-
GRAPH_EXPANSION_ENV,
|
|
271
|
-
default=False,
|
|
272
|
-
name="graph_expansion",
|
|
273
|
-
detail="Memories one link away from a hit are offered as extra candidates.",
|
|
274
|
-
)
|
|
275
|
-
DEFAULT_EXPANSION_SEEDS = 3
|
|
276
|
-
DEFAULT_EXPANSION_CAP = 5
|
|
277
|
-
#: Expanded candidates inherit a damped share of their seed's score: they are
|
|
278
|
-
#: related to a hit, they are not themselves a hit.
|
|
279
|
-
EXPANSION_DECAY = 0.5
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
def graph_expansion_enabled() -> bool:
|
|
283
|
-
"""True when the expansion gate opts in (default: off)."""
|
|
284
|
-
return GRAPH_EXPANSION_GATE.enabled()
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
def expand_with_neighbors(
|
|
288
|
-
seeds: Sequence[Tuple[str, float]],
|
|
289
|
-
neighbors_fn: Callable[[str], Any],
|
|
290
|
-
*,
|
|
291
|
-
exclude: Optional[Sequence[str]] = None,
|
|
292
|
-
cap: int = DEFAULT_EXPANSION_CAP,
|
|
293
|
-
) -> Tuple[List[Dict[str, Any]], Dict[str, Any]]:
|
|
294
|
-
"""One-hop neighbours of ``seeds`` as extra candidates + an honest report.
|
|
295
|
-
|
|
296
|
-
``seeds`` is ``[(node_id, score)]`` best first; ``neighbors_fn`` is the
|
|
297
|
-
store's ``neighbors``. Returns ``(candidates, report)`` where each
|
|
298
|
-
candidate carries its originating seed and a decayed score, and the report
|
|
299
|
-
states how many seeds were walked, how many candidates were kept, and
|
|
300
|
-
whether the cap bit. A neighbour lookup that fails is skipped and counted,
|
|
301
|
-
never raised: expansion is an optimization, not a dependency.
|
|
302
|
-
"""
|
|
303
|
-
known = {str(item) for item in (exclude or ())}
|
|
304
|
-
cap = max(0, int(cap))
|
|
305
|
-
candidates: List[Dict[str, Any]] = []
|
|
306
|
-
failures = 0
|
|
307
|
-
for seed_id, seed_score in seeds:
|
|
308
|
-
if len(candidates) >= cap:
|
|
309
|
-
break
|
|
310
|
-
try:
|
|
311
|
-
payload = neighbors_fn(seed_id) or {}
|
|
312
|
-
except Exception: # noqa: BLE001 — a bad seed must not fail the search
|
|
313
|
-
failures += 1
|
|
314
|
-
continue
|
|
315
|
-
for node in payload.get("neighbors") or []:
|
|
316
|
-
node_id = str(node.get("id") or "")
|
|
317
|
-
if not node_id or node_id in known:
|
|
318
|
-
continue
|
|
319
|
-
known.add(node_id)
|
|
320
|
-
candidates.append(
|
|
321
|
-
{
|
|
322
|
-
"node": node,
|
|
323
|
-
"seed": str(seed_id),
|
|
324
|
-
"score": round(float(seed_score) * EXPANSION_DECAY, 6),
|
|
325
|
-
}
|
|
326
|
-
)
|
|
327
|
-
if len(candidates) >= cap:
|
|
328
|
-
break
|
|
329
|
-
report = {
|
|
330
|
-
"enabled": True,
|
|
331
|
-
"seeds": len(seeds),
|
|
332
|
-
"added": len(candidates),
|
|
333
|
-
"cap": cap,
|
|
334
|
-
"truncated": len(candidates) >= cap,
|
|
335
|
-
"failed_seeds": failures,
|
|
336
|
-
}
|
|
337
|
-
return candidates, report
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
def fusion_profile(
|
|
341
|
-
query: Any,
|
|
342
|
-
*,
|
|
343
|
-
overrides: Optional[Mapping[str, Mapping[str, float]]] = None,
|
|
344
|
-
strategies: Optional[Mapping[str, str]] = None,
|
|
345
|
-
) -> Dict[str, Any]:
|
|
346
|
-
"""Classify ``query`` and return its resolved fusion weights.
|
|
347
|
-
|
|
348
|
-
Returns ``{"query_class": str, "weights": {keyword, vector, graph},
|
|
349
|
-
"alpha": float, "strategy": "alpha"|"rrf"}``. ``weights`` feeds the
|
|
350
|
-
three-channel service fusion; ``alpha`` feeds the two-channel graph-layer
|
|
351
|
-
fusion; ``strategy`` selects *how* those channels combine and is
|
|
352
|
-
``"alpha"`` for every class unless configured otherwise.
|
|
353
|
-
"""
|
|
354
|
-
query_class = classify_query(query)
|
|
355
|
-
table = fusion_weight_table(overrides)
|
|
356
|
-
resolved = table.get(query_class)
|
|
357
|
-
if resolved is None: # defensive — classify_query only emits known classes
|
|
358
|
-
query_class = "fact"
|
|
359
|
-
resolved = table["fact"]
|
|
360
|
-
return {
|
|
361
|
-
"query_class": query_class,
|
|
362
|
-
"weights": {
|
|
363
|
-
"keyword": resolved["keyword"],
|
|
364
|
-
"vector": resolved["vector"],
|
|
365
|
-
"graph": resolved["graph"],
|
|
366
|
-
},
|
|
367
|
-
"alpha": resolved["alpha"],
|
|
368
|
-
"strategy": fusion_strategy_table(strategies)[query_class],
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
__all__ = [
|
|
373
|
-
"DEFAULT_EXPANSION_CAP",
|
|
374
|
-
"DEFAULT_EXPANSION_SEEDS",
|
|
375
|
-
"DEFAULT_FUSION_STRATEGY",
|
|
376
|
-
"DEFAULT_FUSION_WEIGHTS",
|
|
377
|
-
"DEFAULT_RRF_K",
|
|
378
|
-
"EXPANSION_DECAY",
|
|
379
|
-
"FUSION_RRF_ENV",
|
|
380
|
-
"FUSION_RRF_GATE",
|
|
381
|
-
"FUSION_STRATEGIES",
|
|
382
|
-
"FUSION_STRATEGY_ENV",
|
|
383
|
-
"FUSION_WEIGHTS_ENV",
|
|
384
|
-
"GRAPH_EXPANSION_ENV",
|
|
385
|
-
"GRAPH_EXPANSION_GATE",
|
|
386
|
-
"QUERY_CLASSES",
|
|
387
|
-
"classify_query",
|
|
388
|
-
"expand_with_neighbors",
|
|
389
|
-
"fusion_profile",
|
|
390
|
-
"fusion_strategy_table",
|
|
391
|
-
"fusion_weight_table",
|
|
392
|
-
"graph_expansion_enabled",
|
|
393
|
-
"rrf_fuse",
|
|
394
|
-
"rrf_score",
|
|
395
|
-
]
|
|
@@ -1,175 +0,0 @@
|
|
|
1
|
-
"""Device identity — the sovereignty primitive.
|
|
2
|
-
|
|
3
|
-
Every Lattice installation owns an Ed25519 keypair. Exports are signed by
|
|
4
|
-
it, peers pair against its public key, and imported knowledge records which
|
|
5
|
-
device it came from. The private key never leaves the machine: it lives in
|
|
6
|
-
the OS keyring when one is available, otherwise in a 0600 file under the
|
|
7
|
-
data directory (the storage backend is reported honestly).
|
|
8
|
-
"""
|
|
9
|
-
|
|
10
|
-
from __future__ import annotations
|
|
11
|
-
|
|
12
|
-
import base64
|
|
13
|
-
import hashlib
|
|
14
|
-
import json
|
|
15
|
-
import logging
|
|
16
|
-
import os
|
|
17
|
-
from pathlib import Path
|
|
18
|
-
from typing import Any, Dict, Optional
|
|
19
|
-
|
|
20
|
-
from cryptography.hazmat.primitives import serialization
|
|
21
|
-
from cryptography.hazmat.primitives.asymmetric.ed25519 import (
|
|
22
|
-
Ed25519PrivateKey,
|
|
23
|
-
Ed25519PublicKey,
|
|
24
|
-
)
|
|
25
|
-
|
|
26
|
-
_KEYRING_SERVICE = "lattice-ai-device-identity"
|
|
27
|
-
_KEYRING_ENTRY = "ed25519-private-key"
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
def _b64(data: bytes) -> str:
|
|
31
|
-
return base64.urlsafe_b64encode(data).decode("ascii").rstrip("=")
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
def _unb64(text: str) -> bytes:
|
|
35
|
-
padded = text + "=" * (-len(text) % 4)
|
|
36
|
-
return base64.urlsafe_b64decode(padded.encode("ascii"))
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
def _keyring_opt_in() -> bool:
|
|
40
|
-
"""Keyring storage is opt-in (LATTICEAI_DEVICE_KEY_KEYRING=1).
|
|
41
|
-
|
|
42
|
-
OS keychain access can block or prompt during startup/tests; the default
|
|
43
|
-
is a 0600 file under the data dir, and ``describe()`` reports which
|
|
44
|
-
backend holds the key — no silent security theater either way.
|
|
45
|
-
"""
|
|
46
|
-
return os.getenv("LATTICEAI_DEVICE_KEY_KEYRING", "").strip() in {"1", "true", "yes"}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
class DeviceIdentity:
|
|
50
|
-
"""Loads-or-creates the installation's Ed25519 keypair."""
|
|
51
|
-
|
|
52
|
-
def __init__(self, data_dir: Path, *, use_keyring: Optional[bool] = None):
|
|
53
|
-
if use_keyring is None:
|
|
54
|
-
use_keyring = _keyring_opt_in()
|
|
55
|
-
self._data_dir = Path(data_dir)
|
|
56
|
-
self._key_file = self._data_dir / "device_identity.key"
|
|
57
|
-
self._private: Ed25519PrivateKey
|
|
58
|
-
self.storage: str # "keyring" | "file"
|
|
59
|
-
self._load_or_create(use_keyring)
|
|
60
|
-
|
|
61
|
-
# ── key material ───────────────────────────────────────────────────────
|
|
62
|
-
def _load_or_create(self, use_keyring: bool) -> None:
|
|
63
|
-
raw: Optional[bytes] = None
|
|
64
|
-
backend = "file"
|
|
65
|
-
if use_keyring:
|
|
66
|
-
try:
|
|
67
|
-
import keyring
|
|
68
|
-
|
|
69
|
-
stored = keyring.get_password(_KEYRING_SERVICE, _KEYRING_ENTRY)
|
|
70
|
-
if stored:
|
|
71
|
-
raw = _unb64(stored)
|
|
72
|
-
backend = "keyring"
|
|
73
|
-
except Exception as exc:
|
|
74
|
-
logging.debug("device identity: keyring unavailable (%s)", exc)
|
|
75
|
-
if raw is None and self._key_file.exists():
|
|
76
|
-
raw = _unb64(self._key_file.read_text().strip())
|
|
77
|
-
backend = "file"
|
|
78
|
-
if raw is None:
|
|
79
|
-
key = Ed25519PrivateKey.generate()
|
|
80
|
-
raw = key.private_bytes(
|
|
81
|
-
serialization.Encoding.Raw,
|
|
82
|
-
serialization.PrivateFormat.Raw,
|
|
83
|
-
serialization.NoEncryption(),
|
|
84
|
-
)
|
|
85
|
-
backend = self._persist_new(raw, use_keyring)
|
|
86
|
-
self._private = Ed25519PrivateKey.from_private_bytes(raw)
|
|
87
|
-
self.storage = backend
|
|
88
|
-
|
|
89
|
-
def _persist_new(self, raw: bytes, use_keyring: bool) -> str:
|
|
90
|
-
if use_keyring:
|
|
91
|
-
try:
|
|
92
|
-
import keyring
|
|
93
|
-
|
|
94
|
-
keyring.set_password(_KEYRING_SERVICE, _KEYRING_ENTRY, _b64(raw))
|
|
95
|
-
return "keyring"
|
|
96
|
-
except Exception as exc:
|
|
97
|
-
logging.debug("device identity: keyring store failed (%s); using file", exc)
|
|
98
|
-
self._data_dir.mkdir(parents=True, exist_ok=True)
|
|
99
|
-
self._key_file.write_text(_b64(raw))
|
|
100
|
-
os.chmod(self._key_file, 0o600)
|
|
101
|
-
return "file"
|
|
102
|
-
|
|
103
|
-
# ── public surface ─────────────────────────────────────────────────────
|
|
104
|
-
@property
|
|
105
|
-
def public_key_b64(self) -> str:
|
|
106
|
-
return _b64(
|
|
107
|
-
self._private.public_key().public_bytes(
|
|
108
|
-
serialization.Encoding.Raw, serialization.PublicFormat.Raw
|
|
109
|
-
)
|
|
110
|
-
)
|
|
111
|
-
|
|
112
|
-
@property
|
|
113
|
-
def fingerprint(self) -> str:
|
|
114
|
-
"""Short human-comparable id: sha256 of the raw public key."""
|
|
115
|
-
raw = self._private.public_key().public_bytes(
|
|
116
|
-
serialization.Encoding.Raw, serialization.PublicFormat.Raw
|
|
117
|
-
)
|
|
118
|
-
digest = hashlib.sha256(raw).hexdigest()
|
|
119
|
-
return ":".join(digest[i : i + 4] for i in range(0, 16, 4))
|
|
120
|
-
|
|
121
|
-
def describe(self) -> Dict[str, Any]:
|
|
122
|
-
return {
|
|
123
|
-
"fingerprint": self.fingerprint,
|
|
124
|
-
"public_key": self.public_key_b64,
|
|
125
|
-
"algorithm": "ed25519",
|
|
126
|
-
"storage": self.storage,
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
# ── signing ────────────────────────────────────────────────────────────
|
|
130
|
-
def sign(self, payload: bytes) -> str:
|
|
131
|
-
return _b64(self._private.sign(payload))
|
|
132
|
-
|
|
133
|
-
def sign_manifest(self, manifest: Dict[str, Any]) -> Dict[str, Any]:
|
|
134
|
-
"""Detached signature over the canonical JSON of a manifest."""
|
|
135
|
-
canonical = json.dumps(manifest, sort_keys=True, ensure_ascii=False).encode("utf-8")
|
|
136
|
-
return {
|
|
137
|
-
"algorithm": "ed25519",
|
|
138
|
-
"public_key": self.public_key_b64,
|
|
139
|
-
"fingerprint": self.fingerprint,
|
|
140
|
-
"signature": self.sign(canonical),
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
def fingerprint_of(public_key_b64: str) -> str:
|
|
145
|
-
"""Human-comparable fingerprint of an Ed25519 public key.
|
|
146
|
-
|
|
147
|
-
Raises ValueError when the input is not a valid key — the pairing flow
|
|
148
|
-
uses this as its validation gate.
|
|
149
|
-
"""
|
|
150
|
-
raw = _unb64(public_key_b64)
|
|
151
|
-
Ed25519PublicKey.from_public_bytes(raw) # validates; raises on garbage
|
|
152
|
-
digest = hashlib.sha256(raw).hexdigest()
|
|
153
|
-
return ":".join(digest[i : i + 4] for i in range(0, 16, 4))
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
def verify_signature(public_key_b64: str, payload: bytes, signature_b64: str) -> bool:
|
|
157
|
-
"""True iff ``signature`` is valid for ``payload`` under the given key."""
|
|
158
|
-
try:
|
|
159
|
-
key = Ed25519PublicKey.from_public_bytes(_unb64(public_key_b64))
|
|
160
|
-
key.verify(_unb64(signature_b64), payload)
|
|
161
|
-
return True
|
|
162
|
-
except Exception:
|
|
163
|
-
return False
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
def verify_manifest(manifest: Dict[str, Any], signature_block: Dict[str, Any]) -> bool:
|
|
167
|
-
canonical = json.dumps(manifest, sort_keys=True, ensure_ascii=False).encode("utf-8")
|
|
168
|
-
return verify_signature(
|
|
169
|
-
str(signature_block.get("public_key") or ""),
|
|
170
|
-
canonical,
|
|
171
|
-
str(signature_block.get("signature") or ""),
|
|
172
|
-
)
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
__all__ = ["DeviceIdentity", "verify_signature", "verify_manifest"]
|