sidegraph 0.1.0__py3-none-any.whl

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.
sidegraph/config.py ADDED
@@ -0,0 +1,148 @@
1
+ """Shared store-path resolution (design §5: Configuration & wiring).
2
+
3
+ The ONE place ``cli.py``, ``server.py``, and ``host/hooks.py`` resolve a bare
4
+ ``Store(...)`` path from, so all three agree on env-var precedence and the
5
+ ``SIDEGRAPH_DB`` legacy-dispatch rule. Deliberately neutral and dependency-free
6
+ (stdlib only) so every caller can import it cheaply: ``host/hooks.py`` needs it without
7
+ dragging in ``server.py``'s FastMCP app, and ``server.py`` needs it without dragging in
8
+ ``cli.py``'s argparse machinery.
9
+
10
+ Precedence (an explicit path wins outright — nothing below it is even consulted)::
11
+
12
+ explicit arg > $SIDEGRAPH_DIR > $SIDEGRAPH_DB (deprecated) > existing ``.sidegraph/``
13
+ > default ``.sidegraph`` (first-creation warning)
14
+
15
+ See ``docs/reference/configuration.md`` for the full precedence and path-resolution rules.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import os
21
+ import sys
22
+ from pathlib import Path
23
+
24
+ #: The canonical, directory-based store convention (design §1) — the primary default.
25
+ DEFAULT_STORE_DIR = ".sidegraph"
26
+
27
+ # Emitted at most once per PROCESS (module-level flag, not per-call): a long-lived server
28
+ # or a hook that re-resolves the path on every request must not spam stderr repeatedly.
29
+ # Tests reset this via ``monkeypatch.setattr(config, "_deprecation_warned", False)``.
30
+ _deprecation_warned = False
31
+
32
+
33
+ def _dispatch_sidegraph_db(value: str, anchor) -> str:
34
+ """Apply the ``SIDEGRAPH_DB`` legacy-dispatch rule (pinned from review):
35
+
36
+ - an existing legacy ``*.db`` FILE -> pass through unchanged; ``Store`` migrates it
37
+ on open (see ``Store._migrate_legacy``).
38
+ - an existing DIRECTORY (canonical layout or not — ``Store`` already knows how to
39
+ open or populate either shape) -> use it directly, never its parent.
40
+ - a NONEXISTENT path that looks like a file sitting inside a directory (e.g.
41
+ ``.sidegraph/decisions.db``) -> use its PARENT directory instead. This rescues
42
+ every old config snippet in the wild (``SIDEGRAPH_DB=.sidegraph/decisions.db``) by
43
+ pointing ``Store`` at the ``.sidegraph`` directory, rather than creating a fresh
44
+ canonical layout literally named ``decisions.db``.
45
+ - a NONEXISTENT BARE filename with no directory component at all (e.g. the historic
46
+ bare-file default ``SIDEGRAPH_DB=sidegraph.db``) -> fall through to
47
+ ``DEFAULT_STORE_DIR`` instead. "Its parent" for a bare filename is the CURRENT
48
+ DIRECTORY itself (or ``$CLAUDE_PROJECT_DIR`` under a host anchor) — rescuing to
49
+ that would make the entire root the store: canonical layout, ``.gitignore``, and
50
+ the root tmp-sweep (see ``Store._sweep_stale_tmp_files``) all operating on
51
+ whatever else happens to live there. Checked against the RAW (pre-anchor) value:
52
+ anchoring a bare filename still doesn't give it a real "directory it lives in" to
53
+ rescue to, it just moves the ambiguity from cwd onto the anchor root instead.
54
+
55
+ ``anchor`` is the same relative-path anchoring callable ``resolve_store_path`` builds
56
+ (identity when no ``root`` was given) — reused both to resolve ``value`` against the
57
+ filesystem and, in the bare-filename case, to anchor the ``DEFAULT_STORE_DIR`` fallback
58
+ consistently.
59
+ """
60
+ anchored = anchor(value)
61
+ path = Path(anchored)
62
+ if path.is_file() or path.is_dir():
63
+ return anchored
64
+ if Path(value).parent == Path("."):
65
+ return anchor(DEFAULT_STORE_DIR)
66
+ return str(path.parent)
67
+
68
+
69
+ def _warn_deprecated_sidegraph_db(value: str) -> None:
70
+ global _deprecation_warned
71
+ if _deprecation_warned:
72
+ return
73
+ _deprecation_warned = True
74
+ print(
75
+ f"sidegraph: $SIDEGRAPH_DB is deprecated (was {value!r}); set $SIDEGRAPH_DIR to "
76
+ "the store directory instead",
77
+ file=sys.stderr,
78
+ )
79
+
80
+
81
+ # The bridge between processes (D2): the PreToolUse hook knows the host's session id, the
82
+ # MCP server does not. SessionStart publishes it here — index-only key/value, the same
83
+ # mechanism host/hooks.py's `_PRETOOL_NUDGE_KEY_PREFIX` uses. The value carries a write
84
+ # timestamp because `meta` never expires on its own (this repo's live store holds 34 stale
85
+ # nudge keys), and a key that outlived its session would attribute every later CLI or pytest
86
+ # retrieval to it. Defined here, not in host/hooks.py, so server.py (core) can read it
87
+ # without importing the host seam — host/hooks.py re-exports it for its own callers/tests.
88
+ TELEMETRY_SESSION_KEY = "telemetry:session"
89
+
90
+
91
+ def telemetry_enabled() -> bool:
92
+ """True unless ``SIDEGRAPH_TELEMETRY=off``. Opt-out, read at point of use and never
93
+ cached at import — one definition, two consumers (the MCP server and the PreToolUse
94
+ hook), because two copies of an env check drift."""
95
+ return os.environ.get("SIDEGRAPH_TELEMETRY", "on").strip().lower() != "off"
96
+
97
+
98
+ def resolve_store_path(
99
+ explicit: str | None = None,
100
+ *,
101
+ root: str | None = None,
102
+ warn_on_create: bool = True,
103
+ ) -> str:
104
+ """Resolve the store path a caller should pass to ``Store(...)``.
105
+
106
+ ``explicit`` is an already-parsed ``--db`` CLI flag (``None`` if the flag was
107
+ omitted). When given, it wins outright — no env var or default is even consulted,
108
+ matching every existing CLI's convention.
109
+
110
+ ``root`` anchors a RELATIVE result to a base directory — plugin hooks may run with an
111
+ arbitrary cwd (see ``host.hooks._env_path``); ``None`` (the default) leaves relative
112
+ results as-is, resolved against cwd like every non-host caller.
113
+
114
+ ``warn_on_create`` gates the "creating new store" stderr notice emitted when nothing
115
+ resolved and the default ``.sidegraph`` doesn't exist yet — ``sidegraph-init`` passes
116
+ ``False`` since it already announces the same fact on stdout.
117
+ """
118
+
119
+ def _anchor(value: str) -> str:
120
+ if root and not os.path.isabs(value):
121
+ return os.path.join(root, value)
122
+ return value
123
+
124
+ if explicit is not None:
125
+ return _anchor(explicit)
126
+
127
+ dir_env = os.environ.get("SIDEGRAPH_DIR")
128
+ if dir_env:
129
+ return _anchor(dir_env)
130
+
131
+ db_env = os.environ.get("SIDEGRAPH_DB")
132
+ if db_env:
133
+ # SIDEGRAPH_DIR unset (or empty) is what makes SIDEGRAPH_DB "actually used" —
134
+ # the deprecation note fires here, never when SIDEGRAPH_DIR already won above.
135
+ _warn_deprecated_sidegraph_db(db_env)
136
+ return _dispatch_sidegraph_db(db_env, _anchor)
137
+
138
+ default_path = _anchor(DEFAULT_STORE_DIR)
139
+ if Path(default_path).exists():
140
+ return default_path
141
+
142
+ if warn_on_create:
143
+ print(
144
+ f"warning: creating new store at {default_path}; pass --db or set "
145
+ "SIDEGRAPH_DIR if this is not the store you meant",
146
+ file=sys.stderr,
147
+ )
148
+ return default_path