ltcai 11.2.0 → 11.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -53
- package/docs/CHANGELOG.md +61 -0
- package/docs/COMMUNITY_AND_PLUGINS.md +1 -1
- package/docs/DEVELOPMENT.md +1 -1
- package/docs/MULTI_AGENT_RUNTIME.md +1 -1
- package/docs/ONBOARDING.md +1 -1
- package/docs/OPERATIONS.md +6 -2
- package/docs/PERMISSION_MODE.md +1 -1
- package/docs/TRUST_MODEL.md +1 -1
- package/docs/WHY_LATTICE.md +1 -1
- package/docs/kg-schema.md +2 -2
- package/docs/v11.3.0_PLAN.md +202 -0
- package/docs/v11.4.0_RUST_FOUNDATION_PLAN.md +176 -0
- package/lattice_brain/__init__.py +1 -1
- package/lattice_brain/graph/_kg_common/__init__.py +287 -0
- package/lattice_brain/graph/_kg_common/extraction.py +516 -0
- package/lattice_brain/graph/_kg_common/relations.py +161 -0
- package/lattice_brain/graph/_kg_common/text.py +479 -0
- package/lattice_brain/graph/discovery_index/__init__.py +35 -0
- package/lattice_brain/graph/discovery_index/cleanup.py +182 -0
- package/lattice_brain/graph/discovery_index/extract.py +137 -0
- package/lattice_brain/graph/discovery_index/scan.py +411 -0
- package/lattice_brain/graph/discovery_index/upsert.py +495 -0
- package/lattice_brain/graph/projection/__init__.py +42 -0
- package/lattice_brain/graph/projection/curation.py +500 -0
- package/lattice_brain/graph/{projection.py → projection/v2_schema.py} +15 -477
- package/lattice_brain/graph/retrieval/__init__.py +54 -0
- package/lattice_brain/graph/retrieval/context.py +197 -0
- package/lattice_brain/graph/retrieval/graph_view.py +319 -0
- package/lattice_brain/graph/retrieval/hybrid.py +488 -0
- package/lattice_brain/graph/retrieval/maintenance.py +121 -0
- package/lattice_brain/graph/retrieval/signals.py +95 -0
- package/lattice_brain/graph/retrieval_vector/__init__.py +42 -0
- package/lattice_brain/graph/retrieval_vector/fingerprint.py +97 -0
- package/lattice_brain/graph/retrieval_vector/indexing.py +347 -0
- package/lattice_brain/graph/retrieval_vector/search.py +560 -0
- package/lattice_brain/graph/retrieval_vector/status.py +374 -0
- package/lattice_brain/ingestion/__init__.py +130 -0
- package/lattice_brain/ingestion/_contract.py +90 -0
- package/lattice_brain/ingestion/constants.py +127 -0
- package/lattice_brain/ingestion/folder_scan.py +57 -0
- package/lattice_brain/ingestion/folders.py +258 -0
- package/lattice_brain/ingestion/hashing.py +26 -0
- package/lattice_brain/ingestion/jobs_api.py +107 -0
- package/lattice_brain/ingestion/models.py +80 -0
- package/lattice_brain/ingestion/pipeline.py +486 -0
- package/lattice_brain/ingestion/quality.py +209 -0
- package/lattice_brain/ingestion/routing.py +295 -0
- package/lattice_brain/multimodal/__init__.py +164 -0
- package/lattice_brain/multimodal/audio.py +77 -0
- package/lattice_brain/multimodal/common.py +118 -0
- package/lattice_brain/multimodal/images.py +498 -0
- package/lattice_brain/multimodal/ports.py +169 -0
- package/lattice_brain/multimodal/video.py +410 -0
- package/lattice_brain/portability/__init__.py +90 -0
- package/lattice_brain/portability/_contract.py +42 -0
- package/lattice_brain/portability/backups.py +338 -0
- package/lattice_brain/portability/bundles.py +136 -0
- package/lattice_brain/portability/constants.py +93 -0
- package/lattice_brain/portability/fsops.py +138 -0
- package/lattice_brain/portability/service.py +41 -0
- package/lattice_brain/{portability.py → portability/sharing.py} +44 -677
- package/lattice_brain/runtime/__init__.py +1 -1
- package/lattice_brain/runtime/multi_agent.py +1 -1
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/chronicle.py +63 -0
- package/latticeai/core/agent/__init__.py +93 -0
- package/latticeai/core/agent/_contract.py +79 -0
- package/latticeai/core/agent/context.py +57 -0
- package/latticeai/core/agent/deps.py +125 -0
- package/latticeai/core/agent/execution.py +622 -0
- package/latticeai/core/agent/planning.py +145 -0
- package/latticeai/core/agent/recovery.py +157 -0
- package/latticeai/core/agent/runtime.py +210 -0
- package/latticeai/core/agent/verification.py +231 -0
- package/latticeai/core/embedding_providers/__init__.py +151 -0
- package/latticeai/core/embedding_providers/base.py +199 -0
- package/latticeai/core/embedding_providers/captions.py +162 -0
- package/latticeai/core/embedding_providers/profiles.py +126 -0
- package/latticeai/core/embedding_providers/text.py +350 -0
- package/latticeai/core/embedding_providers/vision.py +352 -0
- package/latticeai/core/file_generation/__init__.py +115 -0
- package/latticeai/core/file_generation/bundles.py +76 -0
- package/latticeai/core/file_generation/extraction.py +154 -0
- package/latticeai/core/file_generation/inference.py +235 -0
- package/latticeai/core/file_generation/orchestration.py +152 -0
- package/latticeai/core/file_generation/prompting.py +117 -0
- package/latticeai/core/file_generation/repair.py +114 -0
- package/latticeai/core/file_generation/sanitize.py +61 -0
- package/latticeai/core/file_generation/validation.py +201 -0
- package/latticeai/core/legacy_compatibility.py +1 -1
- package/latticeai/core/marketplace.py +1 -1
- package/latticeai/core/messages.py +9 -0
- package/latticeai/core/workspace_os_constants.py +1 -1
- package/latticeai/integrations/telegram_bot/__init__.py +123 -0
- package/latticeai/integrations/telegram_bot/__main__.py +17 -0
- package/latticeai/integrations/telegram_bot/config.py +86 -0
- package/latticeai/integrations/telegram_bot/dispatch.py +311 -0
- package/latticeai/integrations/telegram_bot/flows.py +478 -0
- package/latticeai/integrations/telegram_bot/helpers.py +322 -0
- package/latticeai/integrations/telegram_bot/screens.py +394 -0
- package/latticeai/models/router/__init__.py +88 -0
- package/latticeai/models/router/_contract.py +66 -0
- package/latticeai/models/router/branding.py +56 -0
- package/latticeai/models/router/catalog.py +69 -0
- package/latticeai/models/router/documents.py +199 -0
- package/latticeai/models/router/errors.py +37 -0
- package/latticeai/models/router/generation.py +258 -0
- package/latticeai/models/router/loading.py +291 -0
- package/latticeai/models/router/local_models.py +85 -0
- package/latticeai/models/router/registry.py +147 -0
- package/latticeai/runtime/build_phases/__init__.py +82 -0
- package/latticeai/runtime/build_phases/features.py +407 -0
- package/latticeai/runtime/build_phases/foundation.py +555 -0
- package/latticeai/runtime/build_phases/web.py +492 -0
- package/latticeai/runtime/runtime_context.py +1 -0
- package/latticeai/services/architecture_readiness.py +48 -19
- package/latticeai/services/brain_intelligence/__init__.py +58 -0
- package/latticeai/services/brain_intelligence/_contract.py +71 -0
- package/latticeai/services/brain_intelligence/consistency.py +193 -0
- package/latticeai/services/brain_intelligence/constants.py +47 -0
- package/latticeai/services/brain_intelligence/digest.py +258 -0
- package/latticeai/services/brain_intelligence/health.py +331 -0
- package/latticeai/services/brain_intelligence/proposals.py +264 -0
- package/latticeai/services/brain_intelligence/sampling.py +84 -0
- package/latticeai/services/brain_intelligence/service.py +48 -0
- package/latticeai/services/chronicle.py +557 -0
- package/latticeai/services/memory_service/__init__.py +52 -0
- package/latticeai/services/memory_service/_contract.py +100 -0
- package/latticeai/services/memory_service/brief.py +431 -0
- package/latticeai/services/memory_service/constants.py +57 -0
- package/latticeai/services/memory_service/maintenance.py +138 -0
- package/latticeai/services/memory_service/manager.py +186 -0
- package/latticeai/services/memory_service/proof.py +136 -0
- package/latticeai/services/memory_service/recall.py +225 -0
- package/latticeai/services/memory_service/service.py +48 -0
- package/latticeai/services/memory_service/stores.py +110 -0
- package/latticeai/services/model_runtime/__init__.py +322 -0
- package/latticeai/services/model_runtime/cloud.py +87 -0
- package/latticeai/services/model_runtime/download.py +282 -0
- package/latticeai/services/model_runtime/engines.py +341 -0
- package/latticeai/services/model_runtime/loading.py +178 -0
- package/latticeai/services/model_runtime/service.py +129 -0
- package/latticeai/services/model_runtime/state.py +131 -0
- package/latticeai/services/model_runtime/status.py +255 -0
- package/latticeai/services/product_readiness.py +15 -7
- package/latticeai/setup/wizard/__init__.py +126 -0
- package/latticeai/setup/wizard/catalog.py +172 -0
- package/latticeai/setup/wizard/detect.py +323 -0
- package/latticeai/setup/wizard/install.py +348 -0
- package/latticeai/setup/wizard/paths.py +168 -0
- package/latticeai/setup/wizard/plans.py +74 -0
- package/latticeai/setup/wizard/recommend.py +320 -0
- package/package.json +6 -2
- package/scripts/bump_version.py +14 -0
- package/scripts/capture_release_evidence.mjs +33 -21
- package/scripts/check_current_release_docs.mjs +1 -1
- package/scripts/check_i18n_namespace_coverage.mjs +41 -4
- package/scripts/check_max_file_lines.mjs +102 -0
- package/scripts/check_release_evidence_bound.mjs +30 -15
- package/scripts/check_screenshot_pixel_delta.py +34 -4
- package/scripts/check_server_i18n.mjs +1 -0
- package/scripts/generate_rust_parity_fixtures.py +562 -0
- package/scripts/lib/mock_server_fingerprint.mjs +94 -0
- package/scripts/release_screen_claims.json +31 -2
- package/src-tauri/Cargo.lock +361 -3
- package/src-tauri/Cargo.toml +6 -1
- package/src-tauri/src/backend.rs +349 -0
- package/src-tauri/src/folder.rs +33 -0
- package/src-tauri/src/main.rs +97 -399
- package/src-tauri/tauri.conf.json +1 -1
- package/static/app/asset-manifest.json +41 -37
- package/static/app/assets/Act-yYpYnn0v.js +1 -0
- package/static/app/assets/AdminConsole-DL3Cr5pL.js +1 -0
- package/static/app/assets/{Brain-tuhI4sOC.js → Brain-C1HBN0Wf.js} +2 -2
- package/static/app/assets/BrainHome-DoXRhUUC.js +2 -0
- package/static/app/assets/BrainSignals-6yR6ir5t.js +1 -0
- package/static/app/assets/Capture-CFIRsFNE.js +1 -0
- package/static/app/assets/Chronicle-BZbEgiwN.js +1 -0
- package/static/app/assets/CommandPalette-D2pMxC2I.js +1 -0
- package/static/app/assets/Library-DwO3yZST.js +1 -0
- package/static/app/assets/{LivingBrain-DBwhto14.js → LivingBrain-Jn1GK0-S.js} +1 -1
- package/static/app/assets/ProductFlow-B-w1R4Oo.js +1 -0
- package/static/app/assets/ReviewCard-6B27X8Vg.js +3 -0
- package/static/app/assets/System-DW8F-2xL.js +1 -0
- package/static/app/assets/arrow-left-DXvKg9U6.js +1 -0
- package/static/app/assets/{bot-Cia42c2h.js → bot-IM_E_Y12.js} +1 -1
- package/static/app/assets/brain-Ci1CkWjM.js +1 -0
- package/static/app/assets/{button-2j2Ijzgq.js → button-COwyqfHM.js} +1 -1
- package/static/app/assets/circle-check-DfInj-qD.js +1 -0
- package/static/app/assets/{circle-pause-BEFeWpVW.js → circle-pause-DEM4A1Y5.js} +1 -1
- package/static/app/assets/{circle-play-ujXMcHxl.js → circle-play-C9djDuLd.js} +1 -1
- package/static/app/assets/{cpu-k4awryFq.js → cpu-DFdo1gw-.js} +1 -1
- package/static/app/assets/{download-DFbLJ_ig.js → download-SnJL6oqk.js} +1 -1
- package/static/app/assets/{folder-open-7y_b6xkM.js → folder-open-CqZeDkjE.js} +1 -1
- package/static/app/assets/{hard-drive-Bidh02Kr.js → hard-drive-j1jJXYYf.js} +1 -1
- package/static/app/assets/{index-DwDl9-8Y.css → index-BLPb5lmE.css} +1 -1
- package/static/app/assets/index-_u5iUHDr.js +10 -0
- package/static/app/assets/input-B0lPdRQZ.js +1 -0
- package/static/app/assets/link-2-CoFbooHS.js +1 -0
- package/static/app/assets/{permissionCopy-Bpb83Hx9.js → permissionCopy-BsyLxtao.js} +1 -1
- package/static/app/assets/primitives-DEbN-d6p.js +1 -0
- package/static/app/assets/search-BybIWPNd.js +1 -0
- package/static/app/assets/{share-2-BH1M-WNi.js → share-2-CVtZ_ewX.js} +1 -1
- package/static/app/assets/{shield-alert-BlKdBXcG.js → shield-alert-CBi2GNWM.js} +1 -1
- package/static/app/assets/{textarea-CCWbUfFB.js → textarea-DNMpB5ih.js} +1 -1
- package/static/app/assets/{useFocusTrap-YdHQ7pJ1.js → useFocusTrap-C83t3GXF.js} +1 -1
- package/static/app/assets/useMutation-DtbJDoyz.js +1 -0
- package/static/app/assets/{useQuery-CXQiwbVT.js → useQuery-Dcp1OChy.js} +1 -1
- package/static/app/assets/utils-BlZr7Pd4.js +4 -0
- package/static/app/assets/workspace-jJY4RuAV.js +1 -0
- package/static/app/index.html +4 -4
- package/static/sw.js +1 -1
- package/lattice_brain/graph/_kg_common.py +0 -1331
- package/lattice_brain/graph/discovery_index.py +0 -1141
- package/lattice_brain/graph/retrieval.py +0 -1120
- package/lattice_brain/graph/retrieval_vector.py +0 -1293
- package/lattice_brain/ingestion.py +0 -1525
- package/lattice_brain/multimodal.py +0 -1258
- package/latticeai/core/agent.py +0 -1465
- package/latticeai/core/embedding_providers.py +0 -1196
- package/latticeai/core/file_generation.py +0 -1047
- package/latticeai/integrations/telegram_bot.py +0 -1390
- package/latticeai/models/router.py +0 -1007
- package/latticeai/runtime/build_phases.py +0 -1450
- package/latticeai/services/brain_intelligence.py +0 -1083
- package/latticeai/services/memory_service.py +0 -1177
- package/latticeai/services/model_runtime.py +0 -1281
- package/latticeai/setup/wizard.py +0 -1310
- package/static/app/assets/Act-AWf0SAKp.js +0 -1
- package/static/app/assets/AdminConsole-D0u8Tiyj.js +0 -1
- package/static/app/assets/BrainHome-Ts7G_Ila.js +0 -2
- package/static/app/assets/BrainSignals-jMYgQ2Ar.js +0 -1
- package/static/app/assets/Capture-CqOSzyPr.js +0 -1
- package/static/app/assets/CommandPalette-DC0Bzh-I.js +0 -1
- package/static/app/assets/Library-CX-bbhmK.js +0 -1
- package/static/app/assets/ProductFlow-BHA2cfKI.js +0 -1
- package/static/app/assets/ReviewCard-BUhCKRNM.js +0 -3
- package/static/app/assets/System-Bu2t5hn1.js +0 -1
- package/static/app/assets/arrow-left-Dzwa5zRb.js +0 -1
- package/static/app/assets/brain-DJMoqrwx.js +0 -1
- package/static/app/assets/index-BpYkzcVm.js +0 -10
- package/static/app/assets/input-DSlJJxRs.js +0 -1
- package/static/app/assets/primitives-BCx6TvfG.js +0 -1
- package/static/app/assets/search-Cgy8cCFJ.js +0 -1
- package/static/app/assets/utils-zqPZJxdx.js +0 -4
- package/static/app/assets/workspace-DXTihhfU.js +0 -1
|
@@ -1,39 +1,32 @@
|
|
|
1
|
+
"""The legacy → v2 projection: schema, backfill, per-row writes, mirrors.
|
|
2
|
+
|
|
3
|
+
The v2 tables and the ``kgv2_*`` reconstruction views are *derived* — the legacy
|
|
4
|
+
``nodes``/``edges`` tables stay authoritative — which is what makes the whole
|
|
5
|
+
DROP → CREATE → VIEWS → BACKFILL → stamp migration safe to run in one
|
|
6
|
+
transaction and simply retry on the next startup when it fails. The trigram FTS
|
|
7
|
+
index lives here too: it is the other index projected off the same rows.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
# ruff: noqa: F403,F405,S608
|
|
1
11
|
from __future__ import annotations
|
|
2
12
|
|
|
3
13
|
from typing import TYPE_CHECKING
|
|
4
14
|
|
|
5
|
-
from
|
|
6
|
-
|
|
7
|
-
# ruff: noqa: F403,F405
|
|
8
|
-
from ._kg_common import * # noqa: F403,F401
|
|
15
|
+
from ...quiet import quiet
|
|
16
|
+
from .._kg_common import * # noqa: F401
|
|
9
17
|
|
|
10
18
|
# The cross-mixin surface (`_connect`, `_upsert_node`, …) is declared in
|
|
11
19
|
# `_kg_contract.KnowledgeGraphCore`. It is a typing-only base: at runtime this
|
|
12
20
|
# is `object`, so the MRO of `KnowledgeGraphStore` is unchanged.
|
|
13
21
|
if TYPE_CHECKING:
|
|
14
|
-
from
|
|
22
|
+
from .._kg_contract import KnowledgeGraphCore as _Core
|
|
15
23
|
else:
|
|
16
24
|
_Core = object
|
|
17
25
|
|
|
18
26
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
# human decision instead of writing them immediately. Explicit review_mode=
|
|
22
|
-
# argument wins; otherwise this env opt-in decides; default stays auto-promote.
|
|
23
|
-
_PROMOTION_REVIEW_ENV = "LATTICEAI_GRAPH_PROMOTION_REVIEW"
|
|
24
|
-
_PENDING_PROMOTIONS_KEY = "pending_promotions"
|
|
25
|
-
_PENDING_PROMOTIONS_CAP = 100
|
|
26
|
-
|
|
27
|
-
# graph_meta stamp written by an applied (dry_run=False) noise-curate run; the
|
|
28
|
-
# Command Center hygiene advisory reads it to pace its suggestion (Wave 2.5).
|
|
29
|
-
_LAST_NOISE_CURATE_KEY = "last_noise_curate_at"
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
def _promotion_review_default() -> bool:
|
|
33
|
-
return os.getenv(_PROMOTION_REVIEW_ENV, "").strip().lower() in ("1", "true", "yes")
|
|
34
|
-
|
|
27
|
+
class KnowledgeGraphV2SchemaMixin(_Core):
|
|
28
|
+
"""Projection + FTS. Mixed into ``KnowledgeGraphProjectionMixin``."""
|
|
35
29
|
|
|
36
|
-
class KnowledgeGraphProjectionMixin(_Core):
|
|
37
30
|
_FTS_SQL = """
|
|
38
31
|
CREATE VIRTUAL TABLE IF NOT EXISTS node_fts USING fts5(
|
|
39
32
|
node_id UNINDEXED, title, summary, metadata, tokenize='trigram'
|
|
@@ -501,461 +494,6 @@ class KnowledgeGraphProjectionMixin(_Core):
|
|
|
501
494
|
ex,
|
|
502
495
|
)
|
|
503
496
|
|
|
504
|
-
def curate(
|
|
505
|
-
self,
|
|
506
|
-
*,
|
|
507
|
-
max_documents: int = 200,
|
|
508
|
-
max_new_nodes: int = 8,
|
|
509
|
-
review_mode: Optional[bool] = None,
|
|
510
|
-
) -> Dict[str, Any]:
|
|
511
|
-
"""On-demand graph curation (T4.4 — graph_curator goes live).
|
|
512
|
-
|
|
513
|
-
Runs the curator's gated topic-promotion pipeline over recent content
|
|
514
|
-
nodes: candidates are clustered, secret-bearing labels are refused,
|
|
515
|
-
and only multi-source topics above the importance threshold become
|
|
516
|
-
Topic nodes (with MENTIONS edges back to their sources and a real
|
|
517
|
-
importance_score in nodes_v2). Explicit and observable — the result
|
|
518
|
-
reports everything promoted AND everything skipped, with reasons.
|
|
519
|
-
|
|
520
|
-
``review_mode`` (review 2026-07-25 Wave 4): when True, nothing is
|
|
521
|
-
written — the would-be promotions are parked in ``graph_meta`` as
|
|
522
|
-
``pending_promotions`` for a human decision via
|
|
523
|
-
:meth:`apply_pending_promotions` / :meth:`reject_pending_promotions`.
|
|
524
|
-
Explicit argument wins; ``None`` falls back to the
|
|
525
|
-
``LATTICEAI_GRAPH_PROMOTION_REVIEW`` env opt-in; default stays the
|
|
526
|
-
historical auto-promote behavior.
|
|
527
|
-
"""
|
|
528
|
-
from .curator import auto_build_graph_overlay
|
|
529
|
-
|
|
530
|
-
content_types = (
|
|
531
|
-
"Document",
|
|
532
|
-
"File",
|
|
533
|
-
"CodeFile",
|
|
534
|
-
"Message",
|
|
535
|
-
"AIResponse",
|
|
536
|
-
"Chat",
|
|
537
|
-
"Page",
|
|
538
|
-
"Slide",
|
|
539
|
-
"Spreadsheet",
|
|
540
|
-
)
|
|
541
|
-
nt, _ = self._read_tables()
|
|
542
|
-
with self._connect() as conn:
|
|
543
|
-
placeholders = ",".join("?" for _ in content_types)
|
|
544
|
-
rows = conn.execute(
|
|
545
|
-
f"""
|
|
546
|
-
SELECT id, type, title, summary FROM {nt}
|
|
547
|
-
WHERE type IN ({placeholders})
|
|
548
|
-
ORDER BY updated_at DESC, id ASC LIMIT ?
|
|
549
|
-
""",
|
|
550
|
-
(*content_types, max(1, min(int(max_documents), 2000))),
|
|
551
|
-
).fetchall()
|
|
552
|
-
existing_labels = {
|
|
553
|
-
str(row["title"] or "").strip().lower()
|
|
554
|
-
for row in conn.execute(
|
|
555
|
-
f"SELECT title FROM {nt} WHERE type IN ('Topic', 'Concept')"
|
|
556
|
-
).fetchall()
|
|
557
|
-
}
|
|
558
|
-
documents = [
|
|
559
|
-
{
|
|
560
|
-
"id": row["id"],
|
|
561
|
-
"text": f"{row['title']} {row['summary'] or ''}",
|
|
562
|
-
"kind": "file"
|
|
563
|
-
if row["type"] in {"Document", "File", "CodeFile", "Spreadsheet"}
|
|
564
|
-
else "chat",
|
|
565
|
-
}
|
|
566
|
-
for row in rows
|
|
567
|
-
]
|
|
568
|
-
overlay = auto_build_graph_overlay(
|
|
569
|
-
documents,
|
|
570
|
-
existing_node_labels=existing_labels,
|
|
571
|
-
max_new_nodes=max(1, min(int(max_new_nodes), 50)),
|
|
572
|
-
)
|
|
573
|
-
valid_ids = {row["id"] for row in rows}
|
|
574
|
-
review = review_mode if review_mode is not None else _promotion_review_default()
|
|
575
|
-
if review:
|
|
576
|
-
proposed_at = _now()
|
|
577
|
-
proposed = [
|
|
578
|
-
{
|
|
579
|
-
"id": f"topic:{_slug(promo['label'])}",
|
|
580
|
-
"label": promo["label"],
|
|
581
|
-
"importance": promo["importance"],
|
|
582
|
-
"aliases": promo["aliases"],
|
|
583
|
-
"sources": [s for s in promo["sources"][:10] if s in valid_ids],
|
|
584
|
-
"proposed_at": proposed_at,
|
|
585
|
-
}
|
|
586
|
-
for promo in overlay["promotions"]
|
|
587
|
-
]
|
|
588
|
-
with self._connect() as conn:
|
|
589
|
-
merged = self._merge_pending_promotions(conn, proposed)
|
|
590
|
-
return {
|
|
591
|
-
"status": "pending_review",
|
|
592
|
-
"documents_scanned": len(documents),
|
|
593
|
-
"candidates_total": overlay["candidates_total"],
|
|
594
|
-
"pending": proposed,
|
|
595
|
-
"pending_total": len(merged),
|
|
596
|
-
"skipped": overlay["skipped"][:50],
|
|
597
|
-
"skipped_total": len(overlay["skipped"]),
|
|
598
|
-
}
|
|
599
|
-
promoted: List[Dict[str, Any]] = []
|
|
600
|
-
with self._connect() as conn:
|
|
601
|
-
for promo in overlay["promotions"]:
|
|
602
|
-
promoted.append(
|
|
603
|
-
self._write_promotion(conn, promo, valid_source_ids=valid_ids)
|
|
604
|
-
)
|
|
605
|
-
return {
|
|
606
|
-
"status": "ok",
|
|
607
|
-
"documents_scanned": len(documents),
|
|
608
|
-
"candidates_total": overlay["candidates_total"],
|
|
609
|
-
"promoted": promoted,
|
|
610
|
-
"skipped": overlay["skipped"][:50],
|
|
611
|
-
"skipped_total": len(overlay["skipped"]),
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
def _write_promotion(
|
|
615
|
-
self,
|
|
616
|
-
conn: sqlite3.Connection,
|
|
617
|
-
promo: Dict[str, Any],
|
|
618
|
-
*,
|
|
619
|
-
valid_source_ids: Optional[set] = None,
|
|
620
|
-
) -> Dict[str, Any]:
|
|
621
|
-
"""Write one curator promotion: Topic node + importance + MENTIONS edges.
|
|
622
|
-
|
|
623
|
-
Single write path shared by direct ``curate()`` and
|
|
624
|
-
:meth:`apply_pending_promotions`, so a human-approved promotion lands
|
|
625
|
-
exactly like an auto-promoted one. ``valid_source_ids`` restricts the
|
|
626
|
-
linkable sources to this curate run's scanned rows; when ``None``
|
|
627
|
-
(apply-after-review), each stored source is checked for existence so a
|
|
628
|
-
node deleted between propose and apply is skipped, not an error.
|
|
629
|
-
"""
|
|
630
|
-
topic_id = str(promo.get("id") or f"topic:{_slug(str(promo['label']))}")
|
|
631
|
-
self._upsert_node(
|
|
632
|
-
conn,
|
|
633
|
-
topic_id,
|
|
634
|
-
"Topic",
|
|
635
|
-
str(promo["label"]),
|
|
636
|
-
metadata={
|
|
637
|
-
"curated": True,
|
|
638
|
-
"importance": promo["importance"],
|
|
639
|
-
"aliases": list(promo.get("aliases") or []),
|
|
640
|
-
"source": "graph_curator",
|
|
641
|
-
},
|
|
642
|
-
)
|
|
643
|
-
conn.execute(
|
|
644
|
-
"UPDATE nodes_v2 SET importance_score=? WHERE id=?",
|
|
645
|
-
(float(promo["importance"]), topic_id),
|
|
646
|
-
)
|
|
647
|
-
linked = 0
|
|
648
|
-
for source_id in list(promo.get("sources") or [])[:10]:
|
|
649
|
-
if valid_source_ids is not None:
|
|
650
|
-
if source_id not in valid_source_ids:
|
|
651
|
-
continue
|
|
652
|
-
elif not conn.execute(
|
|
653
|
-
"SELECT 1 FROM nodes WHERE id=?", (source_id,)
|
|
654
|
-
).fetchone():
|
|
655
|
-
continue
|
|
656
|
-
self._upsert_edge(
|
|
657
|
-
conn,
|
|
658
|
-
source_id,
|
|
659
|
-
topic_id,
|
|
660
|
-
"MENTIONS",
|
|
661
|
-
weight=0.6,
|
|
662
|
-
metadata={"source": "graph_curator"},
|
|
663
|
-
)
|
|
664
|
-
linked += 1
|
|
665
|
-
return {
|
|
666
|
-
"node_id": topic_id,
|
|
667
|
-
"label": promo["label"],
|
|
668
|
-
"importance": promo["importance"],
|
|
669
|
-
"linked_sources": linked,
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
# ── pending promotion queue (review 2026-07-25 Wave 4) ───────────────────
|
|
673
|
-
|
|
674
|
-
def _read_pending_promotions(
|
|
675
|
-
self, conn: sqlite3.Connection
|
|
676
|
-
) -> List[Dict[str, Any]]:
|
|
677
|
-
try:
|
|
678
|
-
row = conn.execute(
|
|
679
|
-
"SELECT value FROM graph_meta WHERE key=?",
|
|
680
|
-
(_PENDING_PROMOTIONS_KEY,),
|
|
681
|
-
).fetchone()
|
|
682
|
-
except sqlite3.Error:
|
|
683
|
-
return []
|
|
684
|
-
if not row or not row["value"]:
|
|
685
|
-
return []
|
|
686
|
-
try:
|
|
687
|
-
parsed = json.loads(row["value"])
|
|
688
|
-
except (TypeError, ValueError):
|
|
689
|
-
return []
|
|
690
|
-
if not isinstance(parsed, list):
|
|
691
|
-
return []
|
|
692
|
-
return [
|
|
693
|
-
item for item in parsed if isinstance(item, dict) and item.get("id")
|
|
694
|
-
]
|
|
695
|
-
|
|
696
|
-
def _store_pending_promotions(
|
|
697
|
-
self, conn: sqlite3.Connection, entries: List[Dict[str, Any]]
|
|
698
|
-
) -> None:
|
|
699
|
-
conn.execute(
|
|
700
|
-
"INSERT OR REPLACE INTO graph_meta(key, value) VALUES (?, ?)",
|
|
701
|
-
(_PENDING_PROMOTIONS_KEY, json.dumps(entries, ensure_ascii=False)),
|
|
702
|
-
)
|
|
703
|
-
|
|
704
|
-
def _merge_pending_promotions(
|
|
705
|
-
self, conn: sqlite3.Connection, proposed: List[Dict[str, Any]]
|
|
706
|
-
) -> List[Dict[str, Any]]:
|
|
707
|
-
"""Merge new proposals into the stored queue (dedupe by id, cap 100)."""
|
|
708
|
-
merged: Dict[str, Dict[str, Any]] = {}
|
|
709
|
-
for item in self._read_pending_promotions(conn) + list(proposed):
|
|
710
|
-
merged[str(item["id"])] = item # newest proposal wins per id
|
|
711
|
-
entries = list(merged.values())[-_PENDING_PROMOTIONS_CAP:]
|
|
712
|
-
self._store_pending_promotions(conn, entries)
|
|
713
|
-
return entries
|
|
714
|
-
|
|
715
|
-
def pending_promotions(self) -> List[Dict[str, Any]]:
|
|
716
|
-
"""List promotions waiting for a human decision (review mode)."""
|
|
717
|
-
with self._connect() as conn:
|
|
718
|
-
return self._read_pending_promotions(conn)
|
|
719
|
-
|
|
720
|
-
def apply_pending_promotions(
|
|
721
|
-
self, ids: Optional[List[str]] = None
|
|
722
|
-
) -> Dict[str, Any]:
|
|
723
|
-
"""Apply stored pending promotions (all of them when ``ids`` is None).
|
|
724
|
-
|
|
725
|
-
Uses the exact node-writing path as direct ``curate()`` via
|
|
726
|
-
:meth:`_write_promotion`; applied entries leave the queue.
|
|
727
|
-
"""
|
|
728
|
-
wanted = None if ids is None else {str(item) for item in ids}
|
|
729
|
-
applied: List[Dict[str, Any]] = []
|
|
730
|
-
remaining: List[Dict[str, Any]] = []
|
|
731
|
-
with self._connect() as conn:
|
|
732
|
-
for promo in self._read_pending_promotions(conn):
|
|
733
|
-
if wanted is not None and str(promo.get("id")) not in wanted:
|
|
734
|
-
remaining.append(promo)
|
|
735
|
-
continue
|
|
736
|
-
applied.append(self._write_promotion(conn, promo))
|
|
737
|
-
self._store_pending_promotions(conn, remaining)
|
|
738
|
-
return {"status": "ok", "applied": applied, "remaining": len(remaining)}
|
|
739
|
-
|
|
740
|
-
def reject_pending_promotions(
|
|
741
|
-
self, ids: Optional[List[str]] = None
|
|
742
|
-
) -> Dict[str, Any]:
|
|
743
|
-
"""Drop pending promotions without writing (all when ``ids`` is None)."""
|
|
744
|
-
wanted = None if ids is None else {str(item) for item in ids}
|
|
745
|
-
rejected: List[str] = []
|
|
746
|
-
remaining: List[Dict[str, Any]] = []
|
|
747
|
-
with self._connect() as conn:
|
|
748
|
-
for promo in self._read_pending_promotions(conn):
|
|
749
|
-
if wanted is not None and str(promo.get("id")) not in wanted:
|
|
750
|
-
remaining.append(promo)
|
|
751
|
-
continue
|
|
752
|
-
rejected.append(str(promo.get("id")))
|
|
753
|
-
self._store_pending_promotions(conn, remaining)
|
|
754
|
-
return {"status": "ok", "rejected": rejected, "remaining": len(remaining)}
|
|
755
|
-
|
|
756
|
-
_NOISE_CONTENT_TYPES = (
|
|
757
|
-
"Document",
|
|
758
|
-
"File",
|
|
759
|
-
"CodeFile",
|
|
760
|
-
"Message",
|
|
761
|
-
"AIResponse",
|
|
762
|
-
"Chat",
|
|
763
|
-
"Page",
|
|
764
|
-
"Slide",
|
|
765
|
-
"Spreadsheet",
|
|
766
|
-
)
|
|
767
|
-
_NOISE_CONCEPT_TYPES = ("Concept", "Feature", "Topic", "Code", "Error")
|
|
768
|
-
|
|
769
|
-
def curate_noise(
|
|
770
|
-
self,
|
|
771
|
-
*,
|
|
772
|
-
dry_run: bool = True,
|
|
773
|
-
max_df_ratio: float = 0.8,
|
|
774
|
-
min_doc_frequency: int = 1,
|
|
775
|
-
min_corpus_docs: int = 5,
|
|
776
|
-
normalize_verbs: bool = True,
|
|
777
|
-
max_removals: int = 200,
|
|
778
|
-
) -> Dict[str, Any]:
|
|
779
|
-
"""Noise-reduction curation job (backlog #10, review §7.2 D).
|
|
780
|
-
|
|
781
|
-
(a) Removes heuristic concept nodes (``auto_extracted`` /
|
|
782
|
-
``graph_curator``-promoted) whose document frequency marks them as
|
|
783
|
-
noise: ubiquitous (low IDF — linked from more than ``max_df_ratio`` of
|
|
784
|
-
content docs once the corpus has ``min_corpus_docs``) or below the
|
|
785
|
-
``min_doc_frequency`` floor. Explicitly user-created nodes are never
|
|
786
|
-
touched, whatever their stats.
|
|
787
|
-
|
|
788
|
-
(b) Normalizes free-string relation verbs on the legacy edge table via
|
|
789
|
-
the ko/en dictionary in :mod:`lattice_brain.graph.curator`
|
|
790
|
-
('만들다/만든/creates' → 'created', …), merging rows that collide
|
|
791
|
-
after the rename.
|
|
792
|
-
|
|
793
|
-
``dry_run=True`` (the default) only *reports* what would change.
|
|
794
|
-
"""
|
|
795
|
-
from .curator import (
|
|
796
|
-
build_relation_verb_index,
|
|
797
|
-
plan_concept_noise_reduction,
|
|
798
|
-
plan_relation_normalization,
|
|
799
|
-
)
|
|
800
|
-
|
|
801
|
-
max_removals = max(0, int(max_removals))
|
|
802
|
-
# Operate on the legacy write tables directly: they are the mutation
|
|
803
|
-
# target, and raw free-string verbs only exist there (the v4 write
|
|
804
|
-
# door normalizes new edges; the kgv2_* read views collapse
|
|
805
|
-
# legacy_type and would hide exactly the rows this job cleans up).
|
|
806
|
-
nt, et = "nodes", "edges"
|
|
807
|
-
with self._connect() as conn:
|
|
808
|
-
content_ph = ",".join("?" for _ in self._NOISE_CONTENT_TYPES)
|
|
809
|
-
total_docs = conn.execute(
|
|
810
|
-
f"SELECT COUNT(*) AS c FROM {nt} WHERE type IN ({content_ph})",
|
|
811
|
-
self._NOISE_CONTENT_TYPES,
|
|
812
|
-
).fetchone()["c"]
|
|
813
|
-
|
|
814
|
-
concept_ph = ",".join("?" for _ in self._NOISE_CONCEPT_TYPES)
|
|
815
|
-
concept_rows = conn.execute(
|
|
816
|
-
f"SELECT id, type, title, metadata_json FROM {nt} WHERE type IN ({concept_ph})",
|
|
817
|
-
self._NOISE_CONCEPT_TYPES,
|
|
818
|
-
).fetchall()
|
|
819
|
-
concepts = []
|
|
820
|
-
for row in concept_rows:
|
|
821
|
-
meta = _safe_loads(row["metadata_json"]) or {}
|
|
822
|
-
heuristic = bool(meta.get("auto_extracted")) or (
|
|
823
|
-
meta.get("source") == "graph_curator" or meta.get("curated") is True
|
|
824
|
-
)
|
|
825
|
-
# Document frequency: distinct *content* nodes linked to this
|
|
826
|
-
# concept in either direction.
|
|
827
|
-
df = conn.execute(
|
|
828
|
-
f"""
|
|
829
|
-
SELECT COUNT(DISTINCT n.id) AS c
|
|
830
|
-
FROM {et} e
|
|
831
|
-
JOIN {nt} n
|
|
832
|
-
ON n.id = CASE WHEN e.to_node = ? THEN e.from_node ELSE e.to_node END
|
|
833
|
-
WHERE (e.to_node = ? OR e.from_node = ?)
|
|
834
|
-
AND n.type IN ({content_ph})
|
|
835
|
-
""",
|
|
836
|
-
(row["id"], row["id"], row["id"], *self._NOISE_CONTENT_TYPES),
|
|
837
|
-
).fetchone()["c"]
|
|
838
|
-
concepts.append({
|
|
839
|
-
"id": row["id"],
|
|
840
|
-
"label": row["title"],
|
|
841
|
-
"type": row["type"],
|
|
842
|
-
"df": int(df or 0),
|
|
843
|
-
"heuristic": heuristic,
|
|
844
|
-
})
|
|
845
|
-
|
|
846
|
-
plan = plan_concept_noise_reduction(
|
|
847
|
-
concepts,
|
|
848
|
-
total_docs,
|
|
849
|
-
max_df_ratio=max_df_ratio,
|
|
850
|
-
min_doc_frequency=min_doc_frequency,
|
|
851
|
-
min_corpus_docs=min_corpus_docs,
|
|
852
|
-
)
|
|
853
|
-
removals = plan["remove"][:max_removals]
|
|
854
|
-
|
|
855
|
-
verb_index = build_relation_verb_index()
|
|
856
|
-
edge_type_rows = conn.execute(
|
|
857
|
-
f"SELECT DISTINCT type FROM {et}"
|
|
858
|
-
).fetchall()
|
|
859
|
-
verb_plan = (
|
|
860
|
-
plan_relation_normalization(
|
|
861
|
-
(row["type"] for row in edge_type_rows), index=verb_index,
|
|
862
|
-
)
|
|
863
|
-
if normalize_verbs
|
|
864
|
-
else {}
|
|
865
|
-
)
|
|
866
|
-
|
|
867
|
-
removed_count = 0
|
|
868
|
-
renamed_edges = 0
|
|
869
|
-
if not dry_run:
|
|
870
|
-
for decision in removals:
|
|
871
|
-
node_id = decision["id"]
|
|
872
|
-
conn.execute(
|
|
873
|
-
"DELETE FROM edges WHERE from_node=? OR to_node=?",
|
|
874
|
-
(node_id, node_id),
|
|
875
|
-
)
|
|
876
|
-
conn.execute(
|
|
877
|
-
"DELETE FROM vector_embeddings WHERE item_id=?", (node_id,)
|
|
878
|
-
)
|
|
879
|
-
conn.execute("DELETE FROM nodes WHERE id=?", (node_id,))
|
|
880
|
-
self._v2_delete_nodes(conn, [node_id])
|
|
881
|
-
removed_count += 1
|
|
882
|
-
for original, canonical in verb_plan.items():
|
|
883
|
-
renamed_edges += conn.execute(
|
|
884
|
-
"SELECT COUNT(*) AS c FROM edges WHERE type=?", (original,)
|
|
885
|
-
).fetchone()["c"]
|
|
886
|
-
# UNIQUE(from_node, to_node, type): merge rows that collide
|
|
887
|
-
# after the rename instead of failing the UPDATE.
|
|
888
|
-
conn.execute(
|
|
889
|
-
"UPDATE OR IGNORE edges SET type=? WHERE type=?",
|
|
890
|
-
(canonical, original),
|
|
891
|
-
)
|
|
892
|
-
conn.execute("DELETE FROM edges WHERE type=?", (original,))
|
|
893
|
-
# Stamp every applied run — even a no-op one means the graph
|
|
894
|
-
# was inspected, so the Command Center hygiene advisory
|
|
895
|
-
# (review 2026-07-25 Wave 2.5) stops re-suggesting for a while.
|
|
896
|
-
conn.execute(
|
|
897
|
-
"INSERT OR REPLACE INTO graph_meta(key, value) VALUES (?, ?)",
|
|
898
|
-
(_LAST_NOISE_CURATE_KEY, _now()),
|
|
899
|
-
)
|
|
900
|
-
|
|
901
|
-
return {
|
|
902
|
-
"status": "ok",
|
|
903
|
-
"dry_run": bool(dry_run),
|
|
904
|
-
"total_content_docs": int(total_docs or 0),
|
|
905
|
-
"concepts_examined": len(concepts),
|
|
906
|
-
"remove": removals,
|
|
907
|
-
"remove_total": len(plan["remove"]),
|
|
908
|
-
"kept": len(plan["keep"]),
|
|
909
|
-
"protected_user_nodes": sum(
|
|
910
|
-
1 for item in plan["keep"] if item.get("reason") == "user_created_protected"
|
|
911
|
-
),
|
|
912
|
-
"verb_normalizations": verb_plan,
|
|
913
|
-
"applied": {
|
|
914
|
-
"removed_nodes": removed_count,
|
|
915
|
-
"renamed_edges": renamed_edges,
|
|
916
|
-
},
|
|
917
|
-
"thresholds": {
|
|
918
|
-
"max_df_ratio": float(max_df_ratio),
|
|
919
|
-
"min_doc_frequency": int(min_doc_frequency),
|
|
920
|
-
"min_corpus_docs": int(min_corpus_docs),
|
|
921
|
-
},
|
|
922
|
-
}
|
|
923
|
-
|
|
924
|
-
def last_noise_curate_at(self) -> Optional[str]:
|
|
925
|
-
"""Timestamp of the last applied (dry_run=False) noise-curate run.
|
|
926
|
-
|
|
927
|
-
``None`` when the job never ran or the meta table is unreadable —
|
|
928
|
-
advisory readers treat both as "curation is due" (fail-open).
|
|
929
|
-
"""
|
|
930
|
-
try:
|
|
931
|
-
with self._connect() as conn:
|
|
932
|
-
row = conn.execute(
|
|
933
|
-
"SELECT value FROM graph_meta WHERE key=?",
|
|
934
|
-
(_LAST_NOISE_CURATE_KEY,),
|
|
935
|
-
).fetchone()
|
|
936
|
-
except sqlite3.Error:
|
|
937
|
-
return None
|
|
938
|
-
return str(row["value"]) if row and row["value"] else None
|
|
939
|
-
|
|
940
|
-
def mark_superseded(self, old_node_id: str, new_node_id: str) -> Dict[str, Any]:
|
|
941
|
-
"""Record that ``old_node_id`` was replaced by ``new_node_id``.
|
|
942
|
-
|
|
943
|
-
The old node stays queryable (knowledge is durable); readers can follow
|
|
944
|
-
the revision chain via ``nodes_v2.superseded_by``.
|
|
945
|
-
"""
|
|
946
|
-
with self._connect() as conn:
|
|
947
|
-
for node_id in (old_node_id, new_node_id):
|
|
948
|
-
exists = conn.execute(
|
|
949
|
-
"SELECT 1 FROM nodes_v2 WHERE id=?", (node_id,)
|
|
950
|
-
).fetchone()
|
|
951
|
-
if not exists:
|
|
952
|
-
raise FileNotFoundError(node_id)
|
|
953
|
-
conn.execute(
|
|
954
|
-
"UPDATE nodes_v2 SET superseded_by=?, updated_at=? WHERE id=?",
|
|
955
|
-
(new_node_id, _now(), old_node_id),
|
|
956
|
-
)
|
|
957
|
-
return {"status": "ok", "node_id": old_node_id, "superseded_by": new_node_id}
|
|
958
|
-
|
|
959
497
|
def _v2_delete_nodes(self, conn: sqlite3.Connection, ids) -> None:
|
|
960
498
|
"""Mirror legacy node deletions into v2 (edges_v2 cascade on the FK)."""
|
|
961
499
|
if KGStoreV2 is None:
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Retrieval: the read path from a query to grounded context.
|
|
2
|
+
|
|
3
|
+
v11.3.0 turned this module into a package. ``KnowledgeGraphRetrievalMixin`` is
|
|
4
|
+
now composed from four cohesive sub-mixins — graph view + lexical search,
|
|
5
|
+
the hybrid pipeline, context assembly, and destructive maintenance — each of
|
|
6
|
+
which moved here verbatim. Every name this module exported before still
|
|
7
|
+
resolves from ``lattice_brain.graph.retrieval``.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
# ruff: noqa: F403,F405
|
|
13
|
+
from .._kg_common import * # noqa: F403,F401
|
|
14
|
+
from ..fusion import ( # noqa: F401
|
|
15
|
+
DEFAULT_EXPANSION_CAP,
|
|
16
|
+
DEFAULT_EXPANSION_SEEDS,
|
|
17
|
+
expand_with_neighbors,
|
|
18
|
+
graph_expansion_enabled,
|
|
19
|
+
rrf_fuse,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
# --- Compat seam (v9.9.5 decomposition) -------------------------------------
|
|
23
|
+
# The non-search read surface (list_documents / workspaces_of /
|
|
24
|
+
# filter_scoped_nodes / neighbors / get_node / relationship_search /
|
|
25
|
+
# traverse / stats) moved byte-identically to .retrieval_reads as
|
|
26
|
+
# KnowledgeGraphReadsMixin. Re-exported here so any legacy
|
|
27
|
+
# ``from lattice_brain.graph.retrieval import ...`` site keeps resolving.
|
|
28
|
+
from ..retrieval_reads import KnowledgeGraphReadsMixin # noqa: F401
|
|
29
|
+
from .context import _ContextMixin
|
|
30
|
+
from .graph_view import _GraphViewMixin
|
|
31
|
+
from .hybrid import _HybridSearchMixin
|
|
32
|
+
from .maintenance import _MaintenanceMixin
|
|
33
|
+
from .signals import ( # noqa: F401
|
|
34
|
+
MULTIMODAL_NODE_TYPES,
|
|
35
|
+
context_quality_signal,
|
|
36
|
+
multimodal_signal,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class KnowledgeGraphRetrievalMixin(
|
|
41
|
+
_ContextMixin,
|
|
42
|
+
_HybridSearchMixin,
|
|
43
|
+
_GraphViewMixin,
|
|
44
|
+
_MaintenanceMixin,
|
|
45
|
+
):
|
|
46
|
+
"""The graph read surface, composed from its four cohesive halves.
|
|
47
|
+
|
|
48
|
+
The sub-mixins define disjoint method sets, so resolution order changes
|
|
49
|
+
nothing at runtime: this class exposes exactly the methods it exposed when
|
|
50
|
+
they all lived in one 1,120-line module. The order is written
|
|
51
|
+
most-composed-first (context → hybrid → graph view) because each half
|
|
52
|
+
names the one below it as its typing-only base, and C3 needs a subclass
|
|
53
|
+
ahead of the class it extends.
|
|
54
|
+
"""
|