memstack-skill-loader 4.18.0__tar.gz → 4.18.1__tar.gz
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.
- {memstack_skill_loader-4.18.0/src/memstack_skill_loader.egg-info → memstack_skill_loader-4.18.1}/PKG-INFO +1 -1
- {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.18.1}/pyproject.toml +51 -51
- {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.18.1}/src/memstack_skill_loader/__init__.py +3 -3
- {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.18.1}/src/memstack_skill_loader/memory_db.py +97 -52
- {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.18.1}/src/memstack_skill_loader/memory_migrations.py +852 -774
- {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.18.1/src/memstack_skill_loader.egg-info}/PKG-INFO +1 -1
- {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.18.1}/tests/test_memory_migrations.py +178 -22
- {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.18.1}/MANIFEST.in +0 -0
- {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.18.1}/README.md +0 -0
- {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.18.1}/setup.cfg +0 -0
- {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.18.1}/src/memstack_skill_loader/__main__.py +0 -0
- {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.18.1}/src/memstack_skill_loader/agent_runner.py +0 -0
- {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.18.1}/src/memstack_skill_loader/categories.py +0 -0
- {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.18.1}/src/memstack_skill_loader/compression.py +0 -0
- {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.18.1}/src/memstack_skill_loader/config.py +0 -0
- {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.18.1}/src/memstack_skill_loader/dashboard.html +0 -0
- {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.18.1}/src/memstack_skill_loader/dashboard.py +0 -0
- {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.18.1}/src/memstack_skill_loader/diary_ingest.py +0 -0
- {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.18.1}/src/memstack_skill_loader/goal.py +0 -0
- {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.18.1}/src/memstack_skill_loader/goal_engine.py +0 -0
- {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.18.1}/src/memstack_skill_loader/hooks/__init__.py +0 -0
- {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.18.1}/src/memstack_skill_loader/hooks/deny_git.py +0 -0
- {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.18.1}/src/memstack_skill_loader/hooks/deny_manager_write.py +0 -0
- {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.18.1}/src/memstack_skill_loader/hooks/deny_reviewer_git.py +0 -0
- {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.18.1}/src/memstack_skill_loader/hooks/git_policy.py +0 -0
- {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.18.1}/src/memstack_skill_loader/index_meta.py +0 -0
- {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.18.1}/src/memstack_skill_loader/indexer.py +0 -0
- {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.18.1}/src/memstack_skill_loader/license.py +0 -0
- {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.18.1}/src/memstack_skill_loader/memory_engine.py +0 -0
- {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.18.1}/src/memstack_skill_loader/proxy/__init__.py +0 -0
- {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.18.1}/src/memstack_skill_loader/proxy/_diag.py +0 -0
- {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.18.1}/src/memstack_skill_loader/proxy/body_parser.py +0 -0
- {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.18.1}/src/memstack_skill_loader/proxy/compressor.py +0 -0
- {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.18.1}/src/memstack_skill_loader/proxy/forwarder.py +0 -0
- {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.18.1}/src/memstack_skill_loader/proxy/pro_compressor.py +0 -0
- {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.18.1}/src/memstack_skill_loader/proxy/server.py +0 -0
- {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.18.1}/src/memstack_skill_loader/proxy/stats_tracker.py +0 -0
- {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.18.1}/src/memstack_skill_loader/search.py +0 -0
- {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.18.1}/src/memstack_skill_loader/server.py +0 -0
- {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.18.1}/src/memstack_skill_loader/session_context.py +0 -0
- {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.18.1}/src/memstack_skill_loader/skill_config.py +0 -0
- {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.18.1}/src/memstack_skill_loader/skill_descriptions.json +0 -0
- {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.18.1}/src/memstack_skill_loader/stats.py +0 -0
- {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.18.1}/src/memstack_skill_loader/tfidf_search.py +0 -0
- {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.18.1}/src/memstack_skill_loader/version_check.py +0 -0
- {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.18.1}/src/memstack_skill_loader.egg-info/SOURCES.txt +0 -0
- {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.18.1}/src/memstack_skill_loader.egg-info/dependency_links.txt +0 -0
- {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.18.1}/src/memstack_skill_loader.egg-info/entry_points.txt +0 -0
- {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.18.1}/src/memstack_skill_loader.egg-info/requires.txt +0 -0
- {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.18.1}/src/memstack_skill_loader.egg-info/top_level.txt +0 -0
- {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.18.1}/tests/test_advisor_strategy.py +0 -0
- {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.18.1}/tests/test_body_parser_metric.py +0 -0
- {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.18.1}/tests/test_builder_scope.py +0 -0
- {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.18.1}/tests/test_code_read_savings.py +0 -0
- {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.18.1}/tests/test_dashscan.py +0 -0
- {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.18.1}/tests/test_diary_ingest.py +0 -0
- {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.18.1}/tests/test_failed_approaches.py +0 -0
- {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.18.1}/tests/test_git_policy.py +0 -0
- {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.18.1}/tests/test_index_meta_guard.py +0 -0
- {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.18.1}/tests/test_license_environment.py +0 -0
- {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.18.1}/tests/test_license_grace.py +0 -0
- {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.18.1}/tests/test_memory_engine.py +0 -0
- {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.18.1}/tests/test_memory_inspector.py +0 -0
- {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.18.1}/tests/test_memory_scoring.py +0 -0
- {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.18.1}/tests/test_memory_tools.py +0 -0
- {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.18.1}/tests/test_pro_compressor.py +0 -0
- {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.18.1}/tests/test_pro_skills_update.py +0 -0
- {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.18.1}/tests/test_pro_source_resolution.py +0 -0
- {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.18.1}/tests/test_project_dir_canonical.py +0 -0
- {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.18.1}/tests/test_search_memory.py +0 -0
- {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.18.1}/tests/test_session_context.py +0 -0
- {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.18.1}/tests/test_skill_drift.py +0 -0
- {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.18.1}/tests/test_wheel_contents.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: memstack-skill-loader
|
|
3
|
-
Version: 4.18.
|
|
3
|
+
Version: 4.18.1
|
|
4
4
|
Summary: MCP server that vector-indexes MemStack Pro skills for on-demand loading
|
|
5
5
|
Project-URL: Homepage, https://memstack.pro
|
|
6
6
|
Project-URL: Documentation, https://memstack.pro/docs
|
|
@@ -1,51 +1,51 @@
|
|
|
1
|
-
[build-system]
|
|
2
|
-
requires = ["setuptools>=68.0"]
|
|
3
|
-
build-backend = "setuptools.build_meta"
|
|
4
|
-
|
|
5
|
-
[project]
|
|
6
|
-
name = "memstack-skill-loader"
|
|
7
|
-
version = "4.18.
|
|
8
|
-
description = "MCP server that vector-indexes MemStack Pro skills for on-demand loading"
|
|
9
|
-
readme = "README.md"
|
|
10
|
-
requires-python = ">=3.10"
|
|
11
|
-
dependencies = [
|
|
12
|
-
# Upper bound is load-bearing: mcp 2.0.0 (2026-07-28) removed the
|
|
13
|
-
# Server.list_tools()/call_tool() decorator API that server.py is built on.
|
|
14
|
-
# Do not relax without porting server.py to the 2.x API first.
|
|
15
|
-
"mcp>=1.0.0,<2.0.0",
|
|
16
|
-
"lancedb>=0.6.0",
|
|
17
|
-
"sentence-transformers>=2.2.0",
|
|
18
|
-
"pyarrow>=14.0.0",
|
|
19
|
-
"httpx>=0.24.0",
|
|
20
|
-
"aiohttp>=3.9",
|
|
21
|
-
]
|
|
22
|
-
|
|
23
|
-
[project.urls]
|
|
24
|
-
Homepage = "https://memstack.pro"
|
|
25
|
-
Documentation = "https://memstack.pro/docs"
|
|
26
|
-
|
|
27
|
-
[project.optional-dependencies]
|
|
28
|
-
# setuptools is a TEST dependency here, not only a build dependency. The
|
|
29
|
-
# packaging guard in tests/test_wheel_contents.py builds a wheel and inspects
|
|
30
|
-
# it, and it fails rather than skips when the backend is absent, because a skip
|
|
31
|
-
# is a silent pass. Python 3.12+ venvs no longer ship setuptools, so a bare venv
|
|
32
|
-
# needs: pip install -e ".[dev]"
|
|
33
|
-
dev = ["setuptools>=68"]
|
|
34
|
-
|
|
35
|
-
[project.scripts]
|
|
36
|
-
memstack-index = "memstack_skill_loader.indexer:main"
|
|
37
|
-
|
|
38
|
-
[tool.setuptools.packages.find]
|
|
39
|
-
where = ["src"]
|
|
40
|
-
|
|
41
|
-
[tool.setuptools.package-data]
|
|
42
|
-
memstack_skill_loader = ["dashboard.html", "skill_descriptions.json"]
|
|
43
|
-
|
|
44
|
-
[tool.pytest.ini_options]
|
|
45
|
-
# Test the WORKING TREE, not whatever is installed. This package is installed
|
|
46
|
-
# non-editable, so without this the suite imported memstack_skill_loader from
|
|
47
|
-
# site-packages and silently graded a stale copy: a source change could be
|
|
48
|
-
# green here and still be broken, and a new test could fail against code that
|
|
49
|
-
# was already fixed. pythonpath puts src/ ahead of site-packages for the whole
|
|
50
|
-
# suite, so `python -m pytest` needs no PYTHONPATH prefix to be correct.
|
|
51
|
-
pythonpath = ["src"]
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "memstack-skill-loader"
|
|
7
|
+
version = "4.18.1"
|
|
8
|
+
description = "MCP server that vector-indexes MemStack Pro skills for on-demand loading"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
dependencies = [
|
|
12
|
+
# Upper bound is load-bearing: mcp 2.0.0 (2026-07-28) removed the
|
|
13
|
+
# Server.list_tools()/call_tool() decorator API that server.py is built on.
|
|
14
|
+
# Do not relax without porting server.py to the 2.x API first.
|
|
15
|
+
"mcp>=1.0.0,<2.0.0",
|
|
16
|
+
"lancedb>=0.6.0",
|
|
17
|
+
"sentence-transformers>=2.2.0",
|
|
18
|
+
"pyarrow>=14.0.0",
|
|
19
|
+
"httpx>=0.24.0",
|
|
20
|
+
"aiohttp>=3.9",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[project.urls]
|
|
24
|
+
Homepage = "https://memstack.pro"
|
|
25
|
+
Documentation = "https://memstack.pro/docs"
|
|
26
|
+
|
|
27
|
+
[project.optional-dependencies]
|
|
28
|
+
# setuptools is a TEST dependency here, not only a build dependency. The
|
|
29
|
+
# packaging guard in tests/test_wheel_contents.py builds a wheel and inspects
|
|
30
|
+
# it, and it fails rather than skips when the backend is absent, because a skip
|
|
31
|
+
# is a silent pass. Python 3.12+ venvs no longer ship setuptools, so a bare venv
|
|
32
|
+
# needs: pip install -e ".[dev]"
|
|
33
|
+
dev = ["setuptools>=68"]
|
|
34
|
+
|
|
35
|
+
[project.scripts]
|
|
36
|
+
memstack-index = "memstack_skill_loader.indexer:main"
|
|
37
|
+
|
|
38
|
+
[tool.setuptools.packages.find]
|
|
39
|
+
where = ["src"]
|
|
40
|
+
|
|
41
|
+
[tool.setuptools.package-data]
|
|
42
|
+
memstack_skill_loader = ["dashboard.html", "skill_descriptions.json"]
|
|
43
|
+
|
|
44
|
+
[tool.pytest.ini_options]
|
|
45
|
+
# Test the WORKING TREE, not whatever is installed. This package is installed
|
|
46
|
+
# non-editable, so without this the suite imported memstack_skill_loader from
|
|
47
|
+
# site-packages and silently graded a stale copy: a source change could be
|
|
48
|
+
# green here and still be broken, and a new test could fail against code that
|
|
49
|
+
# was already fixed. pythonpath puts src/ ahead of site-packages for the whole
|
|
50
|
+
# suite, so `python -m pytest` needs no PYTHONPATH prefix to be correct.
|
|
51
|
+
pythonpath = ["src"]
|
{memstack_skill_loader-4.18.0 → memstack_skill_loader-4.18.1}/src/memstack_skill_loader/__init__.py
RENAMED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
"""MemStack Skill Loader — MCP server for semantic skill search."""
|
|
2
|
-
|
|
3
|
-
__version__ = "4.18.
|
|
1
|
+
"""MemStack Skill Loader — MCP server for semantic skill search."""
|
|
2
|
+
|
|
3
|
+
__version__ = "4.18.1"
|
{memstack_skill_loader-4.18.0 → memstack_skill_loader-4.18.1}/src/memstack_skill_loader/memory_db.py
RENAMED
|
@@ -11,6 +11,18 @@ from pathlib import Path, PureWindowsPath
|
|
|
11
11
|
DB_PATH = Path.home() / ".memstack" / "memory.db"
|
|
12
12
|
SCHEMA_VERSION = 1
|
|
13
13
|
|
|
14
|
+
#: Seconds a connection waits for a lock before raising "database is locked".
|
|
15
|
+
#: Deliberately the same number as ``memory_engine._BUSY_TIMEOUT_S``: the two
|
|
16
|
+
#: modules open connections to the SAME file, and a five-second waiter losing to
|
|
17
|
+
#: a thirty-second one is a difference with no reason behind it.
|
|
18
|
+
#:
|
|
19
|
+
#: This is a cushion for TRANSIENT contention, not a fix for a held lock. The
|
|
20
|
+
#: rehome migration used to compute its map inside its own BEGIN IMMEDIATE, so
|
|
21
|
+
#: every connection opened here waited the full timeout and then failed no
|
|
22
|
+
#: matter how large the number was — the holder was the caller. That map now
|
|
23
|
+
#: runs before the write lock is taken, so this timeout should never be reached.
|
|
24
|
+
_BUSY_TIMEOUT_S = 30
|
|
25
|
+
|
|
14
26
|
# ── project_dir canonicalization ─────────────────────────────────────────────
|
|
15
27
|
# project_dir was stored raw from os.getcwd(), which on Windows returns whichever
|
|
16
28
|
# drive-letter case the process happened to be launched with. Two spellings of
|
|
@@ -63,11 +75,20 @@ def _resolve_db_path() -> Path:
|
|
|
63
75
|
|
|
64
76
|
|
|
65
77
|
def _get_conn() -> sqlite3.Connection:
|
|
78
|
+
# NOTE: opening a connection WRITES. _init_tables ends with an
|
|
79
|
+
# "INSERT OR IGNORE INTO schema_version", and SQLite takes the write lock
|
|
80
|
+
# at statement start whether or not the row already exists. So this is not
|
|
81
|
+
# a read-only operation and must never be called from inside another
|
|
82
|
+
# transaction on the same file.
|
|
66
83
|
db_path = _resolve_db_path()
|
|
67
84
|
db_path.parent.mkdir(parents=True, exist_ok=True)
|
|
68
|
-
conn = sqlite3.connect(str(db_path), timeout=
|
|
85
|
+
conn = sqlite3.connect(str(db_path), timeout=_BUSY_TIMEOUT_S)
|
|
69
86
|
conn.row_factory = sqlite3.Row
|
|
70
87
|
conn.execute("PRAGMA journal_mode=WAL")
|
|
88
|
+
# Stated as a PRAGMA as well as the connect() kwarg. They set the same
|
|
89
|
+
# thing, but the PRAGMA is the one that survives a future refactor of the
|
|
90
|
+
# connect call, and it is the form the engine uses.
|
|
91
|
+
conn.execute(f"PRAGMA busy_timeout={_BUSY_TIMEOUT_S * 1000}")
|
|
71
92
|
_init_tables(conn)
|
|
72
93
|
return conn
|
|
73
94
|
|
|
@@ -771,59 +792,67 @@ def find_project_dirs_by_name(name: str) -> list[str]:
|
|
|
771
792
|
stored paths are Windows backslash paths, and ``os.path.basename`` does not
|
|
772
793
|
split on ``\\`` when this DB is read on a POSIX host. ``PureWindowsPath``
|
|
773
794
|
treats both ``\\`` and ``/`` as separators regardless of the running OS.
|
|
774
|
-
"""
|
|
775
|
-
try:
|
|
776
|
-
if name is None:
|
|
777
|
-
return []
|
|
778
|
-
if not isinstance(name, str):
|
|
779
|
-
name = str(name)
|
|
780
|
-
key = name.strip().lower()
|
|
781
|
-
if not key:
|
|
782
|
-
return []
|
|
783
795
|
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
"UNION SELECT project_dir FROM temporal_events "
|
|
792
|
-
"UNION SELECT project_dir FROM failed_approaches"
|
|
793
|
-
") WHERE project_dir IS NOT NULL"
|
|
794
|
-
).fetchall()
|
|
795
|
-
finally:
|
|
796
|
-
conn.close()
|
|
796
|
+
RAISES on an unreachable store. This used to swallow every exception and
|
|
797
|
+
return ``[]``, which made "the store has never seen this project" and "the
|
|
798
|
+
store could not be read" the same answer. That cost a data migration: the
|
|
799
|
+
rehome migration's fail-open tripwire lives in ``diary_ingest._resolves``,
|
|
800
|
+
one level ABOVE this function, and could never fire because the exception
|
|
801
|
+
died here. A caller that wants to continue without an answer catches it and
|
|
802
|
+
says so; a caller that must not guess lets it propagate. Both exist.
|
|
797
803
|
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
# Collapse spellings that denote the SAME directory (drive-letter case,
|
|
807
|
-
# separator style) before counting — otherwise one stray "c:\..." row
|
|
808
|
-
# turns a perfectly well-known project into an ambiguous one. Distinct
|
|
809
|
-
# directories that merely share a basename remain separate entries.
|
|
810
|
-
groups: dict = {}
|
|
811
|
-
for match in matches:
|
|
812
|
-
groups.setdefault(_project_dir_key(match), []).append(match)
|
|
813
|
-
|
|
814
|
-
# Deterministic representative per group: sorted() puts the upper-case
|
|
815
|
-
# drive first, and canonicalizing makes the result independent of which
|
|
816
|
-
# spellings happen to be in the store.
|
|
817
|
-
return sorted(
|
|
818
|
-
canonical_project_dir(sorted(spellings)[0])
|
|
819
|
-
for spellings in groups.values()
|
|
820
|
-
)
|
|
821
|
-
except Exception as exc:
|
|
822
|
-
print(
|
|
823
|
-
f"[memstack-memory] find_project_dirs_by_name failed: {exc}",
|
|
824
|
-
file=sys.stderr,
|
|
825
|
-
)
|
|
804
|
+
Removing the swallow also removed a ``print`` of the raw exception text.
|
|
805
|
+
That string is not allowlisted and could carry a path or a connection
|
|
806
|
+
string, so the callers now report the exception CLASS and nothing else.
|
|
807
|
+
|
|
808
|
+
Bad INPUT is still not an error: None, a non-string and an empty name all
|
|
809
|
+
return ``[]``, because those are answerable questions whose answer is "no".
|
|
810
|
+
"""
|
|
811
|
+
if name is None:
|
|
826
812
|
return []
|
|
813
|
+
if not isinstance(name, str):
|
|
814
|
+
name = str(name)
|
|
815
|
+
key = name.strip().lower()
|
|
816
|
+
if not key:
|
|
817
|
+
return []
|
|
818
|
+
|
|
819
|
+
conn = _get_conn()
|
|
820
|
+
try:
|
|
821
|
+
rows = conn.execute(
|
|
822
|
+
"SELECT DISTINCT project_dir FROM ("
|
|
823
|
+
"SELECT project_dir FROM context_facts "
|
|
824
|
+
"UNION SELECT project_dir FROM procedural_memory "
|
|
825
|
+
"UNION SELECT project_dir FROM session_summaries "
|
|
826
|
+
"UNION SELECT project_dir FROM temporal_events "
|
|
827
|
+
"UNION SELECT project_dir FROM failed_approaches"
|
|
828
|
+
") WHERE project_dir IS NOT NULL"
|
|
829
|
+
).fetchall()
|
|
830
|
+
finally:
|
|
831
|
+
conn.close()
|
|
832
|
+
|
|
833
|
+
matches = set()
|
|
834
|
+
for row in rows:
|
|
835
|
+
pdir = row[0]
|
|
836
|
+
if not pdir:
|
|
837
|
+
continue
|
|
838
|
+
if PureWindowsPath(pdir).name.lower() == key:
|
|
839
|
+
matches.add(pdir)
|
|
840
|
+
|
|
841
|
+
# Collapse spellings that denote the SAME directory (drive-letter case,
|
|
842
|
+
# separator style) before counting — otherwise one stray "c:\..." row
|
|
843
|
+
# turns a perfectly well-known project into an ambiguous one. Distinct
|
|
844
|
+
# directories that merely share a basename remain separate entries.
|
|
845
|
+
groups: dict = {}
|
|
846
|
+
for match in matches:
|
|
847
|
+
groups.setdefault(_project_dir_key(match), []).append(match)
|
|
848
|
+
|
|
849
|
+
# Deterministic representative per group: sorted() puts the upper-case
|
|
850
|
+
# drive first, and canonicalizing makes the result independent of which
|
|
851
|
+
# spellings happen to be in the store.
|
|
852
|
+
return sorted(
|
|
853
|
+
canonical_project_dir(sorted(spellings)[0])
|
|
854
|
+
for spellings in groups.values()
|
|
855
|
+
)
|
|
827
856
|
|
|
828
857
|
|
|
829
858
|
def resolve_project_dir_by_name(name: str) -> str | None:
|
|
@@ -833,8 +862,24 @@ def resolve_project_dir_by_name(name: str) -> str | None:
|
|
|
833
862
|
no sensible behavior for a collision. Callers that must distinguish "never
|
|
834
863
|
seen" from "shares a basename with another project" — and act differently —
|
|
835
864
|
should use :func:`find_project_dirs_by_name` and inspect the length.
|
|
865
|
+
|
|
866
|
+
FAIL-OPEN, and this is where that decision now lives rather than inside the
|
|
867
|
+
lookup. Every caller of this function already treats ``None`` as "no usable
|
|
868
|
+
path", so an unreachable store degrading to ``None`` changes nothing for
|
|
869
|
+
them, and the alternative — raising into paths that only wanted a directory
|
|
870
|
+
name — would break working features to report a condition they cannot act
|
|
871
|
+
on. Only the exception CLASS is printed; the message is attacker-influenced
|
|
872
|
+
free text and is never echoed.
|
|
836
873
|
"""
|
|
837
|
-
|
|
874
|
+
try:
|
|
875
|
+
matches = find_project_dirs_by_name(name)
|
|
876
|
+
except Exception as exc: # noqa: BLE001 -- see FAIL-OPEN above
|
|
877
|
+
print(
|
|
878
|
+
f"[memstack-memory] project lookup failed ({type(exc).__name__}); "
|
|
879
|
+
f"treating the project as unknown",
|
|
880
|
+
file=sys.stderr,
|
|
881
|
+
)
|
|
882
|
+
return None
|
|
838
883
|
return matches[0] if len(matches) == 1 else None
|
|
839
884
|
|
|
840
885
|
|