superlocalmemory 4.0.5 → 4.0.7
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/CHANGELOG.md +108 -0
- package/README.md +8 -9
- package/package.json +3 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/CLAUDE.md +3 -3
- package/plugin/agents/slm-governance-advisor.md +1 -1
- package/plugin/agents/slm-loop-runner.md +1 -1
- package/plugin/agents/slm-memory-advisor.md +1 -1
- package/plugin/agents/slm-optimize-advisor.md +1 -1
- package/plugin/requirements.txt +1 -1
- package/plugin/skills/slm-cache/SKILL.md +1 -1
- package/plugin/skills/slm-compress/SKILL.md +1 -1
- package/plugin/skills/slm-governance/SKILL.md +1 -1
- package/plugin/skills/slm-graph/SKILL.md +1 -1
- package/plugin/skills/slm-loop/SKILL.md +1 -1
- package/plugin/skills/slm-mesh/SKILL.md +1 -1
- package/plugin/skills/slm-profile/SKILL.md +1 -1
- package/plugin/skills/slm-recall/SKILL.md +1 -1
- package/plugin/skills/slm-remember/SKILL.md +1 -1
- package/plugin/skills/slm-scope/SKILL.md +1 -1
- package/plugin/skills/slm-session/SKILL.md +1 -1
- package/plugin/skills/slm-status/SKILL.md +1 -1
- package/plugin-src/rules/AGENTS.md +1 -1
- package/pyproject.toml +1 -1
- package/src/superlocalmemory/__init__.py +1 -1
- package/src/superlocalmemory/access/rbac.py +106 -0
- package/src/superlocalmemory/brain/truth.py +80 -10
- package/src/superlocalmemory/cli/__main__.py +17 -0
- package/src/superlocalmemory/cli/commands.py +28 -3
- package/src/superlocalmemory/cli/gdpr_cmd.py +779 -0
- package/src/superlocalmemory/cli/gdpr_io.py +109 -0
- package/src/superlocalmemory/cli/main.py +85 -0
- package/src/superlocalmemory/cli/summary_cmd.py +195 -0
- package/src/superlocalmemory/code_graph/bridge/entity_resolver.py +26 -0
- package/src/superlocalmemory/code_graph/bridge/event_listeners.py +14 -3
- package/src/superlocalmemory/code_graph/bridge/maintenance.py +206 -0
- package/src/superlocalmemory/code_graph/config.py +65 -1
- package/src/superlocalmemory/code_graph/extractors/__init__.py +17 -0
- package/src/superlocalmemory/code_graph/graph_store.py +180 -3
- package/src/superlocalmemory/code_graph/parser.py +280 -100
- package/src/superlocalmemory/compliance/gdpr.py +358 -0
- package/src/superlocalmemory/core/config.py +44 -1
- package/src/superlocalmemory/core/engine_wiring.py +5 -1
- package/src/superlocalmemory/core/fact_consolidator.py +24 -1
- package/src/superlocalmemory/core/maintenance.py +93 -1
- package/src/superlocalmemory/core/recall_worker.py +33 -12
- package/src/superlocalmemory/infra/backup.py +138 -0
- package/src/superlocalmemory/infra/backup_obligations.py +423 -0
- package/src/superlocalmemory/learning/engagement.py +165 -0
- package/src/superlocalmemory/mcp/tools_code_graph.py +78 -7
- package/src/superlocalmemory/mcp/tools_v3.py +20 -6
- package/src/superlocalmemory/retrieval/engine.py +21 -0
- package/src/superlocalmemory/retrieval/remote_reranker.py +108 -11
- package/src/superlocalmemory/server/routes/brain.py +283 -15
- package/src/superlocalmemory/server/routes/learning.py +13 -25
- package/src/superlocalmemory/server/routes/memories.py +61 -0
- package/src/superlocalmemory/server/routes/v3_api.py +171 -60
- package/src/superlocalmemory/storage/database.py +36 -0
- package/src/superlocalmemory/storage/models.py +12 -4
- package/src/superlocalmemory/storage/schema_code_graph.py +44 -1
- package/src/superlocalmemory/summaries/__init__.py +37 -0
- package/src/superlocalmemory/summaries/base.py +108 -0
- package/src/superlocalmemory/summaries/daily_reflection.py +293 -0
- package/src/superlocalmemory/summaries/project_work_log.py +424 -0
- package/src/superlocalmemory/summaries/session_summary.py +307 -0
- package/src/superlocalmemory/ui/css/design-system.css +76 -1
- package/src/superlocalmemory/ui/index.html +29 -12
- package/src/superlocalmemory/ui/js/fact-detail.js +61 -0
- package/src/superlocalmemory/ui/js/od-agents.js +49 -5
- package/src/superlocalmemory/ui/js/od-brain.js +257 -77
- package/src/superlocalmemory/ui/js/od-graph.js +147 -6
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
|
|
2
|
+
# Licensed under AGPL-3.0-or-later - see LICENSE file
|
|
3
|
+
# Part of SuperLocalMemory V4 | https://qualixar.com | https://varunpratap.com
|
|
4
|
+
|
|
5
|
+
"""Shared I/O helpers and compliance constants for ``slm gdpr`` subcommands.
|
|
6
|
+
|
|
7
|
+
Separated from gdpr_cmd.py to honour the 800-line file cap (coding-style.md).
|
|
8
|
+
All symbols here are internal (prefixed ``_``) or ALL_CAPS constants.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json as _json
|
|
14
|
+
import sys
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
20
|
+
# Output helpers
|
|
21
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
def _die(message: str, code: int = 1) -> None:
|
|
24
|
+
"""Print an actionable error to stderr and exit non-zero."""
|
|
25
|
+
print(f"error: {message}", file=sys.stderr)
|
|
26
|
+
sys.exit(code)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _print_json(data: Any) -> None:
|
|
30
|
+
"""Emit pretty-printed JSON to stdout (evidence-pipeline format)."""
|
|
31
|
+
print(_json.dumps(data, indent=2, default=str))
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _json_envelope(
|
|
35
|
+
command: str, *, data: dict | None = None, error: dict | None = None
|
|
36
|
+
) -> dict:
|
|
37
|
+
"""Build the standard agent-native JSON envelope used across all SLM CLIs."""
|
|
38
|
+
from superlocalmemory.cli.json_output import _get_version
|
|
39
|
+
|
|
40
|
+
env: dict = {
|
|
41
|
+
"success": error is None,
|
|
42
|
+
"command": command,
|
|
43
|
+
"version": _get_version(),
|
|
44
|
+
}
|
|
45
|
+
if error is not None:
|
|
46
|
+
env["error"] = error
|
|
47
|
+
else:
|
|
48
|
+
env["data"] = data if data is not None else {}
|
|
49
|
+
return env
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
53
|
+
# Path helpers
|
|
54
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
def _data_root() -> Path:
|
|
57
|
+
"""Resolve the canonical SLM data root (honours SLM_DATA_DIR env var)."""
|
|
58
|
+
from superlocalmemory.infra.data_root import canonical_data_root
|
|
59
|
+
|
|
60
|
+
return canonical_data_root()
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _db_path() -> Path:
|
|
64
|
+
return _data_root() / "memory.db"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _audit_chain_path() -> Path:
|
|
68
|
+
return _data_root() / "audit_chain.db"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
72
|
+
# GDPR compliance constants (DPO-facing, surfaced in ``status`` output)
|
|
73
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
# Known gaps in this release — reported honestly so a DPO can plan remediation.
|
|
76
|
+
# Gaps C1 and C2 are tracked as open items in the Wave 3 backlog.
|
|
77
|
+
KNOWN_GAPS = [
|
|
78
|
+
{
|
|
79
|
+
"ref": "C1",
|
|
80
|
+
"summary": "backups/ directory is outside erasure scope",
|
|
81
|
+
"detail": (
|
|
82
|
+
"Up to 10 rotating snapshots of memory.db, learning.db and "
|
|
83
|
+
"related DB files survive an Art.17 erasure. The erasure receipt "
|
|
84
|
+
"records a 'backup_obligation' so the obligation is tracked, but "
|
|
85
|
+
"the snapshots are not automatically purged. Snapshots are "
|
|
86
|
+
"re-erased on restore."
|
|
87
|
+
),
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
"ref": "C2",
|
|
91
|
+
"summary": "code_graph.db is not in erasure scope",
|
|
92
|
+
"detail": (
|
|
93
|
+
"Repository paths, file names and symbol names (identifying data "
|
|
94
|
+
"in a work context) are stored in code_graph.db. This file is "
|
|
95
|
+
"covered by compliance/gdpr.py but the per-table discovery "
|
|
96
|
+
"operates on the main memory.db only."
|
|
97
|
+
),
|
|
98
|
+
},
|
|
99
|
+
]
|
|
100
|
+
|
|
101
|
+
# Art. coverage flags that compliance/gdpr.py actively implements.
|
|
102
|
+
ART_COVERAGE = {
|
|
103
|
+
"art15_right_to_access": True,
|
|
104
|
+
"art17_right_to_erasure": True,
|
|
105
|
+
"art20_right_to_portability": True,
|
|
106
|
+
"backup_scope": "outstanding_obligation_recorded_on_receipt",
|
|
107
|
+
"audit_chain": "HMAC-chained tamper-evident",
|
|
108
|
+
"erasure_fails_closed": True,
|
|
109
|
+
}
|
|
@@ -88,6 +88,11 @@ _NO_DAEMON_COMMANDS = {
|
|
|
88
88
|
"help",
|
|
89
89
|
# Lifecycle orchestration must run before any global auto-start hook.
|
|
90
90
|
"serve", "restart",
|
|
91
|
+
# V4.0.6: GDPR CLI accesses the DB directly; no daemon required.
|
|
92
|
+
"gdpr",
|
|
93
|
+
# V4.0.7: summaries read memory.db directly and are extractive by default,
|
|
94
|
+
# so they need neither the daemon nor a language model.
|
|
95
|
+
"summary",
|
|
91
96
|
}
|
|
92
97
|
|
|
93
98
|
|
|
@@ -966,6 +971,86 @@ def main() -> None:
|
|
|
966
971
|
_sp.add_argument("--json", action="store_true",
|
|
967
972
|
help="Output structured JSON (agent-native)")
|
|
968
973
|
|
|
974
|
+
# V4.0.7: the readable summary layer from issue #113. The generators shipped
|
|
975
|
+
# in 4.0.6 with no caller; this is the surface that makes them reachable.
|
|
976
|
+
from superlocalmemory.cli.summary_cmd import register_summary_parser
|
|
977
|
+
|
|
978
|
+
register_summary_parser(sub)
|
|
979
|
+
|
|
980
|
+
# Wave-3 / V4.0.6: GDPR subject-rights CLI (Art.15/17/20)
|
|
981
|
+
gdpr_p = sub.add_parser(
|
|
982
|
+
"gdpr",
|
|
983
|
+
help="GDPR subject rights: status, export (Art.15/20), erase (Art.17), verify",
|
|
984
|
+
)
|
|
985
|
+
gdpr_sub = gdpr_p.add_subparsers(dest="gdpr_command", title="gdpr subcommands")
|
|
986
|
+
|
|
987
|
+
gdpr_status_p = gdpr_sub.add_parser(
|
|
988
|
+
"status",
|
|
989
|
+
help="Compliance posture: receipts, audit events, known gaps (read-only)",
|
|
990
|
+
)
|
|
991
|
+
gdpr_status_p.add_argument(
|
|
992
|
+
"--profile", default=None, metavar="PROFILE",
|
|
993
|
+
help="Scope to a specific profile (default: report all)",
|
|
994
|
+
)
|
|
995
|
+
gdpr_status_p.add_argument(
|
|
996
|
+
"--json", action="store_true", help="Output structured JSON (agent-native)",
|
|
997
|
+
)
|
|
998
|
+
|
|
999
|
+
gdpr_export_p = gdpr_sub.add_parser(
|
|
1000
|
+
"export",
|
|
1001
|
+
help="Art.15/20 subject access / data portability export",
|
|
1002
|
+
)
|
|
1003
|
+
gdpr_export_p.add_argument(
|
|
1004
|
+
"--profile", required=True, metavar="PROFILE",
|
|
1005
|
+
help="Profile to export (required)",
|
|
1006
|
+
)
|
|
1007
|
+
gdpr_export_p.add_argument(
|
|
1008
|
+
"--output", default=None, metavar="FILE",
|
|
1009
|
+
help="Write export JSON to FILE (default: stdout)",
|
|
1010
|
+
)
|
|
1011
|
+
gdpr_export_p.add_argument(
|
|
1012
|
+
"--json", action="store_true", help="Output structured JSON envelope",
|
|
1013
|
+
)
|
|
1014
|
+
|
|
1015
|
+
gdpr_erase_p = gdpr_sub.add_parser(
|
|
1016
|
+
"erase",
|
|
1017
|
+
help=(
|
|
1018
|
+
"Art.17 right to erasure — IRREVERSIBLE. "
|
|
1019
|
+
"Requires --profile PROFILE AND --yes."
|
|
1020
|
+
),
|
|
1021
|
+
)
|
|
1022
|
+
gdpr_erase_p.add_argument(
|
|
1023
|
+
"--profile", default=None, metavar="PROFILE",
|
|
1024
|
+
help="Profile to erase (required for live erasure)",
|
|
1025
|
+
)
|
|
1026
|
+
gdpr_erase_p.add_argument(
|
|
1027
|
+
"--yes", action="store_true",
|
|
1028
|
+
help="Confirm irreversible erasure (required together with --profile)",
|
|
1029
|
+
)
|
|
1030
|
+
gdpr_erase_p.add_argument(
|
|
1031
|
+
"--dry-run", action="store_true", dest="dry_run",
|
|
1032
|
+
help="Preview what would be erased without deleting anything",
|
|
1033
|
+
)
|
|
1034
|
+
gdpr_erase_p.add_argument(
|
|
1035
|
+
"--json", action="store_true", help="Output structured JSON",
|
|
1036
|
+
)
|
|
1037
|
+
|
|
1038
|
+
gdpr_verify_p = gdpr_sub.add_parser(
|
|
1039
|
+
"verify",
|
|
1040
|
+
help="Verify HMAC integrity of an erasure receipt (exit 0=ok, 1=tampered, 2=not-found)",
|
|
1041
|
+
)
|
|
1042
|
+
gdpr_verify_p.add_argument(
|
|
1043
|
+
"--receipt-id", dest="receipt_id", default=None, metavar="ID",
|
|
1044
|
+
help="Erasure receipt ID to verify",
|
|
1045
|
+
)
|
|
1046
|
+
gdpr_verify_p.add_argument(
|
|
1047
|
+
"--profile", default=None, metavar="PROFILE",
|
|
1048
|
+
help="Scope receipt lookup to a specific profile",
|
|
1049
|
+
)
|
|
1050
|
+
gdpr_verify_p.add_argument(
|
|
1051
|
+
"--json", action="store_true", help="Output structured JSON",
|
|
1052
|
+
)
|
|
1053
|
+
|
|
969
1054
|
# Wave-3: operational recovery & admin remediation
|
|
970
1055
|
ops_p = sub.add_parser(
|
|
971
1056
|
"ops",
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
|
|
2
|
+
# Licensed under AGPL-3.0-or-later - see LICENSE file
|
|
3
|
+
# Part of SuperLocalMemory | https://qualixar.com
|
|
4
|
+
|
|
5
|
+
"""``slm summary`` — the readable layer over your memories (issue #113).
|
|
6
|
+
|
|
7
|
+
WHY THIS FILE EXISTS
|
|
8
|
+
--------------------
|
|
9
|
+
4.0.6 shipped the three generators in ``superlocalmemory/summaries/`` with no way
|
|
10
|
+
to call them: no command, no MCP tool, no route. The changelog listed the feature
|
|
11
|
+
as added, the issue reply said it had landed, and a user could do nothing with it.
|
|
12
|
+
This is that missing surface.
|
|
13
|
+
|
|
14
|
+
Three summaries, each bounded and traceable:
|
|
15
|
+
|
|
16
|
+
``slm summary session <id>`` what one session covered
|
|
17
|
+
``slm summary day [DATE]`` what a day's main topics were
|
|
18
|
+
``slm summary project <path>`` what was worked on in a project
|
|
19
|
+
|
|
20
|
+
Every result states its coverage. Session data in particular is sparse — roughly
|
|
21
|
+
4% of facts carry a session id on a real store — so a session summary reports what
|
|
22
|
+
fraction it could actually see rather than presenting a slice as the whole.
|
|
23
|
+
|
|
24
|
+
No language model is required: the generators are extractive by default, so this
|
|
25
|
+
works in Local Guardian mode with nothing installed.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import json
|
|
31
|
+
from argparse import Namespace
|
|
32
|
+
from datetime import date, timedelta
|
|
33
|
+
from pathlib import Path
|
|
34
|
+
from typing import Any
|
|
35
|
+
|
|
36
|
+
from superlocalmemory.infra.data_root import state_path
|
|
37
|
+
|
|
38
|
+
def _coverage_is_complete(coverage: str) -> bool:
|
|
39
|
+
"""Whether *coverage* means "this really is the whole picture".
|
|
40
|
+
|
|
41
|
+
Deliberately inverted. My first version listed the values that needed a
|
|
42
|
+
caveat — ``("partial", "sparse", "none", "empty")`` — and three of those four
|
|
43
|
+
are not values this system emits. The real vocabulary is COVERAGE_FULL /
|
|
44
|
+
PARTIAL / INSUFFICIENT / NO_SESSION / UNAVAILABLE, so a session summary
|
|
45
|
+
reporting "no_session" printed no caveat at all: the one honesty feature
|
|
46
|
+
issue #113 asked for, silently inactive.
|
|
47
|
+
|
|
48
|
+
Testing for completeness instead means any value that is not FULL — including
|
|
49
|
+
one added later — gets the caveat. The failure mode becomes an unnecessary
|
|
50
|
+
warning rather than a missing one.
|
|
51
|
+
"""
|
|
52
|
+
try:
|
|
53
|
+
from superlocalmemory.summaries.base import COVERAGE_FULL
|
|
54
|
+
|
|
55
|
+
return coverage == COVERAGE_FULL
|
|
56
|
+
except Exception:
|
|
57
|
+
return coverage == "full"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _db_path() -> Path:
|
|
61
|
+
return state_path("memory.db")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _active_profile() -> str:
|
|
65
|
+
"""Resolve the active profile without a daemon and without side effects.
|
|
66
|
+
|
|
67
|
+
Reads the ``active`` pointer out of ``profiles.json`` directly.
|
|
68
|
+
``ProfileManager`` would give the same answer, but its constructor calls
|
|
69
|
+
``mkdir(parents=True)`` — creating directories is not something a read-only
|
|
70
|
+
summary command should do. ``core.profiles`` also has no module-level
|
|
71
|
+
accessor; ``get_active_profile`` there is a method on the manager, and the
|
|
72
|
+
module-level one lives in ``server/routes/helpers.py``, which the CLI must
|
|
73
|
+
not import.
|
|
74
|
+
"""
|
|
75
|
+
try:
|
|
76
|
+
from superlocalmemory.core.profiles import DEFAULT_PROFILES_FILE
|
|
77
|
+
|
|
78
|
+
path = state_path(DEFAULT_PROFILES_FILE)
|
|
79
|
+
if path.exists():
|
|
80
|
+
raw = json.loads(path.read_text(encoding="utf-8"))
|
|
81
|
+
active = raw.get("active")
|
|
82
|
+
if isinstance(active, str) and active:
|
|
83
|
+
return active
|
|
84
|
+
except Exception:
|
|
85
|
+
pass
|
|
86
|
+
return "default"
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _emit(result: Any, as_json: bool) -> None:
|
|
90
|
+
"""Print a SummaryResult as JSON or as prose."""
|
|
91
|
+
if as_json:
|
|
92
|
+
print(json.dumps({
|
|
93
|
+
"kind": result.kind,
|
|
94
|
+
"profile_id": result.profile_id,
|
|
95
|
+
"content": result.content,
|
|
96
|
+
"source_fact_ids": result.source_fact_ids,
|
|
97
|
+
"coverage": result.coverage,
|
|
98
|
+
"generated_by": result.generated_by,
|
|
99
|
+
"metadata": result.metadata,
|
|
100
|
+
}, indent=2, default=str))
|
|
101
|
+
return
|
|
102
|
+
|
|
103
|
+
print()
|
|
104
|
+
print(result.content.rstrip() or "(nothing recorded)")
|
|
105
|
+
print()
|
|
106
|
+
|
|
107
|
+
# Coverage is not decoration. A summary built from a fraction of the data
|
|
108
|
+
# that presents itself as the whole is the failure mode issue #113 called
|
|
109
|
+
# out by name, so it is stated on every single result, not only bad ones.
|
|
110
|
+
n = len(result.source_fact_ids)
|
|
111
|
+
line = f"Built from {n} memor{'y' if n == 1 else 'ies'} · coverage: {result.coverage}"
|
|
112
|
+
if not _coverage_is_complete(result.coverage):
|
|
113
|
+
line += " — treat as a partial view, not a complete record"
|
|
114
|
+
print(line)
|
|
115
|
+
if result.generated_by:
|
|
116
|
+
print(f"Method: {result.generated_by}")
|
|
117
|
+
print("Use --json to see the exact memories this came from.")
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def cmd_summary(args: Namespace) -> None:
|
|
121
|
+
"""Dispatch ``slm summary <subcommand>``."""
|
|
122
|
+
sub = getattr(args, "summary_command", None)
|
|
123
|
+
as_json = bool(getattr(args, "json", False))
|
|
124
|
+
profile = getattr(args, "profile", None) or _active_profile()
|
|
125
|
+
db = _db_path()
|
|
126
|
+
|
|
127
|
+
if not db.exists():
|
|
128
|
+
print(f"No memory database at {db}. Run `slm status` first.")
|
|
129
|
+
return
|
|
130
|
+
|
|
131
|
+
if sub == "session":
|
|
132
|
+
from superlocalmemory.summaries import generate_session_summary
|
|
133
|
+
|
|
134
|
+
_emit(generate_session_summary(db, args.session_id, profile), as_json)
|
|
135
|
+
return
|
|
136
|
+
|
|
137
|
+
if sub == "day":
|
|
138
|
+
from superlocalmemory.summaries import generate_daily_reflection
|
|
139
|
+
|
|
140
|
+
target = getattr(args, "date", None) or date.today().isoformat()
|
|
141
|
+
if target == "yesterday":
|
|
142
|
+
target = (date.today() - timedelta(days=1)).isoformat()
|
|
143
|
+
elif target == "today":
|
|
144
|
+
target = date.today().isoformat()
|
|
145
|
+
_emit(generate_daily_reflection(db, target, profile), as_json)
|
|
146
|
+
return
|
|
147
|
+
|
|
148
|
+
if sub == "project":
|
|
149
|
+
from superlocalmemory.summaries import generate_project_work_log
|
|
150
|
+
|
|
151
|
+
path = getattr(args, "path", None) or str(Path.cwd())
|
|
152
|
+
_emit(generate_project_work_log(db, path, profile), as_json)
|
|
153
|
+
return
|
|
154
|
+
|
|
155
|
+
print("Usage: slm summary {session <id> | day [DATE] | project [PATH]}")
|
|
156
|
+
print()
|
|
157
|
+
print(" slm summary day what you recorded today")
|
|
158
|
+
print(" slm summary day yesterday ...or yesterday")
|
|
159
|
+
print(" slm summary day 2026-08-17 ...or a specific date")
|
|
160
|
+
print(" slm summary project work log for the current directory")
|
|
161
|
+
print(" slm summary session <id> what one session covered")
|
|
162
|
+
print()
|
|
163
|
+
print("Add --json to include the ids of the memories a summary came from.")
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def register_summary_parser(sub: Any) -> None:
|
|
167
|
+
"""Attach the ``summary`` parser. Called from cli/main.py."""
|
|
168
|
+
p = sub.add_parser(
|
|
169
|
+
"summary",
|
|
170
|
+
help="Readable summaries of your memories (session, day, project)",
|
|
171
|
+
)
|
|
172
|
+
p.add_argument("--json", action="store_true", help="machine-readable output")
|
|
173
|
+
p.add_argument("--profile", help="profile to summarise (default: active)")
|
|
174
|
+
ssub = p.add_subparsers(dest="summary_command", title="summary subcommands")
|
|
175
|
+
|
|
176
|
+
s = ssub.add_parser("session", help="what one session covered")
|
|
177
|
+
s.add_argument("session_id", help="session id (see `slm status`)")
|
|
178
|
+
s.add_argument("--json", action="store_true")
|
|
179
|
+
s.add_argument("--profile")
|
|
180
|
+
|
|
181
|
+
d = ssub.add_parser("day", help="what a day's main topics were")
|
|
182
|
+
d.add_argument(
|
|
183
|
+
"date", nargs="?",
|
|
184
|
+
help="YYYY-MM-DD, 'today' or 'yesterday' (default: today)",
|
|
185
|
+
)
|
|
186
|
+
d.add_argument("--json", action="store_true")
|
|
187
|
+
d.add_argument("--profile")
|
|
188
|
+
|
|
189
|
+
pr = ssub.add_parser("project", help="what was worked on in a project")
|
|
190
|
+
pr.add_argument(
|
|
191
|
+
"path", nargs="?",
|
|
192
|
+
help="project directory (default: current directory)",
|
|
193
|
+
)
|
|
194
|
+
pr.add_argument("--json", action="store_true")
|
|
195
|
+
pr.add_argument("--profile")
|
|
@@ -152,9 +152,25 @@ class EntityResolver:
|
|
|
152
152
|
self,
|
|
153
153
|
fact_text: str,
|
|
154
154
|
fact_id: str,
|
|
155
|
+
max_links: int | None = None,
|
|
155
156
|
) -> list[CodeMemoryLink]:
|
|
156
157
|
"""Resolve code entity mentions in fact text and create links.
|
|
157
158
|
|
|
159
|
+
Args:
|
|
160
|
+
fact_text: Text to scan for code mentions.
|
|
161
|
+
fact_id: The fact these links belong to.
|
|
162
|
+
max_links: Keep at most this many links, highest confidence first.
|
|
163
|
+
``None`` (the default) is unbounded, preserving the behaviour the
|
|
164
|
+
manual ``link_memory_to_code`` path relies on.
|
|
165
|
+
|
|
166
|
+
A bound matters for automatic resolution. One file-path mention
|
|
167
|
+
matches EVERY node in that file: the fact "the parser in
|
|
168
|
+
code_graph/parser.py was dropping edges" produced 17 links at
|
|
169
|
+
confidence 0.6-0.8, while a backticked function name produces one
|
|
170
|
+
at 0.95. Unbounded, a fact naming a few files buries its own
|
|
171
|
+
high-signal links and hands a large node set to HebbianLinker,
|
|
172
|
+
whose neighbourhood expansion then grows accordingly.
|
|
173
|
+
|
|
158
174
|
Returns list of CodeMemoryLink objects created.
|
|
159
175
|
"""
|
|
160
176
|
if not fact_text or not fact_id:
|
|
@@ -177,6 +193,16 @@ class EntityResolver:
|
|
|
177
193
|
if not matches:
|
|
178
194
|
return []
|
|
179
195
|
|
|
196
|
+
selected = list(matches.values())
|
|
197
|
+
if max_links is not None and len(selected) > max_links:
|
|
198
|
+
# Confidence ranks the match kinds correctly already — backticked and
|
|
199
|
+
# call-syntax mentions score 0.95, a bare identifier 0.9, file-path
|
|
200
|
+
# fan-out 0.6-0.8 — so ordering by it keeps the precise mentions and
|
|
201
|
+
# drops the broad ones.
|
|
202
|
+
selected.sort(key=lambda m: m.confidence, reverse=True)
|
|
203
|
+
selected = selected[:max_links]
|
|
204
|
+
matches = {m.node_id: m for m in selected}
|
|
205
|
+
|
|
180
206
|
# Classify link type
|
|
181
207
|
link_type = self._classify_link_type(fact_text)
|
|
182
208
|
now_str = datetime.now(timezone.utc).isoformat()
|
|
@@ -57,12 +57,23 @@ class BridgeEventListeners:
|
|
|
57
57
|
return self._started
|
|
58
58
|
|
|
59
59
|
def start(self, event_bus: Any) -> None:
|
|
60
|
-
"""Register
|
|
60
|
+
"""Register code-graph listeners on the event bus.
|
|
61
61
|
|
|
62
62
|
Registers:
|
|
63
|
-
- on_memory_stored: listens to "memory.stored"
|
|
64
63
|
- on_code_node_deleted: listens to "code_graph.node_deleted"
|
|
65
64
|
- on_code_node_changed: listens to "code_graph.node_changed"
|
|
65
|
+
|
|
66
|
+
DELIBERATELY NOT REGISTERED: ``on_memory_stored``.
|
|
67
|
+
``EventBus._notify_listeners`` invokes every listener synchronously on
|
|
68
|
+
the emitting thread, so subscribing to ``memory.stored`` would run entity
|
|
69
|
+
resolution, enrichment and Hebbian linking inside each ``remember`` —
|
|
70
|
+
putting all three on the write path. That work now runs in background
|
|
71
|
+
maintenance instead; see ``code_graph.bridge.maintenance``.
|
|
72
|
+
|
|
73
|
+
``on_memory_stored`` is kept as a callable so a caller that genuinely
|
|
74
|
+
wants synchronous linking for one fact can invoke it directly, but
|
|
75
|
+
nothing subscribes it to the bus. ``tests/test_code_graph/
|
|
76
|
+
test_bridge_off_write_path.py`` fails if that changes.
|
|
66
77
|
"""
|
|
67
78
|
if self._started:
|
|
68
79
|
logger.warning("BridgeEventListeners already started")
|
|
@@ -70,8 +81,8 @@ class BridgeEventListeners:
|
|
|
70
81
|
|
|
71
82
|
self._event_bus = event_bus
|
|
72
83
|
|
|
84
|
+
# Both events are emitted by code-graph builds, never by memory writes.
|
|
73
85
|
listeners: list[tuple[str, Callable[..., Any]]] = [
|
|
74
|
-
("memory.stored", self.on_memory_stored),
|
|
75
86
|
("code_graph.node_deleted", self.on_code_node_deleted),
|
|
76
87
|
("code_graph.node_changed", self.on_code_node_changed),
|
|
77
88
|
]
|