synap-git 0.2.0__tar.gz → 1.1.0__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.
- {synap_git-0.2.0 → synap_git-1.1.0}/CHANGELOG.md +40 -3
- {synap_git-0.2.0 → synap_git-1.1.0}/PKG-INFO +20 -6
- {synap_git-0.2.0 → synap_git-1.1.0}/README.md +15 -4
- {synap_git-0.2.0 → synap_git-1.1.0}/pyproject.toml +10 -4
- {synap_git-0.2.0 → synap_git-1.1.0}/src/synap_git/__init__.py +1 -1
- {synap_git-0.2.0 → synap_git-1.1.0}/src/synap_git/api/app.py +40 -18
- {synap_git-0.2.0 → synap_git-1.1.0}/src/synap_git/api/static/index.html +33 -23
- synap_git-1.1.0/src/synap_git/cli/main.py +1930 -0
- {synap_git-0.2.0 → synap_git-1.1.0}/src/synap_git/config.py +11 -0
- {synap_git-0.2.0 → synap_git-1.1.0}/src/synap_git/diagnostics/logger.py +24 -4
- synap_git-1.1.0/src/synap_git/indexer/daemon.py +343 -0
- synap_git-1.1.0/src/synap_git/indexer/engine.py +677 -0
- synap_git-1.1.0/src/synap_git/indexer/wiki.py +188 -0
- {synap_git-0.2.0 → synap_git-1.1.0}/src/synap_git/parser/registry.py +38 -1
- {synap_git-0.2.0 → synap_git-1.1.0}/src/synap_git/provider/anthropic.py +48 -0
- {synap_git-0.2.0 → synap_git-1.1.0}/src/synap_git/provider/base.py +14 -0
- {synap_git-0.2.0 → synap_git-1.1.0}/src/synap_git/provider/factory.py +19 -6
- {synap_git-0.2.0 → synap_git-1.1.0}/src/synap_git/provider/gemini.py +15 -0
- {synap_git-0.2.0 → synap_git-1.1.0}/src/synap_git/provider/mock.py +15 -0
- {synap_git-0.2.0 → synap_git-1.1.0}/src/synap_git/provider/ollama.py +42 -0
- {synap_git-0.2.0 → synap_git-1.1.0}/src/synap_git/provider/openai.py +49 -0
- synap_git-1.1.0/src/synap_git/provider/openrouter.py +121 -0
- {synap_git-0.2.0 → synap_git-1.1.0}/src/synap_git/retrieval/engine.py +42 -17
- {synap_git-0.2.0 → synap_git-1.1.0}/src/synap_git/retrieval/memory.py +1 -1
- {synap_git-0.2.0 → synap_git-1.1.0}/src/synap_git/storage/sqlite.py +299 -163
- synap_git-0.2.0/src/synap_git/cli/main.py +0 -1174
- synap_git-0.2.0/src/synap_git/indexer/daemon.py +0 -143
- synap_git-0.2.0/src/synap_git/indexer/engine.py +0 -327
- synap_git-0.2.0/src/synap_git/indexer/wiki.py +0 -78
- {synap_git-0.2.0 → synap_git-1.1.0}/.gitignore +0 -0
- {synap_git-0.2.0 → synap_git-1.1.0}/.synap.example/README.md +0 -0
- {synap_git-0.2.0 → synap_git-1.1.0}/LICENSE.md +0 -0
- {synap_git-0.2.0 → synap_git-1.1.0}/src/synap_git/api/__init__.py +0 -0
- {synap_git-0.2.0 → synap_git-1.1.0}/src/synap_git/cli/__init__.py +0 -0
- {synap_git-0.2.0 → synap_git-1.1.0}/src/synap_git/cli/__main__.py +0 -0
- {synap_git-0.2.0 → synap_git-1.1.0}/src/synap_git/diagnostics/__init__.py +0 -0
- {synap_git-0.2.0 → synap_git-1.1.0}/src/synap_git/diagnostics/tracing.py +0 -0
- {synap_git-0.2.0 → synap_git-1.1.0}/src/synap_git/embeddings/__init__.py +0 -0
- {synap_git-0.2.0 → synap_git-1.1.0}/src/synap_git/git/__init__.py +0 -0
- {synap_git-0.2.0 → synap_git-1.1.0}/src/synap_git/git/state.py +0 -0
- {synap_git-0.2.0 → synap_git-1.1.0}/src/synap_git/indexer/__init__.py +0 -0
- {synap_git-0.2.0 → synap_git-1.1.0}/src/synap_git/indexer/scanner.py +0 -0
- {synap_git-0.2.0 → synap_git-1.1.0}/src/synap_git/mcp/__init__.py +0 -0
- {synap_git-0.2.0 → synap_git-1.1.0}/src/synap_git/mcp/server.py +0 -0
- {synap_git-0.2.0 → synap_git-1.1.0}/src/synap_git/parser/__init__.py +0 -0
- {synap_git-0.2.0 → synap_git-1.1.0}/src/synap_git/py.typed +0 -0
- {synap_git-0.2.0 → synap_git-1.1.0}/src/synap_git/retrieval/__init__.py +0 -0
- {synap_git-0.2.0 → synap_git-1.1.0}/src/synap_git/storage/__init__.py +0 -0
- {synap_git-0.2.0 → synap_git-1.1.0}/src/synap_git/utils/__init__.py +0 -0
- {synap_git-0.2.0 → synap_git-1.1.0}/src/synap_git/utils/serialization.py +0 -0
|
@@ -5,14 +5,51 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.1.0] - 2026-05-28
|
|
9
|
+
|
|
10
|
+
### Added — Git-Snapshot Projection & Performance Refactoring
|
|
11
|
+
- **Split Two-Path Indexing:** Separated initialization and incremental indexing into `_first_run_index` (full scan, CPU-parallelized) and `_incremental_index` (Git delta change detector).
|
|
12
|
+
- **Asynchronous Wiki Generation Queue:** Decoupled slow, non-deterministic LLM wiki generation from structural indexing using a persistent database queue (`wiki_queue`) processed asynchronously by a daemon worker.
|
|
13
|
+
- **Lazy Wiki Caching:** Added synchronous wiki generation fallback to CLI (`wiki show`), Web API, and MCP tools to dynamically build missing or stale pages on-demand.
|
|
14
|
+
- **Process Pool Parallel Parsing:** Parallelized Tree-sitter parsing on first run across all CPU cores utilizing process-based concurrency with independent parser instances.
|
|
15
|
+
- **SQLite Performance Hardening:**
|
|
16
|
+
- WAL mode and NORMAL synchronous configuration enabled during writes.
|
|
17
|
+
- Multi-row symbol and edge inserts batched into a single transaction via `executemany`.
|
|
18
|
+
- Dot-separated `module_key` pre-computation and indexing for $O(1)$ module resolution.
|
|
19
|
+
- SQLite FTS5 index integration for fast sub-millisecond symbol searches, avoiding full-table scans.
|
|
20
|
+
- **Web API Lazy Refreshes:** Updated the `/wiki/{filepath}` GET endpoint to perform lazy refreshes on stale or missing pages before returning content.
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
- **FTS5 Cascade Delete:** Added database trigger `tgr_symbols_delete` to automatically clean up virtual `symbols_fts` entries when parent symbols are deleted.
|
|
24
|
+
- **Duplicate File ID Collision:** Handled unique, path-scoped file identifier generation ensuring files with identical content (like empty `__init__.py`) do not conflict.
|
|
25
|
+
|
|
26
|
+
## [0.2.1] - 2026-05-27
|
|
27
|
+
|
|
28
|
+
### Added — Final Production Hardening & Release Execution
|
|
29
|
+
- `synap rollback --commit <ref>` option: directly target a commit by hash/reference without interactive selection prompt.
|
|
30
|
+
- `synap rollback --yes` / `-y` option: suppress confirmation prompt for non-interactive and scripted rollback flows.
|
|
31
|
+
- Non-interactive guard in `synap rollback`: fails fast with a clear error when used in piped/CI contexts without `--commit` or `--yes`.
|
|
32
|
+
- `synap rollback` invalid commit detection: validates commit reference via `git rev-parse --verify` and rejects unknown refs with a clear message.
|
|
33
|
+
|
|
34
|
+
### Fixed
|
|
35
|
+
- **SQLite migration short-circuit bug**: legacy un-versioned databases (user_version = 0) incorrectly skipped `CREATE TABLE IF NOT EXISTS` execution, leaving the `symbols` table and others uninitialized. The premature short-circuit is removed; all schema tables are now created correctly before bumping to version 1.
|
|
36
|
+
- **Python `import_from_statement` missing symbol extraction**: Tree-sitter AST parser only extracted the module identifier from `from X import Y` statements, discarding `Y`. Now correctly emits `module:symbol` pairs for all imported names, aliases, and grouped imports.
|
|
37
|
+
- **Namespace-aware call edge resolution**: Pass 2 import resolver now splits `module:symbol` import entries to narrow edge targets to the correct module file, eliminating false-positive dependency edges to duplicate class names in sibling namespaces.
|
|
38
|
+
- **FastAPI app version hardcoded**: `create_app()` used a hardcoded version string `"0.2.0"` instead of the canonical `__version__`. Now dynamically imported from `synap_git.__init__`.
|
|
39
|
+
- **Streaming generator cancellation safety**: confirmed `httpx` stream connections are closed cleanly on partial consumption (no socket leaks).
|
|
40
|
+
- **Degraded mode retry logic**: confirmed 2-stage exponential backoff and graceful structural fallback under fully-offline and timeout conditions.
|
|
41
|
+
|
|
42
|
+
### Changed
|
|
43
|
+
- Daemon resilience test (`test_daemon_resilience.py`) hardened against race condition where `SIGKILL` test read a stale PID from a prior run that had already exited.
|
|
44
|
+
|
|
8
45
|
## [0.2.0] - 2026-05-26
|
|
9
46
|
|
|
10
47
|
### Added — Final Polish & Release Readiness
|
|
11
|
-
- CLI
|
|
48
|
+
- CLI usage management: `synap usage show` (displays Rich aggregated usage table and summary panel) and `synap usage clear`.
|
|
12
49
|
- CLI wiki management: `synap wiki list` and `synap wiki show <filepath>` (renders page in terminal via Rich Markdown).
|
|
13
|
-
- LLM call database logging: records `prompt_tokens`, `completion_tokens
|
|
50
|
+
- LLM call database logging: records `prompt_tokens`, `completion_tokens` for retrieval and wiki generation passes.
|
|
14
51
|
- Real-time daemon state: heartbeats integrated into `synap status`, `synap doctor`, and the Web UI status endpoints.
|
|
15
|
-
- Premium Web UI dashboard polish: dual L3 memory (Approved vs Pending) view, real-time LLM
|
|
52
|
+
- Premium Web UI dashboard polish: dual L3 memory (Approved vs Pending) view, real-time LLM usage analytics, and active daemon PID badge.
|
|
16
53
|
- Defensive GHA release pipeline: `.github/workflows/release.yml` automates TestPyPI and PyPI publishing, tag alignment checking, and draft release generation.
|
|
17
54
|
- Clean Typer execution wrapper: intercepts configuration and credential exceptions to output actionable suggestions (e.g. `synap setup`) instead of tracebacks.
|
|
18
55
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: synap-git
|
|
3
|
-
Version:
|
|
3
|
+
Version: 1.1.0
|
|
4
4
|
Summary: Persistent structural context infrastructure for AI coding agents.
|
|
5
5
|
Project-URL: Homepage, https://github.com/saahilpal/synap-git
|
|
6
6
|
Project-URL: Repository, https://github.com/saahilpal/synap-git
|
|
@@ -22,12 +22,15 @@ Requires-Python: >=3.11
|
|
|
22
22
|
Requires-Dist: click==8.1.8
|
|
23
23
|
Requires-Dist: fastapi==0.115.11
|
|
24
24
|
Requires-Dist: gitpython==3.1.44
|
|
25
|
+
Requires-Dist: httpx==0.28.1
|
|
25
26
|
Requires-Dist: keyring==25.6.0
|
|
26
27
|
Requires-Dist: markdown-it-py==3.0.0
|
|
27
28
|
Requires-Dist: mcp==1.3.0
|
|
28
29
|
Requires-Dist: msgpack==1.1.0
|
|
30
|
+
Requires-Dist: prompt-toolkit<3.0.44
|
|
29
31
|
Requires-Dist: pydantic-settings==2.8.1
|
|
30
32
|
Requires-Dist: pydantic==2.10.6
|
|
33
|
+
Requires-Dist: questionary==2.1.0
|
|
31
34
|
Requires-Dist: structlog==25.1.0
|
|
32
35
|
Requires-Dist: tiktoken==0.9.0
|
|
33
36
|
Requires-Dist: tree-sitter-languages==1.10.2
|
|
@@ -36,11 +39,11 @@ Requires-Dist: typer==0.15.1
|
|
|
36
39
|
Requires-Dist: uvicorn==0.34.0
|
|
37
40
|
Provides-Extra: dev
|
|
38
41
|
Requires-Dist: bandit==1.8.3; extra == 'dev'
|
|
39
|
-
Requires-Dist: httpx==0.28.1; extra == 'dev'
|
|
40
42
|
Requires-Dist: mypy==1.15.0; extra == 'dev'
|
|
41
43
|
Requires-Dist: pre-commit==4.1.0; extra == 'dev'
|
|
42
44
|
Requires-Dist: pytest-asyncio==0.25.3; extra == 'dev'
|
|
43
45
|
Requires-Dist: pytest-cov==6.0.0; extra == 'dev'
|
|
46
|
+
Requires-Dist: pytest-timeout==2.3.1; extra == 'dev'
|
|
44
47
|
Requires-Dist: pytest==8.3.4; extra == 'dev'
|
|
45
48
|
Requires-Dist: ruff==0.9.9; extra == 'dev'
|
|
46
49
|
Description-Content-Type: text/markdown
|
|
@@ -71,6 +74,16 @@ Synap is **NOT a RAG system.** It is a strict, deterministic background daemon t
|
|
|
71
74
|
|
|
72
75
|
---
|
|
73
76
|
|
|
77
|
+
## ⚡ Performance Architecture (v1.1.0)
|
|
78
|
+
|
|
79
|
+
Synap is built on the **Git-Snapshot Paradigm** to guarantee sub-100ms response times for everyday agent workflows:
|
|
80
|
+
- **Two separate code paths:** First-run indexing uses process-pool parallel parsing (`ProcessPoolExecutor`) to build the initial codebase structure, while subsequent indexing uses a Git delta change detector (`git diff-tree`) to process only changed files. (Reason: Avoids full filesystem scans on every run).
|
|
81
|
+
- **No filesystem scan / file hashing on incremental runs:** Synap uses Git blob OIDs to detect changed files. (Reason: Git already hashes everything, making filesystem reads redundant).
|
|
82
|
+
- **Decoupled non-deterministic LLM pipeline:** Structural parsing (deterministic, fast) completes immediately and enqueues wiki generation (non-deterministic, slow) to a background worker, while lazy caching fallback handles CLI/API wiki requests. (Reason: Never block structural indexing or CLI commands on LLM API response latency).
|
|
83
|
+
- **SQLite optimizations:** SQLite writes are grouped into a single transaction per batch (Reason: Avoids disk sync overhead per row). SQLite FTS5 index matches symbols in sub-milliseconds (Reason: Eliminates slow wildcard `LIKE` table scans). Pre-computed dot-separated `module_key` columns allow O(1) module resolution (Reason: Prevents suffix-matching scans).
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
74
87
|
## 🏗️ High-Level Architecture (HLD)
|
|
75
88
|
|
|
76
89
|
Synap bridges the gap between your local file system, Git history, and the LLM via a 3-layer indexing strategy.
|
|
@@ -180,9 +193,9 @@ sequenceDiagram
|
|
|
180
193
|
### 1. Installation
|
|
181
194
|
Install the Synap CLI via `pip` or `uv`:
|
|
182
195
|
```bash
|
|
183
|
-
pip install
|
|
196
|
+
pip install synap-git
|
|
184
197
|
# or using uv:
|
|
185
|
-
uv tool install
|
|
198
|
+
uv tool install synap-git
|
|
186
199
|
```
|
|
187
200
|
|
|
188
201
|
### 2. Setup & Initialize
|
|
@@ -234,8 +247,8 @@ Synap uses a powerful, strict CLI interface. Every destructive action prompts fo
|
|
|
234
247
|
### Developer Tools
|
|
235
248
|
- `synap wiki list .` : List all generated wiki documentation files.
|
|
236
249
|
- `synap wiki show <filepath> .` : Render a specific wiki markdown page to the console.
|
|
237
|
-
- `synap
|
|
238
|
-
- `synap
|
|
250
|
+
- `synap usage show .` : Display detailed aggregated LLM token usage.
|
|
251
|
+
- `synap usage clear .` : Purge all LLM call history.
|
|
239
252
|
- `synap doctor .` : Validate SQLite integrity, Tree-sitter, tokenizers, LLM providers, and daemon heartbeat.
|
|
240
253
|
- `synap mcp verify .` : Verify MCP protocol, tool schemas, and contract stability.
|
|
241
254
|
|
|
@@ -249,3 +262,4 @@ Synap is built on the philosophy that AI tools must be transparent and controlla
|
|
|
249
262
|
- Ensure all states are stored exclusively in `synap.db` or `.synap/wiki/`.
|
|
250
263
|
|
|
251
264
|
License: [Apache 2.0](LICENSE.md)
|
|
265
|
+
](LICENSE.md)
|
|
@@ -24,6 +24,16 @@ Synap is **NOT a RAG system.** It is a strict, deterministic background daemon t
|
|
|
24
24
|
|
|
25
25
|
---
|
|
26
26
|
|
|
27
|
+
## ⚡ Performance Architecture (v1.1.0)
|
|
28
|
+
|
|
29
|
+
Synap is built on the **Git-Snapshot Paradigm** to guarantee sub-100ms response times for everyday agent workflows:
|
|
30
|
+
- **Two separate code paths:** First-run indexing uses process-pool parallel parsing (`ProcessPoolExecutor`) to build the initial codebase structure, while subsequent indexing uses a Git delta change detector (`git diff-tree`) to process only changed files. (Reason: Avoids full filesystem scans on every run).
|
|
31
|
+
- **No filesystem scan / file hashing on incremental runs:** Synap uses Git blob OIDs to detect changed files. (Reason: Git already hashes everything, making filesystem reads redundant).
|
|
32
|
+
- **Decoupled non-deterministic LLM pipeline:** Structural parsing (deterministic, fast) completes immediately and enqueues wiki generation (non-deterministic, slow) to a background worker, while lazy caching fallback handles CLI/API wiki requests. (Reason: Never block structural indexing or CLI commands on LLM API response latency).
|
|
33
|
+
- **SQLite optimizations:** SQLite writes are grouped into a single transaction per batch (Reason: Avoids disk sync overhead per row). SQLite FTS5 index matches symbols in sub-milliseconds (Reason: Eliminates slow wildcard `LIKE` table scans). Pre-computed dot-separated `module_key` columns allow O(1) module resolution (Reason: Prevents suffix-matching scans).
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
27
37
|
## 🏗️ High-Level Architecture (HLD)
|
|
28
38
|
|
|
29
39
|
Synap bridges the gap between your local file system, Git history, and the LLM via a 3-layer indexing strategy.
|
|
@@ -133,9 +143,9 @@ sequenceDiagram
|
|
|
133
143
|
### 1. Installation
|
|
134
144
|
Install the Synap CLI via `pip` or `uv`:
|
|
135
145
|
```bash
|
|
136
|
-
pip install
|
|
146
|
+
pip install synap-git
|
|
137
147
|
# or using uv:
|
|
138
|
-
uv tool install
|
|
148
|
+
uv tool install synap-git
|
|
139
149
|
```
|
|
140
150
|
|
|
141
151
|
### 2. Setup & Initialize
|
|
@@ -187,8 +197,8 @@ Synap uses a powerful, strict CLI interface. Every destructive action prompts fo
|
|
|
187
197
|
### Developer Tools
|
|
188
198
|
- `synap wiki list .` : List all generated wiki documentation files.
|
|
189
199
|
- `synap wiki show <filepath> .` : Render a specific wiki markdown page to the console.
|
|
190
|
-
- `synap
|
|
191
|
-
- `synap
|
|
200
|
+
- `synap usage show .` : Display detailed aggregated LLM token usage.
|
|
201
|
+
- `synap usage clear .` : Purge all LLM call history.
|
|
192
202
|
- `synap doctor .` : Validate SQLite integrity, Tree-sitter, tokenizers, LLM providers, and daemon heartbeat.
|
|
193
203
|
- `synap mcp verify .` : Verify MCP protocol, tool schemas, and contract stability.
|
|
194
204
|
|
|
@@ -202,3 +212,4 @@ Synap is built on the philosophy that AI tools must be transparent and controlla
|
|
|
202
212
|
- Ensure all states are stored exclusively in `synap.db` or `.synap/wiki/`.
|
|
203
213
|
|
|
204
214
|
License: [Apache 2.0](LICENSE.md)
|
|
215
|
+
](LICENSE.md)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "synap-git"
|
|
3
|
-
|
|
3
|
+
dynamic = ["version"]
|
|
4
4
|
description = "Persistent structural context infrastructure for AI coding agents."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.11"
|
|
@@ -22,12 +22,15 @@ dependencies = [
|
|
|
22
22
|
"click==8.1.8",
|
|
23
23
|
"fastapi==0.115.11",
|
|
24
24
|
"gitpython==3.1.44",
|
|
25
|
+
"httpx==0.28.1",
|
|
25
26
|
"keyring==25.6.0",
|
|
26
27
|
"markdown-it-py==3.0.0",
|
|
27
28
|
"mcp==1.3.0",
|
|
28
29
|
"msgpack==1.1.0",
|
|
30
|
+
"prompt-toolkit<3.0.44",
|
|
29
31
|
"pydantic==2.10.6",
|
|
30
32
|
"pydantic-settings==2.8.1",
|
|
33
|
+
"questionary==2.1.0",
|
|
31
34
|
"structlog==25.1.0",
|
|
32
35
|
"tiktoken==0.9.0",
|
|
33
36
|
"tree-sitter==0.20.4",
|
|
@@ -46,12 +49,12 @@ Changelog = "https://github.com/saahilpal/synap-git/blob/main/CHANGELOG.md"
|
|
|
46
49
|
[project.optional-dependencies]
|
|
47
50
|
dev = [
|
|
48
51
|
"bandit==1.8.3",
|
|
49
|
-
"httpx==0.28.1",
|
|
50
52
|
"mypy==1.15.0",
|
|
51
53
|
"pre-commit==4.1.0",
|
|
52
54
|
"pytest==8.3.4",
|
|
53
55
|
"pytest-asyncio==0.25.3",
|
|
54
56
|
"pytest-cov==6.0.0",
|
|
57
|
+
"pytest-timeout==2.3.1",
|
|
55
58
|
"ruff==0.9.9",
|
|
56
59
|
]
|
|
57
60
|
|
|
@@ -62,6 +65,9 @@ synap = "synap_git.cli:app"
|
|
|
62
65
|
requires = ["hatchling"]
|
|
63
66
|
build-backend = "hatchling.build"
|
|
64
67
|
|
|
68
|
+
[tool.hatch.version]
|
|
69
|
+
path = "src/synap_git/__init__.py"
|
|
70
|
+
|
|
65
71
|
[tool.hatch.build.targets.sdist]
|
|
66
72
|
include = [
|
|
67
73
|
"src/synap_git",
|
|
@@ -81,7 +87,7 @@ src = ["src", "tests"]
|
|
|
81
87
|
|
|
82
88
|
[tool.ruff.lint]
|
|
83
89
|
select = ["A", "B", "C4", "E", "F", "I", "N", "UP", "W", "S", "PT", "ARG", "PTH"]
|
|
84
|
-
ignore = ["B008", "E501", "B904", "B007", "F841", "W291", "S101", "S105", "S106", "ARG001", "ARG002", "S110", "S310"]
|
|
90
|
+
ignore = ["B008", "E501", "B904", "B007", "F841", "W291", "S101", "S105", "S106", "ARG001", "ARG002", "S110", "S310", "N806", "S603", "S607", "PTH123"]
|
|
85
91
|
|
|
86
92
|
[tool.ruff.lint.per-file-ignores]
|
|
87
93
|
"tests/*" = ["S603"]
|
|
@@ -104,7 +110,7 @@ module = ["msgpack"]
|
|
|
104
110
|
ignore_missing_imports = true
|
|
105
111
|
|
|
106
112
|
[tool.pytest.ini_options]
|
|
107
|
-
addopts = "-ra --strict-markers --strict-config"
|
|
113
|
+
addopts = "-ra --strict-markers --strict-config --timeout=60"
|
|
108
114
|
testpaths = ["tests"]
|
|
109
115
|
asyncio_mode = "auto"
|
|
110
116
|
asyncio_default_fixture_loop_scope = "function"
|
|
@@ -10,6 +10,7 @@ from fastapi.middleware.cors import CORSMiddleware
|
|
|
10
10
|
from fastapi.responses import HTMLResponse, StreamingResponse
|
|
11
11
|
from fastapi.staticfiles import StaticFiles
|
|
12
12
|
|
|
13
|
+
from synap_git import __version__
|
|
13
14
|
from synap_git.indexer.engine import SynapRuntime
|
|
14
15
|
|
|
15
16
|
|
|
@@ -17,7 +18,7 @@ def create_app(runtime: SynapRuntime) -> FastAPI:
|
|
|
17
18
|
app = FastAPI(
|
|
18
19
|
title="Synap Context Diagnostics",
|
|
19
20
|
description="Deterministic structural context infrastructure for AI coding agents.",
|
|
20
|
-
version=
|
|
21
|
+
version=__version__,
|
|
21
22
|
)
|
|
22
23
|
|
|
23
24
|
app.add_middleware(
|
|
@@ -36,17 +37,20 @@ def create_app(runtime: SynapRuntime) -> FastAPI:
|
|
|
36
37
|
@app.get("/", response_class=HTMLResponse)
|
|
37
38
|
async def get_index() -> str:
|
|
38
39
|
index_file = static_dir / "index.html"
|
|
39
|
-
|
|
40
|
-
|
|
40
|
+
exists = await asyncio.to_thread(index_file.exists)
|
|
41
|
+
if exists:
|
|
42
|
+
return await asyncio.to_thread(index_file.read_text, encoding="utf-8")
|
|
41
43
|
return "<h1>Synap Diagnostic UI is ready.</h1>"
|
|
42
44
|
|
|
43
45
|
@app.get("/api/v1/status")
|
|
44
46
|
async def get_status() -> dict[str, Any]:
|
|
45
47
|
try:
|
|
46
|
-
status = runtime.status
|
|
48
|
+
status = await asyncio.to_thread(runtime.status)
|
|
47
49
|
from synap_git.cli.main import _read_daemon_heartbeat
|
|
48
50
|
|
|
49
|
-
daemon_info =
|
|
51
|
+
daemon_info = await asyncio.to_thread(
|
|
52
|
+
_read_daemon_heartbeat, Path(status.repository_path)
|
|
53
|
+
)
|
|
50
54
|
return {
|
|
51
55
|
"repository_path": status.repository_path,
|
|
52
56
|
"branch": status.branch,
|
|
@@ -63,7 +67,9 @@ def create_app(runtime: SynapRuntime) -> FastAPI:
|
|
|
63
67
|
@app.get("/api/v1/trace/latest")
|
|
64
68
|
async def get_latest_trace() -> dict[str, Any]:
|
|
65
69
|
try:
|
|
66
|
-
|
|
70
|
+
if runtime.trace_store:
|
|
71
|
+
return await asyncio.to_thread(runtime.trace_store.get_latest)
|
|
72
|
+
return {}
|
|
67
73
|
except Exception as exc:
|
|
68
74
|
raise HTTPException(status_code=500, detail=str(exc))
|
|
69
75
|
|
|
@@ -73,7 +79,7 @@ def create_app(runtime: SynapRuntime) -> FastAPI:
|
|
|
73
79
|
|
|
74
80
|
async def event_generator() -> AsyncGenerator[str, None]:
|
|
75
81
|
while True:
|
|
76
|
-
status = runtime.status
|
|
82
|
+
status = await asyncio.to_thread(runtime.status)
|
|
77
83
|
yield f"data: {json.dumps(status.__dict__)}\n\n"
|
|
78
84
|
await asyncio.sleep(2)
|
|
79
85
|
|
|
@@ -81,31 +87,47 @@ def create_app(runtime: SynapRuntime) -> FastAPI:
|
|
|
81
87
|
|
|
82
88
|
@app.get("/wiki/{filepath:path}")
|
|
83
89
|
async def get_wiki_page(filepath: str) -> dict[str, Any]:
|
|
84
|
-
|
|
85
|
-
if
|
|
86
|
-
|
|
90
|
+
target = filepath
|
|
91
|
+
if target.endswith(".md"):
|
|
92
|
+
target = target[:-3]
|
|
93
|
+
try:
|
|
94
|
+
await asyncio.to_thread(runtime.wiki.ensure_wiki_page, target)
|
|
95
|
+
except Exception:
|
|
96
|
+
pass
|
|
97
|
+
|
|
98
|
+
wiki_path = runtime.wiki.wiki_dir / f"{target}.md"
|
|
99
|
+
exists = await asyncio.to_thread(wiki_path.exists)
|
|
100
|
+
if exists:
|
|
101
|
+
content = await asyncio.to_thread(wiki_path.read_text, encoding="utf-8")
|
|
102
|
+
return {"status": "ok", "content": content}
|
|
87
103
|
return {"status": "error", "message": "Wiki not found"}
|
|
88
104
|
|
|
89
105
|
@app.get("/api/v1/memory")
|
|
90
106
|
async def get_memory_page() -> dict[str, Any]:
|
|
91
|
-
approved = runtime.store.get_lessons
|
|
92
|
-
pending = runtime.store.get_lessons
|
|
107
|
+
approved = await asyncio.to_thread(runtime.store.get_lessons, "approved")
|
|
108
|
+
pending = await asyncio.to_thread(runtime.store.get_lessons, "pending")
|
|
93
109
|
return {"status": "ok", "approved": approved, "pending": pending}
|
|
94
110
|
|
|
95
|
-
|
|
96
|
-
async def get_cost_page() -> dict[str, Any]:
|
|
111
|
+
def _fetch_calls() -> list[dict[str, Any]]:
|
|
97
112
|
with runtime.store.connect() as conn:
|
|
98
113
|
rows = conn.execute("SELECT * FROM llm_calls ORDER BY created_at DESC").fetchall()
|
|
99
|
-
|
|
114
|
+
return [dict(r) for r in rows]
|
|
115
|
+
|
|
116
|
+
@app.get("/api/v1/usage")
|
|
117
|
+
async def get_usage_page() -> dict[str, Any]:
|
|
118
|
+
calls = await asyncio.to_thread(_fetch_calls)
|
|
100
119
|
return {"status": "ok", "calls": calls}
|
|
101
120
|
|
|
102
|
-
|
|
103
|
-
async def get_checkpoints_page() -> dict[str, Any]:
|
|
121
|
+
def _fetch_checkpoints() -> list[dict[str, Any]]:
|
|
104
122
|
with runtime.store.connect() as conn:
|
|
105
123
|
rows = conn.execute(
|
|
106
124
|
"SELECT * FROM checkpoints ORDER BY created_at DESC LIMIT 20"
|
|
107
125
|
).fetchall()
|
|
108
|
-
|
|
126
|
+
return [dict(r) for r in rows]
|
|
127
|
+
|
|
128
|
+
@app.get("/api/v1/checkpoints")
|
|
129
|
+
async def get_checkpoints_page() -> dict[str, Any]:
|
|
130
|
+
cps = await asyncio.to_thread(_fetch_checkpoints)
|
|
109
131
|
return {"status": "ok", "checkpoints": cps}
|
|
110
132
|
|
|
111
133
|
return app
|
|
@@ -202,15 +202,11 @@
|
|
|
202
202
|
</div>
|
|
203
203
|
</div>
|
|
204
204
|
|
|
205
|
-
<!-- LLM
|
|
205
|
+
<!-- LLM Usage -->
|
|
206
206
|
<div class="card" style="grid-column: span 2;">
|
|
207
|
-
<h2>LLM Call
|
|
207
|
+
<h2>LLM Call Usage</h2>
|
|
208
208
|
<div style="display: grid; grid-template-columns: 1fr 2fr; gap: 1.5rem;">
|
|
209
209
|
<div class="stats-grid" style="grid-template-columns: 1fr; gap: 0.75rem;">
|
|
210
|
-
<div class="stat-box">
|
|
211
|
-
<div class="stat-value" id="cost-total-usd">$0.000000</div>
|
|
212
|
-
<div class="stat-label">Total Cost (USD)</div>
|
|
213
|
-
</div>
|
|
214
210
|
<div class="stat-box">
|
|
215
211
|
<div class="stat-value" id="cost-total-calls">0</div>
|
|
216
212
|
<div class="stat-label">Total LLM Calls</div>
|
|
@@ -231,11 +227,10 @@
|
|
|
231
227
|
<th>Model</th>
|
|
232
228
|
<th>Purpose</th>
|
|
233
229
|
<th>Tokens</th>
|
|
234
|
-
<th>Cost</th>
|
|
235
230
|
</tr>
|
|
236
231
|
</thead>
|
|
237
232
|
<tbody id="cost-calls-body">
|
|
238
|
-
<tr><td colspan="
|
|
233
|
+
<tr><td colspan="5" class="empty-state">No calls recorded.</td></tr>
|
|
239
234
|
</tbody>
|
|
240
235
|
</table>
|
|
241
236
|
</div>
|
|
@@ -443,44 +438,59 @@
|
|
|
443
438
|
pendingBody.innerHTML = `<tr><td colspan="3" class="empty-state">No pending lessons needing approval.</td></tr>`;
|
|
444
439
|
}
|
|
445
440
|
|
|
446
|
-
// Fetch
|
|
447
|
-
const
|
|
448
|
-
const
|
|
441
|
+
// Fetch Usage
|
|
442
|
+
const usageRes = await fetch('/api/v1/usage');
|
|
443
|
+
const usageData = await usageRes.json();
|
|
449
444
|
const costBody = document.getElementById('cost-calls-body');
|
|
450
445
|
|
|
451
|
-
let totalCost = 0;
|
|
452
446
|
let totalCalls = 0;
|
|
453
447
|
let totalTokens = 0;
|
|
454
448
|
|
|
455
|
-
if (
|
|
456
|
-
totalCalls =
|
|
457
|
-
|
|
458
|
-
totalCost += c.cost_usd;
|
|
449
|
+
if (usageData.calls && usageData.calls.length > 0) {
|
|
450
|
+
totalCalls = usageData.calls.length;
|
|
451
|
+
usageData.calls.slice(0, 15).map(c => {
|
|
459
452
|
const tokens = c.input_tokens + c.output_tokens;
|
|
460
453
|
totalTokens += tokens;
|
|
461
454
|
|
|
462
455
|
const timeStr = new Date(c.created_at * 1000).toLocaleTimeString();
|
|
463
|
-
|
|
456
|
+
const row = `
|
|
464
457
|
<tr class="row-hover">
|
|
465
458
|
<td>${timeStr}</td>
|
|
466
459
|
<td>${c.provider}</td>
|
|
467
460
|
<td><code>${c.model}</code></td>
|
|
468
461
|
<td>${c.purpose}</td>
|
|
469
462
|
<td>${tokens.toLocaleString()}</td>
|
|
470
|
-
<td style="color:var(--success); font-family:var(--font-mono); font-size:11px;">$${c.cost_usd.toFixed(6)}</td>
|
|
471
463
|
</tr>
|
|
472
464
|
`;
|
|
473
|
-
|
|
465
|
+
costBody.insertAdjacentHTML('beforeend', row);
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
// Clear the loading message if it exists
|
|
469
|
+
if (usageData.calls.length > 0 && costBody.querySelector('.empty-state')) {
|
|
470
|
+
costBody.innerHTML = '';
|
|
471
|
+
// Re-render the 15 calls because we just cleared it
|
|
472
|
+
costBody.innerHTML = usageData.calls.slice(0, 15).map(c => {
|
|
473
|
+
const tokens = c.input_tokens + c.output_tokens;
|
|
474
|
+
const timeStr = new Date(c.created_at * 1000).toLocaleTimeString();
|
|
475
|
+
return `
|
|
476
|
+
<tr class="row-hover">
|
|
477
|
+
<td>${timeStr}</td>
|
|
478
|
+
<td>${c.provider}</td>
|
|
479
|
+
<td><code>${c.model}</code></td>
|
|
480
|
+
<td>${c.purpose}</td>
|
|
481
|
+
<td>${tokens.toLocaleString()}</td>
|
|
482
|
+
</tr>
|
|
483
|
+
`;
|
|
484
|
+
}).join('');
|
|
485
|
+
}
|
|
474
486
|
|
|
475
|
-
|
|
476
|
-
totalCost += c.cost_usd;
|
|
487
|
+
usageData.calls.slice(15).forEach(c => {
|
|
477
488
|
totalTokens += (c.input_tokens + c.output_tokens);
|
|
478
489
|
});
|
|
479
490
|
} else {
|
|
480
|
-
costBody.innerHTML = `<tr><td colspan="
|
|
491
|
+
costBody.innerHTML = `<tr><td colspan="5" class="empty-state">No LLM calls recorded yet.</td></tr>`;
|
|
481
492
|
}
|
|
482
493
|
|
|
483
|
-
document.getElementById('cost-total-usd').textContent = `$${totalCost.toFixed(6)}`;
|
|
484
494
|
document.getElementById('cost-total-calls').textContent = totalCalls.toLocaleString();
|
|
485
495
|
document.getElementById('cost-total-tokens').textContent = totalTokens.toLocaleString();
|
|
486
496
|
|