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
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Dump the Python server-message catalog for the Rust parity test (WP-I3).
|
|
3
|
+
|
|
4
|
+
``lattice-core::messages`` is a port of :mod:`latticeai.core.messages`. A port
|
|
5
|
+
is only worth having if something keeps proving the two still agree, so this
|
|
6
|
+
script is the Python half of that proof: it imports the live catalog and writes
|
|
7
|
+
every id rendered in both languages (with representative interpolation args
|
|
8
|
+
where a template takes them) plus a ``resolve_language`` vector table that
|
|
9
|
+
walks every branch of the Python function.
|
|
10
|
+
|
|
11
|
+
The Rust side loads ``rust/fixtures/messages_catalog.json`` and asserts
|
|
12
|
+
byte-identical ``text`` / ``http_error`` bodies and matching language
|
|
13
|
+
resolution. Determinism is the constraint: keys are sorted, interpolation
|
|
14
|
+
args are a stable subset of :data:`REPRESENTATIVE_ARGS`, and running the
|
|
15
|
+
script twice must produce the same bytes.
|
|
16
|
+
|
|
17
|
+
Usage::
|
|
18
|
+
|
|
19
|
+
.venv/bin/python scripts/gen_messages_catalog_fixture.py
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import json
|
|
25
|
+
import re
|
|
26
|
+
import sys
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
from typing import Any, Dict, List, Mapping, Optional
|
|
29
|
+
|
|
30
|
+
REPO_ROOT = Path(__file__).resolve().parents[1]
|
|
31
|
+
if str(REPO_ROOT) not in sys.path:
|
|
32
|
+
sys.path.insert(0, str(REPO_ROOT))
|
|
33
|
+
|
|
34
|
+
from latticeai.core.messages import ( # noqa: E402
|
|
35
|
+
DEFAULT_LANGUAGE,
|
|
36
|
+
LANGUAGE_HEADER,
|
|
37
|
+
MESSAGES,
|
|
38
|
+
SUPPORTED_LANGUAGES,
|
|
39
|
+
resolve_language,
|
|
40
|
+
translate,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
FIXTURE_PATH = REPO_ROOT / "rust" / "fixtures" / "messages_catalog.json"
|
|
44
|
+
SCHEMA = "messages-catalog-parity/v1"
|
|
45
|
+
|
|
46
|
+
#: ``{name}`` in a catalog string — the same substitution ``translate`` does.
|
|
47
|
+
_PLACEHOLDER = re.compile(r"\{([A-Za-z_][A-Za-z0-9_]*)\}")
|
|
48
|
+
|
|
49
|
+
#: One value per placeholder the catalog currently uses. Values are strings
|
|
50
|
+
#: because ``translate`` runs ``str(value)`` before replacing; dumping the
|
|
51
|
+
#: already-stringified form keeps the fixture language-neutral.
|
|
52
|
+
REPRESENTATIVE_ARGS: Dict[str, str] = {
|
|
53
|
+
"allowed": "user, assistant",
|
|
54
|
+
"arg": "label",
|
|
55
|
+
"cap": "50",
|
|
56
|
+
"feature": "telepathy",
|
|
57
|
+
"kind": "docx",
|
|
58
|
+
"limit": "26214400",
|
|
59
|
+
"max": "100",
|
|
60
|
+
"mcp_id": "slack",
|
|
61
|
+
"min": "1",
|
|
62
|
+
"model": "gemma-4",
|
|
63
|
+
"name": "notes.md",
|
|
64
|
+
"op": "add_node",
|
|
65
|
+
"provider": "mlx",
|
|
66
|
+
"reason": "no-gpu",
|
|
67
|
+
"role": "system",
|
|
68
|
+
"size": "31457280",
|
|
69
|
+
"source": "notion",
|
|
70
|
+
"status": "rejected",
|
|
71
|
+
"suffix": ".ogg",
|
|
72
|
+
"tool": "write_file",
|
|
73
|
+
"value": "maybe",
|
|
74
|
+
"workspace": "ws-alpha",
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class _Request:
|
|
79
|
+
"""Minimal stand-in: ``resolve_language`` only ever reads ``.headers``."""
|
|
80
|
+
|
|
81
|
+
def __init__(self, headers: Optional[Mapping[str, str]]) -> None:
|
|
82
|
+
self.headers = headers
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class _Bare:
|
|
86
|
+
"""No ``headers`` attribute — the ``getattr(..., None)`` branch."""
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _placeholders(*texts: str) -> List[str]:
|
|
90
|
+
names = set()
|
|
91
|
+
for text in texts:
|
|
92
|
+
names.update(_PLACEHOLDER.findall(text))
|
|
93
|
+
return sorted(names)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _args_for(key: str) -> Dict[str, str]:
|
|
97
|
+
entry = MESSAGES[key]
|
|
98
|
+
names = _placeholders(*entry.values())
|
|
99
|
+
missing = [name for name in names if name not in REPRESENTATIVE_ARGS]
|
|
100
|
+
if missing:
|
|
101
|
+
raise SystemExit(
|
|
102
|
+
f"{key} uses placeholders {missing} with no representative args; "
|
|
103
|
+
"add them to REPRESENTATIVE_ARGS"
|
|
104
|
+
)
|
|
105
|
+
return {name: REPRESENTATIVE_ARGS[name] for name in names}
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _render_all() -> List[Dict[str, Any]]:
|
|
109
|
+
rows: List[Dict[str, Any]] = []
|
|
110
|
+
for key in sorted(MESSAGES):
|
|
111
|
+
args = _args_for(key)
|
|
112
|
+
for language in SUPPORTED_LANGUAGES:
|
|
113
|
+
text = translate(key, language, **args)
|
|
114
|
+
rows.append(
|
|
115
|
+
{
|
|
116
|
+
"args": args,
|
|
117
|
+
"http_error": {"detail": text},
|
|
118
|
+
"id": key,
|
|
119
|
+
"lang": language,
|
|
120
|
+
"text": text,
|
|
121
|
+
}
|
|
122
|
+
)
|
|
123
|
+
return rows
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _resolve_row(
|
|
127
|
+
case_id: str,
|
|
128
|
+
*,
|
|
129
|
+
bare: bool = False,
|
|
130
|
+
x_lattice_language: Optional[str] = None,
|
|
131
|
+
accept_language: Optional[str] = None,
|
|
132
|
+
) -> Dict[str, Any]:
|
|
133
|
+
if bare:
|
|
134
|
+
expected = resolve_language(_Bare())
|
|
135
|
+
else:
|
|
136
|
+
headers: Dict[str, str] = {}
|
|
137
|
+
if x_lattice_language is not None:
|
|
138
|
+
headers[LANGUAGE_HEADER] = x_lattice_language
|
|
139
|
+
if accept_language is not None:
|
|
140
|
+
headers["accept-language"] = accept_language
|
|
141
|
+
expected = resolve_language(_Request(headers))
|
|
142
|
+
return {
|
|
143
|
+
"accept_language": accept_language,
|
|
144
|
+
"bare": bare,
|
|
145
|
+
"expected": expected,
|
|
146
|
+
"id": case_id,
|
|
147
|
+
"x_lattice_language": x_lattice_language,
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _resolve_vectors() -> List[Dict[str, Any]]:
|
|
152
|
+
"""Every branch of ``resolve_language`` / ``_normalize``.
|
|
153
|
+
|
|
154
|
+
Names are stable ids, not prose: the Rust test reports the id on mismatch.
|
|
155
|
+
Expected values come from the live Python function, never from a restated
|
|
156
|
+
table, so a Python behaviour change fails the next generate-and-compare.
|
|
157
|
+
"""
|
|
158
|
+
cases = [
|
|
159
|
+
# getattr(request, "headers", None) is None → default
|
|
160
|
+
_resolve_row("bare_request", bare=True),
|
|
161
|
+
# empty mapping is observationally the same as missing headers
|
|
162
|
+
_resolve_row("empty_headers"),
|
|
163
|
+
# explicit product header
|
|
164
|
+
_resolve_row("explicit_ko", x_lattice_language="ko"),
|
|
165
|
+
_resolve_row("explicit_en", x_lattice_language="en"),
|
|
166
|
+
_resolve_row("explicit_en_uppercase", x_lattice_language="EN"),
|
|
167
|
+
_resolve_row("explicit_en_gb", x_lattice_language="en-GB"),
|
|
168
|
+
_resolve_row("explicit_en_us_underscore", x_lattice_language="en_US"),
|
|
169
|
+
_resolve_row("explicit_ko_kr", x_lattice_language="ko-KR"),
|
|
170
|
+
_resolve_row("explicit_ko_kr_underscore", x_lattice_language="ko_kr"),
|
|
171
|
+
_resolve_row(
|
|
172
|
+
"explicit_wins_over_accept",
|
|
173
|
+
x_lattice_language="en",
|
|
174
|
+
accept_language="ko-KR,ko;q=0.9",
|
|
175
|
+
),
|
|
176
|
+
_resolve_row(
|
|
177
|
+
"explicit_ko_wins_over_accept_en",
|
|
178
|
+
x_lattice_language="ko",
|
|
179
|
+
accept_language="en",
|
|
180
|
+
),
|
|
181
|
+
# explicit that _normalize rejects falls through to Accept-Language
|
|
182
|
+
_resolve_row(
|
|
183
|
+
"explicit_unsupported_falls_through",
|
|
184
|
+
x_lattice_language="fr",
|
|
185
|
+
accept_language="en",
|
|
186
|
+
),
|
|
187
|
+
_resolve_row(
|
|
188
|
+
"explicit_unsupported_no_accept_defaults",
|
|
189
|
+
x_lattice_language="fr",
|
|
190
|
+
),
|
|
191
|
+
_resolve_row(
|
|
192
|
+
"explicit_empty_falls_through",
|
|
193
|
+
x_lattice_language="",
|
|
194
|
+
accept_language="en",
|
|
195
|
+
),
|
|
196
|
+
_resolve_row(
|
|
197
|
+
"explicit_whitespace_falls_through",
|
|
198
|
+
x_lattice_language=" ",
|
|
199
|
+
accept_language="en",
|
|
200
|
+
),
|
|
201
|
+
# explicit does not split on ';', so this is not a language tag
|
|
202
|
+
_resolve_row(
|
|
203
|
+
"explicit_qvalue_not_stripped",
|
|
204
|
+
x_lattice_language="en;q=1",
|
|
205
|
+
accept_language="ko",
|
|
206
|
+
),
|
|
207
|
+
_resolve_row(
|
|
208
|
+
"explicit_star_falls_through",
|
|
209
|
+
x_lattice_language="*",
|
|
210
|
+
accept_language="en",
|
|
211
|
+
),
|
|
212
|
+
_resolve_row(
|
|
213
|
+
"explicit_english_word_falls_through",
|
|
214
|
+
x_lattice_language="english",
|
|
215
|
+
accept_language="ko",
|
|
216
|
+
),
|
|
217
|
+
# Accept-Language: first supported tag in send order, q-values ignored
|
|
218
|
+
_resolve_row(
|
|
219
|
+
"accept_en_gb_then_ko",
|
|
220
|
+
accept_language="en-GB,en;q=0.9,ko;q=0.8",
|
|
221
|
+
),
|
|
222
|
+
_resolve_row("accept_ko_kr", accept_language="ko-KR,ko;q=0.9"),
|
|
223
|
+
_resolve_row(
|
|
224
|
+
"accept_unsupported_then_en",
|
|
225
|
+
accept_language="fr-FR,fr;q=0.9,en;q=0.8",
|
|
226
|
+
),
|
|
227
|
+
_resolve_row("accept_zh_cn_defaults", accept_language="zh-CN"),
|
|
228
|
+
_resolve_row("accept_xx_defaults", accept_language="xx"),
|
|
229
|
+
_resolve_row("accept_star_defaults", accept_language="*"),
|
|
230
|
+
_resolve_row("accept_empty_defaults", accept_language=""),
|
|
231
|
+
_resolve_row("accept_whitespace_defaults", accept_language=" "),
|
|
232
|
+
_resolve_row("accept_en_us_underscore", accept_language="en_US"),
|
|
233
|
+
_resolve_row("accept_en_us_mixed_case", accept_language="EN-us"),
|
|
234
|
+
_resolve_row("accept_padded_en", accept_language=" en "),
|
|
235
|
+
_resolve_row("accept_de_fr_ko", accept_language="de,fr,ko"),
|
|
236
|
+
_resolve_row("accept_de_fr_defaults", accept_language="de,fr"),
|
|
237
|
+
# q-value disagrees with send order — send order wins
|
|
238
|
+
_resolve_row(
|
|
239
|
+
"accept_order_not_qvalue",
|
|
240
|
+
accept_language="ko;q=0.1,en;q=0.9",
|
|
241
|
+
),
|
|
242
|
+
_resolve_row("accept_en_with_q", accept_language="en;q=0.9"),
|
|
243
|
+
_resolve_row(
|
|
244
|
+
"accept_ja_zh_then_ko",
|
|
245
|
+
accept_language="ja-JP,zh-CN,ko-KR",
|
|
246
|
+
),
|
|
247
|
+
_resolve_row(
|
|
248
|
+
"accept_space_after_comma",
|
|
249
|
+
accept_language="en-US, en;q=0.9",
|
|
250
|
+
),
|
|
251
|
+
_resolve_row("accept_en_gb_with_q", accept_language="en-GB;q=0.8"),
|
|
252
|
+
_resolve_row("accept_leading_comma", accept_language=",en"),
|
|
253
|
+
_resolve_row("accept_trailing_comma", accept_language="en,"),
|
|
254
|
+
_resolve_row(
|
|
255
|
+
"accept_fr_then_low_q_en",
|
|
256
|
+
accept_language="fr;q=1,en;q=0.1",
|
|
257
|
+
),
|
|
258
|
+
_resolve_row("accept_ko_kr_then_en", accept_language="ko-kr;q=0.8,en"),
|
|
259
|
+
_resolve_row("accept_tab_defaults", accept_language="\t"),
|
|
260
|
+
_resolve_row("accept_only_semicolon", accept_language=";q=0.9"),
|
|
261
|
+
]
|
|
262
|
+
cases.sort(key=lambda row: row["id"])
|
|
263
|
+
return cases
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def build() -> Dict[str, Any]:
|
|
267
|
+
catalog = {
|
|
268
|
+
key: {"en": MESSAGES[key]["en"], "ko": MESSAGES[key]["ko"]}
|
|
269
|
+
for key in sorted(MESSAGES)
|
|
270
|
+
}
|
|
271
|
+
return {
|
|
272
|
+
"catalog": catalog,
|
|
273
|
+
"default_language": DEFAULT_LANGUAGE,
|
|
274
|
+
"language_header": LANGUAGE_HEADER,
|
|
275
|
+
"renders": _render_all(),
|
|
276
|
+
"resolve_language": _resolve_vectors(),
|
|
277
|
+
"schema": SCHEMA,
|
|
278
|
+
"supported_languages": list(SUPPORTED_LANGUAGES),
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def dumps(payload: Dict[str, Any]) -> str:
|
|
283
|
+
return json.dumps(payload, ensure_ascii=False, indent=2, sort_keys=True) + "\n"
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def main() -> int:
|
|
287
|
+
payload = build()
|
|
288
|
+
FIXTURE_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
289
|
+
text = dumps(payload)
|
|
290
|
+
FIXTURE_PATH.write_text(text, encoding="utf-8")
|
|
291
|
+
n_ids = len(payload["catalog"])
|
|
292
|
+
n_renders = len(payload["renders"])
|
|
293
|
+
n_resolve = len(payload["resolve_language"])
|
|
294
|
+
print(
|
|
295
|
+
f"wrote {FIXTURE_PATH.relative_to(REPO_ROOT)} "
|
|
296
|
+
f"({n_ids} ids, {n_renders} renders, {n_resolve} resolve vectors)"
|
|
297
|
+
)
|
|
298
|
+
return 0
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
if __name__ == "__main__":
|
|
302
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Split the committed OpenAPI contract into per-family fragments (v11.6.0 §I5).
|
|
3
|
+
|
|
4
|
+
``frontend/openapi.json`` stays the single committed contract. v11.6.0 moves
|
|
5
|
+
~439 of its 463 operations out of Python and into the Rust gateway, and a spec
|
|
6
|
+
generated from a shrinking FastAPI app can only describe what is left. So the
|
|
7
|
+
spec of every migrated family is committed *as data* next to the crates that
|
|
8
|
+
serve it, and this script is what carves it out:
|
|
9
|
+
|
|
10
|
+
rust/fixtures/openapi/<family>.json one fragment per work-package family
|
|
11
|
+
rust/fixtures/openapi/worker_keep.json the surface the Python worker keeps
|
|
12
|
+
rust/fixtures/openapi/_envelope.json everything that is not a path
|
|
13
|
+
rust/fixtures/openapi/_index.json counts + per-fragment sha256
|
|
14
|
+
|
|
15
|
+
The route → family map is ``scripts/openapi_route_families.json`` (hand
|
|
16
|
+
maintained; ``tests/unit/test_openapi_composition.py`` proves it stays a
|
|
17
|
+
bijection with the committed spec). ``scripts/compose_openapi.py`` runs the
|
|
18
|
+
split backwards and byte-compares the result against the committed contract,
|
|
19
|
+
so the fragments cannot rot: either they still reassemble into the exact
|
|
20
|
+
committed bytes, or CI says so.
|
|
21
|
+
|
|
22
|
+
Three properties this file exists to hold:
|
|
23
|
+
|
|
24
|
+
* **Byte-exactness.** ``scripts/export_openapi.py`` serialises with
|
|
25
|
+
``json.dumps(schema, sort_keys=True, indent=2) + "\\n"``. Sorted keys mean
|
|
26
|
+
the committed bytes are a pure function of the *content*, so composition
|
|
27
|
+
needs no key-order metadata — but it does need the same serialiser, which
|
|
28
|
+
lives here (``canonical_json``) and is imported by the composer rather than
|
|
29
|
+
retyped.
|
|
30
|
+
* **Order is still recorded.** Arrays inside a spec (``required``, ``anyOf``,
|
|
31
|
+
``enum``) carry meaning and are preserved verbatim; on top of that each
|
|
32
|
+
fragment records ``path_order`` / ``operation_order`` — the order the
|
|
33
|
+
committed spec lists them in — so a Rust test can compare its router's
|
|
34
|
+
declared paths against the fragment without re-deriving a sort.
|
|
35
|
+
* **Fragments are self-contained.** Each one carries the transitive closure of
|
|
36
|
+
the ``#/components/schemas`` its operations reference, so a crate owns its
|
|
37
|
+
whole contract. Schemas shared by several families are duplicated across
|
|
38
|
+
fragments on purpose; the composer asserts the copies are identical. Each one
|
|
39
|
+
also carries ``greedy_path_params``: the FastAPI ``{name:path}`` converters
|
|
40
|
+
the schema flattens to ``{name}``. Those segments match slashes, and a port
|
|
41
|
+
that mounts a plain capture for them 404s on every id containing a ``/``.
|
|
42
|
+
|
|
43
|
+
Usage::
|
|
44
|
+
|
|
45
|
+
.venv/bin/python scripts/gen_openapi_fragments.py # rewrite fragments
|
|
46
|
+
.venv/bin/python scripts/gen_openapi_fragments.py --check # verify only
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
from __future__ import annotations
|
|
50
|
+
|
|
51
|
+
import argparse
|
|
52
|
+
import hashlib
|
|
53
|
+
import json
|
|
54
|
+
import sys
|
|
55
|
+
from pathlib import Path
|
|
56
|
+
from typing import Any, Dict, Iterable, List, Mapping, Set, Tuple
|
|
57
|
+
|
|
58
|
+
REPO_ROOT = Path(__file__).resolve().parents[1]
|
|
59
|
+
SPEC_PATH = REPO_ROOT / "frontend" / "openapi.json"
|
|
60
|
+
MAPPING_PATH = REPO_ROOT / "scripts" / "openapi_route_families.json"
|
|
61
|
+
FRAGMENT_DIR = REPO_ROOT / "rust" / "fixtures" / "openapi"
|
|
62
|
+
|
|
63
|
+
ENVELOPE_NAME = "_envelope.json"
|
|
64
|
+
INDEX_NAME = "_index.json"
|
|
65
|
+
WORKER_FAMILY = "worker_keep"
|
|
66
|
+
GENERATOR = "scripts/gen_openapi_fragments.py"
|
|
67
|
+
|
|
68
|
+
#: The exact serialisation ``scripts/export_openapi.py`` writes. Recorded in
|
|
69
|
+
#: ``_index.json`` as well, so the contract is readable without this file.
|
|
70
|
+
SERIALIZATION = {
|
|
71
|
+
"encoder": "json.dumps",
|
|
72
|
+
"sort_keys": True,
|
|
73
|
+
"indent": 2,
|
|
74
|
+
"ensure_ascii": True,
|
|
75
|
+
"trailing_newline": True,
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
SCHEMA_REF_PREFIX = "#/components/schemas/"
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def canonical_json(payload: Any) -> str:
|
|
82
|
+
"""Serialise exactly the way the committed contract is serialised."""
|
|
83
|
+
return json.dumps(payload, sort_keys=True, indent=2) + "\n"
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def sha256_text(text: str) -> str:
|
|
87
|
+
return hashlib.sha256(text.encode("utf-8")).hexdigest()
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def load_json(path: Path) -> Any:
|
|
91
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def load_spec(path: Path = SPEC_PATH) -> Dict[str, Any]:
|
|
95
|
+
spec: Dict[str, Any] = load_json(path)
|
|
96
|
+
return spec
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def load_mapping(path: Path = MAPPING_PATH) -> Dict[str, Any]:
|
|
100
|
+
mapping: Dict[str, Any] = load_json(path)
|
|
101
|
+
return mapping
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def operation_key(method: str, path: str) -> str:
|
|
105
|
+
"""``GET /workspace`` — the key shape used by the map and the fragments."""
|
|
106
|
+
return f"{method.upper()} {path}"
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def spec_operations(spec: Mapping[str, Any]) -> List[Tuple[str, str, str]]:
|
|
110
|
+
"""Every operation as ``(key, path, method)`` in committed-spec order."""
|
|
111
|
+
found: List[Tuple[str, str, str]] = []
|
|
112
|
+
for path, item in spec["paths"].items():
|
|
113
|
+
for method in item:
|
|
114
|
+
found.append((operation_key(method, path), path, method))
|
|
115
|
+
return found
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def schema_refs(node: Any) -> Set[str]:
|
|
119
|
+
"""Names of every ``#/components/schemas/X`` reference reachable in ``node``."""
|
|
120
|
+
names: Set[str] = set()
|
|
121
|
+
if isinstance(node, dict):
|
|
122
|
+
for key, value in node.items():
|
|
123
|
+
if key == "$ref" and isinstance(value, str) and value.startswith(SCHEMA_REF_PREFIX):
|
|
124
|
+
names.add(value[len(SCHEMA_REF_PREFIX) :])
|
|
125
|
+
else:
|
|
126
|
+
names.update(schema_refs(value))
|
|
127
|
+
elif isinstance(node, list):
|
|
128
|
+
for value in node:
|
|
129
|
+
names.update(schema_refs(value))
|
|
130
|
+
return names
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def schema_closure(seeds: Iterable[str], schemas: Mapping[str, Any]) -> Set[str]:
|
|
134
|
+
"""The transitive closure of ``seeds`` under ``$ref`` inside ``schemas``."""
|
|
135
|
+
reached: Set[str] = set()
|
|
136
|
+
pending = list(seeds)
|
|
137
|
+
while pending:
|
|
138
|
+
name = pending.pop()
|
|
139
|
+
if name in reached:
|
|
140
|
+
continue
|
|
141
|
+
reached.add(name)
|
|
142
|
+
pending.extend(schema_refs(schemas.get(name, {})) - reached)
|
|
143
|
+
return reached
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def family_of(key: str, mapping: Mapping[str, Any]) -> str:
|
|
147
|
+
"""The family a route belongs to, or a loud failure if it is unmapped."""
|
|
148
|
+
entry = mapping["operations"].get(key)
|
|
149
|
+
if entry is None:
|
|
150
|
+
raise KeyError(
|
|
151
|
+
f"{key} is not in {MAPPING_PATH.name}. Every operation in "
|
|
152
|
+
f"{SPEC_PATH.name} needs a family — add it there first."
|
|
153
|
+
)
|
|
154
|
+
family: str = entry["family"]
|
|
155
|
+
return family
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def split_spec(spec: Mapping[str, Any], mapping: Mapping[str, Any]) -> Dict[str, Dict[str, Any]]:
|
|
159
|
+
"""Carve the spec into one fragment payload per family."""
|
|
160
|
+
schemas = spec["components"]["schemas"]
|
|
161
|
+
families: Dict[str, Dict[str, Any]] = {}
|
|
162
|
+
for name, meta in mapping["families"].items():
|
|
163
|
+
families[name] = {
|
|
164
|
+
"family": name,
|
|
165
|
+
"wp": meta["wp"],
|
|
166
|
+
"target": meta["target"],
|
|
167
|
+
"summary": meta["summary"],
|
|
168
|
+
"generated_by": GENERATOR,
|
|
169
|
+
"source": SPEC_PATH.relative_to(REPO_ROOT).as_posix(),
|
|
170
|
+
"spec_version": spec["info"]["version"],
|
|
171
|
+
"path_order": [],
|
|
172
|
+
"operation_order": [],
|
|
173
|
+
"greedy_path_params": {},
|
|
174
|
+
"paths": {},
|
|
175
|
+
"components": {"schemas": {}},
|
|
176
|
+
}
|
|
177
|
+
greedy: Mapping[str, str] = mapping.get("greedy_path_params", {})
|
|
178
|
+
|
|
179
|
+
seeds: Dict[str, Set[str]] = {name: set() for name in families}
|
|
180
|
+
for key, path, method in spec_operations(spec):
|
|
181
|
+
name = family_of(key, mapping)
|
|
182
|
+
fragment = families[name]
|
|
183
|
+
if path not in fragment["paths"]:
|
|
184
|
+
fragment["paths"][path] = {}
|
|
185
|
+
fragment["path_order"].append(path)
|
|
186
|
+
fragment["paths"][path][method] = spec["paths"][path][method]
|
|
187
|
+
fragment["operation_order"].append(key)
|
|
188
|
+
if key in greedy:
|
|
189
|
+
fragment["greedy_path_params"][key] = greedy[key]
|
|
190
|
+
seeds[name].update(schema_refs(spec["paths"][path][method]))
|
|
191
|
+
|
|
192
|
+
for name, fragment in families.items():
|
|
193
|
+
for schema_name in sorted(schema_closure(seeds[name], schemas)):
|
|
194
|
+
fragment["components"]["schemas"][schema_name] = schemas[schema_name]
|
|
195
|
+
fragment["path_count"] = len(fragment["paths"])
|
|
196
|
+
fragment["operation_count"] = len(fragment["operation_order"])
|
|
197
|
+
fragment["schema_count"] = len(fragment["components"]["schemas"])
|
|
198
|
+
return families
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def build_envelope(spec: Mapping[str, Any], attributed: Set[str]) -> Dict[str, Any]:
|
|
202
|
+
"""Everything the fragments do not carry: the spec minus paths and schemas.
|
|
203
|
+
|
|
204
|
+
``unattributed`` schemas — defined but referenced by no operation — would
|
|
205
|
+
otherwise vanish from the composition, so they ride along here. Today the
|
|
206
|
+
committed spec has none; the field is what proves that, release to release.
|
|
207
|
+
"""
|
|
208
|
+
schemas = spec["components"]["schemas"]
|
|
209
|
+
unattributed = sorted(set(schemas) - attributed)
|
|
210
|
+
skeleton: Dict[str, Any] = {
|
|
211
|
+
key: value for key, value in spec.items() if key not in ("paths", "components")
|
|
212
|
+
}
|
|
213
|
+
components = {key: value for key, value in spec["components"].items() if key != "schemas"}
|
|
214
|
+
components["schemas"] = {name: schemas[name] for name in unattributed}
|
|
215
|
+
skeleton["components"] = components
|
|
216
|
+
return {
|
|
217
|
+
"family": "_envelope",
|
|
218
|
+
"generated_by": GENERATOR,
|
|
219
|
+
"source": SPEC_PATH.relative_to(REPO_ROOT).as_posix(),
|
|
220
|
+
"spec_version": spec["info"]["version"],
|
|
221
|
+
"note": [
|
|
222
|
+
"The non-path half of the contract: openapi version, info, and any",
|
|
223
|
+
"component that no operation references. scripts/compose_openapi.py",
|
|
224
|
+
"starts from this skeleton and merges every fragment's paths and",
|
|
225
|
+
"schemas into it.",
|
|
226
|
+
],
|
|
227
|
+
"unattributed_schemas": unattributed,
|
|
228
|
+
"spec": skeleton,
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def build_index(
|
|
233
|
+
spec: Mapping[str, Any],
|
|
234
|
+
fragments: Mapping[str, Dict[str, Any]],
|
|
235
|
+
envelope: Mapping[str, Any],
|
|
236
|
+
) -> Dict[str, Any]:
|
|
237
|
+
"""The manifest: what was written, how big it is, and its sha256."""
|
|
238
|
+
entries = []
|
|
239
|
+
for name in sorted(fragments):
|
|
240
|
+
fragment = fragments[name]
|
|
241
|
+
entries.append(
|
|
242
|
+
{
|
|
243
|
+
"family": name,
|
|
244
|
+
"file": f"{name}.json",
|
|
245
|
+
"paths": fragment["path_count"],
|
|
246
|
+
"operations": fragment["operation_count"],
|
|
247
|
+
"schemas": fragment["schema_count"],
|
|
248
|
+
"sha256": sha256_text(canonical_json(fragment)),
|
|
249
|
+
}
|
|
250
|
+
)
|
|
251
|
+
return {
|
|
252
|
+
"generated_by": GENERATOR,
|
|
253
|
+
"source": SPEC_PATH.relative_to(REPO_ROOT).as_posix(),
|
|
254
|
+
"spec_version": spec["info"]["version"],
|
|
255
|
+
"note": [
|
|
256
|
+
"Committed manifest for the per-family OpenAPI fragments.",
|
|
257
|
+
"'serialization' is the encoding frontend/openapi.json is written",
|
|
258
|
+
"with; composition reproduces the committed bytes only because the",
|
|
259
|
+
"composer uses the same one.",
|
|
260
|
+
"Regenerate with: .venv/bin/python scripts/gen_openapi_fragments.py",
|
|
261
|
+
],
|
|
262
|
+
"serialization": dict(SERIALIZATION),
|
|
263
|
+
"totals": {
|
|
264
|
+
"paths": len(spec["paths"]),
|
|
265
|
+
"operations": sum(entry["operations"] for entry in entries),
|
|
266
|
+
"schemas": len(spec["components"]["schemas"]),
|
|
267
|
+
"families": len(entries),
|
|
268
|
+
},
|
|
269
|
+
"envelope": {
|
|
270
|
+
"file": ENVELOPE_NAME,
|
|
271
|
+
"unattributed_schemas": len(envelope["unattributed_schemas"]),
|
|
272
|
+
"sha256": sha256_text(canonical_json(envelope)),
|
|
273
|
+
},
|
|
274
|
+
"fragments": entries,
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def render_all(spec: Mapping[str, Any], mapping: Mapping[str, Any]) -> Dict[str, str]:
|
|
279
|
+
"""Filename → file text for every artefact this script owns."""
|
|
280
|
+
fragments = split_spec(spec, mapping)
|
|
281
|
+
attributed: Set[str] = set()
|
|
282
|
+
for fragment in fragments.values():
|
|
283
|
+
attributed.update(fragment["components"]["schemas"])
|
|
284
|
+
envelope = build_envelope(spec, attributed)
|
|
285
|
+
index = build_index(spec, fragments, envelope)
|
|
286
|
+
rendered = {f"{name}.json": canonical_json(fragment) for name, fragment in fragments.items()}
|
|
287
|
+
rendered[ENVELOPE_NAME] = canonical_json(envelope)
|
|
288
|
+
rendered[INDEX_NAME] = canonical_json(index)
|
|
289
|
+
return rendered
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def write_fragments(directory: Path, rendered: Mapping[str, str]) -> List[str]:
|
|
293
|
+
"""Write every artefact; return the names whose bytes changed."""
|
|
294
|
+
directory.mkdir(parents=True, exist_ok=True)
|
|
295
|
+
changed: List[str] = []
|
|
296
|
+
for name in sorted(rendered):
|
|
297
|
+
target = directory / name
|
|
298
|
+
text = rendered[name]
|
|
299
|
+
if not target.exists() or target.read_text(encoding="utf-8") != text:
|
|
300
|
+
changed.append(name)
|
|
301
|
+
target.write_text(text, encoding="utf-8")
|
|
302
|
+
stale = sorted(
|
|
303
|
+
path.name
|
|
304
|
+
for path in directory.glob("*.json")
|
|
305
|
+
if path.name not in rendered
|
|
306
|
+
)
|
|
307
|
+
for name in stale:
|
|
308
|
+
(directory / name).unlink()
|
|
309
|
+
changed.append(name)
|
|
310
|
+
return changed
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def check_fragments(directory: Path, rendered: Mapping[str, str]) -> List[str]:
|
|
314
|
+
"""Names of artefacts that are missing, stale or unexpected on disk."""
|
|
315
|
+
problems: List[str] = []
|
|
316
|
+
for name in sorted(rendered):
|
|
317
|
+
target = directory / name
|
|
318
|
+
if not target.exists():
|
|
319
|
+
problems.append(f"missing: {name}")
|
|
320
|
+
elif target.read_text(encoding="utf-8") != rendered[name]:
|
|
321
|
+
problems.append(f"stale: {name}")
|
|
322
|
+
for path in sorted(directory.glob("*.json")):
|
|
323
|
+
if path.name not in rendered:
|
|
324
|
+
problems.append(f"unexpected: {path.name}")
|
|
325
|
+
return problems
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def main(argv: List[str] | None = None) -> int:
|
|
329
|
+
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
|
330
|
+
parser.add_argument("--spec", type=Path, default=SPEC_PATH, help="committed contract")
|
|
331
|
+
parser.add_argument("--mapping", type=Path, default=MAPPING_PATH, help="route → family map")
|
|
332
|
+
parser.add_argument("--out", type=Path, default=FRAGMENT_DIR, help="fragment directory")
|
|
333
|
+
parser.add_argument(
|
|
334
|
+
"--check",
|
|
335
|
+
action="store_true",
|
|
336
|
+
help="fail instead of writing when the committed fragments are stale",
|
|
337
|
+
)
|
|
338
|
+
args = parser.parse_args(argv)
|
|
339
|
+
|
|
340
|
+
spec = load_spec(args.spec)
|
|
341
|
+
mapping = load_mapping(args.mapping)
|
|
342
|
+
rendered = render_all(spec, mapping)
|
|
343
|
+
|
|
344
|
+
if args.check:
|
|
345
|
+
problems = check_fragments(args.out, rendered)
|
|
346
|
+
for problem in problems:
|
|
347
|
+
print(f" {problem}")
|
|
348
|
+
if problems:
|
|
349
|
+
print(f"{len(problems)} fragment(s) out of date — run {GENERATOR}")
|
|
350
|
+
return 1
|
|
351
|
+
print(f"fragments up to date ({len(rendered)} files in {args.out})")
|
|
352
|
+
return 0
|
|
353
|
+
|
|
354
|
+
changed = write_fragments(args.out, rendered)
|
|
355
|
+
total_ops = len(spec_operations(spec))
|
|
356
|
+
print(
|
|
357
|
+
f"wrote {len(rendered)} files to {args.out} "
|
|
358
|
+
f"({len(spec['paths'])} paths / {total_ops} operations); "
|
|
359
|
+
f"{len(changed)} changed"
|
|
360
|
+
)
|
|
361
|
+
return 0
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
if __name__ == "__main__":
|
|
365
|
+
sys.exit(main())
|